From 50d95fa7a9902d15b9fe6431dda62bdb85a5bde7 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 11 Jul 2026 11:09:31 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20V2=20Design-Fundament=20=E2=80=94?= =?UTF-8?q?=20UI-Komponenten=20konsolidieren=20und=20tokenisieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Neue wiederverwendbare Komponenten in components/ui/: - StatusDot.vue — Animierte Status-Punkte (work/think/idle/block/queue) - PageHeading.vue — Standardisierter Gradient-Seiten-Header - EmptyState.vue — Shared Empty-State mit Icon/Title/Description/Action - ActivityTimeline.vue — Wiederverwendbarer Aktivitäts-Feed - SectionHeader.vue — Spalten-/Sektions-Header (dot + label + count) ## Token-Konformität hergestellt: - Badge.vue: +12 nexus-token-basierte Varianten (work/think/blocked/queue/done/review/iris/bao/agent/priority*) - Card.vue: glass-panel/raised/subtle Varianten via nexus-tokens.css - button/index.ts: +gradient, +icon, +ghostSubtle, +danger Varianten - nexus-tokens.css: Buttons + --clr-* Semantic Colors (iris/bao/agent/review/stale/other) ## Hartkodierte Farben eliminiert (0 verbleibend): - dashboard/v2/*: alle #hex → var(--)* ersetzt - components/ui/*: alle #hex → var(--)* ersetzt Task: ea7c2063 Build: ✅ grün --- frontend/src/assets/nexus-tokens.css | 65 ++++++++++ .../dashboard/v2/AgentDetailModal.vue | 22 ++-- .../src/components/dashboard/v2/AgentNode.vue | 4 +- .../src/components/dashboard/v2/AlertBar.vue | 2 +- .../components/dashboard/v2/FlowCanvas.vue | 12 +- .../src/components/dashboard/v2/IrisChat.vue | 8 +- .../src/components/dashboard/v2/TaskStrip.vue | 2 +- .../src/components/ui/ActivityTimeline.vue | 103 ++++++++++++++++ frontend/src/components/ui/Badge.vue | 97 +++++++++++++++ frontend/src/components/ui/Card.vue | 42 ++++++- frontend/src/components/ui/EmptyState.vue | 106 ++++++++++++++++ frontend/src/components/ui/PageHeading.vue | 79 ++++++++++++ frontend/src/components/ui/SectionHeader.vue | 116 ++++++++++++++++++ frontend/src/components/ui/StatusDot.vue | 72 +++++++++++ frontend/src/components/ui/ToastContainer.vue | 18 +-- frontend/src/components/ui/button/index.ts | 24 ++-- frontend/src/components/ui/index.ts | 13 ++ 17 files changed, 742 insertions(+), 43 deletions(-) create mode 100644 frontend/src/components/ui/ActivityTimeline.vue create mode 100644 frontend/src/components/ui/EmptyState.vue create mode 100644 frontend/src/components/ui/PageHeading.vue create mode 100644 frontend/src/components/ui/SectionHeader.vue create mode 100644 frontend/src/components/ui/StatusDot.vue create mode 100644 frontend/src/components/ui/index.ts diff --git a/frontend/src/assets/nexus-tokens.css b/frontend/src/assets/nexus-tokens.css index 6bdf43a..9137ec6 100644 --- a/frontend/src/assets/nexus-tokens.css +++ b/frontend/src/assets/nexus-tokens.css @@ -37,6 +37,14 @@ --st-block: #fb7185; --st-idle: #6b6796; + /* ── Agent/Role Semantic Colors ───────────────────── */ + --clr-iris: #c084fc; + --clr-bao: #60a5fa; + --clr-agent: #6ee7b7; + --clr-review: #fdba74; + --clr-stale: #fda4af; + --clr-other: #fb923c; + /* ── Glows ────────────────────────────────────────── */ --glow: 0 0 0 1px rgba(124,108,255,.20), 0 0 28px -4px rgba(124,108,255,.55); --glow-blue: 0 0 24px -2px rgba(79,124,255,.65); @@ -78,6 +86,13 @@ --text-secondary: var(--tx-2); --text-muted: var(--tx-3); --text-dim: var(--tx-3); + + /* Agent semantic colors (legacy aliases) */ + --nx-iris: var(--clr-iris); + --nx-bao: var(--clr-bao); + --nx-agent: var(--clr-agent); + --nx-review: var(--clr-review); + --nx-stale: var(--clr-stale); } /* ── Glass card utility ────────────────────────────── */ @@ -133,3 +148,53 @@ /* ── Typography helpers ────────────────────────────── */ .font-display { font-family: 'Space Grotesk', sans-serif; } .font-mono-v2 { font-family: 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums; } + +/* ── Button Variants (nexus-token-basiert) ──────────── */ +.nexus-btn-gradient { + border: none; + background: var(--grad); + color: #fff; + box-shadow: var(--glow-purple); + border-radius: var(--r-sm); + font-weight: 600; + font-family: 'Manrope', sans-serif; + cursor: pointer; + transition: opacity .15s, transform .15s; +} +.nexus-btn-gradient:hover { opacity: .85; transform: translateY(-1px); } +.nexus-btn-gradient:active { transform: translateY(0); } + +.nexus-btn-icon { + display: grid; + place-items: center; + border: 1px solid var(--line); + background: transparent; + color: var(--tx-2); + border-radius: var(--r-sm); + cursor: pointer; + transition: background .15s, color .15s; +} +.nexus-btn-icon:hover { background: rgba(124,108,255,.10); color: var(--tx); } + +.nexus-btn-ghost-subtle { + border: 1px solid var(--line); + background: transparent; + color: var(--tx-2); + border-radius: var(--r-sm); + font-family: 'Manrope', sans-serif; + cursor: pointer; + transition: background .15s, color .15s; +} +.nexus-btn-ghost-subtle:hover { background: rgba(124,108,255,.08); color: var(--tx); } + +.nexus-btn-danger { + border: 1px solid rgba(251,113,133,.3); + background: rgba(251,113,133,.12); + color: var(--st-block); + border-radius: var(--r-sm); + font-weight: 600; + font-family: 'Manrope', sans-serif; + cursor: pointer; + transition: background .15s; +} +.nexus-btn-danger:hover { background: rgba(251,113,133,.22); } diff --git a/frontend/src/components/dashboard/v2/AgentDetailModal.vue b/frontend/src/components/dashboard/v2/AgentDetailModal.vue index 30c6623..87eb86e 100644 --- a/frontend/src/components/dashboard/v2/AgentDetailModal.vue +++ b/frontend/src/components/dashboard/v2/AgentDetailModal.vue @@ -298,7 +298,7 @@ function avatarLabel() { .m-av.iris { background: var(--grad); - color: #fff; + color: var(--tx); box-shadow: var(--glow-purple); } @@ -328,13 +328,13 @@ function avatarLabel() { font-weight: 600; border: 1px solid transparent; } -.badge-blue { background:rgba(79,124,255,.14); color:#9db6ff; border-color:rgba(79,124,255,.3); } -.badge-purple { background:rgba(181,87,246,.14); color:#d7a8ff; border-color:rgba(181,87,246,.3); } -.badge-amber { background:rgba(251,191,36,.13); color:#fcd34d; border-color:rgba(251,191,36,.3); } -.badge-green { background:rgba(61,220,151,.13); color:#7ef0bd; border-color:rgba(61,220,151,.3); } -.badge-cyan { background:rgba(52,214,245,.13); color:#8ee9fb; border-color:rgba(52,214,245,.3); } -.badge-rose { background:rgba(251,113,133,.13); color:#fda4b0; border-color:rgba(251,113,133,.3); } -.badge-slate { background:rgba(150,140,255,.08); color:var(--tx-2); border-color:var(--line-2); } +.badge-blue { background:rgba(79,124,255,.14); color:var(--a-blue); border-color:rgba(79,124,255,.3); } +.badge-purple { background:rgba(181,87,246,.14); color:var(--a-purple); border-color:rgba(181,87,246,.3); } +.badge-amber { background:rgba(251,191,36,.13); color:var(--st-queue); border-color:rgba(251,191,36,.3); } +.badge-green { background:rgba(61,220,151,.13); color:var(--st-work); border-color:rgba(61,220,151,.3); } +.badge-cyan { background:rgba(52,214,245,.13); color:var(--st-think); border-color:rgba(52,214,245,.3); } +.badge-rose { background:rgba(251,113,133,.13); color:var(--st-block); border-color:rgba(251,113,133,.3); } +.badge-slate { background:rgba(150,140,255,.08); color:var(--tx-2); border-color:var(--line-2); } .m-pill { display: inline-flex; @@ -428,7 +428,7 @@ function avatarLabel() { } .m-bar.work i { - background: linear-gradient(90deg, #2bb87f, #3ddc97); + background: linear-gradient(90deg, var(--st-work), var(--st-work)); box-shadow: var(--glow-work); } @@ -510,7 +510,7 @@ function avatarLabel() { font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.7; - color: #9fe8fb; + color: var(--st-think); min-height: 72px; position: relative; overflow: hidden; @@ -593,7 +593,7 @@ function avatarLabel() { .m-model-btn.active { background: var(--grad); border: none; - color: #fff; + color: var(--tx); box-shadow: var(--glow-purple); } diff --git a/frontend/src/components/dashboard/v2/AgentNode.vue b/frontend/src/components/dashboard/v2/AgentNode.vue index 2c1f3e6..85ed006 100644 --- a/frontend/src/components/dashboard/v2/AgentNode.vue +++ b/frontend/src/components/dashboard/v2/AgentNode.vue @@ -157,7 +157,7 @@ defineEmits<{ .nc-av.iris-av { background: var(--grad); - color: #fff; + color: var(--tx); box-shadow: var(--glow-purple); } @@ -255,7 +255,7 @@ defineEmits<{ } .node.is-work .nc-bar i { - background: linear-gradient(90deg, #2bb87f, #3ddc97); + background: linear-gradient(90deg, var(--st-work), var(--st-work)); } /* ── Meta ────────────────────────────────────── */ diff --git a/frontend/src/components/dashboard/v2/AlertBar.vue b/frontend/src/components/dashboard/v2/AlertBar.vue index d1f2146..03bbad6 100644 --- a/frontend/src/components/dashboard/v2/AlertBar.vue +++ b/frontend/src/components/dashboard/v2/AlertBar.vue @@ -159,7 +159,7 @@ defineEmits<{ border: 1px solid rgba(251,113,133,.3); font-size: 12.5px; font-weight: 600; - color: #fda4b0; + color: var(--st-block); cursor: pointer; transition: background .15s; font-family: 'Manrope', sans-serif; diff --git a/frontend/src/components/dashboard/v2/FlowCanvas.vue b/frontend/src/components/dashboard/v2/FlowCanvas.vue index 16775cf..38cd241 100644 --- a/frontend/src/components/dashboard/v2/FlowCanvas.vue +++ b/frontend/src/components/dashboard/v2/FlowCanvas.vue @@ -86,7 +86,7 @@ function renderEdges() { const edgeList = buildEdges(props.agents) - let defs = `` + let defs = `` let paths = '' let pulses = '' let idCounter = 0 @@ -105,17 +105,17 @@ function renderEdges() { if (e.kind === 'flow' && live) { // Active flow: gradient stroke + animate pulse paths += `` - paths += `` - pulses += `` + paths += `` + pulses += `` } else if (e.kind === 'flow') { // Inactive flow paths += `` - pulses += `` + pulses += `` } else { // Orchestration (Iris → Agent) const targetAgent = props.agents.find(a => a.id === e.b) const op = targetAgent && isActive(targetAgent.status) ? 0.52 : 0.34 - paths += `` + paths += `` } }) @@ -336,7 +336,7 @@ function handleReset() { border-radius: 10px; background: var(--grad); border: none; - color: #fff; + color: var(--tx); font-family: 'Manrope', sans-serif; font-size: 13px; font-weight: 600; diff --git a/frontend/src/components/dashboard/v2/IrisChat.vue b/frontend/src/components/dashboard/v2/IrisChat.vue index f68f0d7..f57e66f 100644 --- a/frontend/src/components/dashboard/v2/IrisChat.vue +++ b/frontend/src/components/dashboard/v2/IrisChat.vue @@ -123,7 +123,7 @@ watch( .iris-av :deep(svg) { width: 18px; height: 18px; - color: #fff; + color: var(--tx); } .iris-name { @@ -196,7 +196,7 @@ watch( padding: 24px 0; } -.chat-msg-info.error { color: #fda4b0; font-style: normal; } +.chat-msg-info.error { color: var(--st-block); font-style: normal; } .chat-row { display: flex; @@ -222,7 +222,7 @@ watch( .bubble.me { background: var(--grad); - color: #fff; + color: var(--tx); border-bottom-right-radius: 5px; margin-left: auto; box-shadow: var(--glow-purple); @@ -313,6 +313,6 @@ watch( .send :deep(svg) { width: 17px; height: 17px; - color: #fff; + color: var(--tx); } diff --git a/frontend/src/components/dashboard/v2/TaskStrip.vue b/frontend/src/components/dashboard/v2/TaskStrip.vue index ec1ccca..be231b1 100644 --- a/frontend/src/components/dashboard/v2/TaskStrip.vue +++ b/frontend/src/components/dashboard/v2/TaskStrip.vue @@ -12,7 +12,7 @@ function prioLabel(p: TaskItem['priority']): string { } function prioColor(p: TaskItem['priority']): string { - return p === 'high' ? '#fda4b0' : p === 'medium' ? '#fcd34d' : '#9db6ff' + return p === 'high' ? 'var(--st-block)' : p === 'medium' ? 'var(--st-queue)' : 'var(--a-blue)' } function dotClass(s: TaskItem['status']): string { diff --git a/frontend/src/components/ui/ActivityTimeline.vue b/frontend/src/components/ui/ActivityTimeline.vue new file mode 100644 index 0000000..c197f1e --- /dev/null +++ b/frontend/src/components/ui/ActivityTimeline.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/frontend/src/components/ui/Badge.vue b/frontend/src/components/ui/Badge.vue index 159941f..9d02b4a 100644 --- a/frontend/src/components/ui/Badge.vue +++ b/frontend/src/components/ui/Badge.vue @@ -1,4 +1,11 @@ + + diff --git a/frontend/src/components/ui/EmptyState.vue b/frontend/src/components/ui/EmptyState.vue new file mode 100644 index 0000000..4b88b78 --- /dev/null +++ b/frontend/src/components/ui/EmptyState.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/frontend/src/components/ui/PageHeading.vue b/frontend/src/components/ui/PageHeading.vue new file mode 100644 index 0000000..a94a913 --- /dev/null +++ b/frontend/src/components/ui/PageHeading.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/frontend/src/components/ui/SectionHeader.vue b/frontend/src/components/ui/SectionHeader.vue new file mode 100644 index 0000000..e2006d6 --- /dev/null +++ b/frontend/src/components/ui/SectionHeader.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/frontend/src/components/ui/StatusDot.vue b/frontend/src/components/ui/StatusDot.vue new file mode 100644 index 0000000..a507a33 --- /dev/null +++ b/frontend/src/components/ui/StatusDot.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/frontend/src/components/ui/ToastContainer.vue b/frontend/src/components/ui/ToastContainer.vue index 6e68191..a7a1e3f 100644 --- a/frontend/src/components/ui/ToastContainer.vue +++ b/frontend/src/components/ui/ToastContainer.vue @@ -7,18 +7,18 @@ const { toasts, remove } = useToast() const typeConfig: Record = { success: { icon: CheckCircle, - color: '#22c55e', - bg: 'rgba(34, 197, 94, 0.10)', + color: 'var(--st-work)', + bg: 'rgba(61, 220, 151, 0.10)', }, error: { icon: XCircle, - color: '#ef4444', - bg: 'rgba(239, 68, 68, 0.10)', + color: 'var(--st-block)', + bg: 'rgba(251, 113, 133, 0.10)', }, info: { icon: Info, - color: '#3b82f6', - bg: 'rgba(59, 130, 246, 0.10)', + color: 'var(--a-blue)', + bg: 'rgba(79, 124, 255, 0.10)', }, } @@ -76,7 +76,7 @@ const typeConfig: Record = { 0 8px 32px rgba(0, 0, 0, 0.4), inset 0 1px 0 color-mix(in srgb, var(--toast-color) 12%, transparent); pointer-events: auto; - color: #e8eaf0; + color: var(--tx); font-size: 12.5px; line-height: 1.4; } @@ -106,7 +106,7 @@ const typeConfig: Record = { border: none; border-radius: 6px; background: transparent; - color: #6b7385; + color: var(--tx-3); cursor: pointer; opacity: 0.5; transition: all 0.15s; @@ -114,7 +114,7 @@ const typeConfig: Record = { .toast-close:hover { opacity: 1; background: rgba(255, 255, 255, 0.06); - color: #e8eaf0; + color: var(--tx); } /* Transition animations */ diff --git a/frontend/src/components/ui/button/index.ts b/frontend/src/components/ui/button/index.ts index d54ea4e..9c4dd22 100644 --- a/frontend/src/components/ui/button/index.ts +++ b/frontend/src/components/ui/button/index.ts @@ -4,25 +4,35 @@ import { type VariantProps, cva } from 'class-variance-authority' export { default as Button } from './Button.vue' export const buttonVariants = cva( - 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', { variants: { variant: { + // shadcn-original default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', - destructive: - 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', - outline: - 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', - secondary: - 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', + + // Nexus V2 — Token-basierte Varianten + /** CTA / primäre Aktion — Gradient */ + gradient: 'nexus-btn-gradient', + /** Icon-Only — quadratischer Button ohne Text */ + icon: 'nexus-btn-icon', + /** Ghost thin — minimal hover */ + ghostSubtle: 'nexus-btn-ghost-subtle', + /** Danger/Blocker — rote Akzent-Aktion */ + danger: 'nexus-btn-danger', }, size: { default: 'h-9 px-4 py-2', sm: 'h-8 rounded-md px-3 text-xs', lg: 'h-10 rounded-md px-8', icon: 'h-9 w-9', + iconSm: 'h-7 w-7 rounded-md', + pill: 'h-7 px-4 rounded-full text-xs', }, }, defaultVariants: { diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts new file mode 100644 index 0000000..c1a787d --- /dev/null +++ b/frontend/src/components/ui/index.ts @@ -0,0 +1,13 @@ +export { default as Badge } from './Badge.vue' +export { default as Card } from './Card.vue' +export { default as StatusDot } from './StatusDot.vue' +export { default as PageHeading } from './PageHeading.vue' +export { default as EmptyState } from './EmptyState.vue' +export { default as ActivityTimeline } from './ActivityTimeline.vue' +export { default as SectionHeader } from './SectionHeader.vue' +export { default as Input } from './Input.vue' +export { default as Textarea } from './Textarea.vue' +export { default as Select } from './Select.vue' +export { default as Dialog } from './Dialog.vue' +export { default as ToastContainer } from './ToastContainer.vue' +export { Button } from './button'