@artooi/ag-ui-web-component 0.47.0 → 0.49.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/CHANGELOG.md +205 -1
- package/README.md +235 -16
- package/dist/ag-ui-web-component.bundle.js +647 -181
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +27 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +44 -5
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/events/bar_height_detail.d.ts +6 -0
- package/dist/core/events/bar_height_detail.d.ts.map +1 -0
- package/dist/dom/highlight_overlay.d.ts +2 -1
- package/dist/dom/highlight_overlay.d.ts.map +1 -1
- package/dist/dom/ring_color.d.ts +8 -0
- package/dist/dom/ring_color.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1184 -69
- package/dist/index.js.map +4 -4
- package/dist/tools/chat_surface_tools.d.ts +8 -1
- package/dist/tools/chat_surface_tools.d.ts.map +1 -1
- package/dist/ui/history/conversation_history.d.ts +16 -3
- package/dist/ui/history/conversation_history.d.ts.map +1 -1
- package/dist/ui/placement/collapse_styles.d.ts.map +1 -1
- package/dist/ui/placement/dock_styles.d.ts +2 -0
- package/dist/ui/placement/dock_styles.d.ts.map +1 -0
- package/dist/ui/placement/panel_placement.d.ts +16 -0
- package/dist/ui/placement/panel_placement.d.ts.map +1 -1
- package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -1
- package/dist/ui/placement/surface_for.d.ts +32 -19
- package/dist/ui/placement/surface_for.d.ts.map +1 -1
- package/dist/ui/progress/tool_group_styles.d.ts.map +1 -1
- package/dist/ui/progress/tool_step_styles.d.ts.map +1 -1
- package/dist/ui/shell/build_launcher.d.ts +15 -4
- package/dist/ui/shell/build_launcher.d.ts.map +1 -1
- package/dist/ui/shell/token_styles.d.ts.map +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/themes/quiet_styles.d.ts.map +1 -1
- package/dist/ui/themes/scheme_styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +30 -2
- package/src/core/ag_ui_chat.ts +335 -28
- package/src/core/events/bar_height_detail.ts +5 -0
- package/src/dom/highlight_overlay.ts +2 -1
- package/src/dom/ring_color.ts +141 -4
- package/src/index.ts +4 -0
- package/src/tools/chat_surface_tools.ts +13 -5
- package/src/ui/history/conversation_history.ts +51 -7
- package/src/ui/placement/collapse_styles.ts +248 -2
- package/src/ui/placement/dock_styles.ts +95 -0
- package/src/ui/placement/panel_placement.ts +300 -16
- package/src/ui/placement/small_viewport_styles.ts +9 -0
- package/src/ui/placement/surface_for.ts +58 -22
- package/src/ui/progress/tool_group_styles.ts +52 -2
- package/src/ui/progress/tool_step_styles.ts +5 -3
- package/src/ui/shell/build_launcher.ts +32 -5
- package/src/ui/shell/token_styles.ts +58 -1
- package/src/ui/styles.ts +4 -0
- package/src/ui/themes/quiet_styles.ts +19 -3
- package/src/ui/themes/scheme_styles.ts +10 -1
- package/src/version.ts +1 -1
package/src/dom/ring_color.ts
CHANGED
|
@@ -25,6 +25,14 @@ const PACKAGE_ACCENT = "#4f46e5";
|
|
|
25
25
|
* host that themes `--ag-ui-accent` on its page keeps the rings it has; the
|
|
26
26
|
* ring token sits above both so a host can keep the ring apart from the accent.
|
|
27
27
|
*
|
|
28
|
+
* Only the chat's own accent is adjusted, because it follows the widget's
|
|
29
|
+
* scheme rather than the page's: a dark widget's near-white accent on a light
|
|
30
|
+
* page all but disappears. It is held to 3:1 against the surface behind the
|
|
31
|
+
* target (see {@link surfaceBehind}): the accent as resolved if it holds, else
|
|
32
|
+
* the other half of the chat's light-dark pair, else the accent stepped toward
|
|
33
|
+
* black or white. Every value above it is a choice someone made, and is drawn
|
|
34
|
+
* as given.
|
|
35
|
+
*
|
|
28
36
|
* `chat` defaults to the most recently connected `<ag-ui-chat>`, the common
|
|
29
37
|
* page with one widget. A page with two passes the one the ring speaks for.
|
|
30
38
|
*/
|
|
@@ -42,8 +50,130 @@ export function ringColor(target: HTMLElement, chat?: HTMLElement): string {
|
|
|
42
50
|
if (accent !== "") {
|
|
43
51
|
return accent;
|
|
44
52
|
}
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
if (owner === null) {
|
|
54
|
+
return PACKAGE_ACCENT;
|
|
55
|
+
}
|
|
56
|
+
const chatAccent = resolveOn(owner, CHAT_ACCENT);
|
|
57
|
+
return chatAccent === "" ? PACKAGE_ACCENT : legible(owner, chatAccent, target);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The contrast a ring needs against what it is drawn on (WCAG 2.4.13). */
|
|
61
|
+
const RING_CONTRAST = 3;
|
|
62
|
+
|
|
63
|
+
/** How far each step moves a failing accent toward black or white. */
|
|
64
|
+
const STEP = 0.1;
|
|
65
|
+
|
|
66
|
+
type Rgba = readonly [number, number, number, number];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `accent`, or the nearest variant of it that holds {@link RING_CONTRAST}
|
|
70
|
+
* against the surface behind `target`.
|
|
71
|
+
*
|
|
72
|
+
* Colours are compared as sRGB bytes painted onto a one-pixel canvas, the one
|
|
73
|
+
* reading that turns a resolved color-mix() or oklch() into numbers. Where
|
|
74
|
+
* there is no 2d context to paint on, nothing can be measured, and the accent
|
|
75
|
+
* is drawn as it was.
|
|
76
|
+
*/
|
|
77
|
+
function legible(chat: HTMLElement, accent: string, target: HTMLElement): string {
|
|
78
|
+
const paint = document.createElement("canvas").getContext("2d", { willReadFrequently: true });
|
|
79
|
+
if (paint === null) {
|
|
80
|
+
return accent;
|
|
81
|
+
}
|
|
82
|
+
const bytes = (color: string): Rgba => {
|
|
83
|
+
paint.clearRect(0, 0, 1, 1);
|
|
84
|
+
paint.fillStyle = "rgb(0 0 0 / 0)";
|
|
85
|
+
paint.fillStyle = color;
|
|
86
|
+
paint.fillRect(0, 0, 1, 1);
|
|
87
|
+
const [r = 0, g = 0, b = 0, a = 0] = paint.getImageData(0, 0, 1, 1).data;
|
|
88
|
+
return [r, g, b, a / 255];
|
|
89
|
+
};
|
|
90
|
+
const surface = surfaceBehind(target, bytes);
|
|
91
|
+
const holds = (color: Rgba): boolean => contrast(over(color, surface), surface) >= RING_CONTRAST;
|
|
92
|
+
const own = [accent, resolveOn(chat, CHAT_ACCENT, "light"), resolveOn(chat, CHAT_ACCENT, "dark")];
|
|
93
|
+
const fit = own.find((candidate) => holds(bytes(candidate)));
|
|
94
|
+
if (fit !== undefined) {
|
|
95
|
+
return fit;
|
|
96
|
+
}
|
|
97
|
+
// Black or white, whichever the surface sets off better, holds at least
|
|
98
|
+
// 4.5:1 against any surface, so the walk always ends by the last step.
|
|
99
|
+
const from = over(bytes(accent), surface);
|
|
100
|
+
const black: Rgba = [0, 0, 0, 1];
|
|
101
|
+
const white: Rgba = [255, 255, 255, 1];
|
|
102
|
+
const toward = contrast(black, surface) > contrast(white, surface) ? black : white;
|
|
103
|
+
let step = 1;
|
|
104
|
+
while (!holds(mix(from, toward, step * STEP))) {
|
|
105
|
+
step += 1;
|
|
106
|
+
}
|
|
107
|
+
const [r, g, b] = mix(from, toward, step * STEP);
|
|
108
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The colour of the surface a ring around `target` is drawn on, as opaque
|
|
113
|
+
* sRGB bytes.
|
|
114
|
+
*
|
|
115
|
+
* The ring sits outside the target's border box (the flash outline is offset
|
|
116
|
+
* from it, the overlay leaves a gap), so the target's own fill is not under
|
|
117
|
+
* it: a white field on a dark page is ringed on the dark page. The surface is
|
|
118
|
+
* every background from the target's parent up, composited until one is
|
|
119
|
+
* opaque, over the page canvas, which follows the root's colour scheme. A
|
|
120
|
+
* background image is not read; its colour beneath is.
|
|
121
|
+
*/
|
|
122
|
+
function surfaceBehind(target: HTMLElement, bytes: (color: string) => Rgba): Rgba {
|
|
123
|
+
const layers: Rgba[] = [];
|
|
124
|
+
for (let el = parentOf(target); el !== null; el = parentOf(el)) {
|
|
125
|
+
const layer = bytes(window.getComputedStyle(el).backgroundColor);
|
|
126
|
+
layers.push(layer);
|
|
127
|
+
if (layer[3] === 1) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const probe = document.createElement("span");
|
|
132
|
+
probe.hidden = true;
|
|
133
|
+
probe.style.color = "Canvas";
|
|
134
|
+
document.documentElement.appendChild(probe);
|
|
135
|
+
let surface = bytes(window.getComputedStyle(probe).color);
|
|
136
|
+
probe.remove();
|
|
137
|
+
for (const layer of layers.reverse()) {
|
|
138
|
+
surface = over(layer, surface);
|
|
139
|
+
}
|
|
140
|
+
return surface;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The element above `el`, crossing out of a shadow root to its host. */
|
|
144
|
+
function parentOf(el: Element): Element | null {
|
|
145
|
+
if (el.parentElement !== null) {
|
|
146
|
+
return el.parentElement;
|
|
147
|
+
}
|
|
148
|
+
const root = el.getRootNode();
|
|
149
|
+
return root instanceof ShadowRoot ? root.host : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `top` painted over the opaque `under`. */
|
|
153
|
+
function over(top: Rgba, under: Rgba): Rgba {
|
|
154
|
+
return mix(under, top, top[3]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** `from` moved `share` of the way to `to`, as opaque bytes. */
|
|
158
|
+
function mix(from: Rgba, to: Rgba, share: number): Rgba {
|
|
159
|
+
const channel = (index: 0 | 1 | 2): number =>
|
|
160
|
+
Math.round(from[index] + (to[index] - from[index]) * share);
|
|
161
|
+
return [channel(0), channel(1), channel(2), 1];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** WCAG relative luminance of opaque sRGB bytes. */
|
|
165
|
+
function luminance([r, g, b]: Rgba): number {
|
|
166
|
+
const linear = (channel: number): number => {
|
|
167
|
+
const c = channel / 255;
|
|
168
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
169
|
+
};
|
|
170
|
+
return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** WCAG contrast ratio between two opaque colours. */
|
|
174
|
+
function contrast(a: Rgba, b: Rgba): number {
|
|
175
|
+
const [la, lb] = [luminance(a), luminance(b)];
|
|
176
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
47
177
|
}
|
|
48
178
|
|
|
49
179
|
function readToken(el: HTMLElement, name: string): string {
|
|
@@ -62,9 +192,11 @@ function readToken(el: HTMLElement, name: string): string {
|
|
|
62
192
|
* scheme, and its computed `color` is the resolved value.
|
|
63
193
|
*
|
|
64
194
|
* An element with no open shadow root (a host passing something other than a
|
|
65
|
-
* chat) is read as it is, unresolved; that is the best it offers.
|
|
195
|
+
* chat) is read as it is, unresolved; that is the best it offers. `scheme`
|
|
196
|
+
* forces one half of the pair, which is how a ring finds the chat's other
|
|
197
|
+
* accent when its own scheme's is too faint against the page.
|
|
66
198
|
*/
|
|
67
|
-
function resolveOn(chat: HTMLElement, name: string): string {
|
|
199
|
+
function resolveOn(chat: HTMLElement, name: string, scheme?: "light" | "dark"): string {
|
|
68
200
|
const value = readToken(chat, name);
|
|
69
201
|
const root = chat.shadowRoot;
|
|
70
202
|
if (value === "" || root === null) {
|
|
@@ -72,6 +204,11 @@ function resolveOn(chat: HTMLElement, name: string): string {
|
|
|
72
204
|
}
|
|
73
205
|
const probe = document.createElement("span");
|
|
74
206
|
probe.hidden = true;
|
|
207
|
+
// A scheme named here picks that half of a light-dark() pair instead of
|
|
208
|
+
// the one the chat's own scheme picks.
|
|
209
|
+
if (scheme !== undefined) {
|
|
210
|
+
probe.style.colorScheme = scheme;
|
|
211
|
+
}
|
|
75
212
|
probe.style.color = value;
|
|
76
213
|
// An unparseable value leaves the declaration empty, and the probe would
|
|
77
214
|
// then report the colour it inherits instead of saying it has none.
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
export {
|
|
4
4
|
ATTACHMENT_EVENT,
|
|
5
|
+
BAR_HEIGHT_EVENT,
|
|
5
6
|
CHART_ACTIVITY_TYPE,
|
|
6
7
|
COMPACTION_ACTIVITY_TYPE,
|
|
7
8
|
CUSTOM_AGENT_EVENT,
|
|
@@ -21,7 +22,9 @@ export {
|
|
|
21
22
|
TOGGLE_EVENT,
|
|
22
23
|
TOOL_CALL_STATUS,
|
|
23
24
|
TOOL_DISPLAY,
|
|
25
|
+
TOOL_GROUP_DISPLAY,
|
|
24
26
|
TOOL_OUTCOME,
|
|
27
|
+
type ToolGroupDisplayMode,
|
|
25
28
|
UNREAD_EVENT,
|
|
26
29
|
X_CONFIRM_KEY,
|
|
27
30
|
X_DESTRUCTIVE_KEY,
|
|
@@ -58,6 +61,7 @@ export {
|
|
|
58
61
|
} from "./core/create_http_agent.js";
|
|
59
62
|
export { defineAgUiChat } from "./core/define_ag_ui_chat.js";
|
|
60
63
|
export type { AttachmentsDetail } from "./core/events/attachments_detail.js";
|
|
64
|
+
export type { BarHeightDetail } from "./core/events/bar_height_detail.js";
|
|
61
65
|
export type { CustomAgentDetail } from "./core/events/custom_agent_detail.js";
|
|
62
66
|
export type { FeedbackDetail } from "./core/events/feedback_detail.js";
|
|
63
67
|
export type { InvalidateDetail } from "./core/events/invalidate_detail.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { X_SUMMARY_KEY } from "../constants.js";
|
|
2
|
+
import type { CollapseTarget } from "../ui/placement/surface_for.js";
|
|
2
3
|
import type { ClientTool } from "./client_tool_registry.js";
|
|
3
4
|
|
|
4
5
|
/** Every corner the panel can be sent to, in the order the schema states them. */
|
|
@@ -11,7 +12,13 @@ export type ChatCorner = (typeof CHAT_CORNERS)[number];
|
|
|
11
12
|
export interface ChatSurfaceReport {
|
|
12
13
|
/** The placement in force, or `null` when the host set none. */
|
|
13
14
|
readonly placement: string | null;
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* What the panel collapses to: `launcher`, `rail`, `header`, `bar` (the
|
|
17
|
+
* bottom bar it rises from), `none` (hidden, brought back by the host's own
|
|
18
|
+
* control), or `null` when it cannot collapse.
|
|
19
|
+
*/
|
|
20
|
+
readonly collapseTo: CollapseTarget;
|
|
21
|
+
/** Whether the panel is currently collapsed. */
|
|
15
22
|
readonly collapsed: boolean;
|
|
16
23
|
/** Whether this placement has a collapsed state at all. */
|
|
17
24
|
readonly collapsible: boolean;
|
|
@@ -166,9 +173,10 @@ export function createChatSurfaceTools(surface: ChatSurface): ClientTool[] {
|
|
|
166
173
|
{
|
|
167
174
|
name: "minimise_chat",
|
|
168
175
|
description:
|
|
169
|
-
"Collapse your own panel
|
|
170
|
-
"
|
|
171
|
-
"
|
|
176
|
+
"Collapse your own panel, so the user can see the whole page. The user reopens " +
|
|
177
|
+
"it from where it collapsed to (a launcher, a rail, a bottom bar or the page's " +
|
|
178
|
+
"own control). Answers `minimised: false` when the placement has no collapsed " +
|
|
179
|
+
"state, which is the case for a full-page chat.",
|
|
172
180
|
parameters: {
|
|
173
181
|
type: "object",
|
|
174
182
|
properties: {},
|
|
@@ -180,7 +188,7 @@ export function createChatSurfaceTools(surface: ChatSurface): ClientTool[] {
|
|
|
180
188
|
if (!report.collapsible) {
|
|
181
189
|
return {
|
|
182
190
|
minimised: false,
|
|
183
|
-
reason: "this placement has no collapsed state, so there is
|
|
191
|
+
reason: "this placement has no collapsed state, so there is nothing to minimise to",
|
|
184
192
|
};
|
|
185
193
|
}
|
|
186
194
|
surface.setCollapsed(true, { announce: true });
|
|
@@ -216,6 +216,8 @@ export class ConversationHistory {
|
|
|
216
216
|
* Also what tells a pick refused for a load from one refused for a run.
|
|
217
217
|
*/
|
|
218
218
|
#loading: (() => void) | null = null;
|
|
219
|
+
/** The thread switch under way and the promise that settles it, or `null`. */
|
|
220
|
+
#switching: { readonly threadId: string; readonly done: Promise<void> } | null = null;
|
|
219
221
|
|
|
220
222
|
constructor(host: ConversationHistoryHost) {
|
|
221
223
|
this.#host = host;
|
|
@@ -331,17 +333,56 @@ export class ConversationHistory {
|
|
|
331
333
|
this.#host.checkpoints.open();
|
|
332
334
|
}
|
|
333
335
|
|
|
334
|
-
/**
|
|
335
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Switch the active conversation to an existing thread and replay it.
|
|
338
|
+
*
|
|
339
|
+
* A call for the thread a switch is already loading is handed that switch's
|
|
340
|
+
* own promise, so it settles when the thread is drawn rather than at once,
|
|
341
|
+
* before the replay.
|
|
342
|
+
*
|
|
343
|
+
* A store that rejects the load leaves the element on the thread it was on:
|
|
344
|
+
* the switch has already cleared the transcript, so it points back at the
|
|
345
|
+
* old thread and draws it again, saying nothing, and then rejects with the
|
|
346
|
+
* store's error. Left as it was, the element named a thread it was not
|
|
347
|
+
* showing and never reported it.
|
|
348
|
+
*/
|
|
349
|
+
switchThread(threadId: string): Promise<void> {
|
|
336
350
|
if (threadId === this.#threadId) {
|
|
337
|
-
return;
|
|
351
|
+
return this.#switching?.threadId === threadId ? this.#switching.done : Promise.resolve();
|
|
338
352
|
}
|
|
353
|
+
const previous = this.#threadId;
|
|
354
|
+
const done = this.#switchTo(threadId, previous).finally(() => {
|
|
355
|
+
if (this.#switching === entry) {
|
|
356
|
+
this.#switching = null;
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
const entry = { threadId, done };
|
|
360
|
+
this.#switching = entry;
|
|
361
|
+
return done;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async #switchTo(threadId: string, previous: string): Promise<void> {
|
|
339
365
|
this.#host.cancelRun();
|
|
340
366
|
this.#host.resetState();
|
|
341
367
|
this.#host.conversationStore().setActiveThread(threadId);
|
|
342
368
|
this.#threadId = threadId;
|
|
343
369
|
this.#host.setRunning(false);
|
|
344
|
-
|
|
370
|
+
try {
|
|
371
|
+
await this.rehydrate("switch");
|
|
372
|
+
} catch (error) {
|
|
373
|
+
// Only while this switch is still the one on screen: a newer switch, a
|
|
374
|
+
// new chat or a delete has moved the thread on, and undoing that would
|
|
375
|
+
// put the old one back over it.
|
|
376
|
+
if (this.#threadId === threadId) {
|
|
377
|
+
this.#threadId = previous;
|
|
378
|
+
this.#host.conversationStore().setActiveThread(previous);
|
|
379
|
+
// Drawn again without a word: the thread on screen is the one the last
|
|
380
|
+
// event reported, so there is nothing to tell a host. A store that
|
|
381
|
+
// cannot answer for it either leaves the screen as it is.
|
|
382
|
+
await this.rehydrate(null).catch(() => undefined);
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
345
386
|
}
|
|
346
387
|
|
|
347
388
|
/** Rename a thread, then reload the list that shows its title. */
|
|
@@ -638,9 +679,10 @@ export class ConversationHistory {
|
|
|
638
679
|
* instead; see `#forgetStopped`.
|
|
639
680
|
*
|
|
640
681
|
* `reason` is what the thread event this restore ends with reports as why
|
|
641
|
-
* the conversation changed. A restore that is stopped reports nothing
|
|
682
|
+
* the conversation changed. A restore that is stopped reports nothing, and
|
|
683
|
+
* neither does one given `null`, which draws the thread already reported.
|
|
642
684
|
*/
|
|
643
|
-
async rehydrate(reason: ThreadDetail["reason"]): Promise<void> {
|
|
685
|
+
async rehydrate(reason: ThreadDetail["reason"] | null): Promise<void> {
|
|
644
686
|
// Claimed before awaiting, and checked by `#restoring` wherever the restore
|
|
645
687
|
// has waited or run host code. With a slow remote store, picking thread B
|
|
646
688
|
// then C interleaved both replays into one transcript, and New chat drew
|
|
@@ -745,7 +787,9 @@ export class ConversationHistory {
|
|
|
745
787
|
// still current: one that was stopped returned above, so two quick
|
|
746
788
|
// switches report the second alone. Before a resume, because the resume
|
|
747
789
|
// is a run, and waiting for it reported the change a whole answer late.
|
|
748
|
-
|
|
790
|
+
if (reason !== null) {
|
|
791
|
+
this.#host.threadChanged(threadId, reason);
|
|
792
|
+
}
|
|
749
793
|
if (checkpoint !== null) {
|
|
750
794
|
// A listener is host code, and can stop the restore as a replayed
|
|
751
795
|
// renderer can. Resuming then answered the call into the conversation
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Collapsing and expanding: the animation toward the launcher,
|
|
2
|
-
//
|
|
1
|
+
// Collapsing and expanding: the animation toward the launcher, the in-flow
|
|
2
|
+
// and page placements that collapse differently or not at all, and the two
|
|
3
|
+
// collapse-to targets: the bottom bar, and the host's own (hidden here).
|
|
3
4
|
export const COLLAPSE_STYLES: string = `
|
|
4
5
|
/* ── Collapse ───────────────────────────────────────────────────────────────
|
|
5
6
|
Collapsing shrinks the widget to the round floating launcher: the panel
|
|
@@ -101,12 +102,18 @@ export const COLLAPSE_STYLES: string = `
|
|
|
101
102
|
:host([collapsed][placement="embedded"]) .messages-wrap,
|
|
102
103
|
:host([collapsed][placement="embedded"]) .messages,
|
|
103
104
|
:host([collapsed][placement="embedded"]) .input-row,
|
|
105
|
+
:host([collapsed][placement="embedded"]) slot[name="above-composer"],
|
|
104
106
|
:host([collapsed][placement="embedded"]) .skill-chips,
|
|
105
107
|
:host([collapsed][placement="embedded"]) .skill-palette,
|
|
106
108
|
:host([collapsed][placement="embedded"]) .skill-hint {
|
|
107
109
|
display: none;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
/* The above-composer slot is in the list above because it is the host's content
|
|
113
|
+
against the composer: a bare slot has no box of its own, so hiding the input
|
|
114
|
+
row alone left the slotted row showing under the header bar. Its sibling slot
|
|
115
|
+
below the composer is a footer and is left to the host. */
|
|
116
|
+
|
|
110
117
|
/* The page placement has no collapsed state, so it offers no control for one.
|
|
111
118
|
A dedicated route has no "away" to go to: shrinking it left a strip of
|
|
112
119
|
application chrome fixed over a route that no longer had an owner, and this
|
|
@@ -134,4 +141,243 @@ export const COLLAPSE_STYLES: string = `
|
|
|
134
141
|
transform: none;
|
|
135
142
|
visibility: visible;
|
|
136
143
|
}
|
|
144
|
+
|
|
145
|
+
/* ── Bottom bar (collapse-to="bar") ──────────────────────────────────────────
|
|
146
|
+
The round launcher drawn as a strip along the bottom edge: the title, the
|
|
147
|
+
unread badge and a running mark, with a history button beside it. The
|
|
148
|
+
wrapper draws no box anywhere else, so the launcher lays out against the
|
|
149
|
+
host as it always has; under the bar it is fixed to the bottom of the
|
|
150
|
+
viewport, outside the host's box, which sits where the panel opens.
|
|
151
|
+
|
|
152
|
+
Here rather than with the launcher because it restyles the launcher's
|
|
153
|
+
collapsed and pressed states, which this module follows in the cascade.
|
|
154
|
+
The strip stays drawn while the panel is open on a wide viewport, as the
|
|
155
|
+
way to minimise it; the phone block hides it then. */
|
|
156
|
+
.dock-bar {
|
|
157
|
+
display: contents;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.bar-history {
|
|
161
|
+
display: none;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
:host([data-collapsed-to="bar"]) .dock-bar {
|
|
165
|
+
position: fixed;
|
|
166
|
+
inset: auto var(--_viewport-inset-right) var(--_viewport-inset-bottom) var(--_viewport-inset-left);
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
justify-content: flex-end;
|
|
170
|
+
gap: 4px;
|
|
171
|
+
box-sizing: border-box;
|
|
172
|
+
min-height: 40px;
|
|
173
|
+
padding: 4px 8px;
|
|
174
|
+
border-top: 1px solid var(--_border);
|
|
175
|
+
background: var(--_bg);
|
|
176
|
+
color: var(--_fg);
|
|
177
|
+
pointer-events: auto;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* A left-hand chat keeps its side: its controls and its panel at the left end. */
|
|
181
|
+
:host([data-collapsed-to="bar"][placement="bottom-left"]) .dock-bar {
|
|
182
|
+
justify-content: flex-start;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* The launcher as the strip's title button. Its resting state hides it behind
|
|
186
|
+
an open panel; the bar keeps it on screen, so every part of that state is
|
|
187
|
+
restated, and the hover and press scales are cancelled at the weight of the
|
|
188
|
+
rules that set them. The title truncates rather than taking a fixed width,
|
|
189
|
+
leaving room beside it for whatever a later release adds to the row. */
|
|
190
|
+
:host([data-collapsed-to="bar"]) .launcher {
|
|
191
|
+
position: static;
|
|
192
|
+
inset: auto;
|
|
193
|
+
flex: 0 1 auto;
|
|
194
|
+
justify-content: flex-start;
|
|
195
|
+
gap: 8px;
|
|
196
|
+
width: auto;
|
|
197
|
+
min-width: 0;
|
|
198
|
+
max-width: 100%;
|
|
199
|
+
height: auto;
|
|
200
|
+
min-height: 32px;
|
|
201
|
+
padding: 4px 10px;
|
|
202
|
+
border-radius: 8px;
|
|
203
|
+
background: transparent;
|
|
204
|
+
color: var(--_fg);
|
|
205
|
+
box-shadow: none;
|
|
206
|
+
touch-action: manipulation;
|
|
207
|
+
opacity: 1;
|
|
208
|
+
transform: none;
|
|
209
|
+
visibility: visible;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
:host([data-collapsed-to="bar"]) .launcher:hover {
|
|
213
|
+
background: var(--_wash);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Bare at rest, like the history button beside it. The themes frame the
|
|
217
|
+
round launcher with a border or a shadow at the weight of the rule above
|
|
218
|
+
and later in the cascade, so the child combinator on the bar is there for
|
|
219
|
+
weight, not scope: the launcher is always the bar's direct child. */
|
|
220
|
+
:host([data-collapsed-to="bar"]) .dock-bar > .launcher {
|
|
221
|
+
border: 0;
|
|
222
|
+
box-shadow: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
:host([data-collapsed-to="bar"][collapsed]) .launcher:hover,
|
|
226
|
+
:host([data-collapsed-to="bar"][collapsed]) .launcher:active {
|
|
227
|
+
transform: none;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
:host([data-collapsed-to="bar"]) .launcher .icon-holder {
|
|
231
|
+
flex: none;
|
|
232
|
+
width: 18px;
|
|
233
|
+
height: 18px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
:host([data-collapsed-to="bar"]) .rail-label {
|
|
237
|
+
display: block;
|
|
238
|
+
min-width: 0;
|
|
239
|
+
overflow: hidden;
|
|
240
|
+
font-size: 0.9em;
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
white-space: nowrap;
|
|
243
|
+
text-overflow: ellipsis;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
:host([data-collapsed-to="bar"]) .launcher-badge {
|
|
247
|
+
position: static;
|
|
248
|
+
flex: none;
|
|
249
|
+
box-shadow: none;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* The running mark: the element already reflects a run as data-running, so
|
|
253
|
+
the bar shows one without a state of its own. */
|
|
254
|
+
:host([data-collapsed-to="bar"][data-running]) .launcher::after {
|
|
255
|
+
content: "";
|
|
256
|
+
flex: none;
|
|
257
|
+
width: 8px;
|
|
258
|
+
height: 8px;
|
|
259
|
+
border-radius: 50%;
|
|
260
|
+
background: var(--_accent);
|
|
261
|
+
animation: ag-ui-dock-running 1.2s ease-in-out infinite;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@keyframes ag-ui-dock-running {
|
|
265
|
+
50% {
|
|
266
|
+
opacity: 0.35;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@media (prefers-reduced-motion: reduce) {
|
|
271
|
+
:host([data-collapsed-to="bar"][data-running]) .launcher::after {
|
|
272
|
+
animation: none;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
:host([data-collapsed-to="bar"]) .bar-history {
|
|
277
|
+
display: inline-flex;
|
|
278
|
+
flex: none;
|
|
279
|
+
align-items: center;
|
|
280
|
+
justify-content: center;
|
|
281
|
+
width: 32px;
|
|
282
|
+
height: 32px;
|
|
283
|
+
padding: 0;
|
|
284
|
+
border: 0;
|
|
285
|
+
border-radius: 8px;
|
|
286
|
+
background: transparent;
|
|
287
|
+
color: var(--_muted);
|
|
288
|
+
font: inherit;
|
|
289
|
+
cursor: pointer;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
:host([data-collapsed-to="bar"]) .bar-history:hover {
|
|
293
|
+
background: var(--_wash);
|
|
294
|
+
color: var(--_fg);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* Both declare a display of their own, so each needs its own hidden rule. */
|
|
298
|
+
:host([data-collapsed-to]) .dock-bar[hidden],
|
|
299
|
+
:host([data-collapsed-to]) .bar-history[hidden] {
|
|
300
|
+
display: none;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* History from the bar: the panel's own drawer, moved beside the bar while
|
|
304
|
+
the panel is away, drawn as a popover standing on the bar's end. There is
|
|
305
|
+
no backdrop; a press outside the bar closes it, as Escape does.
|
|
306
|
+
|
|
307
|
+
Its outer edge keeps the panel's gutter, so the popover and the panel line
|
|
308
|
+
up on the side they share. The bar is already inset by the host's reserved
|
|
309
|
+
edges, so the gutter alone is the whole offset from it, and the width cap
|
|
310
|
+
leaves the gutter free on both sides. A phone keeps a narrow margin
|
|
311
|
+
instead, in the block below: the panel there is the whole screen, so there
|
|
312
|
+
is no edge to line up with. */
|
|
313
|
+
:host([data-collapsed-to="bar"]) .dock-bar > .drawer {
|
|
314
|
+
inset: auto var(--_edge-gutter) calc(100% + 8px) auto;
|
|
315
|
+
width: min(320px, calc(var(--_viewport-width) - 2 * var(--_edge-gutter)));
|
|
316
|
+
height: min(440px, calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - 16px));
|
|
317
|
+
border-radius: var(--_panel-radius);
|
|
318
|
+
box-shadow: var(--_shadow);
|
|
319
|
+
overflow: hidden;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
:host([data-collapsed-to="bar"][placement="bottom-left"]) .dock-bar > .drawer {
|
|
323
|
+
inset: auto auto calc(100% + 8px) var(--_edge-gutter);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@media (max-width: 600px) {
|
|
327
|
+
:host([data-collapsed-to="bar"]:not([data-small-viewport="off"])) .dock-bar > .drawer {
|
|
328
|
+
inset: auto 8px calc(100% + 8px) auto;
|
|
329
|
+
width: min(320px, calc(100vw - 16px));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
:host([data-collapsed-to="bar"][placement="bottom-left"]:not([data-small-viewport="off"])) .dock-bar > .drawer {
|
|
333
|
+
inset: auto auto calc(100% + 8px) 8px;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
:host([data-collapsed-to="bar"]) .dock-bar > .drawer .drawer-backdrop {
|
|
338
|
+
display: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* The popover closes in one step. The in-panel drawer slides its panel out
|
|
342
|
+
and holds the overlay's visibility until the slide ends; here the overlay
|
|
343
|
+
is the popover's own box, rounded and clipped, so the slide moved the list
|
|
344
|
+
inside a box that stayed put (read as a shrink) and the box went only once
|
|
345
|
+
the slide had finished. This outweighs the drawer's own hidden rule, so the
|
|
346
|
+
box hides at once, and whatever the list inside does is out of sight. The
|
|
347
|
+
way in needs no rule: the drawer is moved into the bar as it opens, and a
|
|
348
|
+
moved element starts no transition. Nothing is left for reduced motion. */
|
|
349
|
+
:host([data-collapsed-to="bar"]) .dock-bar > .drawer {
|
|
350
|
+
transition: none;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* Border-box, so the list's own border sits inside the popover's clipped box
|
|
354
|
+
rather than 2px past its bottom edge. */
|
|
355
|
+
:host([data-collapsed-to="bar"]) .dock-bar > .drawer .drawer-panel {
|
|
356
|
+
box-sizing: border-box;
|
|
357
|
+
width: 100%;
|
|
358
|
+
border: 1px solid var(--_border);
|
|
359
|
+
border-radius: inherit;
|
|
360
|
+
box-shadow: none;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* ── The host's own bar (collapse-to="none") ─────────────────────────────────
|
|
364
|
+
Collapsed is hidden: no launcher, no rail, no header bar. The host's control
|
|
365
|
+
calls the public API, and the panel rises from its anchor (the dock module)
|
|
366
|
+
or from the placement's corner. Each placement's own collapsed rule is
|
|
367
|
+
overridden at its weight or above, later in the cascade. */
|
|
368
|
+
:host([data-collapsed-to="none"][collapsed]) {
|
|
369
|
+
pointer-events: none;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
:host([data-collapsed-to="none"][collapsed]) .launcher {
|
|
373
|
+
display: none;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
:host([data-collapsed-to="none"][collapsed][placement="sidebar"]) {
|
|
377
|
+
width: 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
:host([data-collapsed-to="none"][collapsed][placement="embedded"]) {
|
|
381
|
+
display: none;
|
|
382
|
+
}
|
|
137
383
|
`;
|