fix(ui): исправить фокус списка серверов и контраст общих элементов

This commit is contained in:
Denozordec committed 2026-10-15 14:15:56 +07:00
1 parent 9b0d7bb6e8
commit 3c9096804b
21 files changed
+6952 -58

No files matched your search

+10 -9
View File
@@ -7,6 +7,7 @@ import {
} from "@tanstack/react-table"
import type { Server, ServerType } from "@/lib/data"
import { StatusBadge } from "@/components/status-badge"
import { Badge, type BadgeProps } from "@/components/reui/badge"
import { Flag } from "@/components/flag"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
@@ -49,10 +50,10 @@ const TYPE_LABELS: Record<ServerType, string> = {
"home-router": "Home Router",
}
const TYPE_STYLES: Record<ServerType, string> = {
"jump-host": "bg-violet-500/10 text-violet-400 border-violet-500/20",
"exit-node": "bg-sky-500/10 text-sky-400 border-sky-500/20",
"home-router": "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
const TYPE_VARIANTS: Record<ServerType, BadgeProps["variant"]> = {
"jump-host": "primary-light",
"exit-node": "info-light",
"home-router": "success-light",
}
function rosVer(os: string): number {
@@ -63,13 +64,13 @@ function rosVer(os: string): number {
function TypeBadge({ type }: { type: ServerType }) {
const icon =
type === "jump-host" ? <ServerIcon className="size-3 mr-1" />
: type === "exit-node" ? <NetworkIcon className="size-3 mr-1" />
: <HomeIcon className="size-3 mr-1" />
type === "jump-host" ? <ServerIcon aria-hidden="true" />
: type === "exit-node" ? <NetworkIcon aria-hidden="true" />
: <HomeIcon aria-hidden="true" />
return (
<span className={cn("inline-flex items-center text-xs font-medium border rounded px-2 py-0.5", TYPE_STYLES[type])}>
<Badge variant={TYPE_VARIANTS[type]} className="text-foreground">
{icon}{TYPE_LABELS[type]}
</span>
</Badge>
)
}
+1 -1
View File
@@ -37,7 +37,7 @@ function SegmentedControl<T extends string>({
>
{option.label}
{option.count != null && (
<span className="text-xs tabular-nums opacity-60">{option.count}</span>
<span className="text-xs tabular-nums">{option.count}</span>
)}
</button>
))}
+34 -6
View File
@@ -35,6 +35,10 @@ import { LayersIcon, SearchIcon, ServerIcon } from "lucide-react"
/** Preview: https://reui.io/preview/base/list-9 · https://reui.io/docs/components/base/icon-tile · https://reui.io/docs/components/base/frame · https://reui.io/docs/components/base/badge */
export const ALL_SERVERS_ID = "all"
// Device URL changes can remount the page. Keep only the current keyboard intent,
// and never restore it after the user has focused another control.
let pendingKeyboardFocus: { path: string; id: string; source: Element | null; until: number } | null = null
export interface ServerTileItem {
id: string
name: string
@@ -185,7 +189,15 @@ function ServerTileRail({
useEffect(() => {
const viewport = listRef.current?.closest<HTMLElement>('[data-slot="scroll-area-viewport"]')
viewport?.scrollTo({ top: 0 })
listRef.current?.querySelector<HTMLElement>('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" })
const selected = listRef.current?.querySelector<HTMLElement>('[aria-selected="true"]')
selected?.scrollIntoView({ block: "nearest" })
const pending = pendingKeyboardFocus
if (pending && (pending.until < performance.now() || pending.path !== window.location.pathname)) {
pendingKeyboardFocus = null
} else if (pending && pending.id === resolvedId && selected?.getClientRects().length) {
if (document.activeElement === document.body || document.activeElement === pending.source) selected.focus({ preventScroll: true })
pendingKeyboardFocus = null
}
}, [pageIndex, resolvedId, q, pageSize])
const visibleIds = new Set(table.getRowModel().rows.map(row => row.id))
const visibleGroups = groups.map(group => ({ ...group, items: group.items.filter(item => visibleIds.has(item.id)) })).filter(group => group.items.length > 0)
@@ -200,6 +212,19 @@ function ServerTileRail({
return ids
}, [filtered, showAllTile])
const tabStopId = activeId && selectableIds.includes(activeId)
? activeId
: showAllTile ? ALL_SERVERS_ID : table.getRowModel().rows.find(row => isSelectable(row.original))?.id
function selectWithKeyboard(id: string) {
if (id === resolvedId) {
listRef.current?.querySelector<HTMLElement>('[aria-selected="true"]')?.focus()
return
}
pendingKeyboardFocus = { path: window.location.pathname, id, source: document.activeElement, until: performance.now() + 10000 }
onSelect(id)
}
function moveSelection(delta: number) {
if (selectableIds.length === 0) return
const idx = selectableIds.indexOf(resolvedId)
@@ -208,7 +233,7 @@ function ServerTileRail({
? delta > 0 ? 0 : selectableIds.length - 1
: Math.min(selectableIds.length - 1, Math.max(0, idx + delta))
const nextId = selectableIds[nextIdx]
if (nextId) onSelect(nextId)
if (nextId) selectWithKeyboard(nextId)
}
function handleListKeyDown(event: KeyboardEvent<HTMLDivElement>) {
@@ -225,13 +250,13 @@ function ServerTileRail({
}
if (event.key === "Home") {
event.preventDefault()
if (selectableIds[0]) onSelect(selectableIds[0])
if (selectableIds[0]) selectWithKeyboard(selectableIds[0])
return
}
if (event.key === "End") {
event.preventDefault()
const last = selectableIds[selectableIds.length - 1]
if (last) onSelect(last)
if (last) selectWithKeyboard(last)
}
}
@@ -283,8 +308,6 @@ function ServerTileRail({
className="flex flex-col gap-0.5 p-1.5"
role="listbox"
aria-label="Серверы"
aria-activedescendant={activeId ? `${railId}-server-tile-${activeId}` : undefined}
tabIndex={0}
onKeyDown={handleListKeyDown}
>
{showAllTile ? (
@@ -292,6 +315,7 @@ function ServerTileRail({
id={`${railId}-server-tile-${ALL_SERVERS_ID}`}
item={allItem}
selected={resolvedId === ALL_SERVERS_ID}
tabStop={tabStopId === ALL_SERVERS_ID}
onSelect={() => onSelect(ALL_SERVERS_ID)}
showCount={showCount}
showType={false}
@@ -315,6 +339,7 @@ function ServerTileRail({
id={`${railId}-server-tile-${item.id}`}
item={item}
selected={resolvedId === item.id}
tabStop={tabStopId === item.id}
onSelect={() => onSelect(item.id)}
showCount={showCount}
showType={showType}
@@ -354,6 +379,7 @@ function ServerTileButton({
id,
item,
selected,
tabStop,
onSelect,
showCount,
showType,
@@ -363,6 +389,7 @@ function ServerTileButton({
id: string
item: ServerTileItem
selected: boolean
tabStop: boolean
onSelect: () => void
showCount: boolean
showType: boolean
@@ -385,6 +412,7 @@ function ServerTileButton({
type="button"
role="option"
aria-selected={selected}
tabIndex={tabStop ? 0 : -1}
title={item.title ?? hostname}
disabled={!selectable}
onClick={onSelect}