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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-BLpIFZ-c.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-MrCHFz7_.css">
<script type="module" crossorigin src="/assets/index-B3e8J_PY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DoqQd4Zi.css">
</head>
<body>
<div id="root"></div>
+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>
+1 -1
View File
@@ -21,7 +21,7 @@ export function CommandPalette({ onNavigate }: { onNavigate: (v: ViewId) => void
open={open}
onOpenChange={setOpen}
label="Befehlspalette"
className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4 pt-[12vh]"
className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4 pt-[12vh] overscroll-contain"
onClick={() => setOpen(false)}
>
<div
+4 -2
View File
@@ -15,15 +15,16 @@ export interface CustomDialogProps {
export function CustomDialog({ type, title, message, defaultValue, autoValue, autoLabel, onConfirm, onCancel }: CustomDialogProps) {
const inputRef = useRef<HTMLInputElement>(null)
return (
<div className="fixed inset-0 z-[99] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
<div className="fixed inset-0 z-[99] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 overscroll-contain" role="dialog" aria-modal="true" aria-label={title}>
<div className="w-full max-w-sm 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">{title}</h3>
<button
onClick={onCancel || (() => onConfirm())}
aria-label="Schließen"
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
>
<X className="h-4 w-4" />
<X className="h-4 w-4" aria-hidden="true" />
</button>
</div>
<p className="text-xs text-muted-foreground leading-relaxed">{message}</p>
@@ -34,6 +35,7 @@ export function CustomDialog({ type, title, message, defaultValue, autoValue, au
ref={inputRef}
type="text"
defaultValue={defaultValue}
aria-label={title}
className="flex-1 h-9 px-3 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
autoFocus
onKeyDown={(e) => {
+13 -3
View File
@@ -722,15 +722,20 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
type={showSudo ? "text" : "password"}
value={sudoPasswordInput}
onChange={(e) => setSudoPasswordInput(e.target.value)}
aria-label="Host Sudo-Passwort"
name="sudo-password"
autoComplete="off"
spellCheck={false}
placeholder="Sudo-Passwort für System-Operationen"
className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
/>
<button
type="button"
onClick={() => setShowSudo(!showSudo)}
aria-label={showSudo ? "Passwort verbergen" : "Passwort anzeigen"}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors"
>
{showSudo ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
{showSudo ? <EyeOff className="h-4 w-4" aria-hidden="true" /> : <Eye className="h-4 w-4" aria-hidden="true" />}
</button>
</div>
<p className="text-[9px] text-muted-foreground leading-normal">
@@ -749,15 +754,20 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
type={showHf ? "text" : "password"}
value={hfTokenInput}
onChange={(e) => setHfTokenInput(e.target.value)}
placeholder="hf_..."
aria-label="HuggingFace API Token"
name="hf-token"
autoComplete="off"
spellCheck={false}
placeholder="hf_…"
className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
/>
<button
type="button"
onClick={() => setShowHf(!showHf)}
aria-label={showHf ? "Token verbergen" : "Token anzeigen"}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors"
>
{showHf ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
{showHf ? <EyeOff className="h-4 w-4" aria-hidden="true" /> : <Eye className="h-4 w-4" aria-hidden="true" />}
</button>
</div>
<p className="text-[9px] text-muted-foreground leading-normal">
@@ -39,7 +39,8 @@ export function MemoryInputCard() {
<textarea
value={memContent}
onChange={(e) => setMemContent(e.target.value)}
placeholder="Fakt / Regel im Pool speichern..."
aria-label="Fakt oder Regel im Gedächtnis speichern"
placeholder="Fakt / Regel im Pool speichern…"
rows={2}
className="w-full resize-none rounded-xl border border-border/50 bg-background/30 p-2 text-[10px] outline-none focus:ring-1 focus:ring-primary transition-all leading-normal"
/>
@@ -47,6 +48,7 @@ export function MemoryInputCard() {
<select
value={memCat}
onChange={(e) => setMemCat(e.target.value)}
aria-label="Kategorie"
className="h-7 rounded-lg border border-border/50 bg-background/50 px-2 text-[10px] outline-none cursor-pointer"
>
<option value="stable">🔵 Fakt</option>
@@ -96,6 +96,7 @@ export function LaneEditor() {
value={val as number}
min={spec.min}
max={spec.max}
aria-label={spec.label}
onChange={(e) => set(k, Number(e.target.value) as any)}
className="h-8 w-full rounded-lg border border-border/40 bg-background/40 px-3 font-mono text-[11px] text-foreground outline-none focus:border-primary/50"
/>
@@ -103,6 +104,8 @@ export function LaneEditor() {
<input
type="text"
value={val as string}
aria-label={spec.label}
spellCheck={false}
placeholder={k === "coder_lite" ? "(leer = aus)" : ""}
onChange={(e) => set(k, e.target.value as any)}
className="h-8 w-full rounded-lg border border-border/40 bg-background/40 px-3 font-mono text-[11px] text-foreground outline-none focus:border-primary/50"
+23 -4
View File
@@ -21,6 +21,7 @@
--input: 222 18% 23%;
--ring: 172 72% 50%;
--radius: 0.75rem;
color-scheme: dark;
}
.light {
@@ -39,6 +40,7 @@
--border: 220 13% 88%;
--input: 220 13% 88%;
--ring: 172 66% 50%;
color-scheme: light;
}
@theme inline {
@@ -97,6 +99,23 @@
outline-color: hsl(var(--primary) / 0.5);
}
/* Sichtbarer Fokus für Tastatur-Nutzer auf allen interaktiven Elementen
(Inputs haben ihren eigenen Ring unten via box-shadow). */
:focus-visible {
outline: 2px solid hsl(var(--primary) / 0.7);
outline-offset: 2px;
}
/* Bewegungs-Sensibilität respektieren: Animationen/Transitions praktisch aus. */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
html,
body,
#root {
@@ -139,7 +158,7 @@ body {
box-shadow: 0 10px 30px -15px rgba(0, 0, 0, 0.45), inset 0 1px 0 0 rgba(255, 255, 255, 0.05) !important;
position: relative;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
/* Gorgeous gradient top accent line to all main cards to inject color and structure */
@@ -171,7 +190,7 @@ body {
.rounded-xl.border.bg-card\/45 {
background-color: hsl(var(--card) / 0.88) !important;
border-color: hsl(var(--border) / 0.9) !important;
transition: all 0.2s ease !important;
transition: background-color 0.2s ease, border-color 0.2s ease !important;
}
.rounded-xl.border.bg-card\/45:hover {
background-color: hsl(var(--card) / 0.95) !important;
@@ -179,7 +198,7 @@ body {
}
/* Sidebar active link styling */
aside nav button[class*="bg-primary/15"]:not([class*="justify-center"]) {
aside nav a[class*="bg-primary/15"]:not([class*="justify-center"]) {
background: linear-gradient(90deg, hsl(var(--primary) / 0.18), rgba(99, 102, 241, 0.12)) !important;
color: hsl(var(--primary)) !important;
border-left: 3.5px solid hsl(var(--primary)) !important;
@@ -188,7 +207,7 @@ aside nav button[class*="bg-primary/15"]:not([class*="justify-center"]) {
box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.02) !important;
}
aside nav button[class*="bg-primary/15"][class*="justify-center"] {
aside nav a[class*="bg-primary/15"][class*="justify-center"] {
background: hsl(var(--primary) / 0.18) !important;
color: hsl(var(--primary)) !important;
box-shadow: 0 0 12px 1px hsl(var(--primary) / 0.1) !important;
+1 -1
View File
@@ -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">
+4
View File
@@ -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"
/>
+14 -8
View File
@@ -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>
+5 -5
View File
@@ -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">
+9 -3
View File
@@ -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"
/>