From 2ee1fe973fbc5d9ecc069be173e6fe2a39a11131 Mon Sep 17 00:00:00 2001 From: DevOps Date: Sat, 11 Jul 2026 12:10:51 +0200 Subject: [PATCH] feat(board): rework quick-peek modal (Linear-style) + board toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modal redesign — compact summary instead of two long cards: - Topbar: breadcrumb + state pill + copyable task-id/link chip; review actions (approve / request changes) live in the modal header too - Main column: title, meta chips (ball, created, last update), compact description, then square summary tiles (done/total with progress, active, open, blocked) + per-agent pills — details stay in Vollansicht - Right sidebar: properties as slim Linear-style rows (status, priority, assignee, due, source) with hover-reveal controls; activity moved next to properties as a compact timeline with a new comment composer (POST tasks/{id}/activity via new store action postTaskActivity) - Unified button system (32px: primary/ghost/approve/changes/icon) - flattenBoard includes nested children again so quick-peek works for child rows inside master cards Board (issue-tracker basics): - Toolbar with search (title/detail/id/child titles) and ball filters: Alle / Du bist dran / Bei Iris / Haengt Co-Authored-By: Claude Fable 5 --- 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 - frontend/src/stores/tasks.ts | 12 + frontend/src/views/TaskBoardView.vue | 527 ++++++++++++------ 19 files changed, 402 insertions(+), 922 deletions(-) delete mode 100644 frontend/src/components/ui/ActivityTimeline.vue delete mode 100644 frontend/src/components/ui/EmptyState.vue delete mode 100644 frontend/src/components/ui/PageHeading.vue delete mode 100644 frontend/src/components/ui/SectionHeader.vue delete mode 100644 frontend/src/components/ui/StatusDot.vue delete 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 9137ec6..6bdf43a 100644 --- a/frontend/src/assets/nexus-tokens.css +++ b/frontend/src/assets/nexus-tokens.css @@ -37,14 +37,6 @@ --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); @@ -86,13 +78,6 @@ --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 ────────────────────────────── */ @@ -148,53 +133,3 @@ /* ── 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 87eb86e..30c6623 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: var(--tx); + color: #fff; 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: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); } +.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); } .m-pill { display: inline-flex; @@ -428,7 +428,7 @@ function avatarLabel() { } .m-bar.work i { - background: linear-gradient(90deg, var(--st-work), var(--st-work)); + background: linear-gradient(90deg, #2bb87f, #3ddc97); box-shadow: var(--glow-work); } @@ -510,7 +510,7 @@ function avatarLabel() { font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.7; - color: var(--st-think); + color: #9fe8fb; min-height: 72px; position: relative; overflow: hidden; @@ -593,7 +593,7 @@ function avatarLabel() { .m-model-btn.active { background: var(--grad); border: none; - color: var(--tx); + color: #fff; box-shadow: var(--glow-purple); } diff --git a/frontend/src/components/dashboard/v2/AgentNode.vue b/frontend/src/components/dashboard/v2/AgentNode.vue index 85ed006..2c1f3e6 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: var(--tx); + color: #fff; box-shadow: var(--glow-purple); } @@ -255,7 +255,7 @@ defineEmits<{ } .node.is-work .nc-bar i { - background: linear-gradient(90deg, var(--st-work), var(--st-work)); + background: linear-gradient(90deg, #2bb87f, #3ddc97); } /* ── Meta ────────────────────────────────────── */ diff --git a/frontend/src/components/dashboard/v2/AlertBar.vue b/frontend/src/components/dashboard/v2/AlertBar.vue index 03bbad6..d1f2146 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: var(--st-block); + color: #fda4b0; 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 38cd241..16775cf 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: var(--tx); + color: #fff; 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 f57e66f..f68f0d7 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: var(--tx); + color: #fff; } .iris-name { @@ -196,7 +196,7 @@ watch( padding: 24px 0; } -.chat-msg-info.error { color: var(--st-block); font-style: normal; } +.chat-msg-info.error { color: #fda4b0; font-style: normal; } .chat-row { display: flex; @@ -222,7 +222,7 @@ watch( .bubble.me { background: var(--grad); - color: var(--tx); + color: #fff; 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: var(--tx); + color: #fff; } diff --git a/frontend/src/components/dashboard/v2/TaskStrip.vue b/frontend/src/components/dashboard/v2/TaskStrip.vue index be231b1..ec1ccca 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' ? 'var(--st-block)' : p === 'medium' ? 'var(--st-queue)' : 'var(--a-blue)' + return p === 'high' ? '#fda4b0' : p === 'medium' ? '#fcd34d' : '#9db6ff' } function dotClass(s: TaskItem['status']): string { diff --git a/frontend/src/components/ui/ActivityTimeline.vue b/frontend/src/components/ui/ActivityTimeline.vue deleted file mode 100644 index c197f1e..0000000 --- a/frontend/src/components/ui/ActivityTimeline.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - - - diff --git a/frontend/src/components/ui/Badge.vue b/frontend/src/components/ui/Badge.vue index 9d02b4a..159941f 100644 --- a/frontend/src/components/ui/Badge.vue +++ b/frontend/src/components/ui/Badge.vue @@ -1,11 +1,4 @@ - - diff --git a/frontend/src/components/ui/EmptyState.vue b/frontend/src/components/ui/EmptyState.vue deleted file mode 100644 index 4b88b78..0000000 --- a/frontend/src/components/ui/EmptyState.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - diff --git a/frontend/src/components/ui/PageHeading.vue b/frontend/src/components/ui/PageHeading.vue deleted file mode 100644 index a94a913..0000000 --- a/frontend/src/components/ui/PageHeading.vue +++ /dev/null @@ -1,79 +0,0 @@ - - - - - diff --git a/frontend/src/components/ui/SectionHeader.vue b/frontend/src/components/ui/SectionHeader.vue deleted file mode 100644 index e2006d6..0000000 --- a/frontend/src/components/ui/SectionHeader.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - - - diff --git a/frontend/src/components/ui/StatusDot.vue b/frontend/src/components/ui/StatusDot.vue deleted file mode 100644 index a507a33..0000000 --- a/frontend/src/components/ui/StatusDot.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - - - diff --git a/frontend/src/components/ui/ToastContainer.vue b/frontend/src/components/ui/ToastContainer.vue index a7a1e3f..6e68191 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: 'var(--st-work)', - bg: 'rgba(61, 220, 151, 0.10)', + color: '#22c55e', + bg: 'rgba(34, 197, 94, 0.10)', }, error: { icon: XCircle, - color: 'var(--st-block)', - bg: 'rgba(251, 113, 133, 0.10)', + color: '#ef4444', + bg: 'rgba(239, 68, 68, 0.10)', }, info: { icon: Info, - color: 'var(--a-blue)', - bg: 'rgba(79, 124, 255, 0.10)', + color: '#3b82f6', + bg: 'rgba(59, 130, 246, 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: var(--tx); + color: #e8eaf0; font-size: 12.5px; line-height: 1.4; } @@ -106,7 +106,7 @@ const typeConfig: Record = { border: none; border-radius: 6px; background: transparent; - color: var(--tx-3); + color: #6b7385; 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: var(--tx); + color: #e8eaf0; } /* Transition animations */ diff --git a/frontend/src/components/ui/button/index.ts b/frontend/src/components/ui/button/index.ts index 9c4dd22..d54ea4e 100644 --- a/frontend/src/components/ui/button/index.ts +++ b/frontend/src/components/ui/button/index.ts @@ -4,35 +4,25 @@ 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-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', + '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', { 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 deleted file mode 100644 index c1a787d..0000000 --- a/frontend/src/components/ui/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -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' diff --git a/frontend/src/stores/tasks.ts b/frontend/src/stores/tasks.ts index b2cd388..7933ae1 100644 --- a/frontend/src/stores/tasks.ts +++ b/frontend/src/stores/tasks.ts @@ -321,6 +321,18 @@ export const useTaskStore = defineStore('tasks', { } }, + /* ── API: Kommentar/Aktivität an Task posten ──── */ + async postTaskActivity(id: string, message: string, type = 'comment') { + const res = await apiFetch(`/api/dashboard/tasks/${id}/activity`, { + method: 'POST', + body: JSON.stringify({ message, type }), + }) + if (!res.ok) { + const body = await res.json().catch(() => ({})) + throw new Error(body.error || `HTTP ${res.status}`) + } + }, + /* ── API: Fetch agent workflow overview ──────── */ async fetchAgentOverview(staleHours = 2) { this.agentOverviewLoading = true diff --git a/frontend/src/views/TaskBoardView.vue b/frontend/src/views/TaskBoardView.vue index 122f320..7e41db1 100644 --- a/frontend/src/views/TaskBoardView.vue +++ b/frontend/src/views/TaskBoardView.vue @@ -13,7 +13,7 @@ * - Waiting section for Iris overview */ import { computed, onBeforeUnmount, onMounted, onUnmounted, reactive, ref, watch } from 'vue' -import { Plus, X, CalendarDays, Clock3, ExternalLink, Link2, ListChecks, Save, AlertTriangle, Eye, Bot, ShieldBan, MessageSquareText, RotateCcw } from '@lucide/vue' +import { Plus, X, ExternalLink, Save, AlertTriangle, Eye, Bot, ShieldBan, RotateCcw, Check, Copy, Send, Search } from '@lucide/vue' import { useRouter } from 'vue-router' import { useAuthStore } from '../stores/auth' import { useTaskStore, type DashboardTaskDto } from '../stores/tasks' @@ -213,13 +213,16 @@ function toDateInputValue(date?: string | null): string { } function flattenBoard() { - return [ + const masters = [ ...taskStore.board.offen, ...taskStore.board.inProgress, ...taskStore.board.review, ...taskStore.board.blocked, ...taskStore.board.done, ] + // Children sind keine eigenen Spalten-Karten mehr — fuer Quick-Peek + // (Klick auf Teilaufgabe) muessen sie hier trotzdem auffindbar sein. + return [...masters, ...masters.flatMap(m => m.childTasks ?? [])] } const allBoardTasks = computed(() => flattenBoard()) @@ -233,13 +236,63 @@ const canSaveDetail = computed(() => detailForm.title.trim().length > 0 && !deta */ const canChangeState = computed(() => authStore.isIris || authStore.isBao) +/* ── Board-Filter (Linear-Style Toolbar) ─────────── */ +const searchQuery = ref('') +const ballFilter = ref<'all' | 'bao' | 'iris' | 'stalled'>('all') +const ballFilters = [ + { key: 'all', label: 'Alle' }, + { key: 'bao', label: 'Du bist dran' }, + { key: 'iris', label: 'Bei Iris' }, + { key: 'stalled', label: 'Hängt' }, +] as const + +function minutesSince(dateStr?: string | null): number { + if (!dateStr) return Infinity + return (Date.now() - new Date(dateStr).getTime()) / 60000 +} + +function isStalledTask(t: DashboardTaskDto): boolean { + if (t.state.toLowerCase() !== 'in progress') return false + return minutesSince(t.lastActivityAt ?? t.updatedAt) > STALL_THRESHOLD_MIN +} + +/** Ball = wer als Naechstes handeln muss (gleiche Logik wie BoardCard). */ +function ballOf(t: DashboardTaskDto): string | null { + const s = t.state.toLowerCase() + if (s === 'review') return 'bao' + if (s === 'done') return null + if (s === 'backlog') return t.expectedFrom || 'iris' + return t.expectedFrom || t.assignedTo || 'iris' +} + +function matchesFilters(t: DashboardTaskDto): boolean { + const q = searchQuery.value.trim().toLowerCase() + if (q) { + const hay = [t.title, t.detail ?? '', t.id, ...(t.childTasks ?? []).map(c => c.title)] + .join(' ') + .toLowerCase() + if (!hay.includes(q)) return false + } + if (ballFilter.value === 'bao') return ballOf(t) === 'bao' + if (ballFilter.value === 'iris') return ballOf(t) === 'iris' + if (ballFilter.value === 'stalled') { + const children = t.childTasks ?? [] + return children.length ? children.some(isStalledTask) : isStalledTask(t) + } + return true +} + +function filterTasks(list: DashboardTaskDto[]): DashboardTaskDto[] { + return list.filter(matchesFilters) +} + /* Spalten-Konfiguration — Board zeigt nur Master-Tasks (Children nested in der Karte). */ const columns = computed(() => [ - { key: 'offen', name: 'Offen', tasks: taskStore.board.offen, dot: 'var(--st-queue)', ring: 'rgba(251,191,36,.25)' }, - { key: 'inProgress', name: 'In Bearbeitung', tasks: taskStore.board.inProgress, dot: 'var(--st-work)', ring: 'rgba(61,220,151,.25)' }, - { key: 'review', name: 'Review', tasks: taskStore.board.review, dot: '#fb923c', ring: 'rgba(251,146,60,.25)' }, - { key: 'done', name: 'Erledigt', tasks: taskStore.board.done, dot: 'var(--st-work)', ring: 'rgba(61,220,151,.25)' }, - { key: 'blocked', name: 'Blockiert', tasks: taskStore.board.blocked, dot: 'var(--st-block)', ring: 'rgba(251,113,133,.25)' }, + { key: 'offen', name: 'Offen', tasks: filterTasks(taskStore.board.offen), dot: 'var(--st-queue)', ring: 'rgba(251,191,36,.25)' }, + { key: 'inProgress', name: 'In Bearbeitung', tasks: filterTasks(taskStore.board.inProgress), dot: 'var(--st-work)', ring: 'rgba(61,220,151,.25)' }, + { key: 'review', name: 'Review', tasks: filterTasks(taskStore.board.review), dot: '#fb923c', ring: 'rgba(251,146,60,.25)' }, + { key: 'done', name: 'Erledigt', tasks: filterTasks(taskStore.board.done), dot: 'var(--st-work)', ring: 'rgba(61,220,151,.25)' }, + { key: 'blocked', name: 'Blockiert', tasks: filterTasks(taskStore.board.blocked), dot: 'var(--st-block)', ring: 'rgba(251,113,133,.25)' }, ]) function hydrateDetailForm(task: BoardTask | null) { @@ -335,6 +388,7 @@ async function openQuickPeek(taskId: string) { } function closeDetailPanel() { + commentText.value = '' showDetailPanel.value = false selectedTaskId.value = null childTasks.value = [] @@ -392,6 +446,55 @@ async function saveTaskDetail() { } } +/* ── Detail-Modal: Kommentar, Link kopieren, Child-Summary ── */ +const commentText = ref('') +const commentSending = ref(false) +const linkCopied = ref(false) + +async function submitComment() { + const text = commentText.value.trim() + if (!text || !selectedTask.value || commentSending.value) return + commentSending.value = true + try { + await taskStore.postTaskActivity(selectedTask.value.id, text) + commentText.value = '' + taskActivity.value = await taskStore.fetchTaskActivity(selectedTask.value.id) + } catch (_err) { + detailError.value = 'Kommentar konnte nicht gespeichert werden' + } finally { + commentSending.value = false + } +} + +function copyTaskLink() { + if (!selectedTask.value) return + navigator.clipboard?.writeText(`${window.location.origin}/tasks/${selectedTask.value.id}`) + linkCopied.value = true + setTimeout(() => { linkCopied.value = false }, 1500) +} + +const childStats = computed(() => { + const list = childTasks.value + const done = list.filter(c => c.state === 'Done').length + const active = list.filter(c => c.state === 'In progress').length + const blocked = list.filter(c => c.state === 'Blocked').length + return { total: list.length, done, active, blocked, open: list.length - done - active - blocked } +}) + +const childAgentSummary = computed(() => { + const map = new Map() + for (const c of childTasks.value) { + const key = c.assignedTo || 'unassigned' + const cur = map.get(key) ?? { done: 0, total: 0 } + cur.total++ + if (c.state === 'Done') cur.done++ + map.set(key, cur) + } + return [...map.entries()].map(([agent, stats]) => ({ agent, ...stats })) +}) + +const recentActivity = computed(() => taskActivity.value.slice(0, 8)) + function onGlobalKeydown(event: KeyboardEvent) { if (event.key === 'Escape' && showDetailPanel.value) { closeDetailPanel() @@ -456,6 +559,23 @@ onUnmounted(() => { + +
+ +
+ +
+
+
@@ -682,161 +802,145 @@ onUnmounted(() => {
+ +
@@ -954,63 +1058,126 @@ select:disabled { opacity: .45; cursor: not-allowed; } .form-error, .detail-flash.error { color: var(--st-block); font-size: 11px; margin: 0; font-family: 'Manrope', sans-serif; } .detail-flash.success { color: #86efac; font-size: 11px; margin: 0; } .modal-actions, .detail-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; } -.btn-cancel { padding: 7px 14px; border: 1px solid var(--line); border-radius: var(--r-sm, 10px); background: transparent; color: var(--tx-2); font-size: 12px; font-weight: 500; font-family: 'Manrope', sans-serif; cursor: pointer; transition: background .15s, color .15s; } +.btn-cancel { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 0 13px; border: 1px solid var(--line); border-radius: 9px; background: transparent; color: var(--tx-2); font-size: 12px; font-weight: 600; font-family: 'Manrope', sans-serif; cursor: pointer; transition: background .15s, color .15s; } .btn-cancel:hover { background: rgba(124,108,255,.08); color: var(--tx); } -.btn-ghost { padding: 7px 14px; border: 1px solid var(--line); border-radius: var(--r-sm, 10px); background: transparent; color: var(--tx-2); font-size: 12px; font-weight: 500; font-family: 'Manrope', sans-serif; cursor: pointer; transition: background .15s, color .15s; display: inline-flex; align-items: center; gap: 6px; } +.btn-ghost { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 32px; padding: 0 13px; border: 1px solid var(--line); border-radius: 9px; background: transparent; color: var(--tx-2); font-size: 12px; font-weight: 600; font-family: 'Manrope', sans-serif; cursor: pointer; transition: background .15s, color .15s; } .btn-ghost:hover { background: rgba(124,108,255,.08); color: var(--tx); } -.btn-submit { padding: 7px 18px; border: none; border-radius: var(--r-sm, 10px); background: var(--grad); color: #fff; font-size: 12px; font-weight: 600; font-family: 'Manrope', sans-serif; cursor: pointer; transition: opacity .15s, transform .15s; box-shadow: var(--glow-purple); display: inline-flex; align-items: center; gap: 6px; } +.btn-submit { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 32px; padding: 0 16px; border: none; border-radius: 9px; background: var(--grad); color: #fff; font-size: 12px; font-weight: 600; font-family: 'Manrope', sans-serif; cursor: pointer; transition: opacity .15s, transform .15s; box-shadow: var(--glow-purple); } .btn-submit:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; } .btn-submit:not(:disabled):hover { opacity: .85; transform: translateY(-1px); } -/* Detail Panel */ -.detail-panel { width: min(1120px, calc(100vw - 48px)); height: min(88vh, 860px); display: flex; flex-direction: column; background: linear-gradient(180deg, rgba(13,11,28,.97), rgba(10,9,24,.96)); border: 1px solid rgba(124,108,255,.18); border-radius: 22px; box-shadow: 0 28px 90px rgba(0,0,0,.45); overflow: hidden; } -.detail-topbar { display: flex; align-items: center; justify-content: space-between; padding: 18px 22px; border-bottom: 1px solid var(--line); } -.detail-breadcrumb { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--tx-3); } -.detail-content { display: grid; grid-template-columns: minmax(0, 1fr) 320px; min-height: 0; flex: 1; } -.detail-main { padding: 24px; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; } -.detail-sidebar { padding: 24px; border-left: 1px solid var(--line); background: rgba(255,255,255,.02); display: flex; flex-direction: column; gap: 14px; } -.detail-title-block { display: flex; flex-direction: column; gap: 12px; } -.detail-state-pill { width: fit-content; border-radius: 999px; padding: 5px 10px; font-size: 11px; font-weight: 700; letter-spacing: .03em; border: 1px solid transparent; } +/* ── Toolbar (Suche + Ball-Filter) ── */ +.board-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.tb-search { display: flex; align-items: center; gap: 8px; flex: 0 1 320px; min-width: 200px; height: 34px; padding: 0 12px; border: 1px solid var(--line); border-radius: 10px; background: rgba(124,108,255,.05); color: var(--tx-3); transition: border-color .15s; } +.tb-search:focus-within { border-color: var(--a-mid); } +.tb-search input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; color: var(--tx); font-size: 12.5px; font-family: 'Manrope', sans-serif; } +.tb-clear { border: none; background: transparent; color: var(--tx-3); font-size: 15px; cursor: pointer; padding: 0 2px; } +.tb-clear:hover { color: var(--tx); } +.tb-chips { display: flex; gap: 6px; flex-wrap: wrap; } +.tb-chip { height: 28px; padding: 0 12px; border-radius: 20px; border: 1px solid var(--line); background: transparent; color: var(--tx-2); font-size: 11.5px; font-weight: 600; font-family: 'Manrope', sans-serif; cursor: pointer; transition: background .15s, color .15s, border-color .15s; } +.tb-chip:hover { background: rgba(124,108,255,.08); color: var(--tx); } +.tb-chip.active { background: linear-gradient(90deg, rgba(124,108,255,.22), rgba(124,108,255,.06)); border-color: rgba(124,108,255,.35); color: #fff; } + +/* ── Buttons (einheitliches System) ── */ +.btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 32px; padding: 0 13px; border-radius: 9px; font-size: 12px; font-weight: 600; font-family: 'Manrope', sans-serif; cursor: pointer; border: 1px solid transparent; transition: background .15s, color .15s, filter .15s; } +.btn-primary { border: none; background: var(--grad); color: #fff; box-shadow: var(--glow-purple); } +.btn-primary:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; } +.btn-primary:not(:disabled):hover { filter: brightness(1.08); } +.btn-approve { border-color: rgba(61,220,151,.3); background: rgba(61,220,151,.14); color: var(--st-work); } +.btn-approve:hover { background: rgba(61,220,151,.24); } +.btn-changes { border-color: rgba(251,146,60,.3); background: rgba(251,146,60,.12); color: #fdba74; } +.btn-changes:hover { background: rgba(251,146,60,.22); } +.icon-btn { width: 30px; height: 30px; display: grid; place-items: center; border: none; border-radius: 8px; background: transparent; color: var(--tx-3); cursor: pointer; transition: background .15s, color .15s; } +.icon-btn:hover { background: rgba(124,108,255,.1); color: var(--tx); } + +/* ── Detail Panel (Quick Peek — kompakte Zusammenfassung) ── */ +.detail-panel { width: min(880px, calc(100vw - 40px)); max-height: min(84vh, 780px); display: flex; flex-direction: column; background: linear-gradient(180deg, rgba(15,13,32,.97), rgba(10,9,24,.97)); border: 1px solid rgba(124,108,255,.18); border-radius: 18px; box-shadow: 0 28px 90px rgba(0,0,0,.5); overflow: hidden; } +.detail-topbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--line); flex: 0 0 auto; } +.detail-crumb { display: flex; align-items: center; gap: 8px; min-width: 0; } +.crumb-root { font-size: 11px; text-transform: uppercase; letter-spacing: .07em; color: var(--tx-3); white-space: nowrap; } +.crumb-sep { color: var(--tx-3); font-size: 11px; } +.id-chip { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 9px; border-radius: 7px; border: 1px solid var(--line); background: rgba(124,108,255,.06); color: var(--tx-3); font-family: 'JetBrains Mono', monospace; font-size: 10.5px; cursor: pointer; transition: color .15s, border-color .15s; } +.id-chip:hover { color: var(--tx); border-color: var(--line-2); } +.detail-topbar-actions { display: flex; align-items: center; gap: 7px; flex: 0 0 auto; } + +.detail-state-pill { border-radius: 999px; padding: 3px 9px; font-size: 10.5px; font-weight: 700; letter-spacing: .03em; border: 1px solid transparent; white-space: nowrap; } .detail-state-pill.is-backlog { color: #fde68a; background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.25); } .detail-state-pill.is-progress { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); } .detail-state-pill.is-review { color: #fdba74; background: rgba(249,115,22,.12); border-color: rgba(249,115,22,.25); } .detail-state-pill.is-blocked { color: #fda4af; background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.25); } .detail-state-pill.is-done { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); } -.detail-title-input { background: transparent; border: 1px solid transparent; border-radius: 12px; padding: 0; color: var(--tx); font-family: 'Space Grotesk', sans-serif; font-size: 31px; font-weight: 700; letter-spacing: -0.03em; outline: none; } -.detail-meta-row { display: flex; flex-wrap: wrap; gap: 14px; color: var(--tx-3); font-size: 11.5px; } -.detail-meta-row span { display: inline-flex; align-items: center; gap: 5px; } -.meta-agent-tag { color: #c084fc; } -.meta-expected { color: #a78bfa; } -.detail-progress-banner { padding: 10px 12px; border-radius: 12px; background: rgba(124,108,255,.08); border: 1px solid rgba(124,108,255,.14); color: var(--tx-2); font-size: 12px; } -.detail-section { background: rgba(255,255,255,.02); border: 1px solid var(--line); border-radius: 16px; padding: 16px; } -.detail-section-header, .sidebar-heading { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; color: var(--tx-2); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; } -.detail-textarea { width: 100%; min-height: 180px; border-radius: 12px; border: 1px solid var(--line); background: rgba(10,9,24,.55); color: var(--tx); padding: 14px; font-size: 14px; line-height: 1.6; outline: none; box-sizing: border-box; } -.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } -.detail-stack, .activity-list { display: flex; flex-direction: column; gap: 10px; } -.mini-card { border: 1px solid var(--line); border-radius: 12px; padding: 12px; background: rgba(10,9,24,.45); } -.mini-card-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; } -.mini-title { font-size: 13px; font-weight: 600; color: var(--tx); } -.mini-state { font-size: 10px; color: var(--tx-3); text-transform: uppercase; letter-spacing: .05em; } -.mini-copy { margin: 8px 0 0; font-size: 11.5px; color: var(--tx-2); line-height: 1.5; } -.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: linear-gradient(135deg, #8b5cf6, #3b82f6); box-shadow: 0 0 0 4px rgba(124,108,255,.12); } -.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; } -.sidebar-card { border: 1px solid var(--line); border-radius: 16px; padding: 16px; background: rgba(10,9,24,.45); } -.sidebar-card.subtle { background: rgba(255,255,255,.02); } -.sidebar-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } -.sidebar-field:last-child { margin-bottom: 0; } -.slim { min-height: 38px; } -.snapshot-list { display: flex; flex-direction: column; gap: 12px; margin: 0; } -.snapshot-list div { display: flex; justify-content: space-between; gap: 12px; } -.snapshot-list dt { color: var(--tx-3); font-size: 11px; } -.snapshot-list dd { margin: 0; color: var(--tx); font-size: 12px; text-align: right; } + +.detail-content { display: grid; grid-template-columns: minmax(0, 1fr) 292px; min-height: 0; flex: 1; } +.detail-main { padding: 18px 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; } +.detail-title-input { background: transparent; border: none; padding: 0; color: var(--tx); font-family: 'Space Grotesk', sans-serif; font-size: 23px; font-weight: 700; letter-spacing: -0.02em; outline: none; width: 100%; } +.detail-title-input:focus { box-shadow: none; } + +.detail-chips { display: flex; flex-wrap: wrap; gap: 6px; } +.meta-chip { display: inline-flex; align-items: center; gap: 4px; height: 22px; padding: 0 9px; border-radius: 20px; font-size: 10.5px; font-weight: 600; background: rgba(124,108,255,.09); border: 1px solid rgba(124,108,255,.16); color: var(--tx-2); font-family: 'Manrope', sans-serif; } +.meta-chip.dim { background: transparent; border-color: var(--line); color: var(--tx-3); font-weight: 500; } +.meta-chip.ball-bao { background: rgba(59,130,246,.14); border-color: rgba(59,130,246,.3); color: #60a5fa; } +.meta-chip.ball-iris { background: rgba(147,51,234,.14); border-color: rgba(147,51,234,.3); color: #c084fc; } + +.detail-desc { width: 100%; min-height: 96px; max-height: 220px; border-radius: 11px; border: 1px solid var(--line); background: rgba(10,9,24,.5); color: var(--tx); padding: 11px 13px; font-size: 12.5px; line-height: 1.6; outline: none; resize: vertical; box-sizing: border-box; font-family: 'Manrope', sans-serif; transition: border-color .15s; } +.detail-desc:focus { border-color: var(--a-mid); box-shadow: none; } + +/* Summary-Kacheln (quadratisch) */ +.tile-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } +.tile { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; aspect-ratio: 1.15 / 1; border: 1px solid var(--line); border-radius: 13px; background: rgba(28,24,64,.28); padding: 10px; } +.tile-num { font-family: 'Space Grotesk', sans-serif; font-size: 26px; font-weight: 700; color: var(--tx); line-height: 1; } +.tile-num small { font-size: 14px; color: var(--tx-3); font-weight: 600; } +.tile-label { font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: var(--tx-3); font-weight: 600; text-align: center; } +.tile-track { width: 70%; height: 4px; border-radius: 2px; background: var(--space-3); overflow: hidden; margin-top: 4px; } +.tile-fill { height: 100%; background: var(--grad); } +.t-active { color: var(--st-think); } +.t-open { color: var(--st-queue); } +.t-blocked { color: var(--st-block); } + +.agent-summary { display: flex; flex-wrap: wrap; gap: 6px; } +.agent-pill { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 11px; border-radius: 20px; border: 1px solid var(--line); background: rgba(16,185,129,.07); color: var(--tx-2); font-size: 11px; font-family: 'Manrope', sans-serif; cursor: pointer; transition: border-color .15s, color .15s; } +.agent-pill b { color: var(--tx); font-family: 'JetBrains Mono', monospace; font-size: 10.5px; font-weight: 600; } +.agent-pill:hover { border-color: var(--line-2); color: var(--tx); } + +/* Seitenleiste: Eigenschaften + Aktivität */ +.detail-side { border-left: 1px solid var(--line); background: rgba(255,255,255,.015); padding: 14px 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; } +.side-block { display: flex; flex-direction: column; gap: 2px; } +.side-heading { display: flex; align-items: center; gap: 7px; margin-bottom: 7px; color: var(--tx-3); font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; } +.side-count { font-family: 'JetBrains Mono', monospace; font-size: 9.5px; padding: 0 6px; border-radius: 8px; background: var(--glass-2); color: var(--tx-3); } +.prop-row { display: grid; grid-template-columns: 82px minmax(0, 1fr); align-items: center; gap: 8px; min-height: 32px; border-radius: 8px; padding: 0 6px; transition: background .15s; } +.prop-row:hover { background: rgba(124,108,255,.05); } +.prop-label { font-size: 11px; color: var(--tx-3); font-family: 'Manrope', sans-serif; } +.prop-control { width: 100%; height: 28px; padding: 0 7px; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--tx); font-size: 12px; font-family: 'Manrope', sans-serif; outline: none; cursor: pointer; transition: border-color .15s, background .15s; box-sizing: border-box; } +.prop-row:hover .prop-control:not(:disabled) { border-color: var(--line); background: rgba(10,9,24,.5); } +.prop-control:focus { border-color: var(--a-mid); background: rgba(10,9,24,.6); } +.prop-control:disabled { opacity: .5; cursor: not-allowed; } +select.prop-control option { background: var(--space-2); color: var(--tx); } +.prop-static { font-size: 12px; color: var(--tx-2); padding: 0 7px; } + +.side-activity { flex: 1; min-height: 0; } +.mini-timeline { display: flex; flex-direction: column; gap: 9px; overflow-y: auto; max-height: 236px; padding-right: 3px; } +.mini-entry { display: grid; grid-template-columns: 8px minmax(0, 1fr); gap: 8px; } +.mini-dot { width: 6px; height: 6px; border-radius: 50%; margin-top: 5px; background: var(--a-mid); box-shadow: 0 0 0 3px rgba(124,108,255,.12); } +.mini-msg { font-size: 11px; color: var(--tx-2); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +.mini-time { font-size: 9.5px; color: var(--tx-3); margin-top: 2px; font-family: 'JetBrains Mono', monospace; } +.comment-box { display: flex; align-items: center; gap: 6px; margin-top: 10px; padding: 4px 4px 4px 11px; border: 1px solid var(--line); border-radius: 10px; background: rgba(10,9,24,.5); transition: border-color .15s; flex: 0 0 auto; } +.comment-box:focus-within { border-color: var(--a-mid); } +.comment-box input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; color: var(--tx); font-size: 12px; font-family: 'Manrope', sans-serif; } +.send-btn { width: 26px; height: 26px; border-radius: 7px; background: rgba(124,108,255,.14); color: var(--a-mid); flex: 0 0 auto; } +.send-btn:disabled { opacity: .4; cursor: not-allowed; } +.send-btn:not(:disabled):hover { background: rgba(124,108,255,.24); color: var(--tx); } + +/* Footer */ +.detail-footer { display: flex; align-items: center; gap: 8px; padding: 11px 16px; border-top: 1px solid var(--line); flex: 0 0 auto; } +.footer-spacer { flex: 1; } +.detail-flash.error { color: var(--st-block); font-size: 11px; margin: 0; } +.detail-flash.success { color: var(--st-work); font-size: 11px; margin: 0; } + .board-columns::-webkit-scrollbar { height: 9px; } .board-columns::-webkit-scrollbar-thumb { background: rgba(124,108,255,.22); border-radius: 9px; border: 2px solid transparent; background-clip: padding-box; } .board-columns::-webkit-scrollbar-thumb:hover { background: rgba(124,108,255,.4); background-clip: padding-box; } .board-columns::-webkit-scrollbar-track { background: transparent; } -@media (max-width: 1100px) { .detail-content { grid-template-columns: 1fr; } .detail-sidebar { border-left: none; border-top: 1px solid var(--line); } } -@media (max-width: 860px) { .board-columns { overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: thin; } .col { min-width: 260px; } .detail-panel { width: 100vw; height: 100vh; border-radius: 0; } .detail-grid { grid-template-columns: 1fr; } .detail-main, .detail-sidebar { padding: 18px; } .detail-title-input { font-size: 24px; } } +@media (max-width: 1100px) { .detail-content { grid-template-columns: 1fr; } .detail-side { border-left: none; border-top: 1px solid var(--line); } } +@media (max-width: 860px) { .board-columns { overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: thin; } .col { min-width: 260px; } .detail-panel { width: 100vw; max-height: 100vh; height: 100vh; border-radius: 0; } .detail-main { padding: 14px; } .detail-side { padding: 12px 14px; } .detail-title-input { font-size: 19px; } .tile-grid { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 900px) { .iris-panel-grid { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 600px) { .iris-panel-grid { grid-template-columns: 1fr; } }