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:
+29
-9
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user