@artooi/ag-ui-web-component 0.21.0 → 0.23.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 +142 -1
- package/README.md +127 -12
- package/dist/ag-ui-web-component.bundle.js +459 -108
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/constants.d.ts +45 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +19 -4
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +619 -141
- package/dist/index.js.map +2 -2
- package/dist/ui/attachment_chips.d.ts +12 -1
- package/dist/ui/attachment_chips.d.ts.map +1 -1
- package/dist/ui/attachment_tray.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/ui_strings.d.ts +8 -1
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +55 -0
- package/src/core/ag_ui_chat.ts +205 -62
- package/src/index.ts +2 -0
- package/src/ui/attachment_chips.ts +18 -6
- package/src/ui/attachment_tray.ts +2 -1
- package/src/ui/styles.ts +433 -80
- package/src/ui/ui_strings.ts +9 -1
- package/src/ui/voice_input.ts +7 -1
- package/src/version.ts +1 -1
package/src/ui/styles.ts
CHANGED
|
@@ -74,6 +74,36 @@ export const STYLES = `
|
|
|
74
74
|
--_icon-size: var(--ag-ui-icon-size, 22px);
|
|
75
75
|
--_icon-radius: var(--ag-ui-icon-radius, 4px);
|
|
76
76
|
|
|
77
|
+
/* Composer — one bordered surface holding the field and its tool row. */
|
|
78
|
+
--_composer-radius: var(--ag-ui-composer-radius, 14px);
|
|
79
|
+
--_composer-max-height: var(--ag-ui-composer-max-height, 40vh);
|
|
80
|
+
--_tool-btn-size: var(--ag-ui-tool-btn-size, 30px);
|
|
81
|
+
--_send-size: var(--ag-ui-send-size, 30px);
|
|
82
|
+
--_glyph-size: var(--ag-ui-glyph-size, 18px);
|
|
83
|
+
--_glyph-stroke: var(--ag-ui-glyph-stroke, 1.75);
|
|
84
|
+
|
|
85
|
+
/* Unread badge on the launcher. */
|
|
86
|
+
--_badge-bg: var(--ag-ui-badge-bg, var(--_danger));
|
|
87
|
+
--_badge-fg: var(--ag-ui-badge-fg, #ffffff);
|
|
88
|
+
--_badge-size: var(--ag-ui-badge-size, 18px);
|
|
89
|
+
--_badge-font-size: var(--ag-ui-badge-font-size, 11px);
|
|
90
|
+
|
|
91
|
+
/* The floating launcher a collapsed widget shrinks to. */
|
|
92
|
+
--_launcher-size: var(--ag-ui-launcher-size, 56px);
|
|
93
|
+
--_launcher-bg: var(--ag-ui-launcher-bg, var(--_header-bg));
|
|
94
|
+
--_launcher-fg: var(--ag-ui-launcher-fg, var(--_header-fg));
|
|
95
|
+
--_launcher-radius: var(--ag-ui-launcher-radius, 50%);
|
|
96
|
+
--_launcher-icon-size: var(--ag-ui-launcher-icon-size, 26px);
|
|
97
|
+
--_launcher-inset: var(--ag-ui-launcher-inset, auto 0 0 auto);
|
|
98
|
+
|
|
99
|
+
/* Motion. One duration and two curves drive every collapse, expand and
|
|
100
|
+
slide-over, so the whole widget accelerates and settles as one thing.
|
|
101
|
+
The default curve is a long-tailed ease-out (a fast start that decelerates
|
|
102
|
+
into place); the pop curve overshoots slightly, for something arriving. */
|
|
103
|
+
--_motion: var(--ag-ui-motion, 0.28s);
|
|
104
|
+
--_ease: var(--ag-ui-ease, cubic-bezier(0.32, 0.72, 0, 1));
|
|
105
|
+
--_ease-pop: var(--ag-ui-ease-pop, cubic-bezier(0.34, 1.36, 0.64, 1));
|
|
106
|
+
|
|
77
107
|
/* Spacing — the density preset overrides these. */
|
|
78
108
|
--_space: var(--ag-ui-space, 10px);
|
|
79
109
|
--_pad: var(--ag-ui-pad, 16px);
|
|
@@ -238,56 +268,161 @@ export const STYLES = `
|
|
|
238
268
|
--_height: var(--ag-ui-height, 100vh);
|
|
239
269
|
--_max-height: var(--ag-ui-max-height, 100vh);
|
|
240
270
|
--_radius: var(--ag-ui-radius, 0);
|
|
241
|
-
transition: width
|
|
271
|
+
transition: width var(--_motion) var(--_ease);
|
|
242
272
|
}
|
|
243
273
|
|
|
244
274
|
:host([placement="sidebar"][data-side="left"]) {
|
|
245
275
|
--_inset: var(--ag-ui-inset, 0 auto 0 0);
|
|
246
276
|
}
|
|
247
277
|
|
|
278
|
+
/* The docked panel is pinned to the edge it docks against rather than filling
|
|
279
|
+
the host as a flex child. Collapsing shrinks the host to the rail width, and
|
|
280
|
+
a flex child would be squashed to 52px on the way out instead of sliding out
|
|
281
|
+
at full width. */
|
|
248
282
|
:host([placement="sidebar"]) .chat {
|
|
249
|
-
|
|
283
|
+
position: absolute;
|
|
284
|
+
inset: 0 0 0 auto;
|
|
285
|
+
width: var(--_width);
|
|
286
|
+
transform-origin: right center;
|
|
250
287
|
}
|
|
251
288
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
289
|
+
:host([placement="sidebar"][data-side="left"]) .chat {
|
|
290
|
+
inset: 0 auto 0 0;
|
|
291
|
+
transform-origin: left center;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* Collapsed sidebar: shrink the host to the rail width and slide the panel out
|
|
295
|
+
through the edge it docks against. Higher specificity than the generic
|
|
296
|
+
collapse rules, so it wins regardless of source order. */
|
|
255
297
|
:host([placement="sidebar"][collapsed]) {
|
|
256
298
|
width: var(--_rail-width);
|
|
257
299
|
height: 100vh;
|
|
258
300
|
max-height: 100vh;
|
|
259
301
|
bottom: 0;
|
|
302
|
+
pointer-events: auto;
|
|
260
303
|
}
|
|
261
304
|
|
|
262
305
|
:host([placement="sidebar"][collapsed]) .chat {
|
|
263
|
-
|
|
306
|
+
transform: translateX(100%);
|
|
264
307
|
}
|
|
265
308
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
309
|
+
:host([placement="sidebar"][data-side="left"][collapsed]) .chat {
|
|
310
|
+
transform: translateX(-100%);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@media (prefers-reduced-motion: reduce) {
|
|
314
|
+
:host([placement="sidebar"]),
|
|
315
|
+
:host([placement="sidebar"]) .chat {
|
|
316
|
+
transition: none;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ── Launcher ───────────────────────────────────────────────────────────────
|
|
321
|
+
What a collapsed widget shrinks to: a round floating button by default, the
|
|
322
|
+
sidebar's slim edge rail under that placement. A sibling of the panel, so it
|
|
323
|
+
survives the panel being hidden, and the only part of a collapsed widget
|
|
324
|
+
that takes pointer events. */
|
|
325
|
+
.launcher {
|
|
326
|
+
display: flex;
|
|
327
|
+
position: absolute;
|
|
328
|
+
inset: var(--_launcher-inset);
|
|
329
|
+
align-items: center;
|
|
330
|
+
justify-content: center;
|
|
331
|
+
width: var(--_launcher-size);
|
|
332
|
+
height: var(--_launcher-size);
|
|
333
|
+
padding: 0;
|
|
270
334
|
border: none;
|
|
335
|
+
border-radius: var(--_launcher-radius);
|
|
336
|
+
background: var(--_launcher-bg);
|
|
337
|
+
color: var(--_launcher-fg);
|
|
338
|
+
box-shadow: var(--_shadow);
|
|
271
339
|
font: inherit;
|
|
340
|
+
cursor: pointer;
|
|
341
|
+
pointer-events: auto;
|
|
342
|
+
opacity: 0;
|
|
343
|
+
transform: scale(0.4);
|
|
344
|
+
visibility: hidden;
|
|
345
|
+
transition:
|
|
346
|
+
opacity var(--_motion) var(--_ease),
|
|
347
|
+
transform var(--_motion) var(--_ease-pop),
|
|
348
|
+
visibility var(--_motion) var(--_ease);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* The launcher grows out of the corner the panel shrank into.
|
|
352
|
+
It is laid out at rest rather than display:none, which is what lets it
|
|
353
|
+
animate in *and* out: an element that was not rendered has no before-change
|
|
354
|
+
style to transition from, and one whose display flips to none cannot
|
|
355
|
+
transition at all. visibility keeps it unpaintable, untabbable and
|
|
356
|
+
unclickable in between -- the expanded panel's own controls sit under it and
|
|
357
|
+
must stay reachable. */
|
|
358
|
+
:host([collapsed]) .launcher {
|
|
359
|
+
opacity: 1;
|
|
360
|
+
transform: none;
|
|
361
|
+
visibility: visible;
|
|
272
362
|
}
|
|
273
363
|
|
|
274
|
-
:host([
|
|
275
|
-
|
|
364
|
+
:host([collapsed]) .launcher:hover {
|
|
365
|
+
transform: scale(1.06);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
:host([collapsed]) .launcher:active {
|
|
369
|
+
transform: scale(0.94);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.launcher .icon-holder {
|
|
373
|
+
width: var(--_launcher-icon-size);
|
|
374
|
+
height: var(--_launcher-icon-size);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* The unread badge rides the launcher's top-right corner. The ring in the
|
|
378
|
+
widget's own background is what separates it from the launcher underneath,
|
|
379
|
+
whatever colour the host themes either one. */
|
|
380
|
+
.launcher-badge {
|
|
276
381
|
position: absolute;
|
|
382
|
+
top: -2px;
|
|
383
|
+
right: -2px;
|
|
384
|
+
display: inline-flex;
|
|
385
|
+
align-items: center;
|
|
386
|
+
justify-content: center;
|
|
387
|
+
box-sizing: border-box;
|
|
388
|
+
min-width: var(--_badge-size);
|
|
389
|
+
height: var(--_badge-size);
|
|
390
|
+
padding: 0 5px;
|
|
391
|
+
border-radius: 999px;
|
|
392
|
+
background: var(--_badge-bg);
|
|
393
|
+
color: var(--_badge-fg);
|
|
394
|
+
font-size: var(--_badge-font-size);
|
|
395
|
+
font-weight: 600;
|
|
396
|
+
line-height: 1;
|
|
397
|
+
box-shadow: 0 0 0 2px var(--_bg);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.launcher-badge[hidden] {
|
|
401
|
+
display: none;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* The sidebar collapses to an edge rail instead: full height, square, flush
|
|
405
|
+
against the dock. It slides in with the panel rather than popping. */
|
|
406
|
+
:host([placement="sidebar"][collapsed]) .launcher {
|
|
277
407
|
inset: 0;
|
|
408
|
+
width: auto;
|
|
409
|
+
height: auto;
|
|
278
410
|
align-items: flex-start;
|
|
279
|
-
justify-content: center;
|
|
280
411
|
padding-top: 16px;
|
|
281
412
|
border: 1px solid var(--_border);
|
|
413
|
+
border-radius: 0;
|
|
282
414
|
background: var(--_header-bg);
|
|
283
415
|
color: var(--_header-fg);
|
|
284
|
-
|
|
416
|
+
box-shadow: none;
|
|
417
|
+
transform: none;
|
|
285
418
|
}
|
|
286
419
|
|
|
420
|
+
/* Every transition in this file is timed by --_motion, so collapsing it to a
|
|
421
|
+
frame is the whole reduced-motion story: states still change (and display
|
|
422
|
+
still flips at the end of its discrete transition), nothing travels. */
|
|
287
423
|
@media (prefers-reduced-motion: reduce) {
|
|
288
|
-
:host
|
|
289
|
-
|
|
290
|
-
transition: none;
|
|
424
|
+
:host {
|
|
425
|
+
--_motion: var(--ag-ui-motion, 0.001s);
|
|
291
426
|
}
|
|
292
427
|
}
|
|
293
428
|
|
|
@@ -378,27 +513,78 @@ export const STYLES = `
|
|
|
378
513
|
background: rgba(255, 255, 255, 0.18);
|
|
379
514
|
}
|
|
380
515
|
|
|
381
|
-
/*
|
|
382
|
-
|
|
516
|
+
/* ── Collapse ───────────────────────────────────────────────────────────────
|
|
517
|
+
Collapsing shrinks the widget to the round floating launcher: the panel
|
|
518
|
+
scales down toward the launcher's corner and fades out, the launcher pops in
|
|
519
|
+
from that same point. Both halves are transform and opacity only, so the
|
|
520
|
+
morph runs on the compositor and never reflows the host page.
|
|
521
|
+
|
|
522
|
+
The host box keeps its expanded size. Animating it would animate layout, and
|
|
523
|
+
a dragged --ag-ui-width would then fight the launcher's own size. Nothing
|
|
524
|
+
paints there once the panel is gone, so the box only has to stop swallowing
|
|
525
|
+
clicks: pointer events go to none, and the launcher takes them back.
|
|
526
|
+
|
|
527
|
+
Two placements collapse to something else and restore what they need:
|
|
528
|
+
"sidebar" slides to its rail (below), while "embedded" and "page" keep the
|
|
529
|
+
original header bar — embedded is laid out by the host page, where a
|
|
530
|
+
floating circle would escape the layout, and page is a full-screen route
|
|
531
|
+
with no corner to float in. */
|
|
383
532
|
:host([collapsed]) {
|
|
533
|
+
pointer-events: none;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
:host([collapsed]) .chat {
|
|
537
|
+
opacity: 0;
|
|
538
|
+
transform: scale(0.94);
|
|
539
|
+
visibility: hidden;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* visibility is what keeps the panel out of the tab order and the a11y tree at
|
|
543
|
+
rest without display:none killing the transition. It interpolates so that any
|
|
544
|
+
progress below 1 still counts as visible: the panel stays on screen for the
|
|
545
|
+
whole collapse and flips hidden exactly at the end, and on expand it is
|
|
546
|
+
visible from the first frame. */
|
|
547
|
+
.chat {
|
|
548
|
+
transform-origin: bottom right;
|
|
549
|
+
transition:
|
|
550
|
+
opacity var(--_motion) var(--_ease),
|
|
551
|
+
transform var(--_motion) var(--_ease),
|
|
552
|
+
visibility var(--_motion) var(--_ease);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
:host([placement="bottom-left"]) .chat {
|
|
556
|
+
transform-origin: bottom left;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/* The two in-flow placements keep the original collapse: hide the body, let
|
|
560
|
+
the host shrink to the header bar. */
|
|
561
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) {
|
|
384
562
|
height: auto;
|
|
385
563
|
max-height: none;
|
|
564
|
+
pointer-events: auto;
|
|
386
565
|
}
|
|
387
566
|
|
|
388
|
-
:host([collapsed]) .
|
|
389
|
-
:
|
|
390
|
-
:
|
|
391
|
-
:
|
|
392
|
-
:host([collapsed]) .skill-hint {
|
|
393
|
-
display: none;
|
|
567
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .chat {
|
|
568
|
+
opacity: 1;
|
|
569
|
+
transform: none;
|
|
570
|
+
visibility: visible;
|
|
394
571
|
}
|
|
395
572
|
|
|
396
|
-
/*
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
:host([collapsed][placement="
|
|
401
|
-
|
|
573
|
+
/* These two keep the header bar, so the launcher must stay out of the way —
|
|
574
|
+
an embedded host is position: static, which would otherwise leave an
|
|
575
|
+
absolutely-positioned circle to escape the layout entirely and land against
|
|
576
|
+
whatever the page happens to position. */
|
|
577
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .launcher {
|
|
578
|
+
visibility: hidden;
|
|
579
|
+
opacity: 0;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .messages,
|
|
583
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .input-row,
|
|
584
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-chips,
|
|
585
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-palette,
|
|
586
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-hint {
|
|
587
|
+
display: none;
|
|
402
588
|
}
|
|
403
589
|
|
|
404
590
|
.messages {
|
|
@@ -1071,102 +1257,187 @@ export const STYLES = `
|
|
|
1071
1257
|
opacity: 0.35;
|
|
1072
1258
|
}
|
|
1073
1259
|
|
|
1260
|
+
/* ── Composer ───────────────────────────────────────────────────────────────
|
|
1261
|
+
One surface owns the border, the background and the focus ring; the field and
|
|
1262
|
+
its tool row sit inside it. The row used to be four siblings stretched to the
|
|
1263
|
+
textarea's height, which gave a paperclip the same visual weight as the field
|
|
1264
|
+
and turned Send into a full-height slab. */
|
|
1074
1265
|
.input-row {
|
|
1075
1266
|
display: flex;
|
|
1076
|
-
gap: 8px;
|
|
1077
1267
|
padding: 12px;
|
|
1078
1268
|
border-top: 1px solid var(--_border);
|
|
1079
1269
|
}
|
|
1080
1270
|
|
|
1081
|
-
.
|
|
1271
|
+
.composer {
|
|
1082
1272
|
flex: 1;
|
|
1083
|
-
|
|
1273
|
+
min-width: 0;
|
|
1274
|
+
display: flex;
|
|
1275
|
+
flex-direction: column;
|
|
1276
|
+
gap: 4px;
|
|
1277
|
+
padding: 6px 6px 6px 10px;
|
|
1084
1278
|
background: var(--_input-bg);
|
|
1085
1279
|
border: 1px solid var(--_border);
|
|
1086
|
-
border-radius:
|
|
1087
|
-
|
|
1280
|
+
border-radius: var(--_composer-radius);
|
|
1281
|
+
transition: border-color var(--_motion) var(--_ease);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.composer:focus-within {
|
|
1285
|
+
border-color: var(--_accent);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/* The field grows with its content (sized by #autoGrow) up to the ceiling,
|
|
1289
|
+
then scrolls. border-box keeps that measurement stable: with content-box the
|
|
1290
|
+
padding would be added to every scrollHeight read and the field would creep
|
|
1291
|
+
taller on each keystroke. */
|
|
1292
|
+
.input {
|
|
1293
|
+
box-sizing: border-box;
|
|
1294
|
+
resize: none;
|
|
1295
|
+
max-height: var(--_composer-max-height);
|
|
1296
|
+
overflow-y: auto;
|
|
1297
|
+
padding: 6px 4px 2px;
|
|
1298
|
+
background: transparent;
|
|
1299
|
+
border: none;
|
|
1088
1300
|
font: inherit;
|
|
1089
1301
|
color: inherit;
|
|
1090
1302
|
outline: none;
|
|
1091
1303
|
}
|
|
1092
1304
|
|
|
1093
|
-
.
|
|
1094
|
-
|
|
1305
|
+
.composer-tools {
|
|
1306
|
+
display: flex;
|
|
1307
|
+
align-items: center;
|
|
1308
|
+
gap: 2px;
|
|
1095
1309
|
}
|
|
1096
1310
|
|
|
1097
|
-
.
|
|
1311
|
+
/* Icon buttons: quiet at rest, so the field is what the eye lands on. */
|
|
1312
|
+
.attach-btn,
|
|
1313
|
+
.voice-btn {
|
|
1314
|
+
display: inline-flex;
|
|
1315
|
+
align-items: center;
|
|
1316
|
+
justify-content: center;
|
|
1317
|
+
width: var(--_tool-btn-size);
|
|
1318
|
+
height: var(--_tool-btn-size);
|
|
1319
|
+
padding: 0;
|
|
1098
1320
|
border: none;
|
|
1099
1321
|
border-radius: 8px;
|
|
1100
|
-
|
|
1322
|
+
background: transparent;
|
|
1323
|
+
color: var(--_muted);
|
|
1324
|
+
font: inherit;
|
|
1325
|
+
line-height: 1;
|
|
1326
|
+
cursor: pointer;
|
|
1327
|
+
transition:
|
|
1328
|
+
background-color var(--_motion) var(--_ease),
|
|
1329
|
+
color var(--_motion) var(--_ease);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.attach-btn:hover,
|
|
1333
|
+
.voice-btn:hover {
|
|
1334
|
+
background: var(--_hover);
|
|
1335
|
+
color: var(--_fg);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.attach-btn:disabled,
|
|
1339
|
+
.voice-btn:disabled {
|
|
1340
|
+
cursor: default;
|
|
1341
|
+
opacity: 0.6;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/* Send closes the row on the right: a circle, the only filled control in the
|
|
1345
|
+
composer, so "the thing that acts" reads at a glance. */
|
|
1346
|
+
.send {
|
|
1347
|
+
margin-left: auto;
|
|
1348
|
+
display: inline-flex;
|
|
1349
|
+
align-items: center;
|
|
1350
|
+
justify-content: center;
|
|
1351
|
+
flex: none;
|
|
1352
|
+
width: var(--_send-size);
|
|
1353
|
+
height: var(--_send-size);
|
|
1354
|
+
padding: 0;
|
|
1355
|
+
border: none;
|
|
1356
|
+
border-radius: 50%;
|
|
1101
1357
|
background: var(--_accent);
|
|
1102
1358
|
color: #ffffff;
|
|
1103
1359
|
font: inherit;
|
|
1104
|
-
|
|
1360
|
+
line-height: 1;
|
|
1105
1361
|
cursor: pointer;
|
|
1362
|
+
transition:
|
|
1363
|
+
background-color var(--_motion) var(--_ease),
|
|
1364
|
+
transform var(--_motion) var(--_ease-pop);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
.send:hover {
|
|
1368
|
+
transform: scale(1.08);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
.send:active {
|
|
1372
|
+
transform: scale(0.92);
|
|
1106
1373
|
}
|
|
1107
1374
|
|
|
1108
1375
|
.send:disabled {
|
|
1109
1376
|
opacity: 0.5;
|
|
1110
1377
|
cursor: default;
|
|
1378
|
+
transform: none;
|
|
1111
1379
|
}
|
|
1112
1380
|
|
|
1113
|
-
/* The composer button doubles as the Stop control while a run is in flight
|
|
1381
|
+
/* The composer button doubles as the Stop control while a run is in flight —
|
|
1382
|
+
same circle, different glyph, so nothing moves when a run starts. */
|
|
1114
1383
|
.send[data-state="running"] {
|
|
1115
1384
|
background: var(--_muted);
|
|
1116
1385
|
}
|
|
1117
1386
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
border: 1px solid var(--_border);
|
|
1122
|
-
border-radius: 8px;
|
|
1123
|
-
padding: 0 10px;
|
|
1124
|
-
background: var(--_input-bg);
|
|
1125
|
-
color: inherit;
|
|
1126
|
-
font: inherit;
|
|
1127
|
-
cursor: pointer;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
.attach-btn:hover {
|
|
1131
|
-
border-color: var(--_accent);
|
|
1387
|
+
.send[data-state="idle"] .send-stop,
|
|
1388
|
+
.send[data-state="running"] .send-send {
|
|
1389
|
+
display: none;
|
|
1132
1390
|
}
|
|
1133
1391
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1392
|
+
/* Glyphs — one class, painted from the button's own colour so every state
|
|
1393
|
+
(hover, recording, running) carries the icon with it. */
|
|
1394
|
+
.glyph {
|
|
1395
|
+
width: var(--_glyph-size);
|
|
1396
|
+
height: var(--_glyph-size);
|
|
1397
|
+
fill: none;
|
|
1398
|
+
stroke: currentColor;
|
|
1399
|
+
stroke-width: var(--_glyph-stroke);
|
|
1400
|
+
stroke-linecap: round;
|
|
1401
|
+
stroke-linejoin: round;
|
|
1136
1402
|
}
|
|
1137
1403
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1404
|
+
.glyph--solid {
|
|
1405
|
+
fill: currentColor;
|
|
1406
|
+
stroke: none;
|
|
1141
1407
|
}
|
|
1142
1408
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
color: inherit;
|
|
1149
|
-
font: inherit;
|
|
1150
|
-
cursor: pointer;
|
|
1409
|
+
/* In an icon holder (header brand, launcher) the glyph takes the holder's size
|
|
1410
|
+
rather than the composer's. */
|
|
1411
|
+
.icon-holder .glyph {
|
|
1412
|
+
width: 100%;
|
|
1413
|
+
height: 100%;
|
|
1151
1414
|
}
|
|
1152
1415
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1416
|
+
/* ── File attachments ───────────────────────────────────────────────────── */
|
|
1417
|
+
/* The picker button sits in the composer's tool row; hidden until
|
|
1418
|
+
data-attachments-url. */
|
|
1419
|
+
.attach-input {
|
|
1420
|
+
display: none;
|
|
1155
1421
|
}
|
|
1156
1422
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1423
|
+
/* The mic button's mount point; filled only once #wireVoice mounts the
|
|
1424
|
+
control. */
|
|
1425
|
+
.voice-slot {
|
|
1426
|
+
display: contents;
|
|
1160
1427
|
}
|
|
1161
1428
|
|
|
1162
1429
|
/* Recording: a red tint + a gentle pulse so it's clearly "live". */
|
|
1163
1430
|
.voice-btn[data-state="recording"] {
|
|
1164
|
-
border-color: var(--_danger);
|
|
1165
1431
|
background: var(--_danger);
|
|
1166
1432
|
color: #ffffff;
|
|
1167
1433
|
animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
|
|
1168
1434
|
}
|
|
1169
1435
|
|
|
1436
|
+
.voice-btn[data-state="recording"]:hover {
|
|
1437
|
+
background: var(--_danger);
|
|
1438
|
+
color: #ffffff;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1170
1441
|
@keyframes ag-ui-voice-pulse {
|
|
1171
1442
|
0%, 100% { opacity: 1; }
|
|
1172
1443
|
50% { opacity: 0.6; }
|
|
@@ -1183,11 +1454,25 @@ export const STYLES = `
|
|
|
1183
1454
|
display: contents;
|
|
1184
1455
|
}
|
|
1185
1456
|
|
|
1457
|
+
/* The padding is the only separation the tray gets: its slot is display:
|
|
1458
|
+
contents, so the tray is a direct child of the .chat column, and that column
|
|
1459
|
+
sets no gap. The bottom value keeps a chip off the composer's top edge.
|
|
1460
|
+
The inline value here is only the default: the page placement overrides
|
|
1461
|
+
padding-inline further down to compute its reading-column gutter, and wins
|
|
1462
|
+
on specificity whichever way this declaration is written. Read the two
|
|
1463
|
+
together before changing either. */
|
|
1186
1464
|
.attachment-tray {
|
|
1187
1465
|
display: flex;
|
|
1188
1466
|
flex-wrap: wrap;
|
|
1189
1467
|
gap: 6px;
|
|
1190
|
-
padding: 8px 12px
|
|
1468
|
+
padding: 8px 12px;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/* The tray sets the hidden property while empty, and an author display beats
|
|
1472
|
+
the UA stylesheet's rule for it, so without this the empty tray keeps
|
|
1473
|
+
laying out and its padding is permanent dead space above the composer. */
|
|
1474
|
+
.attachment-tray[hidden] {
|
|
1475
|
+
display: none;
|
|
1191
1476
|
}
|
|
1192
1477
|
|
|
1193
1478
|
.attachment-chips {
|
|
@@ -1223,15 +1508,29 @@ export const STYLES = `
|
|
|
1223
1508
|
overflow-wrap: anywhere;
|
|
1224
1509
|
}
|
|
1225
1510
|
|
|
1511
|
+
/* A chip carries its own text colour because it carries its own background.
|
|
1512
|
+
The same chip renders in two places with opposite inherited colours: in the
|
|
1513
|
+
composer tray it inherits the panel's, but on a sent user bubble it inherits
|
|
1514
|
+
the user foreground, which is white on the stock light theme. Against the
|
|
1515
|
+
assistant surface underneath that was a 1.13:1 filename — invisible, and only
|
|
1516
|
+
the size stayed legible because it sets its own muted colour. Pair the colour
|
|
1517
|
+
with the background it belongs to, and both placements read the same.
|
|
1518
|
+
Overridden below for an errored chip, which must keep its red. */
|
|
1226
1519
|
.attachment-chip {
|
|
1227
1520
|
display: inline-flex;
|
|
1228
1521
|
align-items: center;
|
|
1229
1522
|
gap: 6px;
|
|
1523
|
+
/* Without this the 100% below caps the content box, so the chip still
|
|
1524
|
+
overflows its container by its own padding and border. Invisible while a
|
|
1525
|
+
character cap kept names short; routine once the container is what bounds
|
|
1526
|
+
the name. */
|
|
1527
|
+
box-sizing: border-box;
|
|
1230
1528
|
max-width: 100%;
|
|
1231
1529
|
padding: 4px 8px;
|
|
1232
1530
|
border: 1px solid var(--_border);
|
|
1233
1531
|
border-radius: 999px;
|
|
1234
1532
|
background: var(--_assistant-bg);
|
|
1533
|
+
color: var(--_text);
|
|
1235
1534
|
font-size: 0.85em;
|
|
1236
1535
|
position: relative;
|
|
1237
1536
|
}
|
|
@@ -1241,11 +1540,31 @@ export const STYLES = `
|
|
|
1241
1540
|
color: var(--_danger);
|
|
1242
1541
|
}
|
|
1243
1542
|
|
|
1543
|
+
/* The type mark. Painted from currentColor, so it carries the chip's state
|
|
1544
|
+
with it and turns red along with an errored one; muted by opacity rather
|
|
1545
|
+
than a colour, which is what keeps that true. Sized from the chip's own text
|
|
1546
|
+
rather than --ag-ui-glyph-size, the way an icon holder's glyph takes the
|
|
1547
|
+
holder's size: the composer's 18px buttons would make a chip button-height. */
|
|
1548
|
+
.attachment-chip-icon {
|
|
1549
|
+
display: inline-flex;
|
|
1550
|
+
flex: none;
|
|
1551
|
+
opacity: 0.75;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
.attachment-chip-icon .glyph {
|
|
1555
|
+
width: 1.25em;
|
|
1556
|
+
height: 1.25em;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/* No character cap: the chip is already max-width 100%, so its container is
|
|
1560
|
+
what bounds the name, and a fixed cap only truncated names the chip had room
|
|
1561
|
+
for. min-width: 0 is what lets the flex item shrink past its content, so the
|
|
1562
|
+
ellipsis appears at the container edge instead of the chip overflowing. */
|
|
1244
1563
|
.attachment-chip-name {
|
|
1245
1564
|
overflow: hidden;
|
|
1246
1565
|
text-overflow: ellipsis;
|
|
1247
1566
|
white-space: nowrap;
|
|
1248
|
-
|
|
1567
|
+
min-width: 0;
|
|
1249
1568
|
}
|
|
1250
1569
|
|
|
1251
1570
|
.attachment-chip-size {
|
|
@@ -1557,16 +1876,35 @@ export const STYLES = `
|
|
|
1557
1876
|
color: var(--_danger);
|
|
1558
1877
|
}
|
|
1559
1878
|
|
|
1560
|
-
/* Chat-history drawer — a slide-over within the chat panel.
|
|
1879
|
+
/* Chat-history drawer — a slide-over within the chat panel.
|
|
1880
|
+
The hidden attribute stays the single source of truth for open/closed (no
|
|
1881
|
+
JS-driven animation state): display is transitioned discretely, so on close
|
|
1882
|
+
the drawer stays displayed for the whole exit and is removed only at the end,
|
|
1883
|
+
which is what lets the backdrop fade and the panel slide out. */
|
|
1561
1884
|
.drawer {
|
|
1562
1885
|
position: absolute;
|
|
1563
1886
|
inset: 0;
|
|
1564
1887
|
z-index: 5;
|
|
1565
1888
|
display: flex;
|
|
1889
|
+
transition: visibility var(--_motion) var(--_ease);
|
|
1566
1890
|
}
|
|
1567
1891
|
|
|
1892
|
+
/* Closed. The overlay keeps its box (display, not none) so the backdrop and
|
|
1893
|
+
panel inside it stay rendered and can transition both ways; visibility is
|
|
1894
|
+
what takes the whole subtree out of the tab order, the a11y tree and hit
|
|
1895
|
+
testing at rest, and it holds off until the slide has finished. */
|
|
1568
1896
|
.drawer[hidden] {
|
|
1569
|
-
display:
|
|
1897
|
+
display: flex;
|
|
1898
|
+
visibility: hidden;
|
|
1899
|
+
pointer-events: none;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
.drawer[hidden] .drawer-backdrop {
|
|
1903
|
+
opacity: 0;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
.drawer[hidden] .drawer-panel {
|
|
1907
|
+
transform: translateX(-100%);
|
|
1570
1908
|
}
|
|
1571
1909
|
|
|
1572
1910
|
.checkpoints {
|
|
@@ -1584,10 +1922,21 @@ export const STYLES = `
|
|
|
1584
1922
|
box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
|
|
1585
1923
|
max-height: 60%;
|
|
1586
1924
|
overflow-y: auto;
|
|
1925
|
+
transform-origin: top center;
|
|
1926
|
+
transition:
|
|
1927
|
+
opacity var(--_motion) var(--_ease),
|
|
1928
|
+
transform var(--_motion) var(--_ease),
|
|
1929
|
+
visibility var(--_motion) var(--_ease);
|
|
1587
1930
|
}
|
|
1588
1931
|
|
|
1932
|
+
/* Same idiom as the drawer: laid out at rest, hidden by visibility, so the
|
|
1933
|
+
popover can animate open and closed. */
|
|
1589
1934
|
.checkpoints[hidden] {
|
|
1590
|
-
display:
|
|
1935
|
+
display: flex;
|
|
1936
|
+
visibility: hidden;
|
|
1937
|
+
pointer-events: none;
|
|
1938
|
+
opacity: 0;
|
|
1939
|
+
transform: scale(0.96) translateY(-6px);
|
|
1591
1940
|
}
|
|
1592
1941
|
|
|
1593
1942
|
.checkpoints-title {
|
|
@@ -1649,6 +1998,8 @@ export const STYLES = `
|
|
|
1649
1998
|
position: absolute;
|
|
1650
1999
|
inset: 0;
|
|
1651
2000
|
background: rgba(20, 20, 50, 0.32);
|
|
2001
|
+
opacity: 1;
|
|
2002
|
+
transition: opacity var(--_motion) var(--_ease);
|
|
1652
2003
|
}
|
|
1653
2004
|
|
|
1654
2005
|
.drawer-panel {
|
|
@@ -1661,6 +2012,8 @@ export const STYLES = `
|
|
|
1661
2012
|
border-right: 1px solid var(--_border);
|
|
1662
2013
|
box-shadow: var(--_shadow);
|
|
1663
2014
|
overflow: hidden;
|
|
2015
|
+
transform: none;
|
|
2016
|
+
transition: transform var(--_motion) var(--_ease);
|
|
1664
2017
|
}
|
|
1665
2018
|
|
|
1666
2019
|
.drawer-header {
|