@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.
- package/README.md +140 -8
- package/dist/index.cjs +90 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1055 -24
- package/dist/index.d.ts +1055 -24
- package/dist/index.global.js +111 -60
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +90 -39
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/widget.css +836 -513
- package/package.json +1 -1
- package/src/artifacts-session.test.ts +80 -0
- package/src/client.test.ts +20 -21
- package/src/client.ts +153 -4
- package/src/components/approval-bubble.ts +45 -42
- package/src/components/artifact-card.ts +91 -0
- package/src/components/artifact-pane.ts +501 -0
- package/src/components/composer-builder.ts +32 -27
- package/src/components/event-stream-view.ts +40 -40
- package/src/components/feedback.ts +36 -36
- package/src/components/forms.ts +11 -11
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +55 -36
- package/src/components/header-layouts.ts +58 -125
- package/src/components/launcher.ts +36 -21
- package/src/components/message-bubble.ts +92 -65
- package/src/components/messages.ts +2 -2
- package/src/components/panel.ts +42 -11
- package/src/components/reasoning-bubble.ts +23 -23
- package/src/components/registry.ts +4 -0
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +32 -32
- package/src/defaults.ts +30 -4
- package/src/index.ts +80 -2
- package/src/install.ts +22 -0
- package/src/plugins/types.ts +23 -0
- package/src/postprocessors.ts +2 -2
- package/src/runtime/host-layout.ts +174 -0
- package/src/runtime/init.test.ts +236 -0
- package/src/runtime/init.ts +114 -55
- package/src/session.ts +135 -2
- package/src/styles/tailwind.css +1 -1
- package/src/styles/widget.css +836 -513
- package/src/types/theme.ts +354 -0
- package/src/types.ts +314 -15
- package/src/ui.docked.test.ts +104 -0
- package/src/ui.ts +940 -227
- package/src/utils/artifact-gate.test.ts +255 -0
- package/src/utils/artifact-gate.ts +142 -0
- package/src/utils/artifact-resize.test.ts +64 -0
- package/src/utils/artifact-resize.ts +67 -0
- package/src/utils/attachment-manager.ts +10 -10
- package/src/utils/code-generators.test.ts +52 -0
- package/src/utils/code-generators.ts +40 -36
- package/src/utils/dock.ts +17 -0
- package/src/utils/dom-context.test.ts +504 -0
- package/src/utils/dom-context.ts +896 -0
- package/src/utils/dom.ts +12 -1
- package/src/utils/message-fingerprint.test.ts +187 -0
- package/src/utils/message-fingerprint.ts +105 -0
- package/src/utils/migration.ts +179 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/plugins.ts +175 -0
- package/src/utils/positioning.ts +4 -4
- package/src/utils/theme.test.ts +125 -0
- package/src/utils/theme.ts +216 -60
- 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
|
-
"
|
|
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
|
-
"
|
|
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(--
|
|
295
|
-
filePreview.style.border = "1px solid var(--
|
|
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(--
|
|
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(--
|
|
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
|
-
"
|
|
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
|
}
|