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:
+1
-1
File diff suppressed because one or more lines are too long
+264
-264
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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