@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.
- package/.agents/skills/capri-zen-ui/SKILL.md +59 -0
- package/AGENTS.md +57 -0
- package/CLAUDE.md +7 -0
- package/LICENSE +21 -0
- package/README.md +93 -2
- package/bin/capri.mjs +1187 -0
- package/dist/lib/audio/ZenAudioController.d.ts +10 -0
- package/dist/lib/audio/ZenAudioController.d.ts.map +1 -0
- package/dist/lib/audio/useZenAudioSynth.d.ts +26 -0
- package/dist/lib/audio/useZenAudioSynth.d.ts.map +1 -0
- package/dist/lib/capri/capri-engine.d.ts +127 -0
- package/dist/lib/capri/capri-engine.d.ts.map +1 -0
- package/dist/lib/components/ZenAIConstruct.d.ts +24 -0
- package/dist/lib/components/ZenAIConstruct.d.ts.map +1 -0
- package/dist/lib/components/ZenAmbientBackground.d.ts +8 -0
- package/dist/lib/components/ZenAmbientBackground.d.ts.map +1 -0
- package/dist/lib/components/ZenBadge.d.ts +21 -0
- package/dist/lib/components/ZenBadge.d.ts.map +1 -0
- package/dist/lib/components/ZenButton.d.ts +24 -0
- package/dist/lib/components/ZenButton.d.ts.map +1 -0
- package/dist/lib/components/ZenCard.d.ts +36 -0
- package/dist/lib/components/ZenCard.d.ts.map +1 -0
- package/dist/lib/components/ZenDataDisplay.d.ts +50 -0
- package/dist/lib/components/ZenDataDisplay.d.ts.map +1 -0
- package/dist/lib/components/ZenDataTable.d.ts +20 -0
- package/dist/lib/components/ZenDataTable.d.ts.map +1 -0
- package/dist/lib/components/ZenDossierPanel.d.ts +33 -0
- package/dist/lib/components/ZenDossierPanel.d.ts.map +1 -0
- package/dist/lib/components/ZenDropdown.d.ts +19 -0
- package/dist/lib/components/ZenDropdown.d.ts.map +1 -0
- package/dist/lib/components/ZenFeedback.d.ts +24 -0
- package/dist/lib/components/ZenFeedback.d.ts.map +1 -0
- package/dist/lib/components/ZenForms.d.ts +84 -0
- package/dist/lib/components/ZenForms.d.ts.map +1 -0
- package/dist/lib/components/ZenHeading.d.ts +15 -0
- package/dist/lib/components/ZenHeading.d.ts.map +1 -0
- package/dist/lib/components/ZenIcon.d.ts +24 -0
- package/dist/lib/components/ZenIcon.d.ts.map +1 -0
- package/dist/lib/components/ZenInput.d.ts +12 -0
- package/dist/lib/components/ZenInput.d.ts.map +1 -0
- package/dist/lib/components/ZenModal.d.ts +12 -0
- package/dist/lib/components/ZenModal.d.ts.map +1 -0
- package/dist/lib/components/ZenNavbar.d.ts +11 -0
- package/dist/lib/components/ZenNavbar.d.ts.map +1 -0
- package/dist/lib/components/ZenNavigation.d.ts +52 -0
- package/dist/lib/components/ZenNavigation.d.ts.map +1 -0
- package/dist/lib/components/ZenOverlays.d.ts +48 -0
- package/dist/lib/components/ZenOverlays.d.ts.map +1 -0
- package/dist/lib/components/ZenSegmentedControl.d.ts +18 -0
- package/dist/lib/components/ZenSegmentedControl.d.ts.map +1 -0
- package/dist/lib/components/ZenSwitch.d.ts +11 -0
- package/dist/lib/components/ZenSwitch.d.ts.map +1 -0
- package/dist/lib/components/ZenTabs.d.ts +17 -0
- package/dist/lib/components/ZenTabs.d.ts.map +1 -0
- package/dist/lib/components/ZenTimelineCard.d.ts +14 -0
- package/dist/lib/components/ZenTimelineCard.d.ts.map +1 -0
- package/dist/lib/components/ZenToast.d.ts +18 -0
- package/dist/lib/components/ZenToast.d.ts.map +1 -0
- package/dist/lib/cosmos/ZenCosmosCanvas.d.ts +10 -0
- package/dist/lib/cosmos/ZenCosmosCanvas.d.ts.map +1 -0
- package/dist/lib/cosmos/ZenTelemetryHud.d.ts +45 -0
- package/dist/lib/cosmos/ZenTelemetryHud.d.ts.map +1 -0
- package/dist/lib/index.d.ts +33 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +3922 -0
- package/dist/lib/theme/ZenThemeProvider.d.ts +47 -0
- package/dist/lib/theme/ZenThemeProvider.d.ts.map +1 -0
- package/dist/lib/tokens.css +888 -0
- package/dist/lib/universal/zen-universal.d.ts +77 -0
- package/dist/lib/universal/zen-universal.d.ts.map +1 -0
- package/dist/lib/universal/zen-universal.js +18 -0
- package/dist/lib/zen-universal-C0PD8AI9.js +1352 -0
- package/package.json +77 -3
- package/src/lib/audio/ZenAudioController.tsx +114 -0
- package/src/lib/audio/useZenAudioSynth.ts +289 -0
- package/src/lib/capri/capri-engine.ts +939 -0
- package/src/lib/components/ZenAIConstruct.tsx +594 -0
- package/src/lib/components/ZenAmbientBackground.tsx +210 -0
- package/src/lib/components/ZenBadge.tsx +111 -0
- package/src/lib/components/ZenButton.tsx +127 -0
- package/src/lib/components/ZenCard.tsx +180 -0
- package/src/lib/components/ZenDataDisplay.tsx +349 -0
- package/src/lib/components/ZenDataTable.tsx +143 -0
- package/src/lib/components/ZenDossierPanel.tsx +120 -0
- package/src/lib/components/ZenDropdown.tsx +208 -0
- package/src/lib/components/ZenFeedback.tsx +137 -0
- package/src/lib/components/ZenForms.tsx +664 -0
- package/src/lib/components/ZenHeading.tsx +63 -0
- package/src/lib/components/ZenIcon.tsx +220 -0
- package/src/lib/components/ZenInput.tsx +120 -0
- package/src/lib/components/ZenModal.tsx +103 -0
- package/src/lib/components/ZenNavbar.tsx +106 -0
- package/src/lib/components/ZenNavigation.tsx +300 -0
- package/src/lib/components/ZenOverlays.tsx +386 -0
- package/src/lib/components/ZenSegmentedControl.tsx +113 -0
- package/src/lib/components/ZenSwitch.tsx +63 -0
- package/src/lib/components/ZenTabs.tsx +103 -0
- package/src/lib/components/ZenTimelineCard.tsx +64 -0
- package/src/lib/components/ZenToast.tsx +96 -0
- package/src/lib/cosmos/ZenCosmosCanvas.tsx +931 -0
- package/src/lib/cosmos/ZenTelemetryHud.tsx +385 -0
- package/src/lib/index.ts +293 -0
- package/src/lib/styles/zen-tokens.css +888 -0
- package/src/lib/theme/ZenThemeProvider.tsx +328 -0
- 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
|
+
}
|