@runtypelabs/persona 4.15.0 → 4.17.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/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +283 -9
- package/dist/context-mentions.d.ts +283 -9
- package/dist/index.cjs +68 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +299 -10
- package/dist/index.d.ts +299 -10
- package/dist/index.global.js +57 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +69 -69
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +36 -36
- package/dist/smart-dom-reader.d.cts +283 -9
- package/dist/smart-dom-reader.d.ts +283 -9
- package/dist/testing.cjs +3 -3
- package/dist/testing.d.cts +7 -0
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +64 -64
- package/dist/theme-editor-preview.d.cts +283 -9
- package/dist/theme-editor-preview.d.ts +283 -9
- package/dist/theme-editor-preview.js +56 -56
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +283 -10
- package/dist/theme-editor.d.ts +283 -10
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +66 -3
- package/dist/theme-reference.d.ts +66 -3
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +2 -2
- package/src/components/artifact-pane.test.ts +27 -0
- package/src/components/artifact-pane.ts +7 -3
- package/src/components/header-builder.ts +27 -2
- package/src/components/header-layouts.test.ts +56 -0
- package/src/components/header-layouts.ts +29 -9
- package/src/components/launcher-surface.test.ts +403 -0
- package/src/components/launcher.ts +297 -2
- package/src/components/panel.ts +69 -74
- package/src/components/suggestions.test.ts +40 -0
- package/src/components/suggestions.ts +213 -153
- package/src/components/welcome.test.ts +121 -0
- package/src/components/welcome.ts +305 -0
- package/src/components/widget-view.test.ts +5 -3
- package/src/components/widget-view.ts +14 -7
- package/src/defaults.ts +2 -2
- package/src/index-core.ts +12 -1
- package/src/install.test.ts +35 -1
- package/src/install.ts +47 -11
- package/src/launcher-global.ts +10 -7
- package/src/plugins/types.ts +66 -1
- package/src/styles/widget-styles.test.ts +220 -0
- package/src/styles/widget.css +353 -9
- package/src/testing/mock-stream.test.ts +19 -0
- package/src/testing/mock-stream.ts +20 -0
- package/src/theme-editor/sections.ts +30 -1
- package/src/theme-reference.ts +26 -4
- package/src/types/theme.ts +40 -3
- package/src/types.ts +164 -6
- package/src/ui.body-top-slot.test.ts +95 -0
- package/src/ui.content-width.test.ts +91 -0
- package/src/ui.docked.test.ts +7 -4
- package/src/ui.launcher-teaser.test.ts +162 -0
- package/src/ui.rebuild-composer.test.ts +491 -0
- package/src/ui.render-welcome.test.ts +505 -0
- package/src/ui.scroll-additive.test.ts +91 -21
- package/src/ui.scroll.test.ts +302 -14
- package/src/ui.suggest-replies.test.ts +46 -0
- package/src/ui.ts +747 -136
- package/src/ui.welcome.test.ts +504 -0
- package/src/utils/artifact-gate.test.ts +27 -0
- package/src/utils/artifact-gate.ts +9 -0
- package/src/utils/attachment-manager.test.ts +37 -0
- package/src/utils/attachment-manager.ts +15 -0
- package/src/utils/auto-follow.test.ts +23 -0
- package/src/utils/auto-follow.ts +11 -1
- package/src/utils/config-merge.ts +2 -0
- package/src/utils/content-width.test.ts +37 -0
- package/src/utils/content-width.ts +17 -0
- package/src/utils/plugin-storage.test.ts +94 -0
- package/src/utils/plugin-storage.ts +98 -0
- package/src/utils/theme.test.ts +137 -0
- package/src/utils/tokens.ts +49 -3
- package/src/welcome.test.ts +213 -0
- package/src/welcome.ts +130 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { resolveContentMaxWidth } from "./content-width";
|
|
4
|
+
import type { AgentWidgetConfig } from "../types";
|
|
5
|
+
|
|
6
|
+
const cfg = (value?: Partial<AgentWidgetConfig>) =>
|
|
7
|
+
value as AgentWidgetConfig | undefined;
|
|
8
|
+
|
|
9
|
+
describe("resolveContentMaxWidth", () => {
|
|
10
|
+
it("defaults panels to the surveyed 768px column", () => {
|
|
11
|
+
expect(resolveContentMaxWidth(cfg({}), false)).toBe("768px");
|
|
12
|
+
expect(resolveContentMaxWidth(undefined, false)).toBe("768px");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("keeps the composer-bar's own 720px fallback", () => {
|
|
16
|
+
expect(resolveContentMaxWidth(cfg({}), true)).toBe("720px");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("lets layout.contentMaxWidth win everywhere", () => {
|
|
20
|
+
const config = cfg({ layout: { contentMaxWidth: "90ch" } });
|
|
21
|
+
expect(resolveContentMaxWidth(config, false)).toBe("90ch");
|
|
22
|
+
expect(resolveContentMaxWidth(config, true)).toBe("90ch");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("passes the \"none\" opt-out through untouched", () => {
|
|
26
|
+
expect(
|
|
27
|
+
resolveContentMaxWidth(cfg({ layout: { contentMaxWidth: "none" } }), false)
|
|
28
|
+
).toBe("none");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("prefers composerBar.contentMaxWidth over the 720px fallback", () => {
|
|
32
|
+
const config = cfg({
|
|
33
|
+
launcher: { composerBar: { contentMaxWidth: "600px" } },
|
|
34
|
+
});
|
|
35
|
+
expect(resolveContentMaxWidth(config, true)).toBe("600px");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentWidgetConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolver-owned default: every surveyed product caps the conversation
|
|
5
|
+
* column (704 to 896px, modal value 768px,
|
|
6
|
+
* "Hero width versus transcript width"). Kept out of defaults.ts so a
|
|
7
|
+
* materialized value cannot override the composer-bar's own 720px fallback.
|
|
8
|
+
* `"none"` opts out (applied as `max-width: none`).
|
|
9
|
+
*/
|
|
10
|
+
export const resolveContentMaxWidth = (
|
|
11
|
+
config: AgentWidgetConfig | undefined,
|
|
12
|
+
composerBar: boolean
|
|
13
|
+
): string =>
|
|
14
|
+
config?.layout?.contentMaxWidth ??
|
|
15
|
+
(composerBar
|
|
16
|
+
? config?.launcher?.composerBar?.contentMaxWidth ?? "720px"
|
|
17
|
+
: "768px");
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createPluginStorageFactory, pluginStorageKey } from "./plugin-storage";
|
|
6
|
+
import type { AgentWidgetConfig } from "../types";
|
|
7
|
+
|
|
8
|
+
const config = (patch: Partial<AgentWidgetConfig> = {}): AgentWidgetConfig =>
|
|
9
|
+
({ apiUrl: "https://api.example.com/chat", ...patch }) as AgentWidgetConfig;
|
|
10
|
+
|
|
11
|
+
describe("plugin storage facade", () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
window.localStorage.clear();
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("namespaces keys by keyPrefix and plugin id", () => {
|
|
18
|
+
expect(pluginStorageKey(config(), "home-screen", "view")).toBe(
|
|
19
|
+
"persona-plugin:home-screen:view"
|
|
20
|
+
);
|
|
21
|
+
expect(
|
|
22
|
+
pluginStorageKey(
|
|
23
|
+
config({ persistState: { keyPrefix: "myapp-" } }),
|
|
24
|
+
"home-screen",
|
|
25
|
+
"view"
|
|
26
|
+
)
|
|
27
|
+
).toBe("myapp-plugin:home-screen:view");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("reads and writes localStorage under the namespaced key", () => {
|
|
31
|
+
const storage = createPluginStorageFactory(() => config())("pre-chat");
|
|
32
|
+
storage.set("identity", "ada");
|
|
33
|
+
|
|
34
|
+
expect(window.localStorage.getItem("persona-plugin:pre-chat:identity")).toBe(
|
|
35
|
+
"ada"
|
|
36
|
+
);
|
|
37
|
+
expect(storage.get("identity")).toBe("ada");
|
|
38
|
+
|
|
39
|
+
storage.remove("identity");
|
|
40
|
+
expect(storage.get("identity")).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("keeps two plugins from colliding on the same key", () => {
|
|
44
|
+
const factory = createPluginStorageFactory(() => config());
|
|
45
|
+
factory("a").set("view", "home");
|
|
46
|
+
factory("b").set("view", "chat");
|
|
47
|
+
|
|
48
|
+
expect(factory("a").get("view")).toBe("home");
|
|
49
|
+
expect(factory("b").get("view")).toBe("chat");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("downgrades to an in-memory map under persistState: false", () => {
|
|
53
|
+
const storage = createPluginStorageFactory(() =>
|
|
54
|
+
config({ persistState: false })
|
|
55
|
+
)("pre-chat");
|
|
56
|
+
storage.set("identity", "ada");
|
|
57
|
+
|
|
58
|
+
expect(storage.get("identity")).toBe("ada");
|
|
59
|
+
expect(window.localStorage.getItem("persona-plugin:pre-chat:identity")).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("falls back to memory when localStorage access throws (private mode)", () => {
|
|
63
|
+
const setItem = vi
|
|
64
|
+
.spyOn(Storage.prototype, "setItem")
|
|
65
|
+
.mockImplementation(() => {
|
|
66
|
+
throw new Error("blocked");
|
|
67
|
+
});
|
|
68
|
+
const getItem = vi
|
|
69
|
+
.spyOn(Storage.prototype, "getItem")
|
|
70
|
+
.mockImplementation(() => {
|
|
71
|
+
throw new Error("blocked");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const storage = createPluginStorageFactory(() => config())("home-screen");
|
|
75
|
+
expect(() => storage.set("view", "home")).not.toThrow();
|
|
76
|
+
expect(storage.get("view")).toBe("home");
|
|
77
|
+
|
|
78
|
+
setItem.mockRestore();
|
|
79
|
+
getItem.mockRestore();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("follows a live keyPrefix change through controller.update()", () => {
|
|
83
|
+
let current = config();
|
|
84
|
+
const storage = createPluginStorageFactory(() => current)("home-screen");
|
|
85
|
+
storage.set("view", "home");
|
|
86
|
+
|
|
87
|
+
current = config({ persistState: { keyPrefix: "next-" } });
|
|
88
|
+
expect(storage.get("view")).toBeNull();
|
|
89
|
+
storage.set("view", "chat");
|
|
90
|
+
expect(window.localStorage.getItem("next-plugin:home-screen:view")).toBe(
|
|
91
|
+
"chat"
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { AgentWidgetConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Synchronous key-value store handed to plugin render hooks. Backed by
|
|
5
|
+
* `localStorage`, namespaced per plugin id. The async `storageAdapter` cannot
|
|
6
|
+
* back a synchronous API and `persistState` is not a general key-value surface.
|
|
7
|
+
*/
|
|
8
|
+
export interface AgentWidgetPluginStorage {
|
|
9
|
+
get: (key: string) => string | null;
|
|
10
|
+
set: (key: string, value: string) => void;
|
|
11
|
+
remove: (key: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** One factory per widget instance; the in-memory fallback is shared by it. */
|
|
15
|
+
export type PluginStorageFactory = (
|
|
16
|
+
pluginId: string
|
|
17
|
+
) => AgentWidgetPluginStorage;
|
|
18
|
+
|
|
19
|
+
const resolveKeyPrefix = (config: AgentWidgetConfig | undefined): string => {
|
|
20
|
+
const persist = config?.persistState;
|
|
21
|
+
return (
|
|
22
|
+
(typeof persist === "object" ? persist?.keyPrefix : undefined) ?? "persona-"
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const pluginStorageKey = (
|
|
27
|
+
config: AgentWidgetConfig | undefined,
|
|
28
|
+
pluginId: string,
|
|
29
|
+
key: string
|
|
30
|
+
): string => `${resolveKeyPrefix(config)}plugin:${pluginId}:${key}`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `persistState: false` and blocked storage both downgrade to a per-instance
|
|
34
|
+
* Map. localStorage access can throw (Safari private mode, partitioned
|
|
35
|
+
* iframes), so the first throw switches this instance to the Map for good.
|
|
36
|
+
*/
|
|
37
|
+
export const createPluginStorageFactory = (
|
|
38
|
+
getConfig: () => AgentWidgetConfig | undefined
|
|
39
|
+
): PluginStorageFactory => {
|
|
40
|
+
const memory = new Map<string, string>();
|
|
41
|
+
let blocked = false;
|
|
42
|
+
|
|
43
|
+
const store = (): Storage | null => {
|
|
44
|
+
if (blocked) return null;
|
|
45
|
+
if (getConfig()?.persistState === false) return null;
|
|
46
|
+
try {
|
|
47
|
+
if (typeof window === "undefined" || !window.localStorage) return null;
|
|
48
|
+
return window.localStorage;
|
|
49
|
+
} catch {
|
|
50
|
+
blocked = true;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (pluginId: string): AgentWidgetPluginStorage => {
|
|
56
|
+
const keyFor = (key: string) =>
|
|
57
|
+
pluginStorageKey(getConfig(), pluginId, key);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
get: (key) => {
|
|
61
|
+
const full = keyFor(key);
|
|
62
|
+
const local = store();
|
|
63
|
+
if (local) {
|
|
64
|
+
try {
|
|
65
|
+
return local.getItem(full);
|
|
66
|
+
} catch {
|
|
67
|
+
blocked = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return memory.get(full) ?? null;
|
|
71
|
+
},
|
|
72
|
+
set: (key, value) => {
|
|
73
|
+
const full = keyFor(key);
|
|
74
|
+
const local = store();
|
|
75
|
+
if (local) {
|
|
76
|
+
try {
|
|
77
|
+
local.setItem(full, value);
|
|
78
|
+
return;
|
|
79
|
+
} catch {
|
|
80
|
+
blocked = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
memory.set(full, value);
|
|
84
|
+
},
|
|
85
|
+
remove: (key) => {
|
|
86
|
+
const full = keyFor(key);
|
|
87
|
+
memory.delete(full);
|
|
88
|
+
const local = store();
|
|
89
|
+
if (!local) return;
|
|
90
|
+
try {
|
|
91
|
+
local.removeItem(full);
|
|
92
|
+
} catch {
|
|
93
|
+
blocked = true;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
};
|
package/src/utils/theme.test.ts
CHANGED
|
@@ -137,6 +137,36 @@ describe('theme utils', () => {
|
|
|
137
137
|
expect(cssVars['--persona-button-radius']).toBe('6px');
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
it('zeroes horizontal intro-card padding when the card resolves flat', () => {
|
|
141
|
+
const cssVars = themeToCssVariables(createTheme({} as any));
|
|
142
|
+
expect(cssVars['--persona-intro-card-padding']).toBe('1.5rem 0');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('keeps full intro-card padding when a background or shadow makes it a card', () => {
|
|
146
|
+
const withBackground = themeToCssVariables(
|
|
147
|
+
createTheme({
|
|
148
|
+
components: { introCard: { background: '#ffffff' } },
|
|
149
|
+
} as any)
|
|
150
|
+
);
|
|
151
|
+
expect(withBackground['--persona-intro-card-padding']).toBe('1.5rem');
|
|
152
|
+
|
|
153
|
+
const withShadow = themeToCssVariables(
|
|
154
|
+
createTheme({
|
|
155
|
+
components: { introCard: { shadow: '0 1px 2px rgba(0,0,0,0.1)' } },
|
|
156
|
+
} as any)
|
|
157
|
+
);
|
|
158
|
+
expect(withShadow['--persona-intro-card-padding']).toBe('1.5rem');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('lets an explicit intro-card padding token win over the flat default', () => {
|
|
162
|
+
const cssVars = themeToCssVariables(
|
|
163
|
+
createTheme({
|
|
164
|
+
components: { introCard: { padding: '2rem' } },
|
|
165
|
+
} as any)
|
|
166
|
+
);
|
|
167
|
+
expect(cssVars['--persona-intro-card-padding']).toBe('2rem');
|
|
168
|
+
});
|
|
169
|
+
|
|
140
170
|
it.each([
|
|
141
171
|
{
|
|
142
172
|
role: 'surface',
|
|
@@ -287,6 +317,61 @@ describe('theme utils', () => {
|
|
|
287
317
|
expect(customVars['--persona-header-action-icon-fg']).toBe('#9ca3af');
|
|
288
318
|
});
|
|
289
319
|
|
|
320
|
+
it('emits full-path CSS variables for header and welcome text style tokens', () => {
|
|
321
|
+
const theme = createTheme({
|
|
322
|
+
components: {
|
|
323
|
+
introCard: {
|
|
324
|
+
title: { fontFamily: 'Georgia, serif', fontSize: '1.5rem' },
|
|
325
|
+
},
|
|
326
|
+
header: {
|
|
327
|
+
title: {
|
|
328
|
+
fontFamily: 'Inter, sans-serif',
|
|
329
|
+
color: 'palette.colors.secondary.500',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
} as any);
|
|
334
|
+
const cssVars = themeToCssVariables(theme);
|
|
335
|
+
|
|
336
|
+
expect(cssVars['--persona-components-introCard-title-fontFamily']).toBe(
|
|
337
|
+
'Georgia, serif'
|
|
338
|
+
);
|
|
339
|
+
expect(cssVars['--persona-components-introCard-title-fontSize']).toBe('1.5rem');
|
|
340
|
+
expect(cssVars['--persona-components-header-title-fontFamily']).toBe(
|
|
341
|
+
'Inter, sans-serif'
|
|
342
|
+
);
|
|
343
|
+
// title.color supersedes the legacy titleForeground alias.
|
|
344
|
+
expect(cssVars['--persona-header-title-fg']).toBe('#8b5cf6');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('keeps titleForeground working when no header.title.color is set', () => {
|
|
348
|
+
const cssVars = themeToCssVariables(
|
|
349
|
+
createTheme({
|
|
350
|
+
components: {
|
|
351
|
+
header: { titleForeground: 'palette.colors.gray.500' },
|
|
352
|
+
},
|
|
353
|
+
} as any)
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
expect(cssVars['--persona-header-title-fg']).toBe('#6b7280');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('emits suggestion itemGap for every variant, defaulting to 8px', () => {
|
|
360
|
+
const defaults = themeToCssVariables(createTheme());
|
|
361
|
+
expect(defaults['--persona-components-suggestion-chip-itemGap']).toBe('8px');
|
|
362
|
+
expect(defaults['--persona-components-suggestion-card-itemGap']).toBe('8px');
|
|
363
|
+
expect(defaults['--persona-components-suggestion-list-itemGap']).toBe('8px');
|
|
364
|
+
|
|
365
|
+
const custom = themeToCssVariables(
|
|
366
|
+
createTheme({
|
|
367
|
+
components: { suggestion: { list: { itemGap: '20px' } } },
|
|
368
|
+
} as any)
|
|
369
|
+
);
|
|
370
|
+
expect(custom['--persona-components-suggestion-list-itemGap']).toBe('20px');
|
|
371
|
+
// Sibling variants keep the default.
|
|
372
|
+
expect(custom['--persona-components-suggestion-chip-itemGap']).toBe('8px');
|
|
373
|
+
});
|
|
374
|
+
|
|
290
375
|
it('maps button.ghost tokens to the composer ghost icon-button CSS variables', () => {
|
|
291
376
|
const cssVars = themeToCssVariables(createTheme());
|
|
292
377
|
|
|
@@ -574,4 +659,56 @@ describe('theme utils', () => {
|
|
|
574
659
|
const cssVars = themeToCssVariables(theme);
|
|
575
660
|
expect(cssVars['--persona-code-bg']).toBe('#fafafa');
|
|
576
661
|
});
|
|
662
|
+
|
|
663
|
+
it('derives the shared scrollbar variables from components.scrollbar with border fallback', () => {
|
|
664
|
+
const defaults = themeToCssVariables(createTheme({} as any));
|
|
665
|
+
expect(defaults['--persona-scrollbar-thumb']).toBe(defaults['--persona-border']);
|
|
666
|
+
expect(defaults['--persona-scrollbar-track']).toBe('transparent');
|
|
667
|
+
|
|
668
|
+
const themed = themeToCssVariables(
|
|
669
|
+
createTheme({
|
|
670
|
+
components: { scrollbar: { thumb: '#00dfc1', track: '#111111' } },
|
|
671
|
+
} as any)
|
|
672
|
+
);
|
|
673
|
+
expect(themed['--persona-scrollbar-thumb']).toBe('#00dfc1');
|
|
674
|
+
expect(themed['--persona-scrollbar-track']).toBe('#111111');
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('exposes intro card border and composer border color tokens', () => {
|
|
678
|
+
const defaults = themeToCssVariables(createTheme({} as any));
|
|
679
|
+
expect(defaults['--persona-intro-card-border']).toBe('none');
|
|
680
|
+
expect(defaults['--persona-composer-border-color']).toBe(defaults['--persona-border']);
|
|
681
|
+
|
|
682
|
+
const themed = themeToCssVariables(
|
|
683
|
+
createTheme({
|
|
684
|
+
components: {
|
|
685
|
+
introCard: { border: '1px solid rgba(0, 0, 0, 0.1)' },
|
|
686
|
+
composer: { borderColor: 'rgba(29, 28, 23, 0.25)' },
|
|
687
|
+
},
|
|
688
|
+
} as any)
|
|
689
|
+
);
|
|
690
|
+
expect(themed['--persona-intro-card-border']).toBe('1px solid rgba(0, 0, 0, 0.1)');
|
|
691
|
+
expect(themed['--persona-composer-border-color']).toBe('rgba(29, 28, 23, 0.25)');
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('follows the palette radius for markdown code blocks so square themes get square code', () => {
|
|
695
|
+
const defaults = themeToCssVariables(createTheme({} as any));
|
|
696
|
+
expect(defaults['--persona-md-code-block-border-radius']).toBe('0.375rem');
|
|
697
|
+
|
|
698
|
+
const square = themeToCssVariables(
|
|
699
|
+
createTheme({
|
|
700
|
+
palette: {
|
|
701
|
+
radius: { sm: '0px', md: '0px', lg: '0px', xl: '0px', '2xl': '0px' },
|
|
702
|
+
},
|
|
703
|
+
} as any)
|
|
704
|
+
);
|
|
705
|
+
expect(square['--persona-md-code-block-border-radius']).toBe('0px');
|
|
706
|
+
|
|
707
|
+
const explicit = themeToCssVariables(
|
|
708
|
+
createTheme({
|
|
709
|
+
components: { markdown: { codeBlock: { borderRadius: '2px' } } },
|
|
710
|
+
} as any)
|
|
711
|
+
);
|
|
712
|
+
expect(explicit['--persona-md-code-block-border-radius']).toBe('2px');
|
|
713
|
+
});
|
|
577
714
|
});
|
package/src/utils/tokens.ts
CHANGED
|
@@ -344,6 +344,7 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
344
344
|
borderRadius: 'palette.radius.full',
|
|
345
345
|
padding: '0.5rem 0.875rem',
|
|
346
346
|
gap: '0.5rem',
|
|
347
|
+
itemGap: '8px',
|
|
347
348
|
minHeight: '36px',
|
|
348
349
|
fontSize: '0.8125rem',
|
|
349
350
|
lineHeight: '1.25',
|
|
@@ -362,6 +363,7 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
362
363
|
padding: '1rem',
|
|
363
364
|
shadow: 'palette.shadows.sm',
|
|
364
365
|
gap: '0.625rem',
|
|
366
|
+
itemGap: '8px',
|
|
365
367
|
minHeight: '72px',
|
|
366
368
|
fontSize: '0.875rem',
|
|
367
369
|
lineHeight: '1.35',
|
|
@@ -379,6 +381,7 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
379
381
|
borderRadius: 'palette.radius.md',
|
|
380
382
|
padding: '0.75rem',
|
|
381
383
|
gap: '0.625rem',
|
|
384
|
+
itemGap: '8px',
|
|
382
385
|
minHeight: '44px',
|
|
383
386
|
fontSize: '0.875rem',
|
|
384
387
|
lineHeight: '1.35',
|
|
@@ -399,6 +402,10 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
399
402
|
composer: {
|
|
400
403
|
shadow: 'palette.shadows.none',
|
|
401
404
|
},
|
|
405
|
+
scrollbar: {
|
|
406
|
+
thumb: 'semantic.colors.border',
|
|
407
|
+
track: 'transparent',
|
|
408
|
+
},
|
|
402
409
|
markdown: {
|
|
403
410
|
inlineCode: {
|
|
404
411
|
background: 'palette.colors.gray.50',
|
|
@@ -415,6 +422,7 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
415
422
|
background: 'semantic.colors.container',
|
|
416
423
|
borderColor: 'semantic.colors.border',
|
|
417
424
|
textColor: 'inherit',
|
|
425
|
+
borderRadius: 'palette.radius.md',
|
|
418
426
|
},
|
|
419
427
|
table: {
|
|
420
428
|
headerBackground: 'semantic.colors.container',
|
|
@@ -851,10 +859,15 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
851
859
|
cssVars['--persona-components-header-iconBackground'] ?? cssVars['--persona-primary'];
|
|
852
860
|
cssVars['--persona-header-icon-fg'] =
|
|
853
861
|
cssVars['--persona-components-header-iconForeground'] ?? cssVars['--persona-text-inverse'];
|
|
862
|
+
// `title.color` / `subtitle.color` supersede the legacy *Foreground tokens.
|
|
854
863
|
cssVars['--persona-header-title-fg'] =
|
|
855
|
-
cssVars['--persona-components-header-
|
|
864
|
+
cssVars['--persona-components-header-title-color'] ??
|
|
865
|
+
cssVars['--persona-components-header-titleForeground'] ??
|
|
866
|
+
cssVars['--persona-primary'];
|
|
856
867
|
cssVars['--persona-header-subtitle-fg'] =
|
|
857
|
-
cssVars['--persona-components-header-
|
|
868
|
+
cssVars['--persona-components-header-subtitle-color'] ??
|
|
869
|
+
cssVars['--persona-components-header-subtitleForeground'] ??
|
|
870
|
+
cssVars['--persona-text-muted'];
|
|
858
871
|
cssVars['--persona-header-action-icon-fg'] =
|
|
859
872
|
cssVars['--persona-components-header-actionIconForeground'] ?? cssVars['--persona-muted'];
|
|
860
873
|
|
|
@@ -870,10 +883,25 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
870
883
|
cssVars['--persona-components-introCard-background'] ?? 'transparent';
|
|
871
884
|
cssVars['--persona-intro-card-radius'] =
|
|
872
885
|
cssVars['--persona-components-introCard-borderRadius'] ?? '1rem';
|
|
873
|
-
|
|
886
|
+
// Flat cards (transparent background, no shadow) drop the horizontal
|
|
887
|
+
// component of the stock padding so the welcome text shares the content
|
|
888
|
+
// column's left edge instead of carrying an invisible card inset. Compared
|
|
889
|
+
// by resolved value, not key presence: DEFAULT_COMPONENTS materializes the
|
|
890
|
+
// introCard tokens into every theme. A non-stock padding is emitted as-is;
|
|
891
|
+
// '1.5rem 1.5rem' forces the symmetric inset on a flat card.
|
|
892
|
+
const introBg = cssVars['--persona-components-introCard-background'];
|
|
893
|
+
const introShadow = cssVars['--persona-components-introCard-shadow'];
|
|
894
|
+
const introCardFlat =
|
|
895
|
+
(!introBg || introBg === 'transparent' || introBg === 'none') &&
|
|
896
|
+
(!introShadow || introShadow === 'none');
|
|
897
|
+
const introPadding =
|
|
874
898
|
cssVars['--persona-components-introCard-padding'] ?? '1.5rem';
|
|
899
|
+
cssVars['--persona-intro-card-padding'] =
|
|
900
|
+
introCardFlat && introPadding === '1.5rem' ? '1.5rem 0' : introPadding;
|
|
875
901
|
cssVars['--persona-intro-card-shadow'] =
|
|
876
902
|
cssVars['--persona-components-introCard-shadow'] ?? 'none';
|
|
903
|
+
cssVars['--persona-intro-card-border'] =
|
|
904
|
+
cssVars['--persona-components-introCard-border'] ?? 'none';
|
|
877
905
|
|
|
878
906
|
cssVars['--persona-input-background'] =
|
|
879
907
|
cssVars['--persona-components-input-background'] ?? cssVars['--persona-surface'];
|
|
@@ -947,6 +975,18 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
947
975
|
cssVars['--persona-components-composer-fontSize'] ?? '0.875rem';
|
|
948
976
|
cssVars['--persona-composer-line-height'] =
|
|
949
977
|
cssVars['--persona-components-composer-lineHeight'] ?? '1.25rem';
|
|
978
|
+
cssVars['--persona-composer-border-color'] =
|
|
979
|
+
cssVars['--persona-components-composer-borderColor'] ??
|
|
980
|
+
cssVars['--persona-border'] ??
|
|
981
|
+
'#e5e7eb';
|
|
982
|
+
|
|
983
|
+
// Scrollbars (every scroller in the widget consumes these two)
|
|
984
|
+
cssVars['--persona-scrollbar-thumb'] =
|
|
985
|
+
cssVars['--persona-components-scrollbar-thumb'] ??
|
|
986
|
+
cssVars['--persona-border'] ??
|
|
987
|
+
'#e5e7eb';
|
|
988
|
+
cssVars['--persona-scrollbar-track'] =
|
|
989
|
+
cssVars['--persona-components-scrollbar-track'] ?? 'transparent';
|
|
950
990
|
|
|
951
991
|
cssVars['--persona-md-inline-code-bg'] =
|
|
952
992
|
cssVars['--persona-components-markdown-inlineCode-background'] ?? cssVars['--persona-container'];
|
|
@@ -979,6 +1019,12 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
979
1019
|
cssVars['--persona-components-markdown-codeBlock-borderColor'] ?? cssVars['--persona-border'];
|
|
980
1020
|
cssVars['--persona-md-code-block-text-color'] =
|
|
981
1021
|
cssVars['--persona-components-markdown-codeBlock-textColor'] ?? 'inherit';
|
|
1022
|
+
// Follows the palette radius so square-corner themes get square code blocks;
|
|
1023
|
+
// the 0.375rem fallback matches palette.radius.md's default.
|
|
1024
|
+
cssVars['--persona-md-code-block-border-radius'] =
|
|
1025
|
+
cssVars['--persona-components-markdown-codeBlock-borderRadius'] ??
|
|
1026
|
+
cssVars['--persona-radius-md'] ??
|
|
1027
|
+
'0.375rem';
|
|
982
1028
|
|
|
983
1029
|
// Markdown table
|
|
984
1030
|
cssVars['--persona-md-table-header-bg'] =
|