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>
+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
+5 -3
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
<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"
/>