Polish: A11y-, Motion- & Deep-Linking-Sweep (Web Interface Guidelines)

Aus dem web-design-guidelines-Audit über das ganze Frontend:

P1 — index.css: prefers-reduced-motion-Block (Animationen/Transitions aus),
color-scheme dark/light, zwei `transition: all` → explizite Properties,
globaler :focus-visible-Ring. Formular-aria-labels (SystemDrawer-Passwörter
inkl. name/autocomplete/spellCheck, Memory, Connect, ModelBrowse, LaneEditor,
CustomDialog). Icon-Button-aria-labels + dekorative Icons aria-hidden.

P2 — Memory-Lösch-Bestätigung (war sofort), overscroll-behavior:contain auf
Modals (CustomDialog/Agent/Cockpit/CommandPalette), ModelBrowse fmtN → Intl.NumberFormat.

P3 — Deep-Linking: top-level View im URL-Hash (#models), Reload/Teilen/Cmd-Klick
funktionieren; Sidebar-Nav als <a href="#id"> (echte Links, aria-current),
hashchange-Sync; index.css-Aktiv-Selektoren button→a nachgezogen.

Verifiziert: npm run build (tsc strict) clean; live gegen die Box (Hash-Nav,
Reload-Persistenz, aria-current, Aktiv-Styling teal+Akzent, keine Konsolenfehler).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-29 20:25:08 +02:00
parent 8108d3fd28
commit 087eb2259d
17 changed files with 378 additions and 306 deletions
+29 -9
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"
import { useEffect, useState, useCallback } from "react"
import { Command as CommandIcon, ChevronLeft, ChevronRight } from "lucide-react"
import { NAV, type ViewId } from "@/nav"
import { CommandPalette } from "@/components/CommandPalette"
@@ -18,7 +18,15 @@ import { cn } from "@/lib/utils"
export default function App() {
useMetricsFeeder() // sammelt Live-Verlauf global, unabhängig vom aktiven Tab
const [view, setView] = useState<ViewId>("dashboard")
// View ist im URL-Hash (#models) verankert → Reload/Teilen/Cmd-Klick funktionieren.
const [view, setView] = useState<ViewId>(() => {
const h = window.location.hash.slice(1) as ViewId
return NAV.some((n) => n.id === h) ? h : "dashboard"
})
const navigate = useCallback((v: ViewId) => {
if (window.location.hash.slice(1) === v) setView(v)
else window.location.hash = v
}, [])
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem("mc_sidebar_collapsed") === "true")
const [drawerOpen, setDrawerOpen] = useState(false)
const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance")
@@ -43,10 +51,19 @@ export default function App() {
useEffect(() => {
const handleNav = (e: Event) => {
const v = (e as CustomEvent).detail?.view as ViewId | undefined
if (v) setView(v)
if (v) navigate(v)
}
window.addEventListener("mc-navigate", handleNav)
return () => window.removeEventListener("mc-navigate", handleNav)
}, [navigate])
useEffect(() => {
const onHash = () => {
const h = window.location.hash.slice(1) as ViewId
if (NAV.some((n) => n.id === h)) setView(h)
}
window.addEventListener("hashchange", onHash)
return () => window.removeEventListener("hashchange", onHash)
}, [])
const active = NAV.find((n) => n.id === view)!
@@ -61,7 +78,7 @@ export default function App() {
<div className="absolute top-[30%] right-[20%] h-[40%] w-[40%] rounded-full bg-indigo-500/10 blur-[140px]" />
</div>
<CommandPalette onNavigate={setView} />
<CommandPalette onNavigate={navigate} />
<SystemDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} defaultTab={drawerTab} />
{/* Sidebar */}
@@ -88,17 +105,19 @@ export default function App() {
})
}}
className="p-1 rounded-md hover:bg-accent text-muted-foreground transition-colors cursor-pointer"
aria-label={sidebarCollapsed ? "Seitenleiste ausklappen" : "Seitenleiste einklappen"}
title={sidebarCollapsed ? "Maximieren" : "Minimieren"}
>
{sidebarCollapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
{sidebarCollapsed ? <ChevronRight className="h-4 w-4" aria-hidden="true" /> : <ChevronLeft className="h-4 w-4" aria-hidden="true" />}
</button>
</div>
<nav className="flex-1 space-y-1 px-3 py-4 overflow-y-auto">
{NAV.map((item) => (
<button
<a
key={item.id}
onClick={() => setView(item.id)}
href={`#${item.id}`}
aria-current={view === item.id ? "page" : undefined}
className={cn(
"flex w-full items-center rounded-md text-sm transition-all cursor-pointer",
sidebarCollapsed ? "justify-center p-2.5" : "gap-3 px-3 py-2",
@@ -107,10 +126,11 @@ export default function App() {
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
)}
title={sidebarCollapsed ? item.label : undefined}
aria-label={sidebarCollapsed ? item.label : undefined}
>
<item.icon className="h-4.5 w-4.5 shrink-0" />
<item.icon className="h-4.5 w-4.5 shrink-0" aria-hidden="true" />
{!sidebarCollapsed && <span className="truncate">{item.label}</span>}
</button>
</a>
))}
</nav>