@runtypelabs/persona 3.18.0 → 3.19.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 +1 -1
- package/dist/index.cjs +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +281 -4
- package/dist/index.d.ts +281 -4
- package/dist/index.global.js +102 -1636
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +47 -47
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +1438 -619
- package/dist/theme-editor.d.cts +119 -1
- package/dist/theme-editor.d.ts +119 -1
- package/dist/theme-editor.js +1552 -619
- package/dist/widget.css +348 -0
- package/package.json +1 -1
- package/src/components/composer-builder.test.ts +52 -0
- package/src/components/composer-builder.ts +67 -490
- package/src/components/composer-parts.test.ts +152 -0
- package/src/components/composer-parts.ts +452 -0
- package/src/components/header-builder.ts +22 -299
- package/src/components/header-parts.ts +360 -0
- package/src/components/panel.test.ts +61 -0
- package/src/components/panel.ts +262 -5
- package/src/components/pill-composer-builder.test.ts +85 -0
- package/src/components/pill-composer-builder.ts +183 -0
- package/src/index.ts +4 -0
- package/src/runtime/init.ts +4 -2
- package/src/runtime/persist-state.test.ts +152 -0
- package/src/styles/widget.css +348 -0
- package/src/types.ts +121 -1
- package/src/ui.component-directive.test.ts +183 -0
- package/src/ui.composer-bar.test.ts +1009 -0
- package/src/ui.ts +809 -72
- package/src/utils/attachment-manager.ts +1 -1
- package/src/utils/dock.test.ts +45 -0
- package/src/utils/dock.ts +3 -0
- package/src/utils/icons.ts +314 -58
- package/src/utils/stream-animation.ts +7 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createAgentExperience } from "../ui";
|
|
6
|
+
import { createLocalStorageAdapter } from "../utils/storage";
|
|
7
|
+
import type { AgentWidgetStorageAdapter } from "../types";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_KEY = "persona-state";
|
|
10
|
+
|
|
11
|
+
const baseConfig = () => ({
|
|
12
|
+
apiUrl: "https://api.example.com/chat",
|
|
13
|
+
launcher: { enabled: false } as const,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const inject = (controller: ReturnType<typeof createAgentExperience>) =>
|
|
17
|
+
controller.injectAssistantMessage({ content: "hello world" });
|
|
18
|
+
|
|
19
|
+
describe("persistState gates storage adapter", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
document.body.innerHTML = "";
|
|
22
|
+
try {
|
|
23
|
+
window.localStorage.clear();
|
|
24
|
+
} catch {
|
|
25
|
+
/* jsdom edge cases */
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("persistState: false skips the default localStorage adapter", () => {
|
|
30
|
+
const mount = document.createElement("div");
|
|
31
|
+
document.body.appendChild(mount);
|
|
32
|
+
|
|
33
|
+
const controller = createAgentExperience(mount, {
|
|
34
|
+
...baseConfig(),
|
|
35
|
+
persistState: false,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
inject(controller);
|
|
39
|
+
|
|
40
|
+
expect(window.localStorage.getItem(DEFAULT_KEY)).toBeNull();
|
|
41
|
+
controller.destroy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("persistState: false ignores any user-supplied storageAdapter (strict semantic)", () => {
|
|
45
|
+
const mount = document.createElement("div");
|
|
46
|
+
document.body.appendChild(mount);
|
|
47
|
+
|
|
48
|
+
const customAdapter: AgentWidgetStorageAdapter = {
|
|
49
|
+
load: vi.fn(() => null),
|
|
50
|
+
save: vi.fn(),
|
|
51
|
+
clear: vi.fn(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const controller = createAgentExperience(mount, {
|
|
55
|
+
...baseConfig(),
|
|
56
|
+
persistState: false,
|
|
57
|
+
storageAdapter: customAdapter,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
inject(controller);
|
|
61
|
+
|
|
62
|
+
expect(customAdapter.load).not.toHaveBeenCalled();
|
|
63
|
+
expect(customAdapter.save).not.toHaveBeenCalled();
|
|
64
|
+
controller.destroy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("default config (persistState undefined) writes to the default localStorage key", () => {
|
|
68
|
+
const mount = document.createElement("div");
|
|
69
|
+
document.body.appendChild(mount);
|
|
70
|
+
|
|
71
|
+
const controller = createAgentExperience(mount, baseConfig());
|
|
72
|
+
|
|
73
|
+
inject(controller);
|
|
74
|
+
|
|
75
|
+
const stored = window.localStorage.getItem(DEFAULT_KEY);
|
|
76
|
+
expect(stored).not.toBeNull();
|
|
77
|
+
const parsed = JSON.parse(stored!);
|
|
78
|
+
expect(parsed.messages).toBeInstanceOf(Array);
|
|
79
|
+
expect(parsed.messages.length).toBeGreaterThan(0);
|
|
80
|
+
controller.destroy();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("persistState: true keeps using the default localStorage adapter", () => {
|
|
84
|
+
const mount = document.createElement("div");
|
|
85
|
+
document.body.appendChild(mount);
|
|
86
|
+
|
|
87
|
+
const controller = createAgentExperience(mount, {
|
|
88
|
+
...baseConfig(),
|
|
89
|
+
persistState: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
inject(controller);
|
|
93
|
+
|
|
94
|
+
expect(window.localStorage.getItem(DEFAULT_KEY)).not.toBeNull();
|
|
95
|
+
controller.destroy();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("two widgets with different storageAdapter keys keep their messages isolated", () => {
|
|
99
|
+
const mountA = document.createElement("div");
|
|
100
|
+
const mountB = document.createElement("div");
|
|
101
|
+
document.body.appendChild(mountA);
|
|
102
|
+
document.body.appendChild(mountB);
|
|
103
|
+
|
|
104
|
+
const controllerA = createAgentExperience(mountA, {
|
|
105
|
+
...baseConfig(),
|
|
106
|
+
storageAdapter: createLocalStorageAdapter("persona-state-test-a"),
|
|
107
|
+
});
|
|
108
|
+
const controllerB = createAgentExperience(mountB, {
|
|
109
|
+
...baseConfig(),
|
|
110
|
+
storageAdapter: createLocalStorageAdapter("persona-state-test-b"),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
controllerA.injectAssistantMessage({ content: "message in A" });
|
|
114
|
+
controllerB.injectAssistantMessage({ content: "message in B" });
|
|
115
|
+
|
|
116
|
+
const storedA = JSON.parse(window.localStorage.getItem("persona-state-test-a")!);
|
|
117
|
+
const storedB = JSON.parse(window.localStorage.getItem("persona-state-test-b")!);
|
|
118
|
+
|
|
119
|
+
const aHasA = storedA.messages.some((m: { content?: string }) => m.content === "message in A");
|
|
120
|
+
const aHasB = storedA.messages.some((m: { content?: string }) => m.content === "message in B");
|
|
121
|
+
const bHasA = storedB.messages.some((m: { content?: string }) => m.content === "message in A");
|
|
122
|
+
const bHasB = storedB.messages.some((m: { content?: string }) => m.content === "message in B");
|
|
123
|
+
|
|
124
|
+
expect(aHasA).toBe(true);
|
|
125
|
+
expect(aHasB).toBe(false);
|
|
126
|
+
expect(bHasB).toBe(true);
|
|
127
|
+
expect(bHasA).toBe(false);
|
|
128
|
+
expect(window.localStorage.getItem(DEFAULT_KEY)).toBeNull();
|
|
129
|
+
|
|
130
|
+
controllerA.destroy();
|
|
131
|
+
controllerB.destroy();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("persistState: false does not read from localStorage on init", () => {
|
|
135
|
+
// Pre-seed the default key with a stored message.
|
|
136
|
+
window.localStorage.setItem(
|
|
137
|
+
DEFAULT_KEY,
|
|
138
|
+
JSON.stringify({ messages: [{ id: "stale", role: "assistant", content: "stale" }] })
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const mount = document.createElement("div");
|
|
142
|
+
document.body.appendChild(mount);
|
|
143
|
+
|
|
144
|
+
const controller = createAgentExperience(mount, {
|
|
145
|
+
...baseConfig(),
|
|
146
|
+
persistState: false,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(controller.getMessages()).toEqual([]);
|
|
150
|
+
controller.destroy();
|
|
151
|
+
});
|
|
152
|
+
});
|
package/src/styles/widget.css
CHANGED
|
@@ -3389,3 +3389,351 @@
|
|
|
3389
3389
|
animation: none !important;
|
|
3390
3390
|
}
|
|
3391
3391
|
}
|
|
3392
|
+
|
|
3393
|
+
/* ===========================================================================
|
|
3394
|
+
* Composer-bar mode (`launcher.mountMode: "composer-bar"`)
|
|
3395
|
+
*
|
|
3396
|
+
* Geometry (position, width, height) is set per-state inline by
|
|
3397
|
+
* `applyComposerBarGeometry()` in ui.ts. The pill composer
|
|
3398
|
+
* (`pill-composer-builder.ts`) ships with its own clean className
|
|
3399
|
+
* (`persona-pill-composer`, no `persona-flex-col` / `persona-rounded-2xl`
|
|
3400
|
+
* baggage), so layout CSS does not need to fight utility classes — no
|
|
3401
|
+
* !important. The pill is the visible element in both collapsed and
|
|
3402
|
+
* expanded states; only the panel above it appears/disappears.
|
|
3403
|
+
*
|
|
3404
|
+
* Avoid `transform: scale(...)` on the wrapper — it breaks the textarea
|
|
3405
|
+
* caret/IME.
|
|
3406
|
+
* ======================================================================= */
|
|
3407
|
+
.persona-widget-wrapper[data-persona-composer-bar] {
|
|
3408
|
+
/* Flex column so the inner panel (which has `flex-1`) fills the wrapper.
|
|
3409
|
+
* Without this, the panel would shrink-wrap its children (chat container
|
|
3410
|
+
* + pill) and a gap would appear between the pill and the wrapper's
|
|
3411
|
+
* `bottom: 16px` edge. */
|
|
3412
|
+
display: flex;
|
|
3413
|
+
flex-direction: column;
|
|
3414
|
+
transition:
|
|
3415
|
+
max-width 220ms ease,
|
|
3416
|
+
bottom 220ms ease,
|
|
3417
|
+
top 220ms ease,
|
|
3418
|
+
left 220ms ease,
|
|
3419
|
+
right 220ms ease,
|
|
3420
|
+
transform 220ms ease,
|
|
3421
|
+
border-radius 220ms ease,
|
|
3422
|
+
height 220ms ease,
|
|
3423
|
+
width 220ms ease;
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
/* Modal and anchored: the wrapper goes from collapsed (no inline geometry,
|
|
3427
|
+
* so `top/left/transform` resolve to their auto/none defaults) to its
|
|
3428
|
+
* expanded position. The default `transform 220ms ease` transition would
|
|
3429
|
+
* interpolate `none → translate(...)` and visibly slide the wrapper from
|
|
3430
|
+
* its static-default origin toward its target — diagonally for modal,
|
|
3431
|
+
* horizontally from the right for anchored. Neither mode is morphing
|
|
3432
|
+
* something the user can see (the wrapper is invisible when collapsed
|
|
3433
|
+
* because the pill lives in pillRoot, not the wrapper), so the slide
|
|
3434
|
+
* is pure motion noise. Disable geometry transitions for both; the
|
|
3435
|
+
* container's opacity fade-in keyframe is the reveal.
|
|
3436
|
+
*
|
|
3437
|
+
* Fullscreen intentionally keeps its geometry transition — that's the
|
|
3438
|
+
* one mode where the wrapper genuinely morphs (empty → full viewport),
|
|
3439
|
+
* and the staggered fade-in cascade below is built to mask the
|
|
3440
|
+
* outer-edge/inner-content desync during that morph. */
|
|
3441
|
+
.persona-widget-wrapper[data-persona-composer-bar][data-expanded-size="modal"],
|
|
3442
|
+
.persona-widget-wrapper[data-persona-composer-bar][data-expanded-size="anchored"] {
|
|
3443
|
+
transition: none;
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
/* --- Pill composer chrome (always visible in composer-bar mode) -------- */
|
|
3447
|
+
|
|
3448
|
+
/* Pill is a single-row grid: paperclip · textarea · mic + send. */
|
|
3449
|
+
.persona-pill-composer {
|
|
3450
|
+
display: grid;
|
|
3451
|
+
grid-template-columns: auto 1fr auto;
|
|
3452
|
+
align-items: center;
|
|
3453
|
+
gap: 8px;
|
|
3454
|
+
padding: 6px 14px;
|
|
3455
|
+
border-radius: 9999px;
|
|
3456
|
+
background: var(--persona-surface, #ffffff);
|
|
3457
|
+
border: 1px solid var(--persona-border, #e5e7eb);
|
|
3458
|
+
box-shadow:
|
|
3459
|
+
0 6px 24px rgba(15, 23, 42, 0.10),
|
|
3460
|
+
0 1px 2px rgba(15, 23, 42, 0.04);
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
.persona-pill-composer .persona-composer-textarea {
|
|
3464
|
+
/* Single line by default; the autoresize closure expands up to the
|
|
3465
|
+
* max-height set inline by pill-composer-builder (100px). Width 100%
|
|
3466
|
+
* fills the `1fr` middle grid cell so wrapping only happens once the
|
|
3467
|
+
* pill itself can't grow any wider. */
|
|
3468
|
+
min-height: 24px;
|
|
3469
|
+
width: 100%;
|
|
3470
|
+
padding: 4px 0;
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
/* Responsive default width for the collapsed pill — applied only when
|
|
3474
|
+
* `applyComposerBarGeometry()` leaves the wrapper's inline width empty
|
|
3475
|
+
* (i.e., the user did NOT set `composerBar.collapsedMaxWidth`). Inline
|
|
3476
|
+
* width from a user config still wins via specificity (inline > class). */
|
|
3477
|
+
/* Pill responsive widths now live on the viewport-fixed pillRoot below;
|
|
3478
|
+
* the wrapper no longer doubles as the pill container. */
|
|
3479
|
+
|
|
3480
|
+
/* The action cells host inline-flex buttons; without explicit flex layout
|
|
3481
|
+
* on the cell, the buttons sit on the inline baseline and the cell extends
|
|
3482
|
+
* below them with descender space, making the buttons look top-aligned. */
|
|
3483
|
+
.persona-pill-composer__left,
|
|
3484
|
+
.persona-pill-composer__right {
|
|
3485
|
+
display: flex;
|
|
3486
|
+
align-items: center;
|
|
3487
|
+
gap: 4px;
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
/* Footer wrapping the pill: transparent surface, vertical stack with
|
|
3491
|
+
* room above the pill for the previews row. */
|
|
3492
|
+
.persona-widget-footer--pill {
|
|
3493
|
+
background: transparent;
|
|
3494
|
+
border-top: none;
|
|
3495
|
+
padding: 0;
|
|
3496
|
+
display: flex;
|
|
3497
|
+
flex-direction: column;
|
|
3498
|
+
gap: 8px;
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
/* Attachment previews row (above the pill). AttachmentManager toggles
|
|
3502
|
+
* display:flex when items are added; default is display:none. */
|
|
3503
|
+
.persona-pill-composer__previews {
|
|
3504
|
+
padding: 0 8px;
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
/* --- Container chrome ---------------------------------------------------
|
|
3508
|
+
*
|
|
3509
|
+
* Container chrome (background, border, border-radius, box-shadow) is
|
|
3510
|
+
* applied inline by `applyFullHeightStyles()` in ui.ts so it can flow through
|
|
3511
|
+
* the same `theme.components.panel.{shadow,border,borderRadius}` contract
|
|
3512
|
+
* used by every other mount mode. Collapsed hides the container entirely
|
|
3513
|
+
* (only the pill is visible); expanded re-applies the chrome. The
|
|
3514
|
+
* `fullscreen` expanded variant intentionally stays chrome-less. */
|
|
3515
|
+
|
|
3516
|
+
/* Composer overlay (interactive sheets like ask_user_question) hidden in
|
|
3517
|
+
* collapsed; the pill has no room and the panel above is gone anyway. */
|
|
3518
|
+
[data-persona-composer-bar][data-state="collapsed"] .persona-composer-overlay {
|
|
3519
|
+
display: none;
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
/* --- Pill root: viewport-fixed sibling of the wrapper ------------------
|
|
3523
|
+
*
|
|
3524
|
+
* In composer-bar mode the pill (`footer`) and peek banner live in a
|
|
3525
|
+
* viewport-fixed sibling of the wrapper, NOT inside the wrapper. Reason:
|
|
3526
|
+
* modal mode applies `transform: translate(-50%, -50%)` to the wrapper,
|
|
3527
|
+
* which establishes a containing block for `position: fixed` descendants —
|
|
3528
|
+
* a fixed pill inside a transformed wrapper would be positioned relative
|
|
3529
|
+
* to the wrapper, not the viewport.
|
|
3530
|
+
*
|
|
3531
|
+
* Bottom offset and (optionally) width are written inline by ui.ts based
|
|
3532
|
+
* on `launcher.composerBar.{bottomOffset, collapsedMaxWidth}`; otherwise
|
|
3533
|
+
* the responsive defaults below apply. The pillRoot mirrors the wrapper's
|
|
3534
|
+
* `[data-state]` and `[data-expanded-size]` attributes so peek/pill rules
|
|
3535
|
+
* keyed off those attributes still cascade. */
|
|
3536
|
+
.persona-widget-pill-root {
|
|
3537
|
+
position: fixed;
|
|
3538
|
+
bottom: 16px;
|
|
3539
|
+
left: 50%;
|
|
3540
|
+
transform: translateX(-50%);
|
|
3541
|
+
width: 90vw;
|
|
3542
|
+
max-width: calc(100vw - 32px);
|
|
3543
|
+
display: flex;
|
|
3544
|
+
flex-direction: column;
|
|
3545
|
+
gap: 8px;
|
|
3546
|
+
}
|
|
3547
|
+
@media (min-width: 640px) {
|
|
3548
|
+
.persona-widget-pill-root {
|
|
3549
|
+
width: 70vw;
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
@media (min-width: 1024px) {
|
|
3553
|
+
.persona-widget-pill-root {
|
|
3554
|
+
width: 50vw;
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
/* Container hidden when the chat is collapsed — only the pill is visible. */
|
|
3559
|
+
[data-persona-composer-bar][data-state="collapsed"] .persona-widget-container {
|
|
3560
|
+
display: none;
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
/* --- Peek banner ------------------------------------------------------- */
|
|
3564
|
+
/* The peek is the chrome-less row above the pill (composer-bar mode only)
|
|
3565
|
+
* that previews the trailing 100 chars of the most recent assistant
|
|
3566
|
+
* message. ui.ts toggles `--visible` based on streaming/hover/open state;
|
|
3567
|
+
* pointer-events are gated so the faded-out peek never swallows clicks
|
|
3568
|
+
* targeted at the pill below. */
|
|
3569
|
+
.persona-pill-peek {
|
|
3570
|
+
display: flex;
|
|
3571
|
+
align-items: center;
|
|
3572
|
+
gap: 8px;
|
|
3573
|
+
padding: 6px 14px;
|
|
3574
|
+
/* Frosted-glass chrome so the text stays readable over arbitrary host
|
|
3575
|
+
* backgrounds (busy hero images, dark sections, brand colors). The
|
|
3576
|
+
* translucent fill + backdrop blur turns the underlying area into a
|
|
3577
|
+
* neutral surface; the hairline border + soft shadow define the edge
|
|
3578
|
+
* without competing with the pill below. */
|
|
3579
|
+
background: rgba(255, 255, 255, 0.78);
|
|
3580
|
+
-webkit-backdrop-filter: blur(10px) saturate(1.5);
|
|
3581
|
+
backdrop-filter: blur(10px) saturate(1.5);
|
|
3582
|
+
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
3583
|
+
border-radius: 9999px;
|
|
3584
|
+
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
|
3585
|
+
color: var(--persona-primary, #111827);
|
|
3586
|
+
font: inherit;
|
|
3587
|
+
font-size: 13px;
|
|
3588
|
+
line-height: 1.4;
|
|
3589
|
+
cursor: pointer;
|
|
3590
|
+
opacity: 0;
|
|
3591
|
+
pointer-events: none;
|
|
3592
|
+
transform: translateY(2px);
|
|
3593
|
+
transition:
|
|
3594
|
+
opacity 150ms ease,
|
|
3595
|
+
transform 150ms ease,
|
|
3596
|
+
background-color 150ms ease;
|
|
3597
|
+
text-align: left;
|
|
3598
|
+
/* Full-width row that matches the pill below — text takes the middle
|
|
3599
|
+
* (flex: 1 1 auto) and the chevron pins to the right edge. */
|
|
3600
|
+
width: 100%;
|
|
3601
|
+
align-self: stretch;
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
/* Browsers without backdrop-filter support (older Firefox, some webviews)
|
|
3605
|
+
* fall through to a slightly more opaque solid surface so the underlying
|
|
3606
|
+
* page can't bleed through. The non-supports rule above already gives a
|
|
3607
|
+
* usable look (78% opacity); this just hardens it for non-blur fallback. */
|
|
3608
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
3609
|
+
.persona-pill-peek {
|
|
3610
|
+
background: rgba(255, 255, 255, 0.95);
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
.persona-pill-peek--visible {
|
|
3615
|
+
opacity: 1;
|
|
3616
|
+
pointer-events: auto;
|
|
3617
|
+
transform: translateY(0);
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
.persona-pill-peek:hover {
|
|
3621
|
+
background: rgba(255, 255, 255, 0.92);
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
.persona-pill-peek__icon,
|
|
3625
|
+
.persona-pill-peek__caret {
|
|
3626
|
+
display: inline-flex;
|
|
3627
|
+
flex-shrink: 0;
|
|
3628
|
+
color: currentColor;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
.persona-pill-peek__text {
|
|
3632
|
+
overflow: hidden;
|
|
3633
|
+
text-overflow: ellipsis;
|
|
3634
|
+
white-space: nowrap;
|
|
3635
|
+
flex: 1 1 auto;
|
|
3636
|
+
min-width: 0;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
/* Per-char/per-word animated spans render as inline-block, which interacts
|
|
3640
|
+
* with the peek's nowrap+ellipsis chrome. Setting `vertical-align: baseline`
|
|
3641
|
+
* keeps the spans on the same baseline as surrounding text so reveal
|
|
3642
|
+
* transforms (translateY, scale) don't bump the line height. */
|
|
3643
|
+
[data-persona-root] .persona-pill-peek__text .persona-stream-char,
|
|
3644
|
+
[data-persona-root] .persona-pill-peek__text .persona-stream-word {
|
|
3645
|
+
vertical-align: baseline;
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
/* Peek-scaled skeleton: the bubble-sized 260×10 bar would dwarf a 14px-line
|
|
3649
|
+
* pill. Inside the peek we shrink to a single, full-flex bar that hints at
|
|
3650
|
+
* "more is coming" without dominating the chrome. Used when
|
|
3651
|
+
* `streamAnimation.placeholder === "skeleton"` AND content trims to empty
|
|
3652
|
+
* (e.g. `buffer: "line"` between line completions). */
|
|
3653
|
+
[data-persona-root] .persona-pill-peek__text .persona-pill-peek__skeleton {
|
|
3654
|
+
display: inline-flex;
|
|
3655
|
+
vertical-align: middle;
|
|
3656
|
+
width: 100%;
|
|
3657
|
+
max-width: 200px;
|
|
3658
|
+
padding: 0;
|
|
3659
|
+
}
|
|
3660
|
+
[data-persona-root] .persona-pill-peek__text .persona-pill-peek__skeleton .persona-stream-skeleton-line {
|
|
3661
|
+
height: 8px;
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
/* --- Expanded states: geometry safety nets ----------------------------- */
|
|
3665
|
+
|
|
3666
|
+
/* `fullscreen`: ui.ts clears all inline geometry so this rule lights up. */
|
|
3667
|
+
.persona-widget-wrapper[data-persona-composer-bar][data-state="expanded"][data-expanded-size="fullscreen"] {
|
|
3668
|
+
inset: 0;
|
|
3669
|
+
top: 0;
|
|
3670
|
+
right: 0;
|
|
3671
|
+
bottom: 0;
|
|
3672
|
+
left: 0;
|
|
3673
|
+
transform: none;
|
|
3674
|
+
max-width: none;
|
|
3675
|
+
width: 100%;
|
|
3676
|
+
height: 100%;
|
|
3677
|
+
border-radius: 0;
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
/* Fullscreen "panel behind pill": the chat body fills the full viewport
|
|
3681
|
+
* (including the area behind the fixed pill) so messages scroll under the
|
|
3682
|
+
* pill. The body itself drops its bottom padding so its background extends
|
|
3683
|
+
* to the viewport edge; reachability for the last bubble comes from
|
|
3684
|
+
* padding-bottom on the messages wrapper (body's last child) — that
|
|
3685
|
+
* pushes the final message above the pill area rather than hiding it
|
|
3686
|
+
* behind. */
|
|
3687
|
+
.persona-widget-wrapper[data-persona-composer-bar][data-expanded-size="fullscreen"] .persona-widget-body {
|
|
3688
|
+
padding-bottom: 0;
|
|
3689
|
+
}
|
|
3690
|
+
.persona-widget-wrapper[data-persona-composer-bar][data-expanded-size="fullscreen"] .persona-widget-body > :last-child {
|
|
3691
|
+
padding-bottom: calc(var(--persona-pill-bottom, 16px) + var(--persona-pill-area-height, 80px) + 16px);
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
/* `modal` and `anchored` get their geometry inline from ui.ts; nothing
|
|
3695
|
+
* needed in CSS for those. */
|
|
3696
|
+
|
|
3697
|
+
/* --- Expand fade-in cascade -------------------------------------------- */
|
|
3698
|
+
/*
|
|
3699
|
+
* Two staggered fades that mask the visual desync between the wrapper's
|
|
3700
|
+
* fast-traveling outer edges and the inner content (which is constrained
|
|
3701
|
+
* by `contentMaxWidth` + `margin: auto` and therefore moves at a fraction
|
|
3702
|
+
* of the wrapper's growth rate). Without this, the user perceives an
|
|
3703
|
+
* empty "gap" inside the wrapper as it morphs to fullscreen.
|
|
3704
|
+
*
|
|
3705
|
+
* - Chrome (container background, border, shadow, and the absolute close +
|
|
3706
|
+
* clear buttons) fades in across the same 220ms as the geometry transition,
|
|
3707
|
+
* so the box appears to materialize as it grows rather than snap into
|
|
3708
|
+
* existence at frame 0.
|
|
3709
|
+
* - Body content (intro card, messages, composer overlay) fades in AFTER
|
|
3710
|
+
* the geometry has settled — `animation-delay: 220ms` plus
|
|
3711
|
+
* `animation-fill-mode: both` keeps it at opacity 0 during the morph.
|
|
3712
|
+
*
|
|
3713
|
+
* Because `[data-state="collapsed"] .persona-widget-container { display:
|
|
3714
|
+
* none }`, the container leaves and re-enters the rendering tree on each
|
|
3715
|
+
* expand, which retriggers the keyframe animation cleanly without
|
|
3716
|
+
* needing a JS class toggle.
|
|
3717
|
+
*/
|
|
3718
|
+
@keyframes persona-composer-bar-fade-in {
|
|
3719
|
+
from { opacity: 0; }
|
|
3720
|
+
to { opacity: 1; }
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
.persona-widget-wrapper[data-persona-composer-bar][data-state="expanded"] .persona-widget-container {
|
|
3724
|
+
animation: persona-composer-bar-fade-in 220ms ease both;
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
.persona-widget-wrapper[data-persona-composer-bar][data-state="expanded"] .persona-widget-body {
|
|
3728
|
+
animation: persona-composer-bar-fade-in 200ms ease 220ms both;
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3732
|
+
.persona-widget-wrapper[data-persona-composer-bar] {
|
|
3733
|
+
transition: none !important;
|
|
3734
|
+
}
|
|
3735
|
+
.persona-widget-wrapper[data-persona-composer-bar][data-state="expanded"] .persona-widget-container,
|
|
3736
|
+
.persona-widget-wrapper[data-persona-composer-bar][data-state="expanded"] .persona-widget-body {
|
|
3737
|
+
animation: none !important;
|
|
3738
|
+
}
|
|
3739
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1150,6 +1150,103 @@ export type AgentWidgetDockConfig = {
|
|
|
1150
1150
|
reveal?: "resize" | "overlay" | "push" | "emerge";
|
|
1151
1151
|
};
|
|
1152
1152
|
|
|
1153
|
+
/**
|
|
1154
|
+
* Layout configuration for `mountMode: "composer-bar"`. Controls how the
|
|
1155
|
+
* collapsed pill is positioned and sized, and how the panel expands when
|
|
1156
|
+
* the user opens it.
|
|
1157
|
+
*/
|
|
1158
|
+
export type AgentWidgetComposerBarConfig = {
|
|
1159
|
+
/**
|
|
1160
|
+
* Max-width of the collapsed pill composer at the bottom of the viewport.
|
|
1161
|
+
* @default "720px"
|
|
1162
|
+
*/
|
|
1163
|
+
collapsedMaxWidth?: string;
|
|
1164
|
+
/**
|
|
1165
|
+
* Bottom offset (CSS length) from the viewport edge in the collapsed state.
|
|
1166
|
+
* @default "16px"
|
|
1167
|
+
*/
|
|
1168
|
+
bottomOffset?: string;
|
|
1169
|
+
/**
|
|
1170
|
+
* Auto-expand the panel when the user submits a message while the composer
|
|
1171
|
+
* is collapsed. When false, the message still sends but the panel remains
|
|
1172
|
+
* collapsed (the host can drive expansion programmatically).
|
|
1173
|
+
* @default true
|
|
1174
|
+
*/
|
|
1175
|
+
expandOnSubmit?: boolean;
|
|
1176
|
+
/**
|
|
1177
|
+
* Size of the expanded chat panel.
|
|
1178
|
+
* - `"anchored"` (default): the pill stays at the bottom of the viewport
|
|
1179
|
+
* and the chat history grows upward into a centered column above it.
|
|
1180
|
+
* Width is driven by `expandedMaxWidth`; the panel's top edge sits at
|
|
1181
|
+
* `expandedTopOffset` from the viewport top.
|
|
1182
|
+
* - `"fullscreen"`: covers the entire viewport (mobile-app style). Inner
|
|
1183
|
+
* content is centered horizontally via `contentMaxWidth`.
|
|
1184
|
+
* - `"modal"`: centered sheet with margins; size driven by
|
|
1185
|
+
* `modalMaxWidth` / `modalMaxHeight`.
|
|
1186
|
+
* @default "anchored"
|
|
1187
|
+
*/
|
|
1188
|
+
expandedSize?: "anchored" | "fullscreen" | "modal";
|
|
1189
|
+
/**
|
|
1190
|
+
* When `expandedSize === "anchored"`, max-width of the expanded panel
|
|
1191
|
+
* column. Capped at `calc(100vw - 32px)` on narrow viewports.
|
|
1192
|
+
* @default "880px"
|
|
1193
|
+
*/
|
|
1194
|
+
expandedMaxWidth?: string;
|
|
1195
|
+
/**
|
|
1196
|
+
* When `expandedSize === "anchored"`, distance from the viewport top to
|
|
1197
|
+
* the panel's top edge. Accepts any CSS length.
|
|
1198
|
+
* @default "5vh"
|
|
1199
|
+
*/
|
|
1200
|
+
expandedTopOffset?: string;
|
|
1201
|
+
/**
|
|
1202
|
+
* Max-width applied to messages, composer form, suggestions, and
|
|
1203
|
+
* attachment previews so they center horizontally inside the expanded
|
|
1204
|
+
* panel. Falls back to `layout.contentMaxWidth` when set; otherwise
|
|
1205
|
+
* defaults to this value.
|
|
1206
|
+
* @default "720px"
|
|
1207
|
+
*/
|
|
1208
|
+
contentMaxWidth?: string;
|
|
1209
|
+
/**
|
|
1210
|
+
* When `expandedSize === "modal"`, max-width of the expanded sheet.
|
|
1211
|
+
* @default "880px"
|
|
1212
|
+
*/
|
|
1213
|
+
modalMaxWidth?: string;
|
|
1214
|
+
/**
|
|
1215
|
+
* When `expandedSize === "modal"`, max-height of the expanded sheet.
|
|
1216
|
+
* @default "min(90vh, 800px)"
|
|
1217
|
+
*/
|
|
1218
|
+
modalMaxHeight?: string;
|
|
1219
|
+
/**
|
|
1220
|
+
* Configuration for the "peek" banner — the chrome-less row above the
|
|
1221
|
+
* collapsed pill that previews the most recent assistant message.
|
|
1222
|
+
*/
|
|
1223
|
+
peek?: AgentWidgetComposerBarPeekConfig;
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Configuration for the composer-bar peek banner. Reuses the same
|
|
1228
|
+
* `streamAnimation` shape developers already configure for the main message
|
|
1229
|
+
* stream, so the surface for animations is identical across both contexts.
|
|
1230
|
+
*
|
|
1231
|
+
* Resolution order:
|
|
1232
|
+
* - If `peek.streamAnimation` is set, those values apply to the peek.
|
|
1233
|
+
* - Otherwise the peek inherits from `features.streamAnimation`.
|
|
1234
|
+
*
|
|
1235
|
+
* Per-surface carve-outs:
|
|
1236
|
+
* - `bubbleClass` from a plugin (used by `pop-bubble`) is ignored — the peek
|
|
1237
|
+
* has no bubble analog.
|
|
1238
|
+
* - `containerClass`, `wrap` ("char" | "word"), `useCaret`, `placeholder`
|
|
1239
|
+
* ("skeleton"), `buffer` ("word" | "line"), `speed`, `duration`, and
|
|
1240
|
+
* custom plugins all apply unchanged.
|
|
1241
|
+
*/
|
|
1242
|
+
export type AgentWidgetComposerBarPeekConfig = {
|
|
1243
|
+
/**
|
|
1244
|
+
* Reveal animation for the peek's trailing-message preview. Same shape as
|
|
1245
|
+
* `features.streamAnimation`. Omit to inherit from the main stream config.
|
|
1246
|
+
*/
|
|
1247
|
+
streamAnimation?: AgentWidgetStreamAnimationFeature;
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1153
1250
|
export type AgentWidgetLauncherConfig = {
|
|
1154
1251
|
enabled?: boolean;
|
|
1155
1252
|
title?: string;
|
|
@@ -1164,14 +1261,22 @@ export type AgentWidgetLauncherConfig = {
|
|
|
1164
1261
|
* Controls how the launcher panel is mounted relative to the host page.
|
|
1165
1262
|
* - "floating": default floating launcher / panel behavior
|
|
1166
1263
|
* - "docked": wraps the target container and renders as a sibling dock
|
|
1264
|
+
* - "composer-bar": persistent rounded-pill composer fixed to the bottom of
|
|
1265
|
+
* the viewport that morphs into a fullscreen (or modal) chat panel on
|
|
1266
|
+
* submit and minimizes back to the pill on close.
|
|
1167
1267
|
*
|
|
1168
1268
|
* @default "floating"
|
|
1169
1269
|
*/
|
|
1170
|
-
mountMode?: "floating" | "docked";
|
|
1270
|
+
mountMode?: "floating" | "docked" | "composer-bar";
|
|
1171
1271
|
/**
|
|
1172
1272
|
* Layout configuration for docked mode.
|
|
1173
1273
|
*/
|
|
1174
1274
|
dock?: AgentWidgetDockConfig;
|
|
1275
|
+
/**
|
|
1276
|
+
* Layout configuration for composer-bar mode.
|
|
1277
|
+
* Only applies when `mountMode === "composer-bar"`.
|
|
1278
|
+
*/
|
|
1279
|
+
composerBar?: AgentWidgetComposerBarConfig;
|
|
1175
1280
|
autoExpand?: boolean;
|
|
1176
1281
|
width?: string;
|
|
1177
1282
|
/**
|
|
@@ -3357,6 +3462,13 @@ export type AgentWidgetConfig = {
|
|
|
3357
3462
|
* When `true`, uses default settings with sessionStorage.
|
|
3358
3463
|
* When an object, allows customizing storage type, key prefix, and what to persist.
|
|
3359
3464
|
*
|
|
3465
|
+
* Setting this to `false` is the explicit kill-switch: it disables UI-state
|
|
3466
|
+
* persistence **and** message-history persistence. When `false`, any
|
|
3467
|
+
* `storageAdapter` you configure is ignored and the default localStorage
|
|
3468
|
+
* adapter is not created — no chat history is read or written. Pass `true`
|
|
3469
|
+
* (or omit) to keep the default behavior of persisting messages via the
|
|
3470
|
+
* configured `storageAdapter` (or the built-in localStorage adapter).
|
|
3471
|
+
*
|
|
3360
3472
|
* @example
|
|
3361
3473
|
* ```typescript
|
|
3362
3474
|
* // Simple usage - persist open state in sessionStorage
|
|
@@ -3379,6 +3491,14 @@ export type AgentWidgetConfig = {
|
|
|
3379
3491
|
* }
|
|
3380
3492
|
* }
|
|
3381
3493
|
* ```
|
|
3494
|
+
*
|
|
3495
|
+
* @example
|
|
3496
|
+
* ```typescript
|
|
3497
|
+
* // Ephemeral widget — no message history written anywhere
|
|
3498
|
+
* config: {
|
|
3499
|
+
* persistState: false
|
|
3500
|
+
* }
|
|
3501
|
+
* ```
|
|
3382
3502
|
*/
|
|
3383
3503
|
persistState?: boolean | AgentWidgetPersistStateConfig;
|
|
3384
3504
|
|