ui: unify aero chrome surfaces and shared hero across kanban/graph

This commit is contained in:
zenchantlive 2026-02-13 12:17:57 -08:00
parent c8d7f8eb0d
commit e6317594b6
18 changed files with 540 additions and 995 deletions

View file

@ -23,6 +23,7 @@ import { DependencyFlowStrip } from './dependency-flow-strip';
import { GraphNodeCard, type GraphNodeData } from './graph-node-card';
import { GraphSection } from './graph-section';
import { ProjectScopeControls } from '../shared/project-scope-controls';
import { WorkspaceHero } from '../shared/workspace-hero';
import { buildGraphModel, type GraphNode } from '../../lib/graph';
import {
@ -133,6 +134,7 @@ export function DependencyGraphPage({
const requestedEpicId = searchParams.get('epic');
const requestedTaskId = searchParams.get('task');
const requestedTab = searchParams.get('tab');
const heroTitle = activeTab === 'dependencies' ? 'Graph' : 'Tasks';
const kanbanHref = useMemo(() => {
const params = new URLSearchParams();
if (projectScopeMode !== 'single') {
@ -560,10 +562,24 @@ export function DependencyGraphPage({
target: dep.target,
className: linkedToSelection ? 'workflow-edge-selected' : 'workflow-edge-muted',
animated: linkedToSelection,
label: 'BLOCKS',
labelStyle: {
fill: linkedToSelection ? '#e2e8f0' : '#cbd5e1',
fontSize: 10,
fontWeight: 700,
letterSpacing: '0.08em',
},
labelBgPadding: [6, 3],
labelBgBorderRadius: 999,
labelBgStyle: {
fill: 'rgba(2, 6, 23, 0.92)',
stroke: linkedToSelection ? 'rgba(125, 211, 252, 0.35)' : 'rgba(251, 191, 36, 0.25)',
strokeWidth: 1,
},
style: {
stroke: linkedToSelection ? '#7dd3fc' : '#fbbf24',
strokeWidth: linkedToSelection ? 2.5 : 1.8,
opacity: linkedToSelection ? 1 : 0.55,
strokeWidth: linkedToSelection ? 2.8 : 2.1,
opacity: linkedToSelection ? 1 : 0.78,
},
markerEnd: { type: MarkerType.ArrowClosed, color: linkedToSelection ? '#7dd3fc' : '#fbbf24', width: 14, height: 14 },
});
@ -659,36 +675,34 @@ export function DependencyGraphPage({
return (
<main className="mx-auto max-w-[1880px] px-4 py-4 sm:px-6 sm:py-6 lg:px-10">
{/* Page header */}
<header className="mb-6 rounded-3xl border border-white/5 bg-[radial-gradient(circle_at_2%_2%,rgba(56,189,248,0.12),transparent_40%),linear-gradient(170deg,rgba(15,23,42,0.92),rgba(11,12,16,0.95))] px-5 py-5 sm:px-8 sm:py-8 shadow-[0_32px_64px_-16px_rgba(0,0,0,0.6)] backdrop-blur-2xl">
<p className="font-mono text-[10px] uppercase tracking-[0.2em] text-sky-400/70 font-bold">BeadBoard Workspace</p>
<div className="mt-2 flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold tracking-tight text-text-strong sm:text-4xl">Workflow Explorer</h1>
<Link href={kanbanHref} className="rounded-xl border border-white/10 bg-white/5 px-3 py-1.5 text-[10px] font-bold text-text-body transition-all hover:bg-white/10 hover:border-white/20 sm:px-4 sm:text-xs">
&larr; Kanban
</Link>
</div>
<p className="hidden max-w-md text-sm leading-relaxed text-text-muted/90 sm:block">
Epic-driven dependency visualization. Drill into task relationships, triage blockers, and understand downstream impact at a glance.
</p>
</div>
{activeScope ? (
<p className="mt-3 text-xs text-text-muted/90">
<WorkspaceHero
eyebrow="BeadBoard Workspace"
title={heroTitle}
description="Epic-driven dependency visualization. Drill into task relationships, triage blockers, and understand downstream impact at a glance."
action={(
<Link
href={kanbanHref}
className="ui-text rounded-xl border border-white/10 bg-white/5 px-3 py-1.5 text-[10px] font-bold text-text-body transition-all hover:bg-white/10 hover:border-white/20 sm:px-4 sm:text-xs"
>
&larr; Kanban
</Link>
)}
scope={activeScope ? (
<p className="ui-text text-xs text-text-muted/90">
Scope:{' '}
<span className="rounded-md border border-white/10 bg-white/5 px-2 py-0.5 font-mono text-[11px] text-text-body">
<span className="system-data rounded-md border border-white/10 bg-white/5 px-2 py-0.5 text-[11px] text-text-body">
{activeScope.source === 'local' ? 'local workspace' : activeScope.displayPath}
</span>
</p>
) : null}
<div className="mt-3">
) : undefined}
controls={(
<ProjectScopeControls
projectScopeKey={projectScopeKey}
projectScopeMode={projectScopeMode}
projectScopeOptions={projectScopeOptions}
/>
</div>
</header>
)}
/>
{/* Main content area */}
<section className="rounded-[2.5rem] border border-white/5 bg-[linear-gradient(180deg,rgba(255,255,255,0.015),rgba(255,255,255,0.005))] shadow-2xl backdrop-blur-sm overflow-hidden">

View file

@ -85,7 +85,7 @@ export function GraphSection({
</div>
{/* ReactFlow graph viewport */}
<div className="relative h-[60vh] min-h-[35rem] overflow-hidden rounded-2xl border border-white/5 bg-[radial-gradient(circle_at_50%_50%,rgba(15,23,42,0.4),rgba(5,8,15,0.8))] shadow-inner">
<div className="relative h-[60vh] min-h-[24rem] md:min-h-[35rem] overflow-hidden rounded-2xl border border-white/5 bg-[radial-gradient(circle_at_50%_50%,rgba(15,23,42,0.4),rgba(5,8,15,0.8))] shadow-inner">
<ReactFlow
className="workflow-graph-flow"
defaultEdgeOptions={defaultEdgeOptions}

View file

@ -29,10 +29,14 @@ const STATUS_META: Record<(typeof KANBAN_STATUSES)[number], { label: string; dot
};
const STATUS_COLUMN_CLASS: Record<(typeof KANBAN_STATUSES)[number], string> = {
ready: 'bg-sky-500/10',
in_progress: 'bg-amber-500/10',
blocked: 'bg-rose-500/10',
closed: 'bg-emerald-500/10',
ready:
'bg-[radial-gradient(circle_at_0%_0%,rgba(56,189,248,0.2),transparent_62%),linear-gradient(180deg,rgba(22,27,40,0.66),rgba(10,12,20,0.84))]',
in_progress:
'bg-[radial-gradient(circle_at_0%_0%,rgba(251,191,36,0.2),transparent_62%),linear-gradient(180deg,rgba(22,27,40,0.66),rgba(10,12,20,0.84))]',
blocked:
'bg-[radial-gradient(circle_at_0%_0%,rgba(244,63,94,0.2),transparent_62%),linear-gradient(180deg,rgba(22,27,40,0.66),rgba(10,12,20,0.84))]',
closed:
'bg-[radial-gradient(circle_at_0%_0%,rgba(16,185,129,0.2),transparent_62%),linear-gradient(180deg,rgba(22,27,40,0.66),rgba(10,12,20,0.84))]',
};
export function KanbanBoard({
@ -86,8 +90,10 @@ export function KanbanBoard({
key={status}
onDragOver={(event) => event.preventDefault()}
onDrop={(event) => onDropLane(status, event)}
className={`rounded-2xl border border-border-soft ${STATUS_COLUMN_CLASS[status]} p-2.5 transition ${
activeStatus === status ? 'shadow-card' : 'opacity-90'
className={`rounded-2xl border border-white/[0.04] ${STATUS_COLUMN_CLASS[status]} p-2.5 transition shadow-[0_24px_52px_-20px_rgba(0,0,0,0.82),0_10px_26px_-14px_rgba(0,0,0,0.75),inset_0_1px_0_rgba(255,255,255,0.08)] ${
activeStatus === status
? 'shadow-[0_30px_62px_-18px_rgba(0,0,0,0.86),0_0_0_1px_rgba(125,211,252,0.14)]'
: 'opacity-95'
}`}
>
<div className="flex items-center gap-2">
@ -103,11 +109,11 @@ export function KanbanBoard({
}}
className="flex w-full items-center justify-between rounded-lg px-1 py-0.5 text-left"
>
<strong className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-text-body">
<strong className="ui-text inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-text-body">
<span className={`h-2 w-2 rounded-full ${STATUS_META[status].dot}`} />
{STATUS_META[status].label}
</strong>
<span className="font-mono text-xs text-text-muted">{columns[status].length}</span>
<span className="system-data text-xs text-text-muted">{columns[status].length}</span>
</button>
{activeStatus === status ? (
<button
@ -127,6 +133,7 @@ export function KanbanBoard({
<KanbanCard
key={issue.id}
issue={issue}
issues={allIssues}
parentEpic={parentEpicByIssueId.get(issue.id) ?? null}
graphBaseHref={graphBaseHref}
pending={pendingIssueIds.has(issue.id)}
@ -150,11 +157,11 @@ export function KanbanBoard({
key={issue.id}
type="button"
onClick={() => handleExpandAndSelect(status, issue)}
className="max-w-full rounded-lg border border-border-soft bg-surface-muted/60 px-2 py-1 text-left hover:border-border-strong hover:bg-surface-raised/70"
className="max-w-full rounded-lg border border-border-soft bg-gradient-to-b from-surface-muted/50 to-surface-muted/70 px-2 py-1 text-left hover:border-border-strong hover:from-surface-raised/70 hover:to-surface-raised/90 shadow-[0_1px_3px_rgba(0,0,0,0.1)]"
title={issue.title}
>
<div className="font-mono text-[10px] text-text-muted">{issue.id}</div>
<div className="line-clamp-1 text-xs font-medium text-text-body">{issue.title}</div>
<div className="system-data text-[10px] text-text-muted">{issue.id}</div>
<div className="ui-text line-clamp-1 text-sm font-medium text-text-body">{issue.title}</div>
</button>
))}
{columns[status].length > 6 ? (

View file

@ -4,13 +4,14 @@ import Link from 'next/link';
import { motion } from 'framer-motion';
import type { DragEvent } from 'react';
import { formatUpdatedRecency } from '../../lib/kanban';
import { hasOpenBlockers } from '../../lib/kanban';
import type { BeadIssue } from '../../lib/types';
import { Chip } from '../shared/chip';
interface KanbanCardProps {
issue: BeadIssue;
issues?: BeadIssue[];
parentEpic?: { id: string; title: string } | null;
graphBaseHref: string;
selected: boolean;
@ -20,23 +21,61 @@ interface KanbanCardProps {
onSelect: (issue: BeadIssue) => void;
}
function priorityClass(priority: number): string {
switch (priority) {
case 0:
return 'border-rose-300/45 bg-rose-500/20 text-rose-50';
case 1:
return 'border-amber-300/40 bg-amber-500/20 text-amber-50';
case 2:
return 'border-teal-300/40 bg-teal-500/20 text-teal-50';
case 3:
return 'border-slate-300/35 bg-slate-500/22 text-slate-50';
function statusGradient(status: string): string {
switch (status) {
case 'ready':
case 'open':
return 'bg-[linear-gradient(145deg,rgba(34,45,42,0.92)_0%,rgba(24,32,30,0.88)_50%,rgba(18,28,26,0.9)_100%)]';
case 'in_progress':
return 'bg-[linear-gradient(145deg,rgba(42,40,32,0.92)_0%,rgba(32,30,24,0.88)_50%,rgba(26,24,18,0.9)_100%)]';
case 'blocked':
return 'bg-[linear-gradient(145deg,rgba(60,24,30,0.95)_0%,rgba(45,18,24,0.9)_50%,rgba(32,12,16,0.92)_100%)]';
case 'closed':
return 'bg-[linear-gradient(145deg,rgba(28,30,34,0.75)_0%,rgba(22,24,28,0.72)_50%,rgba(18,20,24,0.75)_100%)] opacity-75';
default:
return 'border-slate-400/35 bg-slate-600/20 text-slate-50';
return 'bg-[linear-gradient(145deg,rgba(38,40,48,0.92)_0%,rgba(28,30,36,0.88)_50%,rgba(22,24,30,0.9)_100%)]';
}
}
function statusBorder(status: string): string {
switch (status) {
case 'ready':
case 'open':
return 'border-emerald-500/20';
case 'in_progress':
return 'border-amber-500/20';
case 'blocked':
return 'border-rose-500/20';
case 'closed':
return 'border-rose-500/30';
default:
return 'border-white/[0.06]';
}
}
function statusDotColor(status: string): string {
switch (status) {
case 'ready':
case 'open':
return 'bg-emerald-400';
case 'in_progress':
return 'bg-amber-400';
case 'blocked':
return 'bg-rose-400';
case 'closed':
return 'bg-slate-400';
default:
return 'bg-slate-400';
}
}
function titleColor(status: string): string {
return status === 'closed' ? 'text-text-muted/70' : 'text-text-strong/95';
}
export function KanbanCard({
issue,
issues = [],
parentEpic = null,
graphBaseHref,
selected,
@ -45,13 +84,15 @@ export function KanbanCard({
onNativeDragStart,
onSelect,
}: KanbanCardProps) {
const projectName = (issue as BeadIssue & { project?: { name?: string } }).project?.name ?? null;
const unblocksCount = new Set(
issue.dependencies.filter((dependency) => dependency.type === 'blocks').map((dependency) => dependency.target),
).size;
const blockerCount = issues.length > 0 ? (hasOpenBlockers(issues, issue.id) ?
issue.dependencies.filter(d => d.type === 'blocks').filter(d => {
const blocker = issues.find(i => i.id === d.target);
return blocker && blocker.status !== 'closed';
}).length : 0) : 0;
const selectedClass = selected
? 'border-amber-200/60 bg-surface-raised shadow-card ring-1 ring-amber-200/20'
: 'border-border-soft bg-surface/95 shadow-[0_6px_18px_rgba(4,8,17,0.5)] hover:border-border-strong hover:bg-surface-raised/95';
? 'ring-1 ring-amber-200/20 shadow-[0_24px_48px_-18px_rgba(0,0,0,0.88),0_0_26px_rgba(251,191,36,0.14)]'
: 'shadow-[0_18px_38px_-18px_rgba(0,0,0,0.82),0_6px_18px_-10px_rgba(0,0,0,0.72)] hover:shadow-[0_24px_52px_-16px_rgba(0,0,0,0.9),0_10px_26px_-10px_rgba(0,0,0,0.78)]';
const graphDetailHref = parentEpic
? (() => {
@ -78,52 +119,44 @@ export function KanbanCard({
onSelect(issue);
}
}}
className={`w-full cursor-pointer rounded-2xl border px-3 py-2.5 text-left transition ${selectedClass} ${
className={`w-full cursor-pointer rounded-xl border ${statusBorder(issue.status)} ${statusGradient(issue.status)} px-3.5 py-3 text-left transition duration-200 shadow-[inset_0_1px_0_rgba(255,255,255,0.06)] ${selectedClass} ${
pending ? 'opacity-70' : ''
}`}
>
<div className="font-mono text-[11px] text-text-muted break-all">{issue.id}</div>
{projectName ? (
<div className="mt-1">
<span className="rounded-md border border-sky-300/25 bg-sky-500/10 px-1.5 py-0.5 font-mono text-[10px] text-sky-200">
project: {projectName}
</span>
</div>
) : null}
<div className="mt-1 text-sm font-semibold leading-5 text-text-strong break-words">{issue.title}</div>
<div className="mt-2 flex flex-wrap gap-1.5">
<span
className={`inline-flex items-center rounded-full border px-2 py-1 font-mono text-[11px] font-semibold ${priorityClass(issue.priority)}`}
>
P{issue.priority}
</span>
<Chip>{issue.issue_type}</Chip>
<Chip tone="status">deps {issue.dependencies.length}</Chip>
{unblocksCount > 0 ? <Chip tone="status">Unblocks {unblocksCount}</Chip> : null}
{/* ID row with status dot */}
<div className="flex items-center gap-2">
<span className={`h-1.5 w-1.5 rounded-full ${statusDotColor(issue.status)} shadow-[0_0_6px_currentColor]`} />
<span className="system-data text-[11px] text-text-muted/60">{issue.id}</span>
</div>
<div className="mt-2 break-words font-mono text-xs text-amber-100/90">
{issue.assignee ? `@${issue.assignee}` : 'unassigned'}
{/* Title */}
<div className={`ui-text mt-2 text-sm font-semibold leading-5 break-words ${titleColor(issue.status)}`}>
{issue.title}
</div>
<div className="mt-1 font-mono text-[11px] text-text-muted">{formatUpdatedRecency(issue.updated_at)}</div>
{/* Labels/Tags row */}
<div className="mt-3 flex flex-wrap items-center gap-1.5">
{issue.labels.slice(0, 3).map((label) => (
<Chip key={`${issue.id}-${label}`}>{label}</Chip>
))}
{blockerCount > 0 && (
<Chip tone="status">{blockerCount} Blocker{blockerCount > 1 ? 's' : ''}</Chip>
)}
</div>
{parentEpic ? (
<div className="mt-2">
<div className="mt-2.5">
<Link
href={graphDetailHref ?? graphBaseHref}
className="inline-flex items-center gap-1 rounded-md border border-sky-300/25 bg-sky-500/10 px-2 py-1 font-mono text-[11px] text-sky-200 hover:border-sky-300/45 hover:bg-sky-500/15"
className="system-data inline-flex items-center gap-1 rounded border border-white/8 bg-white/[0.04] px-1.5 py-0.5 text-[10px] text-text-muted/80 hover:text-text-body hover:bg-white/[0.08]"
onClick={(event) => event.stopPropagation()}
>
epic: {parentEpic.title}
{parentEpic.title}
</Link>
</div>
) : null}
{issue.labels.length > 0 ? (
<div className="mt-2 flex flex-wrap gap-1.5">
{issue.labels.slice(0, 3).map((label) => (
<Chip key={`${issue.id}-${label}`}>#{label}</Chip>
))}
</div>
) : null}
{pending ? <div className="mt-2 text-[11px] font-medium text-amber-200">Saving</div> : null}
{pending ? <div className="ui-text mt-2 text-[11px] font-medium text-amber-200">Saving</div> : null}
</motion.article>
);
}

View file

@ -60,7 +60,7 @@ export function KanbanControls({
<option className="ui-option" value="3">P3</option>
<option className="ui-option" value="4">P4</option>
</select>
<label className="inline-flex w-full items-center justify-center gap-2 rounded-xl border border-border-soft bg-surface-muted/60 px-3 py-2 text-sm text-text-body sm:w-auto sm:justify-start">
<label className="ui-text inline-flex w-full items-center justify-center gap-2 rounded-xl border border-border-soft bg-gradient-to-b from-surface-muted/50 to-surface-muted/70 px-3 py-2 text-sm text-text-body sm:w-auto sm:justify-start shadow-[0_1px_3px_rgba(0,0,0,0.1)]">
<input
type="checkbox"
checked={filters.showClosed ?? false}
@ -72,7 +72,7 @@ export function KanbanControls({
<button
type="button"
onClick={onNextActionable}
className="w-full rounded-xl border border-border-soft bg-surface-muted/70 px-3 py-2 text-sm font-semibold text-text-body transition hover:border-border-strong hover:bg-surface-raised sm:w-auto"
className="ui-text w-full rounded-xl border border-border-soft bg-gradient-to-b from-surface-muted/60 to-surface-muted/80 px-3 py-2 text-sm font-semibold text-text-body transition hover:from-surface-muted/75 hover:to-surface-muted/90 shadow-[0_1px_3px_rgba(0,0,0,0.1)] sm:w-auto"
>
Next Actionable
</button>
@ -85,9 +85,7 @@ export function KanbanControls({
<StatPill label="Done" value={stats.done} />
<StatPill label="P0" value={stats.p0} tone={stats.p0 > 0 ? 'critical' : 'default'} />
</motion.div>
{nextActionableFeedback ? (
<p className="text-xs text-text-muted">{nextActionableFeedback}</p>
) : null}
{nextActionableFeedback ? <p className="ui-text text-xs text-text-muted">{nextActionableFeedback}</p> : null}
</section>
);
}

File diff suppressed because one or more lines are too long

View file

@ -22,6 +22,7 @@ import { KanbanBoard } from './kanban-board';
import { KanbanControls } from './kanban-controls';
import { KanbanDetail } from './kanban-detail';
import { ProjectScopeControls } from '../shared/project-scope-controls';
import { WorkspaceHero } from '../shared/workspace-hero';
interface KanbanPageProps {
issues: BeadIssue[];
@ -242,34 +243,42 @@ export function KanbanPage({
return (
<main className="mx-auto min-h-screen max-w-[1800px] px-4 py-4 sm:px-6 sm:py-6">
<header className="mb-4 rounded-2xl border border-border-soft bg-surface/90 px-4 py-4 shadow-card backdrop-blur md:px-5">
<p className="font-mono text-xs uppercase tracking-[0.14em] text-text-muted">BeadBoard</p>
<div className="mt-1 flex flex-wrap items-center gap-3">
<h1 className="text-2xl font-semibold text-text-strong sm:text-3xl">Kanban Dashboard</h1>
<Link href={graphHref} className="rounded-lg border border-border-soft bg-surface-muted/70 px-2.5 py-1 text-xs text-text-body hover:bg-surface-raised">
<WorkspaceHero
eyebrow="BeadBoard Workspace"
title="Swimlanes"
description="Epic-driven dependency visualization. Drill into task relationships, triage blockers, and understand downstream impact at a glance."
className="mb-4"
action={(
<Link
href={graphHref}
className="ui-text rounded-xl border border-white/10 bg-white/5 px-3 py-1.5 text-[10px] font-bold text-text-body transition-all hover:bg-white/10 hover:border-white/20 sm:px-4 sm:text-xs"
>
Open Graph
</Link>
</div>
<p className="mt-2 text-sm text-text-muted">Tracer Bullet 1 from live `.beads/issues.jsonl` on Windows-native paths.</p>
{activeScope ? (
<p className="mt-2 text-xs text-text-muted">
)}
scope={activeScope ? (
<p className="ui-text text-xs text-text-muted/90">
Scope:{' '}
<span className="rounded-md border border-border-soft bg-surface-muted/50 px-2 py-0.5 font-mono text-[11px] text-text-body">
<span className="system-data rounded-md border border-white/10 bg-white/5 px-2 py-0.5 text-[11px] text-text-body">
{activeScope.source === 'local' ? 'local workspace' : activeScope.displayPath}
</span>
</p>
) : null}
<div className="mt-3">
<ProjectScopeControls
projectScopeKey={projectScopeKey}
projectScopeMode={projectScopeMode}
projectScopeOptions={projectScopeOptions}
/>
</div>
{!allowMutations ? (
<p className="mt-2 text-xs text-amber-200/90">Aggregate mode is read-only. Switch to single project mode to edit status/details.</p>
) : null}
</header>
) : undefined}
controls={(
<>
<ProjectScopeControls
projectScopeKey={projectScopeKey}
projectScopeMode={projectScopeMode}
projectScopeOptions={projectScopeOptions}
/>
{!allowMutations ? (
<p className="ui-text mt-2 text-xs text-amber-200/90">
Aggregate mode is read-only. Switch to single project mode to edit status/details.
</p>
) : null}
</>
)}
/>
<KanbanControls
filters={filters}
stats={stats}
@ -278,10 +287,10 @@ export function KanbanPage({
nextActionableFeedback={nextActionableFeedback}
/>
{mutationError ? (
<div className="mt-3 rounded-xl border border-rose-300/40 bg-rose-950/40 px-3 py-2 text-sm text-rose-100">{mutationError}</div>
<div className="ui-text mt-3 rounded-xl border border-rose-300/40 bg-rose-950/40 px-3 py-2 text-sm text-rose-100">{mutationError}</div>
) : null}
<section
className={`mt-3 overflow-hidden rounded-2xl border border-border-soft bg-surface/82 shadow-card ${
className={`mt-3 overflow-hidden rounded-2xl border border-white/5 bg-[linear-gradient(180deg,rgba(255,255,255,0.02),rgba(255,255,255,0.005))] shadow-[0_28px_62px_-18px_rgba(0,0,0,0.8),0_8px_24px_-10px_rgba(0,0,0,0.72)] backdrop-blur-xl ${
showDesktopDetail ? 'lg:grid lg:grid-cols-[minmax(0,1fr)_minmax(22rem,26rem)]' : ''
}`}
>
@ -303,20 +312,20 @@ export function KanbanPage({
/>
</motion.div>
{showDesktopDetail ? (
<div className="hidden border-t border-border-soft bg-surface/72 p-3 lg:block lg:border-l lg:border-t-0">
<aside className="rounded-xl border border-border-soft bg-surface/78 p-3">
<div className="hidden border-t border-white/5 bg-[rgba(9,13,22,0.78)] p-3 lg:block lg:border-l lg:border-t-0">
<aside className="rounded-xl border border-white/6 bg-[linear-gradient(180deg,rgba(42,44,52,0.54),rgba(18,20,30,0.78))] p-3 shadow-[0_18px_42px_-20px_rgba(0,0,0,0.85),inset_0_1px_0_rgba(255,255,255,0.08)]">
<div className="mb-2 flex items-center justify-end gap-2 border-b border-border-soft pb-2">
<button
type="button"
onClick={() => setDesktopDetailMinimized(true)}
className="rounded-md border border-border-soft bg-surface-muted/70 px-2 py-1 text-xs text-text-body"
className="ui-text rounded-md border border-border-soft bg-surface-muted/70 px-2 py-1 text-xs text-text-body"
>
Minimize
</button>
<button
type="button"
onClick={() => setSelectedIssueId(null)}
className="rounded-md border border-border-soft bg-surface-muted/70 px-2 py-1 text-xs text-text-muted"
className="ui-text rounded-md border border-border-soft bg-surface-muted/70 px-2 py-1 text-xs text-text-muted"
>
Clear
</button>
@ -341,7 +350,7 @@ export function KanbanPage({
<div className="fixed inset-0 z-40 lg:hidden">
<button
type="button"
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
className="absolute inset-0 bg-black/82 backdrop-blur-md"
aria-label="Close details"
onClick={() => setMobileDetailOpen(false)}
/>
@ -350,13 +359,13 @@ export function KanbanPage({
animate={{ y: 0, opacity: 1 }}
exit={{ y: 36, opacity: 0 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
className="absolute inset-x-3 bottom-3 top-20 overflow-y-auto rounded-2xl border border-border-soft bg-surface/98 p-3 shadow-panel backdrop-blur-2xl"
className="absolute inset-x-3 bottom-3 top-20 overflow-y-auto rounded-2xl border border-border-soft bg-surface/96 p-3 shadow-panel backdrop-blur-3xl"
>
<div className="mb-2 flex justify-end">
<button
type="button"
onClick={() => setMobileDetailOpen(false)}
className="rounded-lg border border-border-soft bg-surface-muted/70 px-3 py-1 text-xs font-semibold text-text-body"
className="ui-text rounded-lg border border-border-soft bg-surface-muted/70 px-3 py-1 text-xs font-semibold text-text-body"
>
Close
</button>

View file

@ -6,14 +6,18 @@ interface ChipProps {
}
const CHIP_TONE_CLASS: Record<NonNullable<ChipProps['tone']>, string> = {
default: 'border-border-soft bg-surface-muted/75 text-text-body',
status: 'border-zinc-300/30 bg-zinc-500/20 text-zinc-100',
priority: 'border-amber-300/30 bg-amber-500/20 text-amber-50',
default:
'border border-border-soft bg-gradient-to-b from-surface-muted/60 to-surface-muted/85 text-text-body shadow-[0_1px_2px_rgba(0,0,0,0.15)]',
status: 'border border-border-soft/80 bg-gradient-to-b from-zinc-500/15 to-zinc-500/25 text-zinc-100 shadow-[0_1px_2px_rgba(0,0,0,0.12)]',
priority:
'border border-amber-300/25 bg-gradient-to-b from-amber-500/15 to-amber-500/25 text-amber-50 shadow-[0_1px_2px_rgba(0,0,0,0.12)]',
};
export function Chip({ children, tone = 'default' }: ChipProps) {
return (
<span className={`inline-flex items-center rounded-full border px-2 py-1 text-[11px] font-semibold ${CHIP_TONE_CLASS[tone]}`}>
<span
className={`inline-flex items-center rounded-lg border px-2 py-1 text-[11px] font-semibold tracking-wide ${CHIP_TONE_CLASS[tone]}`}
>
{children}
</span>
);

File diff suppressed because one or more lines are too long

View file

@ -8,9 +8,9 @@ export function StatPill({ label, value, tone = 'default' }: StatPillProps) {
const valueToneClass = tone === 'critical' ? 'text-rose-300' : 'text-text-strong';
return (
<div className="min-w-[5.25rem] rounded-xl border border-border-soft bg-surface-muted/72 px-3 py-2">
<div className="font-mono text-[10px] uppercase tracking-[0.16em] text-text-muted">{label}</div>
<div className={`mt-0.5 text-lg font-semibold ${valueToneClass}`}>{value}</div>
<div className="min-w-[5.25rem] rounded-xl border border-border-soft bg-gradient-to-b from-surface-muted/55 to-surface-muted/75 px-3 py-2 shadow-[0_2px_4px_rgba(0,0,0,0.15)]">
<div className="ui-text text-[10px] uppercase tracking-[0.16em] text-text-muted">{label}</div>
<div className={`system-data mt-0.5 text-lg font-semibold ${valueToneClass}`}>{value}</div>
</div>
);
}

View file

@ -0,0 +1,39 @@
import type { ReactNode } from 'react';
interface WorkspaceHeroProps {
eyebrow: string;
title: string;
description: string;
action?: ReactNode;
scope?: ReactNode;
controls?: ReactNode;
className?: string;
}
export function WorkspaceHero({
eyebrow,
title,
description,
action,
scope,
controls,
className = '',
}: WorkspaceHeroProps) {
return (
<header
className={`mb-6 rounded-3xl border border-white/5 bg-[radial-gradient(circle_at_2%_2%,rgba(56,189,248,0.12),transparent_40%),linear-gradient(170deg,rgba(15,23,42,0.92),rgba(11,12,16,0.95))] px-5 py-5 sm:px-8 sm:py-8 shadow-[0_32px_64px_-16px_rgba(0,0,0,0.6)] backdrop-blur-2xl ${className}`}
>
<p className="system-data text-[10px] uppercase tracking-[0.2em] text-sky-400/70 font-bold">{eyebrow}</p>
<div className="mt-2 flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<h1 className="ui-text text-2xl font-bold tracking-tight text-text-strong sm:text-4xl">{title}</h1>
{action}
</div>
<p className="ui-text hidden max-w-md text-sm leading-relaxed text-text-muted/90 sm:block">{description}</p>
</div>
{scope ? <div className="mt-3">{scope}</div> : null}
{controls ? <div className="mt-3">{controls}</div> : null}
</header>
);
}