@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,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
|
+
}
|