feat(ui): V2 Design-Fundament — UI-Komponenten konsolidieren und tokenisieren
## 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
This commit is contained in:
@@ -37,6 +37,14 @@
|
|||||||
--st-block: #fb7185;
|
--st-block: #fb7185;
|
||||||
--st-idle: #6b6796;
|
--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 ────────────────────────────────────────── */
|
/* ── Glows ────────────────────────────────────────── */
|
||||||
--glow: 0 0 0 1px rgba(124,108,255,.20), 0 0 28px -4px rgba(124,108,255,.55);
|
--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);
|
--glow-blue: 0 0 24px -2px rgba(79,124,255,.65);
|
||||||
@@ -78,6 +86,13 @@
|
|||||||
--text-secondary: var(--tx-2);
|
--text-secondary: var(--tx-2);
|
||||||
--text-muted: var(--tx-3);
|
--text-muted: var(--tx-3);
|
||||||
--text-dim: 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 ────────────────────────────── */
|
/* ── Glass card utility ────────────────────────────── */
|
||||||
@@ -133,3 +148,53 @@
|
|||||||
/* ── Typography helpers ────────────────────────────── */
|
/* ── Typography helpers ────────────────────────────── */
|
||||||
.font-display { font-family: 'Space Grotesk', sans-serif; }
|
.font-display { font-family: 'Space Grotesk', sans-serif; }
|
||||||
.font-mono-v2 { font-family: 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums; }
|
.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); }
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ function avatarLabel() {
|
|||||||
|
|
||||||
.m-av.iris {
|
.m-av.iris {
|
||||||
background: var(--grad);
|
background: var(--grad);
|
||||||
color: #fff;
|
color: var(--tx);
|
||||||
box-shadow: var(--glow-purple);
|
box-shadow: var(--glow-purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,13 +328,13 @@ function avatarLabel() {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
.badge-blue { background:rgba(79,124,255,.14); color:#9db6ff; border-color:rgba(79,124,255,.3); }
|
.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:#d7a8ff; border-color:rgba(181,87,246,.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:#fcd34d; border-color:rgba(251,191,36,.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:#7ef0bd; border-color:rgba(61,220,151,.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:#8ee9fb; border-color:rgba(52,214,245,.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:#fda4b0; border-color:rgba(251,113,133,.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); }
|
.badge-slate { background:rgba(150,140,255,.08); color:var(--tx-2); border-color:var(--line-2); }
|
||||||
|
|
||||||
.m-pill {
|
.m-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -428,7 +428,7 @@ function avatarLabel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.m-bar.work i {
|
.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);
|
box-shadow: var(--glow-work);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,7 +510,7 @@ function avatarLabel() {
|
|||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: #9fe8fb;
|
color: var(--st-think);
|
||||||
min-height: 72px;
|
min-height: 72px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -593,7 +593,7 @@ function avatarLabel() {
|
|||||||
.m-model-btn.active {
|
.m-model-btn.active {
|
||||||
background: var(--grad);
|
background: var(--grad);
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: var(--tx);
|
||||||
box-shadow: var(--glow-purple);
|
box-shadow: var(--glow-purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ defineEmits<{
|
|||||||
|
|
||||||
.nc-av.iris-av {
|
.nc-av.iris-av {
|
||||||
background: var(--grad);
|
background: var(--grad);
|
||||||
color: #fff;
|
color: var(--tx);
|
||||||
box-shadow: var(--glow-purple);
|
box-shadow: var(--glow-purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ defineEmits<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.node.is-work .nc-bar i {
|
.node.is-work .nc-bar i {
|
||||||
background: linear-gradient(90deg, #2bb87f, #3ddc97);
|
background: linear-gradient(90deg, var(--st-work), var(--st-work));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Meta ────────────────────────────────────── */
|
/* ── Meta ────────────────────────────────────── */
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ defineEmits<{
|
|||||||
border: 1px solid rgba(251,113,133,.3);
|
border: 1px solid rgba(251,113,133,.3);
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #fda4b0;
|
color: var(--st-block);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background .15s;
|
transition: background .15s;
|
||||||
font-family: 'Manrope', sans-serif;
|
font-family: 'Manrope', sans-serif;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function renderEdges() {
|
|||||||
|
|
||||||
const edgeList = buildEdges(props.agents)
|
const edgeList = buildEdges(props.agents)
|
||||||
|
|
||||||
let defs = `<defs><linearGradient id="eg2" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#4f7cff"/><stop offset="1" stop-color="#b557f6"/></linearGradient></defs>`
|
let defs = `<defs><linearGradient id="eg2" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="var(--a-blue)"/><stop offset="1" stop-color="var(--a-purple)"/></linearGradient></defs>`
|
||||||
let paths = ''
|
let paths = ''
|
||||||
let pulses = ''
|
let pulses = ''
|
||||||
let idCounter = 0
|
let idCounter = 0
|
||||||
@@ -105,17 +105,17 @@ function renderEdges() {
|
|||||||
if (e.kind === 'flow' && live) {
|
if (e.kind === 'flow' && live) {
|
||||||
// Active flow: gradient stroke + animate pulse
|
// Active flow: gradient stroke + animate pulse
|
||||||
paths += `<path id="${pathId}" d="${d}" fill="none" stroke="url(#eg2)" stroke-width="2.2" opacity="0.85"/>`
|
paths += `<path id="${pathId}" d="${d}" fill="none" stroke="url(#eg2)" stroke-width="2.2" opacity="0.85"/>`
|
||||||
paths += `<path d="${d}" fill="none" stroke="#3ddc97" stroke-width="2.2" stroke-dasharray="5 20" opacity="0.8" style="animation:dashmove 1.1s linear infinite"/>`
|
paths += `<path d="${d}" fill="none" stroke="var(--st-work)" stroke-width="2.2" stroke-dasharray="5 20" opacity="0.8" style="animation:dashmove 1.1s linear infinite"/>`
|
||||||
pulses += `<circle r="3.4" fill="#eafff6"><animateMotion dur="2s" repeatCount="indefinite" rotate="auto"><mpath href="#${pathId}"/></animateMotion></circle>`
|
pulses += `<circle r="3.4" fill="var(--st-work)"><animateMotion dur="2s" repeatCount="indefinite" rotate="auto"><mpath href="#${pathId}"/></animateMotion></circle>`
|
||||||
} else if (e.kind === 'flow') {
|
} else if (e.kind === 'flow') {
|
||||||
// Inactive flow
|
// Inactive flow
|
||||||
paths += `<path id="${pathId}" d="${d}" fill="none" stroke="url(#eg2)" stroke-width="1.8" opacity="0.45"/>`
|
paths += `<path id="${pathId}" d="${d}" fill="none" stroke="url(#eg2)" stroke-width="1.8" opacity="0.45"/>`
|
||||||
pulses += `<circle r="2.8" fill="#c9b8ff" opacity="0.7"><animateMotion dur="3s" repeatCount="indefinite"><mpath href="#${pathId}"/></animateMotion></circle>`
|
pulses += `<circle r="2.8" fill="var(--a-mid)" opacity="0.7"><animateMotion dur="3s" repeatCount="indefinite"><mpath href="#${pathId}"/></animateMotion></circle>`
|
||||||
} else {
|
} else {
|
||||||
// Orchestration (Iris → Agent)
|
// Orchestration (Iris → Agent)
|
||||||
const targetAgent = props.agents.find(a => a.id === e.b)
|
const targetAgent = props.agents.find(a => a.id === e.b)
|
||||||
const op = targetAgent && isActive(targetAgent.status) ? 0.52 : 0.34
|
const op = targetAgent && isActive(targetAgent.status) ? 0.52 : 0.34
|
||||||
paths += `<path d="${d}" fill="none" stroke="#8b7cff" stroke-width="1.45" stroke-dasharray="2 6" opacity="${op}"/>`
|
paths += `<path d="${d}" fill="none" stroke="var(--a-mid)" stroke-width="1.45" stroke-dasharray="2 6" opacity="${op}"/>`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -336,7 +336,7 @@ function handleReset() {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: var(--grad);
|
background: var(--grad);
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: var(--tx);
|
||||||
font-family: 'Manrope', sans-serif;
|
font-family: 'Manrope', sans-serif;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ watch(
|
|||||||
.iris-av :deep(svg) {
|
.iris-av :deep(svg) {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
color: #fff;
|
color: var(--tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iris-name {
|
.iris-name {
|
||||||
@@ -196,7 +196,7 @@ watch(
|
|||||||
padding: 24px 0;
|
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 {
|
.chat-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -222,7 +222,7 @@ watch(
|
|||||||
|
|
||||||
.bubble.me {
|
.bubble.me {
|
||||||
background: var(--grad);
|
background: var(--grad);
|
||||||
color: #fff;
|
color: var(--tx);
|
||||||
border-bottom-right-radius: 5px;
|
border-bottom-right-radius: 5px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
box-shadow: var(--glow-purple);
|
box-shadow: var(--glow-purple);
|
||||||
@@ -313,6 +313,6 @@ watch(
|
|||||||
.send :deep(svg) {
|
.send :deep(svg) {
|
||||||
width: 17px;
|
width: 17px;
|
||||||
height: 17px;
|
height: 17px;
|
||||||
color: #fff;
|
color: var(--tx);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function prioLabel(p: TaskItem['priority']): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function prioColor(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 {
|
function dotClass(s: TaskItem['status']): string {
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* ActivityTimeline — Wiederverwendbare Aktivitäts-Feed-Komponente
|
||||||
|
*
|
||||||
|
* Standardisierte Timeline-Darstellung für Task-Aktivität,
|
||||||
|
* Agent-Aktivität und allgemeine Ereignis-Feeds.
|
||||||
|
* Verwendet ausschließlich nexus-tokens.css Variablen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ActivityEntry {
|
||||||
|
id?: string
|
||||||
|
message: string
|
||||||
|
timestamp?: string
|
||||||
|
type?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
entries: ActivityEntry[]
|
||||||
|
loading?: boolean
|
||||||
|
emptyLabel?: string
|
||||||
|
}>(), {
|
||||||
|
emptyLabel: 'Noch keine Aktivität',
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatDate(date?: string | null): string {
|
||||||
|
if (!date) return ''
|
||||||
|
return new Date(date).toLocaleString('de-DE', {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'short',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="activity-timeline">
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="loading" class="activity-empty">Lade…</div>
|
||||||
|
|
||||||
|
<!-- Empty -->
|
||||||
|
<div v-else-if="!entries.length" class="activity-empty">{{ emptyLabel }}</div>
|
||||||
|
|
||||||
|
<!-- Entries -->
|
||||||
|
<article v-for="(entry, index) in entries" :key="entry.id ?? index" class="activity-item">
|
||||||
|
<div class="activity-dot"></div>
|
||||||
|
<div class="activity-body">
|
||||||
|
<div class="activity-message">{{ entry.message }}</div>
|
||||||
|
<div v-if="entry.timestamp" class="activity-time">
|
||||||
|
{{ formatDate(entry.timestamp) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.activity-timeline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-empty {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tx-3);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'Manrope', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 10px minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
margin-top: 5px;
|
||||||
|
background: var(--grad);
|
||||||
|
box-shadow: 0 0 0 4px rgba(124, 108, 255, .12);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-body {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-message {
|
||||||
|
color: var(--tx);
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-time {
|
||||||
|
color: var(--tx-3);
|
||||||
|
font-size: 10.5px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* Badge — Nexus V2 Status- und Label-Badge
|
||||||
|
*
|
||||||
|
* Erweitert: zusätzliche nexus-token-basierte Varianten für
|
||||||
|
* Agenten-Rollen, Status-Pills und Prioritäten.
|
||||||
|
* Original shadcn-Varianten bleiben erhalten.
|
||||||
|
*/
|
||||||
import type { HTMLAttributes } from 'vue'
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -8,10 +15,25 @@ const badgeVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
// Original shadcn
|
||||||
default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
|
default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
|
||||||
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
destructive: 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
|
destructive: 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
|
||||||
outline: 'text-foreground',
|
outline: 'text-foreground',
|
||||||
|
|
||||||
|
// Nexus V2 — Token-basierte Varianten (Farben aus nexus-tokens.css)
|
||||||
|
work: 'nexus-badge-work',
|
||||||
|
think: 'nexus-badge-think',
|
||||||
|
blocked: 'nexus-badge-blocked',
|
||||||
|
queue: 'nexus-badge-queue',
|
||||||
|
done: 'nexus-badge-done',
|
||||||
|
review: 'nexus-badge-review',
|
||||||
|
iris: 'nexus-badge-iris',
|
||||||
|
bao: 'nexus-badge-bao',
|
||||||
|
agent: 'nexus-badge-agent',
|
||||||
|
priorityHigh: 'nexus-badge-prio-high',
|
||||||
|
priorityMed: 'nexus-badge-prio-med',
|
||||||
|
priorityLow: 'nexus-badge-prio-low',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -35,3 +57,78 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
<slot />
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Nexus V2 Token-basierte Badge-Varianten */
|
||||||
|
.nexus-badge-work {
|
||||||
|
border-color: rgba(61, 220, 151, .25);
|
||||||
|
background: rgba(34, 197, 94, .12);
|
||||||
|
color: var(--st-work);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-badge-think {
|
||||||
|
border-color: rgba(52, 214, 245, .25);
|
||||||
|
background: rgba(52, 214, 245, .1);
|
||||||
|
color: var(--st-think);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-badge-blocked {
|
||||||
|
border-color: rgba(251, 113, 133, .25);
|
||||||
|
background: rgba(244, 63, 94, .12);
|
||||||
|
color: var(--st-block);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-badge-queue {
|
||||||
|
border-color: rgba(251, 191, 36, .25);
|
||||||
|
background: rgba(251, 191, 36, .12);
|
||||||
|
color: var(--st-queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-badge-done {
|
||||||
|
border-color: rgba(34, 197, 94, .25);
|
||||||
|
background: rgba(34, 197, 94, .12);
|
||||||
|
color: var(--st-work);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-badge-review {
|
||||||
|
border-color: rgba(249, 115, 22, .25);
|
||||||
|
background: rgba(249, 115, 22, .12);
|
||||||
|
color: var(--clr-review);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-badge-iris {
|
||||||
|
border-color: rgba(147, 51, 234, .25);
|
||||||
|
background: rgba(147, 51, 234, .12);
|
||||||
|
color: var(--clr-iris);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-badge-bao {
|
||||||
|
border-color: rgba(59, 130, 246, .25);
|
||||||
|
background: rgba(59, 130, 246, .12);
|
||||||
|
color: var(--clr-bao);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-badge-agent {
|
||||||
|
border-color: rgba(16, 185, 129, .2);
|
||||||
|
background: rgba(16, 185, 129, .12);
|
||||||
|
color: var(--clr-agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-badge-prio-high {
|
||||||
|
border-color: var(--st-block);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--st-block);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-badge-prio-med {
|
||||||
|
border-color: var(--st-queue);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--st-queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nexus-badge-prio-low {
|
||||||
|
border-color: var(--a-blue);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--a-blue);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,16 +1,54 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* Card — Nexus V2 glass-panel Karte
|
||||||
|
*
|
||||||
|
* Verwendet nexus-tokens.css als Single Source of Truth.
|
||||||
|
* Hintergrund-kompatibel mit shadcn-Card-Props.
|
||||||
|
* Wrapped content in .glass-panel styles aus tokens.css.
|
||||||
|
*/
|
||||||
import type { HTMLAttributes } from 'vue'
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: HTMLAttributes['class']
|
class?: HTMLAttributes['class']
|
||||||
|
/** Variante: 'glass' (default) | 'raised' | 'subtle' */
|
||||||
|
variant?: 'glass' | 'raised' | 'subtle'
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
variant: 'glass',
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="cn('rounded-xl border bg-card text-card-foreground shadow', props.class)">
|
<div
|
||||||
|
:class="cn(
|
||||||
|
'rounded-xl border',
|
||||||
|
{
|
||||||
|
'glass-panel': variant === 'glass',
|
||||||
|
'card-raised': variant === 'raised',
|
||||||
|
'card-subtle': variant === 'subtle',
|
||||||
|
},
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* glass-panel ist in nexus-tokens.css definiert */
|
||||||
|
|
||||||
|
.card-raised {
|
||||||
|
background: var(--glass-2);
|
||||||
|
border: 1px solid var(--line-2);
|
||||||
|
border-radius: var(--r);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtle {
|
||||||
|
background: rgba(255, 255, 255, .02);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* EmptyState — Standardisierte Leerzustands-Anzeige
|
||||||
|
*
|
||||||
|
* Konsistente Darstellung für "keine Daten"-Zustände im Dashboard,
|
||||||
|
* TaskBoard und allen V2-Views.
|
||||||
|
* Verwendet ausschließlich nexus-tokens.css Variablen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
/** Icon-Klasse (optional, z.B. für SVG-Nutzung) */
|
||||||
|
icon?: string
|
||||||
|
/** Primärer Text */
|
||||||
|
title?: string
|
||||||
|
/** Sekundärer Erklärungstext */
|
||||||
|
description?: string
|
||||||
|
/** Kompakte Darstellung (inline-flex) */
|
||||||
|
compact?: boolean
|
||||||
|
}>(), {
|
||||||
|
title: 'Keine Einträge',
|
||||||
|
compact: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="empty-state" :class="{ compact }">
|
||||||
|
<div v-if="icon" class="empty-icon" v-html="icon"></div>
|
||||||
|
<slot name="icon">
|
||||||
|
<div v-if="!icon" class="empty-icon-default">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
|
||||||
|
<rect x="4" y="6" width="20" height="16" rx="3" stroke="currentColor" stroke-width="1.2" />
|
||||||
|
<line x1="10" y1="12" x2="18" y2="12" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||||
|
<line x1="10" y1="16" x2="15" y2="16" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
<div v-if="!compact" class="empty-title">{{ title }}</div>
|
||||||
|
<div v-if="description" class="empty-desc">{{ description }}</div>
|
||||||
|
<div v-if="$slots.action" class="empty-action">
|
||||||
|
<slot name="action" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state.compact {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon,
|
||||||
|
.empty-icon-default {
|
||||||
|
color: var(--tx-3);
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact .empty-icon,
|
||||||
|
.compact .empty-icon-default {
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon-default {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--tx-2);
|
||||||
|
font-family: 'Manrope', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact .empty-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--tx-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tx-3);
|
||||||
|
font-family: 'Manrope', sans-serif;
|
||||||
|
max-width: 280px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-action {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* PageHeading — Standardisierter Seiten-Header für V2 Views
|
||||||
|
*
|
||||||
|
* Bietet konsistentes grad-text Styling, Eyebrow/Subtitle und Action-Slot.
|
||||||
|
* Verwendet ausschließlich nexus-tokens.css Variablen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
/** Gradient-Text Überschrift */
|
||||||
|
title: string
|
||||||
|
/** Kleiner Eyebrow-Text über der Überschrift (violett) */
|
||||||
|
eyebrow?: string
|
||||||
|
/** Subtitle unter der Überschrift */
|
||||||
|
subtitle?: string
|
||||||
|
}>(), {})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="page-heading">
|
||||||
|
<div>
|
||||||
|
<p v-if="eyebrow || $slots.eyebrow" class="eyebrow">
|
||||||
|
<slot name="eyebrow">{{ eyebrow }}</slot>
|
||||||
|
</p>
|
||||||
|
<h1><span class="grad-text">{{ title }}</span></h1>
|
||||||
|
<p v-if="subtitle" class="board-subtitle">{{ subtitle }}</p>
|
||||||
|
<slot name="meta" />
|
||||||
|
</div>
|
||||||
|
<div v-if="$slots.actions" class="heading-actions">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-heading h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
color: var(--a-mid);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-subtitle {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tx-3);
|
||||||
|
font-family: 'Manrope', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grad-text {
|
||||||
|
background: var(--grad);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* SectionHeader — Titelzeile für Sektionen/Spalten im Dashboard V2
|
||||||
|
*
|
||||||
|
* Kapselt wiederholtes Muster: dot + label + count aus TaskBoard-Spalten
|
||||||
|
* und Iris-Panel-Sektionen. Verwendet ausschließlich Token-Farben.
|
||||||
|
*/
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
label: string
|
||||||
|
/** Status-Farbe des Dots: 'work' | 'think' | 'idle' | 'block' | 'queue' | 'review' | 'iris' | 'bao' */
|
||||||
|
dotColor?: 'work' | 'think' | 'idle' | 'block' | 'queue' | 'review' | 'iris' | 'bao'
|
||||||
|
/** Numerischer Count (rechts) */
|
||||||
|
count?: number
|
||||||
|
/** Variante: 'column' (Spalten-Header) oder 'section' (Iris-Panel) */
|
||||||
|
variant?: 'column' | 'section'
|
||||||
|
}>(), {
|
||||||
|
variant: 'column',
|
||||||
|
})
|
||||||
|
|
||||||
|
function dotColorVar(color: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
work: 'var(--st-work)',
|
||||||
|
think: 'var(--st-think)',
|
||||||
|
idle: 'var(--st-idle)',
|
||||||
|
block: 'var(--st-block)',
|
||||||
|
queue: 'var(--st-queue)',
|
||||||
|
review: 'var(--st-queue)', // orange-ähnlich
|
||||||
|
iris: 'var(--a-purple)',
|
||||||
|
bao: 'var(--a-blue)',
|
||||||
|
}
|
||||||
|
return map[color] || 'var(--st-idle)'
|
||||||
|
}
|
||||||
|
|
||||||
|
function dotRingVar(color: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
work: 'rgba(61, 220, 151, .25)',
|
||||||
|
think: 'rgba(52, 214, 245, .25)',
|
||||||
|
idle: 'rgba(107, 103, 150, .25)',
|
||||||
|
block: 'rgba(251, 113, 133, .25)',
|
||||||
|
queue: 'rgba(251, 191, 36, .25)',
|
||||||
|
review: 'rgba(251, 146, 60, .25)',
|
||||||
|
iris: 'rgba(181, 87, 246, .25)',
|
||||||
|
bao: 'rgba(79, 124, 255, .25)',
|
||||||
|
}
|
||||||
|
return map[color] || 'rgba(107, 103, 150, .25)'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="section-header" :class="variant">
|
||||||
|
<span
|
||||||
|
class="section-dot"
|
||||||
|
:style="{
|
||||||
|
background: dotColorVar(dotColor || 'idle'),
|
||||||
|
boxShadow: `0 0 0 2px ${dotRingVar(dotColor || 'idle')}`,
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
|
<span class="section-label">{{ label }}</span>
|
||||||
|
<span v-if="count !== undefined" class="section-count">{{ count }}</span>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header.column {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header.section {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-dot {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
color: var(--tx-2);
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header.section .section-label {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-count {
|
||||||
|
margin-left: auto;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--glass-2);
|
||||||
|
color: var(--tx-2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* StatusDot — Nexus V2 animierter Status-Indikator
|
||||||
|
*
|
||||||
|
* Kapselt die pulse-keyframes aus nexus-tokens.css.
|
||||||
|
* Status-Farben und Pulse-Animationen stammen ausschließlich aus Tokens.
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* status – 'work' | 'think' | 'idle' | 'block' | 'queue'
|
||||||
|
* size – 'sm' (8px) | 'md' (9px) | 'lg' (12px), default 'md'
|
||||||
|
* pulse – Animation aktiv (default: true für work/think/block)
|
||||||
|
* label – Text-Label rechts neben dem Dot (optional)
|
||||||
|
*/
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
status: 'work' | 'think' | 'idle' | 'block' | 'queue'
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
|
pulse?: boolean
|
||||||
|
label?: string
|
||||||
|
}>(), {
|
||||||
|
size: 'md',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="status-dot-wrapper">
|
||||||
|
<span
|
||||||
|
class="dot"
|
||||||
|
:class="[status, size, { pulse: pulse !== false }]"
|
||||||
|
:aria-label="label || status"
|
||||||
|
role="status"
|
||||||
|
></span>
|
||||||
|
<span v-if="label" class="dot-label">{{ label }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-dot-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.sm { width: 7px; height: 7px; }
|
||||||
|
.dot.md { width: 9px; height: 9px; }
|
||||||
|
.dot.lg { width: 12px; height: 12px; }
|
||||||
|
|
||||||
|
/* Farben aus nexus-tokens.css — Single Source of Truth */
|
||||||
|
.dot.work { background: var(--st-work); }
|
||||||
|
.dot.think { background: var(--st-think); }
|
||||||
|
.dot.idle { background: var(--st-idle); }
|
||||||
|
.dot.block { background: var(--st-block); }
|
||||||
|
.dot.queue { background: var(--st-queue); }
|
||||||
|
|
||||||
|
/* Pulse-Animationen (Keyframes in tokens.css definiert) */
|
||||||
|
.dot.work.pulse { animation: pulse-work 1.8s infinite; box-shadow: 0 0 0 0 rgba(61,220,151,.55); }
|
||||||
|
.dot.think.pulse { animation: pulse-think 1.8s infinite; box-shadow: 0 0 0 0 rgba(52,214,245,.55); }
|
||||||
|
.dot.block.pulse { animation: pulse-block 1.4s infinite; box-shadow: 0 0 0 0 rgba(251,113,133,.55); }
|
||||||
|
|
||||||
|
.dot-label {
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--tx-2);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,18 +7,18 @@ const { toasts, remove } = useToast()
|
|||||||
const typeConfig: Record<string, { icon: any; color: string; bg: string }> = {
|
const typeConfig: Record<string, { icon: any; color: string; bg: string }> = {
|
||||||
success: {
|
success: {
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
color: '#22c55e',
|
color: 'var(--st-work)',
|
||||||
bg: 'rgba(34, 197, 94, 0.10)',
|
bg: 'rgba(61, 220, 151, 0.10)',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
icon: XCircle,
|
icon: XCircle,
|
||||||
color: '#ef4444',
|
color: 'var(--st-block)',
|
||||||
bg: 'rgba(239, 68, 68, 0.10)',
|
bg: 'rgba(251, 113, 133, 0.10)',
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
icon: Info,
|
icon: Info,
|
||||||
color: '#3b82f6',
|
color: 'var(--a-blue)',
|
||||||
bg: 'rgba(59, 130, 246, 0.10)',
|
bg: 'rgba(79, 124, 255, 0.10)',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -76,7 +76,7 @@ const typeConfig: Record<string, { icon: any; color: string; bg: string }> = {
|
|||||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||||
inset 0 1px 0 color-mix(in srgb, var(--toast-color) 12%, transparent);
|
inset 0 1px 0 color-mix(in srgb, var(--toast-color) 12%, transparent);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
color: #e8eaf0;
|
color: var(--tx);
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ const typeConfig: Record<string, { icon: any; color: string; bg: string }> = {
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #6b7385;
|
color: var(--tx-3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
@@ -114,7 +114,7 @@ const typeConfig: Record<string, { icon: any; color: string; bg: string }> = {
|
|||||||
.toast-close:hover {
|
.toast-close:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
color: #e8eaf0;
|
color: var(--tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Transition animations */
|
/* Transition animations */
|
||||||
|
|||||||
@@ -4,25 +4,35 @@ import { type VariantProps, cva } from 'class-variance-authority'
|
|||||||
export { default as Button } from './Button.vue'
|
export { default as Button } from './Button.vue'
|
||||||
|
|
||||||
export const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
// shadcn-original
|
||||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||||
destructive:
|
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||||
'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',
|
||||||
outline:
|
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||||
'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',
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
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: {
|
size: {
|
||||||
default: 'h-9 px-4 py-2',
|
default: 'h-9 px-4 py-2',
|
||||||
sm: 'h-8 rounded-md px-3 text-xs',
|
sm: 'h-8 rounded-md px-3 text-xs',
|
||||||
lg: 'h-10 rounded-md px-8',
|
lg: 'h-10 rounded-md px-8',
|
||||||
icon: 'h-9 w-9',
|
icon: 'h-9 w-9',
|
||||||
|
iconSm: 'h-7 w-7 rounded-md',
|
||||||
|
pill: 'h-7 px-4 rounded-full text-xs',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -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'
|
||||||
Reference in New Issue
Block a user