@runtypelabs/persona 2.3.0 → 3.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 +221 -4
- package/dist/index.cjs +42 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +832 -571
- package/dist/index.d.ts +832 -571
- package/dist/index.global.js +87 -87
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +42 -42
- package/dist/index.js.map +1 -1
- package/dist/widget.css +205 -15
- package/package.json +2 -2
- package/src/components/artifact-card.ts +39 -5
- package/src/components/artifact-pane.ts +67 -126
- package/src/components/composer-builder.ts +3 -23
- package/src/components/header-builder.ts +29 -34
- package/src/components/header-layouts.ts +109 -41
- package/src/components/launcher.ts +10 -7
- package/src/components/message-bubble.ts +7 -11
- package/src/components/panel.ts +4 -4
- package/src/defaults.ts +22 -93
- package/src/index.ts +20 -7
- package/src/presets.ts +66 -51
- package/src/runtime/host-layout.test.ts +196 -0
- package/src/runtime/host-layout.ts +265 -27
- package/src/runtime/init.test.ts +77 -7
- package/src/styles/widget.css +205 -15
- package/src/types/theme.ts +76 -0
- package/src/types.ts +86 -97
- package/src/ui.docked.test.ts +203 -7
- package/src/ui.ts +129 -88
- package/src/utils/buttons.ts +417 -0
- package/src/utils/code-generators.test.ts +43 -7
- package/src/utils/code-generators.ts +9 -25
- package/src/utils/deep-merge.ts +26 -0
- package/src/utils/dock.ts +18 -5
- package/src/utils/dropdown.ts +178 -0
- package/src/utils/sanitize.ts +1 -1
- package/src/utils/theme.test.ts +90 -15
- package/src/utils/theme.ts +20 -46
- package/src/utils/tokens.ts +108 -11
- package/src/utils/migration.ts +0 -220
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createElement } from "./dom";
|
|
2
|
+
import { renderLucideIcon } from "./icons";
|
|
3
|
+
|
|
4
|
+
export interface DropdownMenuItem {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
/** Lucide icon name to show before the label. */
|
|
8
|
+
icon?: string;
|
|
9
|
+
/** When true, item text is styled in a destructive/danger color. */
|
|
10
|
+
destructive?: boolean;
|
|
11
|
+
/** When true, a visual divider is inserted before this item. */
|
|
12
|
+
dividerBefore?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CreateDropdownOptions {
|
|
16
|
+
/** Menu items to render. */
|
|
17
|
+
items: DropdownMenuItem[];
|
|
18
|
+
/** Called when a menu item is selected. */
|
|
19
|
+
onSelect: (id: string) => void;
|
|
20
|
+
/** Anchor element used for positioning. When `portal` is not set the menu is appended inside this element (which must have position: relative). */
|
|
21
|
+
anchor: HTMLElement;
|
|
22
|
+
/** Alignment of the menu relative to the anchor. Default: 'bottom-left'. */
|
|
23
|
+
position?: 'bottom-left' | 'bottom-right';
|
|
24
|
+
/**
|
|
25
|
+
* Portal target element. When set, the menu is appended to this element
|
|
26
|
+
* and uses fixed positioning calculated from the anchor's bounding rect.
|
|
27
|
+
* Use this to escape `overflow: hidden` containers.
|
|
28
|
+
*/
|
|
29
|
+
portal?: HTMLElement;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DropdownMenuHandle {
|
|
33
|
+
/** The menu DOM element. */
|
|
34
|
+
element: HTMLElement;
|
|
35
|
+
/** Show the menu. */
|
|
36
|
+
show: () => void;
|
|
37
|
+
/** Hide the menu. */
|
|
38
|
+
hide: () => void;
|
|
39
|
+
/** Toggle visibility. */
|
|
40
|
+
toggle: () => void;
|
|
41
|
+
/** Remove the menu and clean up all listeners. */
|
|
42
|
+
destroy: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a dropdown menu attached to an anchor element.
|
|
47
|
+
*
|
|
48
|
+
* The menu is styled via `.persona-dropdown-menu` CSS rules and themed
|
|
49
|
+
* through `--persona-dropdown-*` CSS variables with semantic fallbacks.
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { createDropdownMenu } from "@runtypelabs/persona";
|
|
53
|
+
*
|
|
54
|
+
* const dropdown = createDropdownMenu({
|
|
55
|
+
* items: [
|
|
56
|
+
* { id: "edit", label: "Edit", icon: "pencil" },
|
|
57
|
+
* { id: "delete", label: "Delete", icon: "trash-2", destructive: true, dividerBefore: true },
|
|
58
|
+
* ],
|
|
59
|
+
* onSelect: (id) => console.log("selected", id),
|
|
60
|
+
* anchor: buttonElement,
|
|
61
|
+
* });
|
|
62
|
+
* anchor.appendChild(dropdown.element);
|
|
63
|
+
* button.addEventListener("click", () => dropdown.toggle());
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function createDropdownMenu(options: CreateDropdownOptions): DropdownMenuHandle {
|
|
67
|
+
const { items, onSelect, anchor, position = 'bottom-left', portal } = options;
|
|
68
|
+
|
|
69
|
+
const menu = createElement("div", "persona-dropdown-menu persona-hidden");
|
|
70
|
+
menu.setAttribute("role", "menu");
|
|
71
|
+
menu.setAttribute("data-persona-theme-zone", "dropdown");
|
|
72
|
+
|
|
73
|
+
if (portal) {
|
|
74
|
+
// Fixed positioning — menu is portaled outside the anchor's overflow context
|
|
75
|
+
menu.style.position = "fixed";
|
|
76
|
+
menu.style.zIndex = "10000";
|
|
77
|
+
} else {
|
|
78
|
+
// Absolute positioning — menu lives inside the anchor
|
|
79
|
+
menu.style.position = "absolute";
|
|
80
|
+
menu.style.top = "100%";
|
|
81
|
+
menu.style.marginTop = "4px";
|
|
82
|
+
if (position === 'bottom-right') {
|
|
83
|
+
menu.style.right = "0";
|
|
84
|
+
} else {
|
|
85
|
+
menu.style.left = "0";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Build menu items
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
if (item.dividerBefore) {
|
|
92
|
+
const hr = document.createElement("hr");
|
|
93
|
+
menu.appendChild(hr);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const btn = document.createElement("button");
|
|
97
|
+
btn.type = "button";
|
|
98
|
+
btn.setAttribute("role", "menuitem");
|
|
99
|
+
btn.setAttribute("data-dropdown-item-id", item.id);
|
|
100
|
+
if (item.destructive) {
|
|
101
|
+
btn.setAttribute("data-destructive", "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (item.icon) {
|
|
105
|
+
const icon = renderLucideIcon(item.icon, 16, "currentColor", 1.5);
|
|
106
|
+
if (icon) btn.appendChild(icon);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const labelSpan = document.createElement("span");
|
|
110
|
+
labelSpan.textContent = item.label;
|
|
111
|
+
btn.appendChild(labelSpan);
|
|
112
|
+
|
|
113
|
+
btn.addEventListener("click", (e) => {
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
hide();
|
|
116
|
+
onSelect(item.id);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
menu.appendChild(btn);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let cleanupClickOutside: (() => void) | null = null;
|
|
123
|
+
|
|
124
|
+
/** Reposition a portaled menu based on the anchor's current bounding rect. */
|
|
125
|
+
function reposition() {
|
|
126
|
+
if (!portal) return;
|
|
127
|
+
const rect = anchor.getBoundingClientRect();
|
|
128
|
+
menu.style.top = `${rect.bottom + 4}px`;
|
|
129
|
+
if (position === 'bottom-right') {
|
|
130
|
+
menu.style.right = `${window.innerWidth - rect.right}px`;
|
|
131
|
+
menu.style.left = "auto";
|
|
132
|
+
} else {
|
|
133
|
+
menu.style.left = `${rect.left}px`;
|
|
134
|
+
menu.style.right = "auto";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function show() {
|
|
139
|
+
reposition();
|
|
140
|
+
menu.classList.remove("persona-hidden");
|
|
141
|
+
// Defer click-outside listener to avoid catching the triggering click
|
|
142
|
+
requestAnimationFrame(() => {
|
|
143
|
+
const handler = (e: MouseEvent) => {
|
|
144
|
+
if (!menu.contains(e.target as Node) && !anchor.contains(e.target as Node)) {
|
|
145
|
+
hide();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
document.addEventListener("click", handler, true);
|
|
149
|
+
cleanupClickOutside = () => document.removeEventListener("click", handler, true);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hide() {
|
|
154
|
+
menu.classList.add("persona-hidden");
|
|
155
|
+
cleanupClickOutside?.();
|
|
156
|
+
cleanupClickOutside = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function toggle() {
|
|
160
|
+
if (menu.classList.contains("persona-hidden")) {
|
|
161
|
+
show();
|
|
162
|
+
} else {
|
|
163
|
+
hide();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function destroy() {
|
|
168
|
+
hide();
|
|
169
|
+
menu.remove();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Append to portal target or let the caller append manually
|
|
173
|
+
if (portal) {
|
|
174
|
+
portal.appendChild(menu);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { element: menu, show, hide, toggle, destroy };
|
|
178
|
+
}
|
package/src/utils/sanitize.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const createDefaultSanitizer = (): SanitizeFunction => {
|
|
|
59
59
|
const val = data.attrValue;
|
|
60
60
|
if (val.toLowerCase().startsWith("data:") && !SAFE_DATA_URI.test(val)) {
|
|
61
61
|
data.attrValue = "";
|
|
62
|
-
data.
|
|
62
|
+
data.keepAttr = false;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
});
|
package/src/utils/theme.test.ts
CHANGED
|
@@ -12,14 +12,22 @@ describe('theme utils', () => {
|
|
|
12
12
|
const lightAndDarkThemeConfig = {
|
|
13
13
|
colorScheme: 'dark' as const,
|
|
14
14
|
theme: {
|
|
15
|
-
|
|
15
|
+
palette: {
|
|
16
|
+
colors: {
|
|
17
|
+
primary: { 500: '#111111' },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
16
20
|
},
|
|
17
21
|
darkTheme: {
|
|
18
|
-
|
|
22
|
+
palette: {
|
|
23
|
+
colors: {
|
|
24
|
+
primary: { 500: '#22c55e' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
19
27
|
},
|
|
20
28
|
};
|
|
21
29
|
|
|
22
|
-
const activeTheme = getActiveTheme(lightAndDarkThemeConfig
|
|
30
|
+
const activeTheme = getActiveTheme(lightAndDarkThemeConfig);
|
|
23
31
|
const cssVars = themeToCssVariables(activeTheme);
|
|
24
32
|
|
|
25
33
|
expect(cssVars['--persona-palette-colors-primary-500']).toBe('#22c55e');
|
|
@@ -31,14 +39,22 @@ describe('theme utils', () => {
|
|
|
31
39
|
const lightAndDarkThemeConfig = {
|
|
32
40
|
colorScheme: 'auto' as const,
|
|
33
41
|
theme: {
|
|
34
|
-
|
|
42
|
+
palette: {
|
|
43
|
+
colors: {
|
|
44
|
+
primary: { 500: '#111111' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
35
47
|
},
|
|
36
48
|
darkTheme: {
|
|
37
|
-
|
|
49
|
+
palette: {
|
|
50
|
+
colors: {
|
|
51
|
+
primary: { 500: '#22c55e' },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
38
54
|
},
|
|
39
55
|
};
|
|
40
56
|
|
|
41
|
-
const activeTheme = getActiveTheme(lightAndDarkThemeConfig
|
|
57
|
+
const activeTheme = getActiveTheme(lightAndDarkThemeConfig);
|
|
42
58
|
const cssVars = themeToCssVariables(activeTheme);
|
|
43
59
|
|
|
44
60
|
expect(cssVars['--persona-palette-colors-primary-500']).toBe('#22c55e');
|
|
@@ -123,19 +139,74 @@ describe('theme utils', () => {
|
|
|
123
139
|
expect(cssVars['--persona-md-prose-font-family']).toBe('Georgia, serif');
|
|
124
140
|
});
|
|
125
141
|
|
|
126
|
-
it('maps
|
|
142
|
+
it('maps header chrome tokens to dedicated CSS variables with semantic fallbacks', () => {
|
|
143
|
+
const theme = createTheme();
|
|
144
|
+
const cssVars = themeToCssVariables(theme);
|
|
145
|
+
|
|
146
|
+
expect(cssVars['--persona-header-icon-bg']).toBe(cssVars['--persona-primary']);
|
|
147
|
+
expect(cssVars['--persona-header-icon-fg']).toBe(cssVars['--persona-text-inverse']);
|
|
148
|
+
expect(cssVars['--persona-header-title-fg']).toBe(cssVars['--persona-primary']);
|
|
149
|
+
expect(cssVars['--persona-header-subtitle-fg']).toBe(cssVars['--persona-text-muted']);
|
|
150
|
+
expect(cssVars['--persona-header-action-icon-fg']).toBe(cssVars['--persona-muted']);
|
|
151
|
+
|
|
152
|
+
const custom = createTheme({
|
|
153
|
+
components: {
|
|
154
|
+
header: {
|
|
155
|
+
iconBackground: 'palette.colors.accent.500',
|
|
156
|
+
iconForeground: 'palette.colors.gray.900',
|
|
157
|
+
titleForeground: 'palette.colors.secondary.500',
|
|
158
|
+
subtitleForeground: 'palette.colors.gray.500',
|
|
159
|
+
actionIconForeground: 'palette.colors.gray.400',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
} as any);
|
|
163
|
+
const customVars = themeToCssVariables(custom);
|
|
164
|
+
expect(customVars['--persona-header-icon-bg']).toBe('#06b6d4');
|
|
165
|
+
expect(customVars['--persona-header-icon-fg']).toBe('#111827');
|
|
166
|
+
expect(customVars['--persona-header-title-fg']).toBe('#8b5cf6');
|
|
167
|
+
expect(customVars['--persona-header-subtitle-fg']).toBe('#6b7280');
|
|
168
|
+
expect(customVars['--persona-header-action-icon-fg']).toBe('#9ca3af');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('defaults artifact pane fill from semantic container and resolves toolbar background token refs', () => {
|
|
172
|
+
const theme = createTheme();
|
|
173
|
+
const cssVars = themeToCssVariables(theme);
|
|
174
|
+
|
|
175
|
+
expect(cssVars['--persona-components-artifact-pane-background']).toBe('#f3f4f6');
|
|
176
|
+
expect(cssVars['--persona-artifact-toolbar-bg']).toBe('#f3f4f6');
|
|
177
|
+
|
|
178
|
+
const surfacePane = createTheme({
|
|
179
|
+
components: {
|
|
180
|
+
artifact: {
|
|
181
|
+
pane: {
|
|
182
|
+
background: 'semantic.colors.surface',
|
|
183
|
+
toolbarBackground: 'semantic.colors.surface',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
} as any);
|
|
188
|
+
const surfaceVars = themeToCssVariables(surfacePane);
|
|
189
|
+
expect(surfaceVars['--persona-components-artifact-pane-background']).toBe('#f9fafb');
|
|
190
|
+
expect(surfaceVars['--persona-artifact-toolbar-bg']).toBe('#f9fafb');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('maps component bubble shadow tokens to consumer CSS variables', () => {
|
|
127
194
|
const cfg = {
|
|
128
195
|
colorScheme: 'light' as const,
|
|
129
196
|
theme: {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
197
|
+
components: {
|
|
198
|
+
toolBubble: { shadow: 'none' },
|
|
199
|
+
reasoningBubble: { shadow: 'none' },
|
|
200
|
+
composer: { shadow: 'none' },
|
|
201
|
+
message: {
|
|
202
|
+
user: { shadow: 'none' },
|
|
203
|
+
assistant: { shadow: 'none' },
|
|
204
|
+
},
|
|
205
|
+
},
|
|
135
206
|
},
|
|
136
207
|
};
|
|
137
208
|
|
|
138
|
-
const active = getActiveTheme(cfg
|
|
209
|
+
const active = getActiveTheme(cfg);
|
|
139
210
|
const cssVars = themeToCssVariables(active);
|
|
140
211
|
|
|
141
212
|
expect(cssVars['--persona-tool-bubble-shadow']).toBe('none');
|
|
@@ -149,9 +220,13 @@ describe('theme utils', () => {
|
|
|
149
220
|
const el = document.createElement('div');
|
|
150
221
|
applyThemeVariables(el, {
|
|
151
222
|
colorScheme: 'light',
|
|
152
|
-
theme: {
|
|
223
|
+
theme: {
|
|
224
|
+
components: {
|
|
225
|
+
toolBubble: { shadow: '0 1px 2px rgba(255,0,0,0.5)' },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
153
228
|
toolCall: { shadow: 'none' },
|
|
154
|
-
}
|
|
229
|
+
});
|
|
155
230
|
expect(el.style.getPropertyValue('--persona-tool-bubble-shadow').trim()).toBe('none');
|
|
156
231
|
});
|
|
157
232
|
});
|
package/src/utils/theme.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { PersonaTheme } from '../types/theme';
|
|
2
|
-
import type { AgentWidgetConfig
|
|
1
|
+
import type { DeepPartial, PersonaTheme } from '../types/theme';
|
|
2
|
+
import type { AgentWidgetConfig } from '../types';
|
|
3
3
|
import { createTheme, resolveTokens, themeToCssVariables } from './tokens';
|
|
4
|
-
import {
|
|
4
|
+
import { deepMerge } from './deep-merge';
|
|
5
5
|
|
|
6
6
|
export type ColorScheme = 'light' | 'dark' | 'auto';
|
|
7
7
|
|
|
8
8
|
export interface PersonaWidgetConfig {
|
|
9
|
-
theme?:
|
|
10
|
-
darkTheme?:
|
|
9
|
+
theme?: DeepPartial<PersonaTheme>;
|
|
10
|
+
darkTheme?: DeepPartial<PersonaTheme>;
|
|
11
11
|
colorScheme?: ColorScheme;
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -106,43 +106,14 @@ const DARK_PALETTE = {
|
|
|
106
106
|
},
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const deepMerge = <T extends Record<string, unknown>>(
|
|
113
|
-
base: T | undefined,
|
|
114
|
-
override: Record<string, unknown> | undefined
|
|
115
|
-
): T | Record<string, unknown> | undefined => {
|
|
116
|
-
if (!base) return override;
|
|
117
|
-
if (!override) return base;
|
|
118
|
-
|
|
119
|
-
const merged: Record<string, unknown> = { ...base };
|
|
120
|
-
|
|
121
|
-
for (const [key, value] of Object.entries(override)) {
|
|
122
|
-
const existing = merged[key];
|
|
123
|
-
if (isObject(existing) && isObject(value)) {
|
|
124
|
-
merged[key] = deepMerge(existing, value);
|
|
125
|
-
} else {
|
|
126
|
-
merged[key] = value;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return merged;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const isTokenTheme = (theme: unknown): theme is Partial<PersonaTheme> => {
|
|
134
|
-
return isObject(theme) && ('palette' in theme || 'semantic' in theme || 'components' in theme);
|
|
135
|
-
};
|
|
136
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Normalize theme config for merging; rejects non-objects.
|
|
111
|
+
*/
|
|
137
112
|
const normalizeThemeConfig = (
|
|
138
|
-
theme:
|
|
139
|
-
):
|
|
140
|
-
if (!theme) return undefined;
|
|
141
|
-
|
|
142
|
-
if (isTokenTheme(theme)) {
|
|
143
|
-
return deepMerge(migratedTheme, theme) as Partial<PersonaTheme>;
|
|
144
|
-
}
|
|
145
|
-
return migratedTheme;
|
|
113
|
+
theme: DeepPartial<PersonaTheme> | Record<string, unknown> | undefined
|
|
114
|
+
): DeepPartial<PersonaTheme> | undefined => {
|
|
115
|
+
if (!theme || typeof theme !== 'object' || Array.isArray(theme)) return undefined;
|
|
116
|
+
return theme as DeepPartial<PersonaTheme>;
|
|
146
117
|
};
|
|
147
118
|
|
|
148
119
|
export const detectColorScheme = (): 'light' | 'dark' => {
|
|
@@ -170,11 +141,11 @@ export const getColorScheme = (config?: WidgetConfig): 'light' | 'dark' => {
|
|
|
170
141
|
return getColorSchemeFromConfig(config);
|
|
171
142
|
};
|
|
172
143
|
|
|
173
|
-
export const createLightTheme = (userConfig?:
|
|
144
|
+
export const createLightTheme = (userConfig?: DeepPartial<PersonaTheme>): PersonaTheme => {
|
|
174
145
|
return createTheme(userConfig);
|
|
175
146
|
};
|
|
176
147
|
|
|
177
|
-
export const createDarkTheme = (userConfig?:
|
|
148
|
+
export const createDarkTheme = (userConfig?: DeepPartial<PersonaTheme>): PersonaTheme => {
|
|
178
149
|
const baseTheme = createTheme(undefined, { validate: false });
|
|
179
150
|
|
|
180
151
|
return createTheme(
|
|
@@ -194,12 +165,15 @@ export const createDarkTheme = (userConfig?: Partial<PersonaTheme>): PersonaThem
|
|
|
194
165
|
|
|
195
166
|
export const getActiveTheme = (config?: WidgetConfig): PersonaTheme => {
|
|
196
167
|
const scheme = getColorScheme(config);
|
|
197
|
-
const lightThemeConfig = normalizeThemeConfig(config?.theme
|
|
198
|
-
const darkThemeConfig = normalizeThemeConfig(config?.darkTheme
|
|
168
|
+
const lightThemeConfig = normalizeThemeConfig(config?.theme);
|
|
169
|
+
const darkThemeConfig = normalizeThemeConfig(config?.darkTheme);
|
|
199
170
|
|
|
200
171
|
if (scheme === 'dark') {
|
|
201
172
|
return createDarkTheme(
|
|
202
|
-
deepMerge(
|
|
173
|
+
deepMerge(
|
|
174
|
+
(lightThemeConfig ?? {}) as Record<string, unknown>,
|
|
175
|
+
(darkThemeConfig ?? {}) as Record<string, unknown>
|
|
176
|
+
) as DeepPartial<PersonaTheme>
|
|
203
177
|
);
|
|
204
178
|
}
|
|
205
179
|
|
package/src/utils/tokens.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
DeepPartial,
|
|
2
3
|
PersonaTheme,
|
|
3
4
|
ResolvedToken,
|
|
4
5
|
ThemeValidationResult,
|
|
@@ -265,6 +266,11 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
265
266
|
border: 'semantic.colors.border',
|
|
266
267
|
borderRadius: 'palette.radius.xl palette.radius.xl 0 0',
|
|
267
268
|
padding: 'semantic.spacing.md',
|
|
269
|
+
iconBackground: 'semantic.colors.primary',
|
|
270
|
+
iconForeground: 'semantic.colors.textInverse',
|
|
271
|
+
titleForeground: 'semantic.colors.primary',
|
|
272
|
+
subtitleForeground: 'semantic.colors.textMuted',
|
|
273
|
+
actionIconForeground: 'semantic.colors.textMuted',
|
|
268
274
|
},
|
|
269
275
|
message: {
|
|
270
276
|
user: {
|
|
@@ -341,9 +347,15 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
341
347
|
border: 'palette.colors.gray.200',
|
|
342
348
|
},
|
|
343
349
|
},
|
|
350
|
+
artifact: {
|
|
351
|
+
pane: {
|
|
352
|
+
background: 'semantic.colors.container',
|
|
353
|
+
toolbarBackground: 'semantic.colors.container',
|
|
354
|
+
},
|
|
355
|
+
},
|
|
344
356
|
};
|
|
345
357
|
|
|
346
|
-
function resolveTokenValue(theme: PersonaTheme, path: string): string | undefined {
|
|
358
|
+
export function resolveTokenValue(theme: PersonaTheme, path: string): string | undefined {
|
|
347
359
|
if (
|
|
348
360
|
!path.startsWith('palette.') &&
|
|
349
361
|
!path.startsWith('semantic.') &&
|
|
@@ -481,7 +493,7 @@ function deepMergeComponents(
|
|
|
481
493
|
}
|
|
482
494
|
|
|
483
495
|
export function createTheme(
|
|
484
|
-
userConfig?:
|
|
496
|
+
userConfig?: DeepPartial<PersonaTheme>,
|
|
485
497
|
options: CreateThemeOptions = {}
|
|
486
498
|
): PersonaTheme {
|
|
487
499
|
const baseTheme: PersonaTheme = {
|
|
@@ -543,8 +555,11 @@ export function createTheme(
|
|
|
543
555
|
...userConfig?.semantic?.typography,
|
|
544
556
|
},
|
|
545
557
|
},
|
|
546
|
-
components: deepMergeComponents(
|
|
547
|
-
|
|
558
|
+
components: deepMergeComponents(
|
|
559
|
+
baseTheme.components,
|
|
560
|
+
userConfig?.components as Partial<ComponentTokens> | undefined
|
|
561
|
+
),
|
|
562
|
+
} as PersonaTheme;
|
|
548
563
|
|
|
549
564
|
if (options.validate !== false) {
|
|
550
565
|
const validation = validateTheme(theme);
|
|
@@ -606,6 +621,9 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
606
621
|
cssVars['--persona-font-weight'] = cssVars['--persona-semantic-typography-fontWeight'] ?? cssVars['--persona-palette-typography-fontWeight-normal'];
|
|
607
622
|
cssVars['--persona-line-height'] = cssVars['--persona-semantic-typography-lineHeight'] ?? cssVars['--persona-palette-typography-lineHeight-normal'];
|
|
608
623
|
|
|
624
|
+
cssVars['--persona-input-font-family'] = cssVars['--persona-font-family'];
|
|
625
|
+
cssVars['--persona-input-font-weight'] = cssVars['--persona-font-weight'];
|
|
626
|
+
|
|
609
627
|
// Radius aliases used throughout the existing widget CSS.
|
|
610
628
|
cssVars['--persona-radius-sm'] = cssVars['--persona-palette-radius-sm'] ?? '0.125rem';
|
|
611
629
|
cssVars['--persona-radius-md'] = cssVars['--persona-palette-radius-md'] ?? '0.375rem';
|
|
@@ -623,6 +641,12 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
623
641
|
cssVars['--persona-components-panel-borderRadius'] ??
|
|
624
642
|
cssVars['--persona-radius-xl'] ??
|
|
625
643
|
'0.75rem';
|
|
644
|
+
cssVars['--persona-panel-border'] =
|
|
645
|
+
cssVars['--persona-components-panel-border'] ?? `1px solid ${cssVars['--persona-border']}`;
|
|
646
|
+
cssVars['--persona-panel-shadow'] =
|
|
647
|
+
cssVars['--persona-components-panel-shadow'] ??
|
|
648
|
+
cssVars['--persona-palette-shadows-xl'] ??
|
|
649
|
+
'0 25px 50px -12px rgba(0, 0, 0, 0.25)';
|
|
626
650
|
cssVars['--persona-input-radius'] =
|
|
627
651
|
cssVars['--persona-components-input-borderRadius'] ??
|
|
628
652
|
cssVars['--persona-radius-lg'] ??
|
|
@@ -642,6 +666,20 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
642
666
|
cssVars['--persona-components-header-background'] ?? cssVars['--persona-surface'];
|
|
643
667
|
cssVars['--persona-header-border'] =
|
|
644
668
|
cssVars['--persona-components-header-border'] ?? cssVars['--persona-divider'];
|
|
669
|
+
cssVars['--persona-header-icon-bg'] =
|
|
670
|
+
cssVars['--persona-components-header-iconBackground'] ?? cssVars['--persona-primary'];
|
|
671
|
+
cssVars['--persona-header-icon-fg'] =
|
|
672
|
+
cssVars['--persona-components-header-iconForeground'] ?? cssVars['--persona-text-inverse'];
|
|
673
|
+
cssVars['--persona-header-title-fg'] =
|
|
674
|
+
cssVars['--persona-components-header-titleForeground'] ?? cssVars['--persona-primary'];
|
|
675
|
+
cssVars['--persona-header-subtitle-fg'] =
|
|
676
|
+
cssVars['--persona-components-header-subtitleForeground'] ?? cssVars['--persona-text-muted'];
|
|
677
|
+
cssVars['--persona-header-action-icon-fg'] =
|
|
678
|
+
cssVars['--persona-components-header-actionIconForeground'] ?? cssVars['--persona-muted'];
|
|
679
|
+
|
|
680
|
+
const headerTokens = theme.components?.header;
|
|
681
|
+
if (headerTokens?.shadow) cssVars['--persona-header-shadow'] = headerTokens.shadow;
|
|
682
|
+
if (headerTokens?.borderBottom) cssVars['--persona-header-border-bottom'] = headerTokens.borderBottom;
|
|
645
683
|
|
|
646
684
|
cssVars['--persona-message-user-bg'] =
|
|
647
685
|
cssVars['--persona-components-message-user-background'] ?? cssVars['--persona-accent'];
|
|
@@ -689,8 +727,42 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
689
727
|
cssVars['--persona-md-prose-font-family'] = mdProseFont;
|
|
690
728
|
}
|
|
691
729
|
|
|
692
|
-
//
|
|
730
|
+
// Icon button tokens
|
|
693
731
|
const components = theme.components;
|
|
732
|
+
const iconBtn = components?.iconButton;
|
|
733
|
+
if (iconBtn) {
|
|
734
|
+
if (iconBtn.background) cssVars['--persona-icon-btn-bg'] = iconBtn.background;
|
|
735
|
+
if (iconBtn.border) cssVars['--persona-icon-btn-border'] = iconBtn.border;
|
|
736
|
+
if (iconBtn.color) cssVars['--persona-icon-btn-color'] = iconBtn.color;
|
|
737
|
+
if (iconBtn.padding) cssVars['--persona-icon-btn-padding'] = iconBtn.padding;
|
|
738
|
+
if (iconBtn.borderRadius) cssVars['--persona-icon-btn-radius'] = iconBtn.borderRadius;
|
|
739
|
+
if (iconBtn.hoverBackground) cssVars['--persona-icon-btn-hover-bg'] = iconBtn.hoverBackground;
|
|
740
|
+
if (iconBtn.hoverColor) cssVars['--persona-icon-btn-hover-color'] = iconBtn.hoverColor;
|
|
741
|
+
if (iconBtn.activeBackground) cssVars['--persona-icon-btn-active-bg'] = iconBtn.activeBackground;
|
|
742
|
+
if (iconBtn.activeBorder) cssVars['--persona-icon-btn-active-border'] = iconBtn.activeBorder;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Label button tokens
|
|
746
|
+
const labelBtn = components?.labelButton;
|
|
747
|
+
if (labelBtn) {
|
|
748
|
+
if (labelBtn.background) cssVars['--persona-label-btn-bg'] = labelBtn.background;
|
|
749
|
+
if (labelBtn.border) cssVars['--persona-label-btn-border'] = labelBtn.border;
|
|
750
|
+
if (labelBtn.color) cssVars['--persona-label-btn-color'] = labelBtn.color;
|
|
751
|
+
if (labelBtn.padding) cssVars['--persona-label-btn-padding'] = labelBtn.padding;
|
|
752
|
+
if (labelBtn.borderRadius) cssVars['--persona-label-btn-radius'] = labelBtn.borderRadius;
|
|
753
|
+
if (labelBtn.hoverBackground) cssVars['--persona-label-btn-hover-bg'] = labelBtn.hoverBackground;
|
|
754
|
+
if (labelBtn.fontSize) cssVars['--persona-label-btn-font-size'] = labelBtn.fontSize;
|
|
755
|
+
if (labelBtn.gap) cssVars['--persona-label-btn-gap'] = labelBtn.gap;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Toggle group tokens
|
|
759
|
+
const toggleGrp = components?.toggleGroup;
|
|
760
|
+
if (toggleGrp) {
|
|
761
|
+
if (toggleGrp.gap) cssVars['--persona-toggle-group-gap'] = toggleGrp.gap;
|
|
762
|
+
if (toggleGrp.borderRadius) cssVars['--persona-toggle-group-radius'] = toggleGrp.borderRadius;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Artifact tokens
|
|
694
766
|
const artifact = components?.artifact;
|
|
695
767
|
if (artifact?.toolbar) {
|
|
696
768
|
const t = artifact.toolbar;
|
|
@@ -706,11 +778,28 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
706
778
|
if (t.copyColor) cssVars['--persona-artifact-toolbar-copy-color'] = t.copyColor;
|
|
707
779
|
if (t.copyBorderRadius) cssVars['--persona-artifact-toolbar-copy-radius'] = t.copyBorderRadius;
|
|
708
780
|
if (t.copyPadding) cssVars['--persona-artifact-toolbar-copy-padding'] = t.copyPadding;
|
|
709
|
-
if (t.copyMenuBackground)
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (t.
|
|
781
|
+
if (t.copyMenuBackground) {
|
|
782
|
+
cssVars['--persona-artifact-toolbar-copy-menu-bg'] = t.copyMenuBackground;
|
|
783
|
+
cssVars['--persona-dropdown-bg'] = cssVars['--persona-dropdown-bg'] ?? t.copyMenuBackground;
|
|
784
|
+
}
|
|
785
|
+
if (t.copyMenuBorder) {
|
|
786
|
+
cssVars['--persona-artifact-toolbar-copy-menu-border'] = t.copyMenuBorder;
|
|
787
|
+
cssVars['--persona-dropdown-border'] = cssVars['--persona-dropdown-border'] ?? t.copyMenuBorder;
|
|
788
|
+
}
|
|
789
|
+
if (t.copyMenuShadow) {
|
|
790
|
+
cssVars['--persona-artifact-toolbar-copy-menu-shadow'] = t.copyMenuShadow;
|
|
791
|
+
cssVars['--persona-dropdown-shadow'] = cssVars['--persona-dropdown-shadow'] ?? t.copyMenuShadow;
|
|
792
|
+
}
|
|
793
|
+
if (t.copyMenuBorderRadius) {
|
|
794
|
+
cssVars['--persona-artifact-toolbar-copy-menu-radius'] = t.copyMenuBorderRadius;
|
|
795
|
+
cssVars['--persona-dropdown-radius'] = cssVars['--persona-dropdown-radius'] ?? t.copyMenuBorderRadius;
|
|
796
|
+
}
|
|
797
|
+
if (t.copyMenuItemHoverBackground) {
|
|
798
|
+
cssVars['--persona-artifact-toolbar-copy-menu-item-hover-bg'] = t.copyMenuItemHoverBackground;
|
|
799
|
+
cssVars['--persona-dropdown-item-hover-bg'] = cssVars['--persona-dropdown-item-hover-bg'] ?? t.copyMenuItemHoverBackground;
|
|
800
|
+
}
|
|
801
|
+
if (t.iconBackground) cssVars['--persona-artifact-toolbar-icon-bg'] = t.iconBackground;
|
|
802
|
+
if (t.toolbarBorder) cssVars['--persona-artifact-toolbar-border'] = t.toolbarBorder;
|
|
714
803
|
}
|
|
715
804
|
if (artifact?.tab) {
|
|
716
805
|
const t = artifact.tab;
|
|
@@ -719,10 +808,18 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
719
808
|
if (t.activeBorder) cssVars['--persona-artifact-tab-active-border'] = t.activeBorder;
|
|
720
809
|
if (t.borderRadius) cssVars['--persona-artifact-tab-radius'] = t.borderRadius;
|
|
721
810
|
if (t.textColor) cssVars['--persona-artifact-tab-color'] = t.textColor;
|
|
811
|
+
if (t.hoverBackground) cssVars['--persona-artifact-tab-hover-bg'] = t.hoverBackground;
|
|
812
|
+
if (t.listBackground) cssVars['--persona-artifact-tab-list-bg'] = t.listBackground;
|
|
813
|
+
if (t.listBorderColor) cssVars['--persona-artifact-tab-list-border-color'] = t.listBorderColor;
|
|
814
|
+
if (t.listPadding) cssVars['--persona-artifact-tab-list-padding'] = t.listPadding;
|
|
722
815
|
}
|
|
723
816
|
if (artifact?.pane) {
|
|
724
817
|
const t = artifact.pane;
|
|
725
|
-
if (t.toolbarBackground)
|
|
818
|
+
if (t.toolbarBackground) {
|
|
819
|
+
const toolbarBg =
|
|
820
|
+
resolveTokenValue(theme, t.toolbarBackground) ?? t.toolbarBackground;
|
|
821
|
+
cssVars['--persona-artifact-toolbar-bg'] = toolbarBg;
|
|
822
|
+
}
|
|
726
823
|
}
|
|
727
824
|
|
|
728
825
|
return cssVars;
|