@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,300 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
+ import { ChevronRight, ChevronLeft, Check } from 'lucide-react';
3
+
4
+ export interface ZenBreadcrumbItem {
5
+ readonly id: string;
6
+ readonly label: string;
7
+ readonly href?: string;
8
+ readonly onClick?: () => void;
9
+ }
10
+
11
+ export interface ZenBreadcrumbsProps {
12
+ readonly items: readonly ZenBreadcrumbItem[];
13
+ readonly className?: string;
14
+ }
15
+
16
+ export function ZenBreadcrumbs({ items, className = '' }: ZenBreadcrumbsProps) {
17
+ return (
18
+ <nav
19
+ aria-label="Breadcrumb"
20
+ data-zen-occlude="true"
21
+ className={className}
22
+ >
23
+ <ol className="flex flex-wrap items-center gap-1.5 zen-font-mono text-xs">
24
+ {items.map((item, idx) => {
25
+ const isLast = idx === items.length - 1;
26
+ return (
27
+ <li key={item.id} className="inline-flex items-center gap-1.5">
28
+ {idx > 0 && (
29
+ <ChevronRight
30
+ className="w-3.5 h-3.5 text-[var(--zen-ink-muted)] shrink-0"
31
+ aria-hidden="true"
32
+ />
33
+ )}
34
+ {isLast ? (
35
+ <span
36
+ aria-current="page"
37
+ className="font-semibold text-[var(--zen-accent-primary)]"
38
+ >
39
+ {item.label}
40
+ </span>
41
+ ) : (
42
+ <button
43
+ type="button"
44
+ onClick={item.onClick}
45
+ className="zen-focus-ring text-[var(--zen-ink-secondary)] hover:text-[var(--zen-ink-primary)] cursor-pointer transition-colors"
46
+ >
47
+ {item.label}
48
+ </button>
49
+ )}
50
+ </li>
51
+ );
52
+ })}
53
+ </ol>
54
+ </nav>
55
+ );
56
+ }
57
+
58
+ export interface ZenPaginationProps {
59
+ readonly currentPage: number;
60
+ readonly totalPages: number;
61
+ readonly onPageChange: (page: number) => void;
62
+ readonly className?: string;
63
+ }
64
+
65
+ export function ZenPagination({
66
+ currentPage,
67
+ totalPages,
68
+ onPageChange,
69
+ className = '',
70
+ }: ZenPaginationProps) {
71
+ const safeTotal = Math.max(1, totalPages);
72
+ const pages = Array.from({ length: safeTotal }, (_, i) => i + 1);
73
+
74
+ return (
75
+ <nav
76
+ aria-label="Pagination"
77
+ data-zen-occlude="true"
78
+ className={`inline-flex flex-wrap items-center gap-1.5 ${className}`.trim()}
79
+ >
80
+ <button
81
+ type="button"
82
+ aria-label="Previous page"
83
+ disabled={currentPage <= 1}
84
+ onClick={() => onPageChange(Math.max(1, currentPage - 1))}
85
+ className="zen-focus-ring p-2 rounded-xl border border-[var(--zen-border-default)] bg-[var(--zen-surface-1)] text-[var(--zen-ink-primary)] disabled:opacity-40 cursor-pointer"
86
+ >
87
+ <ChevronLeft className="w-4 h-4" />
88
+ </button>
89
+
90
+ {pages.map((p) => {
91
+ const isCurrent = p === currentPage;
92
+ return (
93
+ <button
94
+ key={p}
95
+ type="button"
96
+ aria-current={isCurrent ? 'page' : undefined}
97
+ onClick={() => onPageChange(p)}
98
+ className={`zen-focus-ring min-w-9 h-9 px-2.5 rounded-xl zen-font-mono text-xs font-semibold cursor-pointer transition-all ${
99
+ isCurrent
100
+ ? 'bg-[var(--zen-accent-primary)] text-[var(--zen-ink-on-accent)] shadow-sm'
101
+ : 'bg-[var(--zen-surface-1)] border border-[var(--zen-border-subtle)] text-[var(--zen-ink-secondary)] hover:text-[var(--zen-ink-primary)]'
102
+ }`}
103
+ >
104
+ {p}
105
+ </button>
106
+ );
107
+ })}
108
+
109
+ <button
110
+ type="button"
111
+ aria-label="Next page"
112
+ disabled={currentPage >= safeTotal}
113
+ onClick={() => onPageChange(Math.min(safeTotal, currentPage + 1))}
114
+ className="zen-focus-ring p-2 rounded-xl border border-[var(--zen-border-default)] bg-[var(--zen-surface-1)] text-[var(--zen-ink-primary)] disabled:opacity-40 cursor-pointer"
115
+ >
116
+ <ChevronRight className="w-4 h-4" />
117
+ </button>
118
+ </nav>
119
+ );
120
+ }
121
+
122
+ export interface ZenStepItem {
123
+ readonly id: string;
124
+ readonly title: string;
125
+ readonly subtitle?: string;
126
+ }
127
+
128
+ export interface ZenStepperProps {
129
+ readonly steps: readonly ZenStepItem[];
130
+ readonly activeStepIndex: number;
131
+ readonly onStepClick?: (index: number) => void;
132
+ readonly className?: string;
133
+ }
134
+
135
+ const STEP_GRID_COLS: Record<number, string> = {
136
+ 1: 'grid-cols-1',
137
+ 2: 'grid-cols-1 sm:grid-cols-2',
138
+ 3: 'grid-cols-1 sm:grid-cols-3',
139
+ 4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
140
+ };
141
+
142
+ export function ZenStepper({
143
+ steps,
144
+ activeStepIndex,
145
+ onStepClick,
146
+ className = '',
147
+ }: ZenStepperProps) {
148
+ const colKey = Math.max(1, Math.min(4, steps.length));
149
+ const gridClass = STEP_GRID_COLS[colKey] ?? STEP_GRID_COLS[4];
150
+
151
+ return (
152
+ <ol
153
+ data-zen-occlude="true"
154
+ className={`grid ${gridClass} gap-3 ${className}`.trim()}
155
+ >
156
+ {steps.map((step, idx) => {
157
+ const isDone = idx < activeStepIndex;
158
+ const isCurrent = idx === activeStepIndex;
159
+ return (
160
+ <li key={step.id} className="min-w-0">
161
+ <button
162
+ type="button"
163
+ aria-current={isCurrent ? 'step' : undefined}
164
+ onClick={() => onStepClick?.(idx)}
165
+ className={`zen-focus-ring w-full p-3.5 rounded-2xl border text-left flex items-center gap-3 cursor-pointer transition-all ${
166
+ isCurrent
167
+ ? 'bg-[var(--zen-accent-subtle)] border-[var(--zen-accent-border)]'
168
+ : isDone
169
+ ? 'bg-[var(--zen-surface-1)] border-[var(--zen-border-default)]'
170
+ : 'bg-[var(--zen-surface-subtle)] border-[var(--zen-border-subtle)] opacity-75'
171
+ }`}
172
+ >
173
+ <span
174
+ className={`w-7 h-7 rounded-full zen-font-mono text-xs font-bold flex items-center justify-center shrink-0 ${
175
+ isCurrent || isDone
176
+ ? 'bg-[var(--zen-accent-primary)] text-[var(--zen-ink-on-accent)]'
177
+ : 'border border-[var(--zen-border-control)] text-[var(--zen-ink-muted)]'
178
+ }`}
179
+ >
180
+ {isDone ? <Check className="w-3.5 h-3.5" /> : idx + 1}
181
+ </span>
182
+ <div className="min-w-0">
183
+ <div className="zen-font-display text-sm font-bold text-[var(--zen-ink-primary)] truncate">
184
+ {step.title}
185
+ </div>
186
+ {step.subtitle && (
187
+ <div className="zen-font-mono text-[11px] text-[var(--zen-ink-muted)] truncate">
188
+ {step.subtitle}
189
+ </div>
190
+ )}
191
+ </div>
192
+ </button>
193
+ </li>
194
+ );
195
+ })}
196
+ </ol>
197
+ );
198
+ }
199
+
200
+ export interface ZenSidebarItem {
201
+ readonly id: string;
202
+ readonly label: string;
203
+ readonly icon?: ReactNode;
204
+ readonly badge?: string | number;
205
+ }
206
+
207
+ export interface ZenSidebarProps {
208
+ readonly title: string;
209
+ readonly items: readonly ZenSidebarItem[];
210
+ readonly activeId: string;
211
+ readonly onSelect: (id: string) => void;
212
+ readonly className?: string;
213
+ }
214
+
215
+ export function ZenSidebar({
216
+ title,
217
+ items,
218
+ activeId,
219
+ onSelect,
220
+ className = '',
221
+ }: ZenSidebarProps) {
222
+ return (
223
+ <aside
224
+ aria-label={title}
225
+ data-zen-occlude="true"
226
+ className={`p-4 rounded-3xl zen-surface-static space-y-3 ${className}`.trim()}
227
+ >
228
+ <div className="px-2 zen-telemetry-label text-[var(--zen-accent-primary)]">
229
+ {title}
230
+ </div>
231
+ <ul className="space-y-1">
232
+ {items.map((item) => {
233
+ const active = item.id === activeId;
234
+ return (
235
+ <li key={item.id}>
236
+ <button
237
+ type="button"
238
+ aria-current={active ? 'page' : undefined}
239
+ onClick={() => onSelect(item.id)}
240
+ className={`zen-focus-ring w-full px-3 py-2.5 rounded-xl flex items-center justify-between gap-2 text-xs sm:text-sm font-medium cursor-pointer transition-all ${
241
+ active
242
+ ? 'bg-[var(--zen-accent-primary)] text-[var(--zen-ink-on-accent)] font-semibold shadow-sm'
243
+ : 'text-[var(--zen-ink-secondary)] hover:text-[var(--zen-ink-primary)] hover:bg-[var(--zen-surface-subtle)]'
244
+ }`}
245
+ >
246
+ <span className="inline-flex items-center gap-2.5 truncate">
247
+ {item.icon && <span className="shrink-0">{item.icon}</span>}
248
+ <span className="truncate">{item.label}</span>
249
+ </span>
250
+ {item.badge !== undefined && (
251
+ <span
252
+ className={`zen-font-mono text-[10px] px-2 py-0.5 rounded-full shrink-0 ${
253
+ active
254
+ ? 'bg-black/20 text-[var(--zen-ink-on-accent)]'
255
+ : 'bg-[var(--zen-surface-subtle)] text-[var(--zen-ink-muted)]'
256
+ }`}
257
+ >
258
+ {item.badge}
259
+ </span>
260
+ )}
261
+ </button>
262
+ </li>
263
+ );
264
+ })}
265
+ </ul>
266
+ </aside>
267
+ );
268
+ }
269
+
270
+ export interface ZenIconButtonProps
271
+ extends ButtonHTMLAttributes<HTMLButtonElement> {
272
+ readonly ariaLabel: string;
273
+ readonly active?: boolean;
274
+ readonly children: ReactNode;
275
+ }
276
+
277
+ export function ZenIconButton({
278
+ ariaLabel,
279
+ active = false,
280
+ className = '',
281
+ children,
282
+ ...rest
283
+ }: ZenIconButtonProps) {
284
+ return (
285
+ <button
286
+ type="button"
287
+ data-zen-occlude="true"
288
+ aria-label={ariaLabel}
289
+ aria-pressed={active}
290
+ className={`zen-focus-ring p-2.5 rounded-xl border transition-all cursor-pointer inline-flex items-center justify-center ${
291
+ active
292
+ ? 'bg-[var(--zen-accent-primary)] text-[var(--zen-ink-on-accent)] border-[var(--zen-accent-primary)]'
293
+ : 'bg-[var(--zen-surface-1)] text-[var(--zen-ink-secondary)] border-[var(--zen-border-default)] hover:border-[var(--zen-accent-border)] hover:text-[var(--zen-ink-primary)]'
294
+ } ${className}`.trim()}
295
+ {...rest}
296
+ >
297
+ {children}
298
+ </button>
299
+ );
300
+ }
@@ -0,0 +1,386 @@
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ useId,
5
+ useRef,
6
+ type ReactNode,
7
+ type KeyboardEvent,
8
+ } from 'react';
9
+ import { Search, X, ChevronDown, CornerDownLeft } from 'lucide-react';
10
+ import { ZenKbd } from './ZenBadge';
11
+
12
+ export interface ZenCommandItem {
13
+ readonly id: string;
14
+ readonly label: string;
15
+ readonly group: string;
16
+ readonly shortcut?: readonly string[];
17
+ readonly icon?: ReactNode;
18
+ readonly onSelect: () => void;
19
+ }
20
+
21
+ export interface ZenCommandPaletteProps {
22
+ readonly open: boolean;
23
+ readonly onClose: () => void;
24
+ readonly items: readonly ZenCommandItem[];
25
+ readonly placeholder?: string;
26
+ }
27
+
28
+ export function ZenCommandPalette({
29
+ open,
30
+ onClose,
31
+ items,
32
+ placeholder = 'Execute a command or jump to a primitive...',
33
+ }: ZenCommandPaletteProps) {
34
+ const [query, setQuery] = useState('');
35
+ const [activeIndex, setActiveIndex] = useState(0);
36
+ const inputRef = useRef<HTMLInputElement>(null);
37
+ const listboxId = useId();
38
+
39
+ const filtered = items.filter((item) => {
40
+ if (!query.trim()) return true;
41
+ const q = query.toLowerCase();
42
+ return (
43
+ item.label.toLowerCase().includes(q) ||
44
+ item.group.toLowerCase().includes(q)
45
+ );
46
+ });
47
+
48
+ useEffect(() => {
49
+ setActiveIndex(0);
50
+ }, [query]);
51
+
52
+ useEffect(() => {
53
+ if (!open) return;
54
+ setQuery('');
55
+ setActiveIndex(0);
56
+ window.setTimeout(() => inputRef.current?.focus(), 10);
57
+ }, [open]);
58
+
59
+ if (!open) return null;
60
+
61
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
62
+ if (e.key === 'Escape') {
63
+ e.preventDefault();
64
+ onClose();
65
+ } else if (e.key === 'ArrowDown') {
66
+ e.preventDefault();
67
+ if (filtered.length > 0) {
68
+ setActiveIndex((prev) => (prev + 1) % filtered.length);
69
+ }
70
+ } else if (e.key === 'ArrowUp') {
71
+ e.preventDefault();
72
+ if (filtered.length > 0) {
73
+ setActiveIndex(
74
+ (prev) => (prev - 1 + filtered.length) % filtered.length
75
+ );
76
+ }
77
+ } else if (e.key === 'Enter' && filtered[activeIndex]) {
78
+ e.preventDefault();
79
+ filtered[activeIndex].onSelect();
80
+ onClose();
81
+ }
82
+ };
83
+
84
+ return (
85
+ <div
86
+ data-zen-occlude="true"
87
+ className="fixed inset-0 z-50 flex items-start justify-center pt-12 sm:pt-20 p-3 sm:p-4 bg-black/70 backdrop-blur-md"
88
+ onMouseDown={(e) => {
89
+ if (e.target === e.currentTarget) onClose();
90
+ }}
91
+ >
92
+ <div
93
+ role="dialog"
94
+ aria-modal="true"
95
+ aria-label="Command Palette"
96
+ className="w-full max-w-xl max-h-[85dvh] flex flex-col rounded-3xl zen-surface-selected overflow-hidden shadow-2xl"
97
+ >
98
+ <div className="flex items-center gap-3 px-4 py-3.5 border-b border-[var(--zen-border-subtle)]">
99
+ <Search className="w-4 h-4 text-[var(--zen-accent-primary)] shrink-0" />
100
+ <input
101
+ ref={inputRef}
102
+ role="combobox"
103
+ aria-expanded="true"
104
+ aria-controls={listboxId}
105
+ aria-autocomplete="list"
106
+ value={query}
107
+ onChange={(e) => setQuery(e.target.value)}
108
+ onKeyDown={handleKeyDown}
109
+ placeholder={placeholder}
110
+ className="w-full bg-transparent text-sm text-[var(--zen-ink-primary)] placeholder:text-[var(--zen-ink-muted)] focus:outline-none zen-font-mono"
111
+ />
112
+ <ZenKbd keys={['Esc']} />
113
+ </div>
114
+
115
+ <ul
116
+ id={listboxId}
117
+ role="listbox"
118
+ className="max-h-80 overflow-y-auto p-2 space-y-1"
119
+ >
120
+ {filtered.length === 0 ? (
121
+ <li className="py-8 text-center zen-font-mono text-xs text-[var(--zen-ink-muted)]">
122
+ No matching commands found.
123
+ </li>
124
+ ) : (
125
+ filtered.map((item, idx) => {
126
+ const active = idx === activeIndex;
127
+ return (
128
+ <li
129
+ key={item.id}
130
+ role="option"
131
+ aria-selected={active}
132
+ onMouseEnter={() => setActiveIndex(idx)}
133
+ onMouseDown={(e) => {
134
+ e.preventDefault();
135
+ item.onSelect();
136
+ onClose();
137
+ }}
138
+ className={`flex items-center justify-between gap-3 px-3.5 py-2.5 rounded-xl cursor-pointer transition-colors ${
139
+ active
140
+ ? 'bg-[var(--zen-accent-subtle)] text-[var(--zen-ink-primary)]'
141
+ : 'text-[var(--zen-ink-secondary)]'
142
+ }`}
143
+ >
144
+ <div className="flex items-center gap-2.5 min-w-0">
145
+ {item.icon && (
146
+ <span className="text-[var(--zen-accent-primary)] shrink-0">
147
+ {item.icon}
148
+ </span>
149
+ )}
150
+ <div className="min-w-0">
151
+ <div className="text-xs sm:text-sm font-medium truncate">
152
+ {item.label}
153
+ </div>
154
+ <div className="zen-telemetry-label">{item.group}</div>
155
+ </div>
156
+ </div>
157
+ <div className="flex items-center gap-2 shrink-0">
158
+ {item.shortcut && <ZenKbd keys={item.shortcut} />}
159
+ {active && (
160
+ <CornerDownLeft className="w-3.5 h-3.5 text-[var(--zen-accent-primary)]" />
161
+ )}
162
+ </div>
163
+ </li>
164
+ );
165
+ })
166
+ )}
167
+ </ul>
168
+ </div>
169
+ </div>
170
+ );
171
+ }
172
+
173
+ export interface ZenDrawerProps {
174
+ readonly open: boolean;
175
+ readonly onClose: () => void;
176
+ readonly title: ReactNode;
177
+ readonly subtitle?: ReactNode;
178
+ readonly footer?: ReactNode;
179
+ readonly children: ReactNode;
180
+ }
181
+
182
+ export function ZenDrawer({
183
+ open,
184
+ onClose,
185
+ title,
186
+ subtitle,
187
+ footer,
188
+ children,
189
+ }: ZenDrawerProps) {
190
+ const titleId = useId();
191
+
192
+ useEffect(() => {
193
+ if (!open) return;
194
+ const onKey = (e: globalThis.KeyboardEvent) => {
195
+ if (e.key === 'Escape') onClose();
196
+ };
197
+ window.addEventListener('keydown', onKey);
198
+ return () => window.removeEventListener('keydown', onKey);
199
+ }, [open, onClose]);
200
+
201
+ if (!open) return null;
202
+
203
+ return (
204
+ <div
205
+ data-zen-occlude="true"
206
+ className="fixed inset-0 z-50 flex justify-end bg-black/65 backdrop-blur-sm"
207
+ onMouseDown={(e) => {
208
+ if (e.target === e.currentTarget) onClose();
209
+ }}
210
+ >
211
+ <div
212
+ role="dialog"
213
+ aria-modal="true"
214
+ aria-labelledby={titleId}
215
+ className="w-full max-w-md h-full bg-[var(--zen-bg-elevated)] border-l border-[var(--zen-border-strong)] p-6 sm:p-8 flex flex-col justify-between shadow-2xl overflow-y-auto"
216
+ >
217
+ <div className="space-y-6">
218
+ <div className="flex items-start justify-between gap-4 pb-4 border-b border-[var(--zen-border-subtle)]">
219
+ <div>
220
+ <div className="zen-telemetry-label text-[var(--zen-accent-primary)]">
221
+ SLIDE-OVER INSPECTOR
222
+ </div>
223
+ <h2
224
+ id={titleId}
225
+ className="zen-font-display text-2xl font-bold text-[var(--zen-ink-primary)] mt-1"
226
+ >
227
+ {title}
228
+ </h2>
229
+ {subtitle && (
230
+ <div className="zen-font-serif italic text-lg text-[var(--zen-accent-primary)]">
231
+ {subtitle}
232
+ </div>
233
+ )}
234
+ </div>
235
+ <button
236
+ type="button"
237
+ aria-label="Close drawer"
238
+ onClick={onClose}
239
+ className="zen-focus-ring p-2 rounded-full border border-[var(--zen-border-default)] text-[var(--zen-ink-secondary)] hover:text-[var(--zen-ink-primary)] cursor-pointer"
240
+ >
241
+ <X className="w-4 h-4" />
242
+ </button>
243
+ </div>
244
+
245
+ <div className="text-sm text-[var(--zen-ink-secondary)] space-y-4">
246
+ {children}
247
+ </div>
248
+ </div>
249
+
250
+ {footer && (
251
+ <div className="pt-4 border-t border-[var(--zen-border-subtle)] flex justify-end gap-3">
252
+ {footer}
253
+ </div>
254
+ )}
255
+ </div>
256
+ </div>
257
+ );
258
+ }
259
+
260
+ export interface ZenAccordionItem {
261
+ readonly id: string;
262
+ readonly title: string;
263
+ readonly badge?: string;
264
+ readonly content: ReactNode;
265
+ }
266
+
267
+ export interface ZenAccordionProps {
268
+ readonly items: readonly ZenAccordionItem[];
269
+ readonly defaultOpenId?: string;
270
+ readonly className?: string;
271
+ }
272
+
273
+ export function ZenAccordion({
274
+ items,
275
+ defaultOpenId,
276
+ className = '',
277
+ }: ZenAccordionProps) {
278
+ const [openId, setOpenId] = useState<string | null>(
279
+ defaultOpenId ?? items[0]?.id ?? null
280
+ );
281
+
282
+ return (
283
+ <div
284
+ data-zen-occlude="true"
285
+ className={`space-y-2.5 ${className}`.trim()}
286
+ >
287
+ {items.map((item) => {
288
+ const isOpen = item.id === openId;
289
+ return (
290
+ <div
291
+ key={item.id}
292
+ className="rounded-2xl border border-[var(--zen-border-default)] bg-[var(--zen-surface-1)] overflow-hidden"
293
+ >
294
+ <h3>
295
+ <button
296
+ type="button"
297
+ aria-expanded={isOpen}
298
+ onClick={() => setOpenId(isOpen ? null : item.id)}
299
+ className="zen-focus-ring w-full px-5 py-4 flex items-center justify-between gap-3 text-left cursor-pointer hover:bg-[var(--zen-surface-subtle)] transition-colors"
300
+ >
301
+ <span className="zen-font-display font-bold text-sm sm:text-base text-[var(--zen-ink-primary)]">
302
+ {item.title}
303
+ </span>
304
+ <div className="flex items-center gap-2 shrink-0">
305
+ {item.badge && (
306
+ <span className="zen-telemetry-label text-[var(--zen-accent-primary)]">
307
+ {item.badge}
308
+ </span>
309
+ )}
310
+ <ChevronDown
311
+ className={`w-4 h-4 text-[var(--zen-ink-muted)] transition-transform ${
312
+ isOpen
313
+ ? 'rotate-180 text-[var(--zen-accent-primary)]'
314
+ : ''
315
+ }`}
316
+ />
317
+ </div>
318
+ </button>
319
+ </h3>
320
+ {isOpen && (
321
+ <div className="px-5 pb-4 pt-1 text-xs sm:text-sm text-[var(--zen-ink-secondary)] leading-relaxed border-t border-[var(--zen-border-subtle)]">
322
+ {item.content}
323
+ </div>
324
+ )}
325
+ </div>
326
+ );
327
+ })}
328
+ </div>
329
+ );
330
+ }
331
+
332
+ export interface ZenTooltipProps {
333
+ readonly content: string;
334
+ readonly children: ReactNode;
335
+ }
336
+
337
+ export function ZenTooltip({ content, children }: ZenTooltipProps) {
338
+ const [visible, setVisible] = useState(false);
339
+ const id = useId();
340
+
341
+ return (
342
+ <span
343
+ className="relative inline-flex"
344
+ onMouseEnter={() => setVisible(true)}
345
+ onMouseLeave={() => setVisible(false)}
346
+ onFocus={() => setVisible(true)}
347
+ onBlur={() => setVisible(false)}
348
+ onKeyDown={(e) => {
349
+ if (e.key === 'Escape') setVisible(false);
350
+ }}
351
+ >
352
+ <span aria-describedby={visible ? id : undefined}>{children}</span>
353
+ {visible && (
354
+ <span
355
+ id={id}
356
+ role="tooltip"
357
+ className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2.5 py-1 rounded-lg bg-[var(--zen-bg-elevated)] border border-[var(--zen-border-strong)] zen-font-mono text-[11px] text-[var(--zen-ink-primary)] whitespace-nowrap shadow-lg z-50 pointer-events-none"
358
+ >
359
+ {content}
360
+ </span>
361
+ )}
362
+ </span>
363
+ );
364
+ }
365
+
366
+ export interface ZenDividerProps {
367
+ readonly label?: string;
368
+ readonly className?: string;
369
+ }
370
+
371
+ export function ZenDivider({ label, className = '' }: ZenDividerProps) {
372
+ if (!label) {
373
+ return (
374
+ <hr
375
+ className={`border-0 border-t border-[var(--zen-border-subtle)] ${className}`.trim()}
376
+ />
377
+ );
378
+ }
379
+ return (
380
+ <div className={`flex items-center gap-3 ${className}`.trim()}>
381
+ <div className="h-px flex-1 bg-[var(--zen-border-subtle)]" />
382
+ <span className="zen-telemetry-label">{label}</span>
383
+ <div className="h-px flex-1 bg-[var(--zen-border-subtle)]" />
384
+ </div>
385
+ );
386
+ }