@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
package/src/plugins/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ResolvedWelcomeConfig } from "../welcome";
|
|
2
|
+
import type { AgentWidgetPluginStorage } from "../utils/plugin-storage";
|
|
1
3
|
import {
|
|
2
4
|
AgentWidgetMessage,
|
|
3
5
|
AgentWidgetConfig,
|
|
@@ -13,7 +15,8 @@ import {
|
|
|
13
15
|
AgentWidgetResolvedSuggestion,
|
|
14
16
|
AgentWidgetSuggestionSource,
|
|
15
17
|
AgentWidgetSuggestionSurface,
|
|
16
|
-
AgentWidgetSuggestionVariant
|
|
18
|
+
AgentWidgetSuggestionVariant,
|
|
19
|
+
AgentWidgetWelcomeVariant
|
|
17
20
|
} from "../types";
|
|
18
21
|
|
|
19
22
|
export type AgentWidgetTransformSuggestionsContext = {
|
|
@@ -45,6 +48,28 @@ export type AgentWidgetRenderSuggestionContext = {
|
|
|
45
48
|
select: () => void;
|
|
46
49
|
};
|
|
47
50
|
|
|
51
|
+
export type AgentWidgetRenderWelcomeContext = {
|
|
52
|
+
/** Alias-resolved welcome config; never the raw `copy.welcome*` shape. */
|
|
53
|
+
config: ResolvedWelcomeConfig;
|
|
54
|
+
variant: AgentWidgetWelcomeVariant;
|
|
55
|
+
/** Derived visibility for this render; governs the default renderer only. */
|
|
56
|
+
visible: boolean;
|
|
57
|
+
/** The core-owned welcome card, live-updated by `controller.update()`. */
|
|
58
|
+
defaultRenderer: () => HTMLElement;
|
|
59
|
+
sendMessage: (text: string) => void;
|
|
60
|
+
/** Re-run welcome arbitration: cleanups run, previous content is removed. */
|
|
61
|
+
requestRender: () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Persona's starter renderer, wired through the full select pipeline:
|
|
64
|
+
* `onSuggestionSelect`, cancelable `persona:suggestion:*` events, send/fill.
|
|
65
|
+
*/
|
|
66
|
+
renderStarter: (suggestion: AgentWidgetSuggestion) => HTMLElement;
|
|
67
|
+
/** Sync store namespaced by `persistState.keyPrefix` plus the plugin id. */
|
|
68
|
+
storage: AgentWidgetPluginStorage;
|
|
69
|
+
/** Teardown for this render; runs before the next one and on destroy. */
|
|
70
|
+
onCleanup: (fn: () => void) => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
48
73
|
export type AgentWidgetSuggestionSelectContext = {
|
|
49
74
|
suggestion: AgentWidgetResolvedSuggestion;
|
|
50
75
|
surface: AgentWidgetSuggestionSurface;
|
|
@@ -115,6 +140,37 @@ export interface AgentWidgetPlugin {
|
|
|
115
140
|
onToggle: () => void;
|
|
116
141
|
}) => HTMLElement | null;
|
|
117
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Custom renderer for the welcome surface (card, hero, greeting stack).
|
|
145
|
+
* First plugin returning an element wins; null falls through to the default,
|
|
146
|
+
* same contract as `renderSuggestion`.
|
|
147
|
+
*
|
|
148
|
+
* The core owns the welcome host: it stays mounted for the panel's lifetime
|
|
149
|
+
* and content is swapped inside it. A plugin element renders regardless of
|
|
150
|
+
* `ctx.visible` (derived visibility governs the default renderer only) and
|
|
151
|
+
* the host overlays the transcript while that element is active, so a home
|
|
152
|
+
* screen can return over an existing conversation. Return null again to
|
|
153
|
+
* clear the overlay.
|
|
154
|
+
*
|
|
155
|
+
* The ctx carries no controller: plugins are host-instantiated, so close a
|
|
156
|
+
* factory over the init handle (`plugin.attach(controller)`).
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```typescript
|
|
160
|
+
* renderWelcome: ({ config, renderStarter, requestRender, onCleanup }) => {
|
|
161
|
+
* const root = document.createElement("div");
|
|
162
|
+
* root.textContent = config.title;
|
|
163
|
+
* root.appendChild(renderStarter({ label: "Track my order" }));
|
|
164
|
+
* const timer = setInterval(requestRender, 60_000);
|
|
165
|
+
* onCleanup(() => clearInterval(timer));
|
|
166
|
+
* return root;
|
|
167
|
+
* }
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
renderWelcome?: (
|
|
171
|
+
context: AgentWidgetRenderWelcomeContext
|
|
172
|
+
) => HTMLElement | null;
|
|
173
|
+
|
|
118
174
|
/**
|
|
119
175
|
* Custom renderer for panel header
|
|
120
176
|
* Return null to use default renderer
|
|
@@ -157,6 +213,15 @@ export interface AgentWidgetPlugin {
|
|
|
157
213
|
* Omitted when `config.voiceRecognition.enabled` is not true.
|
|
158
214
|
*/
|
|
159
215
|
onVoiceToggle?: () => void;
|
|
216
|
+
/**
|
|
217
|
+
* Re-run composer arbitration and swap the footer in place. The hook runs
|
|
218
|
+
* once at panel construction, so this is how a composer plugin unlocks
|
|
219
|
+
* (returns null) or re-renders later. Pending attachments, the mention
|
|
220
|
+
* affordances, and the composer listener registry are re-wired by the core.
|
|
221
|
+
*/
|
|
222
|
+
requestRender: () => void;
|
|
223
|
+
/** Same sync store as the `renderWelcome` ctx, namespaced per plugin id. */
|
|
224
|
+
storage: AgentWidgetPluginStorage;
|
|
160
225
|
}) => HTMLElement | null;
|
|
161
226
|
|
|
162
227
|
/**
|
|
@@ -106,6 +106,36 @@ describe("suggestion interaction styles", () => {
|
|
|
106
106
|
expect(rule).toContain("max-width: 600px");
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
+
it("centers label-only items and top-aligns only description cards", () => {
|
|
110
|
+
const cardStart = widgetCss.indexOf(".persona-suggestion--card {");
|
|
111
|
+
const cardRule = widgetCss.slice(
|
|
112
|
+
cardStart,
|
|
113
|
+
widgetCss.indexOf("\n}", cardStart),
|
|
114
|
+
);
|
|
115
|
+
expect(cardRule).toContain("align-items: center");
|
|
116
|
+
|
|
117
|
+
const listStart = widgetCss.indexOf(".persona-suggestion--list {");
|
|
118
|
+
const listRule = widgetCss.slice(
|
|
119
|
+
listStart,
|
|
120
|
+
widgetCss.indexOf("\n}", listStart),
|
|
121
|
+
);
|
|
122
|
+
expect(listRule).toContain("align-items: center");
|
|
123
|
+
|
|
124
|
+
// Only content that actually wraps to two lines earns the top alignment.
|
|
125
|
+
const hasStart = widgetCss.indexOf(
|
|
126
|
+
".persona-suggestion--card:has(.persona-suggestion__description)",
|
|
127
|
+
);
|
|
128
|
+
const hasRule = widgetCss.slice(
|
|
129
|
+
hasStart,
|
|
130
|
+
widgetCss.indexOf("\n}", hasStart),
|
|
131
|
+
);
|
|
132
|
+
expect(hasStart).toBeGreaterThan(-1);
|
|
133
|
+
expect(hasRule).toContain(
|
|
134
|
+
".persona-suggestion--list:has(.persona-suggestion__description)",
|
|
135
|
+
);
|
|
136
|
+
expect(hasRule).toContain("align-items: flex-start");
|
|
137
|
+
});
|
|
138
|
+
|
|
109
139
|
it("masks only the scrollable chip edges that still contain content", () => {
|
|
110
140
|
expect(widgetCss).toContain(
|
|
111
141
|
'[data-overflow="scroll"][data-scroll-right]:not(',
|
|
@@ -120,6 +150,43 @@ describe("suggestion interaction styles", () => {
|
|
|
120
150
|
"--persona-suggestion-scroll-fade-size: 32px",
|
|
121
151
|
);
|
|
122
152
|
});
|
|
153
|
+
|
|
154
|
+
it("centers the chip row on the composer surface only", () => {
|
|
155
|
+
const selector =
|
|
156
|
+
'[data-persona-composer-suggestions].persona-suggestions[data-variant="chip"] {';
|
|
157
|
+
const start = widgetCss.indexOf(selector);
|
|
158
|
+
const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
|
|
159
|
+
|
|
160
|
+
expect(start).toBeGreaterThan(-1);
|
|
161
|
+
// fit-content + auto margins center a single row while wrapped rows keep
|
|
162
|
+
// a shared left edge; per-row justify-content would rag them.
|
|
163
|
+
expect(rule).toContain("width: fit-content");
|
|
164
|
+
expect(rule).toContain("max-width: 100%");
|
|
165
|
+
expect(rule).toContain("margin-inline: auto");
|
|
166
|
+
expect(rule).not.toContain("justify-content");
|
|
167
|
+
|
|
168
|
+
// Transcript follow-ups and welcome rows keep the default left alignment.
|
|
169
|
+
const sharedStart = widgetCss.indexOf(
|
|
170
|
+
'.persona-suggestions[data-variant="chip"] {',
|
|
171
|
+
);
|
|
172
|
+
const sharedRule = widgetCss.slice(
|
|
173
|
+
sharedStart,
|
|
174
|
+
widgetCss.indexOf("\n}", sharedStart),
|
|
175
|
+
);
|
|
176
|
+
expect(sharedStart).toBeGreaterThan(-1);
|
|
177
|
+
expect(sharedRule).not.toContain("justify-content");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("only pulls follow-ups up against the roomy welcome-visible body gap", () => {
|
|
181
|
+
// Unscoped, the -12px offset would zero out the tightened 12px gap that
|
|
182
|
+
// applies when the welcome host is hidden, gluing follow-ups to the answer.
|
|
183
|
+
expect(widgetCss).toContain(
|
|
184
|
+
'.persona-gap-6 > [data-persona-suggestions="follow-up"] {',
|
|
185
|
+
);
|
|
186
|
+
expect(widgetCss).not.toMatch(
|
|
187
|
+
/\n\[data-persona-suggestions="follow-up"\] \{/,
|
|
188
|
+
);
|
|
189
|
+
});
|
|
123
190
|
});
|
|
124
191
|
|
|
125
192
|
describe("composer spacing styles", () => {
|
|
@@ -160,3 +227,156 @@ describe("composer spacing styles", () => {
|
|
|
160
227
|
expect(rule).toContain("gap: 8px");
|
|
161
228
|
});
|
|
162
229
|
});
|
|
230
|
+
|
|
231
|
+
describe("transcript layout styles", () => {
|
|
232
|
+
it("drops the empty messages wrapper from the flex flow", () => {
|
|
233
|
+
// A zero-height flex child still triggers the body's gap, so an empty
|
|
234
|
+
// transcript added phantom scrollable space below the welcome (and a
|
|
235
|
+
// scroll-to-bottom arrow with nothing to scroll to).
|
|
236
|
+
expect(widgetCss).toContain(
|
|
237
|
+
".persona-widget-messages:empty {\n display: none;\n}",
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("launcher teaser styles", () => {
|
|
243
|
+
it("keeps the [hidden] dismiss control hidden despite its own display: grid", () => {
|
|
244
|
+
// dismissible: false sets the hidden property; without this companion
|
|
245
|
+
// rule the block's display: grid outranks the UA [hidden] stylesheet.
|
|
246
|
+
expect(widgetCss).toContain(
|
|
247
|
+
".persona-launcher-teaser-dismiss[hidden] {\n display: none;\n}",
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("plugin welcome styles", () => {
|
|
253
|
+
it("hides the default welcome content while a plugin element owns the host", () => {
|
|
254
|
+
const selector =
|
|
255
|
+
'.persona-welcome[data-persona-welcome-content="plugin"]\n > :not([data-persona-welcome-plugin]) {';
|
|
256
|
+
const start = widgetCss.indexOf(selector);
|
|
257
|
+
const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
|
|
258
|
+
|
|
259
|
+
expect(start).toBeGreaterThan(-1);
|
|
260
|
+
expect(rule).toContain("display: none !important");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("centers the welcome column at wide widths but never the overlay", () => {
|
|
264
|
+
const start = widgetCss.indexOf(
|
|
265
|
+
".persona-welcome:not([data-persona-welcome-overlay]) {",
|
|
266
|
+
);
|
|
267
|
+
const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
|
|
268
|
+
|
|
269
|
+
expect(start).toBeGreaterThan(-1);
|
|
270
|
+
// The card variant shares the transcript column so its left-aligned
|
|
271
|
+
// text lines up with messages and composer.
|
|
272
|
+
expect(rule).toContain("max-width: var(--persona-content-max-width, 768px)");
|
|
273
|
+
expect(rule).toContain("margin-inline: auto");
|
|
274
|
+
// The var is published on the root so plugin content can consume it.
|
|
275
|
+
expect(widgetCss).toContain("--persona-welcome-max-width: 640px");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("keeps the narrower centered column for the hero variant only", () => {
|
|
279
|
+
const start = widgetCss.indexOf(
|
|
280
|
+
'.persona-welcome[data-persona-welcome-variant="hero"]:not(',
|
|
281
|
+
);
|
|
282
|
+
const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
|
|
283
|
+
|
|
284
|
+
expect(start).toBeGreaterThan(-1);
|
|
285
|
+
expect(rule).toContain("max-width: var(--persona-welcome-max-width, 640px)");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("aligns the greeting bubble to the transcript column", () => {
|
|
289
|
+
const start = widgetCss.indexOf(".persona-welcome-greeting {");
|
|
290
|
+
const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
|
|
291
|
+
|
|
292
|
+
expect(start).toBeGreaterThan(-1);
|
|
293
|
+
expect(rule).toContain("max-width: var(--persona-content-max-width, 768px)");
|
|
294
|
+
expect(rule).toContain("margin-inline: auto");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("pads the welcome host from the intro-card alias, full padding on overlays", () => {
|
|
298
|
+
// Flat default resolves the alias to `1.5rem 0` (tokens.ts), aligning
|
|
299
|
+
// the text with the content column; overlays are opaque surfaces and
|
|
300
|
+
// keep a fixed symmetric inset.
|
|
301
|
+
const start = widgetCss.indexOf(".persona-welcome {");
|
|
302
|
+
const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
|
|
303
|
+
|
|
304
|
+
expect(start).toBeGreaterThan(-1);
|
|
305
|
+
expect(rule).toContain("padding: var(--persona-intro-card-padding, 1.5rem)");
|
|
306
|
+
|
|
307
|
+
const overlayStart = widgetCss.indexOf(
|
|
308
|
+
".persona-welcome[data-persona-welcome-overlay] {",
|
|
309
|
+
);
|
|
310
|
+
const overlayRule = widgetCss.slice(
|
|
311
|
+
overlayStart,
|
|
312
|
+
widgetCss.indexOf("\n}", overlayStart),
|
|
313
|
+
);
|
|
314
|
+
expect(overlayRule).toContain("padding: 1.5rem");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("overlays the messages area while plugin content is active", () => {
|
|
318
|
+
const start = widgetCss.indexOf(
|
|
319
|
+
".persona-welcome[data-persona-welcome-overlay] {",
|
|
320
|
+
);
|
|
321
|
+
const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
|
|
322
|
+
|
|
323
|
+
expect(start).toBeGreaterThan(-1);
|
|
324
|
+
expect(rule).toContain("position: absolute");
|
|
325
|
+
expect(rule).toContain("inset: 0");
|
|
326
|
+
// !important: only way to beat the host's inline intro-card background
|
|
327
|
+
// (default transparent), which otherwise lets the transcript bleed
|
|
328
|
+
// through the overlay.
|
|
329
|
+
expect(rule).toMatch(/background:[\s\S]*!important/);
|
|
330
|
+
// Two classes so it outranks the body's own overflow utility.
|
|
331
|
+
expect(widgetCss).toContain(
|
|
332
|
+
".persona-widget-body.persona-welcome-overlay-active {\n overflow: hidden;\n}",
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("scrollbar policy styles", () => {
|
|
338
|
+
it("themes every scroller from the shared tokens", () => {
|
|
339
|
+
// scrollbar-color inherits from the root; scrollbar-width does not, so
|
|
340
|
+
// the width needs the descendant star.
|
|
341
|
+
expect(widgetCss).toMatch(
|
|
342
|
+
/\[data-persona-root\] \{\n {2}scrollbar-color: var\(\n\s+--persona-scrollbar-thumb,/,
|
|
343
|
+
);
|
|
344
|
+
expect(widgetCss).toContain("var(--persona-scrollbar-track, transparent)");
|
|
345
|
+
expect(widgetCss).toContain(
|
|
346
|
+
"[data-persona-root],\n[data-persona-root] * {\n scrollbar-width: thin;\n}",
|
|
347
|
+
);
|
|
348
|
+
// The artifact tab strip keeps its own token as an alias layered on the
|
|
349
|
+
// shared thumb token.
|
|
350
|
+
expect(widgetCss).toContain(
|
|
351
|
+
"--persona-artifact-tab-list-scrollbar,\n var(--persona-scrollbar-thumb, var(--persona-border, #e5e7eb))",
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("hides the transcript bar at rest under the on-scroll policy without reflow", () => {
|
|
356
|
+
// Rest state keys off the ui.ts-owned attributes; the reveal attribute
|
|
357
|
+
// must appear as a :not() so flipping it shows the bar again.
|
|
358
|
+
expect(widgetCss).toContain(
|
|
359
|
+
'[data-persona-scrollbar="on-scroll"]\n .persona-widget-body:not([data-persona-scrollbar-visible]),\n[data-persona-scrollbar="hidden"] .persona-widget-body {\n scrollbar-width: none;\n}',
|
|
360
|
+
);
|
|
361
|
+
// Old-Safari fallback: webkit hiding agrees with the standard property.
|
|
362
|
+
expect(widgetCss).toContain(
|
|
363
|
+
'[data-persona-scrollbar="hidden"] .persona-widget-body::-webkit-scrollbar {\n display: none;\n}',
|
|
364
|
+
);
|
|
365
|
+
// Classic-scrollbar platforms must not reflow the transcript on toggle.
|
|
366
|
+
expect(widgetCss).toContain(
|
|
367
|
+
'[data-persona-scrollbar="on-scroll"] .persona-widget-body {\n scrollbar-gutter: stable;\n}',
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("hover-reveals inner scroller bars under the on-scroll policy", () => {
|
|
372
|
+
expect(widgetCss).toContain(
|
|
373
|
+
'[data-persona-scrollbar="on-scroll"] .persona-artifact-list:not(:hover)',
|
|
374
|
+
);
|
|
375
|
+
expect(widgetCss).toContain(
|
|
376
|
+
'[data-persona-scrollbar="on-scroll"]\n .persona-suggestions[data-overflow="scroll"]:not(:hover)',
|
|
377
|
+
);
|
|
378
|
+
expect(widgetCss).toContain(
|
|
379
|
+
'[data-persona-scrollbar="hidden"] .persona-artifact-list,',
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
});
|