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:
@@ -250,7 +250,7 @@ export function AgentView() {
|
||||
|
||||
{/* Brain Selection Modal — eine Stelle für den Hirn-Wechsel (warm-bewusst + Alias) */}
|
||||
{s && showBrainSelect && (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 overscroll-contain" role="dialog" aria-modal="true" aria-label="Hermes-Hirn konfigurieren">
|
||||
<div className="w-full max-w-md rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-border/20 pb-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-primary flex items-center gap-1.5">
|
||||
|
||||
@@ -178,6 +178,8 @@ export function ConnectView() {
|
||||
<input
|
||||
value={host}
|
||||
onChange={(e) => saveHost(e.target.value)}
|
||||
aria-label="Box LAN IP-Adresse"
|
||||
spellCheck={false}
|
||||
placeholder="z.B. 192.168.178.151"
|
||||
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
|
||||
/>
|
||||
@@ -191,6 +193,8 @@ export function ConnectView() {
|
||||
<input
|
||||
value={mcpPath}
|
||||
onChange={(e) => saveMcpPath(e.target.value)}
|
||||
aria-label="Lokaler MCP-Scriptpfad"
|
||||
spellCheck={false}
|
||||
placeholder="z.B. F:\Coding Stuff\mission-control-2\mcp\mcp_memory.py"
|
||||
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-violet-500/50 text-foreground"
|
||||
/>
|
||||
|
||||
@@ -94,9 +94,11 @@ export function MemoryView() {
|
||||
setContent(""); setShowAdd(false); reloadMemory()
|
||||
}
|
||||
|
||||
async function del(id: string) {
|
||||
await api(`/api/memory/${id}`, { method: "DELETE" })
|
||||
reloadMemory()
|
||||
function del(id: string) {
|
||||
showConfirm("Eintrag löschen?", "Diesen Gedächtnis-Eintrag dauerhaft entfernen?", async () => {
|
||||
try { await api(`/api/memory/${id}`, { method: "DELETE" }); reloadMemory() }
|
||||
catch (e: any) { showAlert("Fehler", `Löschen fehlgeschlagen: ${e.message || e}`) }
|
||||
})
|
||||
}
|
||||
|
||||
async function copyPrompt() {
|
||||
@@ -156,6 +158,8 @@ export function MemoryView() {
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
aria-label="Gedächtnis durchsuchen"
|
||||
type="search"
|
||||
placeholder="Semantisch suchen…"
|
||||
className="w-52 h-9 pl-8 pr-3 rounded-lg border border-border/60 bg-card/45 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
|
||||
/>
|
||||
@@ -176,8 +180,8 @@ export function MemoryView() {
|
||||
</div>
|
||||
<button onClick={cleanup} disabled={deduping}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-card hover:border-primary/50 transition-all cursor-pointer shadow-md disabled:opacity-50"
|
||||
title="Deduplizieren">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
aria-label="Duplikate bereinigen" title="Deduplizieren">
|
||||
<Sparkles className="h-4 w-4 text-primary" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,13 +204,15 @@ export function MemoryView() {
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Neuen Eintrag anlegen</div>
|
||||
<button onClick={() => setShowAdd(false)} className="text-muted-foreground hover:text-foreground"><X className="h-4 w-4" /></button>
|
||||
<button onClick={() => setShowAdd(false)} aria-label="Schließen" className="text-muted-foreground hover:text-foreground"><X className="h-4 w-4" aria-hidden="true" /></button>
|
||||
</div>
|
||||
<textarea value={content} onChange={(e) => setContent(e.target.value)} rows={2}
|
||||
aria-label="Eintragstext"
|
||||
placeholder="Eine Regel, Vorliebe oder einen stabilen Fakt über dich oder das Projekt…"
|
||||
className="w-full resize-none rounded-xl border border-border/60 bg-background/30 p-3 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground leading-relaxed" />
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<select value={category} onChange={(e) => setCategory(e.target.value)}
|
||||
aria-label="Kategorie"
|
||||
className="h-8 rounded-lg border border-border/60 bg-background/50 px-2 text-xs outline-none font-semibold text-foreground cursor-pointer">
|
||||
{CATEGORIES.map((c) => <option key={c} value={c} className="bg-popover text-foreground">{CAT_CONFIG[c]?.label || c}</option>)}
|
||||
</select>
|
||||
@@ -321,9 +327,9 @@ export function MemoryView() {
|
||||
title={isAuto ? "Automatisch gelernt" : "Manuell angelegt"}>
|
||||
{isAuto && <Sparkles className="h-2.5 w-2.5" />}{m.source}
|
||||
</span>
|
||||
<button onClick={() => del(m.id)} title="Eintrag löschen"
|
||||
<button onClick={() => del(m.id)} aria-label="Eintrag löschen" title="Eintrag löschen"
|
||||
className="h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all opacity-0 group-hover:opacity-100">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -655,9 +655,9 @@ export function Cockpit() {
|
||||
<button
|
||||
onClick={() => handleDelete(m.name)}
|
||||
className="h-7 w-7 rounded-lg border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 flex items-center justify-center transition-colors cursor-pointer"
|
||||
title="Modell löschen"
|
||||
aria-label="Modell löschen" title="Modell löschen"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -811,9 +811,9 @@ export function Cockpit() {
|
||||
<button
|
||||
onClick={() => handleDelete(m.name)}
|
||||
className="h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all cursor-pointer"
|
||||
title="Modell löschen"
|
||||
aria-label="Modell löschen" title="Modell löschen"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -836,7 +836,7 @@ export function Cockpit() {
|
||||
: models
|
||||
const assign = (name: string) => { handleRoleChange(activeRoleForAssign, name); setActiveRoleForAssign(null) }
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 overscroll-contain" role="dialog" aria-modal="true" aria-label={`Rolle ${activeRoleForAssign} konfigurieren`}>
|
||||
<div className="w-full max-w-md rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-border/20 pb-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-primary">
|
||||
|
||||
@@ -15,10 +15,9 @@ const CHIPS: { key: string; label: string; q: string }[] = [
|
||||
]
|
||||
const ROLE_OPTS = ["fast", "heavy", "coder", "vision", "scout"]
|
||||
|
||||
const _nf = new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 })
|
||||
function fmtN(n: number): string {
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
||||
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}k`
|
||||
return String(n)
|
||||
return _nf.format(n)
|
||||
}
|
||||
|
||||
export function ModelBrowse() {
|
||||
@@ -143,6 +142,9 @@ export function ModelBrowse() {
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && submitSearch()}
|
||||
aria-label="HuggingFace durchsuchen"
|
||||
type="search"
|
||||
spellCheck={false}
|
||||
placeholder="HuggingFace durchsuchen (z.B. Qwen Coder, Llama-3.1, gemma)…"
|
||||
className="w-full h-10 pl-9 pr-3 rounded-xl border border-border/60 bg-background/40 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
|
||||
/>
|
||||
@@ -227,6 +229,7 @@ export function ModelBrowse() {
|
||||
<select
|
||||
value={quant}
|
||||
onChange={(e) => { setQuant(e.target.value); refreshFit(r.repo, e.target.value, role) }}
|
||||
aria-label="Quantisierung"
|
||||
className="h-8 rounded-lg border border-border/60 bg-background/40 px-2 text-xs text-foreground font-semibold outline-none"
|
||||
>
|
||||
{quants.map((qq) => <option key={qq} value={qq} className="bg-popover text-foreground">{qq}</option>)}
|
||||
@@ -235,6 +238,7 @@ export function ModelBrowse() {
|
||||
<select
|
||||
value={role}
|
||||
onChange={(e) => { setRole(e.target.value); refreshFit(r.repo, quant, e.target.value) }}
|
||||
aria-label="Rolle"
|
||||
title="Optional — bestimmt Auto-Konfiguration + setup-bewussten Kontext"
|
||||
className="h-8 rounded-lg border border-border/60 bg-background/40 px-2 text-xs text-foreground font-semibold outline-none"
|
||||
>
|
||||
@@ -291,6 +295,8 @@ export function ModelBrowse() {
|
||||
value={direct}
|
||||
onChange={(e) => setDirect(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && loadDirect()}
|
||||
aria-label="Repository oder URL direkt eingeben"
|
||||
spellCheck={false}
|
||||
placeholder="org/repo oder HF-URL (z.B. unsloth/Qwen2.5-Coder-7B-Instruct-GGUF)"
|
||||
className="flex-1 h-9 rounded-lg border border-border/60 bg-background/40 px-3 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground font-mono"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user