@sero-ai/ui 0.1.0 → 0.2.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/.turbo/turbo-typecheck.log +1 -1
- package/LICENSE +201 -0
- package/package.json +22 -18
- package/src/components/ai-elements/attachments.tsx +4 -4
- package/src/components/ai-elements/audio-player.tsx +1 -1
- package/src/components/ai-elements/chain-of-thought.tsx +2 -2
- package/src/components/ai-elements/confirmation.tsx +2 -2
- package/src/components/ai-elements/context.tsx +2 -2
- package/src/components/ai-elements/environment-variables.tsx +6 -6
- package/src/components/ai-elements/file-tree.tsx +3 -3
- package/src/components/ai-elements/inline-citation.tsx +2 -2
- package/src/components/ai-elements/jsx-preview.tsx +2 -2
- package/src/components/ai-elements/message.tsx +2 -2
- package/src/components/ai-elements/mic-selector.tsx +8 -8
- package/src/components/ai-elements/open-in-chat.tsx +2 -2
- package/src/components/ai-elements/package-info.tsx +4 -4
- package/src/components/ai-elements/plan.tsx +2 -2
- package/src/components/ai-elements/prompt-input-context.tsx +252 -0
- package/src/components/ai-elements/prompt-input-elements.tsx +371 -0
- package/src/components/ai-elements/prompt-input-textarea.tsx +117 -0
- package/src/components/ai-elements/prompt-input.tsx +59 -990
- package/src/components/ai-elements/queue.tsx +1 -1
- package/src/components/ai-elements/reasoning.tsx +3 -3
- package/src/components/ai-elements/schema-display.tsx +7 -7
- package/src/components/ai-elements/snippet.tsx +3 -3
- package/src/components/ai-elements/sources.tsx +2 -2
- package/src/components/ai-elements/terminal.tsx +5 -5
- package/src/components/ai-elements/test-results.tsx +8 -8
- package/src/components/ai-elements/transcription.tsx +2 -2
- package/src/components/ai-elements/voice-selector-accent.tsx +50 -0
- package/src/components/ai-elements/voice-selector.tsx +8 -177
- package/src/components/ai-elements/web-preview.tsx +5 -6
- package/src/components/model-selection/available-model-picker.tsx +231 -0
- package/src/components/model-selection/model-warning-list.tsx +33 -0
- package/src/components/model-selection/thinking-level-picker.tsx +55 -0
- package/src/components/ui/alert-dialog.tsx +1 -1
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/carousel.tsx +1 -1
- package/src/components/ui/chart.tsx +3 -3
- package/src/components/ui/command.tsx +2 -2
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/field.tsx +2 -2
- package/src/components/ui/form.tsx +2 -2
- package/src/components/ui/input-otp.tsx +2 -2
- package/src/components/ui/input.tsx +3 -1
- package/src/components/ui/menubar.tsx +1 -1
- package/src/components/ui/native-select.tsx +2 -0
- package/src/components/ui/navigation-menu.tsx +1 -1
- package/src/components/ui/plugin-safety-disclaimer.tsx +25 -0
- package/src/components/ui/progress.tsx +1 -1
- package/src/components/ui/resizable.tsx +1 -1
- package/src/components/ui/search-input.tsx +39 -0
- package/src/components/ui/slider.tsx +7 -2
- package/src/components/ui/textarea.tsx +3 -1
- package/src/components/ui/toggle-group.tsx +1 -1
- package/src/components/ui/tree.tsx +3 -3
- package/src/index.ts +128 -7
- package/src/styles/globals.css +91 -26
- package/src/styles/plugin.css +4 -0
- package/src/theme/apply-theme.ts +250 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/types.ts +253 -0
|
@@ -20,7 +20,7 @@ const TreeContext = React.createContext<TreeContextValue>({
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
function useTreeContext<T = any>() {
|
|
23
|
-
return React.
|
|
23
|
+
return React.use(TreeContext) as TreeContextValue<T>;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
interface TreeProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -61,7 +61,7 @@ interface TreeItemProps<T = any>
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* TreeItem
|
|
64
|
+
* TreeItem, full-width row button.
|
|
65
65
|
*
|
|
66
66
|
* The row carries hover / selected / drag-target backgrounds so the
|
|
67
67
|
* highlight spans edge-to-edge (VSCode-style). Indentation is applied
|
|
@@ -140,7 +140,7 @@ interface TreeItemLabelProps<T = any>
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
* TreeItemLabel
|
|
143
|
+
* TreeItemLabel, content wrapper inside a TreeItem.
|
|
144
144
|
*
|
|
145
145
|
* Layout-only (chevron + children). Background lives on the parent
|
|
146
146
|
* TreeItem so it spans the full row width.
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,137 @@
|
|
|
1
1
|
// @sero-ai/ui — Shared UI components, AI elements, and utilities
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// import { Button } from "@sero-ai/ui
|
|
5
|
-
// import { Message } from "@sero-ai/ui/components/ai-elements/message"
|
|
6
|
-
// import { cn } from "@sero-ai/ui/lib/utils"
|
|
7
|
-
// import { useIsMobile } from "@sero-ai/ui/hooks/use-mobile"
|
|
3
|
+
// Prefer importing common primitives from the barrel export:
|
|
4
|
+
// import { Button, Card, Input, cn, useIsMobile } from "@sero-ai/ui"
|
|
8
5
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// Deep subpath imports are still supported when needed. Because this package is
|
|
7
|
+
// native ESM, consumers should use runtime .js extensions for deep imports:
|
|
8
|
+
// import { Button } from "@sero-ai/ui/components/ui/button.js"
|
|
9
|
+
// import { cn } from "@sero-ai/ui/lib/utils.js"
|
|
11
10
|
|
|
12
11
|
// ── Utilities ──
|
|
13
12
|
export { cn } from "./lib/utils";
|
|
14
13
|
|
|
15
14
|
// ── Hooks ──
|
|
16
15
|
export { useIsMobile } from "./hooks/use-mobile";
|
|
16
|
+
|
|
17
|
+
// ── Theme ──
|
|
18
|
+
export * from "./theme";
|
|
19
|
+
|
|
20
|
+
// ── UI Components ──
|
|
21
|
+
export * from "./components/ui/accordion";
|
|
22
|
+
export * from "./components/ui/alert-dialog";
|
|
23
|
+
export * from "./components/ui/alert";
|
|
24
|
+
export * from "./components/ui/aspect-ratio";
|
|
25
|
+
export * from "./components/ui/avatar";
|
|
26
|
+
export * from "./components/ui/badge";
|
|
27
|
+
export * from "./components/ui/breadcrumb";
|
|
28
|
+
export * from "./components/ui/button-group";
|
|
29
|
+
export * from "./components/ui/button";
|
|
30
|
+
export * from "./components/ui/calendar";
|
|
31
|
+
export * from "./components/ui/card";
|
|
32
|
+
export * from "./components/ui/carousel";
|
|
33
|
+
export * from "./components/ui/chart";
|
|
34
|
+
export * from "./components/ui/checkbox";
|
|
35
|
+
export * from "./components/ui/collapsible";
|
|
36
|
+
export * from "./components/ui/combobox";
|
|
37
|
+
export * from "./components/ui/command";
|
|
38
|
+
export * from "./components/ui/context-menu";
|
|
39
|
+
export * from "./components/ui/dialog";
|
|
40
|
+
export * from "./components/ui/direction";
|
|
41
|
+
export * from "./components/ui/drawer";
|
|
42
|
+
export * from "./components/ui/dropdown-menu";
|
|
43
|
+
export * from "./components/ui/empty";
|
|
44
|
+
export * from "./components/ui/field";
|
|
45
|
+
export * from "./components/ui/form";
|
|
46
|
+
export * from "./components/ui/hover-card";
|
|
47
|
+
export * from "./components/ui/input-group";
|
|
48
|
+
export * from "./components/ui/input-otp";
|
|
49
|
+
export * from "./components/ui/input";
|
|
50
|
+
export * from "./components/ui/item";
|
|
51
|
+
export * from "./components/ui/kbd";
|
|
52
|
+
export * from "./components/ui/label";
|
|
53
|
+
export * from "./components/ui/menubar";
|
|
54
|
+
export * from "./components/ui/native-select";
|
|
55
|
+
export * from "./components/ui/navigation-menu";
|
|
56
|
+
export * from "./components/ui/pagination";
|
|
57
|
+
export * from "./components/ui/plugin-safety-disclaimer";
|
|
58
|
+
export * from "./components/ui/popover";
|
|
59
|
+
export * from "./components/ui/progress";
|
|
60
|
+
export * from "./components/ui/radio-group";
|
|
61
|
+
export * from "./components/ui/resizable";
|
|
62
|
+
export * from "./components/ui/scroll-area";
|
|
63
|
+
export * from "./components/ui/search-input";
|
|
64
|
+
export * from "./components/ui/select";
|
|
65
|
+
export * from "./components/ui/separator";
|
|
66
|
+
export * from "./components/ui/sheet";
|
|
67
|
+
export * from "./components/ui/sidebar";
|
|
68
|
+
export * from "./components/ui/skeleton";
|
|
69
|
+
export * from "./components/ui/slider";
|
|
70
|
+
export * from "./components/ui/sonner";
|
|
71
|
+
export * from "./components/ui/spinner";
|
|
72
|
+
export * from "./components/ui/switch";
|
|
73
|
+
export * from "./components/ui/table";
|
|
74
|
+
export * from "./components/ui/tabs";
|
|
75
|
+
export * from "./components/ui/textarea";
|
|
76
|
+
export * from "./components/ui/toggle-group";
|
|
77
|
+
export * from "./components/ui/toggle";
|
|
78
|
+
export * from "./components/ui/tooltip";
|
|
79
|
+
export * from "./components/ui/tree";
|
|
80
|
+
// ── AI Elements ──
|
|
81
|
+
export * from "./components/ai-elements/agent";
|
|
82
|
+
export * from "./components/ai-elements/artifact";
|
|
83
|
+
export * from "./components/ai-elements/attachments";
|
|
84
|
+
export * from "./components/ai-elements/audio-player";
|
|
85
|
+
export * from "./components/ai-elements/canvas";
|
|
86
|
+
export * from "./components/ai-elements/chain-of-thought";
|
|
87
|
+
export * from "./components/ai-elements/checkpoint";
|
|
88
|
+
export * from "./components/ai-elements/code-block";
|
|
89
|
+
export * from "./components/ai-elements/commit";
|
|
90
|
+
export * from "./components/ai-elements/confirmation";
|
|
91
|
+
export * from "./components/ai-elements/connection";
|
|
92
|
+
export * from "./components/ai-elements/context";
|
|
93
|
+
export * from "./components/ai-elements/controls";
|
|
94
|
+
export * from "./components/ai-elements/conversation";
|
|
95
|
+
export * from "./components/ai-elements/edge";
|
|
96
|
+
export * from "./components/ai-elements/environment-variables";
|
|
97
|
+
export * from "./components/ai-elements/file-tree";
|
|
98
|
+
export * from "./components/ai-elements/image";
|
|
99
|
+
export * from "./components/ai-elements/inline-citation";
|
|
100
|
+
export * from "./components/ai-elements/jsx-preview";
|
|
101
|
+
export * from "./components/ai-elements/message";
|
|
102
|
+
export * from "./components/ai-elements/mic-selector";
|
|
103
|
+
export * from "./components/ai-elements/model-selector";
|
|
104
|
+
export * from "./components/ai-elements/node";
|
|
105
|
+
export * from "./components/ai-elements/open-in-chat";
|
|
106
|
+
export * from "./components/ai-elements/package-info";
|
|
107
|
+
export * from "./components/ai-elements/panel";
|
|
108
|
+
export * from "./components/ai-elements/persona";
|
|
109
|
+
export * from "./components/ai-elements/plan";
|
|
110
|
+
export * from "./components/ai-elements/prompt-input-context";
|
|
111
|
+
export * from "./components/ai-elements/prompt-input-elements";
|
|
112
|
+
export * from "./components/ai-elements/prompt-input-textarea";
|
|
113
|
+
export * from "./components/ai-elements/prompt-input";
|
|
114
|
+
export * from "./components/ai-elements/queue";
|
|
115
|
+
export * from "./components/ai-elements/reasoning";
|
|
116
|
+
export * from "./components/ai-elements/sandbox";
|
|
117
|
+
export * from "./components/ai-elements/schema-display";
|
|
118
|
+
export * from "./components/ai-elements/shimmer";
|
|
119
|
+
export * from "./components/ai-elements/snippet";
|
|
120
|
+
export * from "./components/ai-elements/sources";
|
|
121
|
+
export * from "./components/ai-elements/speech-input";
|
|
122
|
+
export * from "./components/ai-elements/stack-trace";
|
|
123
|
+
export * from "./components/ai-elements/suggestion";
|
|
124
|
+
export * from "./components/ai-elements/task";
|
|
125
|
+
export * from "./components/ai-elements/terminal";
|
|
126
|
+
export * from "./components/ai-elements/test-results";
|
|
127
|
+
export * from "./components/ai-elements/tool";
|
|
128
|
+
export * from "./components/ai-elements/toolbar";
|
|
129
|
+
export * from "./components/ai-elements/transcription";
|
|
130
|
+
export * from "./components/ai-elements/voice-selector-accent";
|
|
131
|
+
export * from "./components/ai-elements/voice-selector";
|
|
132
|
+
export * from "./components/ai-elements/web-preview";
|
|
133
|
+
|
|
134
|
+
// ── Model Selection Components ──
|
|
135
|
+
export * from "./components/model-selection/available-model-picker";
|
|
136
|
+
export * from "./components/model-selection/model-warning-list";
|
|
137
|
+
export * from "./components/model-selection/thinking-level-picker";
|
package/src/styles/globals.css
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
/* Border (light) */
|
|
23
23
|
--border-subtle: #d4d8e0;
|
|
24
24
|
--border-default: #bcc1cc;
|
|
25
|
-
--border-focus: #
|
|
25
|
+
--border-focus: #059669;
|
|
26
26
|
|
|
27
27
|
/* Text (light) */
|
|
28
28
|
--text-primary: #0f1117;
|
|
@@ -30,10 +30,26 @@
|
|
|
30
30
|
--text-muted: #6b7280;
|
|
31
31
|
--text-inverse: #fafafa;
|
|
32
32
|
|
|
33
|
-
/*
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
--
|
|
33
|
+
/* Brand colours — product identity, actions, active UI */
|
|
34
|
+
--brand-primary: #059669;
|
|
35
|
+
--brand-primary-hover: #047857;
|
|
36
|
+
--brand-primary-foreground: #ffffff;
|
|
37
|
+
--brand-primary-muted: color-mix(in srgb, var(--brand-primary) 10%, transparent);
|
|
38
|
+
--brand-primary-subtle: color-mix(in srgb, var(--brand-primary) 15%, transparent);
|
|
39
|
+
--brand-primary-faint: color-mix(in srgb, var(--brand-primary) 3%, transparent);
|
|
40
|
+
--brand-primary-border: color-mix(in srgb, var(--brand-primary) 20%, transparent);
|
|
41
|
+
--brand-secondary: #7c3aed;
|
|
42
|
+
--brand-secondary-hover: #6d28d9;
|
|
43
|
+
--brand-secondary-foreground: #ffffff;
|
|
44
|
+
--brand-secondary-muted: color-mix(in srgb, var(--brand-secondary) 10%, transparent);
|
|
45
|
+
--brand-secondary-subtle: color-mix(in srgb, var(--brand-secondary) 15%, transparent);
|
|
46
|
+
--brand-secondary-faint: color-mix(in srgb, var(--brand-secondary) 3%, transparent);
|
|
47
|
+
--brand-secondary-border: color-mix(in srgb, var(--brand-secondary) 20%, transparent);
|
|
48
|
+
|
|
49
|
+
/* Legacy accent / syntax accent */
|
|
50
|
+
--accent-primary: var(--brand-secondary);
|
|
51
|
+
--accent-hover: var(--brand-secondary-hover);
|
|
52
|
+
--accent-muted: var(--brand-secondary-muted);
|
|
37
53
|
--accent-code: #7c3aed;
|
|
38
54
|
|
|
39
55
|
/* Status — solid */
|
|
@@ -79,10 +95,10 @@
|
|
|
79
95
|
--voice-processing-muted: rgba(6, 182, 212, 0.15);
|
|
80
96
|
|
|
81
97
|
/* Semantic colours — indigo accent for notifications/banners */
|
|
82
|
-
--banner-primary:
|
|
83
|
-
--banner-primary-muted:
|
|
84
|
-
--banner-primary-subtle:
|
|
85
|
-
--banner-primary-border:
|
|
98
|
+
--banner-primary: var(--brand-secondary);
|
|
99
|
+
--banner-primary-muted: color-mix(in srgb, var(--banner-primary) 10%, transparent);
|
|
100
|
+
--banner-primary-subtle: color-mix(in srgb, var(--banner-primary) 15%, transparent);
|
|
101
|
+
--banner-primary-border: color-mix(in srgb, var(--banner-primary) 20%, transparent);
|
|
86
102
|
|
|
87
103
|
/* Thinking / reasoning gradients */
|
|
88
104
|
--gradient-thinking-high: linear-gradient(135deg, rgba(245, 158, 11, 0.20), rgba(239, 68, 68, 0.20));
|
|
@@ -131,15 +147,15 @@
|
|
|
131
147
|
shadcn component (Dialog, Command, Dropdown, Tooltip, …)
|
|
132
148
|
automatically follows theme changes.
|
|
133
149
|
NOTE: --accent here is the shadcn "hover/selection bg",
|
|
134
|
-
NOT
|
|
150
|
+
NOT a brand colour (use --brand-primary/secondary). */
|
|
135
151
|
--background: var(--bg-base);
|
|
136
152
|
--foreground: var(--text-primary);
|
|
137
153
|
--card: var(--bg-surface);
|
|
138
154
|
--card-foreground: var(--text-primary);
|
|
139
155
|
--popover: var(--bg-elevated);
|
|
140
156
|
--popover-foreground: var(--text-primary);
|
|
141
|
-
--primary: var(--
|
|
142
|
-
--primary-foreground: var(--
|
|
157
|
+
--primary: var(--brand-primary);
|
|
158
|
+
--primary-foreground: var(--brand-primary-foreground);
|
|
143
159
|
--secondary: var(--bg-surface);
|
|
144
160
|
--secondary-foreground: var(--text-primary);
|
|
145
161
|
--muted: var(--bg-muted);
|
|
@@ -157,8 +173,8 @@
|
|
|
157
173
|
--chart-5: oklch(0.769 0.188 70.08);
|
|
158
174
|
--sidebar: var(--bg-surface);
|
|
159
175
|
--sidebar-foreground: var(--text-primary);
|
|
160
|
-
--sidebar-primary: var(--
|
|
161
|
-
--sidebar-primary-foreground: var(--
|
|
176
|
+
--sidebar-primary: var(--brand-primary);
|
|
177
|
+
--sidebar-primary-foreground: var(--brand-primary-foreground);
|
|
162
178
|
--sidebar-accent: var(--bg-elevated);
|
|
163
179
|
--sidebar-accent-foreground: var(--text-primary);
|
|
164
180
|
--sidebar-border: var(--border-subtle);
|
|
@@ -208,6 +224,38 @@
|
|
|
208
224
|
--radius-4xl: calc(var(--radius) + 16px);
|
|
209
225
|
--color-background: var(--background);
|
|
210
226
|
--color-foreground: var(--foreground);
|
|
227
|
+
--color-brand-primary: var(--brand-primary);
|
|
228
|
+
--color-brand-primary-foreground: var(--brand-primary-foreground);
|
|
229
|
+
--color-brand-primary-muted: var(--brand-primary-muted);
|
|
230
|
+
--color-brand-primary-subtle: var(--brand-primary-subtle);
|
|
231
|
+
--color-brand-primary-faint: var(--brand-primary-faint);
|
|
232
|
+
--color-brand-primary-border: var(--brand-primary-border);
|
|
233
|
+
--color-brand-secondary: var(--brand-secondary);
|
|
234
|
+
--color-brand-secondary-foreground: var(--brand-secondary-foreground);
|
|
235
|
+
--color-brand-secondary-muted: var(--brand-secondary-muted);
|
|
236
|
+
--color-brand-secondary-subtle: var(--brand-secondary-subtle);
|
|
237
|
+
--color-brand-secondary-faint: var(--brand-secondary-faint);
|
|
238
|
+
--color-brand-secondary-border: var(--brand-secondary-border);
|
|
239
|
+
--color-status-success: var(--status-success);
|
|
240
|
+
--color-status-success-muted: var(--status-success-muted);
|
|
241
|
+
--color-status-success-subtle: var(--status-success-subtle);
|
|
242
|
+
--color-status-success-faint: var(--status-success-faint);
|
|
243
|
+
--color-status-success-border: var(--status-success-border);
|
|
244
|
+
--color-status-warning: var(--status-warning);
|
|
245
|
+
--color-status-warning-muted: var(--status-warning-muted);
|
|
246
|
+
--color-status-warning-subtle: var(--status-warning-subtle);
|
|
247
|
+
--color-status-warning-faint: var(--status-warning-faint);
|
|
248
|
+
--color-status-warning-border: var(--status-warning-border);
|
|
249
|
+
--color-status-error: var(--status-error);
|
|
250
|
+
--color-status-error-muted: var(--status-error-muted);
|
|
251
|
+
--color-status-error-subtle: var(--status-error-subtle);
|
|
252
|
+
--color-status-error-faint: var(--status-error-faint);
|
|
253
|
+
--color-status-error-border: var(--status-error-border);
|
|
254
|
+
--color-status-info: var(--status-info);
|
|
255
|
+
--color-status-info-muted: var(--status-info-muted);
|
|
256
|
+
--color-status-info-subtle: var(--status-info-subtle);
|
|
257
|
+
--color-status-info-faint: var(--status-info-faint);
|
|
258
|
+
--color-status-info-border: var(--status-info-border);
|
|
211
259
|
--color-card: var(--card);
|
|
212
260
|
--color-card-foreground: var(--card-foreground);
|
|
213
261
|
--color-popover: var(--popover);
|
|
@@ -250,6 +298,7 @@
|
|
|
250
298
|
/* Border (dark) */
|
|
251
299
|
--border-subtle: #27272a;
|
|
252
300
|
--border-default: #3f3f46;
|
|
301
|
+
--border-focus: #34d399;
|
|
253
302
|
|
|
254
303
|
/* Text (dark) */
|
|
255
304
|
--text-primary: #fafafa;
|
|
@@ -257,10 +306,26 @@
|
|
|
257
306
|
--text-muted: #71717a;
|
|
258
307
|
--text-inverse: #09090b;
|
|
259
308
|
|
|
260
|
-
/*
|
|
261
|
-
--
|
|
262
|
-
--
|
|
263
|
-
--
|
|
309
|
+
/* Brand colours (dark) */
|
|
310
|
+
--brand-primary: #34d399;
|
|
311
|
+
--brand-primary-hover: #6ee7b7;
|
|
312
|
+
--brand-primary-foreground: #052e1c;
|
|
313
|
+
--brand-primary-muted: color-mix(in srgb, var(--brand-primary) 10%, transparent);
|
|
314
|
+
--brand-primary-subtle: color-mix(in srgb, var(--brand-primary) 15%, transparent);
|
|
315
|
+
--brand-primary-faint: color-mix(in srgb, var(--brand-primary) 3%, transparent);
|
|
316
|
+
--brand-primary-border: color-mix(in srgb, var(--brand-primary) 20%, transparent);
|
|
317
|
+
--brand-secondary: #c4b5fd;
|
|
318
|
+
--brand-secondary-hover: #ddd6fe;
|
|
319
|
+
--brand-secondary-foreground: #1e1038;
|
|
320
|
+
--brand-secondary-muted: color-mix(in srgb, var(--brand-secondary) 10%, transparent);
|
|
321
|
+
--brand-secondary-subtle: color-mix(in srgb, var(--brand-secondary) 15%, transparent);
|
|
322
|
+
--brand-secondary-faint: color-mix(in srgb, var(--brand-secondary) 3%, transparent);
|
|
323
|
+
--brand-secondary-border: color-mix(in srgb, var(--brand-secondary) 20%, transparent);
|
|
324
|
+
|
|
325
|
+
/* Legacy accent / syntax accent (dark) */
|
|
326
|
+
--accent-primary: var(--brand-secondary);
|
|
327
|
+
--accent-hover: var(--brand-secondary-hover);
|
|
328
|
+
--accent-muted: var(--brand-secondary-muted);
|
|
264
329
|
--accent-code: #c4b5fd;
|
|
265
330
|
|
|
266
331
|
/* Status — solid (dark) */
|
|
@@ -306,10 +371,10 @@
|
|
|
306
371
|
--voice-processing-muted: rgba(34, 211, 238, 0.15);
|
|
307
372
|
|
|
308
373
|
/* Banner / notifications (dark) */
|
|
309
|
-
--banner-primary:
|
|
310
|
-
--banner-primary-muted:
|
|
311
|
-
--banner-primary-subtle:
|
|
312
|
-
--banner-primary-border:
|
|
374
|
+
--banner-primary: var(--brand-secondary);
|
|
375
|
+
--banner-primary-muted: color-mix(in srgb, var(--banner-primary) 10%, transparent);
|
|
376
|
+
--banner-primary-subtle: color-mix(in srgb, var(--banner-primary) 15%, transparent);
|
|
377
|
+
--banner-primary-border: color-mix(in srgb, var(--banner-primary) 20%, transparent);
|
|
313
378
|
|
|
314
379
|
/* Thinking gradients (dark) */
|
|
315
380
|
--gradient-thinking-high: linear-gradient(135deg, rgba(245, 158, 11, 0.20), rgba(239, 68, 68, 0.20));
|
|
@@ -331,8 +396,8 @@
|
|
|
331
396
|
--card-foreground: var(--text-primary);
|
|
332
397
|
--popover: var(--bg-elevated);
|
|
333
398
|
--popover-foreground: var(--text-primary);
|
|
334
|
-
--primary: var(--
|
|
335
|
-
--primary-foreground: var(--
|
|
399
|
+
--primary: var(--brand-primary);
|
|
400
|
+
--primary-foreground: var(--brand-primary-foreground);
|
|
336
401
|
--secondary: var(--bg-surface);
|
|
337
402
|
--secondary-foreground: var(--text-primary);
|
|
338
403
|
--muted: var(--bg-muted);
|
|
@@ -350,8 +415,8 @@
|
|
|
350
415
|
--chart-5: oklch(0.645 0.246 16.439);
|
|
351
416
|
--sidebar: var(--bg-surface);
|
|
352
417
|
--sidebar-foreground: var(--text-primary);
|
|
353
|
-
--sidebar-primary: var(--
|
|
354
|
-
--sidebar-primary-foreground: var(--
|
|
418
|
+
--sidebar-primary: var(--brand-primary);
|
|
419
|
+
--sidebar-primary-foreground: var(--brand-primary-foreground);
|
|
355
420
|
--sidebar-accent: var(--bg-elevated);
|
|
356
421
|
--sidebar-accent-foreground: var(--text-primary);
|
|
357
422
|
--sidebar-border: var(--border-subtle);
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ColorTokens,
|
|
3
|
+
ThemePreset,
|
|
4
|
+
} from './types';
|
|
5
|
+
|
|
6
|
+
// ── Token → CSS Variable Mapping ─────────────────────────────
|
|
7
|
+
|
|
8
|
+
/** Maps ColorTokens keys to their CSS custom property names. */
|
|
9
|
+
const COLOR_TOKEN_TO_CSS: Record<keyof ColorTokens, string> = {
|
|
10
|
+
bgBase: '--bg-base',
|
|
11
|
+
bgSurface: '--bg-surface',
|
|
12
|
+
bgElevated: '--bg-elevated',
|
|
13
|
+
bgOverlay: '--bg-overlay',
|
|
14
|
+
bgMuted: '--bg-muted',
|
|
15
|
+
borderSubtle: '--border-subtle',
|
|
16
|
+
borderDefault: '--border-default',
|
|
17
|
+
borderFocus: '--border-focus',
|
|
18
|
+
textPrimary: '--text-primary',
|
|
19
|
+
textSecondary: '--text-secondary',
|
|
20
|
+
textMuted: '--text-muted',
|
|
21
|
+
textInverse: '--text-inverse',
|
|
22
|
+
brandPrimary: '--brand-primary',
|
|
23
|
+
brandPrimaryHover: '--brand-primary-hover',
|
|
24
|
+
brandPrimaryForeground: '--brand-primary-foreground',
|
|
25
|
+
brandSecondary: '--brand-secondary',
|
|
26
|
+
brandSecondaryHover: '--brand-secondary-hover',
|
|
27
|
+
brandSecondaryForeground: '--brand-secondary-foreground',
|
|
28
|
+
accentPrimary: '--accent-primary',
|
|
29
|
+
accentHover: '--accent-hover',
|
|
30
|
+
accentMuted: '--accent-muted',
|
|
31
|
+
accentCode: '--accent-code',
|
|
32
|
+
statusSuccess: '--status-success',
|
|
33
|
+
statusWarning: '--status-warning',
|
|
34
|
+
statusError: '--status-error',
|
|
35
|
+
statusInfo: '--status-info',
|
|
36
|
+
collabPrimary: '--collab-primary',
|
|
37
|
+
voiceRecording: '--voice-recording',
|
|
38
|
+
voiceProcessing: '--voice-processing',
|
|
39
|
+
bannerPrimary: '--banner-primary',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Derived opacity variants generated from base semantic colours.
|
|
44
|
+
* Each entry is [css-var-name, base-token-key, percentage].
|
|
45
|
+
*/
|
|
46
|
+
const DERIVED_OPACITY_VARS: Array<[string, keyof ColorTokens, number]> = [
|
|
47
|
+
['--brand-primary-muted', 'brandPrimary', 10],
|
|
48
|
+
['--brand-primary-subtle', 'brandPrimary', 15],
|
|
49
|
+
['--brand-primary-faint', 'brandPrimary', 3],
|
|
50
|
+
['--brand-primary-border', 'brandPrimary', 20],
|
|
51
|
+
['--brand-secondary-muted', 'brandSecondary', 10],
|
|
52
|
+
['--brand-secondary-subtle', 'brandSecondary', 15],
|
|
53
|
+
['--brand-secondary-faint', 'brandSecondary', 3],
|
|
54
|
+
['--brand-secondary-border', 'brandSecondary', 20],
|
|
55
|
+
['--status-success-muted', 'statusSuccess', 10],
|
|
56
|
+
['--status-success-subtle', 'statusSuccess', 15],
|
|
57
|
+
['--status-success-faint', 'statusSuccess', 3],
|
|
58
|
+
['--status-success-border', 'statusSuccess', 20],
|
|
59
|
+
['--status-warning-muted', 'statusWarning', 10],
|
|
60
|
+
['--status-warning-subtle', 'statusWarning', 15],
|
|
61
|
+
['--status-warning-faint', 'statusWarning', 3],
|
|
62
|
+
['--status-warning-border', 'statusWarning', 20],
|
|
63
|
+
['--status-error-muted', 'statusError', 10],
|
|
64
|
+
['--status-error-subtle', 'statusError', 15],
|
|
65
|
+
['--status-error-faint', 'statusError', 3],
|
|
66
|
+
['--status-error-border', 'statusError', 20],
|
|
67
|
+
['--status-info-muted', 'statusInfo', 10],
|
|
68
|
+
['--status-info-subtle', 'statusInfo', 15],
|
|
69
|
+
['--status-info-faint', 'statusInfo', 3],
|
|
70
|
+
['--status-info-border', 'statusInfo', 20],
|
|
71
|
+
['--collab-primary-muted', 'collabPrimary', 10],
|
|
72
|
+
['--collab-primary-subtle', 'collabPrimary', 15],
|
|
73
|
+
['--collab-primary-border', 'collabPrimary', 20],
|
|
74
|
+
['--voice-recording-muted', 'voiceRecording', 20],
|
|
75
|
+
['--voice-processing-muted', 'voiceProcessing', 15],
|
|
76
|
+
['--banner-primary-muted', 'bannerPrimary', 10],
|
|
77
|
+
['--banner-primary-subtle', 'bannerPrimary', 15],
|
|
78
|
+
['--banner-primary-border', 'bannerPrimary', 20],
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
export interface ApplyThemeOptions {
|
|
82
|
+
loadFont?: (fontStack: string) => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function tint(color: string, percentage: number): string {
|
|
86
|
+
return `color-mix(in srgb, ${color} ${percentage}%, transparent)`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Apply / Reset ────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/** Apply a theme preset for the given mode. */
|
|
92
|
+
export function applyThemePreset(
|
|
93
|
+
preset: ThemePreset,
|
|
94
|
+
mode: 'light' | 'dark',
|
|
95
|
+
options: ApplyThemeOptions = {},
|
|
96
|
+
): void {
|
|
97
|
+
const root = document.documentElement;
|
|
98
|
+
const colors = mode === 'dark' ? preset.colors.dark : preset.colors.light;
|
|
99
|
+
|
|
100
|
+
for (const [key, cssVar] of Object.entries(COLOR_TOKEN_TO_CSS)) {
|
|
101
|
+
const value = colors[key as keyof ColorTokens];
|
|
102
|
+
if (value) root.style.setProperty(cssVar, value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const [cssVar, baseKey, percentage] of DERIVED_OPACITY_VARS) {
|
|
106
|
+
const baseColor = colors[baseKey];
|
|
107
|
+
if (baseColor) root.style.setProperty(cssVar, tint(baseColor, percentage));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (preset.typography?.fontSans) {
|
|
111
|
+
options.loadFont?.(preset.typography.fontSans);
|
|
112
|
+
root.style.setProperty('--font-sans', preset.typography.fontSans);
|
|
113
|
+
}
|
|
114
|
+
if (preset.typography?.fontMono) {
|
|
115
|
+
options.loadFont?.(preset.typography.fontMono);
|
|
116
|
+
root.style.setProperty('--font-mono', preset.typography.fontMono);
|
|
117
|
+
}
|
|
118
|
+
if (preset.typography?.fontSizeBase) {
|
|
119
|
+
root.style.setProperty('--font-size-base', preset.typography.fontSizeBase);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (preset.spacing?.md) {
|
|
123
|
+
const mdPx = parseFloat(preset.spacing.md);
|
|
124
|
+
if (Number.isFinite(mdPx) && mdPx > 0) {
|
|
125
|
+
root.style.setProperty('--spacing', `${(mdPx / 3).toFixed(3)}px`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (preset.radius?.md) {
|
|
130
|
+
root.style.setProperty('--radius', preset.radius.md);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
root.classList.toggle('dark', mode === 'dark');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** All CSS custom properties managed by the theme engine. */
|
|
137
|
+
const ALL_MANAGED_VARS = [
|
|
138
|
+
...Object.values(COLOR_TOKEN_TO_CSS),
|
|
139
|
+
...DERIVED_OPACITY_VARS.map(([v]) => v),
|
|
140
|
+
'--font-sans',
|
|
141
|
+
'--font-mono',
|
|
142
|
+
'--font-size-base',
|
|
143
|
+
'--spacing',
|
|
144
|
+
'--radius',
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
/** Remove all inline theme overrides, reverting to CSS defaults. */
|
|
148
|
+
export function resetTheme(): void {
|
|
149
|
+
const root = document.documentElement;
|
|
150
|
+
for (const cssVar of ALL_MANAGED_VARS) {
|
|
151
|
+
root.style.removeProperty(cssVar);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Validation ───────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
const LEGACY_REQUIRED_COLOR_KEYS: Array<keyof ColorTokens> = [
|
|
158
|
+
'bgBase', 'bgSurface', 'bgElevated', 'bgOverlay', 'bgMuted',
|
|
159
|
+
'borderSubtle', 'borderDefault', 'borderFocus',
|
|
160
|
+
'textPrimary', 'textSecondary', 'textMuted', 'textInverse',
|
|
161
|
+
'accentPrimary', 'accentHover', 'accentMuted', 'accentCode',
|
|
162
|
+
'statusSuccess', 'statusWarning', 'statusError', 'statusInfo',
|
|
163
|
+
'collabPrimary', 'voiceRecording', 'voiceProcessing', 'bannerPrimary',
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
/** Validate that a value looks like a CSS colour. */
|
|
167
|
+
function isColorValue(v: unknown): v is string {
|
|
168
|
+
if (typeof v !== 'string') return false;
|
|
169
|
+
return /^(#[0-9a-fA-F]{3,8}|rgba?\(|oklch\(|hsla?\(|color-mix\()/.test(v.trim());
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function optionalColor(o: Record<string, unknown>, key: keyof ColorTokens): string | undefined {
|
|
173
|
+
const value = o[key];
|
|
174
|
+
return isColorValue(value) ? value : undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normaliseColorTokens(obj: unknown): ColorTokens | null {
|
|
178
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
179
|
+
const o = obj as Record<string, unknown>;
|
|
180
|
+
if (!LEGACY_REQUIRED_COLOR_KEYS.every((k) => isColorValue(o[k]))) return null;
|
|
181
|
+
|
|
182
|
+
const legacy = o as Record<keyof ColorTokens, string>;
|
|
183
|
+
return {
|
|
184
|
+
...legacy,
|
|
185
|
+
brandPrimary: optionalColor(o, 'brandPrimary') ?? legacy.statusSuccess,
|
|
186
|
+
brandPrimaryHover: optionalColor(o, 'brandPrimaryHover') ?? legacy.statusSuccess,
|
|
187
|
+
brandPrimaryForeground: optionalColor(o, 'brandPrimaryForeground') ?? legacy.textInverse,
|
|
188
|
+
brandSecondary: optionalColor(o, 'brandSecondary') ?? legacy.accentCode,
|
|
189
|
+
brandSecondaryHover: optionalColor(o, 'brandSecondaryHover') ?? legacy.accentHover,
|
|
190
|
+
brandSecondaryForeground: optionalColor(o, 'brandSecondaryForeground') ?? legacy.textInverse,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function sanitiseTypography(raw: unknown): ThemePreset['typography'] | undefined {
|
|
195
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
196
|
+
const o = raw as Record<string, unknown>;
|
|
197
|
+
const result: Record<string, string> = {};
|
|
198
|
+
for (const key of ['fontSans', 'fontMono', 'fontSizeBase'] as const) {
|
|
199
|
+
if (typeof o[key] === 'string') result[key] = o[key];
|
|
200
|
+
}
|
|
201
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function sanitiseSpacing(raw: unknown): ThemePreset['spacing'] | undefined {
|
|
205
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
206
|
+
const o = raw as Record<string, unknown>;
|
|
207
|
+
const result: Record<string, string> = {};
|
|
208
|
+
for (const key of ['xs', 'sm', 'md', 'lg', 'xl'] as const) {
|
|
209
|
+
if (typeof o[key] === 'string') result[key] = o[key];
|
|
210
|
+
}
|
|
211
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function sanitiseRadius(raw: unknown): ThemePreset['radius'] | undefined {
|
|
215
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
216
|
+
const o = raw as Record<string, unknown>;
|
|
217
|
+
const result: Record<string, string> = {};
|
|
218
|
+
for (const key of ['sm', 'md', 'lg'] as const) {
|
|
219
|
+
if (typeof o[key] === 'string') result[key] = o[key];
|
|
220
|
+
}
|
|
221
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Validate and normalise an unknown value into a ThemePreset. */
|
|
225
|
+
export function validateThemePreset(data: unknown): ThemePreset | null {
|
|
226
|
+
if (!data || typeof data !== 'object') return null;
|
|
227
|
+
const d = data as Record<string, unknown>;
|
|
228
|
+
|
|
229
|
+
if (typeof d.id !== 'string' || !d.id) return null;
|
|
230
|
+
if (typeof d.name !== 'string' || !d.name) return null;
|
|
231
|
+
if (d.version !== 1) return null;
|
|
232
|
+
|
|
233
|
+
const colors = d.colors as Record<string, unknown> | undefined;
|
|
234
|
+
const light = normaliseColorTokens(colors?.light);
|
|
235
|
+
const dark = normaliseColorTokens(colors?.dark);
|
|
236
|
+
if (!light || !dark) return null;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
id: d.id,
|
|
240
|
+
name: d.name,
|
|
241
|
+
description: typeof d.description === 'string' ? d.description : undefined,
|
|
242
|
+
author: typeof d.author === 'string' ? d.author : undefined,
|
|
243
|
+
version: 1,
|
|
244
|
+
builtin: d.builtin === true,
|
|
245
|
+
colors: { light, dark },
|
|
246
|
+
typography: sanitiseTypography(d.typography),
|
|
247
|
+
spacing: sanitiseSpacing(d.spacing),
|
|
248
|
+
radius: sanitiseRadius(d.radius),
|
|
249
|
+
};
|
|
250
|
+
}
|