@runtypelabs/persona 4.10.0 → 4.11.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/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-4ROVJ1gA.d.cts → types-DveIaNx6.d.cts} +11 -316
- package/dist/animations/{types-4ROVJ1gA.d.ts → types-DveIaNx6.d.ts} +11 -316
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-MMUPR2JW.js +1 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/{context-mentions-7S5KVUTG.js → context-mentions-ONG7A7M6.js} +1 -1
- package/dist/context-mentions.d.cts +35 -316
- package/dist/context-mentions.d.ts +35 -316
- package/dist/index.cjs +67 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -317
- package/dist/index.d.ts +40 -317
- package/dist/index.global.js +55 -55
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +78 -78
- package/dist/index.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +35 -316
- package/dist/smart-dom-reader.d.ts +35 -316
- package/dist/theme-editor-preview.cjs +66 -66
- package/dist/theme-editor-preview.d.cts +35 -316
- package/dist/theme-editor-preview.d.ts +35 -316
- package/dist/theme-editor-preview.js +55 -55
- package/dist/theme-editor.cjs +5 -5
- package/dist/theme-editor.d.cts +35 -316
- package/dist/theme-editor.d.ts +35 -316
- package/dist/theme-editor.js +9 -9
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/components/approval-bubble.ts +0 -1
- package/src/components/composer-parts.ts +19 -19
- package/src/components/context-mention-button.test.ts +5 -1
- package/src/components/context-mention-button.ts +6 -4
- package/src/components/header-builder.test.ts +3 -3
- package/src/components/header-parts.ts +14 -108
- package/src/components/message-bubble.test.ts +49 -0
- package/src/components/message-bubble.ts +8 -7
- package/src/components/reasoning-bubble.ts +0 -2
- package/src/components/tool-bubble.ts +0 -2
- package/src/generated/runtype-openapi-contract.ts +11 -317
- package/src/index-core.ts +7 -1
- package/src/runtime/host-layout.test.ts +94 -0
- package/src/runtime/host-layout.ts +9 -1
- package/src/runtime/init.test.ts +33 -0
- package/src/runtime/init.ts +4 -0
- package/src/styles/widget.css +118 -58
- package/src/theme-editor/sections.test.ts +33 -0
- package/src/theme-editor/sections.ts +10 -1
- package/src/types.ts +25 -0
- package/src/ui.attachments-drop.test.ts +2 -1
- package/src/ui.message-width.test.ts +407 -0
- package/src/ui.mount-fill-min-width.test.ts +81 -0
- package/src/ui.ts +158 -238
- package/src/utils/table-scroll-fade.test.ts +123 -0
- package/src/utils/table-scroll-fade.ts +75 -0
- package/src/utils/tooltip.test.ts +226 -0
- package/src/utils/tooltip.ts +245 -0
- package/dist/chunk-IPVK3KOM.js +0 -1
|
@@ -4,6 +4,51 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
4
4
|
|
|
5
5
|
import { createWidgetHostLayout } from "./host-layout";
|
|
6
6
|
|
|
7
|
+
describe("createWidgetHostLayout direct", () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
document.body.innerHTML = "";
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("makes the host shrinkable when the launcher is disabled", () => {
|
|
13
|
+
const target = document.createElement("div");
|
|
14
|
+
document.body.appendChild(target);
|
|
15
|
+
|
|
16
|
+
const layout = createWidgetHostLayout(target, { launcher: { enabled: false } });
|
|
17
|
+
// Flex items floor at min-width:auto; the host must stay shrinkable so a wide
|
|
18
|
+
// artifact split shrinks within the mount rather than enlarging the host.
|
|
19
|
+
expect(layout.host.style.minWidth).toBe("0px");
|
|
20
|
+
expect(layout.host.style.flex).toBe("1 1 auto");
|
|
21
|
+
|
|
22
|
+
layout.destroy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("leaves the host min-width unset when the launcher is enabled", () => {
|
|
26
|
+
const target = document.createElement("div");
|
|
27
|
+
document.body.appendChild(target);
|
|
28
|
+
|
|
29
|
+
const layout = createWidgetHostLayout(target, { launcher: { enabled: true } });
|
|
30
|
+
expect(layout.host.style.minWidth).toBe("");
|
|
31
|
+
|
|
32
|
+
layout.destroy();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("restores/clears the shrinkable host baseline when the launcher toggles at runtime", () => {
|
|
36
|
+
const target = document.createElement("div");
|
|
37
|
+
document.body.appendChild(target);
|
|
38
|
+
|
|
39
|
+
const layout = createWidgetHostLayout(target, { launcher: { enabled: false } });
|
|
40
|
+
expect(layout.host.style.minWidth).toBe("0px");
|
|
41
|
+
|
|
42
|
+
layout.updateConfig({ launcher: { enabled: true } });
|
|
43
|
+
expect(layout.host.style.minWidth).toBe("");
|
|
44
|
+
|
|
45
|
+
layout.updateConfig({ launcher: { enabled: false } });
|
|
46
|
+
expect(layout.host.style.minWidth).toBe("0px");
|
|
47
|
+
|
|
48
|
+
layout.destroy();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
7
52
|
describe("createWidgetHostLayout docked", () => {
|
|
8
53
|
afterEach(() => {
|
|
9
54
|
document.body.innerHTML = "";
|
|
@@ -39,6 +84,55 @@ describe("createWidgetHostLayout docked", () => {
|
|
|
39
84
|
layout.destroy();
|
|
40
85
|
});
|
|
41
86
|
|
|
87
|
+
it("keeps a shrinkable host baseline for non-emerge dock modes but pins it for emerge", () => {
|
|
88
|
+
for (const reveal of ["resize", "overlay", "push"] as const) {
|
|
89
|
+
const parent = document.createElement("div");
|
|
90
|
+
document.body.appendChild(parent);
|
|
91
|
+
const target = document.createElement("div");
|
|
92
|
+
parent.appendChild(target);
|
|
93
|
+
|
|
94
|
+
const layout = createWidgetHostLayout(target, {
|
|
95
|
+
launcher: { mountMode: "docked", autoExpand: false, dock: { width: "320px", reveal } },
|
|
96
|
+
});
|
|
97
|
+
// The docked host must shrink within its mount so a wide artifact split
|
|
98
|
+
// stays contained instead of enlarging the host.
|
|
99
|
+
expect(layout.host.style.minWidth, reveal).toBe("0px");
|
|
100
|
+
|
|
101
|
+
layout.destroy();
|
|
102
|
+
document.body.innerHTML = "";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const parent = document.createElement("div");
|
|
106
|
+
document.body.appendChild(parent);
|
|
107
|
+
const target = document.createElement("div");
|
|
108
|
+
parent.appendChild(target);
|
|
109
|
+
const layout = createWidgetHostLayout(target, {
|
|
110
|
+
launcher: { mountMode: "docked", autoExpand: true, dock: { width: "320px", reveal: "emerge" } },
|
|
111
|
+
});
|
|
112
|
+
// Emerge intentionally pins the host to its fixed width, overriding the baseline.
|
|
113
|
+
expect(layout.host.style.minWidth).toBe("320px");
|
|
114
|
+
layout.destroy();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("restores the shrinkable host baseline when switching away from emerge", () => {
|
|
118
|
+
const parent = document.createElement("div");
|
|
119
|
+
document.body.appendChild(parent);
|
|
120
|
+
const target = document.createElement("div");
|
|
121
|
+
parent.appendChild(target);
|
|
122
|
+
|
|
123
|
+
const layout = createWidgetHostLayout(target, {
|
|
124
|
+
launcher: { mountMode: "docked", autoExpand: true, dock: { width: "320px", reveal: "emerge" } },
|
|
125
|
+
});
|
|
126
|
+
expect(layout.host.style.minWidth).toBe("320px");
|
|
127
|
+
|
|
128
|
+
layout.updateConfig({
|
|
129
|
+
launcher: { mountMode: "docked", autoExpand: true, dock: { width: "320px", reveal: "resize" } },
|
|
130
|
+
});
|
|
131
|
+
expect(layout.host.style.minWidth).toBe("0px");
|
|
132
|
+
|
|
133
|
+
layout.destroy();
|
|
134
|
+
});
|
|
135
|
+
|
|
42
136
|
it("disables dock width transition when dock.animate is false", () => {
|
|
43
137
|
const parent = document.createElement("div");
|
|
44
138
|
document.body.appendChild(parent);
|
|
@@ -119,6 +119,9 @@ const setDirectHostStyles = (host: HTMLElement, config?: AgentWidgetConfig): voi
|
|
|
119
119
|
host.style.flexDirection = launcherEnabled ? "" : "column";
|
|
120
120
|
host.style.flex = launcherEnabled ? "" : "1 1 auto";
|
|
121
121
|
host.style.minHeight = launcherEnabled ? "" : "0";
|
|
122
|
+
// Flex items floor at min-width:auto (content width); without this the host
|
|
123
|
+
// grows past a shrinkable mount when a wide artifact split opens inside it.
|
|
124
|
+
host.style.minWidth = launcherEnabled ? "" : "0";
|
|
122
125
|
};
|
|
123
126
|
|
|
124
127
|
const clearOverlayDockSlotStyles = (dockSlot: HTMLElement): void => {
|
|
@@ -170,7 +173,9 @@ const resetContentSlotFlexSizing = (contentSlot: HTMLElement): void => {
|
|
|
170
173
|
|
|
171
174
|
const clearEmergeDockStyles = (host: HTMLElement, dockSlot: HTMLElement): void => {
|
|
172
175
|
host.style.width = "";
|
|
173
|
-
|
|
176
|
+
// Restore the docked shrinkable baseline, not "": non-emerge dock modes must
|
|
177
|
+
// stay shrinkable. Emerge re-pins minWidth to its fixed width after this.
|
|
178
|
+
host.style.minWidth = "0";
|
|
174
179
|
host.style.maxWidth = "";
|
|
175
180
|
host.style.boxSizing = "";
|
|
176
181
|
dockSlot.style.alignItems = "";
|
|
@@ -298,6 +303,9 @@ const applyDockStyles = (
|
|
|
298
303
|
host.className = "persona-host";
|
|
299
304
|
host.style.height = "100%";
|
|
300
305
|
host.style.minHeight = "0";
|
|
306
|
+
// Shrinkable baseline so a wide artifact split shrinks within the dock host
|
|
307
|
+
// instead of enlarging it. Emerge overwrites this with its fixed width below.
|
|
308
|
+
host.style.minWidth = "0";
|
|
301
309
|
host.style.display = "flex";
|
|
302
310
|
host.style.flexDirection = "column";
|
|
303
311
|
host.style.flex = "1 1 auto";
|
package/src/runtime/init.test.ts
CHANGED
|
@@ -92,6 +92,39 @@ describe("initAgentWidget windowKey and ready notifications", () => {
|
|
|
92
92
|
createAgentExperienceMock.mockImplementation((_mount, config) => createMockController(config));
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
it("makes the mount shrinkable when it fills the host (launcher disabled)", async () => {
|
|
96
|
+
const { initAgentWidget } = await import("./init");
|
|
97
|
+
document.body.innerHTML = `<div id="target"></div>`;
|
|
98
|
+
|
|
99
|
+
const handle = initAgentWidget({
|
|
100
|
+
target: "#target",
|
|
101
|
+
config: { launcher: { enabled: false } },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const mount = createAgentExperienceMock.mock.calls[0][0] as HTMLElement;
|
|
105
|
+
// shouldFillHost mounts must match the host's shrinkable baseline, else a
|
|
106
|
+
// wide artifact split forces the mount past its content-based minimum width.
|
|
107
|
+
expect(mount.style.minWidth).toBe("0px");
|
|
108
|
+
expect(mount.style.minHeight).toBe("0px");
|
|
109
|
+
|
|
110
|
+
handle.destroy();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("leaves the mount min-width unset when the launcher is enabled (floating)", async () => {
|
|
114
|
+
const { initAgentWidget } = await import("./init");
|
|
115
|
+
document.body.innerHTML = `<div id="target"></div>`;
|
|
116
|
+
|
|
117
|
+
const handle = initAgentWidget({
|
|
118
|
+
target: "#target",
|
|
119
|
+
config: { launcher: { enabled: true } },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const mount = createAgentExperienceMock.mock.calls[0][0] as HTMLElement;
|
|
123
|
+
expect(mount.style.minWidth).toBe("");
|
|
124
|
+
|
|
125
|
+
handle.destroy();
|
|
126
|
+
});
|
|
127
|
+
|
|
95
128
|
it("assigns the handle to window[windowKey] when windowKey is provided", async () => {
|
|
96
129
|
const { initAgentWidget } = await import("./init");
|
|
97
130
|
document.body.innerHTML = `<div id="target"></div>`;
|
package/src/runtime/init.ts
CHANGED
|
@@ -63,12 +63,16 @@ export const initAgentWidget = (
|
|
|
63
63
|
const mount = ownerDocument.createElement("div");
|
|
64
64
|
mount.setAttribute("data-persona-root", "true");
|
|
65
65
|
|
|
66
|
+
// Initial fill styling only. On every relayout the controller's
|
|
67
|
+
// applyFullHeightStyles resets mount.style.cssText and re-applies these
|
|
68
|
+
// (including the shrinkable min-width:0), so it owns the runtime truth.
|
|
66
69
|
if (shouldFillHost) {
|
|
67
70
|
mount.style.height = "100%";
|
|
68
71
|
mount.style.display = "flex";
|
|
69
72
|
mount.style.flexDirection = "column";
|
|
70
73
|
mount.style.flex = "1";
|
|
71
74
|
mount.style.minHeight = "0";
|
|
75
|
+
mount.style.minWidth = "0";
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
if (useShadow) {
|
package/src/styles/widget.css
CHANGED
|
@@ -163,6 +163,8 @@
|
|
|
163
163
|
--persona-md-table-header-weight: 600;
|
|
164
164
|
--persona-md-table-cell-padding: 0.5rem 0.75rem;
|
|
165
165
|
--persona-md-table-border-radius: 0.375rem;
|
|
166
|
+
/* Width of the horizontal-scroll edge fade on wide tables; set to 0 to disable. */
|
|
167
|
+
--persona-md-table-scroll-fade: 24px;
|
|
166
168
|
|
|
167
169
|
/* Markdown Horizontal Rule Variables */
|
|
168
170
|
--persona-md-hr-color: var(--persona-divider, #e5e7eb);
|
|
@@ -643,6 +645,47 @@
|
|
|
643
645
|
max-width: 85%;
|
|
644
646
|
}
|
|
645
647
|
|
|
648
|
+
/* Message row geometry is role-configurable. The row always spans the
|
|
649
|
+
transcript; its direct content track either shrink-wraps or fills that row.
|
|
650
|
+
`--persona-message-row-max-width` is resolved per role in ui.ts. */
|
|
651
|
+
.persona-message-row {
|
|
652
|
+
width: 100%;
|
|
653
|
+
min-width: 0;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.persona-message-row > * {
|
|
657
|
+
min-width: 0;
|
|
658
|
+
max-width: var(--persona-message-row-max-width, 85%);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.persona-message-row[data-message-width="content"] > * {
|
|
662
|
+
flex: 0 1 auto;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.persona-message-row[data-message-width="full"] > * {
|
|
666
|
+
flex: 1 1 auto;
|
|
667
|
+
width: 100%;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/* Avatars live inside the role-sized content track. The bubble flexes into
|
|
671
|
+
the space left after the fixed avatar + gap instead of making a full row
|
|
672
|
+
overflow by the avatar width. */
|
|
673
|
+
.persona-message-with-avatar {
|
|
674
|
+
min-width: 0;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.persona-message-with-avatar > .persona-message-bubble {
|
|
678
|
+
min-width: 0;
|
|
679
|
+
max-width: 100%;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.persona-message-row[data-message-width="full"]
|
|
683
|
+
> .persona-message-with-avatar
|
|
684
|
+
> .persona-message-bubble {
|
|
685
|
+
flex: 1 1 auto;
|
|
686
|
+
width: auto;
|
|
687
|
+
}
|
|
688
|
+
|
|
646
689
|
.persona-transition {
|
|
647
690
|
transition: transform 160ms ease, background-color 160ms ease,
|
|
648
691
|
box-shadow 160ms ease, opacity 160ms ease;
|
|
@@ -952,7 +995,8 @@
|
|
|
952
995
|
cursor: pointer;
|
|
953
996
|
}
|
|
954
997
|
|
|
955
|
-
/*
|
|
998
|
+
/* Shared icon-control tooltip. It is portaled to the document body (or the
|
|
999
|
+
widget ShadowRoot) and positioned in JS so panel overflow never clips it. */
|
|
956
1000
|
.persona-send-button-wrapper {
|
|
957
1001
|
position: relative;
|
|
958
1002
|
display: inline-flex;
|
|
@@ -960,47 +1004,46 @@
|
|
|
960
1004
|
justify-content: center;
|
|
961
1005
|
}
|
|
962
1006
|
|
|
963
|
-
.persona-
|
|
964
|
-
position:
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
transform: translateX(-50%);
|
|
1007
|
+
.persona-control-tooltip {
|
|
1008
|
+
position: fixed;
|
|
1009
|
+
box-sizing: border-box;
|
|
1010
|
+
max-width: min(320px, calc(100vw - 16px));
|
|
968
1011
|
background-color: var(--persona-tooltip-background, #111827);
|
|
969
1012
|
color: var(--persona-tooltip-foreground, #ffffff);
|
|
970
1013
|
padding: 6px 12px;
|
|
971
1014
|
border-radius: var(--persona-radius-sm, 0.125rem);
|
|
972
1015
|
font-size: 12px;
|
|
973
|
-
|
|
974
|
-
|
|
1016
|
+
line-height: 1.35;
|
|
1017
|
+
white-space: normal;
|
|
1018
|
+
overflow-wrap: anywhere;
|
|
1019
|
+
box-shadow: var(--persona-tooltip-shadow, 0 4px 12px rgba(15, 23, 42, 0.18));
|
|
1020
|
+
opacity: 1;
|
|
975
1021
|
pointer-events: none;
|
|
976
|
-
transition: opacity
|
|
977
|
-
z-index: 1000;
|
|
1022
|
+
transition: opacity 120ms ease;
|
|
978
1023
|
}
|
|
979
1024
|
|
|
980
|
-
.persona-
|
|
981
|
-
|
|
1025
|
+
.persona-control-tooltip[data-state="measuring"] {
|
|
1026
|
+
visibility: hidden;
|
|
1027
|
+
opacity: 0;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
.persona-control-tooltip__arrow {
|
|
982
1031
|
position: absolute;
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1032
|
+
left: var(--persona-tooltip-arrow-x, 50%);
|
|
1033
|
+
width: 8px;
|
|
1034
|
+
height: 8px;
|
|
1035
|
+
background: var(--persona-tooltip-background, #111827);
|
|
1036
|
+
transform: translateX(-50%) rotate(45deg);
|
|
988
1037
|
}
|
|
989
1038
|
|
|
990
|
-
.persona-
|
|
991
|
-
|
|
992
|
-
opacity: 1;
|
|
1039
|
+
.persona-control-tooltip[data-placement="top"] .persona-control-tooltip__arrow {
|
|
1040
|
+
top: calc(100% - 4px);
|
|
993
1041
|
}
|
|
994
1042
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
.persona-send-button-wrapper:hover .persona-send-button-tooltip,
|
|
998
|
-
.persona-send-button-wrapper:focus-within .persona-send-button-tooltip {
|
|
999
|
-
opacity: 0;
|
|
1000
|
-
}
|
|
1043
|
+
.persona-control-tooltip[data-placement="bottom"] .persona-control-tooltip__arrow {
|
|
1044
|
+
bottom: calc(100% - 4px);
|
|
1001
1045
|
}
|
|
1002
1046
|
|
|
1003
|
-
/* Clear chat button tooltip */
|
|
1004
1047
|
.persona-clear-chat-button-wrapper {
|
|
1005
1048
|
position: relative;
|
|
1006
1049
|
display: inline-flex;
|
|
@@ -1008,30 +1051,9 @@
|
|
|
1008
1051
|
justify-content: center;
|
|
1009
1052
|
}
|
|
1010
1053
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
padding: 6px 12px;
|
|
1015
|
-
border-radius: var(--persona-radius-sm, 0.125rem);
|
|
1016
|
-
font-size: 12px;
|
|
1017
|
-
white-space: nowrap;
|
|
1018
|
-
pointer-events: none;
|
|
1019
|
-
z-index: 10000;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
.persona-clear-chat-tooltip-arrow {
|
|
1023
|
-
content: "";
|
|
1024
|
-
position: absolute;
|
|
1025
|
-
top: 100%;
|
|
1026
|
-
left: 50%;
|
|
1027
|
-
transform: translateX(-50%);
|
|
1028
|
-
border: 4px solid transparent;
|
|
1029
|
-
border-top-color: var(--persona-tooltip-background, #111827);
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
@media (hover: none), (max-width: 500px) {
|
|
1033
|
-
.persona-clear-chat-tooltip {
|
|
1034
|
-
display: none !important;
|
|
1054
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1055
|
+
.persona-control-tooltip {
|
|
1056
|
+
transition: none;
|
|
1035
1057
|
}
|
|
1036
1058
|
}
|
|
1037
1059
|
|
|
@@ -1501,12 +1523,45 @@
|
|
|
1501
1523
|
flex-grow: 1;
|
|
1502
1524
|
}
|
|
1503
1525
|
|
|
1526
|
+
/* Wide tables scroll inside this wrapper instead of widening the chat column.
|
|
1527
|
+
The table keeps width:100% so it fills when it fits, but auto-grows past the
|
|
1528
|
+
wrapper when its content is wider, and the wrapper scrolls. Injected by
|
|
1529
|
+
wrapScrollableTables() before each morph. */
|
|
1530
|
+
.persona-table-scroll {
|
|
1531
|
+
display: block;
|
|
1532
|
+
max-width: 100%;
|
|
1533
|
+
overflow-x: auto;
|
|
1534
|
+
margin: 0.5rem 0;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/* Edge fades (set by table-scroll-fade.ts) cue that the table scrolls; each
|
|
1538
|
+
side shows only when there is more content that way. The fade is applied
|
|
1539
|
+
instantly, not transitioned: each morph strips and re-applies the fade state,
|
|
1540
|
+
so a transition would animate it from 0 on every streaming chunk (flicker) and
|
|
1541
|
+
once more when the finished paint re-enables it (a single flash). Instant also
|
|
1542
|
+
tracks the scroll position exactly instead of lagging behind the finger. */
|
|
1543
|
+
.persona-table-scroll[data-persona-scroll-x] {
|
|
1544
|
+
-webkit-mask-image: linear-gradient(
|
|
1545
|
+
to right,
|
|
1546
|
+
transparent 0,
|
|
1547
|
+
#000 var(--persona-fade-l, 0px),
|
|
1548
|
+
#000 calc(100% - var(--persona-fade-r, 0px)),
|
|
1549
|
+
transparent 100%
|
|
1550
|
+
);
|
|
1551
|
+
mask-image: linear-gradient(
|
|
1552
|
+
to right,
|
|
1553
|
+
transparent 0,
|
|
1554
|
+
#000 var(--persona-fade-l, 0px),
|
|
1555
|
+
#000 calc(100% - var(--persona-fade-r, 0px)),
|
|
1556
|
+
transparent 100%
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1504
1560
|
.persona-message-bubble table {
|
|
1505
1561
|
width: 100%;
|
|
1506
1562
|
border-collapse: collapse;
|
|
1507
|
-
margin: 0
|
|
1563
|
+
margin: 0;
|
|
1508
1564
|
font-size: 0.875rem;
|
|
1509
|
-
overflow: hidden;
|
|
1510
1565
|
border-radius: var(--persona-md-table-border-radius);
|
|
1511
1566
|
border: 1px solid var(--persona-md-table-border-color);
|
|
1512
1567
|
}
|
|
@@ -1536,13 +1591,18 @@
|
|
|
1536
1591
|
background-color: rgba(0, 0, 0, 0.02);
|
|
1537
1592
|
}
|
|
1538
1593
|
|
|
1539
|
-
/* While a message is still streaming, lock
|
|
1540
|
-
append without
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1594
|
+
/* While a message is still streaming, lock column widths to the header row
|
|
1595
|
+
(table-layout: fixed) so incoming rows append without per-chunk reflow. width
|
|
1596
|
+
auto sizes columns from the header and lets cells wrap, so a table that fits
|
|
1597
|
+
once wrapped fills the column (min-width: 100%) with no scroll, while one too
|
|
1598
|
+
wide even wrapped scrolls: it only shows the scroll fade when it genuinely
|
|
1599
|
+
needs it, matching the finished table rather than forcing every cell onto one
|
|
1600
|
+
line. The class is removed on the final render, settling columns to
|
|
1601
|
+
content-fit widths. overflow-wrap keeps long words from spilling the column. */
|
|
1544
1602
|
.persona-content-streaming table {
|
|
1545
1603
|
table-layout: fixed;
|
|
1604
|
+
width: auto;
|
|
1605
|
+
min-width: 100%;
|
|
1546
1606
|
}
|
|
1547
1607
|
|
|
1548
1608
|
.persona-content-streaming th,
|
|
@@ -4,6 +4,39 @@ import { COMPONENTS_SECTIONS, CONFIGURE_SECTIONS, INTERFACE_ROLES_SECTION, STYLE
|
|
|
4
4
|
import { ALL_ROLES } from "./role-mappings";
|
|
5
5
|
|
|
6
6
|
describe("theme editor scroll-to-bottom controls", () => {
|
|
7
|
+
it("exposes clear style semantics and independent role width controls", () => {
|
|
8
|
+
const section = CONFIGURE_SECTIONS.find((entry) => entry.id === "messages-layout");
|
|
9
|
+
const fieldsByPath = new Map(
|
|
10
|
+
section?.fields.map((field) => [field.path, field]) ?? []
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
expect(fieldsByPath.get("layout.messages.layout")?.options).toEqual([
|
|
14
|
+
{ value: "bubble", label: "Bubble — bubbles for both" },
|
|
15
|
+
{ value: "minimal", label: "Minimal — user bubble, open assistant" },
|
|
16
|
+
{ value: "flat", label: "Flat — open messages for both" },
|
|
17
|
+
]);
|
|
18
|
+
expect(fieldsByPath.has("layout.messages.user.style")).toBe(false);
|
|
19
|
+
expect(fieldsByPath.has("layout.messages.assistant.style")).toBe(false);
|
|
20
|
+
|
|
21
|
+
expect(fieldsByPath.get("layout.messages.user.width")?.options).toEqual([
|
|
22
|
+
{ value: "content", label: "Content" },
|
|
23
|
+
{ value: "full", label: "Full" },
|
|
24
|
+
]);
|
|
25
|
+
expect(fieldsByPath.get("layout.messages.assistant.width")?.options).toEqual([
|
|
26
|
+
{ value: "content", label: "Content" },
|
|
27
|
+
{ value: "full", label: "Full" },
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const userMaxWidth = fieldsByPath.get("layout.messages.user.maxWidth");
|
|
31
|
+
const assistantMaxWidth = fieldsByPath.get(
|
|
32
|
+
"layout.messages.assistant.maxWidth"
|
|
33
|
+
);
|
|
34
|
+
expect(userMaxWidth?.parseValue?.(" 72ch ")).toBe("72ch");
|
|
35
|
+
expect(userMaxWidth?.parseValue?.(" ")).toBeUndefined();
|
|
36
|
+
expect(assistantMaxWidth?.parseValue?.("80%")).toBe("80%");
|
|
37
|
+
expect(assistantMaxWidth?.parseValue?.("")).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
7
40
|
it("exposes scroll-to-bottom config controls", () => {
|
|
8
41
|
const featureSection = CONFIGURE_SECTIONS.find((section) => section.id === "features");
|
|
9
42
|
|
|
@@ -22,6 +22,11 @@ import {
|
|
|
22
22
|
ROLE_BORDERS,
|
|
23
23
|
} from './role-mappings';
|
|
24
24
|
|
|
25
|
+
const parseOptionalCssValue = (value: unknown): string | undefined => {
|
|
26
|
+
const normalized = String(value ?? '').trim();
|
|
27
|
+
return normalized || undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
25
30
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
26
31
|
// STYLE TAB: brand colors, chat colors, typography, shape, etc.
|
|
27
32
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
@@ -601,7 +606,11 @@ const headerLayoutSectionDef: SectionDef = {
|
|
|
601
606
|
const messagesLayoutSectionDef: SectionDef = {
|
|
602
607
|
id: 'messages-layout', title: 'Messages', collapsed: true,
|
|
603
608
|
fields: [
|
|
604
|
-
{ id: 'layout-messages', label: '
|
|
609
|
+
{ id: 'layout-messages', label: 'Message Style', description: 'Minimal uses a user bubble with an open assistant response', type: 'select', path: 'layout.messages.layout', defaultValue: 'bubble', options: [{ value: 'bubble', label: 'Bubble — bubbles for both' }, { value: 'minimal', label: 'Minimal — user bubble, open assistant' }, { value: 'flat', label: 'Flat — open messages for both' }] },
|
|
610
|
+
{ id: 'layout-message-user-width', label: 'User Width', description: 'Content hugs the message; Full fills the transcript track', type: 'select', path: 'layout.messages.user.width', defaultValue: 'content', options: [{ value: 'content', label: 'Content' }, { value: 'full', label: 'Full' }] },
|
|
611
|
+
{ id: 'layout-message-user-max-width', label: 'User Max Width', description: 'Optional CSS width such as 80%, 42rem, or 72ch; blank uses the width mode default', type: 'text', path: 'layout.messages.user.maxWidth', defaultValue: '', parseValue: parseOptionalCssValue },
|
|
612
|
+
{ id: 'layout-message-assistant-width', label: 'Assistant Width', description: 'Content hugs the message; Full fills the transcript track', type: 'select', path: 'layout.messages.assistant.width', defaultValue: 'content', options: [{ value: 'content', label: 'Content' }, { value: 'full', label: 'Full' }] },
|
|
613
|
+
{ id: 'layout-message-assistant-max-width', label: 'Assistant Max Width', description: 'Optional CSS width such as 80%, 42rem, or 72ch; blank uses the width mode default', type: 'text', path: 'layout.messages.assistant.maxWidth', defaultValue: '', parseValue: parseOptionalCssValue },
|
|
605
614
|
{ id: 'layout-group', label: 'Group Consecutive', type: 'toggle', path: 'layout.messages.groupConsecutive', defaultValue: false },
|
|
606
615
|
{ id: 'layout-avatar-show', label: 'Show Avatars', type: 'toggle', path: 'layout.messages.avatar.show', defaultValue: false },
|
|
607
616
|
{ id: 'layout-avatar-pos', label: 'Avatar Position', type: 'select', path: 'layout.messages.avatar.position', defaultValue: 'left', options: [{ value: 'left', label: 'Left' }, { value: 'right', label: 'Right' }] },
|
package/src/types.ts
CHANGED
|
@@ -3975,6 +3975,27 @@ export type AgentWidgetTimestampConfig = {
|
|
|
3975
3975
|
format?: (date: Date) => string;
|
|
3976
3976
|
};
|
|
3977
3977
|
|
|
3978
|
+
/**
|
|
3979
|
+
* Width configuration for one message role.
|
|
3980
|
+
*
|
|
3981
|
+
* `content` shrink-wraps the rendered message up to `maxWidth`; `full` fills
|
|
3982
|
+
* the available transcript track up to `maxWidth`. The track already accounts
|
|
3983
|
+
* for transcript padding and, when present, the avatar and its gap.
|
|
3984
|
+
*/
|
|
3985
|
+
export type AgentWidgetMessageRoleLayout = {
|
|
3986
|
+
/**
|
|
3987
|
+
* Message width behavior.
|
|
3988
|
+
* - content: size to the rendered content (the backward-compatible default)
|
|
3989
|
+
* - full: fill the available transcript track
|
|
3990
|
+
*/
|
|
3991
|
+
width?: "content" | "full";
|
|
3992
|
+
/**
|
|
3993
|
+
* Optional CSS max-width (for example "80%", "42rem", or "72ch").
|
|
3994
|
+
* Defaults to "85%" for content width and "100%" for full width.
|
|
3995
|
+
*/
|
|
3996
|
+
maxWidth?: string;
|
|
3997
|
+
};
|
|
3998
|
+
|
|
3978
3999
|
/**
|
|
3979
4000
|
* Message layout configuration
|
|
3980
4001
|
* Allows customization of how chat messages are displayed
|
|
@@ -3993,6 +4014,10 @@ export type AgentWidgetMessageLayoutConfig = {
|
|
|
3993
4014
|
timestamp?: AgentWidgetTimestampConfig;
|
|
3994
4015
|
/** Group consecutive messages from the same role */
|
|
3995
4016
|
groupConsecutive?: boolean;
|
|
4017
|
+
/** Width behavior for user-authored message rows */
|
|
4018
|
+
user?: AgentWidgetMessageRoleLayout;
|
|
4019
|
+
/** Width behavior for assistant-authored message rows and assistant UI variants */
|
|
4020
|
+
assistant?: AgentWidgetMessageRoleLayout;
|
|
3996
4021
|
/**
|
|
3997
4022
|
* Custom renderer for user messages
|
|
3998
4023
|
* When provided, replaces the default user message rendering
|
|
@@ -270,7 +270,8 @@ describe("attachment button live config updates", () => {
|
|
|
270
270
|
expect(button().getAttribute("aria-label")).toBe("Add a photo");
|
|
271
271
|
const updatedSvg = button().querySelector("svg")!.outerHTML;
|
|
272
272
|
expect(updatedSvg).not.toBe(initialSvg);
|
|
273
|
-
|
|
273
|
+
button().parentElement!.dispatchEvent(new MouseEvent("mouseenter"));
|
|
274
|
+
const tooltip = document.body.querySelector(".persona-control-tooltip");
|
|
274
275
|
expect(tooltip?.textContent).toBe("Add a photo");
|
|
275
276
|
|
|
276
277
|
controller.destroy();
|