@runtypelabs/persona 1.48.0 → 2.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.
Files changed (69) hide show
  1. package/README.md +140 -8
  2. package/dist/index.cjs +90 -39
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1055 -24
  5. package/dist/index.d.ts +1055 -24
  6. package/dist/index.global.js +111 -60
  7. package/dist/index.global.js.map +1 -1
  8. package/dist/index.js +90 -39
  9. package/dist/index.js.map +1 -1
  10. package/dist/install.global.js +1 -1
  11. package/dist/install.global.js.map +1 -1
  12. package/dist/widget.css +836 -513
  13. package/package.json +1 -1
  14. package/src/artifacts-session.test.ts +80 -0
  15. package/src/client.test.ts +20 -21
  16. package/src/client.ts +153 -4
  17. package/src/components/approval-bubble.ts +45 -42
  18. package/src/components/artifact-card.ts +91 -0
  19. package/src/components/artifact-pane.ts +501 -0
  20. package/src/components/composer-builder.ts +32 -27
  21. package/src/components/event-stream-view.ts +40 -40
  22. package/src/components/feedback.ts +36 -36
  23. package/src/components/forms.ts +11 -11
  24. package/src/components/header-builder.test.ts +32 -0
  25. package/src/components/header-builder.ts +55 -36
  26. package/src/components/header-layouts.ts +58 -125
  27. package/src/components/launcher.ts +36 -21
  28. package/src/components/message-bubble.ts +92 -65
  29. package/src/components/messages.ts +2 -2
  30. package/src/components/panel.ts +42 -11
  31. package/src/components/reasoning-bubble.ts +23 -23
  32. package/src/components/registry.ts +4 -0
  33. package/src/components/suggestions.ts +1 -1
  34. package/src/components/tool-bubble.ts +32 -32
  35. package/src/defaults.ts +30 -4
  36. package/src/index.ts +80 -2
  37. package/src/install.ts +22 -0
  38. package/src/plugins/types.ts +23 -0
  39. package/src/postprocessors.ts +2 -2
  40. package/src/runtime/host-layout.ts +174 -0
  41. package/src/runtime/init.test.ts +236 -0
  42. package/src/runtime/init.ts +114 -55
  43. package/src/session.ts +135 -2
  44. package/src/styles/tailwind.css +1 -1
  45. package/src/styles/widget.css +836 -513
  46. package/src/types/theme.ts +354 -0
  47. package/src/types.ts +314 -15
  48. package/src/ui.docked.test.ts +104 -0
  49. package/src/ui.ts +940 -227
  50. package/src/utils/artifact-gate.test.ts +255 -0
  51. package/src/utils/artifact-gate.ts +142 -0
  52. package/src/utils/artifact-resize.test.ts +64 -0
  53. package/src/utils/artifact-resize.ts +67 -0
  54. package/src/utils/attachment-manager.ts +10 -10
  55. package/src/utils/code-generators.test.ts +52 -0
  56. package/src/utils/code-generators.ts +40 -36
  57. package/src/utils/dock.ts +17 -0
  58. package/src/utils/dom-context.test.ts +504 -0
  59. package/src/utils/dom-context.ts +896 -0
  60. package/src/utils/dom.ts +12 -1
  61. package/src/utils/message-fingerprint.test.ts +187 -0
  62. package/src/utils/message-fingerprint.ts +105 -0
  63. package/src/utils/migration.ts +179 -0
  64. package/src/utils/morph.ts +1 -1
  65. package/src/utils/plugins.ts +175 -0
  66. package/src/utils/positioning.ts +4 -4
  67. package/src/utils/theme.test.ts +125 -0
  68. package/src/utils/theme.ts +216 -60
  69. package/src/utils/tokens.ts +682 -0
@@ -0,0 +1,255 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ applyArtifactLayoutCssVars,
4
+ applyArtifactPaneAppearance,
5
+ applyArtifactPaneBorderTheme,
6
+ artifactsSidebarEnabled,
7
+ } from "./artifact-gate";
8
+ import type { AgentWidgetConfig } from "../types";
9
+
10
+ function baseConfig(overrides: Partial<AgentWidgetConfig> = {}): AgentWidgetConfig {
11
+ return {
12
+ apiUrl: "/api",
13
+ ...overrides,
14
+ } as AgentWidgetConfig;
15
+ }
16
+
17
+ /** Minimal DOM shim (widget vitest uses `environment: 'node'`). */
18
+ function createFakeMount(): HTMLElement {
19
+ const classes = new Set<string>();
20
+ const cssProps: Record<string, string> = {};
21
+ return {
22
+ classList: {
23
+ remove(...names: string[]) {
24
+ for (const n of names) {
25
+ for (const part of n.split(/\s+/).filter(Boolean)) {
26
+ classes.delete(part);
27
+ }
28
+ }
29
+ },
30
+ add(name: string) {
31
+ classes.add(name);
32
+ },
33
+ contains(name: string) {
34
+ return classes.has(name);
35
+ },
36
+ },
37
+ style: {
38
+ setProperty(name: string, value: string) {
39
+ cssProps[name] = value;
40
+ },
41
+ removeProperty(name: string) {
42
+ delete cssProps[name];
43
+ },
44
+ getPropertyValue(name: string) {
45
+ return cssProps[name] ?? "";
46
+ },
47
+ },
48
+ } as unknown as HTMLElement;
49
+ }
50
+
51
+ describe("applyArtifactPaneAppearance", () => {
52
+ it("clears classes and radius when artifacts disabled", () => {
53
+ const mount = createFakeMount();
54
+ mount.classList.add("persona-artifact-appearance-panel");
55
+ mount.style.setProperty("--persona-artifact-pane-radius", "99px");
56
+ applyArtifactPaneAppearance(
57
+ mount,
58
+ baseConfig({ features: { artifacts: { enabled: false } } })
59
+ );
60
+ expect(mount.classList.contains("persona-artifact-appearance-panel")).toBe(false);
61
+ expect(mount.style.getPropertyValue("--persona-artifact-pane-radius")).toBe("");
62
+ });
63
+
64
+ it("adds panel class by default when artifacts enabled", () => {
65
+ const mount = createFakeMount();
66
+ applyArtifactPaneAppearance(
67
+ mount,
68
+ baseConfig({ features: { artifacts: { enabled: true } } })
69
+ );
70
+ expect(mount.classList.contains("persona-artifact-appearance-panel")).toBe(true);
71
+ expect(mount.style.getPropertyValue("--persona-artifact-pane-radius")).toBe("");
72
+ expect(mount.classList.contains("persona-artifact-unified-split")).toBe(false);
73
+ });
74
+
75
+ it("adds unified split class and optional outer radius", () => {
76
+ const mount = createFakeMount();
77
+ applyArtifactPaneAppearance(
78
+ mount,
79
+ baseConfig({
80
+ features: {
81
+ artifacts: { enabled: true, layout: { unifiedSplitChrome: true, unifiedSplitOuterRadius: "12px" } },
82
+ },
83
+ })
84
+ );
85
+ expect(mount.classList.contains("persona-artifact-unified-split")).toBe(true);
86
+ expect(mount.style.getPropertyValue("--persona-artifact-unified-outer-radius").trim()).toBe("12px");
87
+ });
88
+
89
+ it("adds seamless class and border radius on any mode", () => {
90
+ const ext = createFakeMount();
91
+ applyArtifactPaneAppearance(
92
+ ext,
93
+ baseConfig({
94
+ features: { artifacts: { enabled: true, layout: { paneAppearance: "seamless" } } },
95
+ })
96
+ );
97
+ expect(ext.classList.contains("persona-artifact-appearance-seamless")).toBe(true);
98
+
99
+ const rounded = createFakeMount();
100
+ applyArtifactPaneAppearance(
101
+ rounded,
102
+ baseConfig({
103
+ features: {
104
+ artifacts: { enabled: true, layout: { paneAppearance: "panel", paneBorderRadius: "1rem" } },
105
+ },
106
+ })
107
+ );
108
+ expect(rounded.classList.contains("persona-artifact-appearance-panel")).toBe(true);
109
+ expect(rounded.style.getPropertyValue("--persona-artifact-pane-radius").trim()).toBe("1rem");
110
+ });
111
+
112
+ it("sets paneShadow CSS var when provided", () => {
113
+ const mount = createFakeMount();
114
+ applyArtifactPaneAppearance(
115
+ mount,
116
+ baseConfig({
117
+ features: {
118
+ artifacts: { enabled: true, layout: { paneShadow: "none" } },
119
+ },
120
+ })
121
+ );
122
+ expect(mount.style.getPropertyValue("--persona-artifact-pane-shadow").trim()).toBe("none");
123
+ });
124
+
125
+ it("falls back to panel for invalid paneAppearance", () => {
126
+ const mount = createFakeMount();
127
+ applyArtifactPaneAppearance(
128
+ mount,
129
+ baseConfig({
130
+ features: {
131
+ artifacts: { enabled: true, layout: { paneAppearance: "nope" as never } },
132
+ },
133
+ })
134
+ );
135
+ expect(mount.classList.contains("persona-artifact-appearance-panel")).toBe(true);
136
+ });
137
+ });
138
+
139
+ describe("applyArtifactPaneBorderTheme", () => {
140
+ it("clears when artifacts disabled", () => {
141
+ const mount = createFakeMount();
142
+ mount.classList.add("persona-artifact-border-full");
143
+ mount.style.setProperty("--persona-artifact-pane-border", "2px solid red");
144
+ applyArtifactPaneBorderTheme(
145
+ mount,
146
+ baseConfig({ features: { artifacts: { enabled: false } } })
147
+ );
148
+ expect(mount.classList.contains("persona-artifact-border-full")).toBe(false);
149
+ expect(mount.style.getPropertyValue("--persona-artifact-pane-border")).toBe("");
150
+ });
151
+
152
+ it("prefers paneBorder over paneBorderLeft", () => {
153
+ const mount = createFakeMount();
154
+ applyArtifactPaneBorderTheme(
155
+ mount,
156
+ baseConfig({
157
+ features: {
158
+ artifacts: {
159
+ enabled: true,
160
+ layout: { paneBorder: "1px solid #ccc", paneBorderLeft: "4px solid blue" },
161
+ },
162
+ },
163
+ })
164
+ );
165
+ expect(mount.classList.contains("persona-artifact-border-full")).toBe(true);
166
+ expect(mount.classList.contains("persona-artifact-border-left")).toBe(false);
167
+ expect(mount.style.getPropertyValue("--persona-artifact-pane-border").trim()).toBe("1px solid #ccc");
168
+ });
169
+
170
+ it("sets border-left only when paneBorderLeft alone", () => {
171
+ const mount = createFakeMount();
172
+ applyArtifactPaneBorderTheme(
173
+ mount,
174
+ baseConfig({
175
+ features: { artifacts: { enabled: true, layout: { paneBorderLeft: "1px solid #cccccc" } } },
176
+ })
177
+ );
178
+ expect(mount.classList.contains("persona-artifact-border-left")).toBe(true);
179
+ expect(mount.style.getPropertyValue("--persona-artifact-pane-border-left").trim()).toBe(
180
+ "1px solid #cccccc"
181
+ );
182
+ });
183
+ });
184
+
185
+ describe("artifactsSidebarEnabled", () => {
186
+ it("is true only when enabled flag is true", () => {
187
+ expect(artifactsSidebarEnabled(undefined)).toBe(false);
188
+ expect(artifactsSidebarEnabled(baseConfig())).toBe(false);
189
+ expect(
190
+ artifactsSidebarEnabled(baseConfig({ features: { artifacts: { enabled: true } } }))
191
+ ).toBe(true);
192
+ });
193
+ });
194
+
195
+ describe("applyArtifactLayoutCssVars", () => {
196
+ it("sets pane background and padding CSS vars when provided", () => {
197
+ const mount = createFakeMount();
198
+ applyArtifactLayoutCssVars(
199
+ mount,
200
+ baseConfig({
201
+ features: {
202
+ artifacts: {
203
+ enabled: true,
204
+ layout: {
205
+ paneBackground: "#212121",
206
+ panePadding: "24px",
207
+ },
208
+ },
209
+ },
210
+ })
211
+ );
212
+ expect(mount.style.getPropertyValue("--persona-artifact-pane-bg").trim()).toBe("#212121");
213
+ expect(mount.style.getPropertyValue("--persona-artifact-pane-padding").trim()).toBe("24px");
214
+ });
215
+
216
+ it("clears pane bg and padding when artifacts disabled", () => {
217
+ const mount = createFakeMount();
218
+ mount.style.setProperty("--persona-artifact-pane-bg", "#000");
219
+ mount.style.setProperty("--persona-artifact-pane-padding", "8px");
220
+ applyArtifactLayoutCssVars(
221
+ mount,
222
+ baseConfig({ features: { artifacts: { enabled: false } } })
223
+ );
224
+ expect(mount.style.getPropertyValue("--persona-artifact-pane-bg")).toBe("");
225
+ expect(mount.style.getPropertyValue("--persona-artifact-pane-padding")).toBe("");
226
+ });
227
+
228
+ it("sets document toolbar layout CSS vars when provided", () => {
229
+ const mount = createFakeMount();
230
+ applyArtifactLayoutCssVars(
231
+ mount,
232
+ baseConfig({
233
+ features: {
234
+ artifacts: {
235
+ enabled: true,
236
+ layout: {
237
+ documentToolbarIconColor: "#60a5fa",
238
+ documentToolbarToggleActiveBackground: "#262626",
239
+ documentToolbarToggleActiveBorderColor: "#444444",
240
+ },
241
+ },
242
+ },
243
+ })
244
+ );
245
+ expect(mount.style.getPropertyValue("--persona-artifact-doc-toolbar-icon-color").trim()).toBe(
246
+ "#60a5fa"
247
+ );
248
+ expect(mount.style.getPropertyValue("--persona-artifact-doc-toggle-active-bg").trim()).toBe(
249
+ "#262626"
250
+ );
251
+ expect(mount.style.getPropertyValue("--persona-artifact-doc-toggle-active-border").trim()).toBe(
252
+ "#444444"
253
+ );
254
+ });
255
+ });
@@ -0,0 +1,142 @@
1
+ import type { AgentWidgetConfig, PersonaArtifactKind } from "../types";
2
+
3
+ export function artifactsSidebarEnabled(config: AgentWidgetConfig | undefined): boolean {
4
+ return config?.features?.artifacts?.enabled === true;
5
+ }
6
+
7
+ export function artifactKindAllowedByFeature(
8
+ config: AgentWidgetConfig | undefined,
9
+ kind: PersonaArtifactKind
10
+ ): boolean {
11
+ const allowed = config?.features?.artifacts?.allowedTypes;
12
+ if (!allowed?.length) return true;
13
+ return allowed.includes(kind);
14
+ }
15
+
16
+ /** Optional custom border on artifact pane via CSS vars + root classes. */
17
+ export function applyArtifactPaneBorderTheme(mount: HTMLElement, config: AgentWidgetConfig): void {
18
+ mount.classList.remove("persona-artifact-border-full", "persona-artifact-border-left");
19
+ mount.style.removeProperty("--persona-artifact-pane-border");
20
+ mount.style.removeProperty("--persona-artifact-pane-border-left");
21
+ if (!artifactsSidebarEnabled(config)) return;
22
+
23
+ const l = config.features?.artifacts?.layout;
24
+ const full = l?.paneBorder?.trim();
25
+ const left = l?.paneBorderLeft?.trim();
26
+ if (full) {
27
+ mount.classList.add("persona-artifact-border-full");
28
+ mount.style.setProperty("--persona-artifact-pane-border", full);
29
+ } else if (left) {
30
+ mount.classList.add("persona-artifact-border-left");
31
+ mount.style.setProperty("--persona-artifact-pane-border-left", left);
32
+ }
33
+ }
34
+
35
+ /** Set CSS variables on #persona-root for artifact split/pane sizing. Clears when artifacts disabled. */
36
+ function clearDocumentToolbarLayoutVars(mount: HTMLElement): void {
37
+ mount.style.removeProperty("--persona-artifact-doc-toolbar-icon-color");
38
+ mount.style.removeProperty("--persona-artifact-doc-toggle-active-bg");
39
+ mount.style.removeProperty("--persona-artifact-doc-toggle-active-border");
40
+ }
41
+
42
+ export function applyArtifactLayoutCssVars(mount: HTMLElement, config: AgentWidgetConfig): void {
43
+ if (!artifactsSidebarEnabled(config)) {
44
+ mount.style.removeProperty("--persona-artifact-split-gap");
45
+ mount.style.removeProperty("--persona-artifact-pane-width");
46
+ mount.style.removeProperty("--persona-artifact-pane-max-width");
47
+ mount.style.removeProperty("--persona-artifact-pane-min-width");
48
+ mount.style.removeProperty("--persona-artifact-pane-bg");
49
+ mount.style.removeProperty("--persona-artifact-pane-padding");
50
+ clearDocumentToolbarLayoutVars(mount);
51
+ applyArtifactPaneBorderTheme(mount, config);
52
+ return;
53
+ }
54
+ const l = config.features?.artifacts?.layout;
55
+ mount.style.setProperty("--persona-artifact-split-gap", l?.splitGap ?? "0.5rem");
56
+ mount.style.setProperty("--persona-artifact-pane-width", l?.paneWidth ?? "40%");
57
+ mount.style.setProperty("--persona-artifact-pane-max-width", l?.paneMaxWidth ?? "28rem");
58
+ if (l?.paneMinWidth) {
59
+ mount.style.setProperty("--persona-artifact-pane-min-width", l.paneMinWidth);
60
+ } else {
61
+ mount.style.removeProperty("--persona-artifact-pane-min-width");
62
+ }
63
+ const paneBg = l?.paneBackground?.trim();
64
+ if (paneBg) {
65
+ mount.style.setProperty("--persona-artifact-pane-bg", paneBg);
66
+ } else {
67
+ mount.style.removeProperty("--persona-artifact-pane-bg");
68
+ }
69
+ const panePad = l?.panePadding?.trim();
70
+ if (panePad) {
71
+ mount.style.setProperty("--persona-artifact-pane-padding", panePad);
72
+ } else {
73
+ mount.style.removeProperty("--persona-artifact-pane-padding");
74
+ }
75
+
76
+ const iconColor = l?.documentToolbarIconColor?.trim();
77
+ if (iconColor) {
78
+ mount.style.setProperty("--persona-artifact-doc-toolbar-icon-color", iconColor);
79
+ } else {
80
+ mount.style.removeProperty("--persona-artifact-doc-toolbar-icon-color");
81
+ }
82
+ const toggleBg = l?.documentToolbarToggleActiveBackground?.trim();
83
+ if (toggleBg) {
84
+ mount.style.setProperty("--persona-artifact-doc-toggle-active-bg", toggleBg);
85
+ } else {
86
+ mount.style.removeProperty("--persona-artifact-doc-toggle-active-bg");
87
+ }
88
+ const toggleBorder = l?.documentToolbarToggleActiveBorderColor?.trim();
89
+ if (toggleBorder) {
90
+ mount.style.setProperty("--persona-artifact-doc-toggle-active-border", toggleBorder);
91
+ } else {
92
+ mount.style.removeProperty("--persona-artifact-doc-toggle-active-border");
93
+ }
94
+
95
+ applyArtifactPaneBorderTheme(mount, config);
96
+ }
97
+
98
+ const ARTIFACT_APPEARANCE_MODES = ["panel", "seamless"] as const;
99
+
100
+ /** Toggle root classes for artifact pane appearance, radius, shadow, and unified chrome. */
101
+ export function applyArtifactPaneAppearance(mount: HTMLElement, config: AgentWidgetConfig): void {
102
+ for (const m of ARTIFACT_APPEARANCE_MODES) {
103
+ mount.classList.remove(`persona-artifact-appearance-${m}`);
104
+ }
105
+ mount.classList.remove("persona-artifact-unified-split");
106
+ mount.style.removeProperty("--persona-artifact-pane-radius");
107
+ mount.style.removeProperty("--persona-artifact-pane-shadow");
108
+ mount.style.removeProperty("--persona-artifact-unified-outer-radius");
109
+ if (!artifactsSidebarEnabled(config)) return;
110
+
111
+ const layout = config.features?.artifacts?.layout;
112
+ const raw = layout?.paneAppearance ?? "panel";
113
+ const mode = (ARTIFACT_APPEARANCE_MODES as readonly string[]).includes(raw) ? raw : "panel";
114
+ mount.classList.add(`persona-artifact-appearance-${mode}`);
115
+
116
+ const radius = layout?.paneBorderRadius?.trim();
117
+ if (radius) {
118
+ mount.style.setProperty("--persona-artifact-pane-radius", radius);
119
+ }
120
+
121
+ const shadow = layout?.paneShadow?.trim();
122
+ if (shadow) {
123
+ mount.style.setProperty("--persona-artifact-pane-shadow", shadow);
124
+ }
125
+
126
+ if (layout?.unifiedSplitChrome === true) {
127
+ mount.classList.add("persona-artifact-unified-split");
128
+ const outer = layout.unifiedSplitOuterRadius?.trim() || radius;
129
+ if (outer) {
130
+ mount.style.setProperty("--persona-artifact-unified-outer-radius", outer);
131
+ }
132
+ }
133
+ }
134
+
135
+ /** Widen floating panel when artifacts show (default true); `false` opts out. */
136
+ export function shouldExpandLauncherForArtifacts(
137
+ config: AgentWidgetConfig,
138
+ launcherEnabled: boolean
139
+ ): boolean {
140
+ if (!launcherEnabled || !artifactsSidebarEnabled(config)) return false;
141
+ return config.features?.artifacts?.layout?.expandLauncherPanelWhenOpen !== false;
142
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ ARTIFACT_RESIZE_CHAT_MIN_PX,
4
+ clampArtifactPaneWidth,
5
+ maxArtifactWidthFromSplit,
6
+ parseArtifactResizeMaxPxOptional,
7
+ parseArtifactResizePx,
8
+ resolveArtifactPaneWidthPx,
9
+ } from "./artifact-resize";
10
+
11
+ describe("parseArtifactResizePx", () => {
12
+ it("parses px strings", () => {
13
+ expect(parseArtifactResizePx("240px", 99)).toBe(240);
14
+ expect(parseArtifactResizePx(" 12.5px ", 99)).toBe(12.5);
15
+ });
16
+ it("falls back on invalid or empty", () => {
17
+ expect(parseArtifactResizePx(undefined, 200)).toBe(200);
18
+ expect(parseArtifactResizePx("2rem", 200)).toBe(200);
19
+ expect(parseArtifactResizePx("50%", 200)).toBe(200);
20
+ });
21
+ });
22
+
23
+ describe("parseArtifactResizeMaxPxOptional", () => {
24
+ it("returns null when unset or invalid", () => {
25
+ expect(parseArtifactResizeMaxPxOptional(undefined)).toBe(null);
26
+ expect(parseArtifactResizeMaxPxOptional("40rem")).toBe(null);
27
+ });
28
+ it("parses px", () => {
29
+ expect(parseArtifactResizeMaxPxOptional("400px")).toBe(400);
30
+ });
31
+ });
32
+
33
+ describe("clampArtifactPaneWidth", () => {
34
+ it("clamps to range", () => {
35
+ expect(clampArtifactPaneWidth(100, 200, 400)).toBe(200);
36
+ expect(clampArtifactPaneWidth(500, 200, 400)).toBe(400);
37
+ expect(clampArtifactPaneWidth(300, 200, 400)).toBe(300);
38
+ });
39
+ it("returns min when max below min", () => {
40
+ expect(clampArtifactPaneWidth(300, 200, 100)).toBe(200);
41
+ });
42
+ });
43
+
44
+ describe("maxArtifactWidthFromSplit", () => {
45
+ it("subtracts chat min, gaps, and handle", () => {
46
+ const split = 800;
47
+ const gap = 8;
48
+ const handle = 6;
49
+ const chatMin = ARTIFACT_RESIZE_CHAT_MIN_PX;
50
+ expect(maxArtifactWidthFromSplit(split, gap, handle, chatMin)).toBe(800 - 200 - 16 - 6);
51
+ });
52
+ });
53
+
54
+ describe("resolveArtifactPaneWidthPx", () => {
55
+ it("applies config min and layout max", () => {
56
+ const w = resolveArtifactPaneWidthPx(500, 800, 8, 6, "250px", undefined);
57
+ expect(w).toBeGreaterThanOrEqual(250);
58
+ expect(w).toBeLessThanOrEqual(800 - 200 - 16 - 6);
59
+ });
60
+ it("respects optional max cap", () => {
61
+ const w = resolveArtifactPaneWidthPx(900, 1200, 8, 6, "200px", "320px");
62
+ expect(w).toBeLessThanOrEqual(320);
63
+ });
64
+ });
@@ -0,0 +1,67 @@
1
+ /** Minimum width (px) reserved for the chat column while resizing the artifact pane. */
2
+ export const ARTIFACT_RESIZE_CHAT_MIN_PX = 200;
3
+
4
+ /** Default minimum width (px) for the artifact column when `resizableMinWidth` is unset. */
5
+ export const ARTIFACT_RESIZE_PANE_MIN_DEFAULT_PX = 200;
6
+
7
+ /** Parse a `NNpx` string; returns `fallback` if missing or invalid. */
8
+ export function parseArtifactResizePx(input: string | undefined, fallback: number): number {
9
+ if (!input?.trim()) return fallback;
10
+ const m = /^(\d+(?:\.\d+)?)px\s*$/i.exec(input.trim());
11
+ if (!m) return fallback;
12
+ return Math.max(0, Number(m[1]));
13
+ }
14
+
15
+ /** Optional max from config: only valid `px` strings apply; otherwise no extra cap. */
16
+ export function parseArtifactResizeMaxPxOptional(input: string | undefined): number | null {
17
+ if (!input?.trim()) return null;
18
+ const m = /^(\d+(?:\.\d+)?)px\s*$/i.exec(input.trim());
19
+ if (!m) return null;
20
+ return Math.max(0, Number(m[1]));
21
+ }
22
+
23
+ export function clampArtifactPaneWidth(widthPx: number, minPx: number, maxPx: number): number {
24
+ if (maxPx < minPx) return minPx;
25
+ return Math.min(maxPx, Math.max(minPx, widthPx));
26
+ }
27
+
28
+ /**
29
+ * Upper bound for artifact width (px) from split row geometry: leave room for chat min, two flex gaps, handle.
30
+ */
31
+ export function maxArtifactWidthFromSplit(
32
+ splitWidthPx: number,
33
+ gapPx: number,
34
+ handleWidthPx: number,
35
+ chatMinPx: number
36
+ ): number {
37
+ const raw = splitWidthPx - chatMinPx - 2 * gapPx - handleWidthPx;
38
+ return Math.max(0, raw);
39
+ }
40
+
41
+ /** Read the first gap value from computed `gap` (e.g. `8px` or `8px 8px`). */
42
+ export function readFlexGapPx(splitRoot: HTMLElement, win: Window): number {
43
+ const g = win.getComputedStyle(splitRoot).gap || "0px";
44
+ const first = g.trim().split(/\s+/)[0] ?? "0px";
45
+ const m = /^([\d.]+)px$/i.exec(first);
46
+ if (m) return Number(m[1]);
47
+ const m2 = /^([\d.]+)/.exec(first);
48
+ return m2 ? Number(m2[1]) : 8;
49
+ }
50
+
51
+ export function resolveArtifactPaneWidthPx(
52
+ candidatePx: number,
53
+ splitWidthPx: number,
54
+ gapPx: number,
55
+ handleWidthPx: number,
56
+ resizableMinWidth?: string,
57
+ resizableMaxWidth?: string
58
+ ): number {
59
+ const minPx = parseArtifactResizePx(resizableMinWidth, ARTIFACT_RESIZE_PANE_MIN_DEFAULT_PX);
60
+ let maxPx = maxArtifactWidthFromSplit(splitWidthPx, gapPx, handleWidthPx, ARTIFACT_RESIZE_CHAT_MIN_PX);
61
+ maxPx = Math.max(minPx, maxPx);
62
+ const cap = parseArtifactResizeMaxPxOptional(resizableMaxWidth);
63
+ if (cap !== null) {
64
+ maxPx = Math.min(maxPx, cap);
65
+ }
66
+ return clampArtifactPaneWidth(candidatePx, minPx, maxPx);
67
+ }
@@ -267,7 +267,7 @@ export class AttachmentManager {
267
267
 
268
268
  const previewWrapper = createElement(
269
269
  "div",
270
- "tvw-attachment-preview tvw-relative tvw-inline-block"
270
+ "persona-attachment-preview persona-relative persona-inline-block"
271
271
  );
272
272
  previewWrapper.setAttribute("data-attachment-id", attachment.id);
273
273
  previewWrapper.style.width = "48px";
@@ -279,7 +279,7 @@ export class AttachmentManager {
279
279
  img.src = attachment.previewUrl;
280
280
  img.alt = attachment.file.name;
281
281
  img.className =
282
- "tvw-w-full tvw-h-full tvw-object-cover tvw-rounded-lg tvw-border tvw-border-gray-200";
282
+ "persona-w-full persona-h-full persona-object-cover persona-rounded-lg persona-border persona-border-gray-200";
283
283
  img.style.width = "48px";
284
284
  img.style.height = "48px";
285
285
  img.style.objectFit = "cover";
@@ -291,8 +291,8 @@ export class AttachmentManager {
291
291
  filePreview.style.width = "48px";
292
292
  filePreview.style.height = "48px";
293
293
  filePreview.style.borderRadius = "8px";
294
- filePreview.style.backgroundColor = "var(--cw-container, #f3f4f6)";
295
- filePreview.style.border = "1px solid var(--cw-border, #e5e7eb)";
294
+ filePreview.style.backgroundColor = "var(--persona-container, #f3f4f6)";
295
+ filePreview.style.border = "1px solid var(--persona-border, #e5e7eb)";
296
296
  filePreview.style.display = "flex";
297
297
  filePreview.style.flexDirection = "column";
298
298
  filePreview.style.alignItems = "center";
@@ -302,7 +302,7 @@ export class AttachmentManager {
302
302
 
303
303
  // File icon
304
304
  const iconName = getFileIconName(attachment.file.type);
305
- const fileIcon = renderLucideIcon(iconName, 20, "var(--cw-muted, #6b7280)", 1.5);
305
+ const fileIcon = renderLucideIcon(iconName, 20, "var(--persona-muted, #6b7280)", 1.5);
306
306
  if (fileIcon) {
307
307
  filePreview.appendChild(fileIcon);
308
308
  }
@@ -312,7 +312,7 @@ export class AttachmentManager {
312
312
  typeLabel.textContent = getFileTypeName(attachment.file.type, attachment.file.name);
313
313
  typeLabel.style.fontSize = "8px";
314
314
  typeLabel.style.fontWeight = "600";
315
- typeLabel.style.color = "var(--cw-muted, #6b7280)";
315
+ typeLabel.style.color = "var(--persona-muted, #6b7280)";
316
316
  typeLabel.style.textTransform = "uppercase";
317
317
  typeLabel.style.lineHeight = "1";
318
318
  filePreview.appendChild(typeLabel);
@@ -323,7 +323,7 @@ export class AttachmentManager {
323
323
  // Create remove button
324
324
  const removeBtn = createElement(
325
325
  "button",
326
- "tvw-attachment-remove tvw-absolute tvw-flex tvw-items-center tvw-justify-center"
326
+ "persona-attachment-remove persona-absolute persona-flex persona-items-center persona-justify-center"
327
327
  ) as HTMLButtonElement;
328
328
  removeBtn.type = "button";
329
329
  removeBtn.setAttribute("aria-label", "Remove attachment");
@@ -333,7 +333,7 @@ export class AttachmentManager {
333
333
  removeBtn.style.width = "18px";
334
334
  removeBtn.style.height = "18px";
335
335
  removeBtn.style.borderRadius = "50%";
336
- removeBtn.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
336
+ removeBtn.style.backgroundColor = "var(--persona-palette-colors-black-alpha-60, rgba(0, 0, 0, 0.6))";
337
337
  removeBtn.style.border = "none";
338
338
  removeBtn.style.cursor = "pointer";
339
339
  removeBtn.style.display = "flex";
@@ -342,12 +342,12 @@ export class AttachmentManager {
342
342
  removeBtn.style.padding = "0";
343
343
 
344
344
  // Add X icon
345
- const xIcon = renderLucideIcon("x", 10, "#ffffff", 2);
345
+ const xIcon = renderLucideIcon("x", 10, "var(--persona-text-inverse, #ffffff)", 2);
346
346
  if (xIcon) {
347
347
  removeBtn.appendChild(xIcon);
348
348
  } else {
349
349
  removeBtn.textContent = "×";
350
- removeBtn.style.color = "#ffffff";
350
+ removeBtn.style.color = "var(--persona-text-inverse, #ffffff)";
351
351
  removeBtn.style.fontSize = "14px";
352
352
  removeBtn.style.lineHeight = "1";
353
353
  }