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

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 +78 -4
  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,113 @@
1
+ import { useRef, type KeyboardEvent, type ReactNode } from 'react';
2
+
3
+ export interface ZenSegmentItem<T extends string> {
4
+ readonly value: T;
5
+ readonly label: ReactNode;
6
+ readonly icon?: ReactNode;
7
+ readonly count?: string | number;
8
+ readonly disabled?: boolean;
9
+ }
10
+
11
+ export interface ZenSegmentedControlProps<T extends string> {
12
+ readonly items: readonly ZenSegmentItem<T>[];
13
+ readonly value: T;
14
+ readonly onChange: (nextValue: T) => void;
15
+ readonly ariaLabel: string;
16
+ readonly size?: 'sm' | 'md';
17
+ readonly className?: string;
18
+ }
19
+
20
+ export function ZenSegmentedControl<T extends string>({
21
+ items,
22
+ value,
23
+ onChange,
24
+ ariaLabel,
25
+ size = 'md',
26
+ className = '',
27
+ }: ZenSegmentedControlProps<T>) {
28
+ const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
29
+
30
+ const enabledIndices = items
31
+ .map((item, idx) => (item.disabled ? -1 : idx))
32
+ .filter((idx) => idx !== -1);
33
+
34
+ const focusAndSelect = (targetIdx: number) => {
35
+ const target = items[targetIdx];
36
+ if (!target || target.disabled) return;
37
+ buttonRefs.current[targetIdx]?.focus();
38
+ onChange(target.value);
39
+ };
40
+
41
+ const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>, idx: number) => {
42
+ const pos = enabledIndices.indexOf(idx);
43
+ if (pos === -1) return;
44
+
45
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
46
+ e.preventDefault();
47
+ const nextIdx = enabledIndices[(pos + 1) % enabledIndices.length];
48
+ focusAndSelect(nextIdx);
49
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
50
+ e.preventDefault();
51
+ const prevIdx =
52
+ enabledIndices[
53
+ (pos - 1 + enabledIndices.length) % enabledIndices.length
54
+ ];
55
+ focusAndSelect(prevIdx);
56
+ } else if (e.key === 'Home') {
57
+ e.preventDefault();
58
+ focusAndSelect(enabledIndices[0]);
59
+ } else if (e.key === 'End') {
60
+ e.preventDefault();
61
+ focusAndSelect(enabledIndices[enabledIndices.length - 1]);
62
+ }
63
+ };
64
+
65
+ const padClass = size === 'sm' ? 'px-2.5 sm:px-3 py-1.5 text-[11px]' : 'px-3.5 sm:px-4 py-2 text-xs';
66
+
67
+ return (
68
+ <div
69
+ role="radiogroup"
70
+ aria-label={ariaLabel}
71
+ data-zen-occlude="true"
72
+ className={`inline-flex flex-wrap items-center gap-1.5 p-1.5 max-w-full rounded-2xl zen-surface-static ${className}`.trim()}
73
+ >
74
+ {items.map((item, idx) => {
75
+ const isSelected = item.value === value;
76
+ return (
77
+ <button
78
+ key={item.value}
79
+ ref={(el) => {
80
+ buttonRefs.current[idx] = el;
81
+ }}
82
+ type="button"
83
+ role="radio"
84
+ aria-checked={isSelected}
85
+ disabled={item.disabled}
86
+ tabIndex={isSelected ? 0 : -1}
87
+ onClick={() => onChange(item.value)}
88
+ onKeyDown={(e) => handleKeyDown(e, idx)}
89
+ className={`zen-focus-ring rounded-xl zen-font-mono ${padClass} inline-flex items-center gap-1.5 transition-all cursor-pointer ${
90
+ isSelected
91
+ ? 'bg-[var(--zen-accent-primary)] text-[var(--zen-ink-on-accent)] font-semibold shadow-sm'
92
+ : 'text-[var(--zen-ink-secondary)] hover:text-[var(--zen-ink-primary)] hover:bg-[var(--zen-surface-subtle)]'
93
+ } ${item.disabled ? 'opacity-40 cursor-not-allowed' : ''}`.trim()}
94
+ >
95
+ {item.icon && <span className="shrink-0 inline-flex">{item.icon}</span>}
96
+ <span>{item.label}</span>
97
+ {item.count !== undefined && (
98
+ <span
99
+ className={`px-1.5 py-0.2 rounded-full text-[10px] ${
100
+ isSelected
101
+ ? 'bg-black/20 text-[var(--zen-ink-on-accent)]'
102
+ : 'bg-[var(--zen-surface-subtle)] text-[var(--zen-ink-muted)]'
103
+ }`}
104
+ >
105
+ {item.count}
106
+ </span>
107
+ )}
108
+ </button>
109
+ );
110
+ })}
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,63 @@
1
+ import { useId, type ReactNode } from 'react';
2
+
3
+ export interface ZenSwitchProps {
4
+ readonly checked: boolean;
5
+ readonly onChange: (nextChecked: boolean) => void;
6
+ readonly label: ReactNode;
7
+ readonly description?: string;
8
+ readonly disabled?: boolean;
9
+ readonly className?: string;
10
+ }
11
+
12
+ export function ZenSwitch({
13
+ checked,
14
+ onChange,
15
+ label,
16
+ description,
17
+ disabled = false,
18
+ className = '',
19
+ }: ZenSwitchProps) {
20
+ const labelId = useId();
21
+ return (
22
+ <div
23
+ data-zen-occlude="true"
24
+ className={`flex items-center justify-between gap-4 ${className}`.trim()}
25
+ >
26
+ <div className="min-w-0">
27
+ <div
28
+ id={labelId}
29
+ className="text-sm font-medium text-[var(--zen-ink-primary)]"
30
+ >
31
+ {label}
32
+ </div>
33
+ {description && (
34
+ <div className="zen-font-mono text-[11px] text-[var(--zen-ink-muted)] mt-0.5">
35
+ {description}
36
+ </div>
37
+ )}
38
+ </div>
39
+
40
+ <button
41
+ type="button"
42
+ role="switch"
43
+ aria-labelledby={labelId}
44
+ aria-checked={checked}
45
+ disabled={disabled}
46
+ onClick={() => onChange(!checked)}
47
+ className={`zen-focus-ring relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border transition-colors cursor-pointer ${
48
+ checked
49
+ ? 'bg-[var(--zen-accent-primary)] border-[var(--zen-accent-primary)]'
50
+ : 'bg-[var(--zen-surface-subtle)] border-[var(--zen-border-control)]'
51
+ } ${disabled ? 'opacity-40 cursor-not-allowed' : ''}`}
52
+ >
53
+ <span
54
+ className={`inline-block h-4 w-4 rounded-full transition-transform ${
55
+ checked
56
+ ? 'translate-x-5 bg-[var(--zen-ink-on-accent)]'
57
+ : 'translate-x-1 bg-[var(--zen-ink-primary)]'
58
+ }`}
59
+ />
60
+ </button>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,103 @@
1
+ import { useRef, type KeyboardEvent, type ReactNode } from 'react';
2
+
3
+ export interface ZenTabItem<T extends string> {
4
+ readonly id: T;
5
+ readonly label: ReactNode;
6
+ readonly icon?: ReactNode;
7
+ readonly badge?: string | number;
8
+ readonly disabled?: boolean;
9
+ }
10
+
11
+ export interface ZenTabsProps<T extends string> {
12
+ readonly tabs: readonly ZenTabItem<T>[];
13
+ readonly activeTab: T;
14
+ readonly onChange: (nextTab: T) => void;
15
+ readonly ariaLabel: string;
16
+ readonly className?: string;
17
+ }
18
+
19
+ export function ZenTabs<T extends string>({
20
+ tabs,
21
+ activeTab,
22
+ onChange,
23
+ ariaLabel,
24
+ className = '',
25
+ }: ZenTabsProps<T>) {
26
+ const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
27
+
28
+ const enabledIndices = tabs
29
+ .map((t, idx) => (t.disabled ? -1 : idx))
30
+ .filter((idx) => idx !== -1);
31
+
32
+ const focusAndActivate = (targetIdx: number) => {
33
+ const target = tabs[targetIdx];
34
+ if (!target || target.disabled) return;
35
+ tabRefs.current[targetIdx]?.focus();
36
+ onChange(target.id);
37
+ };
38
+
39
+ const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>, idx: number) => {
40
+ const pos = enabledIndices.indexOf(idx);
41
+ if (pos === -1) return;
42
+
43
+ if (e.key === 'ArrowRight') {
44
+ e.preventDefault();
45
+ const nextIdx = enabledIndices[(pos + 1) % enabledIndices.length];
46
+ focusAndActivate(nextIdx);
47
+ } else if (e.key === 'ArrowLeft') {
48
+ e.preventDefault();
49
+ const prevIdx =
50
+ enabledIndices[
51
+ (pos - 1 + enabledIndices.length) % enabledIndices.length
52
+ ];
53
+ focusAndActivate(prevIdx);
54
+ } else if (e.key === 'Home') {
55
+ e.preventDefault();
56
+ focusAndActivate(enabledIndices[0]);
57
+ } else if (e.key === 'End') {
58
+ e.preventDefault();
59
+ focusAndActivate(enabledIndices[enabledIndices.length - 1]);
60
+ }
61
+ };
62
+
63
+ return (
64
+ <div
65
+ role="tablist"
66
+ aria-label={ariaLabel}
67
+ data-zen-occlude="true"
68
+ className={`flex flex-wrap items-center gap-1.5 max-w-full border-b border-[var(--zen-border-subtle)] pb-2 ${className}`.trim()}
69
+ >
70
+ {tabs.map((tab, idx) => {
71
+ const isSelected = tab.id === activeTab;
72
+ return (
73
+ <button
74
+ key={tab.id}
75
+ ref={(el) => {
76
+ tabRefs.current[idx] = el;
77
+ }}
78
+ type="button"
79
+ role="tab"
80
+ aria-selected={isSelected}
81
+ disabled={tab.disabled}
82
+ tabIndex={isSelected ? 0 : -1}
83
+ onClick={() => onChange(tab.id)}
84
+ onKeyDown={(e) => handleKeyDown(e, idx)}
85
+ className={`zen-focus-ring px-3 sm:px-4 py-2 rounded-xl zen-font-mono text-xs inline-flex items-center gap-2 shrink-0 transition-all cursor-pointer ${
86
+ isSelected
87
+ ? 'bg-[var(--zen-accent-subtle)] text-[var(--zen-accent-primary)] border border-[var(--zen-accent-border)] font-semibold'
88
+ : 'text-[var(--zen-ink-secondary)] hover:text-[var(--zen-ink-primary)] hover:bg-[var(--zen-surface-subtle)] border border-transparent'
89
+ } ${tab.disabled ? 'opacity-40 cursor-not-allowed' : ''}`.trim()}
90
+ >
91
+ {tab.icon && <span className="shrink-0 inline-flex">{tab.icon}</span>}
92
+ <span>{tab.label}</span>
93
+ {tab.badge !== undefined && (
94
+ <span className="px-1.5 py-0.5 rounded-full text-[10px] bg-[var(--zen-surface-subtle)] text-[var(--zen-ink-muted)]">
95
+ {tab.badge}
96
+ </span>
97
+ )}
98
+ </button>
99
+ );
100
+ })}
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,64 @@
1
+ import type { ReactNode } from 'react';
2
+ import { ZenCard, ZenCheckList } from './ZenCard';
3
+ import { ZenTagList } from './ZenBadge';
4
+
5
+ export interface ZenTimelineCardProps {
6
+ readonly period: string;
7
+ readonly location: string;
8
+ readonly title: string;
9
+ readonly institution: string;
10
+ readonly summary: string;
11
+ readonly bullets?: readonly string[];
12
+ readonly tags: readonly string[];
13
+ readonly icon?: ReactNode;
14
+ readonly className?: string;
15
+ }
16
+
17
+ export function ZenTimelineCard({
18
+ period,
19
+ location,
20
+ title,
21
+ institution,
22
+ summary,
23
+ bullets,
24
+ tags,
25
+ icon,
26
+ className = '',
27
+ }: ZenTimelineCardProps) {
28
+ return (
29
+ <ZenCard
30
+ variant="static"
31
+ padding="md"
32
+ className={`flex flex-col justify-between ${className}`.trim()}
33
+ >
34
+ <div>
35
+ <div className="flex items-center justify-between gap-2 zen-font-mono text-xs mb-4">
36
+ <span className="inline-flex items-center gap-1.5 text-[var(--zen-accent-primary)]">
37
+ {icon}
38
+ <span>{period}</span>
39
+ </span>
40
+ <span className="text-[var(--zen-ink-muted)]">{location}</span>
41
+ </div>
42
+
43
+ <h3 className="zen-font-display text-xl sm:text-2xl font-bold text-[var(--zen-ink-primary)]">
44
+ {title}
45
+ </h3>
46
+ <div className="zen-font-serif italic text-xl text-[var(--zen-accent-primary)] mt-0.5 mb-3">
47
+ {institution}
48
+ </div>
49
+
50
+ <p className="text-sm text-[var(--zen-ink-secondary)] leading-relaxed mb-4">
51
+ {summary}
52
+ </p>
53
+
54
+ {bullets && bullets.length > 0 && (
55
+ <ZenCheckList items={bullets} className="mb-5" />
56
+ )}
57
+ </div>
58
+
59
+ <div className="pt-3 border-t border-[var(--zen-border-subtle)]">
60
+ <ZenTagList tags={tags} />
61
+ </div>
62
+ </ZenCard>
63
+ );
64
+ }
@@ -0,0 +1,96 @@
1
+ import { useState, useCallback, type ReactNode } from 'react';
2
+ import { CheckCircle2, AlertTriangle, Info, X } from 'lucide-react';
3
+
4
+ export type ZenToastTone = 'info' | 'success' | 'warning';
5
+
6
+ export interface ZenToastItem {
7
+ readonly id: string;
8
+ readonly title: string;
9
+ readonly detail?: string;
10
+ readonly tone?: ZenToastTone;
11
+ }
12
+
13
+ export interface ZenToastStackProps {
14
+ readonly toasts: readonly ZenToastItem[];
15
+ readonly onDismiss: (id: string) => void;
16
+ }
17
+
18
+ const TONE_ICONS: Record<ZenToastTone, ReactNode> = {
19
+ info: <Info className="w-4 h-4 text-[var(--zen-status-info)] shrink-0" />,
20
+ success: (
21
+ <CheckCircle2 className="w-4 h-4 text-[var(--zen-status-success)] shrink-0" />
22
+ ),
23
+ warning: (
24
+ <AlertTriangle className="w-4 h-4 text-[var(--zen-accent-primary)] shrink-0" />
25
+ ),
26
+ };
27
+
28
+ export function ZenToastStack({ toasts, onDismiss }: ZenToastStackProps) {
29
+ if (toasts.length === 0) return null;
30
+
31
+ return (
32
+ <div
33
+ aria-live="polite"
34
+ data-zen-occlude="true"
35
+ className="fixed bottom-4 inset-x-4 sm:inset-x-auto sm:bottom-6 sm:right-6 z-50 flex flex-col gap-2.5 sm:max-w-sm sm:w-full pointer-events-none"
36
+ >
37
+ {toasts.map((t) => {
38
+ const tone = t.tone ?? 'info';
39
+ return (
40
+ <div
41
+ key={t.id}
42
+ role="status"
43
+ className="pointer-events-auto flex items-start justify-between gap-3 p-4 rounded-2xl bg-[var(--zen-bg-elevated)] border border-[var(--zen-border-strong)] shadow-[var(--zen-shadow-hud)] backdrop-blur-xl"
44
+ >
45
+ <div className="flex items-start gap-2.5 min-w-0">
46
+ <div className="mt-0.5">{TONE_ICONS[tone]}</div>
47
+ <div className="min-w-0">
48
+ <div className="zen-font-mono text-xs font-semibold text-[var(--zen-ink-primary)]">
49
+ {t.title}
50
+ </div>
51
+ {t.detail && (
52
+ <div className="text-xs text-[var(--zen-ink-secondary)] mt-0.5 break-words">
53
+ {t.detail}
54
+ </div>
55
+ )}
56
+ </div>
57
+ </div>
58
+ <button
59
+ type="button"
60
+ aria-label="Dismiss notification"
61
+ onClick={() => onDismiss(t.id)}
62
+ className="zen-focus-ring p-1 rounded-lg text-[var(--zen-ink-muted)] hover:text-[var(--zen-ink-primary)] cursor-pointer shrink-0"
63
+ >
64
+ <X className="w-3.5 h-3.5" />
65
+ </button>
66
+ </div>
67
+ );
68
+ })}
69
+ </div>
70
+ );
71
+ }
72
+
73
+ export function useZenToast() {
74
+ const [toasts, setToasts] = useState<readonly ZenToastItem[]>([]);
75
+
76
+ const dismissToast = useCallback((id: string) => {
77
+ setToasts((prev) => prev.filter((item) => item.id !== id));
78
+ }, []);
79
+
80
+ const pushToast = useCallback(
81
+ (toast: Omit<ZenToastItem, 'id'>) => {
82
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
83
+ setToasts((prev) => [...prev.slice(-3), { ...toast, id }]);
84
+ window.setTimeout(() => {
85
+ dismissToast(id);
86
+ }, 4200);
87
+ },
88
+ [dismissToast]
89
+ );
90
+
91
+ return {
92
+ toasts,
93
+ pushToast,
94
+ dismissToast,
95
+ };
96
+ }