@artooi/ag-ui-web-component 0.21.0 → 0.22.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 +81 -1
- package/README.md +127 -12
- package/dist/ag-ui-web-component.bundle.js +404 -99
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/constants.d.ts +29 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +11 -0
- 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 +550 -106
- package/dist/index.js.map +2 -2
- 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 +35 -0
- package/src/core/ag_ui_chat.ts +197 -32
- package/src/index.ts +2 -0
- package/src/ui/styles.ts +383 -78
- 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; }
|
|
@@ -1557,16 +1828,35 @@ export const STYLES = `
|
|
|
1557
1828
|
color: var(--_danger);
|
|
1558
1829
|
}
|
|
1559
1830
|
|
|
1560
|
-
/* Chat-history drawer — a slide-over within the chat panel.
|
|
1831
|
+
/* Chat-history drawer — a slide-over within the chat panel.
|
|
1832
|
+
The hidden attribute stays the single source of truth for open/closed (no
|
|
1833
|
+
JS-driven animation state): display is transitioned discretely, so on close
|
|
1834
|
+
the drawer stays displayed for the whole exit and is removed only at the end,
|
|
1835
|
+
which is what lets the backdrop fade and the panel slide out. */
|
|
1561
1836
|
.drawer {
|
|
1562
1837
|
position: absolute;
|
|
1563
1838
|
inset: 0;
|
|
1564
1839
|
z-index: 5;
|
|
1565
1840
|
display: flex;
|
|
1841
|
+
transition: visibility var(--_motion) var(--_ease);
|
|
1566
1842
|
}
|
|
1567
1843
|
|
|
1844
|
+
/* Closed. The overlay keeps its box (display, not none) so the backdrop and
|
|
1845
|
+
panel inside it stay rendered and can transition both ways; visibility is
|
|
1846
|
+
what takes the whole subtree out of the tab order, the a11y tree and hit
|
|
1847
|
+
testing at rest, and it holds off until the slide has finished. */
|
|
1568
1848
|
.drawer[hidden] {
|
|
1569
|
-
display:
|
|
1849
|
+
display: flex;
|
|
1850
|
+
visibility: hidden;
|
|
1851
|
+
pointer-events: none;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
.drawer[hidden] .drawer-backdrop {
|
|
1855
|
+
opacity: 0;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
.drawer[hidden] .drawer-panel {
|
|
1859
|
+
transform: translateX(-100%);
|
|
1570
1860
|
}
|
|
1571
1861
|
|
|
1572
1862
|
.checkpoints {
|
|
@@ -1584,10 +1874,21 @@ export const STYLES = `
|
|
|
1584
1874
|
box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
|
|
1585
1875
|
max-height: 60%;
|
|
1586
1876
|
overflow-y: auto;
|
|
1877
|
+
transform-origin: top center;
|
|
1878
|
+
transition:
|
|
1879
|
+
opacity var(--_motion) var(--_ease),
|
|
1880
|
+
transform var(--_motion) var(--_ease),
|
|
1881
|
+
visibility var(--_motion) var(--_ease);
|
|
1587
1882
|
}
|
|
1588
1883
|
|
|
1884
|
+
/* Same idiom as the drawer: laid out at rest, hidden by visibility, so the
|
|
1885
|
+
popover can animate open and closed. */
|
|
1589
1886
|
.checkpoints[hidden] {
|
|
1590
|
-
display:
|
|
1887
|
+
display: flex;
|
|
1888
|
+
visibility: hidden;
|
|
1889
|
+
pointer-events: none;
|
|
1890
|
+
opacity: 0;
|
|
1891
|
+
transform: scale(0.96) translateY(-6px);
|
|
1591
1892
|
}
|
|
1592
1893
|
|
|
1593
1894
|
.checkpoints-title {
|
|
@@ -1649,6 +1950,8 @@ export const STYLES = `
|
|
|
1649
1950
|
position: absolute;
|
|
1650
1951
|
inset: 0;
|
|
1651
1952
|
background: rgba(20, 20, 50, 0.32);
|
|
1953
|
+
opacity: 1;
|
|
1954
|
+
transition: opacity var(--_motion) var(--_ease);
|
|
1652
1955
|
}
|
|
1653
1956
|
|
|
1654
1957
|
.drawer-panel {
|
|
@@ -1661,6 +1964,8 @@ export const STYLES = `
|
|
|
1661
1964
|
border-right: 1px solid var(--_border);
|
|
1662
1965
|
box-shadow: var(--_shadow);
|
|
1663
1966
|
overflow: hidden;
|
|
1967
|
+
transform: none;
|
|
1968
|
+
transition: transform var(--_motion) var(--_ease);
|
|
1664
1969
|
}
|
|
1665
1970
|
|
|
1666
1971
|
.drawer-header {
|
package/src/ui/ui_strings.ts
CHANGED
|
@@ -24,8 +24,15 @@ export interface UiStrings {
|
|
|
24
24
|
newChat: string;
|
|
25
25
|
/** Collapse button. */
|
|
26
26
|
collapse: string;
|
|
27
|
-
/** Expand affordance (the sidebar rail
|
|
27
|
+
/** Expand affordance (the launcher, and the sidebar rail). */
|
|
28
28
|
expand: string;
|
|
29
|
+
/**
|
|
30
|
+
* The launcher's label while unread answers are waiting behind it, with
|
|
31
|
+
* `{count}` replaced by how many. It replaces {@link expand} rather than
|
|
32
|
+
* appending to it, so a translation can order the two parts as its language
|
|
33
|
+
* needs.
|
|
34
|
+
*/
|
|
35
|
+
expandUnread: string;
|
|
29
36
|
/** Built-in header theme toggle (light ⇄ dark). */
|
|
30
37
|
toggleTheme: string;
|
|
31
38
|
|
|
@@ -204,6 +211,7 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
204
211
|
newChat: "New chat",
|
|
205
212
|
collapse: "Collapse",
|
|
206
213
|
expand: "Expand",
|
|
214
|
+
expandUnread: "Expand — {count} unread",
|
|
207
215
|
toggleTheme: "Toggle theme",
|
|
208
216
|
copyCode: "Copy",
|
|
209
217
|
copied: "Copied",
|
package/src/ui/voice_input.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ICON_VOICE } from "../constants.js";
|
|
1
2
|
import type { TranscribeHandler } from "../core/transcribe_audio.js";
|
|
2
3
|
import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
3
4
|
|
|
@@ -49,7 +50,12 @@ export class VoiceInput {
|
|
|
49
50
|
this.element.type = "button";
|
|
50
51
|
this.element.className = "voice-btn";
|
|
51
52
|
this.element.setAttribute("part", "voice-button");
|
|
52
|
-
|
|
53
|
+
// The mic glyph, in a slot so a host can project its own mark. Author-written
|
|
54
|
+
// constant markup, never user or server data.
|
|
55
|
+
const glyph = document.createElement("slot");
|
|
56
|
+
glyph.name = "icon-voice";
|
|
57
|
+
glyph.innerHTML = ICON_VOICE;
|
|
58
|
+
this.element.append(glyph);
|
|
53
59
|
this.#setState("idle");
|
|
54
60
|
this.element.addEventListener("click", () => {
|
|
55
61
|
void this.toggle();
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.22.0";
|