@zen-ui-capri/system 0.0.0-stage → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.agents/skills/capri-zen-ui/SKILL.md +59 -0
  2. package/AGENTS.md +57 -0
  3. package/CLAUDE.md +7 -0
  4. package/LICENSE +21 -0
  5. package/README.md +93 -2
  6. package/bin/capri.mjs +1187 -0
  7. package/dist/lib/audio/ZenAudioController.d.ts +10 -0
  8. package/dist/lib/audio/ZenAudioController.d.ts.map +1 -0
  9. package/dist/lib/audio/useZenAudioSynth.d.ts +26 -0
  10. package/dist/lib/audio/useZenAudioSynth.d.ts.map +1 -0
  11. package/dist/lib/capri/capri-engine.d.ts +127 -0
  12. package/dist/lib/capri/capri-engine.d.ts.map +1 -0
  13. package/dist/lib/components/ZenAIConstruct.d.ts +24 -0
  14. package/dist/lib/components/ZenAIConstruct.d.ts.map +1 -0
  15. package/dist/lib/components/ZenAmbientBackground.d.ts +8 -0
  16. package/dist/lib/components/ZenAmbientBackground.d.ts.map +1 -0
  17. package/dist/lib/components/ZenBadge.d.ts +21 -0
  18. package/dist/lib/components/ZenBadge.d.ts.map +1 -0
  19. package/dist/lib/components/ZenButton.d.ts +24 -0
  20. package/dist/lib/components/ZenButton.d.ts.map +1 -0
  21. package/dist/lib/components/ZenCard.d.ts +36 -0
  22. package/dist/lib/components/ZenCard.d.ts.map +1 -0
  23. package/dist/lib/components/ZenDataDisplay.d.ts +50 -0
  24. package/dist/lib/components/ZenDataDisplay.d.ts.map +1 -0
  25. package/dist/lib/components/ZenDataTable.d.ts +20 -0
  26. package/dist/lib/components/ZenDataTable.d.ts.map +1 -0
  27. package/dist/lib/components/ZenDossierPanel.d.ts +33 -0
  28. package/dist/lib/components/ZenDossierPanel.d.ts.map +1 -0
  29. package/dist/lib/components/ZenDropdown.d.ts +19 -0
  30. package/dist/lib/components/ZenDropdown.d.ts.map +1 -0
  31. package/dist/lib/components/ZenFeedback.d.ts +24 -0
  32. package/dist/lib/components/ZenFeedback.d.ts.map +1 -0
  33. package/dist/lib/components/ZenForms.d.ts +84 -0
  34. package/dist/lib/components/ZenForms.d.ts.map +1 -0
  35. package/dist/lib/components/ZenHeading.d.ts +15 -0
  36. package/dist/lib/components/ZenHeading.d.ts.map +1 -0
  37. package/dist/lib/components/ZenIcon.d.ts +24 -0
  38. package/dist/lib/components/ZenIcon.d.ts.map +1 -0
  39. package/dist/lib/components/ZenInput.d.ts +12 -0
  40. package/dist/lib/components/ZenInput.d.ts.map +1 -0
  41. package/dist/lib/components/ZenModal.d.ts +12 -0
  42. package/dist/lib/components/ZenModal.d.ts.map +1 -0
  43. package/dist/lib/components/ZenNavbar.d.ts +11 -0
  44. package/dist/lib/components/ZenNavbar.d.ts.map +1 -0
  45. package/dist/lib/components/ZenNavigation.d.ts +52 -0
  46. package/dist/lib/components/ZenNavigation.d.ts.map +1 -0
  47. package/dist/lib/components/ZenOverlays.d.ts +48 -0
  48. package/dist/lib/components/ZenOverlays.d.ts.map +1 -0
  49. package/dist/lib/components/ZenSegmentedControl.d.ts +18 -0
  50. package/dist/lib/components/ZenSegmentedControl.d.ts.map +1 -0
  51. package/dist/lib/components/ZenSwitch.d.ts +11 -0
  52. package/dist/lib/components/ZenSwitch.d.ts.map +1 -0
  53. package/dist/lib/components/ZenTabs.d.ts +17 -0
  54. package/dist/lib/components/ZenTabs.d.ts.map +1 -0
  55. package/dist/lib/components/ZenTimelineCard.d.ts +14 -0
  56. package/dist/lib/components/ZenTimelineCard.d.ts.map +1 -0
  57. package/dist/lib/components/ZenToast.d.ts +18 -0
  58. package/dist/lib/components/ZenToast.d.ts.map +1 -0
  59. package/dist/lib/cosmos/ZenCosmosCanvas.d.ts +10 -0
  60. package/dist/lib/cosmos/ZenCosmosCanvas.d.ts.map +1 -0
  61. package/dist/lib/cosmos/ZenTelemetryHud.d.ts +45 -0
  62. package/dist/lib/cosmos/ZenTelemetryHud.d.ts.map +1 -0
  63. package/dist/lib/index.d.ts +33 -0
  64. package/dist/lib/index.d.ts.map +1 -0
  65. package/dist/lib/index.js +3922 -0
  66. package/dist/lib/theme/ZenThemeProvider.d.ts +47 -0
  67. package/dist/lib/theme/ZenThemeProvider.d.ts.map +1 -0
  68. package/dist/lib/tokens.css +888 -0
  69. package/dist/lib/universal/zen-universal.d.ts +77 -0
  70. package/dist/lib/universal/zen-universal.d.ts.map +1 -0
  71. package/dist/lib/universal/zen-universal.js +18 -0
  72. package/dist/lib/zen-universal-C0PD8AI9.js +1352 -0
  73. package/package.json +77 -3
  74. package/src/lib/audio/ZenAudioController.tsx +114 -0
  75. package/src/lib/audio/useZenAudioSynth.ts +289 -0
  76. package/src/lib/capri/capri-engine.ts +939 -0
  77. package/src/lib/components/ZenAIConstruct.tsx +594 -0
  78. package/src/lib/components/ZenAmbientBackground.tsx +210 -0
  79. package/src/lib/components/ZenBadge.tsx +111 -0
  80. package/src/lib/components/ZenButton.tsx +127 -0
  81. package/src/lib/components/ZenCard.tsx +180 -0
  82. package/src/lib/components/ZenDataDisplay.tsx +349 -0
  83. package/src/lib/components/ZenDataTable.tsx +143 -0
  84. package/src/lib/components/ZenDossierPanel.tsx +120 -0
  85. package/src/lib/components/ZenDropdown.tsx +208 -0
  86. package/src/lib/components/ZenFeedback.tsx +137 -0
  87. package/src/lib/components/ZenForms.tsx +664 -0
  88. package/src/lib/components/ZenHeading.tsx +63 -0
  89. package/src/lib/components/ZenIcon.tsx +220 -0
  90. package/src/lib/components/ZenInput.tsx +120 -0
  91. package/src/lib/components/ZenModal.tsx +103 -0
  92. package/src/lib/components/ZenNavbar.tsx +106 -0
  93. package/src/lib/components/ZenNavigation.tsx +300 -0
  94. package/src/lib/components/ZenOverlays.tsx +386 -0
  95. package/src/lib/components/ZenSegmentedControl.tsx +113 -0
  96. package/src/lib/components/ZenSwitch.tsx +63 -0
  97. package/src/lib/components/ZenTabs.tsx +103 -0
  98. package/src/lib/components/ZenTimelineCard.tsx +64 -0
  99. package/src/lib/components/ZenToast.tsx +96 -0
  100. package/src/lib/cosmos/ZenCosmosCanvas.tsx +931 -0
  101. package/src/lib/cosmos/ZenTelemetryHud.tsx +385 -0
  102. package/src/lib/index.ts +293 -0
  103. package/src/lib/styles/zen-tokens.css +888 -0
  104. package/src/lib/theme/ZenThemeProvider.tsx +328 -0
  105. package/src/lib/universal/zen-universal.ts +743 -0
@@ -0,0 +1,349 @@
1
+ import { useId, useState, useEffect, type ReactNode } from 'react';
2
+ import { TrendingUp, TrendingDown } from 'lucide-react';
3
+ import { ZenCard } from './ZenCard';
4
+ import { ZenCopyButton } from './ZenButton';
5
+
6
+ export interface ZenSparklineProps {
7
+ readonly data: readonly number[];
8
+ readonly width?: number;
9
+ readonly height?: number;
10
+ readonly className?: string;
11
+ }
12
+
13
+ export function ZenSparkline({
14
+ data,
15
+ width = 140,
16
+ height = 42,
17
+ className = '',
18
+ }: ZenSparklineProps) {
19
+ const gradId = useId();
20
+ const safeData =
21
+ data.length === 0 ? [0, 0] : data.length === 1 ? [data[0], data[0]] : data;
22
+
23
+ const min = Math.min(...safeData);
24
+ const max = Math.max(...safeData);
25
+ const range = max - min || 1;
26
+
27
+ const coords = safeData.map((val, idx) => {
28
+ const x = (idx / (safeData.length - 1)) * width;
29
+ const y =
30
+ max === min
31
+ ? height * 0.5
32
+ : height - 4 - ((val - min) / range) * (height - 8);
33
+ return { x, y };
34
+ });
35
+
36
+ const linePath = coords
37
+ .map((pt, i) => `${i === 0 ? 'M' : 'L'}${pt.x.toFixed(1)},${pt.y.toFixed(1)}`)
38
+ .join(' ');
39
+ const areaPath = `${linePath} L${width},${height} L0,${height} Z`;
40
+
41
+ return (
42
+ <svg
43
+ width={width}
44
+ height={height}
45
+ viewBox={`0 0 ${width} ${height}`}
46
+ aria-hidden="true"
47
+ className={`overflow-visible ${className}`.trim()}
48
+ >
49
+ <defs>
50
+ <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
51
+ <stop
52
+ offset="0%"
53
+ stopColor="var(--zen-accent-primary)"
54
+ stopOpacity="0.38"
55
+ />
56
+ <stop
57
+ offset="100%"
58
+ stopColor="var(--zen-accent-primary)"
59
+ stopOpacity="0.0"
60
+ />
61
+ </linearGradient>
62
+ </defs>
63
+ <path d={areaPath} fill={`url(#${gradId})`} />
64
+ <path
65
+ d={linePath}
66
+ fill="none"
67
+ stroke="var(--zen-accent-primary)"
68
+ strokeWidth="2"
69
+ strokeLinecap="round"
70
+ strokeLinejoin="round"
71
+ />
72
+ </svg>
73
+ );
74
+ }
75
+
76
+ export interface ZenKpiCardProps {
77
+ readonly label: string;
78
+ readonly value: string;
79
+ readonly delta: string;
80
+ readonly trend?: 'up' | 'down';
81
+ readonly sparkline?: readonly number[];
82
+ readonly subtitle?: string;
83
+ readonly className?: string;
84
+ }
85
+
86
+ export function ZenKpiCard({
87
+ label,
88
+ value,
89
+ delta,
90
+ trend = 'up',
91
+ sparkline,
92
+ subtitle,
93
+ className = '',
94
+ }: ZenKpiCardProps) {
95
+ return (
96
+ <ZenCard
97
+ variant="static"
98
+ padding="sm"
99
+ className={`flex flex-col justify-between gap-3 ${className}`.trim()}
100
+ >
101
+ <div className="flex items-center justify-between gap-2">
102
+ <span className="zen-telemetry-label">{label}</span>
103
+ <span
104
+ className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full zen-font-mono text-[11px] font-semibold ${
105
+ trend === 'up'
106
+ ? 'bg-[var(--zen-status-success)]/15 text-[var(--zen-status-success)]'
107
+ : 'bg-[var(--zen-status-danger)]/15 text-[var(--zen-status-danger)]'
108
+ }`}
109
+ >
110
+ {trend === 'up' ? (
111
+ <TrendingUp className="w-3 h-3" />
112
+ ) : (
113
+ <TrendingDown className="w-3 h-3" />
114
+ )}
115
+ {delta}
116
+ </span>
117
+ </div>
118
+
119
+ <div className="flex items-end justify-between gap-3">
120
+ <div>
121
+ <div className="zen-font-display text-2xl sm:text-3xl font-extrabold text-[var(--zen-ink-primary)]">
122
+ {value}
123
+ </div>
124
+ {subtitle && (
125
+ <div className="zen-font-mono text-[11px] text-[var(--zen-ink-muted)] mt-0.5">
126
+ {subtitle}
127
+ </div>
128
+ )}
129
+ </div>
130
+ {sparkline && <ZenSparkline data={sparkline} width={110} height={36} />}
131
+ </div>
132
+ </ZenCard>
133
+ );
134
+ }
135
+
136
+ export interface ZenProgressBarProps {
137
+ readonly label: string;
138
+ readonly value: number;
139
+ readonly max?: number;
140
+ readonly unit?: string;
141
+ readonly className?: string;
142
+ }
143
+
144
+ export function ZenProgressBar({
145
+ label,
146
+ value,
147
+ max = 100,
148
+ unit = '%',
149
+ className = '',
150
+ }: ZenProgressBarProps) {
151
+ const pct = Math.max(0, Math.min(100, (value / Math.max(max, 1e-6)) * 100));
152
+ return (
153
+ <div
154
+ data-zen-occlude="true"
155
+ className={`space-y-1.5 ${className}`.trim()}
156
+ >
157
+ <div className="flex items-center justify-between zen-font-mono text-xs">
158
+ <span className="zen-telemetry-label">{label}</span>
159
+ <span className="font-semibold text-[var(--zen-ink-primary)]">
160
+ {value}
161
+ {unit}
162
+ </span>
163
+ </div>
164
+ <div
165
+ role="progressbar"
166
+ aria-label={label}
167
+ aria-valuenow={value}
168
+ aria-valuemin={0}
169
+ aria-valuemax={max}
170
+ className="w-full h-2.5 rounded-full bg-[var(--zen-surface-subtle)] border border-[var(--zen-border-subtle)] overflow-hidden"
171
+ >
172
+ <div
173
+ style={{ width: `${pct}%` }}
174
+ className="h-full bg-[var(--zen-accent-primary)] transition-all duration-300"
175
+ />
176
+ </div>
177
+ </div>
178
+ );
179
+ }
180
+
181
+ export interface ZenCircularGaugeProps {
182
+ readonly label: string;
183
+ readonly value: number;
184
+ readonly max?: number;
185
+ readonly unit?: string;
186
+ readonly size?: number;
187
+ }
188
+
189
+ export function ZenCircularGauge({
190
+ label,
191
+ value,
192
+ max = 100,
193
+ unit = '%',
194
+ size = 96,
195
+ }: ZenCircularGaugeProps) {
196
+ const pct = Math.max(0, Math.min(100, (value / Math.max(max, 1e-6)) * 100));
197
+ const strokeWidth = 7;
198
+ const r = (size - strokeWidth * 2) * 0.5;
199
+ const c = 2 * Math.PI * r;
200
+ const offset = c - (pct / 100) * c;
201
+
202
+ return (
203
+ <div
204
+ role="meter"
205
+ aria-label={label}
206
+ aria-valuenow={value}
207
+ aria-valuemin={0}
208
+ aria-valuemax={max}
209
+ className="inline-flex flex-col items-center gap-1.5"
210
+ >
211
+ <div className="relative" style={{ width: size, height: size }}>
212
+ <svg width={size} height={size} className="-rotate-90">
213
+ <circle
214
+ cx={size * 0.5}
215
+ cy={size * 0.5}
216
+ r={r}
217
+ fill="none"
218
+ stroke="var(--zen-border-subtle)"
219
+ strokeWidth={strokeWidth}
220
+ />
221
+ <circle
222
+ cx={size * 0.5}
223
+ cy={size * 0.5}
224
+ r={r}
225
+ fill="none"
226
+ stroke="var(--zen-accent-primary)"
227
+ strokeWidth={strokeWidth}
228
+ strokeDasharray={c}
229
+ strokeDashoffset={offset}
230
+ strokeLinecap="round"
231
+ />
232
+ </svg>
233
+ <div className="absolute inset-0 flex items-center justify-center zen-font-mono text-xs font-bold text-[var(--zen-ink-primary)]">
234
+ {Math.round(pct)}
235
+ {unit}
236
+ </div>
237
+ </div>
238
+ <span className="zen-telemetry-label">{label}</span>
239
+ </div>
240
+ );
241
+ }
242
+
243
+ export interface ZenAvatarProps {
244
+ readonly name: string;
245
+ readonly src?: string;
246
+ readonly status?: 'online' | 'busy' | 'offline';
247
+ readonly size?: 'sm' | 'md' | 'lg';
248
+ }
249
+
250
+ export function ZenAvatar({
251
+ name,
252
+ src,
253
+ status,
254
+ size = 'md',
255
+ }: ZenAvatarProps) {
256
+ const [imgError, setImgError] = useState(false);
257
+
258
+ useEffect(() => {
259
+ setImgError(false);
260
+ }, [src]);
261
+
262
+ const initials = name
263
+ .split(' ')
264
+ .map((p) => p[0])
265
+ .join('')
266
+ .slice(0, 2)
267
+ .toUpperCase();
268
+
269
+ const sizeClass =
270
+ size === 'sm'
271
+ ? 'w-8 h-8 text-xs'
272
+ : size === 'lg'
273
+ ? 'w-12 h-12 text-base'
274
+ : 'w-10 h-10 text-sm';
275
+
276
+ return (
277
+ <div className="relative inline-flex shrink-0">
278
+ {src && !imgError ? (
279
+ <img
280
+ src={src}
281
+ alt={name}
282
+ onError={() => setImgError(true)}
283
+ className={`${sizeClass} rounded-full object-cover border border-[var(--zen-border-strong)]`}
284
+ />
285
+ ) : (
286
+ <span
287
+ aria-label={name}
288
+ className={`${sizeClass} rounded-full bg-[var(--zen-accent-subtle)] border border-[var(--zen-accent-border)] text-[var(--zen-accent-primary)] zen-font-mono font-bold inline-flex items-center justify-center`}
289
+ >
290
+ {initials}
291
+ </span>
292
+ )}
293
+ {status && (
294
+ <span
295
+ className={`absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full ring-2 ring-[var(--zen-bg-canvas)] ${
296
+ status === 'online'
297
+ ? 'bg-[var(--zen-status-success)]'
298
+ : status === 'busy'
299
+ ? 'bg-[var(--zen-status-danger)]'
300
+ : 'bg-[var(--zen-ink-muted)]'
301
+ }`}
302
+ />
303
+ )}
304
+ </div>
305
+ );
306
+ }
307
+
308
+ export interface ZenCodeBlockProps {
309
+ readonly title: string;
310
+ readonly language?: string;
311
+ readonly code: string;
312
+ readonly footerSlot?: ReactNode;
313
+ readonly className?: string;
314
+ }
315
+
316
+ export function ZenCodeBlock({
317
+ title,
318
+ language = 'TSX',
319
+ code,
320
+ footerSlot,
321
+ className = '',
322
+ }: ZenCodeBlockProps) {
323
+ return (
324
+ <div
325
+ data-zen-occlude="true"
326
+ className={`rounded-2xl border border-[var(--zen-border-default)] bg-[var(--zen-bg-canvas)] overflow-hidden ${className}`.trim()}
327
+ >
328
+ <div className="flex items-center justify-between gap-2 px-4 py-2.5 border-b border-[var(--zen-border-subtle)] bg-[var(--zen-surface-subtle)]">
329
+ <div className="flex items-center gap-2 min-w-0">
330
+ <span className="zen-telemetry-label text-[var(--zen-accent-primary)]">
331
+ {language}
332
+ </span>
333
+ <span className="zen-font-mono text-xs text-[var(--zen-ink-secondary)] truncate">
334
+ {title}
335
+ </span>
336
+ </div>
337
+ <ZenCopyButton value={code} label="COPY" size="sm" />
338
+ </div>
339
+ <pre className="p-4 zen-font-mono text-xs text-[var(--zen-ink-primary)] overflow-x-auto leading-relaxed">
340
+ <code>{code}</code>
341
+ </pre>
342
+ {footerSlot && (
343
+ <div className="px-4 py-2 border-t border-[var(--zen-border-subtle)]">
344
+ {footerSlot}
345
+ </div>
346
+ )}
347
+ </div>
348
+ );
349
+ }
@@ -0,0 +1,143 @@
1
+ import { useState, useMemo, type ReactNode } from 'react';
2
+ import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
3
+
4
+ export interface ZenTableColumn<T> {
5
+ readonly key: string;
6
+ readonly header: string;
7
+ readonly align?: 'left' | 'right';
8
+ readonly mono?: boolean;
9
+ readonly sortable?: boolean;
10
+ readonly sortValue?: (row: T) => string | number;
11
+ readonly render: (row: T) => ReactNode;
12
+ }
13
+
14
+ export interface ZenDataTableProps<T> {
15
+ readonly columns: readonly ZenTableColumn<T>[];
16
+ readonly rows: readonly T[];
17
+ readonly rowKey: (row: T) => string;
18
+ readonly caption?: string;
19
+ readonly emptyMessage?: string;
20
+ readonly className?: string;
21
+ }
22
+
23
+ export function ZenDataTable<T>({
24
+ columns,
25
+ rows,
26
+ rowKey,
27
+ caption,
28
+ emptyMessage = 'No telemetry records available.',
29
+ className = '',
30
+ }: ZenDataTableProps<T>) {
31
+ const [sortCol, setSortCol] = useState<string | null>(null);
32
+ const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
33
+
34
+ const handleSort = (colKey: string) => {
35
+ if (sortCol === colKey) {
36
+ setSortDir((prev) => (prev === 'asc' ? 'desc' : 'asc'));
37
+ } else {
38
+ setSortCol(colKey);
39
+ setSortDir('asc');
40
+ }
41
+ };
42
+
43
+ const sortedRows = useMemo(() => {
44
+ if (!sortCol) return rows;
45
+ const col = columns.find((c) => c.key === sortCol);
46
+ if (!col || !col.sortValue) return rows;
47
+ const getter = col.sortValue;
48
+ return [...rows].sort((a, b) => {
49
+ const va = getter(a);
50
+ const vb = getter(b);
51
+ if (va < vb) return sortDir === 'asc' ? -1 : 1;
52
+ if (va > vb) return sortDir === 'asc' ? 1 : -1;
53
+ return 0;
54
+ });
55
+ }, [rows, columns, sortCol, sortDir]);
56
+
57
+ return (
58
+ <div
59
+ data-zen-occlude="true"
60
+ className={`zen-table-wrapper overflow-x-auto rounded-2xl border border-[var(--zen-border-default)] zen-surface-static ${className}`.trim()}
61
+ >
62
+ <table className="w-full border-collapse text-left text-xs sm:text-sm">
63
+ {caption && <caption className="sr-only">{caption}</caption>}
64
+ <thead>
65
+ <tr className="border-b border-[var(--zen-border-subtle)] bg-[var(--zen-surface-subtle)]">
66
+ {columns.map((col) => {
67
+ const isSorted = sortCol === col.key;
68
+ return (
69
+ <th
70
+ key={col.key}
71
+ scope="col"
72
+ aria-sort={
73
+ col.sortable
74
+ ? isSorted
75
+ ? sortDir === 'asc'
76
+ ? 'ascending'
77
+ : 'descending'
78
+ : 'none'
79
+ : undefined
80
+ }
81
+ className={`px-4 py-3 zen-telemetry-label ${
82
+ col.align === 'right' ? 'text-right' : 'text-left'
83
+ }`}
84
+ >
85
+ {col.sortable ? (
86
+ <button
87
+ type="button"
88
+ onClick={() => handleSort(col.key)}
89
+ className="zen-focus-ring inline-flex items-center gap-1.5 hover:text-[var(--zen-ink-primary)] cursor-pointer"
90
+ >
91
+ <span>{col.header}</span>
92
+ {isSorted ? (
93
+ sortDir === 'asc' ? (
94
+ <ArrowUp className="w-3 h-3 text-[var(--zen-accent-primary)]" />
95
+ ) : (
96
+ <ArrowDown className="w-3 h-3 text-[var(--zen-accent-primary)]" />
97
+ )
98
+ ) : (
99
+ <ArrowUpDown className="w-3 h-3 opacity-50" />
100
+ )}
101
+ </button>
102
+ ) : (
103
+ col.header
104
+ )}
105
+ </th>
106
+ );
107
+ })}
108
+ </tr>
109
+ </thead>
110
+ <tbody className="divide-y divide-[var(--zen-border-subtle)]">
111
+ {sortedRows.length === 0 ? (
112
+ <tr>
113
+ <td
114
+ colSpan={columns.length}
115
+ className="px-4 py-8 text-center zen-font-mono text-xs text-[var(--zen-ink-muted)]"
116
+ >
117
+ {emptyMessage}
118
+ </td>
119
+ </tr>
120
+ ) : (
121
+ sortedRows.map((row) => (
122
+ <tr
123
+ key={rowKey(row)}
124
+ className="hover:bg-[var(--zen-surface-subtle)] transition-colors"
125
+ >
126
+ {columns.map((col) => (
127
+ <td
128
+ key={col.key}
129
+ className={`px-4 py-3.5 text-[var(--zen-ink-primary)] ${
130
+ col.align === 'right' ? 'text-right' : 'text-left'
131
+ } ${col.mono ? 'zen-font-mono' : 'zen-font-sans'}`}
132
+ >
133
+ {col.render(row)}
134
+ </td>
135
+ ))}
136
+ </tr>
137
+ ))
138
+ )}
139
+ </tbody>
140
+ </table>
141
+ </div>
142
+ );
143
+ }
@@ -0,0 +1,120 @@
1
+ import type { ReactNode } from 'react';
2
+ import { ArrowUpRight } from 'lucide-react';
3
+ import {
4
+ ZenCard,
5
+ ZenMetricStrip,
6
+ ZenArchitectureNote,
7
+ ZenCheckList,
8
+ type ZenMetricItem,
9
+ } from './ZenCard';
10
+ import { ZenTagList } from './ZenBadge';
11
+
12
+ export interface ZenDossierItem {
13
+ readonly id: string;
14
+ readonly index: string;
15
+ readonly categoryLabel: string;
16
+ readonly year: string;
17
+ readonly title: string;
18
+ readonly role?: string;
19
+ readonly subtitle: string;
20
+ readonly engineeringNote: string;
21
+ readonly engineeringLabel?: string;
22
+ readonly highlights?: readonly string[];
23
+ readonly metrics?: readonly ZenMetricItem[];
24
+ readonly tags: readonly string[];
25
+ readonly primaryAction?: {
26
+ readonly label: string;
27
+ readonly href: string;
28
+ readonly icon?: ReactNode;
29
+ };
30
+ readonly secondaryAction?: {
31
+ readonly label: string;
32
+ readonly href: string;
33
+ readonly icon?: ReactNode;
34
+ };
35
+ }
36
+
37
+ export interface ZenDossierPanelProps {
38
+ readonly item: ZenDossierItem;
39
+ readonly footerSlot?: ReactNode;
40
+ readonly className?: string;
41
+ }
42
+
43
+ export function ZenDossierPanel({
44
+ item,
45
+ footerSlot,
46
+ className = '',
47
+ }: ZenDossierPanelProps) {
48
+ return (
49
+ <ZenCard variant="selected" padding="md" className={className}>
50
+ <div className="flex items-center justify-between zen-telemetry-label text-[var(--zen-accent-primary)] mb-3">
51
+ <span>
52
+ {item.categoryLabel} // {item.index}
53
+ </span>
54
+ <span>{item.year}</span>
55
+ </div>
56
+
57
+ <h3 className="zen-font-serif italic text-4xl sm:text-5xl mb-1 text-[var(--zen-ink-primary)]">
58
+ {item.title}
59
+ </h3>
60
+
61
+ {item.role && (
62
+ <div className="zen-font-mono text-xs text-[var(--zen-accent-secondary)] mb-2">
63
+ {item.role}
64
+ </div>
65
+ )}
66
+
67
+ <div className="zen-font-mono text-xs text-[var(--zen-ink-muted)] mb-5">
68
+ {item.subtitle}
69
+ </div>
70
+
71
+ {item.metrics && item.metrics.length > 0 && (
72
+ <ZenMetricStrip metrics={item.metrics} className="mb-5" />
73
+ )}
74
+
75
+ <ZenArchitectureNote
76
+ label={item.engineeringLabel ?? 'SYSTEM ARCHITECTURE NOTE'}
77
+ className="mb-5"
78
+ >
79
+ {item.engineeringNote}
80
+ </ZenArchitectureNote>
81
+
82
+ {item.highlights && item.highlights.length > 0 && (
83
+ <ZenCheckList items={item.highlights} className="mb-6" />
84
+ )}
85
+
86
+ <ZenTagList tags={item.tags} className="mb-6" />
87
+
88
+ {(item.primaryAction || item.secondaryAction) && (
89
+ <div className="flex flex-col sm:flex-row gap-3">
90
+ {item.primaryAction && (
91
+ <a
92
+ href={item.primaryAction.href}
93
+ target="_blank"
94
+ rel="noopener noreferrer"
95
+ className="zen-btn-base zen-btn-primary zen-focus-ring flex-1 py-3 px-5 text-xs"
96
+ >
97
+ {item.primaryAction.icon}
98
+ <span>{item.primaryAction.label}</span>
99
+ <ArrowUpRight className="w-4 h-4" />
100
+ </a>
101
+ )}
102
+ {item.secondaryAction && (
103
+ <a
104
+ href={item.secondaryAction.href}
105
+ target="_blank"
106
+ rel="noopener noreferrer"
107
+ className="zen-btn-base zen-btn-secondary zen-focus-ring flex-1 py-3 px-5 text-xs"
108
+ >
109
+ {item.secondaryAction.icon}
110
+ <span>{item.secondaryAction.label}</span>
111
+ <ArrowUpRight className="w-4 h-4" />
112
+ </a>
113
+ )}
114
+ </div>
115
+ )}
116
+
117
+ {footerSlot && <div className="mt-4">{footerSlot}</div>}
118
+ </ZenCard>
119
+ );
120
+ }