@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,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
type KeyboardEvent,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { ChevronDown, Check } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
export interface ZenDropdownOption<T extends string> {
|
|
12
|
+
readonly value: T;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly description?: string;
|
|
15
|
+
readonly icon?: ReactNode;
|
|
16
|
+
readonly disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ZenDropdownProps<T extends string> {
|
|
20
|
+
readonly label?: string;
|
|
21
|
+
readonly options: readonly ZenDropdownOption<T>[];
|
|
22
|
+
readonly value: T;
|
|
23
|
+
readonly onChange: (nextValue: T) => void;
|
|
24
|
+
readonly ariaLabel?: string;
|
|
25
|
+
readonly align?: 'start' | 'end';
|
|
26
|
+
readonly className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ZenDropdown<T extends string>({
|
|
30
|
+
label,
|
|
31
|
+
options,
|
|
32
|
+
value,
|
|
33
|
+
onChange,
|
|
34
|
+
ariaLabel,
|
|
35
|
+
align = 'start',
|
|
36
|
+
className = '',
|
|
37
|
+
}: ZenDropdownProps<T>) {
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
const [activeIndex, setActiveIndex] = useState<number>(() =>
|
|
40
|
+
Math.max(
|
|
41
|
+
0,
|
|
42
|
+
options.findIndex((o) => o.value === value)
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
47
|
+
const listboxId = useId();
|
|
48
|
+
|
|
49
|
+
const selectedOption =
|
|
50
|
+
options.find((o) => o.value === value) ?? options[0];
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const idx = options.findIndex((o) => o.value === value);
|
|
54
|
+
if (idx !== -1) {
|
|
55
|
+
setActiveIndex(idx);
|
|
56
|
+
}
|
|
57
|
+
}, [value, options]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!open) return;
|
|
61
|
+
const handlePointerDownOutside = (e: MouseEvent) => {
|
|
62
|
+
if (
|
|
63
|
+
containerRef.current &&
|
|
64
|
+
!containerRef.current.contains(e.target as Node)
|
|
65
|
+
) {
|
|
66
|
+
setOpen(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
window.addEventListener('mousedown', handlePointerDownOutside);
|
|
70
|
+
return () =>
|
|
71
|
+
window.removeEventListener('mousedown', handlePointerDownOutside);
|
|
72
|
+
}, [open]);
|
|
73
|
+
|
|
74
|
+
const commitSelection = (nextValue: T) => {
|
|
75
|
+
onChange(nextValue);
|
|
76
|
+
setOpen(false);
|
|
77
|
+
triggerRef.current?.focus();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
|
81
|
+
if (!open) {
|
|
82
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
setOpen(true);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (e.key === 'Escape') {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
setOpen(false);
|
|
92
|
+
triggerRef.current?.focus();
|
|
93
|
+
} else if (e.key === 'ArrowDown') {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
setActiveIndex((prev) => (prev + 1) % options.length);
|
|
96
|
+
} else if (e.key === 'ArrowUp') {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
setActiveIndex((prev) => (prev - 1 + options.length) % options.length);
|
|
99
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
const candidate = options[activeIndex];
|
|
102
|
+
if (candidate && !candidate.disabled) {
|
|
103
|
+
commitSelection(candidate.value);
|
|
104
|
+
}
|
|
105
|
+
} else if (e.key === 'Tab') {
|
|
106
|
+
setOpen(false);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
ref={containerRef}
|
|
113
|
+
data-zen-occlude="true"
|
|
114
|
+
className={`relative flex flex-col gap-1.5 min-w-0 ${className}`.trim()}
|
|
115
|
+
>
|
|
116
|
+
{label && (
|
|
117
|
+
<span className="zen-telemetry-label text-[var(--zen-ink-secondary)]">
|
|
118
|
+
{label}
|
|
119
|
+
</span>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
<button
|
|
123
|
+
ref={triggerRef}
|
|
124
|
+
type="button"
|
|
125
|
+
aria-haspopup="listbox"
|
|
126
|
+
aria-expanded={open}
|
|
127
|
+
aria-controls={listboxId}
|
|
128
|
+
aria-label={ariaLabel ?? label}
|
|
129
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
130
|
+
onKeyDown={handleKeyDown}
|
|
131
|
+
className="zen-focus-ring flex items-center justify-between gap-3 px-3.5 py-2.5 rounded-xl bg-[var(--zen-surface-1)] border border-[var(--zen-border-default)] hover:border-[var(--zen-border-strong)] text-left text-xs sm:text-sm text-[var(--zen-ink-primary)] cursor-pointer transition-colors"
|
|
132
|
+
>
|
|
133
|
+
<span className="inline-flex items-center gap-2 truncate min-w-0">
|
|
134
|
+
{selectedOption?.icon && (
|
|
135
|
+
<span className="text-[var(--zen-accent-primary)] shrink-0">
|
|
136
|
+
{selectedOption.icon}
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
<span className="zen-font-mono font-medium truncate">
|
|
140
|
+
{selectedOption?.label}
|
|
141
|
+
</span>
|
|
142
|
+
</span>
|
|
143
|
+
<ChevronDown
|
|
144
|
+
className={`w-4 h-4 text-[var(--zen-ink-muted)] shrink-0 transition-transform ${
|
|
145
|
+
open ? 'rotate-180 text-[var(--zen-accent-primary)]' : ''
|
|
146
|
+
}`}
|
|
147
|
+
/>
|
|
148
|
+
</button>
|
|
149
|
+
|
|
150
|
+
{open && (
|
|
151
|
+
<ul
|
|
152
|
+
id={listboxId}
|
|
153
|
+
role="listbox"
|
|
154
|
+
aria-label={ariaLabel ?? label}
|
|
155
|
+
className={`absolute z-50 top-full mt-2 w-full max-w-[calc(100vw-2rem)] max-h-64 overflow-y-auto p-1.5 rounded-2xl bg-[var(--zen-bg-elevated)] border border-[var(--zen-border-strong)] shadow-[var(--zen-shadow-hud)] backdrop-blur-xl ${
|
|
156
|
+
align === 'end' ? 'right-0' : 'left-0'
|
|
157
|
+
}`}
|
|
158
|
+
>
|
|
159
|
+
{options.map((opt, idx) => {
|
|
160
|
+
const isSelected = opt.value === value;
|
|
161
|
+
const isFocused = idx === activeIndex;
|
|
162
|
+
return (
|
|
163
|
+
<li
|
|
164
|
+
key={opt.value}
|
|
165
|
+
role="option"
|
|
166
|
+
aria-selected={isSelected}
|
|
167
|
+
aria-disabled={opt.disabled}
|
|
168
|
+
onMouseEnter={() => setActiveIndex(idx)}
|
|
169
|
+
onMouseDown={(e) => {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
if (!opt.disabled) {
|
|
172
|
+
commitSelection(opt.value);
|
|
173
|
+
}
|
|
174
|
+
}}
|
|
175
|
+
className={`flex items-center justify-between gap-3 px-3 py-2 rounded-xl cursor-pointer transition-colors ${
|
|
176
|
+
isFocused
|
|
177
|
+
? 'bg-[var(--zen-accent-subtle)] text-[var(--zen-ink-primary)]'
|
|
178
|
+
: 'text-[var(--zen-ink-secondary)]'
|
|
179
|
+
} ${opt.disabled ? 'opacity-40 cursor-not-allowed' : ''}`}
|
|
180
|
+
>
|
|
181
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
182
|
+
{opt.icon && (
|
|
183
|
+
<span className="text-[var(--zen-accent-primary)] shrink-0">
|
|
184
|
+
{opt.icon}
|
|
185
|
+
</span>
|
|
186
|
+
)}
|
|
187
|
+
<div className="min-w-0">
|
|
188
|
+
<div className="zen-font-mono text-xs font-medium truncate">
|
|
189
|
+
{opt.label}
|
|
190
|
+
</div>
|
|
191
|
+
{opt.description && (
|
|
192
|
+
<div className="text-[11px] text-[var(--zen-ink-muted)] truncate">
|
|
193
|
+
{opt.description}
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
{isSelected && (
|
|
199
|
+
<Check className="w-3.5 h-3.5 text-[var(--zen-accent-primary)] shrink-0" />
|
|
200
|
+
)}
|
|
201
|
+
</li>
|
|
202
|
+
);
|
|
203
|
+
})}
|
|
204
|
+
</ul>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Info,
|
|
4
|
+
CheckCircle2,
|
|
5
|
+
AlertTriangle,
|
|
6
|
+
ShieldAlert,
|
|
7
|
+
Inbox,
|
|
8
|
+
} from 'lucide-react';
|
|
9
|
+
|
|
10
|
+
export type ZenAlertTone = 'info' | 'success' | 'warning' | 'danger';
|
|
11
|
+
|
|
12
|
+
export interface ZenAlertProps {
|
|
13
|
+
readonly tone?: ZenAlertTone;
|
|
14
|
+
readonly title: string;
|
|
15
|
+
readonly children: ReactNode;
|
|
16
|
+
readonly actionSlot?: ReactNode;
|
|
17
|
+
readonly className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ALERT_STYLES: Record<
|
|
21
|
+
ZenAlertTone,
|
|
22
|
+
{ readonly border: string; readonly icon: ReactNode }
|
|
23
|
+
> = {
|
|
24
|
+
info: {
|
|
25
|
+
border: 'border-[var(--zen-status-info)]/50 bg-[var(--zen-status-info)]/10',
|
|
26
|
+
icon: <Info className="w-4 h-4 text-[var(--zen-status-info)] shrink-0" />,
|
|
27
|
+
},
|
|
28
|
+
success: {
|
|
29
|
+
border:
|
|
30
|
+
'border-[var(--zen-status-success)]/50 bg-[var(--zen-status-success)]/10',
|
|
31
|
+
icon: (
|
|
32
|
+
<CheckCircle2 className="w-4 h-4 text-[var(--zen-status-success)] shrink-0" />
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
warning: {
|
|
36
|
+
border:
|
|
37
|
+
'border-[var(--zen-status-warning)]/50 bg-[var(--zen-status-warning)]/10',
|
|
38
|
+
icon: (
|
|
39
|
+
<AlertTriangle className="w-4 h-4 text-[var(--zen-status-warning)] shrink-0" />
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
danger: {
|
|
43
|
+
border:
|
|
44
|
+
'border-[var(--zen-status-danger)]/50 bg-[var(--zen-status-danger)]/10',
|
|
45
|
+
icon: (
|
|
46
|
+
<ShieldAlert className="w-4 h-4 text-[var(--zen-status-danger)] shrink-0" />
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function ZenAlert({
|
|
52
|
+
tone = 'info',
|
|
53
|
+
title,
|
|
54
|
+
children,
|
|
55
|
+
actionSlot,
|
|
56
|
+
className = '',
|
|
57
|
+
}: ZenAlertProps) {
|
|
58
|
+
const cfg = ALERT_STYLES[tone];
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
role={tone === 'danger' || tone === 'warning' ? 'alert' : 'status'}
|
|
62
|
+
data-zen-occlude="true"
|
|
63
|
+
className={`p-4 rounded-2xl border flex flex-col sm:flex-row sm:items-center justify-between gap-4 ${cfg.border} ${className}`.trim()}
|
|
64
|
+
>
|
|
65
|
+
<div className="flex items-start gap-3">
|
|
66
|
+
<div className="mt-0.5">{cfg.icon}</div>
|
|
67
|
+
<div>
|
|
68
|
+
<div className="zen-font-mono text-xs font-bold text-[var(--zen-ink-primary)]">
|
|
69
|
+
{title}
|
|
70
|
+
</div>
|
|
71
|
+
<div className="text-xs sm:text-sm text-[var(--zen-ink-secondary)] mt-0.5">
|
|
72
|
+
{children}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
{actionSlot && <div className="shrink-0">{actionSlot}</div>}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ZenSkeletonProps {
|
|
82
|
+
readonly lines?: number;
|
|
83
|
+
readonly className?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function ZenSkeleton({ lines = 3, className = '' }: ZenSkeletonProps) {
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
role="status"
|
|
90
|
+
aria-busy="true"
|
|
91
|
+
aria-label="Loading content"
|
|
92
|
+
className={`space-y-2.5 ${className}`.trim()}
|
|
93
|
+
>
|
|
94
|
+
{Array.from({ length: lines }).map((_, idx) => (
|
|
95
|
+
<div
|
|
96
|
+
key={idx}
|
|
97
|
+
style={{ width: idx === lines - 1 ? '65%' : '100%' }}
|
|
98
|
+
className="h-3.5 rounded-lg bg-[var(--zen-surface-subtle-hover)] animate-pulse"
|
|
99
|
+
/>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ZenEmptyStateProps {
|
|
106
|
+
readonly title: string;
|
|
107
|
+
readonly description: string;
|
|
108
|
+
readonly icon?: ReactNode;
|
|
109
|
+
readonly actionSlot?: ReactNode;
|
|
110
|
+
readonly className?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function ZenEmptyState({
|
|
114
|
+
title,
|
|
115
|
+
description,
|
|
116
|
+
icon,
|
|
117
|
+
actionSlot,
|
|
118
|
+
className = '',
|
|
119
|
+
}: ZenEmptyStateProps) {
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
data-zen-occlude="true"
|
|
123
|
+
className={`p-8 rounded-3xl border border-dashed border-[var(--zen-border-strong)] bg-[var(--zen-surface-subtle)] text-center flex flex-col items-center justify-center gap-3 ${className}`.trim()}
|
|
124
|
+
>
|
|
125
|
+
<div className="p-3 rounded-2xl bg-[var(--zen-accent-subtle)] text-[var(--zen-accent-primary)]">
|
|
126
|
+
{icon ?? <Inbox className="w-6 h-6" />}
|
|
127
|
+
</div>
|
|
128
|
+
<div className="zen-font-display text-lg font-bold text-[var(--zen-ink-primary)]">
|
|
129
|
+
{title}
|
|
130
|
+
</div>
|
|
131
|
+
<p className="text-xs sm:text-sm text-[var(--zen-ink-secondary)] max-w-md">
|
|
132
|
+
{description}
|
|
133
|
+
</p>
|
|
134
|
+
{actionSlot && <div className="mt-1">{actionSlot}</div>}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|