@artooi/ag-ui-web-component 0.33.1 → 0.35.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 +563 -1
- package/README.md +279 -16
- package/dist/ag-ui-web-component.bundle.js +676 -101
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +80 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +37 -2
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/dom/animations.d.ts +14 -0
- package/dist/dom/animations.d.ts.map +1 -1
- package/dist/dom/highlight_overlay.d.ts +47 -0
- package/dist/dom/highlight_overlay.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2288 -388
- package/dist/index.js.map +4 -4
- package/dist/tools/chat_surface_tools.d.ts +96 -0
- package/dist/tools/chat_surface_tools.d.ts.map +1 -0
- package/dist/tools/page_action_tools.d.ts +2 -0
- package/dist/tools/page_action_tools.d.ts.map +1 -1
- package/dist/ui/chart_block.d.ts +18 -0
- package/dist/ui/chart_block.d.ts.map +1 -1
- package/dist/ui/clamp_launcher.d.ts +10 -5
- package/dist/ui/clamp_launcher.d.ts.map +1 -1
- package/dist/ui/clamp_panel.d.ts +13 -0
- package/dist/ui/clamp_panel.d.ts.map +1 -0
- package/dist/ui/launcher_drag.d.ts +2 -2
- package/dist/ui/launcher_drag.d.ts.map +1 -1
- package/dist/ui/launcher_placement.d.ts +14 -1
- package/dist/ui/launcher_placement.d.ts.map +1 -1
- package/dist/ui/panel_drag.d.ts +40 -0
- package/dist/ui/panel_drag.d.ts.map +1 -0
- package/dist/ui/place_widget.d.ts +31 -0
- package/dist/ui/place_widget.d.ts.map +1 -0
- package/dist/ui/run_notice.d.ts +14 -3
- package/dist/ui/run_notice.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/thread_drawer.d.ts +21 -0
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +16 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +87 -0
- package/src/core/ag_ui_chat.ts +1146 -38
- package/src/dom/animations.ts +30 -0
- package/src/dom/highlight_overlay.ts +256 -0
- package/src/index.ts +12 -0
- package/src/tools/chat_surface_tools.ts +207 -0
- package/src/tools/page_action_tools.ts +2 -0
- package/src/ui/chart_block.ts +222 -57
- package/src/ui/clamp_launcher.ts +25 -7
- package/src/ui/clamp_panel.ts +30 -0
- package/src/ui/launcher_drag.ts +11 -2
- package/src/ui/launcher_placement.ts +50 -60
- package/src/ui/panel_drag.ts +138 -0
- package/src/ui/place_widget.ts +64 -0
- package/src/ui/run_notice.ts +32 -3
- package/src/ui/styles.ts +622 -47
- package/src/ui/thread_drawer.ts +138 -8
- package/src/ui/ui_strings.ts +24 -0
- package/src/version.ts +1 -1
package/src/ui/styles.ts
CHANGED
|
@@ -48,6 +48,11 @@ export const STYLES = `
|
|
|
48
48
|
/* Message action row: the control box and the mark inside it. The box has a
|
|
49
49
|
floor of 24px so it stays a reliable target at every density. */
|
|
50
50
|
--_action-size: var(--ag-ui-action-size, 28px);
|
|
51
|
+
/* The header's own controls. Sized rather than left to their contents: a row
|
|
52
|
+
of buttons each as wide as the glyph inside it comes out uneven, and uneven
|
|
53
|
+
buttons two pixels apart read as one smudge rather than five controls. */
|
|
54
|
+
--_header-btn-size: var(--ag-ui-header-btn-size, 30px);
|
|
55
|
+
--_header-gap: var(--ag-ui-header-gap, 4px);
|
|
51
56
|
--_action-icon-size: var(--ag-ui-action-icon-size, 15px);
|
|
52
57
|
--_tooltip-bg: var(--ag-ui-tooltip-bg, #1f2430);
|
|
53
58
|
--_tooltip-fg: var(--ag-ui-tooltip-fg, #f5f6fa);
|
|
@@ -109,6 +114,11 @@ export const STYLES = `
|
|
|
109
114
|
--_glyph-size: var(--ag-ui-glyph-size, 18px);
|
|
110
115
|
--_glyph-stroke: var(--ag-ui-glyph-stroke, 1.75);
|
|
111
116
|
|
|
117
|
+
/* How wide a chart is allowed to get. A cap rather than a width: below it
|
|
118
|
+
the chart fills its column, and above it a wider panel is just a wider
|
|
119
|
+
panel. */
|
|
120
|
+
--_chart-max-width: var(--ag-ui-chart-max-width, 480px);
|
|
121
|
+
|
|
112
122
|
/* Unread badge on the launcher. */
|
|
113
123
|
--_badge-bg: var(--ag-ui-badge-bg, var(--_danger));
|
|
114
124
|
--_badge-fg: var(--ag-ui-badge-fg, #ffffff);
|
|
@@ -137,6 +147,68 @@ export const STYLES = `
|
|
|
137
147
|
--_msg-pad: var(--ag-ui-msg-pad, 8px 12px);
|
|
138
148
|
--_msg-radius: var(--ag-ui-msg-radius, 14px);
|
|
139
149
|
|
|
150
|
+
/* Edges of the viewport the host has already spent, and that a fixed
|
|
151
|
+
placement must therefore stay out of: a sticky nav bar, a docked toolbar,
|
|
152
|
+
a device's safe area. Four longhands rather than one shorthand because a
|
|
153
|
+
custom property is a token stream and CSS cannot index one -- the height
|
|
154
|
+
arithmetic below needs the vertical pair on its own.
|
|
155
|
+
|
|
156
|
+
Every fixed placement derives from these, and the heights subtract them in
|
|
157
|
+
one place. A host that reserved its chrome by restating --ag-ui-inset per
|
|
158
|
+
placement had to keep --ag-ui-height in step by hand, and forgetting it
|
|
159
|
+
overflowed the panel past the bottom of the screen with nothing to say so.
|
|
160
|
+
|
|
161
|
+
These take env(safe-area-inset-*) verbatim, which is what a full-bleed
|
|
162
|
+
placement wants on a device with a notch. */
|
|
163
|
+
--_viewport-inset-top: var(--ag-ui-viewport-inset-top, 0px);
|
|
164
|
+
--_viewport-inset-right: var(--ag-ui-viewport-inset-right, 0px);
|
|
165
|
+
--_viewport-inset-bottom: var(--ag-ui-viewport-inset-bottom, 0px);
|
|
166
|
+
--_viewport-inset-left: var(--ag-ui-viewport-inset-left, 0px);
|
|
167
|
+
/* What is left of the viewport once the host's chrome is taken out, on both
|
|
168
|
+
axes, so a placement never composes this itself and no host can subtract
|
|
169
|
+
an edge from the position and forget it in the height.
|
|
170
|
+
|
|
171
|
+
Settable in their own right, and that is not only convention: an on-screen
|
|
172
|
+
keyboard changes no viewport-percentage length -- not vh, not dvh, not svh
|
|
173
|
+
-- so a full-bleed panel on a phone has to be told the height rather than
|
|
174
|
+
deriving it. The value to publish there is the visual viewport's. */
|
|
175
|
+
--_viewport-height: var(--ag-ui-viewport-height, var(--_visual-viewport-height));
|
|
176
|
+
/* The measured height of the part of the screen the user can actually see,
|
|
177
|
+
written by the element from the visual viewport and falling back to the
|
|
178
|
+
layout viewport where nothing has measured yet.
|
|
179
|
+
|
|
180
|
+
This is the on-screen keyboard, and it needs measuring because no CSS
|
|
181
|
+
length describes it: an on-screen keyboard has no effect on any
|
|
182
|
+
viewport-percentage unit, so 100vh, 100dvh and 100svh are all the same
|
|
183
|
+
number with the keyboard up as without it. A full-bleed panel sized from
|
|
184
|
+
any of them puts its composer behind the keyboard the user is typing into.
|
|
185
|
+
|
|
186
|
+
Separate from the token above so a host that states the usable height
|
|
187
|
+
outright still wins: the element writes this one inline, and an inline
|
|
188
|
+
value would otherwise outrank the host's own rule. */
|
|
189
|
+
--_visual-viewport-height: var(
|
|
190
|
+
--ag-ui-visual-viewport-height,
|
|
191
|
+
calc(100vh - var(--_viewport-inset-top) - var(--_viewport-inset-bottom))
|
|
192
|
+
);
|
|
193
|
+
/* How much of the layout viewport is hidden below the visible one, measured
|
|
194
|
+
and written by the element alongside the height above.
|
|
195
|
+
|
|
196
|
+
A shorter panel is not enough on its own for anything anchored to the
|
|
197
|
+
bottom. A floating widget is positioned against the layout viewport, so
|
|
198
|
+
with a keyboard up its bottom edge -- and the launcher that lives at that
|
|
199
|
+
corner -- sits behind the keyboard however tall the panel is. This is what
|
|
200
|
+
lifts it clear. */
|
|
201
|
+
/* Two tokens, for the same reason the height above has two: the element
|
|
202
|
+
writes the measurement inline, and a host that states its own value needs
|
|
203
|
+
a knob that outranks that write rather than one the next write replaces.
|
|
204
|
+
A host wanting no keyboard lift at all sets --ag-ui-keyboard-inset: 0px. */
|
|
205
|
+
--_keyboard-inset: var(--ag-ui-keyboard-inset, var(--_visual-viewport-inset-bottom));
|
|
206
|
+
--_visual-viewport-inset-bottom: var(--ag-ui-visual-viewport-inset-bottom, 0px);
|
|
207
|
+
--_viewport-width: var(
|
|
208
|
+
--ag-ui-viewport-width,
|
|
209
|
+
calc(100vw - var(--_viewport-inset-left) - var(--_viewport-inset-right))
|
|
210
|
+
);
|
|
211
|
+
|
|
140
212
|
/* Layout — override from outside to dock the widget anywhere.
|
|
141
213
|
Set --ag-ui-position: static (and place this element in your own
|
|
142
214
|
grid/flex layout) to embed it in the page flow instead of floating. */
|
|
@@ -144,14 +216,49 @@ export const STYLES = `
|
|
|
144
216
|
--_z-index: var(--ag-ui-z-index, 2147483000);
|
|
145
217
|
--_width: var(--ag-ui-width, 380px);
|
|
146
218
|
--_height: var(--ag-ui-height, 560px);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
219
|
+
/* The gutter a resting floating panel keeps between itself and the edge of
|
|
220
|
+
the box the host left free. One number, because the inset below spends it
|
|
221
|
+
and the cap beneath has to know it was spent. */
|
|
222
|
+
--_edge-gutter: var(--ag-ui-edge-gutter, 24px);
|
|
223
|
+
--_inset: var(
|
|
224
|
+
--ag-ui-inset,
|
|
225
|
+
auto calc(var(--_edge-gutter) + var(--_viewport-inset-right))
|
|
226
|
+
calc(var(--_edge-gutter) + var(--_viewport-inset-bottom) + var(--_keyboard-inset)) auto
|
|
227
|
+
);
|
|
228
|
+
/* The cap is what the host left free, less the *one* gutter the inset above
|
|
229
|
+
spends on the anchored edge -- not two, and not none.
|
|
230
|
+
|
|
231
|
+
Two was the first answer and it was felt on one axis only. The arithmetic
|
|
232
|
+
says why: the default panel is 560 tall against a cap of the viewport
|
|
233
|
+
minus 48, which on an 800px screen with a header reserved is 72px of
|
|
234
|
+
headroom, so the height reaches its cap almost immediately; the default
|
|
235
|
+
width is 380 against a cap near 1230, which is 850px nothing ever reaches.
|
|
236
|
+
Once the size is capped a grip cannot grow the panel, so a pull on the
|
|
237
|
+
anchored edge is written as position instead and the panel travels, and a
|
|
238
|
+
pull on the free edge stops a whole gutter short of an edge a drag can
|
|
239
|
+
reach.
|
|
240
|
+
|
|
241
|
+
None was the correction, and it overshot: the panel is anchored bottom-
|
|
242
|
+
right with a gutter already spent there, so a cap of the full usable
|
|
243
|
+
height puts the far edge exactly one gutter outside it. On an 800px screen
|
|
244
|
+
with nothing reserved that is a top of -24 -- the header, and every
|
|
245
|
+
control in it, off the top of the window. Reserve a 120px header and it
|
|
246
|
+
sits inside that instead, which is the one thing the reservation exists to
|
|
247
|
+
prevent.
|
|
248
|
+
|
|
249
|
+
One is the fixed point of both complaints. A resting panel grown to the
|
|
250
|
+
cap runs from the usable near edge to its gutter on the far one, so the
|
|
251
|
+
near edge is reachable by a resize exactly as it is by a drag, and neither
|
|
252
|
+
can put any part of the panel outside the box. */
|
|
253
|
+
--_max-width: var(--ag-ui-max-width, calc(var(--_viewport-width) - var(--_edge-gutter)));
|
|
254
|
+
--_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_edge-gutter)));
|
|
150
255
|
/* Reading-column width for placement="page" (full-bleed, centred content). */
|
|
151
256
|
--_content-max-width: var(--ag-ui-content-max-width, 820px);
|
|
152
257
|
/* Slim rail the sidebar placement collapses to. Only that placement reads
|
|
153
258
|
it, but it is declared here so every alias has a default in one place. */
|
|
154
259
|
--_rail-width: var(--ag-ui-rail-width, 52px);
|
|
260
|
+
/* Width of the docked conversation list on a full-page chat. */
|
|
261
|
+
--_threads-rail-width: var(--ag-ui-threads-rail-width, 280px);
|
|
155
262
|
|
|
156
263
|
position: var(--_position);
|
|
157
264
|
inset: var(--_inset);
|
|
@@ -222,23 +329,30 @@ export const STYLES = `
|
|
|
222
329
|
|
|
223
330
|
/* ── Placement presets ──────────────────────────────────────────────────── */
|
|
224
331
|
:host([placement="bottom-left"]) {
|
|
225
|
-
--_inset: var(
|
|
332
|
+
--_inset: var(
|
|
333
|
+
--ag-ui-inset,
|
|
334
|
+
auto auto calc(var(--_edge-gutter) + var(--_viewport-inset-bottom) + var(--_keyboard-inset))
|
|
335
|
+
calc(var(--_edge-gutter) + var(--_viewport-inset-left))
|
|
336
|
+
);
|
|
226
337
|
}
|
|
227
338
|
|
|
228
339
|
:host([placement="side"]) {
|
|
229
|
-
--_inset: var(--ag-ui-inset,
|
|
340
|
+
--_inset: var(--ag-ui-inset, var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
230
341
|
--_width: var(--ag-ui-width, 420px);
|
|
231
|
-
--_height: var(--ag-ui-height,
|
|
232
|
-
--_max-height: var(--ag-ui-max-height,
|
|
342
|
+
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
343
|
+
--_max-height: var(--ag-ui-max-height, var(--_viewport-height));
|
|
233
344
|
--_radius: var(--ag-ui-radius, 0);
|
|
234
345
|
}
|
|
235
346
|
|
|
236
347
|
:host([placement="full"]) {
|
|
237
|
-
--_inset: var(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
--
|
|
348
|
+
--_inset: var(
|
|
349
|
+
--ag-ui-inset,
|
|
350
|
+
var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) var(--_viewport-inset-left)
|
|
351
|
+
);
|
|
352
|
+
--_width: var(--ag-ui-width, var(--_viewport-width));
|
|
353
|
+
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
354
|
+
--_max-width: var(--ag-ui-max-width, var(--_viewport-width));
|
|
355
|
+
--_max-height: var(--ag-ui-max-height, var(--_viewport-height));
|
|
242
356
|
--_radius: var(--ag-ui-radius, 0);
|
|
243
357
|
}
|
|
244
358
|
|
|
@@ -248,11 +362,14 @@ export const STYLES = `
|
|
|
248
362
|
composer rather than a per-row wrapper, so user pills still right-align and
|
|
249
363
|
the assistant well spans the column. */
|
|
250
364
|
:host([placement="page"]) {
|
|
251
|
-
--_inset: var(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
--
|
|
365
|
+
--_inset: var(
|
|
366
|
+
--ag-ui-inset,
|
|
367
|
+
var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) var(--_viewport-inset-left)
|
|
368
|
+
);
|
|
369
|
+
--_width: var(--ag-ui-width, var(--_viewport-width));
|
|
370
|
+
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
371
|
+
--_max-width: var(--ag-ui-max-width, var(--_viewport-width));
|
|
372
|
+
--_max-height: var(--ag-ui-max-height, var(--_viewport-height));
|
|
256
373
|
--_radius: var(--ag-ui-radius, 0);
|
|
257
374
|
}
|
|
258
375
|
|
|
@@ -283,28 +400,100 @@ export const STYLES = `
|
|
|
283
400
|
max-width: 100%;
|
|
284
401
|
}
|
|
285
402
|
|
|
403
|
+
/* Small viewports: one shape, reached from whichever placement the host chose.
|
|
404
|
+
|
|
405
|
+
A phone is not an eighth placement, it is an override that collapses the
|
|
406
|
+
others onto one of them. The host picked a placement for the desktop it was
|
|
407
|
+
designing; a 380x560 panel with a 24px margin is not a smaller version of
|
|
408
|
+
that decision, it is most of the screen with a frame drawn round it.
|
|
409
|
+
|
|
410
|
+
The breakpoint is a literal because a custom property cannot be read in a
|
|
411
|
+
media query -- which is exactly why there is an opt-out. Everything the block
|
|
412
|
+
sets is a token a host can re-state, but the *trigger* is unreachable, so a
|
|
413
|
+
host whose layout wants a different threshold, or none, sets
|
|
414
|
+
data-small-viewport="off" and keeps its desktop shape at every width. 600px is above every common phone in portrait and below every
|
|
415
|
+
tablet in landscape, and it is a width rather than a pointer test on purpose:
|
|
416
|
+
a touch laptop is coarse-pointered and wide, a narrow desktop window is
|
|
417
|
+
fine-pointered and small, and conflating the two gets both wrong. What the
|
|
418
|
+
pointer decides is which controls make sense, further down.
|
|
419
|
+
|
|
420
|
+
Two placements are left alone. "page" is already this shape. "embedded" sits
|
|
421
|
+
in a box the host sized and placed, and taking that over would break the app
|
|
422
|
+
shell it was embedded into -- the host is the only party that knows whether
|
|
423
|
+
its column should become the whole screen. */
|
|
424
|
+
@media (max-width: 600px) {
|
|
425
|
+
:host([placement="floating"]:not([data-small-viewport="off"])),
|
|
426
|
+
:host([placement="bottom-left"]:not([data-small-viewport="off"])),
|
|
427
|
+
:host([placement="sidebar"]:not([data-small-viewport="off"])),
|
|
428
|
+
:host([placement="side"]:not([data-small-viewport="off"])),
|
|
429
|
+
:host(:not([placement]):not([data-small-viewport="off"])),
|
|
430
|
+
:host([placement=""]:not([data-small-viewport="off"])) {
|
|
431
|
+
--_inset: var(
|
|
432
|
+
--ag-ui-inset,
|
|
433
|
+
var(--_viewport-inset-top) var(--_viewport-inset-right)
|
|
434
|
+
calc(var(--_viewport-inset-bottom) + var(--_keyboard-inset))
|
|
435
|
+
var(--_viewport-inset-left)
|
|
436
|
+
);
|
|
437
|
+
--_width: var(--ag-ui-width, var(--_viewport-width));
|
|
438
|
+
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
439
|
+
--_max-width: var(--ag-ui-max-width, var(--_viewport-width));
|
|
440
|
+
--_max-height: var(--ag-ui-max-height, var(--_viewport-height));
|
|
441
|
+
--_radius: var(--ag-ui-radius, 0);
|
|
442
|
+
--_shadow: var(--ag-ui-shadow, none);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* Nothing to resize once the panel is the screen, and the grips would sit
|
|
446
|
+
under the thumbs holding the phone. */
|
|
447
|
+
:host([placement="floating"]:not([data-small-viewport="off"])) .resize-handle,
|
|
448
|
+
:host([placement="bottom-left"]:not([data-small-viewport="off"])) .resize-handle,
|
|
449
|
+
:host([placement="sidebar"]:not([data-small-viewport="off"])) .resize-handle,
|
|
450
|
+
:host([placement="side"]:not([data-small-viewport="off"])) .resize-handle,
|
|
451
|
+
:host(:not([placement]):not([data-small-viewport="off"])) .resize-handle,
|
|
452
|
+
:host([placement=""]:not([data-small-viewport="off"])) .resize-handle {
|
|
453
|
+
display: none;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
286
457
|
/* Sidebar: a full-height docked panel that slides open/closed and
|
|
287
458
|
collapses to a slim icon rail (not the floating launcher). Docked right by
|
|
288
459
|
default; data-side="left" docks it left. Overlay by default — set
|
|
289
460
|
--ag-ui-position: static (and place this element in your own layout) for a
|
|
290
461
|
host-managed push instead. */
|
|
291
462
|
:host([placement="sidebar"]) {
|
|
292
|
-
--_inset: var(--ag-ui-inset,
|
|
463
|
+
--_inset: var(--ag-ui-inset, var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
293
464
|
--_width: var(--ag-ui-width, 420px);
|
|
294
|
-
--_height: var(--ag-ui-height,
|
|
295
|
-
--_max-height: var(--ag-ui-max-height,
|
|
465
|
+
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
466
|
+
--_max-height: var(--ag-ui-max-height, var(--_viewport-height));
|
|
296
467
|
--_radius: var(--ag-ui-radius, 0);
|
|
297
468
|
transition: width var(--_motion) var(--_ease);
|
|
298
469
|
}
|
|
299
470
|
|
|
300
471
|
:host([placement="sidebar"][data-side="left"]) {
|
|
301
|
-
--_inset: var(--ag-ui-inset,
|
|
472
|
+
--_inset: var(--ag-ui-inset, var(--_viewport-inset-top) auto var(--_viewport-inset-bottom) var(--_viewport-inset-left));
|
|
302
473
|
}
|
|
303
474
|
|
|
304
475
|
/* The docked panel is pinned to the edge it docks against rather than filling
|
|
305
476
|
the host as a flex child. Collapsing shrinks the host to the rail width, and
|
|
306
477
|
a flex child would be squashed to 52px on the way out instead of sliding out
|
|
307
478
|
at full width. */
|
|
479
|
+
/* The panel is taken out of flow so the collapse can slide it out at full
|
|
480
|
+
width; see the note above. That makes the host its containing block, and the
|
|
481
|
+
host is only one by accident: it is position: fixed by default. A host that
|
|
482
|
+
takes the documented route to a pushed layout instead of an overlay sets
|
|
483
|
+
--ag-ui-position: static, and a static element establishes nothing -- so the
|
|
484
|
+
panel resolved against the initial containing block, landed at the document
|
|
485
|
+
origin, and scrolled away with the page. Docked left it pinned to the
|
|
486
|
+
document's left edge rather than the host's.
|
|
487
|
+
|
|
488
|
+
It looked correct wherever it was first tried, because a full-height column
|
|
489
|
+
at the top of an unscrolled document is exactly where those two answers
|
|
490
|
+
coincide. Containment makes the host a containing block whatever its
|
|
491
|
+
position, so the panel stays in the box the host was given. It is a no-op in
|
|
492
|
+
the default case, which is already positioned. */
|
|
493
|
+
:host([placement="sidebar"]) {
|
|
494
|
+
contain: layout;
|
|
495
|
+
}
|
|
496
|
+
|
|
308
497
|
:host([placement="sidebar"]) .chat {
|
|
309
498
|
position: absolute;
|
|
310
499
|
inset: 0 0 0 auto;
|
|
@@ -320,11 +509,15 @@ export const STYLES = `
|
|
|
320
509
|
/* Collapsed sidebar: shrink the host to the rail width and slide the panel out
|
|
321
510
|
through the edge it docks against. Higher specificity than the generic
|
|
322
511
|
collapse rules, so it wins regardless of source order. */
|
|
512
|
+
/* The rail is the full height of what the host left, not of the screen. It
|
|
513
|
+
said 100vh and pinned its own bottom, which put it under any chrome the host
|
|
514
|
+
had reserved -- and the icon lives at the top of the rail, so the one control
|
|
515
|
+
that reopens the panel was the first thing to disappear behind a sticky
|
|
516
|
+
header. */
|
|
323
517
|
:host([placement="sidebar"][collapsed]) {
|
|
324
518
|
width: var(--_rail-width);
|
|
325
|
-
height:
|
|
326
|
-
max-height:
|
|
327
|
-
bottom: 0;
|
|
519
|
+
height: var(--_viewport-height);
|
|
520
|
+
max-height: var(--_viewport-height);
|
|
328
521
|
pointer-events: auto;
|
|
329
522
|
}
|
|
330
523
|
|
|
@@ -440,17 +633,83 @@ export const STYLES = `
|
|
|
440
633
|
|
|
441
634
|
/* The sidebar collapses to an edge rail instead: full height, square, flush
|
|
442
635
|
against the dock. It slides in with the panel rather than popping. */
|
|
636
|
+
/* The edge rail. It reads as the docked edge of a panel rather than a coloured
|
|
637
|
+
stripe: the surface the panel is made of, a border on the side it docks
|
|
638
|
+
against, and the accent kept for the icon -- a full-height slab of accent is
|
|
639
|
+
the loudest thing on the page and says the least about itself.
|
|
640
|
+
|
|
641
|
+
Content sits at the top rather than centred, because a control floating in
|
|
642
|
+
the middle of a screen-high column has nothing to belong to. */
|
|
443
643
|
:host([placement="sidebar"][collapsed]) .launcher {
|
|
444
644
|
inset: 0;
|
|
445
645
|
width: auto;
|
|
446
646
|
height: auto;
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
647
|
+
flex-direction: column;
|
|
648
|
+
align-items: center;
|
|
649
|
+
justify-content: flex-start;
|
|
650
|
+
gap: 12px;
|
|
651
|
+
padding-top: 14px;
|
|
652
|
+
border: 0;
|
|
653
|
+
border-inline-start: 1px solid var(--_border);
|
|
450
654
|
border-radius: 0;
|
|
655
|
+
background: var(--_bg);
|
|
656
|
+
color: var(--_fg);
|
|
657
|
+
box-shadow: none;
|
|
658
|
+
transform: none;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
:host([placement="sidebar"][data-side="left"][collapsed]) .launcher {
|
|
662
|
+
border-inline-start: 0;
|
|
663
|
+
border-inline-end: 1px solid var(--_border);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/* The icon keeps the accent, so there is one obvious thing to press.
|
|
667
|
+
|
|
668
|
+
The glyph is sized here rather than left to fill the holder. A glyph in an
|
|
669
|
+
icon holder takes the holder's whole box by default, which is right where the
|
|
670
|
+
holder is only a box and wrong the moment it becomes a filled circle: the
|
|
671
|
+
mark then runs edge to edge and reads as a square crammed into a disc. The
|
|
672
|
+
proportion is the floating launcher's own -- a 26px glyph in a 56px bubble --
|
|
673
|
+
so the two collapsed states look like the same widget. */
|
|
674
|
+
:host([placement="sidebar"][collapsed]) .launcher .icon-holder {
|
|
675
|
+
display: inline-flex;
|
|
676
|
+
align-items: center;
|
|
677
|
+
justify-content: center;
|
|
678
|
+
width: 34px;
|
|
679
|
+
height: 34px;
|
|
680
|
+
border-radius: 50%;
|
|
451
681
|
background: var(--_header-bg);
|
|
452
682
|
color: var(--_header-fg);
|
|
453
|
-
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
:host([placement="sidebar"][collapsed]) .launcher .icon-holder .glyph,
|
|
686
|
+
:host([placement="sidebar"][collapsed]) .launcher .icon-holder .icon-img {
|
|
687
|
+
width: 16px;
|
|
688
|
+
height: 16px;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/* Written down the rail, which is the only direction it fits. Reading upward
|
|
692
|
+
is the convention for a right-hand edge and matches how a docked panel's
|
|
693
|
+
label is set everywhere it appears. */
|
|
694
|
+
.rail-label {
|
|
695
|
+
display: none;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
:host([placement="sidebar"][collapsed]) .rail-label {
|
|
699
|
+
display: block;
|
|
700
|
+
writing-mode: vertical-rl;
|
|
701
|
+
transform: rotate(180deg);
|
|
702
|
+
max-height: calc(100% - 96px);
|
|
703
|
+
overflow: hidden;
|
|
704
|
+
font-size: 0.9em;
|
|
705
|
+
font-weight: 600;
|
|
706
|
+
letter-spacing: 0.02em;
|
|
707
|
+
white-space: nowrap;
|
|
708
|
+
text-overflow: ellipsis;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
:host([placement="sidebar"][data-side="left"][collapsed]) .rail-label {
|
|
712
|
+
writing-mode: vertical-rl;
|
|
454
713
|
transform: none;
|
|
455
714
|
}
|
|
456
715
|
|
|
@@ -499,6 +758,41 @@ export const STYLES = `
|
|
|
499
758
|
color: var(--_header-fg);
|
|
500
759
|
}
|
|
501
760
|
|
|
761
|
+
/* The header is the panel's title bar: on the placements that let the widget
|
|
762
|
+
be moved it drags the whole thing, matching the launcher's own drag. The
|
|
763
|
+
selector lists those placements rather than asking JS to stamp an attribute,
|
|
764
|
+
because a cursor that appears one frame late reads as a control that only
|
|
765
|
+
sometimes exists. touch-action keeps a finger drag here moving the panel
|
|
766
|
+
instead of scrolling the page behind it. */
|
|
767
|
+
:host(:not([placement])) .header,
|
|
768
|
+
:host([placement=""]) .header,
|
|
769
|
+
:host([placement="floating"]) .header,
|
|
770
|
+
:host([placement="bottom-left"]) .header {
|
|
771
|
+
cursor: move;
|
|
772
|
+
touch-action: none;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/* A host that turned the drag off keeps a header that says so. */
|
|
776
|
+
:host([data-launcher-drag="false"]) .header {
|
|
777
|
+
cursor: auto;
|
|
778
|
+
touch-action: auto;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/* :host is carried for the specificity, not the scope: the placement rules
|
|
782
|
+
above are a host selector plus a class plus an attribute, and a bare
|
|
783
|
+
.header[data-dragging] loses to them -- silently, because the drag still
|
|
784
|
+
works and only the cursor is wrong. */
|
|
785
|
+
:host .header[data-dragging] {
|
|
786
|
+
cursor: grabbing;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/* The cursor inherits, so a control a host slots into the header would show
|
|
790
|
+
the drag cursor over something the drag deliberately ignores. Named to
|
|
791
|
+
match the controls panel_drag steps aside for. */
|
|
792
|
+
.header ::slotted(:is(button, a, input, select, textarea)) {
|
|
793
|
+
cursor: pointer;
|
|
794
|
+
}
|
|
795
|
+
|
|
502
796
|
.header-title {
|
|
503
797
|
flex: 1;
|
|
504
798
|
min-width: 0;
|
|
@@ -529,17 +823,24 @@ export const STYLES = `
|
|
|
529
823
|
|
|
530
824
|
.header-controls {
|
|
531
825
|
display: flex;
|
|
532
|
-
gap:
|
|
826
|
+
gap: var(--_header-gap);
|
|
533
827
|
flex: none;
|
|
534
828
|
}
|
|
535
829
|
|
|
536
830
|
.header-btn {
|
|
831
|
+
display: inline-flex;
|
|
832
|
+
flex: 0 0 auto;
|
|
833
|
+
align-items: center;
|
|
834
|
+
justify-content: center;
|
|
835
|
+
box-sizing: border-box;
|
|
836
|
+
width: var(--_header-btn-size);
|
|
837
|
+
height: var(--_header-btn-size);
|
|
537
838
|
border: none;
|
|
538
839
|
background: transparent;
|
|
539
840
|
color: inherit;
|
|
540
841
|
font: inherit;
|
|
541
842
|
line-height: 1;
|
|
542
|
-
padding:
|
|
843
|
+
padding: 0;
|
|
543
844
|
border-radius: 6px;
|
|
544
845
|
cursor: pointer;
|
|
545
846
|
opacity: 0.85;
|
|
@@ -561,9 +862,9 @@ export const STYLES = `
|
|
|
561
862
|
Nothing paints there once the panel is gone, so the box only has to stop
|
|
562
863
|
swallowing clicks: pointer events go to none and the launcher takes them.
|
|
563
864
|
|
|
564
|
-
Two placements
|
|
565
|
-
|
|
566
|
-
|
|
865
|
+
Two placements do not use it: "sidebar" slides to its rail (below), and
|
|
866
|
+
"embedded" keeps its header bar, having no corner for a floating circle that
|
|
867
|
+
would escape the host's layout. "page" has no collapsed state at all. */
|
|
567
868
|
:host([collapsed]) {
|
|
568
869
|
pointer-events: none;
|
|
569
870
|
/* A collapsed host has to be allowed to shrink, and in the layout hosts
|
|
@@ -625,37 +926,107 @@ export const STYLES = `
|
|
|
625
926
|
transform-origin: bottom right;
|
|
626
927
|
}
|
|
627
928
|
|
|
628
|
-
/* The
|
|
629
|
-
|
|
630
|
-
:host([collapsed]
|
|
929
|
+
/* The in-flow placement keeps the original collapse: hide the body, let the
|
|
930
|
+
host shrink to the header bar. */
|
|
931
|
+
:host([collapsed][placement="embedded"]) {
|
|
631
932
|
height: auto;
|
|
632
933
|
max-height: none;
|
|
633
934
|
pointer-events: auto;
|
|
634
935
|
}
|
|
635
936
|
|
|
636
|
-
:host([collapsed]
|
|
937
|
+
:host([collapsed][placement="embedded"]) .chat {
|
|
637
938
|
opacity: 1;
|
|
638
939
|
transform: none;
|
|
639
940
|
visibility: visible;
|
|
640
941
|
}
|
|
641
942
|
|
|
642
|
-
/*
|
|
943
|
+
/* It keeps the header bar, so the launcher must stay out of the way: an
|
|
643
944
|
embedded host is position: static, which would let an absolutely-positioned
|
|
644
945
|
circle escape the layout and land against whatever the page positions. */
|
|
645
|
-
:host([collapsed]
|
|
946
|
+
:host([collapsed][placement="embedded"]) .launcher {
|
|
646
947
|
visibility: hidden;
|
|
647
948
|
opacity: 0;
|
|
648
949
|
}
|
|
649
950
|
|
|
650
|
-
:host([collapsed]
|
|
651
|
-
:host([collapsed]
|
|
652
|
-
:host([collapsed]
|
|
653
|
-
:host([collapsed]
|
|
654
|
-
:host([collapsed]
|
|
655
|
-
:host([collapsed]
|
|
951
|
+
:host([collapsed][placement="embedded"]) .messages-wrap,
|
|
952
|
+
:host([collapsed][placement="embedded"]) .messages,
|
|
953
|
+
:host([collapsed][placement="embedded"]) .input-row,
|
|
954
|
+
:host([collapsed][placement="embedded"]) .skill-chips,
|
|
955
|
+
:host([collapsed][placement="embedded"]) .skill-palette,
|
|
956
|
+
:host([collapsed][placement="embedded"]) .skill-hint {
|
|
957
|
+
display: none;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/* A docked conversation list: beside the transcript rather than over it.
|
|
961
|
+
|
|
962
|
+
A full-page chat is the one surface with width to spare, and covering the
|
|
963
|
+
conversation to show the list of conversations is the wrong trade there --
|
|
964
|
+
it hides the thing you are trying to get back to. Everywhere else the panel
|
|
965
|
+
is a few hundred pixels wide and a docked list would leave a column of
|
|
966
|
+
transcript narrower than the messages in it, so this is the only placement
|
|
967
|
+
that gets it.
|
|
968
|
+
|
|
969
|
+
The transcript is moved by padding on the shell rather than by making the
|
|
970
|
+
list a flex sibling: the drawer is the last child of the panel, and no
|
|
971
|
+
selector reaches backwards from it to the rows that have to shift. That is
|
|
972
|
+
also why the state is stamped on the host. */
|
|
973
|
+
:host([data-threads-docked]) .drawer {
|
|
974
|
+
/* Not a scrim: the page behind it is still the user's to work in. */
|
|
975
|
+
pointer-events: none;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
:host([data-threads-docked]) .drawer-backdrop {
|
|
979
|
+
display: none;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
:host([data-threads-docked]) .drawer-panel {
|
|
983
|
+
width: var(--_threads-rail-width);
|
|
984
|
+
pointer-events: auto;
|
|
985
|
+
border-inline-end: 1px solid var(--_border);
|
|
986
|
+
box-shadow: none;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
:host([data-threads-docked]) .messages,
|
|
990
|
+
:host([data-threads-docked]) .input-row,
|
|
991
|
+
:host([data-threads-docked]) .skill-chips,
|
|
992
|
+
:host([data-threads-docked]) .attachment-tray {
|
|
993
|
+
padding-inline-start: calc(
|
|
994
|
+
var(--_threads-rail-width) + max(var(--_pad), (100% - var(--_content-max-width)) / 2)
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
:host([data-threads-docked]) .header {
|
|
999
|
+
padding-inline-start: calc(var(--_threads-rail-width) + var(--_pad));
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/* The page placement has no collapsed state, so it offers no control for one.
|
|
1003
|
+
A dedicated route has no "away" to go to: shrinking it left a strip of
|
|
1004
|
+
application chrome fixed over a route that no longer had an owner, and this
|
|
1005
|
+
is the one placement where the launcher that would bring it back is hidden.
|
|
1006
|
+
|
|
1007
|
+
Hiding the control is only half of it. The state has another way in -- the
|
|
1008
|
+
attribute can be written straight onto the element, and a value stored under
|
|
1009
|
+
a different placement is restored on connect -- and the JS guards that catch
|
|
1010
|
+
those cannot see an attribute set directly. So the placement neutralises the
|
|
1011
|
+
state here as well: with the collapse rules above no longer naming "page", an
|
|
1012
|
+
unguarded collapsed attribute would otherwise fall through to the generic
|
|
1013
|
+
rule that scales the panel away and drops pointer events, leaving nothing on
|
|
1014
|
+
screen and no launcher to press. */
|
|
1015
|
+
:host([placement="page"]) .header-btn--collapse {
|
|
656
1016
|
display: none;
|
|
657
1017
|
}
|
|
658
1018
|
|
|
1019
|
+
:host([placement="page"][collapsed]) {
|
|
1020
|
+
pointer-events: auto;
|
|
1021
|
+
align-self: auto;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
:host([placement="page"][collapsed]) .chat {
|
|
1025
|
+
opacity: 1;
|
|
1026
|
+
transform: none;
|
|
1027
|
+
visibility: visible;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
659
1030
|
/* Jump-to-latest: shown only once the reader has scrolled away *and* missed
|
|
660
1031
|
something. Anchored to the panel rather than the list so it does not scroll
|
|
661
1032
|
with the content it is offering to scroll to. */
|
|
@@ -743,6 +1114,28 @@ export const STYLES = `
|
|
|
743
1114
|
|
|
744
1115
|
The 1px box with clip-path, rather than width/height 0, is the shape that
|
|
745
1116
|
survives: a zero-sized element is dropped from the tree by some engines. */
|
|
1117
|
+
/* A used-value reader for the four viewport-inset tokens, and the only
|
|
1118
|
+
reliable one. getComputedStyle().getPropertyValue() on an unregistered
|
|
1119
|
+
custom property hands back the substituted token stream, not a length: it
|
|
1120
|
+
returns "4rem" verbatim, and "calc(56px + env(safe-area-inset-top))" as
|
|
1121
|
+
"calc(56px + 0px)" -- which parses as NaN and takes the whole inset with it.
|
|
1122
|
+
Padding is a real property, so the same tokens come back resolved to px.
|
|
1123
|
+
|
|
1124
|
+
visibility rather than display: none, because a box that generates no
|
|
1125
|
+
layout has no used values to read. Zero-sized, absolutely positioned and
|
|
1126
|
+
inert, so it costs nothing but the read. */
|
|
1127
|
+
.viewport-probe {
|
|
1128
|
+
position: absolute;
|
|
1129
|
+
top: 0;
|
|
1130
|
+
left: 0;
|
|
1131
|
+
width: 0;
|
|
1132
|
+
height: 0;
|
|
1133
|
+
visibility: hidden;
|
|
1134
|
+
pointer-events: none;
|
|
1135
|
+
padding: var(--_viewport-inset-top) var(--_viewport-inset-right)
|
|
1136
|
+
var(--_viewport-inset-bottom) var(--_viewport-inset-left);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
746
1139
|
.sr-only {
|
|
747
1140
|
position: absolute;
|
|
748
1141
|
width: 1px;
|
|
@@ -758,6 +1151,8 @@ export const STYLES = `
|
|
|
758
1151
|
.messages {
|
|
759
1152
|
flex: 1;
|
|
760
1153
|
overflow-y: auto;
|
|
1154
|
+
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
1155
|
+
overscroll-behavior: contain;
|
|
761
1156
|
/* The browser's own scroll anchoring competes with the scroller for the same
|
|
762
1157
|
job and wins unpredictably -- it can hold the view still exactly when we
|
|
763
1158
|
want to follow. Turned off so following is decided in one place. Safari
|
|
@@ -910,6 +1305,8 @@ export const STYLES = `
|
|
|
910
1305
|
.message--assistant pre {
|
|
911
1306
|
padding: 8px 10px;
|
|
912
1307
|
overflow: auto;
|
|
1308
|
+
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
1309
|
+
overscroll-behavior: contain;
|
|
913
1310
|
background: var(--_bg);
|
|
914
1311
|
border: 1px solid var(--_border);
|
|
915
1312
|
border-radius: 6px;
|
|
@@ -1088,6 +1485,8 @@ export const STYLES = `
|
|
|
1088
1485
|
border-left: 2px solid var(--_border);
|
|
1089
1486
|
max-height: 220px;
|
|
1090
1487
|
overflow: auto;
|
|
1488
|
+
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
1489
|
+
overscroll-behavior: contain;
|
|
1091
1490
|
white-space: pre-wrap;
|
|
1092
1491
|
word-break: break-word;
|
|
1093
1492
|
font-family: inherit;
|
|
@@ -1278,6 +1677,8 @@ export const STYLES = `
|
|
|
1278
1677
|
padding: 6px 8px;
|
|
1279
1678
|
max-height: 160px;
|
|
1280
1679
|
overflow: auto;
|
|
1680
|
+
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
1681
|
+
overscroll-behavior: contain;
|
|
1281
1682
|
background: var(--_bg);
|
|
1282
1683
|
border: 1px solid var(--_border);
|
|
1283
1684
|
border-radius: 6px;
|
|
@@ -1772,6 +2173,8 @@ export const STYLES = `
|
|
|
1772
2173
|
resize: none;
|
|
1773
2174
|
max-height: var(--_composer-max-height);
|
|
1774
2175
|
overflow-y: auto;
|
|
2176
|
+
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
2177
|
+
overscroll-behavior: contain;
|
|
1775
2178
|
padding: 6px 4px 2px;
|
|
1776
2179
|
background: transparent;
|
|
1777
2180
|
border: none;
|
|
@@ -1982,13 +2385,30 @@ export const STYLES = `
|
|
|
1982
2385
|
* values, and the axis furniture inherits currentColor at low opacity so it
|
|
1983
2386
|
* reads correctly in either theme without a second palette.
|
|
1984
2387
|
*/
|
|
2388
|
+
/* A chart is sized like a message, not like a panel: it takes the width it
|
|
2389
|
+
needs and stops, rather than stretching into whatever room the transcript
|
|
2390
|
+
has. A message bubble caps at 80% for the same reason -- widening the panel
|
|
2391
|
+
should not resize what is already in it. The cap is a token so a host with a
|
|
2392
|
+
wide panel and a real reason can raise it; the default is the width this
|
|
2393
|
+
renderer has always drawn at. Below the cap the block still fills its column,
|
|
2394
|
+
which is what a narrow panel needs. */
|
|
1985
2395
|
.chart-block {
|
|
1986
|
-
align-self:
|
|
1987
|
-
|
|
2396
|
+
align-self: flex-start;
|
|
2397
|
+
width: 100%;
|
|
2398
|
+
max-width: var(--_chart-max-width);
|
|
1988
2399
|
margin: 6px 0;
|
|
1989
2400
|
color: var(--_fg);
|
|
1990
2401
|
}
|
|
1991
2402
|
|
|
2403
|
+
/* The drawing fills the block's width and carries its own height in the
|
|
2404
|
+
viewBox, so it is laid out rather than magnified: block display keeps the
|
|
2405
|
+
inline baseline gap from adding a stripe under every chart. */
|
|
2406
|
+
.chart-block svg {
|
|
2407
|
+
display: block;
|
|
2408
|
+
width: 100%;
|
|
2409
|
+
height: auto;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
1992
2412
|
.chart-title {
|
|
1993
2413
|
margin-bottom: 2px;
|
|
1994
2414
|
font-size: 0.85em;
|
|
@@ -2039,6 +2459,30 @@ export const STYLES = `
|
|
|
2039
2459
|
opacity: 0.75;
|
|
2040
2460
|
}
|
|
2041
2461
|
|
|
2462
|
+
/* The one control a notice may carry. Quiet, because it reports something
|
|
2463
|
+
already done rather than asking for a decision. */
|
|
2464
|
+
.run-notice-undo {
|
|
2465
|
+
flex: 0 0 auto;
|
|
2466
|
+
margin-inline-start: auto;
|
|
2467
|
+
padding: 2px 8px;
|
|
2468
|
+
border: 1px solid var(--_border);
|
|
2469
|
+
border-radius: var(--_radius);
|
|
2470
|
+
background: var(--_bg);
|
|
2471
|
+
color: var(--_accent);
|
|
2472
|
+
font: inherit;
|
|
2473
|
+
font-size: 0.9em;
|
|
2474
|
+
cursor: pointer;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
.run-notice-undo:hover:not(:disabled) {
|
|
2478
|
+
background: var(--_hover);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
.run-notice-undo:disabled {
|
|
2482
|
+
opacity: 0.5;
|
|
2483
|
+
cursor: default;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2042
2486
|
.run-notice-text {
|
|
2043
2487
|
min-width: 0;
|
|
2044
2488
|
overflow-wrap: anywhere;
|
|
@@ -2183,6 +2627,8 @@ export const STYLES = `
|
|
|
2183
2627
|
padding: 8px 10px;
|
|
2184
2628
|
max-height: 140px;
|
|
2185
2629
|
overflow: auto;
|
|
2630
|
+
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
2631
|
+
overscroll-behavior: contain;
|
|
2186
2632
|
font-size: 12px;
|
|
2187
2633
|
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
2188
2634
|
background: var(--_assistant-bg);
|
|
@@ -2371,6 +2817,43 @@ export const STYLES = `
|
|
|
2371
2817
|
}
|
|
2372
2818
|
}
|
|
2373
2819
|
|
|
2820
|
+
/* Touch. Separate from the width breakpoint above on purpose: width decides
|
|
2821
|
+
the layout, the pointer decides which controls make sense, and a touch
|
|
2822
|
+
laptop is coarse-pointered and wide while a narrow desktop window is
|
|
2823
|
+
fine-pointered and small. */
|
|
2824
|
+
@media (pointer: coarse) {
|
|
2825
|
+
/* A 6px edge strip is not a control, it is a trap that eats a scroll. The
|
|
2826
|
+
corners are 14px, still under the 24px floor a target is meant to clear,
|
|
2827
|
+
and the panel has other ways to be resized on a device that has a pointer
|
|
2828
|
+
precise enough to grab one. */
|
|
2829
|
+
.resize-handle {
|
|
2830
|
+
display: none;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
/* Platform guidance is 44pt on iOS and 48dp on Android. These sit at 28-30px,
|
|
2834
|
+
which clears the WCAG minimum and misses both. */
|
|
2835
|
+
:host {
|
|
2836
|
+
--_action-size: var(--ag-ui-action-size, 44px);
|
|
2837
|
+
--_tool-btn-size: var(--ag-ui-tool-btn-size, 44px);
|
|
2838
|
+
--_send-size: var(--ag-ui-send-size, 44px);
|
|
2839
|
+
/* Missed the first time these were raised, which left the header -- the
|
|
2840
|
+
widget's primary controls, and the only way to reach history, a new chat
|
|
2841
|
+
or the collapse -- at half the size of everything beside it. */
|
|
2842
|
+
--_header-btn-size: var(--ag-ui-header-btn-size, 44px);
|
|
2843
|
+
--_header-gap: var(--ag-ui-header-gap, 6px);
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
/* iOS Safari zooms the page when a control under 16px takes focus, which
|
|
2847
|
+
drags the whole fixed panel with it and leaves the user pinching back out
|
|
2848
|
+
of a chat they only wanted to type into. The composer inherits the widget
|
|
2849
|
+
font, which is 14px by default and 13px at compact density, so it has to
|
|
2850
|
+
say 16 outright -- and only ever upward, so a host that has deliberately
|
|
2851
|
+
set something larger keeps it. */
|
|
2852
|
+
.input {
|
|
2853
|
+
font-size: max(16px, var(--_font-size));
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2374
2857
|
.message-action[aria-pressed="true"] {
|
|
2375
2858
|
opacity: 1;
|
|
2376
2859
|
color: var(--_accent);
|
|
@@ -2559,6 +3042,8 @@ export const STYLES = `
|
|
|
2559
3042
|
flex-direction: column;
|
|
2560
3043
|
max-height: 220px;
|
|
2561
3044
|
overflow: auto;
|
|
3045
|
+
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
3046
|
+
overscroll-behavior: contain;
|
|
2562
3047
|
background: var(--_bg);
|
|
2563
3048
|
border: 1px solid var(--_border);
|
|
2564
3049
|
border-radius: 8px;
|
|
@@ -2647,6 +3132,8 @@ export const STYLES = `
|
|
|
2647
3132
|
box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
|
|
2648
3133
|
max-height: 60%;
|
|
2649
3134
|
overflow-y: auto;
|
|
3135
|
+
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
3136
|
+
overscroll-behavior: contain;
|
|
2650
3137
|
transform-origin: top center;
|
|
2651
3138
|
transition:
|
|
2652
3139
|
opacity var(--_motion) var(--_ease),
|
|
@@ -2822,10 +3309,20 @@ export const STYLES = `
|
|
|
2822
3309
|
}
|
|
2823
3310
|
|
|
2824
3311
|
.drawer-title {
|
|
3312
|
+
/* Takes the slack so the two controls group at the trailing edge. With
|
|
3313
|
+
space-between and three items the middle one floats, which reads as the
|
|
3314
|
+
title and the close button being a pair with New chat wedged between
|
|
3315
|
+
them. Truncates rather than pushing them off the row. */
|
|
3316
|
+
flex: 1 1 auto;
|
|
3317
|
+
min-width: 0;
|
|
3318
|
+
overflow: hidden;
|
|
3319
|
+
text-overflow: ellipsis;
|
|
3320
|
+
white-space: nowrap;
|
|
2825
3321
|
font-weight: 600;
|
|
2826
3322
|
}
|
|
2827
3323
|
|
|
2828
3324
|
.drawer-new {
|
|
3325
|
+
flex: 0 0 auto;
|
|
2829
3326
|
border: 1px solid var(--_border);
|
|
2830
3327
|
border-radius: var(--_radius);
|
|
2831
3328
|
background: var(--_bg);
|
|
@@ -2836,10 +3333,63 @@ export const STYLES = `
|
|
|
2836
3333
|
cursor: pointer;
|
|
2837
3334
|
}
|
|
2838
3335
|
|
|
3336
|
+
/* Narrows the list. Sits under the header rather than in it: the header's two
|
|
3337
|
+
controls act on the conversation, and a field that filters what is below it
|
|
3338
|
+
belongs with what it filters. */
|
|
3339
|
+
/* What is waiting for the run to finish. Above the composer, beside the
|
|
3340
|
+
attachment tray, because both are things already handed over and not yet
|
|
3341
|
+
sent. Each chip is a button: pressing it takes that message back. */
|
|
3342
|
+
.queued {
|
|
3343
|
+
display: flex;
|
|
3344
|
+
flex-wrap: wrap;
|
|
3345
|
+
gap: var(--_space);
|
|
3346
|
+
padding: 0 var(--_pad) var(--_space);
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
.queued-chip {
|
|
3350
|
+
max-width: 100%;
|
|
3351
|
+
overflow: hidden;
|
|
3352
|
+
padding: 4px 10px;
|
|
3353
|
+
border: 1px dashed var(--_border);
|
|
3354
|
+
border-radius: var(--_msg-radius);
|
|
3355
|
+
background: var(--_bg);
|
|
3356
|
+
color: var(--_muted);
|
|
3357
|
+
font: inherit;
|
|
3358
|
+
font-size: 0.9em;
|
|
3359
|
+
text-overflow: ellipsis;
|
|
3360
|
+
white-space: nowrap;
|
|
3361
|
+
cursor: pointer;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
.queued-chip:hover {
|
|
3365
|
+
border-style: solid;
|
|
3366
|
+
color: var(--_fg);
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
.drawer-filter {
|
|
3370
|
+
flex: 0 0 auto;
|
|
3371
|
+
box-sizing: border-box;
|
|
3372
|
+
width: calc(100% - var(--_pad) * 2);
|
|
3373
|
+
margin: var(--_space) var(--_pad) 0;
|
|
3374
|
+
padding: 6px 10px;
|
|
3375
|
+
border: 1px solid var(--_border);
|
|
3376
|
+
border-radius: var(--_radius);
|
|
3377
|
+
background: var(--_input-bg);
|
|
3378
|
+
color: var(--_fg);
|
|
3379
|
+
font: inherit;
|
|
3380
|
+
font-size: 0.9em;
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
.drawer-filter::placeholder {
|
|
3384
|
+
color: var(--_muted);
|
|
3385
|
+
}
|
|
3386
|
+
|
|
2839
3387
|
.drawer-list {
|
|
2840
3388
|
flex: 1;
|
|
2841
3389
|
min-height: 0;
|
|
2842
3390
|
overflow-y: auto;
|
|
3391
|
+
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
3392
|
+
overscroll-behavior: contain;
|
|
2843
3393
|
}
|
|
2844
3394
|
|
|
2845
3395
|
.drawer-empty {
|
|
@@ -2960,6 +3510,31 @@ export const STYLES = `
|
|
|
2960
3510
|
background: none;
|
|
2961
3511
|
}
|
|
2962
3512
|
|
|
3513
|
+
/* The way back out of the list. It sits beside New chat, which is the control
|
|
3514
|
+
it must not be mistaken for: one returns you to the conversation you were
|
|
3515
|
+
reading, the other replaces it. */
|
|
3516
|
+
.drawer-close {
|
|
3517
|
+
flex: 0 0 auto;
|
|
3518
|
+
display: inline-flex;
|
|
3519
|
+
align-items: center;
|
|
3520
|
+
justify-content: center;
|
|
3521
|
+
width: 26px;
|
|
3522
|
+
height: 26px;
|
|
3523
|
+
padding: 0;
|
|
3524
|
+
border: 0;
|
|
3525
|
+
border-radius: 6px;
|
|
3526
|
+
background: none;
|
|
3527
|
+
color: inherit;
|
|
3528
|
+
font: inherit;
|
|
3529
|
+
font-size: 18px;
|
|
3530
|
+
line-height: 1;
|
|
3531
|
+
cursor: pointer;
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
.drawer-close:hover {
|
|
3535
|
+
background: var(--_hover);
|
|
3536
|
+
}
|
|
3537
|
+
|
|
2963
3538
|
:host([placement="embedded"]) .drawer-panel {
|
|
2964
3539
|
width: 100%;
|
|
2965
3540
|
border-right: none;
|