@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 CHANGED
@@ -7,6 +7,145 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.23.0] — 2026-08-12
11
+
12
+ Attachment chips, which turned out to be the least finished corner of 0.22.
13
+
14
+ ### Fixed
15
+
16
+ - **A filename on a sent attachment chip was invisible on the stock light
17
+ theme.** `.attachment-chip` set the assistant surface as its background but no
18
+ colour, so on a user bubble it inherited `--ag-ui-user-fg` — white on
19
+ `#f1f1f6`, a contrast ratio of 1.13:1 where WCAG AA wants 4.5:1. Only the size
20
+ stayed legible, because it sets its own muted colour, which is exactly how the
21
+ bug read to a user: an icon, a blank gap, and a size. The chip now takes
22
+ `--ag-ui-text`, the same consumer-overridable token the rest of the body text
23
+ uses, so a page that themes its text themes the chip with it. The dark and
24
+ code themes were never affected, which is why this shipped.
25
+
26
+ - **The composer's attachment tray never collapsed.** The tray sets `hidden`
27
+ while it holds no chips, but its rule declared `display: flex` with no
28
+ `[hidden]` guard, and an author `display` beats the UA stylesheet's
29
+ `[hidden] { display: none }`. Every embed that wired uploads therefore carried
30
+ 8px of dead space above the composer at all times. With the tray genuinely
31
+ collapsing, its padding is also symmetric again (`8px 12px`), so a chip clears
32
+ the composer's top edge instead of sitting flush against it.
33
+
34
+ - **Filenames truncated far short of the space available.**
35
+ `.attachment-chip-name` capped itself at `14ch`, so
36
+ `LQ27552-7006-EXHIBIT-A.pdf` rendered as `LQ27552-7006 …` inside a chip with
37
+ room to spare. The cap is gone: the chip is already `max-width: 100%` with the
38
+ name ellipsising, so its container bounds it, and a genuinely long name now
39
+ ellipsises at the edge it actually reaches. The chip is `box-sizing:
40
+ border-box` with it, which stops that `100%` from overflowing its container by
41
+ the chip's own padding and border.
42
+
43
+ ### Changed
44
+
45
+ - **Attachment chip icons are inline SVG.** 0.22 moved the chrome's glyphs to
46
+ inline SVG and stopped at the chips, leaving emoji sitting beside SVG send,
47
+ attach and mic buttons — a different optical weight, varying by platform, and
48
+ taking neither `currentColor` nor a size from CSS. Four marks (image, PDF,
49
+ text, generic) now follow the same contract as the rest, painted from the
50
+ chip's own colour so an errored chip turns red glyph and all. Both the sent
51
+ bubble's chips and the composer tray's change together.
52
+
53
+ ### Removed
54
+
55
+ - **The client-side attachment manifest in `RunAgentInput.context`.**
56
+ `getContext()` appended a one-line summary of the message's attachments; the
57
+ server now derives that from the refs riding the messages, so the client's
58
+ copy only duplicated it on the turn a file was attached. Attachments still
59
+ reach the agent — through the message, which is where they already were. The
60
+ page-map half of `getContext()` is unchanged.
61
+
62
+ **This needs a server that derives the manifest itself.** `django-ag-ui`
63
+ does so from 0.42.0. Against an older server nothing replaces the client's
64
+ copy, so the agent stops being told which attachment ids exist and answers a
65
+ question about an attached file by asking for the file — which is the defect
66
+ the server-side derivation was written to fix. Upgrade the server first, or
67
+ together. A host that read the manifest out of the run context itself is
68
+ likewise affected.
69
+
70
+ ## [0.22.0] — 2026-08-11
71
+
72
+ Two complaints about how the widget *feels*, and both turned out to be structural
73
+ rather than cosmetic.
74
+
75
+ ### Changed
76
+
77
+ - **The composer is one surface, not four boxes.** The input row was a flat flex
78
+ row — attach, mic, textarea, send — with every sibling stretched to the
79
+ textarea's two-row height. That gave a paperclip the same visual weight as the
80
+ field it sits next to and made Send a full-height filled slab. The border,
81
+ background and focus ring now belong to a wrapping `composer-surface`: the
82
+ field is borderless and **grows with what is typed** (from one row up to
83
+ `--ag-ui-composer-max-height`, then scrolls), and a `composer-tools` row
84
+ underneath carries the paperclip and mic as quiet icon buttons with a circular
85
+ Send closing the right-hand end.
86
+
87
+ Send is now icon-only. Its accessible name still comes from the `send` /
88
+ `stop` strings (`aria-label` + `title`), and the run state swaps its glyph
89
+ rather than its text, so nothing moves when a run starts. A host that sized
90
+ `::part(send)` by its padding should switch to `--ag-ui-send-size`.
91
+
92
+ - **Collapsing goes to a round floating launcher.** `collapsed` used to leave the
93
+ full-width header bar sitting on the page, which is most of a chat widget's
94
+ footprint for none of its use. The panel now scales down into a launcher in the
95
+ corner it already occupies and the launcher grows back out of that point;
96
+ `transform` and `opacity` are all that animate, so the morph is
97
+ compositor-only and cannot reflow the host page. `placement="sidebar"` keeps
98
+ its edge rail (and now genuinely *slides* out through the edge it docks
99
+ against — the transition was declared but never wired to a transform), and
100
+ `embedded` / `page` keep the header bar, being host-laid-out and full-screen
101
+ respectively.
102
+
103
+ This changes what an existing floating embed looks like when collapsed. The
104
+ collapsed host keeps its box with `pointer-events: none`, so the page beneath
105
+ stays interactive and the launcher takes the clicks.
106
+
107
+ - **The chat-history drawer and the checkpoints panel slide.** Both were toggled
108
+ with `hidden` alone, which snaps. They now keep their box and hide with
109
+ `visibility`, which is what lets a surface animate *in and out* — an element
110
+ that was never rendered has no before-change style to animate from, and one
111
+ whose `display` flips to `none` cannot animate at all.
112
+
113
+ - **The chrome's glyphs are inline SVG** (send, stop, paperclip, mic, launcher)
114
+ rather than emoji, each in a slot with the mark as its fallback:
115
+ `icon-send`, `icon-stop`, `icon-attach`, `icon-voice`, `launcher`.
116
+
117
+ ### Added
118
+
119
+ - **An unread badge on the launcher.** A collapsed widget is the one state where
120
+ an answer can arrive with nothing on screen to say so, so the launcher now
121
+ counts the answers that finished while it was closed (capped at `9+`) and
122
+ expanding marks them read. It is the only affordance here that is **on by
123
+ default**; `data-unread-badge="false"` turns the badge off, and the count keeps
124
+ running so a host chrome can render its own from the new `ag-ui-unread` event
125
+ (`UNREAD_EVENT` / `UnreadDetail`, plus a `chat.unread` getter). The count is
126
+ also the launcher's accessible name — a coloured dot says nothing to a screen
127
+ reader — via the new `expandUnread` string. Tokens:
128
+ `--ag-ui-badge-{bg,fg,size,font-size}`; part `launcher-badge`.
129
+
130
+ - **`data-launcher-icon-url`** — an icon for the collapsed launcher when it should
131
+ differ from the header's. Falls back to `data-icon-url`, so one attribute still
132
+ feeds both.
133
+
134
+ - **Motion tokens** — `--ag-ui-motion`, `--ag-ui-ease`, `--ag-ui-ease-pop`. One
135
+ duration and two curves drive every collapse, expand and slide-over. Under
136
+ `prefers-reduced-motion` the duration collapses to a frame; `--ag-ui-motion: 0s`
137
+ switches the animation off outright.
138
+
139
+ - **Launcher and composer tokens** — `--ag-ui-launcher-{size,bg,fg,radius,icon-size,inset}`,
140
+ `--ag-ui-composer-{radius,max-height}`, `--ag-ui-send-size`, `--ag-ui-tool-btn-size`,
141
+ `--ag-ui-glyph-{size,stroke}`; parts `composer-surface` and `composer-tools`.
142
+
143
+ - **Motion tests that run in a real browser.** happy-dom runs no transitions, so
144
+ every assertion about this would pass on a stylesheet where nothing animates.
145
+ The Chromium project now asserts on `getAnimations()` — that the browser
146
+ actually *started* the transitions a collapse, an expand and a drawer open are
147
+ supposed to start.
148
+
10
149
  ## [0.21.0] — 2026-08-11
11
150
 
12
151
  Ten findings from a real embed — a cross-origin, cookie-authenticated React host.
@@ -1171,7 +1310,9 @@ hosts that both arrange the page the way it expects.
1171
1310
  ### Notes
1172
1311
  - First release — exercising the automated npm OIDC publish pipeline end-to-end.
1173
1312
 
1174
- [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.21.0...HEAD
1313
+ [Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.23.0...HEAD
1314
+ [0.23.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.22.0...v0.23.0
1315
+ [0.22.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.21.0...v0.22.0
1175
1316
  [0.21.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.20.1...v0.21.0
1176
1317
  [0.20.1]: https://github.com/Artui/ag-ui-web-component/compare/v0.20.0...v0.20.1
1177
1318
  [0.20.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.19.0...v0.20.0
package/README.md CHANGED
@@ -47,6 +47,7 @@ No framework, no Django, no admin specifics live here. Downstream consumers (e.g
47
47
  - [DOM-driver and animation primitives](#dom-driver-and-animation-primitives)
48
48
  - [Page-action tools](#page-action-tools)
49
49
  - [New chat and collapse](#new-chat-and-collapse)
50
+ - [Collapsing to the launcher](#collapsing-to-the-launcher)
50
51
  - [Tool-call display modes](#tool-call-display-modes)
51
52
  - [Markdown rendering](#markdown-rendering)
52
53
  - [Run notices: compaction and agent skills](#run-notices-compaction-and-agent-skills)
@@ -163,17 +164,20 @@ another origin, add `credentials="include"` too; see
163
164
  | `data-tools-url` | — | URL of a server tool-label catalog (`[{ name, summary, description? }]`), fetched with the element's headers and cookie policy; labels tool-call cards for server-side tools. |
164
165
  | `data-threads-url` | — | URL of a server thread index (django-ag-ui's `ThreadsView`); enables durable, cross-device chat history. |
165
166
  | `data-runs-url` | — | URL of a server run index (django-ag-ui's `RunsView`); reveals the header's ⭯ *Continue a run* panel. See [Resuming a run](#resuming-a-run). |
166
- | `data-attachments-url` | — | URL of the file-upload endpoint (django-ag-ui's `AttachmentsView`); reveals the composer's 📎 picker + drag-and-drop. |
167
+ | `data-attachments-url` | — | URL of the file-upload endpoint (django-ag-ui's `AttachmentsView`); reveals the composer's paperclip picker + drag-and-drop. |
167
168
  | `data-attachment-accept` | — | `<input accept>` list for client-side type filtering (e.g. `image/*,.pdf`). The server stays authoritative. |
168
169
  | `data-attachment-max-bytes` | — | Client-side upload size cap in bytes (default 10 MiB; `0` disables). The server stays authoritative. |
169
- | `data-transcribe-url` | — | URL of the voice-transcription endpoint (django-ag-ui's `TranscribeView`); reveals the composer's 🎤 mic button. See [Voice input](#voice-input). |
170
+ | `data-transcribe-url` | — | URL of the voice-transcription endpoint (django-ag-ui's `TranscribeView`); reveals the composer's mic button. See [Voice input](#voice-input). |
170
171
  | `data-theme-toggle` | — | Boolean: show a built-in header light⇄dark toggle (persists per tab). Off by default. See [Theme toggle](#theme-toggle). |
171
172
  | `data-strings` | `strings` | Partial JSON override of the UI string table (localization). The property wins key-by-key over the attribute; see [Internationalization](#internationalization-i18n). |
172
- | `data-icon-url` | — | Header (and sidebar-rail) icon image URL. A slotted `slot="icon"` wins; see [Header & launcher icon](#header-and-launcher-icon). |
173
+ | `data-icon-url` | — | Header (and launcher) icon image URL. A slotted `slot="icon"` wins; see [Header & launcher icon](#header-and-launcher-icon). |
174
+ | `data-launcher-icon-url` | — | Icon image URL for the collapsed launcher only, when it should differ from the header's. Falls back to `data-icon-url`; a slotted `slot="launcher"` wins over both. |
175
+ | `data-unread-badge` | — | **On by default.** `="false"` hides the launcher's unread badge; the count and the `ag-ui-unread` event keep running. See [Collapsing to the launcher](#collapsing-to-the-launcher). |
173
176
 
174
177
  Each header control also takes its own icon slot — `icon-history`, `icon-checkpoints`,
175
178
  `icon-new`, `icon-collapse` — with the built-in glyph as the fallback, so a host can project a
176
- brand `<img>` or `<svg>` rather than only restyling the character:
179
+ brand `<img>` or `<svg>` rather than only restyling the character. The composer's glyphs work the
180
+ same way: `icon-send`, `icon-stop`, `icon-attach`, `icon-voice`.
177
181
 
178
182
  ```html
179
183
  <ag-ui-chat endpoint="/agent/">
@@ -183,7 +187,7 @@ brand `<img>` or `<svg>` rather than only restyling the character:
183
187
  | `data-page-actions` | — | Opt-in built-in page-action tools: a comma list of `scroll` / `drag` (e.g. `"scroll,drag"`). See [Page-action tools](#page-action-tools). |
184
188
  | `data-side` | — | CSS-only, for `placement="sidebar"`: which edge it docks to — `right` (default) / `left`. |
185
189
  | `data-answer-well` | — | CSS-only boolean: box each assistant turn (its text, tool cards, and thinking) in one bordered "well". Off by default. See [The answer well](#the-answer-well). |
186
- | `collapsed` | `collapsed` | Reflected boolean; collapses the widget (to a rail under `placement="sidebar"`). Persisted per-tab in `sessionStorage`. |
190
+ | `collapsed` | `collapsed` | Reflected boolean; collapses the widget to its [launcher](#collapsing-to-the-launcher) (a rail under `placement="sidebar"`, the header bar under `embedded` / `page`). Persisted per-tab in `sessionStorage`. |
187
191
  | `theme` | — | CSS-only: `light` (default) / `dark` / `auto` / `code`. |
188
192
  | `density` | — | CSS-only: `comfortable` (default) / `compact`. |
189
193
  | `placement` | — | CSS-only: `floating` (default) / `bottom-left` / `side` / `sidebar` / `full` / `page` / `embedded`. |
@@ -686,6 +690,71 @@ chat.toggleCollapsed();
686
690
  chat.addEventListener("ag-ui-toggle", (e) => console.log(e.detail.collapsed));
687
691
  ```
688
692
 
693
+ ### Collapsing to the launcher
694
+
695
+ A collapsed widget shrinks to a round **floating launcher** in the corner it already occupies: the
696
+ panel scales down into it and fades, the launcher grows out of the same point, and clicking the
697
+ launcher reverses it. Only `transform` and `opacity` animate, so the motion is compositor-only and
698
+ never reflows your page. Two placements collapse to something else instead — `sidebar` slides out
699
+ to its [edge rail](#sidebar-placement), and `embedded` / `page` keep the header bar, since one is
700
+ laid out by your page and the other is a full-screen route.
701
+
702
+ The launcher's mark comes from the same seam as the header icon, most specific first: a slotted
703
+ `slot="launcher"` child, then `data-launcher-icon-url`, then `data-icon-url`, then the built-in
704
+ speech bubble.
705
+
706
+ ```html
707
+ <ag-ui-chat endpoint="/agent/" data-launcher-icon-url="/mark.svg"></ag-ui-chat>
708
+
709
+ <!-- or any markup at all -->
710
+ <ag-ui-chat endpoint="/agent/">
711
+ <svg slot="launcher" width="26" height="26"><!-- ... --></svg>
712
+ </ag-ui-chat>
713
+ ```
714
+
715
+ ```css
716
+ ag-ui-chat {
717
+ --ag-ui-launcher-size: 56px;
718
+ --ag-ui-launcher-radius: 50%; /* 12px for a squircle */
719
+ --ag-ui-launcher-bg: #14532d; /* defaults to the header background */
720
+ --ag-ui-launcher-fg: #ffffff;
721
+ --ag-ui-launcher-icon-size: 26px;
722
+ --ag-ui-launcher-inset: auto 0 0 auto; /* which corner of the widget's box */
723
+ }
724
+ ```
725
+
726
+ > **The collapsed host keeps its box.** Animating the element's own width and height would animate
727
+ > layout; instead the box stays put with `pointer-events: none`, and the launcher takes the clicks.
728
+ > A host measuring `getBoundingClientRect()` on a collapsed widget still sees the panel's
729
+ > footprint — nothing there paints or takes input.
730
+
731
+ #### The unread badge
732
+
733
+ A collapsed widget is the one state where an answer can arrive with nothing on screen to say so,
734
+ so the launcher carries a count of the answers that finished while it was closed (capped at `9+`).
735
+ Expanding — or `newChat()` — marks them read. It is the only affordance here that is **on by
736
+ default**; `data-unread-badge="false"` turns the badge off.
737
+
738
+ The count is also the launcher's accessible name (`Expand — 2 unread`, from the `expandUnread`
739
+ string), because a coloured dot says nothing to a screen reader.
740
+
741
+ ```js
742
+ chat.unread; // 2
743
+
744
+ // Every change, whether or not the badge renders it — so a host that hides the
745
+ // badge can put the count in its own chrome.
746
+ chat.addEventListener("ag-ui-unread", (e) => setDockBadge(e.detail.unread));
747
+ ```
748
+
749
+ ```css
750
+ ag-ui-chat {
751
+ --ag-ui-badge-bg: #b91c1c; /* defaults to --ag-ui-danger */
752
+ --ag-ui-badge-fg: #ffffff;
753
+ --ag-ui-badge-size: 18px;
754
+ --ag-ui-badge-font-size: 11px;
755
+ }
756
+ ```
757
+
689
758
  ---
690
759
 
691
760
  ## Tool-call display modes
@@ -1149,6 +1218,7 @@ re-export point. Internal modules import from leaf paths.
1149
1218
  | `MessageRole` | type | Role of a rendered chat message. |
1150
1219
  | `SubmitDetail` | type | `detail` shape of the submit event. |
1151
1220
  | `ToggleDetail` | type | `detail` shape of the `ag-ui-toggle` event (`{ collapsed }`). |
1221
+ | `UnreadDetail` | type | `detail` shape of the `ag-ui-unread` event (`{ unread }`). |
1152
1222
 
1153
1223
  ### AG-UI client & agent
1154
1224
 
@@ -1236,6 +1306,7 @@ re-export point. Internal modules import from leaf paths.
1236
1306
  | `ELEMENT_TAG` | The registered tag name (`ag-ui-chat`). |
1237
1307
  | `SUBMIT_EVENT` | The submit CustomEvent name. |
1238
1308
  | `TOGGLE_EVENT` | The collapse-toggle CustomEvent name (`ag-ui-toggle`). |
1309
+ | `UNREAD_EVENT` | The unread-count CustomEvent name (`ag-ui-unread`). |
1239
1310
  | `MESSAGE_ROLE` | Message role constants. |
1240
1311
  | `TOOL_CALL_STATUS` | Tool-call card status constants. |
1241
1312
  | `TOOL_DISPLAY` | Tool-call display-mode constants (`minimal` / `compact` / `full`). |
@@ -1364,14 +1435,14 @@ Available parts: `panel`, `header`, `title`, `icon`, `header-controls`, `header-
1364
1435
  `-args` / `-actions` / `-button` / `-cancel` / `-confirm`),
1365
1436
  `approval` (plus `approval-body` / `-actions` / `-button` / `-approve` / `-deny`),
1366
1437
  `question` (plus `question-body` / `-options` / `-choice` / `-choice-text` / `-radio` / `-input` /
1367
- `-actions` / `-button`), `composer`, `input`, `send`,
1438
+ `-actions` / `-button`), `composer` (plus `composer-surface` / `composer-tools`), `input`, `send`,
1368
1439
  `attach-button`, `voice-button`,
1369
1440
  the attachment chips — `attachment-tray` and `attachment-chips` (the read-only chips on sent
1370
1441
  bubbles) with the shared chip parts `attachment-chip` (plus `-icon` / `-name` / `-size` / `-bar` /
1371
1442
  `-bar-fill` / `-retry` / `-remove`),
1372
1443
  the skills UI (`skill-chips`, `skill-chip`, `skill-palette`, `skill-item`, `skill-item-title`,
1373
1444
  `skill-item-desc`, and the missing-placeholder `skill-hint`),
1374
- `launcher`, `launcher-icon`, and the drawer parts
1445
+ `launcher`, `launcher-icon`, `launcher-badge`, and the drawer parts
1375
1446
  (`drawer`, `drawer-backdrop`, `drawer-panel`, `drawer-header`, `drawer-title`, `drawer-new`,
1376
1447
  `drawer-list`, `drawer-empty`, `drawer-row`, `drawer-row-select`, `drawer-row-title`,
1377
1448
  `drawer-row-time`, `drawer-row-preview`, `drawer-row-actions`, `drawer-row-rename`,
@@ -1407,7 +1478,10 @@ with a matching `slot=`):
1407
1478
  | `header-actions` | Extra controls between the title and the built-in buttons. |
1408
1479
  | `empty` | The empty-state shown before any message. |
1409
1480
  | `footer` | Below the composer. |
1410
- | `launcher` | The collapsed sidebar rail's content. |
1481
+ | `launcher` | The collapsed widget's mark — the floating launcher, or the sidebar rail. |
1482
+ | `icon-send` / `icon-stop` | The composer button's two glyphs (idle and mid-run). |
1483
+ | `icon-attach` / `icon-voice` | The paperclip and mic glyphs. |
1484
+ | `icon-history` / `icon-checkpoints` / `icon-new` / `icon-collapse` | The header controls' glyphs. |
1411
1485
 
1412
1486
  ```html
1413
1487
  <ag-ui-chat endpoint="/agent/">
@@ -1430,9 +1504,11 @@ stays icon-less. The same icon seam feeds the collapsed sidebar rail. Size it vi
1430
1504
  ### Sidebar placement
1431
1505
 
1432
1506
  `placement="sidebar"` is a full-height **docked** panel that slides open/closed and collapses to a
1433
- slim **icon rail** (rather than the floating launcher). It docks right by default; `data-side="left"`
1434
- docks it left. Collapse state reuses the `collapsed` attribute (persisted per-tab), and the rail
1435
- carries `aria-expanded`. The slide honours `prefers-reduced-motion`.
1507
+ slim **icon rail** rather than the [floating launcher](#collapsing-to-the-launcher) the same
1508
+ element, shaped by the placement. It docks right by default; `data-side="left"` docks it left. The
1509
+ panel slides out through the edge it docks against. Collapse state reuses the `collapsed` attribute
1510
+ (persisted per-tab), and the rail carries `aria-expanded`. The slide honours
1511
+ `prefers-reduced-motion`.
1436
1512
 
1437
1513
  ```html
1438
1514
  <ag-ui-chat endpoint="/agent/" placement="sidebar" data-side="left"></ag-ui-chat>
@@ -1476,9 +1552,48 @@ web component handles the `REASONING_*` event family (and the deprecated `THINKI
1476
1552
  `@ag-ui/client` maps onto it), so no client config is needed — the thoughts appear whenever the
1477
1553
  server forwards reasoning.
1478
1554
 
1555
+ ### The composer
1556
+
1557
+ The composer is one bordered surface (part `composer-surface`) that owns the border and the focus
1558
+ ring: the field sits on top and grows with what is typed until it hits its ceiling and scrolls,
1559
+ and a tool row (part `composer-tools`) sits underneath with the paperclip and mic as quiet icon
1560
+ buttons on the left and a circular **Send** on the right. Send is icon-only — its accessible name
1561
+ still comes from the `send` / `stop` [strings](#internationalization-i18n) — and it becomes the
1562
+ Stop control mid-run by swapping its glyph, so nothing moves when a run starts.
1563
+
1564
+ ```css
1565
+ ag-ui-chat {
1566
+ --ag-ui-composer-radius: 14px;
1567
+ --ag-ui-composer-max-height: 40vh; /* where the growing field starts scrolling */
1568
+ --ag-ui-tool-btn-size: 30px; /* the paperclip / mic hit targets */
1569
+ --ag-ui-send-size: 30px; /* the send circle */
1570
+ --ag-ui-glyph-size: 18px;
1571
+ --ag-ui-glyph-stroke: 1.75;
1572
+ }
1573
+ ```
1574
+
1575
+ Every glyph is a slot with the built-in mark as its fallback (`icon-send`, `icon-stop`,
1576
+ `icon-attach`, `icon-voice`), so projecting your own icon set never means restyling a character.
1577
+
1578
+ ### Motion
1579
+
1580
+ One duration and two curves drive every collapse, expand and slide-over, so the whole widget
1581
+ settles as one thing:
1582
+
1583
+ ```css
1584
+ ag-ui-chat {
1585
+ --ag-ui-motion: 0.28s;
1586
+ --ag-ui-ease: cubic-bezier(0.32, 0.72, 0, 1); /* the settle */
1587
+ --ag-ui-ease-pop: cubic-bezier(0.34, 1.36, 0.64, 1); /* the arrival, with overshoot */
1588
+ }
1589
+ ```
1590
+
1591
+ Under `prefers-reduced-motion: reduce` the duration collapses to a single frame: states still
1592
+ change, nothing travels. Set `--ag-ui-motion: 0s` to switch the animation off outright.
1593
+
1479
1594
  ### Voice input
1480
1595
 
1481
- Set `data-transcribe-url` (django-ag-ui's `TranscribeView`) to reveal a 🎤 mic button in the
1596
+ Set `data-transcribe-url` (django-ag-ui's `TranscribeView`) to reveal a mic button in the
1482
1597
  composer (part `voice-button`). Click it to record via `MediaRecorder`, click again to stop — the
1483
1598
  clip is POSTed to the endpoint and the returned transcript is dropped into the textarea. Swap the
1484
1599
  transport with a custom `transcribeHandler` — `(audio: Blob) => Promise<string>` — to use a