@medalsocial/meda 0.2.0 → 1.0.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.
- package/README.md +26 -0
- package/dist/brand/index.d.ts +2 -0
- package/dist/brand/index.js +1 -0
- package/dist/brand/medal-social-mark.d.ts +7 -0
- package/dist/brand/medal-social-mark.js +6 -0
- package/dist/brand/public.d.ts +2 -0
- package/dist/brand/public.js +1 -0
- package/dist/chat/index.d.ts +11 -0
- package/dist/chat/index.js +5 -0
- package/dist/chat/latency-badge.d.ts +7 -0
- package/dist/chat/latency-badge.js +23 -0
- package/dist/chat/latency-breakdown.d.ts +8 -0
- package/dist/chat/latency-breakdown.js +9 -0
- package/dist/chat/public.d.ts +1 -0
- package/dist/chat/public.js +1 -0
- package/dist/chat/tool-call-block.d.ts +6 -0
- package/dist/chat/tool-call-block.js +19 -0
- package/dist/chat/transcript-stream.d.ts +10 -0
- package/dist/chat/transcript-stream.js +40 -0
- package/dist/chat/turn-card.d.ts +10 -0
- package/dist/chat/turn-card.js +25 -0
- package/dist/chat/types.d.ts +36 -0
- package/dist/chat/types.js +1 -0
- package/dist/components/ui/command.d.ts +19 -0
- package/dist/components/ui/command.js +37 -0
- package/dist/components/ui/dialog.d.ts +26 -0
- package/dist/components/ui/dialog.js +39 -0
- package/dist/components/ui/drawer.d.ts +16 -0
- package/dist/components/ui/drawer.js +35 -0
- package/dist/components/ui/dropdown-menu.d.ts +32 -0
- package/dist/components/ui/dropdown-menu.js +68 -0
- package/dist/components/ui/tooltip.d.ts +6 -0
- package/dist/components/ui/tooltip.js +17 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/lib/format-time.d.ts +9 -0
- package/dist/lib/format-time.js +34 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/marketing/index.d.ts +5 -0
- package/dist/marketing/index.js +4 -0
- package/dist/marketing/marketing-callout.d.ts +2 -0
- package/dist/marketing/marketing-callout.js +8 -0
- package/dist/marketing/marketing-contact.d.ts +2 -0
- package/dist/marketing/marketing-contact.js +19 -0
- package/dist/marketing/marketing-cta-list.d.ts +2 -0
- package/dist/marketing/marketing-cta-list.js +21 -0
- package/dist/marketing/marketing-lead-magnet.d.ts +2 -0
- package/dist/marketing/marketing-lead-magnet.js +9 -0
- package/dist/marketing/public.d.ts +1 -0
- package/dist/marketing/public.js +1 -0
- package/dist/marketing/types.d.ts +67 -0
- package/dist/marketing/types.js +1 -0
- package/dist/marketing/utils.d.ts +2 -0
- package/dist/marketing/utils.js +6 -0
- package/dist/panel/index.d.ts +6 -0
- package/dist/panel/index.js +3 -0
- package/dist/panel/inspector-field.d.ts +9 -0
- package/dist/panel/inspector-field.js +4 -0
- package/dist/panel/inspector-json.d.ts +7 -0
- package/dist/panel/inspector-json.js +75 -0
- package/dist/panel/inspector.d.ts +7 -0
- package/dist/panel/inspector.js +12 -0
- package/dist/panel/public.d.ts +1 -0
- package/dist/panel/public.js +1 -0
- package/dist/panel/types.d.ts +6 -0
- package/dist/panel/types.js +1 -0
- package/dist/shell/app-shell.d.ts +9 -0
- package/dist/shell/app-shell.js +11 -0
- package/dist/shell/command-palette.d.ts +23 -0
- package/dist/shell/command-palette.js +138 -0
- package/dist/shell/context-rail.d.ts +31 -0
- package/dist/shell/context-rail.js +81 -0
- package/dist/shell/extras/index.d.ts +4 -0
- package/dist/shell/extras/index.js +4 -0
- package/dist/shell/extras/shell-desktop-panel-dock.d.ts +14 -0
- package/dist/shell/extras/shell-desktop-panel-dock.js +43 -0
- package/dist/shell/extras/shell-scrollable-content.d.ts +9 -0
- package/dist/shell/extras/shell-scrollable-content.js +7 -0
- package/dist/shell/extras/shell-tab-bar.d.ts +18 -0
- package/dist/shell/extras/shell-tab-bar.js +27 -0
- package/dist/shell/extras/types.d.ts +64 -0
- package/dist/shell/extras/types.js +2 -0
- package/dist/shell/extras/workbench-layout.d.ts +10 -0
- package/dist/shell/extras/workbench-layout.js +29 -0
- package/dist/shell/icon-rail.d.ts +29 -0
- package/dist/shell/icon-rail.js +38 -0
- package/dist/shell/index.d.ts +24 -15
- package/dist/shell/index.js +35 -15
- package/dist/shell/layout-state.d.ts +32 -0
- package/dist/shell/layout-state.js +98 -0
- package/dist/shell/mobile/mobile-bottom-nav.d.ts +15 -0
- package/dist/shell/mobile/mobile-bottom-nav.js +76 -0
- package/dist/shell/mobile/mobile-drawers.d.ts +20 -0
- package/dist/shell/mobile/mobile-drawers.js +62 -0
- package/dist/shell/mobile/mobile-header.d.ts +23 -0
- package/dist/shell/mobile/mobile-header.js +22 -0
- package/dist/shell/motion.d.ts +22 -0
- package/dist/shell/motion.js +19 -0
- package/dist/shell/public.d.ts +16 -2
- package/dist/shell/public.js +20 -1
- package/dist/shell/resizable-shell.d.ts +45 -0
- package/dist/shell/resizable-shell.js +15 -0
- package/dist/shell/right-panel.d.ts +15 -0
- package/dist/shell/right-panel.js +126 -0
- package/dist/shell/shell-auth-frame.d.ts +22 -0
- package/dist/shell/shell-auth-frame.js +60 -0
- package/dist/shell/shell-header.d.ts +10 -9
- package/dist/shell/shell-header.js +57 -14
- package/dist/shell/shell-layout-utils.d.ts +2 -2
- package/dist/shell/shell-main.d.ts +8 -0
- package/dist/shell/shell-main.js +11 -0
- package/dist/shell/shell-provider.d.ts +65 -0
- package/dist/shell/shell-provider.js +152 -0
- package/dist/shell/shell-route-utils.d.ts +1 -1
- package/dist/shell/theme-next-themes.d.ts +4 -0
- package/dist/shell/theme-next-themes.js +24 -0
- package/dist/shell/theme.d.ts +9 -0
- package/dist/shell/theme.js +58 -0
- package/dist/shell/types.d.ts +41 -41
- package/dist/shell/types.js +1 -0
- package/dist/shell/use-shell-viewport.d.ts +2 -0
- package/dist/shell/use-shell-viewport.js +40 -0
- package/dist/shell/utils.d.ts +1 -1
- package/dist/styles/globals.css +26 -0
- package/dist/styles/theme.css +147 -0
- package/dist/styles/tokens.css +338 -0
- package/dist/timeline/date-switcher.d.ts +11 -0
- package/dist/timeline/date-switcher.js +64 -0
- package/dist/timeline/event-card.d.ts +9 -0
- package/dist/timeline/event-card.js +18 -0
- package/dist/timeline/index.d.ts +13 -0
- package/dist/timeline/index.js +6 -0
- package/dist/timeline/live-indicator.d.ts +7 -0
- package/dist/timeline/live-indicator.js +7 -0
- package/dist/timeline/public.d.ts +1 -0
- package/dist/timeline/public.js +1 -0
- package/dist/timeline/scrub-bar.d.ts +14 -0
- package/dist/timeline/scrub-bar.js +43 -0
- package/dist/timeline/timeline-rail.d.ts +19 -0
- package/dist/timeline/timeline-rail.js +66 -0
- package/dist/timeline/timeline-tape.d.ts +19 -0
- package/dist/timeline/timeline-tape.js +65 -0
- package/dist/timeline/types.d.ts +40 -0
- package/dist/timeline/types.js +1 -0
- package/dist/voice/voice-level.js +3 -1
- package/dist/voice/voice-orb-scene.js +1 -0
- package/dist/voice/voice-orb.js +9 -11
- package/dist/voice/voice-status-pill.js +4 -9
- package/package.json +67 -7
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
Meda design tokens — canonical from 00-design-system.lib.pen
|
|
3
|
+
Source: /Users/ali/Library/CloudStorage/GoogleDrive-ali@medalsocial.com/
|
|
4
|
+
Delte disker/03 Product/Pencil Design/00-design-system.lib.pen
|
|
5
|
+
Retrieved via mcp__pencil__get_variables 2026-04-27.
|
|
6
|
+
|
|
7
|
+
NOTE: 00-design-system.pen and 050-meda-ui.pen are illustrative
|
|
8
|
+
companion boards — not normative. This file is derived from the
|
|
9
|
+
.lib.pen (library file) which is the sole canonical contract.
|
|
10
|
+
============================================================ */
|
|
11
|
+
|
|
12
|
+
/* ============================================================
|
|
13
|
+
Layer 0 — Color primitives (theme: brand, constant)
|
|
14
|
+
============================================================ */
|
|
15
|
+
:root {
|
|
16
|
+
/* Brand (purple) — 11 stops */
|
|
17
|
+
--color-brand-50: #fafafd;
|
|
18
|
+
--color-brand-100: #eeeaf5;
|
|
19
|
+
--color-brand-200: #dcd4e8;
|
|
20
|
+
--color-brand-300: #b8a3d2;
|
|
21
|
+
--color-brand-400: #9a6ac2;
|
|
22
|
+
--color-brand-500: #7e3fac;
|
|
23
|
+
--color-brand-600: #6a2e96;
|
|
24
|
+
--color-brand-700: #482070;
|
|
25
|
+
--color-brand-800: #2f1552;
|
|
26
|
+
--color-brand-900: #1c0e38;
|
|
27
|
+
--color-brand-950: #120a24;
|
|
28
|
+
|
|
29
|
+
/* Neutral (zinc-aligned) — 11 stops */
|
|
30
|
+
--color-neutral-50: #fafafa;
|
|
31
|
+
--color-neutral-100: #f4f4f5;
|
|
32
|
+
--color-neutral-200: #e4e4e7;
|
|
33
|
+
--color-neutral-300: #a1a1aa;
|
|
34
|
+
--color-neutral-400: #71717a;
|
|
35
|
+
--color-neutral-500: #52525b;
|
|
36
|
+
--color-neutral-600: #3f3f46;
|
|
37
|
+
--color-neutral-700: #27272a;
|
|
38
|
+
--color-neutral-800: #18181b;
|
|
39
|
+
--color-neutral-900: #111113;
|
|
40
|
+
--color-neutral-950: #09090b;
|
|
41
|
+
|
|
42
|
+
/* Error (rose) — 10 stops */
|
|
43
|
+
--color-error-50: #fff1f2;
|
|
44
|
+
--color-error-100: #ffe4e6;
|
|
45
|
+
--color-error-200: #fecdd3;
|
|
46
|
+
--color-error-300: #fda4af;
|
|
47
|
+
--color-error-400: #fb7185;
|
|
48
|
+
--color-error-500: #f43f5e;
|
|
49
|
+
--color-error-600: #e11d48;
|
|
50
|
+
--color-error-700: #be123c;
|
|
51
|
+
--color-error-800: #9f1239;
|
|
52
|
+
--color-error-900: #881337;
|
|
53
|
+
|
|
54
|
+
/* Info (blue) — 10 stops */
|
|
55
|
+
--color-info-50: #eff6ff;
|
|
56
|
+
--color-info-100: #dbeafe;
|
|
57
|
+
--color-info-200: #bfdbfe;
|
|
58
|
+
--color-info-300: #93c5fd;
|
|
59
|
+
--color-info-400: #60a5fa;
|
|
60
|
+
--color-info-500: #3b82f6;
|
|
61
|
+
--color-info-600: #2563eb;
|
|
62
|
+
--color-info-700: #1d4ed8;
|
|
63
|
+
--color-info-800: #1e3a8a;
|
|
64
|
+
--color-info-900: #172554;
|
|
65
|
+
|
|
66
|
+
/* Success (teal) — 10 stops */
|
|
67
|
+
--color-success-50: #f0fdfa;
|
|
68
|
+
--color-success-100: #ccfbf1;
|
|
69
|
+
--color-success-200: #99f6e4;
|
|
70
|
+
--color-success-300: #5eead4;
|
|
71
|
+
--color-success-400: #2dd4bf;
|
|
72
|
+
--color-success-500: #14b8a6;
|
|
73
|
+
--color-success-600: #0d9488;
|
|
74
|
+
--color-success-700: #0f766e;
|
|
75
|
+
--color-success-800: #115e59;
|
|
76
|
+
--color-success-900: #134e4a;
|
|
77
|
+
|
|
78
|
+
/* Warning (amber) — 10 stops */
|
|
79
|
+
--color-warning-50: #fffbeb;
|
|
80
|
+
--color-warning-100: #fef3c7;
|
|
81
|
+
--color-warning-200: #fde68a;
|
|
82
|
+
--color-warning-300: #fcd34d;
|
|
83
|
+
--color-warning-400: #fbbf24;
|
|
84
|
+
--color-warning-500: #f59e0b;
|
|
85
|
+
--color-warning-600: #d97706;
|
|
86
|
+
--color-warning-700: #b45309;
|
|
87
|
+
--color-warning-800: #92400e;
|
|
88
|
+
--color-warning-900: #78350f;
|
|
89
|
+
|
|
90
|
+
/* Surface primitives (dark-mode only) */
|
|
91
|
+
--color-surface-bg: #09090b;
|
|
92
|
+
--color-surface-card: #18181b;
|
|
93
|
+
--color-surface-muted: #27272a;
|
|
94
|
+
--color-surface-border: #2e2e33;
|
|
95
|
+
--color-surface-sidebar: #0f0f12;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ============================================================
|
|
99
|
+
Layer 1 — shadcn semantic + status + sidebar + chart + text
|
|
100
|
+
Light mode (default on :root)
|
|
101
|
+
============================================================ */
|
|
102
|
+
:root {
|
|
103
|
+
/* shadcn shape */
|
|
104
|
+
--background: var(--color-neutral-50);
|
|
105
|
+
--foreground: var(--color-neutral-950);
|
|
106
|
+
--card: var(--color-neutral-50);
|
|
107
|
+
--card-foreground: var(--color-neutral-950);
|
|
108
|
+
--popover: var(--color-neutral-50);
|
|
109
|
+
--popover-foreground: var(--color-neutral-950);
|
|
110
|
+
--primary: var(--color-brand-800);
|
|
111
|
+
--primary-foreground: var(--color-brand-50);
|
|
112
|
+
--secondary: var(--color-neutral-200);
|
|
113
|
+
--secondary-foreground: var(--color-neutral-950);
|
|
114
|
+
--muted: var(--color-neutral-100);
|
|
115
|
+
--muted-foreground: var(--color-neutral-500);
|
|
116
|
+
--muted-foreground-strong: var(--color-neutral-600);
|
|
117
|
+
--accent: var(--color-neutral-200);
|
|
118
|
+
--accent-foreground: var(--color-neutral-950);
|
|
119
|
+
--destructive: var(--color-error-600);
|
|
120
|
+
--destructive-foreground: var(--color-neutral-50);
|
|
121
|
+
--destructive-bg: var(--color-error-50);
|
|
122
|
+
--danger: var(--color-error-600);
|
|
123
|
+
--border: var(--color-neutral-200);
|
|
124
|
+
--input: var(--color-neutral-200);
|
|
125
|
+
--ring: var(--color-brand-600);
|
|
126
|
+
|
|
127
|
+
/* Status */
|
|
128
|
+
--success: var(--color-success-600);
|
|
129
|
+
--success-foreground: var(--color-neutral-950);
|
|
130
|
+
--success-text: var(--color-success-700);
|
|
131
|
+
--warning: var(--color-warning-600);
|
|
132
|
+
--warning-foreground: var(--color-neutral-950);
|
|
133
|
+
--warning-text: var(--color-warning-800);
|
|
134
|
+
--info: var(--color-info-600);
|
|
135
|
+
--info-foreground: var(--color-neutral-50);
|
|
136
|
+
|
|
137
|
+
/* Sidebar */
|
|
138
|
+
--sidebar: var(--color-neutral-100);
|
|
139
|
+
--sidebar-foreground: var(--color-neutral-950);
|
|
140
|
+
--sidebar-primary: var(--color-brand-700);
|
|
141
|
+
--sidebar-primary-foreground: var(--color-neutral-50);
|
|
142
|
+
--sidebar-accent: var(--color-neutral-50);
|
|
143
|
+
--sidebar-accent-foreground: var(--color-neutral-950);
|
|
144
|
+
--sidebar-border: var(--color-neutral-200);
|
|
145
|
+
--sidebar-ring: var(--color-brand-500);
|
|
146
|
+
|
|
147
|
+
/* Chart */
|
|
148
|
+
--chart-1: var(--color-brand-600);
|
|
149
|
+
--chart-2: var(--color-info-600);
|
|
150
|
+
--chart-3: var(--color-success-600);
|
|
151
|
+
--chart-4: var(--color-warning-600);
|
|
152
|
+
--chart-5: var(--color-error-600);
|
|
153
|
+
|
|
154
|
+
/* Text */
|
|
155
|
+
--text: var(--color-neutral-950);
|
|
156
|
+
--text-foreground: var(--color-neutral-950);
|
|
157
|
+
--text-muted: var(--color-neutral-500);
|
|
158
|
+
--text-link: var(--color-brand-600);
|
|
159
|
+
--text-primary: var(--color-neutral-950);
|
|
160
|
+
--text-secondary: var(--color-neutral-600);
|
|
161
|
+
--highlight-foreground: var(--color-brand-500);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Dark mode overrides — class AND data-theme to support both runtime + Storybook */
|
|
165
|
+
.dark,
|
|
166
|
+
[data-theme="dark"] {
|
|
167
|
+
/* shadcn shape */
|
|
168
|
+
--background: var(--color-surface-bg);
|
|
169
|
+
--foreground: var(--color-neutral-50);
|
|
170
|
+
--card: var(--color-surface-card);
|
|
171
|
+
--card-foreground: var(--color-neutral-50);
|
|
172
|
+
--popover: var(--color-surface-card);
|
|
173
|
+
--popover-foreground: var(--color-neutral-50);
|
|
174
|
+
--primary: var(--color-brand-100);
|
|
175
|
+
--primary-foreground: var(--color-brand-900);
|
|
176
|
+
--secondary: var(--color-surface-muted);
|
|
177
|
+
--secondary-foreground: var(--color-neutral-50);
|
|
178
|
+
--muted: var(--color-surface-muted);
|
|
179
|
+
--muted-foreground: var(--color-neutral-300);
|
|
180
|
+
--muted-foreground-strong: var(--color-neutral-300);
|
|
181
|
+
--accent: var(--color-surface-muted);
|
|
182
|
+
--accent-foreground: var(--color-neutral-50);
|
|
183
|
+
--destructive: var(--color-error-400);
|
|
184
|
+
--destructive-foreground: var(--color-neutral-950);
|
|
185
|
+
--destructive-bg: var(--color-error-900);
|
|
186
|
+
--danger: var(--color-error-400);
|
|
187
|
+
--border: var(--color-surface-border);
|
|
188
|
+
--input: var(--color-surface-muted);
|
|
189
|
+
--ring: var(--color-brand-400);
|
|
190
|
+
|
|
191
|
+
/* Status */
|
|
192
|
+
--success: var(--color-success-400);
|
|
193
|
+
--success-foreground: var(--color-neutral-950);
|
|
194
|
+
--success-text: var(--color-success-400);
|
|
195
|
+
--warning: var(--color-warning-400);
|
|
196
|
+
--warning-foreground: var(--color-neutral-950);
|
|
197
|
+
--warning-text: var(--color-warning-400);
|
|
198
|
+
--info: var(--color-info-400);
|
|
199
|
+
--info-foreground: var(--color-neutral-950);
|
|
200
|
+
|
|
201
|
+
/* Sidebar */
|
|
202
|
+
--sidebar: var(--color-surface-sidebar);
|
|
203
|
+
--sidebar-foreground: var(--color-neutral-50);
|
|
204
|
+
--sidebar-primary: var(--color-brand-400);
|
|
205
|
+
--sidebar-primary-foreground: var(--color-neutral-950);
|
|
206
|
+
--sidebar-accent: var(--color-surface-card);
|
|
207
|
+
--sidebar-accent-foreground: var(--color-neutral-50);
|
|
208
|
+
--sidebar-border: var(--color-surface-border);
|
|
209
|
+
--sidebar-ring: var(--color-brand-400);
|
|
210
|
+
|
|
211
|
+
/* Chart */
|
|
212
|
+
--chart-1: var(--color-brand-500);
|
|
213
|
+
--chart-2: var(--color-info-500);
|
|
214
|
+
--chart-3: var(--color-success-400);
|
|
215
|
+
--chart-4: var(--color-warning-500);
|
|
216
|
+
--chart-5: var(--color-error-400);
|
|
217
|
+
|
|
218
|
+
/* Text */
|
|
219
|
+
--text: var(--color-neutral-50);
|
|
220
|
+
--text-foreground: var(--color-neutral-50);
|
|
221
|
+
--text-muted: var(--color-neutral-300);
|
|
222
|
+
--text-link: var(--color-brand-400);
|
|
223
|
+
--text-primary: var(--color-neutral-100);
|
|
224
|
+
--text-secondary: var(--color-neutral-300);
|
|
225
|
+
--highlight-foreground: var(--color-brand-200);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ============================================================
|
|
229
|
+
Layer 2 — Shell semantic + sizing
|
|
230
|
+
============================================================ */
|
|
231
|
+
:root {
|
|
232
|
+
--shell-header: var(--color-neutral-50);
|
|
233
|
+
--shell-rail: var(--color-neutral-50);
|
|
234
|
+
--shell-context: var(--color-neutral-50);
|
|
235
|
+
--shell-main: var(--color-neutral-100);
|
|
236
|
+
--shell-panel: var(--color-neutral-50);
|
|
237
|
+
--shell-border: var(--color-neutral-200);
|
|
238
|
+
--shell-shadow: 0 4px 12px hsl(0 0% 0% / 0.04);
|
|
239
|
+
--shell-header-height: 56px;
|
|
240
|
+
--shell-rail-width: 60px;
|
|
241
|
+
--shell-context-default: 300px;
|
|
242
|
+
--shell-context-min: 240px;
|
|
243
|
+
--shell-context-max: 420px;
|
|
244
|
+
--shell-panel-default: 340px;
|
|
245
|
+
--shell-panel-min: 300px;
|
|
246
|
+
--shell-panel-max: 520px;
|
|
247
|
+
--shell-bottom-nav-height: 56px;
|
|
248
|
+
--z-shell-header: 40;
|
|
249
|
+
--z-shell-panel: 45;
|
|
250
|
+
--z-shell-fullscreen: 60;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.dark,
|
|
254
|
+
[data-theme="dark"] {
|
|
255
|
+
--shell-header: var(--color-surface-card);
|
|
256
|
+
--shell-rail: var(--color-surface-sidebar);
|
|
257
|
+
--shell-context: var(--color-surface-sidebar);
|
|
258
|
+
--shell-main: var(--color-surface-bg);
|
|
259
|
+
--shell-panel: var(--color-surface-card);
|
|
260
|
+
--shell-border: var(--color-surface-border);
|
|
261
|
+
--shell-shadow: 0 4px 12px hsl(0 0% 0% / 0.45);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* ============================================================
|
|
265
|
+
Motion (theme: base, constant)
|
|
266
|
+
Mirror of motion.ts (Phase 5)
|
|
267
|
+
============================================================ */
|
|
268
|
+
:root {
|
|
269
|
+
--motion-fast: 140ms;
|
|
270
|
+
--motion-default: 200ms;
|
|
271
|
+
--motion-panel: 240ms;
|
|
272
|
+
--motion-fullscreen: 280ms;
|
|
273
|
+
--motion-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* ============================================================
|
|
277
|
+
Typography (theme: base, constant)
|
|
278
|
+
Font family: Geist + Geist Mono only. No Inter.
|
|
279
|
+
============================================================ */
|
|
280
|
+
:root {
|
|
281
|
+
--font-family-sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
282
|
+
--font-family-mono: "Geist Mono", "SF Mono", "Fira Code", monospace;
|
|
283
|
+
--font-size-display: 36px;
|
|
284
|
+
--font-size-h1: 30px;
|
|
285
|
+
--font-size-h2: 24px;
|
|
286
|
+
--font-size-h3: 20px;
|
|
287
|
+
--font-size-h4: 18px;
|
|
288
|
+
--font-size-body-lg: 16px;
|
|
289
|
+
--font-size-body: 14px;
|
|
290
|
+
--font-size-body-sm: 13px;
|
|
291
|
+
--font-size-caption: 11px;
|
|
292
|
+
--font-size-overline: 10px;
|
|
293
|
+
--font-size-mono-data: 12px;
|
|
294
|
+
--font-size-mono-caption: 10px;
|
|
295
|
+
--font-weight-regular: 400;
|
|
296
|
+
--font-weight-medium: 500;
|
|
297
|
+
--font-weight-semibold: 600;
|
|
298
|
+
--font-weight-bold: 700;
|
|
299
|
+
--line-height-display: 1.1;
|
|
300
|
+
--line-height-heading: 1.15;
|
|
301
|
+
--line-height-tight: 1.2;
|
|
302
|
+
--line-height-snug: 1.25;
|
|
303
|
+
--line-height-normal: 1.5;
|
|
304
|
+
--line-height-relaxed: 1.6;
|
|
305
|
+
--letter-spacing-tighter: -0.5px;
|
|
306
|
+
--letter-spacing-tight: -0.2px;
|
|
307
|
+
--letter-spacing-normal: 0;
|
|
308
|
+
--letter-spacing-wide: 1.2px;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* ============================================================
|
|
312
|
+
Radii (theme: base, constant)
|
|
313
|
+
============================================================ */
|
|
314
|
+
:root {
|
|
315
|
+
--radius: 8px;
|
|
316
|
+
--radius-sm: 4px;
|
|
317
|
+
--radius-md: 6px;
|
|
318
|
+
--radius-lg: 8px;
|
|
319
|
+
--radius-xl: 12px;
|
|
320
|
+
--radius-2xl: 16px;
|
|
321
|
+
--radius-3xl: 20px;
|
|
322
|
+
--radius-4xl: 24px;
|
|
323
|
+
--radius-full: 9999px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* ============================================================
|
|
327
|
+
Spacing scale (theme: base, constant)
|
|
328
|
+
============================================================ */
|
|
329
|
+
:root {
|
|
330
|
+
--spacing-xs: 4px;
|
|
331
|
+
--spacing-sm: 8px;
|
|
332
|
+
--spacing-md: 12px;
|
|
333
|
+
--spacing-lg: 16px;
|
|
334
|
+
--spacing-xl: 24px;
|
|
335
|
+
--spacing-2xl: 32px;
|
|
336
|
+
--spacing-3xl: 48px;
|
|
337
|
+
--spacing-4xl: 64px;
|
|
338
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DateSwitcherProps {
|
|
2
|
+
/** Currently selected date. */
|
|
3
|
+
value: Date;
|
|
4
|
+
/** Reference for "today" comparison and forward-disable. */
|
|
5
|
+
now: Date;
|
|
6
|
+
/** IANA timezone for "same day" / display semantics. Defaults to local. */
|
|
7
|
+
tz?: string;
|
|
8
|
+
onChange: (next: Date) => void;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function DateSwitcher({ value, now, tz, onChange, className }: DateSwitcherProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
// "YYYY-MM-DD" in the given tz (or local if undefined). Comparing these
|
|
6
|
+
// strings is a robust same-day check that doesn't depend on local-time math.
|
|
7
|
+
function ymdInTz(d, tz) {
|
|
8
|
+
return new Intl.DateTimeFormat('en-CA', {
|
|
9
|
+
year: 'numeric',
|
|
10
|
+
month: '2-digit',
|
|
11
|
+
day: '2-digit',
|
|
12
|
+
timeZone: tz,
|
|
13
|
+
}).format(d);
|
|
14
|
+
}
|
|
15
|
+
function isSameDay(a, b, tz) {
|
|
16
|
+
return ymdInTz(a, tz) === ymdInTz(b, tz);
|
|
17
|
+
}
|
|
18
|
+
// Returns the tz offset (in minutes) for `date` in `tz`. Positive = east of
|
|
19
|
+
// UTC, e.g. Europe/Stockholm CET → +60. Tolerates "GMT", "GMT+05", and
|
|
20
|
+
// "GMT+05:30" formats from Intl.DateTimeFormat.
|
|
21
|
+
function tzOffsetMinutes(date, tz) {
|
|
22
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
23
|
+
timeZone: tz,
|
|
24
|
+
timeZoneName: 'longOffset',
|
|
25
|
+
}).formatToParts(date);
|
|
26
|
+
const name = parts.find((p) => p.type === 'timeZoneName')?.value ?? 'GMT';
|
|
27
|
+
const m = /GMT(?:([+-])(\d{1,2})(?::?(\d{2}))?)?/.exec(name);
|
|
28
|
+
if (!m?.[1])
|
|
29
|
+
return 0;
|
|
30
|
+
const sign = m[1] === '+' ? 1 : -1;
|
|
31
|
+
const hours = Number.parseInt(m[2] ?? '0', 10);
|
|
32
|
+
const mins = Number.parseInt(m[3] ?? '0', 10);
|
|
33
|
+
return sign * (hours * 60 + mins);
|
|
34
|
+
}
|
|
35
|
+
function addDays(d, delta, tz) {
|
|
36
|
+
if (!tz) {
|
|
37
|
+
// No tz: step the calendar day in the browser's local timezone (DST-safe
|
|
38
|
+
// there because setDate honors local DST transitions).
|
|
39
|
+
const c = new Date(d);
|
|
40
|
+
c.setDate(c.getDate() + delta);
|
|
41
|
+
return c;
|
|
42
|
+
}
|
|
43
|
+
// tz-aware: advance 24h, then correct for any DST shift between the
|
|
44
|
+
// original day and the target day so the *wall clock* in `tz` lands on the
|
|
45
|
+
// same hour:minute on the next/previous calendar day.
|
|
46
|
+
const offsetBefore = tzOffsetMinutes(d, tz);
|
|
47
|
+
const candidate = new Date(d.getTime() + delta * 86_400_000);
|
|
48
|
+
const offsetAfter = tzOffsetMinutes(candidate, tz);
|
|
49
|
+
return new Date(candidate.getTime() + (offsetBefore - offsetAfter) * 60_000);
|
|
50
|
+
}
|
|
51
|
+
function fmtDate(d, tz) {
|
|
52
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
53
|
+
day: 'numeric',
|
|
54
|
+
month: 'short',
|
|
55
|
+
year: 'numeric',
|
|
56
|
+
timeZone: tz,
|
|
57
|
+
}).format(d);
|
|
58
|
+
}
|
|
59
|
+
export function DateSwitcher({ value, now, tz, onChange, className }) {
|
|
60
|
+
const isToday = isSameDay(value, now, tz);
|
|
61
|
+
const goPrev = () => onChange(addDays(value, -1, tz));
|
|
62
|
+
const goNext = () => onChange(addDays(value, 1, tz));
|
|
63
|
+
return (_jsxs("div", { className: cn('flex items-center gap-2', className), children: [_jsx("button", { type: "button", "aria-label": "Previous day", onClick: goPrev, className: "inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground", children: _jsx(ChevronLeft, { className: "size-4" }) }), _jsxs("div", { className: "flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 text-sm", children: [_jsx(Calendar, { className: "size-3.5 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: fmtDate(value, tz) }), isToday && (_jsx("span", { className: "ml-1 rounded-full bg-success/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-success-800", children: "today" }))] }), _jsx("button", { type: "button", "aria-label": "Next day", disabled: isToday, onClick: goNext, className: "inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent", children: _jsx(ChevronRight, { className: "size-4" }) })] }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TimelineEvent } from './types.js';
|
|
2
|
+
export type EventCardSize = 'default' | 'tiny';
|
|
3
|
+
export interface EventCardProps {
|
|
4
|
+
event: TimelineEvent;
|
|
5
|
+
size?: EventCardSize;
|
|
6
|
+
onSelect?: (event: TimelineEvent) => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function EventCard({ event, size, onSelect, className }: EventCardProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
export function EventCard({ event, size = 'default', onSelect, className }) {
|
|
5
|
+
const isLive = event.isLive ?? false;
|
|
6
|
+
const isSelected = event.selected ?? false;
|
|
7
|
+
const isError = event.kind === 'error';
|
|
8
|
+
const liveClasses = isLive ? 'border-success/40 bg-gradient-to-b from-success/10 to-card' : '';
|
|
9
|
+
const errorClasses = isError ? 'border-destructive/40' : '';
|
|
10
|
+
const selectedClasses = isSelected
|
|
11
|
+
? 'border-primary/60 bg-primary/[0.10] shadow-[0_4px_12px_rgba(0,0,0,0.15)]'
|
|
12
|
+
: '';
|
|
13
|
+
const sizeClasses = size === 'tiny' ? 'px-2 py-1 text-[11px]' : 'px-2.5 py-2 text-xs';
|
|
14
|
+
return (_jsxs("button", { type: "button", "data-live": String(isLive), "data-selected": String(isSelected), "data-kind": event.kind, onClick: () => onSelect?.(event), className: cn('relative w-full cursor-pointer rounded-md border border-border bg-card text-left transition-[border-color,transform] duration-100', 'hover:translate-x-px hover:border-primary/40', sizeClasses, liveClasses, errorClasses, selectedClasses, className), children: [_jsx("span", { "aria-hidden": "true", className: "absolute -left-2.5 top-1/2 h-px w-2.5 bg-border" }), _jsxs("span", { className: "flex items-center gap-1.5 font-medium", children: [isLive && _jsx(LiveDot, {}), _jsx("span", { className: "min-w-0 flex-1 truncate", children: event.primary })] }), size === 'default' && event.secondary != null && (_jsx("span", { className: "mt-0.5 block text-[10px] text-muted-foreground tabular-nums", children: event.secondary }))] }));
|
|
15
|
+
}
|
|
16
|
+
function LiveDot() {
|
|
17
|
+
return (_jsx("span", { "aria-hidden": "true", className: "inline-block size-1.5 shrink-0 animate-pulse rounded-full bg-success shadow-[0_0_0_3px_rgba(16,185,129,0.20)]" }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type { DateSwitcherProps } from './date-switcher.js';
|
|
2
|
+
export { DateSwitcher } from './date-switcher.js';
|
|
3
|
+
export type { EventCardProps, EventCardSize } from './event-card.js';
|
|
4
|
+
export { EventCard } from './event-card.js';
|
|
5
|
+
export type { LiveIndicatorProps } from './live-indicator.js';
|
|
6
|
+
export { LiveIndicator } from './live-indicator.js';
|
|
7
|
+
export type { ScrubBarProps } from './scrub-bar.js';
|
|
8
|
+
export { ScrubBar } from './scrub-bar.js';
|
|
9
|
+
export type { TimelineRailProps } from './timeline-rail.js';
|
|
10
|
+
export { TimelineRail } from './timeline-rail.js';
|
|
11
|
+
export type { TimelineTapeProps } from './timeline-tape.js';
|
|
12
|
+
export { TimelineTape } from './timeline-tape.js';
|
|
13
|
+
export type { ScrubMark, TimelineEvent, TimelineEventKind } from './types.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { DateSwitcher } from './date-switcher.js';
|
|
2
|
+
export { EventCard } from './event-card.js';
|
|
3
|
+
export { LiveIndicator } from './live-indicator.js';
|
|
4
|
+
export { ScrubBar } from './scrub-bar.js';
|
|
5
|
+
export { TimelineRail } from './timeline-rail.js';
|
|
6
|
+
export { TimelineTape } from './timeline-tape.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface LiveIndicatorProps {
|
|
2
|
+
now: Date;
|
|
3
|
+
/** Optional IANA timezone for the displayed clock. */
|
|
4
|
+
tz?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function LiveIndicator({ now, tz, className }: LiveIndicatorProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { formatClock } from '../lib/format-time.js';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
export function LiveIndicator({ now, tz, className }) {
|
|
6
|
+
return (_jsxs("div", { role: "status", "aria-live": "polite", className: cn('relative h-0 pointer-events-none', className), children: [_jsx("span", { className: "pointer-events-auto absolute left-0 top-0 inline-flex items-center rounded-md bg-success-700 px-2.5 py-1 text-[11px] font-bold uppercase tracking-wider text-white shadow-[0_6px_18px_rgba(16,185,129,0.35)]", children: "LIVE" }), _jsx("span", { "aria-hidden": "true", className: "absolute right-0 top-1/2 left-[60px] h-px bg-gradient-to-r from-success via-success/30 to-transparent" }), _jsx("span", { className: "absolute right-1.5 top-[-2px] text-[10px] font-semibold tabular-nums text-success-700", children: formatClock(now, { tz }) })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { ScrubMark, TimelineEvent, TimelineEventKind } from './types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ScrubMark } from './types.js';
|
|
2
|
+
export interface ScrubBarProps {
|
|
3
|
+
durationMs: number;
|
|
4
|
+
positionMs: number;
|
|
5
|
+
isLive?: boolean;
|
|
6
|
+
marks: ScrubMark[];
|
|
7
|
+
onSeek: (positionMs: number) => void;
|
|
8
|
+
isPlaying?: boolean;
|
|
9
|
+
onPlayPause?: () => void;
|
|
10
|
+
onSkipBack?: () => void;
|
|
11
|
+
onSkipForward?: () => void;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function ScrubBar({ durationMs, positionMs, isLive, marks, onSeek, isPlaying, onPlayPause, onSkipBack, onSkipForward, className, }: ScrubBarProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Pause, Play, SkipBack, SkipForward } from 'lucide-react';
|
|
4
|
+
import { useRef } from 'react';
|
|
5
|
+
import { formatDuration } from '../lib/format-time.js';
|
|
6
|
+
import { cn } from '../lib/utils.js';
|
|
7
|
+
const KIND_COLOR = {
|
|
8
|
+
turn: 'bg-primary',
|
|
9
|
+
tool: 'bg-warning',
|
|
10
|
+
barge: 'bg-success',
|
|
11
|
+
error: 'bg-destructive',
|
|
12
|
+
};
|
|
13
|
+
export function ScrubBar({ durationMs, positionMs, isLive = false, marks, onSeek, isPlaying, onPlayPause, onSkipBack, onSkipForward, className, }) {
|
|
14
|
+
const trackRef = useRef(null);
|
|
15
|
+
const showPlayPause = isPlaying !== undefined;
|
|
16
|
+
const isPlayingValue = isPlaying ?? false;
|
|
17
|
+
const fillPct = durationMs > 0 ? Math.min(100, (positionMs / durationMs) * 100) : 0;
|
|
18
|
+
const handleSeek = (e) => {
|
|
19
|
+
const rect = trackRef.current?.getBoundingClientRect();
|
|
20
|
+
if (!rect || rect.width === 0)
|
|
21
|
+
return;
|
|
22
|
+
const x = e.clientX - rect.left;
|
|
23
|
+
const pct = Math.max(0, Math.min(1, x / rect.width));
|
|
24
|
+
onSeek(pct * durationMs);
|
|
25
|
+
};
|
|
26
|
+
const handleKeyDown = (e) => {
|
|
27
|
+
const step = durationMs / 100;
|
|
28
|
+
if (e.key === 'ArrowLeft') {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
onSeek(Math.max(0, positionMs - step));
|
|
31
|
+
}
|
|
32
|
+
else if (e.key === 'ArrowRight') {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
onSeek(Math.min(durationMs, positionMs + step));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return (_jsxs("div", { className: cn('rounded-2xl border border-border bg-card p-3.5', className), children: [_jsxs("div", { className: "mb-2.5 flex items-center justify-between", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-muted-foreground", children: "Timeline \u00B7 this call" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("span", { className: "px-1.5 text-[11px] tabular-nums text-muted-foreground", children: [formatDuration(positionMs), isLive ? ' / live' : ` / ${formatDuration(durationMs)}`] }), onSkipBack && (_jsx(TransportBtn, { label: "Skip back", onClick: onSkipBack, children: _jsx(SkipBack, { className: "size-3.5" }) })), showPlayPause && (_jsx(TransportBtn, { label: isPlayingValue ? 'Pause' : 'Play', variant: "primary", onClick: onPlayPause ?? (() => { }), children: isPlayingValue ? _jsx(Pause, { className: "size-3.5" }) : _jsx(Play, { className: "size-3.5" }) })), onSkipForward && (_jsx(TransportBtn, { label: "Skip forward", onClick: onSkipForward, children: _jsx(SkipForward, { className: "size-3.5" }) }))] })] }), _jsxs("div", { ref: trackRef, role: "slider", "aria-label": "Conversation position", "aria-valuemin": 0, "aria-valuemax": durationMs, "aria-valuenow": positionMs, onClick: handleSeek, onKeyDown: handleKeyDown, tabIndex: 0, className: "relative h-8 cursor-pointer overflow-hidden rounded-md bg-muted focus:outline-none focus:ring-2 focus:ring-primary/50", children: [_jsx("span", { "aria-hidden": "true", className: "absolute inset-y-0 left-0 bg-primary/10", style: { width: `${fillPct}%` } }), marks.map((m) => (_jsx("span", { "data-mark-kind": m.kind, title: m.label ?? m.kind, className: cn('absolute inset-y-1 w-0.5 rounded-sm', KIND_COLOR[m.kind]), style: { left: `${m.positionPct}%` } }, m.id))), _jsx("span", { "aria-hidden": "true", className: "absolute -inset-y-0.5 w-0.5 bg-foreground", style: { left: `${fillPct}%` } })] }), _jsxs("div", { className: "flex justify-between pt-2 text-[10px] tabular-nums text-muted-foreground", children: [_jsx("span", { children: "0:00" }), _jsx("span", { children: isLive ? 'now' : formatDuration(durationMs) })] })] }));
|
|
38
|
+
}
|
|
39
|
+
function TransportBtn({ label, onClick, variant, children, }) {
|
|
40
|
+
return (_jsx("button", { type: "button", "aria-label": label, onClick: onClick, className: cn('inline-flex size-6 items-center justify-center rounded', variant === 'primary'
|
|
41
|
+
? 'bg-primary text-primary-foreground'
|
|
42
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'), children: children }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TimelineEvent } from './types.js';
|
|
2
|
+
export interface TimelineRailProps {
|
|
3
|
+
date: Date;
|
|
4
|
+
now: Date;
|
|
5
|
+
events: TimelineEvent[];
|
|
6
|
+
/** px per second. Default 1.2 (72 px/min). */
|
|
7
|
+
zoom?: number;
|
|
8
|
+
/** When true (default), tape auto-scrolls to keep LIVE pinned. */
|
|
9
|
+
followLive?: boolean;
|
|
10
|
+
tz?: string;
|
|
11
|
+
onZoomChange?: (px: number) => void;
|
|
12
|
+
onDateChange?: (next: Date) => void;
|
|
13
|
+
onSelect?: (event: TimelineEvent) => void;
|
|
14
|
+
onFollowChange?: (following: boolean) => void;
|
|
15
|
+
/** Click target on the "Manage retention" footer. */
|
|
16
|
+
onManageRetention?: () => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function TimelineRail({ date, now, events, zoom, followLive, tz, onZoomChange, onDateChange, onSelect, onFollowChange, onManageRetention, className, }: TimelineRailProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Archive } from 'lucide-react';
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { cn } from '../lib/utils.js';
|
|
6
|
+
import { DateSwitcher } from './date-switcher.js';
|
|
7
|
+
import { LiveIndicator } from './live-indicator.js';
|
|
8
|
+
import { TimelineTape } from './timeline-tape.js';
|
|
9
|
+
const FUTURE_PAD_SEC = 300;
|
|
10
|
+
const PAST_SPAN_SEC = 6 * 60 * 60;
|
|
11
|
+
const STICKY_TOP_PX = 12;
|
|
12
|
+
export function TimelineRail({ date, now, events, zoom = 1.2, followLive = true, tz, onZoomChange, onDateChange, onSelect, onFollowChange, onManageRetention, className, }) {
|
|
13
|
+
const scrollRef = useRef(null);
|
|
14
|
+
const [isFollowing, setIsFollowing] = useState(followLive);
|
|
15
|
+
// Mirror parent prop changes into local state so a controlled toggle of
|
|
16
|
+
// `followLive` from outside actually flips auto-follow.
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
setIsFollowing(followLive);
|
|
19
|
+
}, [followLive]);
|
|
20
|
+
const liveCount = useMemo(() => events.filter((e) => e.isLive).length, [events]);
|
|
21
|
+
const totalCalls = useMemo(() => events.filter((e) => e.kind === 'session').length, [events]);
|
|
22
|
+
// NOW y inside the tape canvas
|
|
23
|
+
const nowY = FUTURE_PAD_SEC * zoom;
|
|
24
|
+
// Keep LIVE pinned by auto-scrolling when isFollowing
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const el = scrollRef.current;
|
|
27
|
+
if (!el)
|
|
28
|
+
return;
|
|
29
|
+
if (isFollowing) {
|
|
30
|
+
el.scrollTop = nowY - STICKY_TOP_PX;
|
|
31
|
+
}
|
|
32
|
+
}, [isFollowing, nowY]);
|
|
33
|
+
// Detect manual scroll-away → flip off auto-follow
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const el = scrollRef.current;
|
|
36
|
+
if (!el)
|
|
37
|
+
return;
|
|
38
|
+
const onScroll = () => {
|
|
39
|
+
if (!el)
|
|
40
|
+
return;
|
|
41
|
+
const expected = nowY - STICKY_TOP_PX;
|
|
42
|
+
const drift = Math.abs(el.scrollTop - expected);
|
|
43
|
+
if (drift > 32 && isFollowing) {
|
|
44
|
+
setIsFollowing(false);
|
|
45
|
+
onFollowChange?.(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
49
|
+
return () => el.removeEventListener('scroll', onScroll);
|
|
50
|
+
}, [nowY, isFollowing, onFollowChange]);
|
|
51
|
+
const jumpToLive = () => {
|
|
52
|
+
setIsFollowing(true);
|
|
53
|
+
onFollowChange?.(true);
|
|
54
|
+
};
|
|
55
|
+
return (_jsxs("aside", { className: cn('relative flex h-full flex-col border-r border-border bg-card text-sm', className), "data-meda-component": "timeline-rail", children: [_jsx("div", { className: "px-3.5 pb-2.5 pt-3.5", children: _jsx(DateSwitcher, { value: date, now: now, tz: tz, onChange: (d) => onDateChange?.(d) }) }), _jsxs("div", { className: "flex items-center gap-2.5 px-3.5 pb-3", children: [_jsx(ZoomBtn, { label: "\u2212", onClick: () => onZoomChange?.(Math.max(0.2, zoom - 0.4)) }), _jsx(ZoomTrack, { value: zoom, min: 0.2, max: 6 }), _jsx(ZoomBtn, { label: "+", onClick: () => onZoomChange?.(Math.min(6, zoom + 0.4)) })] }), _jsxs("div", { className: "flex items-center gap-2 border-b border-border px-3.5 pb-2.5 text-[11px] tabular-nums text-muted-foreground", children: [liveCount > 0 && (_jsxs("span", { className: "inline-flex items-center gap-1.5 font-semibold text-success-700", children: [_jsx("span", { className: "size-1.5 animate-pulse rounded-full bg-success shadow-[0_0_0_3px_rgba(16,185,129,0.18)]" }), liveCount, " live"] })), _jsxs("span", { className: "ml-auto", children: [_jsx("strong", { className: "font-semibold text-foreground", children: totalCalls }), " sessions"] })] }), _jsxs("div", { ref: scrollRef, className: "relative flex-1 overflow-y-auto overflow-x-hidden pb-16", children: [_jsx("div", { className: "sticky top-3 z-10 mx-3.5", children: _jsx(LiveIndicator, { now: now, tz: tz }) }), _jsx(TimelineTape, { now: now, events: events, pxPerSec: zoom, futurePadSec: FUTURE_PAD_SEC, pastSpanSec: PAST_SPAN_SEC, tz: tz, onSelect: onSelect })] }), !isFollowing && (_jsx("button", { type: "button", onClick: jumpToLive,
|
|
56
|
+
// bg-success-700/-800 (vs. -600/-700) so 11px white text clears
|
|
57
|
+
// WCAG AA on hover and at rest.
|
|
58
|
+
className: "absolute right-3 top-32 z-20 inline-flex items-center gap-1.5 rounded-full bg-success-700 px-3 py-1 text-[11px] font-semibold text-white shadow-md hover:bg-success-800", children: "Jump to live" })), _jsxs("button", { type: "button", onClick: () => onManageRetention?.(), className: "absolute inset-x-0 bottom-0 z-20 flex items-center justify-center gap-1.5 bg-gradient-to-t from-card via-card/85 to-transparent px-3.5 py-2.5 text-[12px] text-primary hover:text-primary/80", children: [_jsx(Archive, { className: "size-3.5" }), " Manage retention"] })] }));
|
|
59
|
+
}
|
|
60
|
+
function ZoomBtn({ label, onClick }) {
|
|
61
|
+
return (_jsx("button", { type: "button", onClick: onClick, className: "inline-flex size-5 items-center justify-center rounded bg-muted text-sm leading-none text-muted-foreground hover:text-foreground", children: label }));
|
|
62
|
+
}
|
|
63
|
+
function ZoomTrack({ value, min, max }) {
|
|
64
|
+
const pct = Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100));
|
|
65
|
+
return (_jsx("div", { tabIndex: 0, role: "slider", "aria-label": "Zoom", "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": value, className: "relative h-1 flex-1 rounded-sm bg-muted", children: _jsx("span", { className: "absolute -top-[3px] size-2.5 -translate-x-1/2 rounded-full bg-primary shadow-[0_0_0_4px_rgba(137,117,255,0.18)]", style: { left: `${pct}%` } }) }));
|
|
66
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TimelineEvent } from './types.js';
|
|
2
|
+
export interface TimelineTapeProps {
|
|
3
|
+
/** "Now" reference; canvas top represents `now + futurePadSec`. */
|
|
4
|
+
now: Date;
|
|
5
|
+
/** Event list. Order doesn't matter — placement uses startedAt. */
|
|
6
|
+
events: TimelineEvent[];
|
|
7
|
+
/** px per second of vertical scroll. Default 1.2 = 72 px/min. */
|
|
8
|
+
pxPerSec?: number;
|
|
9
|
+
/** Seconds of "future" rendered above NOW. Default 300 (5 min). */
|
|
10
|
+
futurePadSec?: number;
|
|
11
|
+
/** Total seconds visible past NOW (governs canvas height). Default 6h. */
|
|
12
|
+
pastSpanSec?: number;
|
|
13
|
+
/** Timezone for tick labels (caller's local tz by default). */
|
|
14
|
+
tz?: string;
|
|
15
|
+
/** Selection callback. */
|
|
16
|
+
onSelect?: (event: TimelineEvent) => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function TimelineTape({ now, events, pxPerSec, futurePadSec, pastSpanSec, tz, onSelect, className, }: TimelineTapeProps): import("react/jsx-runtime").JSX.Element;
|