inline-chat-kit 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +2111 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1430 -0
  4. package/dist/AnswerActions/AnswerActions.d.ts +35 -0
  5. package/dist/Approval/Approval.d.ts +42 -0
  6. package/dist/Artifact/ArtifactCard.d.ts +45 -0
  7. package/dist/Artifact/ArtifactPane.d.ts +50 -0
  8. package/dist/Artifact/ChatLayout.d.ts +35 -0
  9. package/dist/Artifact/useArtifacts.d.ts +21 -0
  10. package/dist/Attachments/Attachments.d.ts +50 -0
  11. package/dist/Branch/Branch.d.ts +28 -0
  12. package/dist/Button/Button.d.ts +23 -0
  13. package/dist/ChainOfThought/ChainOfThought.d.ts +49 -0
  14. package/dist/ChatHeader/ChatHeader.d.ts +89 -0
  15. package/dist/ChatInput/AddCardsOverlay.d.ts +11 -0
  16. package/dist/ChatInput/ChatInput.d.ts +118 -0
  17. package/dist/ChatInput/HoverActionsRow.d.ts +13 -0
  18. package/dist/ChatInput/MorphGlyph.d.ts +15 -0
  19. package/dist/ChatTurnRow/ChatTurnRow.d.ts +115 -0
  20. package/dist/Chip/Chip.d.ts +7 -0
  21. package/dist/CodeBlock/CodeBlock.d.ts +24 -0
  22. package/dist/CodeBlock/grammars.d.ts +16 -0
  23. package/dist/CodeBlock/highlight.d.ts +38 -0
  24. package/dist/Context/Context.d.ts +36 -0
  25. package/dist/Conversation/Conversation.d.ts +64 -0
  26. package/dist/CustomCursor/CustomCursor.d.ts +1 -0
  27. package/dist/EmptyState/EmptyState.d.ts +24 -0
  28. package/dist/GlassButton/GlassButton.d.ts +19 -0
  29. package/dist/InlineCitation/InlineCitation.d.ts +30 -0
  30. package/dist/Loader/Loader.d.ts +23 -0
  31. package/dist/QuestionCard/QuestionCard.d.ts +32 -0
  32. package/dist/QuestionCard/parts.d.ts +84 -0
  33. package/dist/QuestionCard/types.d.ts +54 -0
  34. package/dist/QuestionGroup/QuestionGroup.d.ts +87 -0
  35. package/dist/Reasoning/Reasoning.d.ts +37 -0
  36. package/dist/ReplyThreadPopup/ReplyThreadPopup.d.ts +18 -0
  37. package/dist/Sources/Sources.d.ts +46 -0
  38. package/dist/SystemMessage/SystemMessage.d.ts +39 -0
  39. package/dist/TaskList/TaskList.d.ts +42 -0
  40. package/dist/TextHighlighter/TextHighlighter.d.ts +17 -0
  41. package/dist/Tool/Tool.d.ts +41 -0
  42. package/dist/announce/announce.d.ts +27 -0
  43. package/dist/disclosure/DisclosureBody.d.ts +22 -0
  44. package/dist/disclosure/DisclosureHeader.d.ts +42 -0
  45. package/dist/disclosure/useDisclosure.d.ts +30 -0
  46. package/dist/duration/formatDuration.d.ts +9 -0
  47. package/dist/grammars-B19jp7qm.js +3181 -0
  48. package/dist/grammars-B19jp7qm.js.map +1 -0
  49. package/dist/index.d.ts +80 -0
  50. package/dist/inline-chat-kit.css +2 -0
  51. package/dist/inline-chat-kit.js +5314 -0
  52. package/dist/inline-chat-kit.js.map +1 -0
  53. package/dist/markdown/parse.d.ts +105 -0
  54. package/dist/markdown/parseMarkdown.d.ts +47 -0
  55. package/dist/radiusCorrection/useCorrectedRadius.d.ts +24 -0
  56. package/dist/reducedMotion/reducedMotion.d.ts +3 -0
  57. package/dist/stateGlyph/StateGlyph.d.ts +23 -0
  58. package/dist/turnParts/turnParts.d.ts +156 -0
  59. package/dist/useChatTurns/useChatTurns.d.ts +127 -0
  60. package/dist/voice/useVoiceInput.d.ts +79 -0
  61. package/package.json +95 -0
  62. package/theming.md +234 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,2111 @@
1
+ # Changelog
2
+
3
+ Dates are the day the work landed on `main`.
4
+
5
+ The versions before 1.0 follow the pre-release convention: **a breaking change
6
+ or new public API bumps the minor**, and the patch is for fixes. Anything that would break an
7
+ existing install is called out under **Breaking**, with what to do about it.
8
+
9
+ ## 0.49.0 — 2026-09-02
10
+
11
+ ### Changed
12
+
13
+ - **New defaults for the composer's motion**, settled on the dial rather than
14
+ argued about. Four numbers moved:
15
+
16
+ | | was | now |
17
+ | --- | --- | --- |
18
+ | `bubble.damping` | 21.5 | 22 |
19
+ | `bubble.mass` | 0.2 | 0.3 |
20
+ | `button.staggerEnter` | 0.12 | 0.06 |
21
+ | `enterButton.bounce` | 0.3 | 0.2 |
22
+
23
+ A heavier, better-damped pill and a tighter row: the controls now return
24
+ half a beat apart rather than a whole one, and the send glyph arrives with
25
+ less overshoot.
26
+
27
+ Nothing about the API changed. A host passing its own `animationConfig` is
28
+ unaffected; one relying on `defaultInlineAnimConfig` gets the new feel and
29
+ can pin the old values by passing them.
30
+
31
+ ### Added
32
+
33
+ - **`wrap.exitDuration`** on `InlineAnimConfig` — how long the controls take
34
+ to leave when the composer grows onto a second line. It had been a literal
35
+ `0.15` in three places, which made it the one part of the choreography no
36
+ configuration could reach.
37
+
38
+ ## 0.48.0 — 2026-09-01
39
+
40
+ ### Added
41
+
42
+ - **Dictation.** Pass `onTranscribe` to `<ChatInput>` or `<ChatTurnRow>` and
43
+ the composer offers a microphone beside its plus; leave it out and there is
44
+ no microphone at all.
45
+
46
+ The kit records — permission, `MediaRecorder`, the level of the incoming
47
+ signal — and hands over a `Blob`. What turns audio into words is a service,
48
+ and a kit that chose one for its consumers would be wrong for most of them,
49
+ so the handler is yours:
50
+
51
+ ```tsx
52
+ <ChatInput
53
+ onTranscribe={async function* (audio, { signal, mimeType }) {
54
+ const res = await fetch("/api/transcribe", { method: "POST", body: audio, signal });
55
+ for await (const chunk of res.body.pipeThrough(new TextDecoderStream())) yield chunk;
56
+ }}
57
+ />
58
+ ```
59
+
60
+ A string, a promise of one, or an async iterable of deltas — the same three
61
+ shapes `onSend` takes. The transcript is inserted **at the caret**, so
62
+ dictating into a half-typed sentence finishes it rather than replacing it.
63
+
64
+ `useVoiceInput` is exported for a host building its own composer.
65
+
66
+ **`SpeechRecognition` is out of reach through this**, because it holds the
67
+ microphone itself and will not accept a recording. Stated here rather than
68
+ discovered: it is the only free transcriber there is.
69
+
70
+ - **`--ick-voice-meter` and `--ick-voice-meter-spread`** — the ring around the
71
+ microphone while it listens, and how far it swells at full volume. A wash of
72
+ the marker rather than a colour of its own.
73
+
74
+ ## 0.47.1 — 2026-09-01
75
+
76
+ ### Changed
77
+
78
+ - **The card whose pane is open wears a neutral ring, not the accent.** The
79
+ marker means "this one *chosen*" — a picked option, a badge, a citation — and
80
+ an artifact being shown beside the conversation is not a choice anybody made
81
+ about it, it is where the reader is. A ring all the way round instead of an
82
+ edge on one side, in ink at 45% so it inverts: `--ick-border-bright` is
83
+ literally `rgb(paper)`, a visible edge on a dark card and nothing at all on a
84
+ white one.
85
+
86
+ ## 0.47.0 — 2026-09-01
87
+
88
+ ### Changed
89
+
90
+ - **One column down an answer.** New `--ick-answer-column`: everything that
91
+ draws in a turn starts on it, and only the boxes bleed wider.
92
+
93
+ An answer had two families, each consistent with itself. Things in its flow
94
+ began at the turn's own edge — the prose, the actions row, and the four
95
+ disclosures that carry no surface at all (`<Reasoning>`,
96
+ `<ChainOfThought>`, `<TaskList>`, `<Sources>`). Anything in a box began at
97
+ its ground's padding plus its card's column. Two families 32px apart down one
98
+ answer, which is what "the text does not line up with the card" was.
99
+
100
+ The column is the value the boxed family already reached — a ground's padding
101
+ plus a card's — so `<Tool>`, `<QuestionGroup>`, `<Approval>` and
102
+ `<SystemMessage>` did not move at all. Measured after: all nine components,
103
+ the prose and the actions row on 32.
104
+
105
+ - **`<ArtifactCard>` has its ground back.** It was taken away in 0.46.0 while
106
+ the prose still began at the turn's edge, where a ground under a single card
107
+ was 32px of indent putting the card's words on a different line from the
108
+ answer's. With one column it is the opposite: the ground is what puts them
109
+ *on* it, the same way a tool call's does.
110
+
111
+ ### Fixed
112
+
113
+ - The actions row is inside `.answer`, so giving it a column of its own put the
114
+ icons at 64 — an indent applied twice. Guarded, since nothing about the
115
+ stylesheet says which of the two is the parent.
116
+
117
+ ## 0.46.1 — 2026-09-01
118
+
119
+ ### Fixed
120
+
121
+ - **The pane has 24 of padding everywhere**, inside and out. It is the only
122
+ surface in the kit that holds a whole document rather than rows of a
123
+ component, and a document wants a margin — the nesting chain's 16 is a card's
124
+ gap around its rows, which is a different job at a different size. It was
125
+ also running into the top and bottom of the viewport, and a card that runs
126
+ into the window is not one. Measured: 24 from every edge.
127
+
128
+ ## 0.46.0 — 2026-09-01
129
+
130
+ ### Changed
131
+
132
+ - **The pane takes a third of the room and can be widened to two thirds.** A
133
+ fixed 420px was a strip on a wide monitor and half the screen on a laptop;
134
+ a proportion keeps its proportion. There is a floor either way — a document
135
+ needs a line length before anything else.
136
+
137
+ The control is in the pane's header and the width is `<ChatLayout>`'s, which
138
+ is the same split as everything else here: how much room the pane takes is a
139
+ fact about the layout, and the button that changes it has to be somewhere
140
+ somebody can find it. `pane` is handed `expanded` and `toggleExpanded`
141
+ alongside `narrow`. There is no widening while it is covering the
142
+ conversation, because there is nothing left to take.
143
+
144
+ - **The pane sits away from the edges.** New `--ick-artifact-pane-inset`, on
145
+ the layout's slot rather than on the pane — how far a card is from the edge
146
+ of the window is a fact about the layout. It ran into the top and bottom of
147
+ the viewport, and a card that runs into the window is not one. The covering
148
+ variant drops it along with its corners.
149
+
150
+ ### Removed
151
+
152
+ - **An artifact card's ground.** Every other ground in this kit carries
153
+ something besides its card — a tool call's carries the header, a question
154
+ group's the title, an approval's the asking and the answering. This one held
155
+ a single card and nothing else, which is not a surface but an indent, and it
156
+ read as one: the answer's own prose ran along the turn's left edge while the
157
+ card's words sat 32px inside it, so the two halves of one answer were two
158
+ columns. Measured after: card, prose and the actions row all at 312.
159
+
160
+ ## 0.45.1 — 2026-09-01
161
+
162
+ ### Fixed
163
+
164
+ - **The conversation lost 96px of width the day `ChatLayout` went around it.**
165
+ `.chat` is a flex column, and a child with `margin: 0 auto` is a flex item
166
+ that has opted out of stretching — so the playground's centred page was sized
167
+ by its content instead of by its `max-width`, 624px against 720. Documented
168
+ on `ChatLayout` as well, since any host wrapping an existing centred page
169
+ will meet it.
170
+
171
+ ## 0.45.0 — 2026-09-01
172
+
173
+ ### Added
174
+
175
+ - **The artifact pane** — `<ArtifactCard>`, `<ArtifactPane>`, `<ChatLayout>`,
176
+ `useArtifacts`, and an `artifact` part so a stream can send one.
177
+
178
+ **The kit decides the layout.** On the right, with the conversation making
179
+ room; below `<ChatLayout>`'s own width it covers the conversation instead. A
180
+ preview pane is one of the few patterns every AI chat now has, and the worth
181
+ of a pattern is that it is the same every time.
182
+
183
+ **The kit does not decide the content.** `<ArtifactPane>` takes children,
184
+ because a plan, a table and a diagram are the part that differs between
185
+ products — and the only part.
186
+
187
+ The card is a window: eight lines with a fade at the cut, because an artifact
188
+ poured out in full is a long message rather than an artifact. It arrives
189
+ before its content does, which is how one really arrives, and shimmers its
190
+ own name while it is being written.
191
+
192
+ `modal` is the one prop that changes behaviour, and it is not about position.
193
+ On open, focus moves to the pane's heading — not into its first control,
194
+ which would skip what the thing is — and it is **not** trapped unless the
195
+ pane is covering the conversation, where trapping it would lock a reader out
196
+ of the chat they are still reading. `<ChatLayout>` sets it from its width.
197
+
198
+ A container query rather than a media query: the pane answers to the width it
199
+ has. A kit inside a 480px column on a wide page would otherwise put a 420px
200
+ pane beside a 60px conversation.
201
+
202
+ ### Fixed
203
+
204
+ - Two things the axe pass caught in the pane and reading did not:
205
+ `role="dialog"` is not allowed on an `<aside>`, and a `<header>` inside a
206
+ landmark is a banner landmark claiming to be the page's.
207
+
208
+ ## 0.44.0 — 2026-09-01
209
+
210
+ ### Added
211
+
212
+ - **`<SystemMessage>`** — the conversation saying something about itself, and a
213
+ `notice` part so a stream can send one. `<Context>` warns from 80% that the
214
+ oldest messages will start dropping out and then goes quiet at the moment
215
+ they do; this is what says so.
216
+
217
+ Three absences, each of them the component rather than a corner cut. **No
218
+ icon**: every picture in this kit carries a state the words beside it also
219
+ carry, so one here would say "something is being announced" beside a sentence
220
+ announcing it. **No dismiss and no action**: it is a line of the transcript,
221
+ not a toast. **No live region**: the kit has one, written to on a later tick,
222
+ and a second says everything twice.
223
+
224
+ Two tones — `notice` and `danger` — because `<Context>` already settled that
225
+ an amber in the middle makes somebody learn a scale to read a state they can
226
+ read in words.
227
+
228
+ Five tokens, all pointing at the stack: the ground it sits on, the row corner,
229
+ the danger tint the tool's error section already uses.
230
+
231
+ - The playground says it when the window actually fills, and announces it
232
+ through `announce` — which is what a host streaming a `notice` should do.
233
+
234
+ ## 0.43.1 — 2026-09-01
235
+
236
+ ### Fixed
237
+
238
+ - **A tool call inside an approval had no room above its header.** Zeroing
239
+ `--ick-tool-ground-pad` put the header's glyph on the column the title and
240
+ the buttons stand on, and put the header flush against the card's top edge.
241
+ One axis was measured and the other was not.
242
+
243
+ It is `var(--ick-nest-pad) 0 0` now: nothing sideways, so the column holds;
244
+ 8 above, so the header has room; nothing below, because the tool's body ends
245
+ in its own gap already. Measured in both states — open and collapsed — the
246
+ card gives 8 above the header and 8 below whatever it ends with.
247
+
248
+ The guard reads the shorthand and states the rule in both directions rather
249
+ than pinning the value: sideways must be nothing, above must not be. Verified
250
+ to fail each way.
251
+
252
+ ## 0.43.0 — 2026-09-01
253
+
254
+ ### Removed
255
+
256
+ - **The shield.** It said "this is about permission" in a picture, beside a
257
+ title that says it in words, above a card showing the thing and three buttons
258
+ that are visibly a decision. Every other glyph in the kit carries *state* —
259
+ queued, running, failed, allowed, denied — and pairs with a word for anybody
260
+ the picture is not reaching. This one carried a category nothing else could be
261
+ mistaken for.
262
+
263
+ It was also drawn at **24 against everything else's 14**: `.glyph` set the
264
+ badge's width and height on the icon itself rather than on a box around it,
265
+ so the `size={15}` in the component was never what the browser used. Which is
266
+ how it came up at all — it looked too big, and the answer to "how big should
267
+ it be" turned out to be that it should not be there.
268
+
269
+ `--ick-approval-glyph` goes with it, and the title now starts on the column
270
+ directly rather than after a badge slot.
271
+
272
+ ### Changed
273
+
274
+ - **The approval draws the card; the subject brings what goes in it.** A tool
275
+ call inside one brings no surfaces of its own — not its ground, not its card,
276
+ not its shadow.
277
+
278
+ This is the third arrangement and the reason is worth writing down. The
279
+ approval's card wrapping all three (title, subject, buttons) made an approval
280
+ holding a tool call two papers for one thing. Letting the tool bring its own
281
+ card instead left its header stranded on the ground beside the title — two
282
+ headers over one card, and the second of them a stray row. One card, holding
283
+ everything that belongs to the subject.
284
+
285
+ `.subject` takes no padding, deliberately: what is inside is padded to the
286
+ column already, so a gap there would push all of it eight pixels off the line
287
+ the title and the buttons stand on. Measured: title, tool glyph and overview
288
+ all at 48; chevron and the primary button both ending at 504.
289
+
290
+ ## 0.42.0 — 2026-09-01
291
+
292
+ ### Changed
293
+
294
+ - **`<Approval>` asks above the card and answers below it.** The title and the
295
+ buttons are on the ground; the card between them is the subject. They are not
296
+ part of the thing being approved — they are the asking and the answering.
297
+
298
+ The card is gone with them, because the subject already brings one. An
299
+ approval holding a tool call was a card inside a card: two papers, two
300
+ shadows, one thing. That is also why the tool no longer has to be flattened
301
+ into a row on it — an approval sets `--ick-tool-ground: transparent` and
302
+ nothing else, so a tool call inside one is a tool call.
303
+
304
+ - **Everything lines up on the subject's own column.** The shield where the
305
+ tool's glyph is, the buttons ending where its panels end. Measured: shield
306
+ box, tool glyph and overview text all at 48, chevron and the primary button
307
+ both ending at 504, against a card at [32, 520].
308
+
309
+ - **Deny is pulled back by its own padding.** A ghost button is *text* — what
310
+ you see is the word, not the invisible box around it — and the word sat 16px
311
+ inside the line the shield above it is on. The two that say yes are the other
312
+ way round: filled, so the box is the visible thing and its edge is what lines
313
+ up. A guard ties the pull-back to `Button`'s own padding rather than to a
314
+ number that happens to look right.
315
+
316
+ ### Added
317
+
318
+ - **`--ick-nest-column`**, the column a card's content starts in, stated once.
319
+ Three components have to agree on it now — a question's rows, a tool call's
320
+ panels, and an approval's title and buttons standing over one — so
321
+ `--ick-question-pad`, `--ick-tool-pad` and `--ick-approval-column` all read
322
+ it rather than three numbers happening to match.
323
+ - An approval repoints `--ick-code-pad` to that column, so a subject that is a
324
+ bare code block lands on it too. Its own 12 would have sat four pixels inside.
325
+
326
+ ### Removed
327
+
328
+ - **The approval's own `.card`.** With it goes the nesting row that checked its
329
+ corner, and the guards that required a tool call inside one to be a row: both
330
+ described the arrangement that caused the doubled paper.
331
+
332
+ ## 0.41.0 — 2026-09-01
333
+
334
+ ### Changed
335
+
336
+ - **`<Tool>` is a question group's shape now: a header on the ground, a card
337
+ under it.** The header was the card's own top edge, which meant it had to
338
+ move whenever the card grew, and — the reason it was raised — that everything
339
+ in it was measured against the **card's edges**. That is what put the chevron
340
+ hard into the corner with nothing beneath it to agree with.
341
+
342
+ On the ground it takes `--ick-disclosure-inset`, so the glyph starts on the
343
+ column the card's panels start on and the chevron ends where they end.
344
+ Measured: glyph left 48 and chevron right 504, against a card at [32, 520] —
345
+ 16 in from each edge, the same column a question card uses.
346
+
347
+ - **The card says something when it is shut.** One row — the call's summary, or
348
+ the state in words when it has no summary — padded to the card's column and
349
+ 40 tall, which is a folded question group's row exactly, so a stack of the
350
+ two lines up.
351
+
352
+ It stays when the call opens, and that is a deliberate difference from the
353
+ question group. There the two bodies swap, and because both are anchored to
354
+ the same top edge they want the same band and cannot be crossfaded without
355
+ drawing one over the other. Here nothing swaps: the summary is the card's
356
+ first row in both states and the detail opens underneath it. Same shape, one
357
+ fewer thing to go wrong.
358
+
359
+ - **No hover wash on a tool's header**, for the reason a question group has
360
+ none: the wash is a rounded box the width of the header, and on a ground
361
+ there is nothing under it for that shape to agree with.
362
+
363
+ ### Removed
364
+
365
+ - **`DisclosureHeader`'s `filled` variant.** `<Tool>` was its only consumer, so
366
+ moving the header onto the ground left the prop, two CSS rules and a guard
367
+ that "guarded" a variant nothing rendered. The guard now makes the same
368
+ claim — a tool shut is a folded question's row — against the row that
369
+ actually draws it.
370
+
371
+ ### Added
372
+
373
+ - `--ick-tool-pad`, the column a tool card's content starts in, the same job
374
+ and the same number as `--ick-question-pad`. A guard computes it from how the
375
+ card actually reaches that column — its own padding plus a panel's — so if
376
+ either moves this fails rather than quietly meaning something else, and
377
+ requires it to equal the question card's. One rule, not two that agree.
378
+
379
+ ## 0.40.0 — 2026-09-01
380
+
381
+ ### Fixed
382
+
383
+ - **The two bodies are never drawn over each other.** Recorded and looked at
384
+ frame by frame, what read as a flicker was a superimposition: the folded row
385
+ and the first card are anchored to the same top edge, so they always want the
386
+ same 40px band, and crossfading them put
387
+
388
+ ```
389
+ 3 answers About them · Household · Support needed
390
+ 1 About them Milica Stevanović 84 +1
391
+ ```
392
+
393
+ on top of each other at half opacity each. No timing fixes that, because the
394
+ overlap **is** the crossfade — a dissolve between two different sentences is
395
+ mush whatever its duration.
396
+
397
+ The arriving body waits for the leaving one to be gone. Measured across both
398
+ directions: the two never carry ink at the same time. What carries the eye
399
+ across the handover is the box, which is growing throughout — so the pause
400
+ costs nothing, and there is no bare frame at real speed.
401
+
402
+ ### Added
403
+
404
+ - **`FoldMotion.fadeInDelay`**, back with a job this time: it is what holds the
405
+ two bodies apart, not a stylistic pause. `fadeOut` is 0.08 and the delay
406
+ matches it. A guard requires `fadeInDelay >= fadeOut`.
407
+
408
+ ### Changed
409
+
410
+ - **`FoldingSlowly` scales every number measured in seconds**, derived from
411
+ `defaultFoldMotion` rather than listed by hand. Listing them is how the story
412
+ started lying: it scaled the two fades and not the delay between them, so it
413
+ showed a long crossfade the component does not do. A slowed-down story that
414
+ shows something else is worse than none, because it is the one you trust to
415
+ see the detail — and it disagreed with the instrumented measurement, which is
416
+ how it was caught.
417
+
418
+ - Two fold guards match loosely on the source instead of pinning the exact
419
+ spelling of a call. Both broke on a refactor that changed nothing about the
420
+ behaviour, which is a guard failing on a rename while it would pass on a
421
+ wrong number. The number they were standing in for is asserted directly now.
422
+
423
+ ## 0.39.0 — 2026-09-01
424
+
425
+ ### Changed
426
+
427
+ - **Collapsing is expanding run backwards.** A row leaving was a plain tween
428
+ while a row arriving was sprung, and the two bodies staggered in opposite
429
+ directions — last out, first in. The reverse is right when something is being
430
+ *dismissed*, because it unwinds the way it was built; this is not a
431
+ dismissal, it is one body replaced by another on the same edge, holding the
432
+ same answers. One spring builder now serves both states, and both stagger
433
+ forwards.
434
+
435
+ Sampled frame by frame in both directions: the first row leads either way,
436
+ and `y` runs 0 → -10 on the way out exactly as it runs -10 → 0 on the way in.
437
+
438
+ - **`rowBounce` is 0.12**, down a third from 0.18. Measured, `y` no longer
439
+ passes its target at all on the way in — it settles rather than arriving and
440
+ correcting.
441
+
442
+ ## 0.38.4 — 2026-08-31
443
+
444
+ ### Fixed
445
+
446
+ - **Text beside a badge takes the badge's line box.** A badge is 24 tall with
447
+ `line-height: 1`; text beside it inherited whatever line height its type
448
+ gave it, and two line boxes of different heights, both centred in the same
449
+ row, land half a pixel apart. Measured with a `Range` over the text itself,
450
+ the folded group's summary sat at 86.5 against the count's 87.
451
+
452
+ `.title` and `.optionTitle` had the rule already — the line box takes the
453
+ badge's height rather than the badge taking a margin, because a margin is a
454
+ guess that has to be re-guessed whenever the type changes. `.summaryList`,
455
+ `.collapsedTitle` and `.upcomingLabel` take it now too, and a guard walks all
456
+ five.
457
+
458
+ Only `.summaryList` moved: the card's own rows were already baseline-aligned,
459
+ the badge and the title simply being different type sizes. The rule is stated
460
+ on all of them so the next one does not have to be found by eye.
461
+
462
+ ## 0.38.3 — 2026-08-31
463
+
464
+ ### Fixed
465
+
466
+ - **A folding row gets a compositor layer while it moves, and gives it back.**
467
+ Motion animates `y` as an independent transform, and an independent transform
468
+ does not promote the element on its own — sampled through a whole fold, every
469
+ row read `will-change: auto`. Motion's own guidance is to name the properties
470
+ being animated and then take the hint away again, since a permanent hint is a
471
+ permanent layer. `[data-moving]` on the ground is on for the length of one
472
+ fold: measured, the layers exist from the click to about 460ms and are gone
473
+ by 500.
474
+
475
+ The taking-away needed care. Both bodies report completion and the leaving
476
+ one finishes first, so clearing on the first report took the layers back at
477
+ 165ms with the rows still travelling until 300. It clears on the *arriving*
478
+ body now.
479
+
480
+ ### Changed
481
+
482
+ - The stylesheet guards look up a rule by its selector at the start of a line.
483
+ A plain substring found `.summary` inside `.group[data-moving] .summary` and
484
+ read a `will-change` rule when it wanted the padding one.
485
+
486
+ ## 0.38.2 — 2026-08-31
487
+
488
+ ### Fixed
489
+
490
+ - **Every badge in a question card is one size.** `--ick-chip-height` was 22
491
+ against the badge's 24 — measured across every state the card has, the chip
492
+ was the only box in it that was not 24, two pixels shorter than the numbered
493
+ badge at the other end of its own row. It points at `--ick-badge-size` now,
494
+ so a host that resizes one resizes both.
495
+
496
+ ## 0.38.1 — 2026-08-31
497
+
498
+ ### Fixed
499
+
500
+ - **An answer chip corners like the badge beside it.** New `--ick-chip-radius`,
501
+ pointed at `--ick-nest-inner` — the chain's innermost corner, the one a badge
502
+ already takes. They sit in the same row, the number at one end and the answer
503
+ at the other, and a fully round pill beside an 8px badge is two shapes for one
504
+ level of the nesting. `--ick-radius-pill` still means pill; buttons and the
505
+ composer are what want one.
506
+
507
+ ## 0.38.0 — 2026-08-31
508
+
509
+ ### Changed
510
+
511
+ - **The fold's bodies arrive on springs, one row at a time.** 0.37.0 fixed the
512
+ distortion and left the swap itself a crossfade — two blocks dissolving
513
+ through each other. A dissolve is what you reach for when two things are
514
+ unrelated, and the summary row and the stack of cards are the same answers in
515
+ two states. It read as the box moving while the content sat there bleeding
516
+ through itself.
517
+
518
+ Rows now enter a little above their place and settle into it, one after the
519
+ next, and leave the same way; the ground follows them. Both sides of the fold
520
+ are lists of rows, even the folded side that holds one, so both arrive by the
521
+ same rule.
522
+
523
+ - **Springs for what travels, a tween for opacity.** Not a preference. A spring
524
+ describes where a thing is going and how it arrives, and opacity has nowhere
525
+ to go — bounded at 0 and 1, so a spring with any bounce overshoots into a
526
+ clamp and spends the overshoot sitting still. Position and size have no
527
+ ceiling, which is what makes them worth springing.
528
+
529
+ ### Breaking
530
+
531
+ - **`FoldMotion` lost `fadeInDelay` and gained four.** `rowDuration`,
532
+ `rowBounce`, `rowOffset` and `stagger` describe a row arriving; the delay is
533
+ gone because the stagger is what sequences them now. `visualDuration`,
534
+ `bounce`, `fadeIn` and `fadeOut` are unchanged in meaning.
535
+
536
+ Passing the old shape still type-checks for every key it kept and the rest
537
+ fall back to `defaultFoldMotion`, so nothing breaks silently — a `fadeInDelay`
538
+ in an override is simply ignored.
539
+
540
+ ### Fixed
541
+
542
+ - **`package.json` was published empty in 0.37.0.** A `open(path, "w")` in the
543
+ script that bumped the version truncated the file before the read that was
544
+ supposed to fill it. Restored from 0.36.1 with the version applied.
545
+
546
+ ## 0.37.0 — 2026-08-31
547
+
548
+ ### Fixed
549
+
550
+ - **The question group's fold no longer distorts everything inside it.** Motion
551
+ animates a size the only way it can: it puts the new one in the DOM and
552
+ scales the box back. Everything in that box that is not itself a layout child
553
+ rides the scale — and the section title was a plain `<div>`. Sampled frame by
554
+ frame over one open: the header went from 23px to **11.67 in a single frame**
555
+ and stretched back over the next 450ms, the title with it, the cards too.
556
+ The header wrapper and the body are `layout="position"` now, on the ground's
557
+ own transition, and the header measures a flat 23 through the whole thing.
558
+
559
+ - **The leaving body stays where it was.** `AnimatePresence mode="popLayout"`
560
+ makes it `position: absolute`, and with no positioned parent it resolved
561
+ **67px down the page** and faded out somewhere it had never been. The body is
562
+ `position: relative`.
563
+
564
+ - **And it stays inside the ground.** While the ground shrank, the popped-out
565
+ list stayed pinned at full height and hung three rows out of the bottom over
566
+ whatever was underneath. The ground clips; its own 16 of padding is more than
567
+ the float shadow reaches, so card shadows are unaffected.
568
+
569
+ - **The fold is a crossfade, not a relay.** The arriving body waited 100ms for
570
+ the leaving one — a tenth of a second of grown, empty box, which is most of
571
+ what read as the flicker. Measured at the midpoint, the two now sum to about
572
+ 0.9 of an opaque body; they summed to 0.4.
573
+
574
+ ### Added
575
+
576
+ - **`defaultFoldMotion` and a `foldMotion` prop** on `<QuestionGroup>`, passed
577
+ through `<ChatTurnRow>`. Five numbers: `visualDuration` and `bounce` for the
578
+ box, and `fadeIn` / `fadeInDelay` / `fadeOut` for the bodies.
579
+
580
+ `visualDuration` rather than stiffness and damping on purpose. The two of
581
+ them describe the same spring without either one answering "how long is
582
+ this", which is the only question anybody tuning it is asking.
583
+
584
+ - **A `FoldingSlowly` story**, the same fold at a quarter speed — because
585
+ everything that goes wrong in it goes wrong in about eighty milliseconds.
586
+ - `src/__tests__/foldMotion.test.ts` — the four rules above, each verified to
587
+ fail by putting the fault back.
588
+
589
+ ## 0.36.1 — 2026-08-31
590
+
591
+ ### Fixed
592
+
593
+ - **The count on a folded question group is a badge.** It stands in the badge
594
+ column, on a card, one fold away from the numbered badges it stands in for —
595
+ and it was a pill: 2px of padding, the smallest type in the kit and the
596
+ ground's fill, which came out about half their height. Same box now, and only
597
+ the width differs, because this one holds words rather than a numeral.
598
+
599
+ ## 0.36.0 — 2026-08-31
600
+
601
+ ### Changed
602
+
603
+ - **The dark theme's ground goes down now.** `--ick-ground` is a wash of ink,
604
+ and ink in the dark is white — so the recess a group of cards sits in came
605
+ out *lighter* than the page, with the card lighter again. Three surfaces
606
+ stacked on the same side of the page, and a card that read as a slightly
607
+ different patch of its own ground.
608
+
609
+ It never showed up as a number, because the number was fine: ground to card
610
+ measured 1.11 in the light and 1.115 in the dark, which is as matched as two
611
+ themes get. What the light has and the dark cannot is the shadow. A dark
612
+ shadow on a white ground is plainly visible and does half the work of lifting
613
+ a card; the same shadow on a near-black ground does almost nothing. So in the
614
+ dark the tone has to carry both jobs, and 1.11 is one job's worth.
615
+
616
+ New `--ick-dark-ground`, a shade rather than a wash of ink, which puts the
617
+ ground at 10 against the card's 38 and the pair at 1.32. Every ground in the
618
+ kit reads it — question groups, tool calls, approvals.
619
+
620
+ - **A section title stands on the column its cards' words start on.** It was
621
+ sixteen pixels left of the numbers under it, and its chevron four pixels off
622
+ the pencils. New `--ick-disclosure-inset` moves a header's own content inward
623
+ from both ends at once; `<QuestionGroup>` sets it to `--ick-question-pad`.
624
+
625
+ - **A disclosure header's box is its container's content column**, widened by
626
+ its own padding so only the hover wash reaches past it. It was `width: 100%`
627
+ pulled back on the left alone, which left the chevron six pixels shy of an
628
+ edge every other row in the kit sits on. Affects every header that folds —
629
+ `<Tool>`, `<Reasoning>`, `<ChainOfThought>`, `<TaskList>`, `<Sources>`.
630
+
631
+ - **A question's folded rows are padded to the same column on both sides.** The
632
+ right was 8 against the left's 16, so the pencil sat half a column nearer its
633
+ edge than the badge sat to its own. New `--ick-question-pad` names that
634
+ column, and the rows, the summary and the title all read it.
635
+
636
+ - **No wash under a section title on hover.** It is a rounded box the width of
637
+ the header and there is nothing under it for that shape to agree with, so it
638
+ read as a stray highlight off the card grid. The chevron lights up instead —
639
+ which every header now does, wash or no wash. New `--ick-disclosure-hover`,
640
+ set to `transparent` by `<QuestionGroup>`.
641
+
642
+ ### Added
643
+
644
+ - `src/__tests__/alignment.test.ts` — the columns, with the browser's own
645
+ arithmetic done on them rather than a check that particular tokens were
646
+ spelled a particular way. Two rows can reach the same column through
647
+ different tokens and both be right.
648
+ - A surfaces guard that composites the dark ground over the page, takes its
649
+ contrast against the card, and requires more of it than the light theme has —
650
+ because the light theme has a shadow helping and the dark does not.
651
+
652
+ ## 0.35.0 — 2026-08-31
653
+
654
+ ### Added
655
+
656
+ - **`<QuestionGroup>` takes a `title`** — what the step is about, at the top of
657
+ the group — and `TurnPart` of kind `question` carries it through.
658
+
659
+ ### Changed
660
+
661
+ - **The fold control is that header, and it does not move.** It was the summary
662
+ card itself: folded, a card at the top; expanded, a row at the bottom. One
663
+ control in two places, so opening the group meant one shape leaving and
664
+ another arriving somewhere else, and both directions read badly however the
665
+ animation was tuned.
666
+
667
+ A header that stays put turns the whole thing into one control with two
668
+ bodies — the questions, or the one row they fold into. Which is also the
669
+ shape `<Tool>`, `<TaskList>` and `<Sources>` already fold in, so the kit has
670
+ one folding idiom rather than two.
671
+
672
+ Without a title, a foldable group names its control with the count. Without
673
+ either, there is no header and the group is the list, as before.
674
+
675
+ ### Removed
676
+
677
+ - `labels.hide` on `<QuestionGroup>`. It worded a pill that no longer exists —
678
+ the header carries the section's name in both states, so there is nothing
679
+ left to word.
680
+
681
+ ## 0.34.0 — 2026-08-31
682
+
683
+ ### Fixed
684
+
685
+ - **Shadows are dark in the dark.** `--ick-shadow-1`, `-2`, `-3` and `-inset`
686
+ are built on `--ick-ink-rgb`, which is near-black in the light and
687
+ `245 245 245` in the dark — so every `primary` and `secondary` button was
688
+ casting a **white glow**. Measured: the outer layer of `shadow-2` lifted the
689
+ page from 18 to 40, brighter than the card it sat on.
690
+
691
+ Four `--ick-dark-shadow-*` counterparts, made of the shade, which is black in
692
+ both themes. The light values are untouched. A test now says every ink-based
693
+ shadow needs a dark counterpart and every dark one is made of the shade — it
694
+ found `-3` on its first run, which nothing in the kit draws with and which
695
+ would have been wrong the day somebody reached for it.
696
+
697
+ - **`<QuestionGroup>` folds with one control instead of two.** It had a
698
+ full-width card when folded and a centred pill underneath the list when
699
+ expanded — two shapes for one job, so the fold cross-faded a button through a
700
+ div and neither knew where the other had been.
701
+
702
+ The same element now, kept mounted in both states, so Motion moves it rather
703
+ than replacing it. Label left, chevron hard right, and the chevron **turns**
704
+ rather than being swapped for a second glyph.
705
+
706
+ - **The chevron had no gap.** It was passed as a child, so it landed inside the
707
+ button's label span rather than in the trailing icon slot — "Hide answers⌃"
708
+ with the caret against the word. There is no `<Button>` there at all now, but
709
+ the same mistake is worth naming: `iconRight` is the slot.
710
+
711
+ ## 0.33.2 — 2026-08-31
712
+
713
+ ### Fixed
714
+
715
+ - **A badge sits in the middle of the line it belongs to.** The badge is 24
716
+ tall and a title line was 16, both top-aligned — which put the number **four
717
+ pixels** below the middle of the question, and a letter **two** below the
718
+ middle of its option. Measured in a browser; at four pixels it reads as wrong
719
+ long before anybody can name it.
720
+
721
+ The text's line box takes the badge's height, so the two centre together and
722
+ stay centred if the type changes — rather than a margin on the badge, which
723
+ is a number that has to be re-guessed every time either side moves. Field
724
+ rows were the only ones already right, because `.fieldLabel` had been doing
725
+ exactly this all along.
726
+
727
+ On the first line, deliberately: a question long enough to wrap keeps its
728
+ badge on the line it starts on.
729
+
730
+ - **The corner stops breathing during a morph.** A `layout` animation does not
731
+ resize a box, it **scales** one — and a browser scaling a box scales the
732
+ corner with it. Measured on a group opening a card: `scaleY` ran 0.932 → 1
733
+ while `border-radius` stayed a flat 40px, painting a 40 × 37 ellipse that
734
+ eased back to a circle.
735
+
736
+ Motion has a corrector for this and it was not running: it only touches
737
+ values Motion is *managing*, and a radius living in a CSS class is invisible
738
+ to it. The number is now read off the element and handed back through
739
+ `style`, where Motion can see it — read rather than hard-coded, since the
740
+ corner is a token a host may retune. Both `<QuestionGroup>` and
741
+ `<QuestionCard>` do it; the card is the one that scales furthest, morphing
742
+ between a tall card and a 40px row.
743
+
744
+ ## 0.33.1 — 2026-08-31
745
+
746
+ ### Fixed
747
+
748
+ - **The float shadow is softer in the dark.** It was black at 50% and 35% — six
749
+ times the light theme's alpha, on the reasoning that a shadow in the dark has
750
+ to darken something already dark. It overshot: measured on a rendered frame,
751
+ the ground under a card is `26 26 26` and the shadow's core was taking it to
752
+ **13**, half its brightness. That is not a card lifted off a page, it is a
753
+ card cut out of one, and the ring around it was the first thing you saw.
754
+
755
+ At 0.28 and 0.18 the core lands at **22** — a four-level dip, the same read
756
+ the light theme gets going 255 → 235. `<Tool>` and `<Approval>` take the same
757
+ token, so all three lift the same way again.
758
+
759
+ ## 0.33.0 — 2026-08-31
760
+
761
+ ### Added
762
+
763
+ - **`<Branch>`, and regenerating no longer throws the old answer away.**
764
+ Re-submitting a turn overwrote `ai` and `parts` — so the answer you were
765
+ comparing against was gone the moment the second one started, and comparing
766
+ is the only reason anybody presses regenerate.
767
+
768
+ - `ChatTurn.versions: TurnVersion[]` and `versionIndex`, both optional, so a
769
+ turn a host built by hand still renders.
770
+ - `useChatTurns` returns **`showVersion(id, index)`**. Out of range is
771
+ ignored rather than clamped: asking for version 7 of a turn with two is a
772
+ bug, and quietly showing the last one hides it.
773
+ - `<ChatTurnRow>` takes `onShowVersion` and draws the control beside the
774
+ answer actions.
775
+
776
+ `<Branch>` draws **nothing** below two answers. A control reading "1 of 1"
777
+ offers to take you nowhere, so a turn answered once looks exactly as it did.
778
+
779
+ `ai`/`parts` are still the answer on screen and stay equal to
780
+ `versions[versionIndex]` through one function that both writers — the batched
781
+ stream flush and the turn patcher — go through. The first version of this had
782
+ two, and the archive stayed empty while the screen filled.
783
+
784
+ ## 0.32.2 — 2026-08-31
785
+
786
+ ### Fixed
787
+
788
+ - **A tool call's sections all start on one column.** A fenced value is a
789
+ `CodeBlock` — its own panel, with its own inner padding. A string value was a
790
+ bare stack: no surface, no padding of its own, so its label and its text sat
791
+ flush at the body's 8px while the block's sat 12px inside the block. Two
792
+ sections of one tool call at two left edges.
793
+
794
+ A text section is the same panel now, and both read `--ick-code-pad`. `Tool`
795
+ repoints that to 8, so every label, every value and the code itself land
796
+ **16 from the card's edge** — the column the header's glyph is in. It was 20,
797
+ which is the kind of four-pixel near-miss that reads as wrong without being
798
+ nameable.
799
+
800
+ - **A failed section is that panel tinted, not a second panel inside it.** The
801
+ error box sat inside the section's box, at a third inset again.
802
+
803
+ ### Added
804
+
805
+ - `--ick-code-pad`, the inner padding of a `<CodeBlock>` — its label's left
806
+ edge and its code's. A box that nests one repoints it, the way it already
807
+ repoints the fill and the corner.
808
+
809
+ ## 0.32.1 — 2026-08-31
810
+
811
+ ### Fixed
812
+
813
+ - **The composer's attachment section is back.** Making attachments survive
814
+ the send should not have touched how they look, and it did: the tray lost its
815
+ surface and its corner and became a bare padded row. It is a section at the
816
+ top of the composer again — its own paper, its own corner, and transparent
817
+ once the bubble goes glass, which is how it has looked since it held one
818
+ image. The thumbnail keeps its `--ick-radius-md` corner and its lift.
819
+
820
+ `--ick-attachment-radius` names that corner so it is a token rather than a
821
+ number somebody has to remember not to change.
822
+
823
+ ## 0.32.0 — 2026-08-31
824
+
825
+ ### Added
826
+
827
+ - **`<Attachments>`, and attachments that survive the send.** The composer
828
+ could already pick an image and show it. It could not send it: `onSubmit`
829
+ took the text and nothing else, so the message went and the picture did not.
830
+ Offering an attach button and then losing what it attached is worse than not
831
+ offering one.
832
+
833
+ - `onSubmit` is `(value, attachments)`.
834
+ - `SendContext` carries `attachments`, so a handler gets the file and the
835
+ sentence together.
836
+ - `ChatTurn.attachments`, and `<ChatTurnRow>` hands them back to the composer
837
+ — the bubble *is* the message, so the same component that held them shows
838
+ them afterwards, read-only.
839
+ - A picture on its own is a message: an empty box with something attached
840
+ still sends.
841
+
842
+ `<Attachments>` draws them. An image with a `url` shows itself; everything
843
+ else gets a glyph, its name and its size, because a thumbnail of a PDF at
844
+ 64px is a grey rectangle with a corner turned down. `onRemove` is the whole
845
+ difference between the composer's copy and the sent message's.
846
+
847
+ - **`<ChatInput>` takes `attachments`, `onAttach`, `onRemoveAttachment`,
848
+ `accept` and `multiple`.** Controlled if given, its own otherwise — the rule
849
+ `useDisclosure` already follows. `onAttach` is for a host that wants to
850
+ upload first and attach the URL it gets back.
851
+
852
+ ### Fixed
853
+
854
+ - **The object URLs are revoked.** `createObjectURL` pins the file in memory
855
+ until it is, and nothing was revoking: attaching and removing an image ten
856
+ times leaked ten of them, replacing one leaked the one it replaced, and
857
+ navigating away with one attached leaked that. Now on remove, on replace, and
858
+ on unmount — and only the ones the composer made, since a URL the host passed
859
+ in is the host's.
860
+
861
+ ## 0.31.1 — 2026-08-31
862
+
863
+ ### Fixed
864
+
865
+ - **A citation in a sentence takes the width of its number.** It was the same
866
+ 16px square the source list uses — right in a list, where the numbers sit in
867
+ a column and have to line up, and wrong in running prose. A single digit is
868
+ about five pixels wide, so five and a half pixels of empty badge sat either
869
+ side of it and whatever followed — a comma, most often — came after that gap
870
+ and read as detached from the number it belongs to.
871
+
872
+ Padding in `em` so it tracks the number, and no minimum: what makes it a
873
+ badge is a fill, a corner and the line's height, not a width. The one in the
874
+ list keeps its square.
875
+
876
+ ## 0.31.0 — 2026-08-31
877
+
878
+ ### Changed
879
+
880
+ - **An approval is not tinted any more.** It stands on the same neutral ground
881
+ a question group does, because it *is* a question: "may I run this" is the
882
+ same species as "who else lives in the household" — the assistant asking,
883
+ blocking, with an answer.
884
+
885
+ The tint was there on the reasoning that this is the one thing in an answer
886
+ allowed to look like a box that stops you. The reasoning is real; colour was
887
+ the wrong instrument for it. Everywhere else in the kit the marker is a small
888
+ mark pointed at one thing and means **this one** — the badge on the option
889
+ you picked, the stroke you drew, a citation, a source's number. A wash across
890
+ a whole box meaning "this kind of box" is a second job for the one accent.
891
+
892
+ It also did not carry its weight: at 8% it was too weak to be an alarm and
893
+ too strong to be nothing, and in the dark it came out olive, which is not in
894
+ the palette. What says "this is unanswered" is that the box has **buttons in
895
+ it**, and nothing else in a turn does.
896
+
897
+ `--ick-approval-surface` is `var(--ick-ground)`. `--ick-approval-edge` is
898
+ gone with the ring it drew.
899
+
900
+ - **Settled, nothing about the box changes** — the buttons simply leave. It
901
+ used to shed its background and pick up a border, because the tint was the
902
+ thing saying "unanswered" and had to stop saying it. A question card keeps
903
+ its paper and its shadow when it folds into an answered row; this does the
904
+ same.
905
+
906
+ - **The shield rides in a badge's box**, so the title starts on the line a
907
+ question card's title starts on.
908
+
909
+ ### Breaking
910
+
911
+ - `--ick-approval-edge` is removed. If you were overriding it to re-tint the
912
+ ring, set `box-shadow` on the component instead — or override
913
+ `--ick-approval-surface`, which is still the ground it stands on.
914
+
915
+ ## 0.30.0 — 2026-08-31
916
+
917
+ ### Changed
918
+
919
+ - **An approval is a ground holding a card, like everything else.** It was a
920
+ tinted box with the tool call as the only thing on paper — so the title, the
921
+ description and the three buttons sat directly on the ground. A question card
922
+ puts its header *and* its Next button on the card; this was the same three
923
+ surfaces in a different arrangement, which beside one read as a different
924
+ object.
925
+
926
+ ```
927
+ ground 40 (tinted) → card 24 → row 16
928
+ ```
929
+
930
+ The tool call on that card is now a **row** rather than a second card: inset,
931
+ at the row corner, no shadow. Two cards stacked is one surface more than
932
+ there is depth for — the same fault as two grounds, from the other end. What
933
+ the row holds takes the corner inside that, and its fenced value is paper,
934
+ the way a question's badge is paper on a grey row.
935
+
936
+ Settled, the card stops floating along with the tint: nothing is being asked
937
+ any more.
938
+
939
+ - `--ick-approval-radius` is derived from `--ick-nest-card` rather than from
940
+ `--ick-tool-radius`. The tool's corner was standing in for the card's back
941
+ when a tool call was the only thing on the ground.
942
+
943
+ ## 0.29.1 — 2026-08-31
944
+
945
+ ### Fixed
946
+
947
+ - **The card is opaque in the dark too.** `--ick-dark-card` was
948
+ `rgb(var(--ick-ink-rgb) / 0.09)` — a wash. On the page that is
949
+ indistinguishable from an opaque mix; on anything tinted it is not. A tool
950
+ call inside an `<Approval>` sits on the marker at eight percent, and the card
951
+ and the code block inside it both came out **olive** — the panel wearing its
952
+ ground, which is the one thing "the card is paper and opaque" exists to
953
+ prevent.
954
+
955
+ It held in the light, where the card is `#fff`, and was never true in the
956
+ dark, which is where it is easiest to see. `--ick-dark-card` is a
957
+ `color-mix` of paper and ink now, and `--ick-inset` is mixed **from the
958
+ card** rather than washed over whatever is behind it. Same colour on the
959
+ page; committed to rather than borrowed.
960
+
961
+ `--ick-ground` stays a wash on purpose — it is the bottom of the stack, so
962
+ there is nothing under it to pick up but the page.
963
+
964
+ The guard that was meant to catch this checked the **wiring** — that the card
965
+ points at paper in the light and at its own token in the dark — and never
966
+ looked at the value. It reads the value now.
967
+
968
+ ## 0.29.0 — 2026-08-31
969
+
970
+ ### Changed
971
+
972
+ - **One header for everything that folds.** `Tool`, `Reasoning`,
973
+ `ChainOfThought`, `TaskList` and `Sources` each had a row you click to open
974
+ something, written out five times — with two pairs byte-for-byte identical
975
+ and the rest differing in ways nobody had decided on. The shimmer under the
976
+ label was in two of them, twenty lines of gradient each. The reveal beneath
977
+ was the same ten lines of `motion` props in all five.
978
+
979
+ `DisclosureHeader` and `DisclosureBody` are internals, not exports. **Nothing
980
+ moved on screen** — the side-by-side story is pixel-identical.
981
+
982
+ What is *not* unified is the shape, because the difference is real. A `band`
983
+ is a full-width row with a right edge to push the meta and the chevron to;
984
+ `inline` is a label that hugs its own words, for a header that sits in the
985
+ flow of an answer. An inline header's chevron pushed to a right edge 500px
986
+ away floats alone in white space with nothing beside it.
987
+
988
+ - **`--ick-disclosure-*` replaces the per-component header tokens.** `-label`,
989
+ `-font`, `-weight`, `-size`, `-glyph`, `-meta`. A component repoints them
990
+ from its own root when it means something different — `Sources` and
991
+ `TaskList` head a list so their label is ink at medium; `Tool`'s is an
992
+ identifier so it is mono.
993
+
994
+ Repointed rather than overridden with a class, deliberately: two `.label`
995
+ rules in two stylesheets have equal specificity and which one wins is import
996
+ order, which is not a thing to build on.
997
+
998
+ - **The stylesheet is 4.1 kB smaller** (72.81 kB, 12.48 kB gzip), and the entry
999
+ is 4.2 kB smaller.
1000
+
1001
+ ### Removed
1002
+
1003
+ - `--ick-reasoning-glyph` and `--ick-chain-header`, which said what
1004
+ `--ick-disclosure-glyph` and `--ick-disclosure-label` now say. The kit's
1005
+ `<Reasoning>` and `<ChainOfThought>` no longer set a `.header` of their own
1006
+ for anything to point at.
1007
+
1008
+ ## 0.28.0 — 2026-08-31
1009
+
1010
+ ### Added
1011
+
1012
+ - **`data-active-input` on the row that owns the live composer.** Which turn
1013
+ that is, is something `<ChatTurnRow>` knows and a host does not — the class
1014
+ it used to set was a hashed CSS-module name nothing outside the package could
1015
+ target, and no rule in the kit used it either, so it did nothing at all.
1016
+
1017
+ The case that wanted it: a page fading its conversation off the bottom edge
1018
+ has no way to exempt the composer. This kit's composer lives at the *end of
1019
+ the feed*, because it morphs into the message it sends — so after a long
1020
+ answer it lands inside the gradient and comes out washed to nearly nothing.
1021
+ It was never actually blocked; a fade is `pointer-events: none`. It just
1022
+ looked unavailable, which is enough.
1023
+
1024
+ ```css
1025
+ .feed [data-active-input] { position: relative; z-index: 6; }
1026
+ ```
1027
+
1028
+ ## 0.27.0 — 2026-08-31
1029
+
1030
+ ### Changed
1031
+
1032
+ - **The syntax grammars are a chunk, fetched when something needs one.**
1033
+ `lowlight` and eleven grammars were 25 kB gzipped in everybody's bundle for a
1034
+ thing most conversations never show.
1035
+
1036
+ ```
1037
+ entry 60.10 kB gzip → 35.05 kB gzip −42%
1038
+ chunk 25.10 kB gzip only if an answer has a fence
1039
+ ```
1040
+
1041
+ A code block paints its code plain on the first paint and colours in when the
1042
+ chunk lands. Every block after the first is coloured from its first paint,
1043
+ because the loaded highlighter is kept. No layout shift either way: the text
1044
+ is identical, only the colour arrives late.
1045
+
1046
+ Call `loadHighlighter()` at start-up if that trade is wrong for you — a docs
1047
+ tool where every answer is code. Idempotent; concurrent callers share the one
1048
+ fetch. `canHighlight(lang)` still answers without loading anything.
1049
+
1050
+ The grammars live in their own module with **static** imports, and the guard
1051
+ that keeps them there says why: `lowlight`'s entry re-exports `all` (190
1052
+ grammars) beside `createLowlight`, so importing the *package* dynamically
1053
+ materialises the whole namespace and nothing can shake it back out — 301 kB
1054
+ gzip, thirteen times what deferring it saves. The first attempt did exactly
1055
+ that. Static named imports inside a deferred module shake normally.
1056
+
1057
+ ### Added
1058
+
1059
+ - `loadHighlighter` and `canHighlight` are public.
1060
+
1061
+ ### Breaking
1062
+
1063
+ - `highlightCode(code, lang)` is gone from the module's surface; it was never
1064
+ exported from the package. The highlighter now arrives through
1065
+ `await loadHighlighter()`, which returns the same function.
1066
+
1067
+ - A bundler-less consumer loading the ESM directly now sees a second request
1068
+ the first time an answer contains a fence.
1069
+
1070
+ ## 0.26.0 — 2026-08-31
1071
+
1072
+ ### Added
1073
+
1074
+ - **`chain` and `sources` are turn parts.** `<ChainOfThought>` and `<Sources>`
1075
+ were in Storybook and unreachable from a conversation — nothing a stream
1076
+ could send drew them. A `SendHandler` yields
1077
+ `{ kind: "chain", id, steps, state }` and
1078
+ `{ kind: "sources", id, sources, title?, collapsible? }` now, and
1079
+ `<ChatTurnRow>` draws both.
1080
+
1081
+ - **`[^1]` in the prose is a citation.** The number is a position in the turn's
1082
+ `sources` part, and the marker draws as an `<InlineCitation>` carrying that
1083
+ source's title.
1084
+
1085
+ This is the kit's one extension to the markdown grammar, and it exists for a
1086
+ specific reason: a citation is a component, a stream sends text. Before this,
1087
+ `<InlineCitation>` could only be written by hand in JSX — so the component
1088
+ existed and no actual conversation could reach it. GFM spells footnotes the
1089
+ same way but wants a `[^1]: …` definition in the document; a model emits the
1090
+ marker and sends its sources beside the text, never below it.
1091
+
1092
+ The marker is kept **out of the tokens**, so a highlight drawn across the
1093
+ sentence — and the text a thread quotes back — do not contain a stray `[1]`.
1094
+ A marker whose source has not arrived yet still draws, and starts working
1095
+ when the list lands.
1096
+
1097
+ - **`<TextHighlighter>` takes `sources` and `onSelectSource`**, which is how the
1098
+ citations reach it. `<ChatTurnRow>` passes the turn's first `sources` part.
1099
+
1100
+ ## 0.25.1 — 2026-08-30
1101
+
1102
+ ### Fixed
1103
+
1104
+ - **`highlight.js` is declared.** `CodeBlock` imports eleven grammars from it
1105
+ one at a time, and the package listed only `lowlight`. It resolved because
1106
+ `lowlight` depends on `highlight.js` and npm hoists it into reach — so under
1107
+ a strict resolver, or the day `lowlight` picks a different highlighter, it is
1108
+ a consumer's build breaking on an import this package wrote. It is a
1109
+ dependency now, at the version `lowlight` already pulls.
1110
+
1111
+ Nothing new is installed and the bundle does not move; it was always there.
1112
+ What changes is that it is there on purpose.
1113
+
1114
+ ### Added
1115
+
1116
+ - **A test for what installing this costs.** The runtime dependencies, the
1117
+ peers, and every bare import in `src` checked against both — so an import
1118
+ cannot outrun the manifest again, and the playground's dev tooling (DialKit,
1119
+ the perf HUD) cannot cross into the package by being convenient. It found the
1120
+ `highlight.js` gap on its first run.
1121
+
1122
+ ## 0.25.0 — 2026-08-30
1123
+
1124
+ ### Changed
1125
+
1126
+ - **One nesting chain for the whole kit.** A tool call and a question card are
1127
+ the same object holding different things, and now they are built out of the
1128
+ same four numbers.
1129
+
1130
+ ```
1131
+ --ick-nest-inner 8 a badge, a chip, a fenced value
1132
+ --ick-nest-row 16 an inset panel: an option, a field, a block of output
1133
+ --ick-nest-card 24 paper, lifted off the ground
1134
+ --ick-nest-ground 40 what the paper stands on
1135
+ ```
1136
+
1137
+ with `--ick-nest-pad` (8) inside a card and a row, and
1138
+ `--ick-nest-ground-pad` (16) around a card on its ground.
1139
+
1140
+ Each component used to derive its own. A tool call was a 6px block in a 14px
1141
+ card on a 22px ground, beside a question's 8 / 16 / 24 / 40 — both internally
1142
+ concentric and the two nothing like each other. The same three surfaces at
1143
+ two scales reads as two systems, not as one object holding different things.
1144
+
1145
+ The reason the tool seeded a tighter chain was that a row folded shut is only
1146
+ 40px tall, and a 24px corner on it clamps to a pill. It does. So does a
1147
+ question folded shut, which is 40px tall and carries the same 24. Both come
1148
+ out the same shape, which was the point.
1149
+
1150
+ - **A folded tool call is a folded question, to the pixel.** 40 tall, 16 in
1151
+ from the left, 8 from the right, and the state glyph rides in the 24px box a
1152
+ question's badge sits in — so the name and the title start on one line when
1153
+ the two are stacked. There is a story for it: **Side by side → The same row,
1154
+ twice**.
1155
+
1156
+ - **A tool call's ground is padded like a question group's** — 16 rather than
1157
+ 8, so more of it shows around the card. Its output panels take the row corner
1158
+ (16) rather than a 6, which is what the card's 24 and its 8 of padding imply.
1159
+
1160
+ - **An `<Approval>` is padded like a ground**, because it is one: 16 rather than
1161
+ 12, and its corner follows to 40 — the same box a question group is.
1162
+
1163
+ ### Fixed
1164
+
1165
+ - **The ground in the question stories was cornered by hand** — `padding: 16,
1166
+ borderRadius: 24`, a ground padded like a ground and cornered like the card
1167
+ standing on it. It is `card + padding`, or 40. A test now walks every story
1168
+ for a hand-built ground, since a style object is not a stylesheet and the
1169
+ chain cannot reach into one.
1170
+
1171
+ ### Breaking
1172
+
1173
+ - `--ick-tool-radius` is **24** (was 14), `--ick-tool-inner-radius` **16**
1174
+ (was 6), `--ick-tool-ground-pad` **16** (was 8), `--ick-approval-pad` **16**
1175
+ (was 12). Every one still resolves through the same token name, so an
1176
+ override you had set still works — but if you set one to match the old
1177
+ numbers, it is now out of step with the chain around it. Point it at
1178
+ `--ick-nest-*` instead, or move the seed and let all of it follow.
1179
+
1180
+ ## 0.24.1 — 2026-08-30
1181
+
1182
+ ### Changed
1183
+
1184
+ - **A tool call gets the ground its card stands on.** The last release made it
1185
+ a card; a card needs something to be a card *on*, and on the page alone it
1186
+ was only paper with a shadow. It is a ground holding a card holding its input
1187
+ and output now — the same three the question card has, in the same order.
1188
+
1189
+ `--ick-tool-ground` and `--ick-tool-ground-pad`, with the corner chain
1190
+ running through both: **inset 6 → card 14 → ground 22**.
1191
+
1192
+ Inside an `<Approval>` the tool brings neither a ground nor a shadow, because
1193
+ the approval already is the ground. Two grounds is one more than there is
1194
+ depth for.
1195
+
1196
+ ## 0.24.0 — 2026-08-30
1197
+
1198
+ ### Changed
1199
+
1200
+ - **A tool call is a card, the same object a question is.** Paper, lifted off
1201
+ the page, with what it holds inset into it.
1202
+
1203
+ It was the other way round — a recessed grey strip with white panels inside —
1204
+ which is the same three surfaces stacked backwards. On its own that reads
1205
+ fine; beside a question card it reads as a different system. Lined up in the
1206
+ new *Side by side* story, it was the first thing you saw.
1207
+
1208
+ `--ick-tool-shadow` is new, and an `<Approval>` sets it to `none`: a card
1209
+ that already has a ground under it does not need to float off it as well.
1210
+ Which also retires the three tokens the approval was repointing — saying a
1211
+ tool call is a card is the tool's job now, not something done to it from
1212
+ outside.
1213
+
1214
+ - **A `Side by side` story.** Everything the kit draws, next to everything else
1215
+ it draws. Not a demo: every fault worth fixing so far was found by looking at
1216
+ two things at once, and none of them were visible in one component alone.
1217
+
1218
+ ## 0.23.1 — 2026-08-30
1219
+
1220
+ ### Changed
1221
+
1222
+ - **Three rules removed.** A tool call drew one under its header and a code
1223
+ block one under its label, so an open tool with an input and an output had
1224
+ three stacked rules in a component the size of a paragraph.
1225
+
1226
+ The question card underlines nothing — a row is an inset panel with space
1227
+ around it, and that is the whole of it. Two ways of saying the same thing was
1228
+ one too many, and the surface was already saying it. A test now fails if a
1229
+ rule goes back between stacked boxes.
1230
+
1231
+ A rule down the **side** is a different device and stays: `Reasoning` marks
1232
+ an aside with one, and `ChainOfThought`'s says each step follows from the one
1233
+ above it. Neither is separating stacked boxes.
1234
+
1235
+ ## 0.23.0 — 2026-08-30
1236
+
1237
+ ### Changed
1238
+
1239
+ - **The kit parses markdown itself.** `unified` + `remark-parse` +
1240
+ `remark-gfm` were 31.6 KB gzip — a third of the package — and the slowest
1241
+ thing in it. They are gone from `dependencies`; `lowlight` is the only one
1242
+ left.
1243
+
1244
+ | | before | after |
1245
+ | --- | --- | --- |
1246
+ | bundle | 90.8 KB gzip | **58.2 KB gzip** |
1247
+ | parse, 1.7k chars | 1.95 ms | **0.049 ms** |
1248
+ | parse, 5.2k chars | 5.84 ms | **0.110 ms** |
1249
+
1250
+ That second column is the one that mattered: parsing runs once per frame
1251
+ while an answer streams, so 5.8 ms was a third of a frame spent re-reading an
1252
+ answer that grew by one word.
1253
+
1254
+ The kit only ever used a thin slice — nine block types and nine inline ones,
1255
+ walked straight into a flat token list — so it parses that slice in about
1256
+ three hundred lines. **It is not a CommonMark implementation and does not
1257
+ claim to be.**
1258
+
1259
+ What makes that safe: `remark` stays a **dev** dependency, and a test parses
1260
+ a corpus of real answers through both and compares the finished documents —
1261
+ including every prefix of one, which is what streaming actually parses and
1262
+ where the two first disagreed. Three real differences came out of that and
1263
+ were resolved deliberately: trailing whitespace at the end of a paragraph
1264
+ (matched), a delimiter row that must match the header's cell count before a
1265
+ half-typed table becomes a table (matched), and short table rows, where this
1266
+ pads and `remark` leaves it to the renderer (kept, so a streaming table's
1267
+ last column does not pop in and out).
1268
+
1269
+ ## 0.22.0 — 2026-08-30
1270
+
1271
+ ### Changed
1272
+
1273
+ - **The concentric corner rule is the kit's, not the question card's.** It was
1274
+ worked out for one component and stayed there, and the same fault turned up
1275
+ immediately in a tool call inside an approval: the tool and the code block in
1276
+ it both had an 8px corner at two different insets, which is the crescent the
1277
+ rule exists to prevent.
1278
+
1279
+ Read inward here rather than outward, because the outer box is short — a tool
1280
+ row folded shut is about 40px tall, and a corner derived from a generous
1281
+ inner gap would make it a pill. So the gap is tight and the block inside
1282
+ takes the small corner: **code 6 → tool 14 → approval 26**, gaps 8 and 12.
1283
+
1284
+ `<CodeBlock>` now reads `--ick-code-radius`, so a box that nests one hands it
1285
+ a smaller corner the way `<Tool>` already handed it a different fill. The
1286
+ guard covers every nesting in the kit — adding a box that holds a box means
1287
+ adding a row to it.
1288
+
1289
+ - **One stack of surfaces, named once.** `--ick-ground`, `--ick-card`,
1290
+ `--ick-inset`: what a group sits on, an opaque panel raised on it, a row set
1291
+ into the panel. The question card had worked this out and kept it to itself.
1292
+
1293
+ Which is how a tool call inside an approval came out **pale green**: the
1294
+ tool's own surface is a translucent grey, and a translucent panel on a tinted
1295
+ ground wears the tint. An approval is a ground now, so the tool on it is a
1296
+ card and the code panel in that is inset — the same three steps the question
1297
+ card uses.
1298
+
1299
+ - **Three weights for an approval's three answers.** Filled for the narrow yes,
1300
+ outlined for the standing one, flat for no. Two outlined buttons beside each
1301
+ other said the last two were equals, which they are not. Left to right: Deny,
1302
+ Always allow, Allow once — and Deny is still first in the DOM, so a keyboard
1303
+ lands on the safe answer without tabbing past two that say yes.
1304
+
1305
+ ## 0.21.0 — 2026-08-30
1306
+
1307
+ ### Added
1308
+
1309
+ - **`<Context>`.** How full the context window is, as a small ring.
1310
+
1311
+ It is a gauge, not a feature, and it earns its place for one reason: it is
1312
+ the only honest way to explain why a long conversation starts forgetting.
1313
+ Without it the forgetting looks like the model being stupid rather than the
1314
+ window being full.
1315
+
1316
+ Which is why the warning says **what happens next** rather than only that a
1317
+ number is high — "82%" tells somebody nothing they can act on. The whole
1318
+ sentence is the meter's accessible name and its `title`, so it reaches a
1319
+ pointer and a screen reader alike.
1320
+
1321
+ Two colours, not three: quiet until `warnAt`, then the danger colour. A gauge
1322
+ with an amber in the middle makes somebody learn a scale to read a number
1323
+ they can already see.
1324
+
1325
+ `role="meter"` with the value on it, rather than a number a screen reader has
1326
+ to find in a sentence. A total of zero reads as empty rather than full — a
1327
+ window nobody has reported yet is not a full one — and a host summing its own
1328
+ tokens is clamped at 100 rather than drawn past it.
1329
+
1330
+ **This finishes the agent tier.** G1 through G7 are done.
1331
+
1332
+ ## 0.20.1 — 2026-08-30
1333
+
1334
+ ### Changed
1335
+
1336
+ - **Deny moved to the left of an approval,** with the two that say yes pushed
1337
+ right. It is first in the DOM as well, so a keyboard reaches the safe answer
1338
+ without tabbing past the other two — the gap between it and the affirmative
1339
+ choices is still what stops the mis-click.
1340
+
1341
+ ## 0.20.0 — 2026-08-30
1342
+
1343
+ ### Added
1344
+
1345
+ - **`<Approval>`.** "It wants to do this. Is that all right?" — the one pattern
1346
+ from a coding agent that generalises to any agent that acts, and the only
1347
+ component here whose whole job is to slow somebody down for a moment.
1348
+
1349
+ **Three answers, not two.** "Yes" and "yes forever" are not the same answer,
1350
+ and one button for both collects the wrong one. Allow once is the primary:
1351
+ the narrow permission should be the easy one to give and the standing one
1352
+ should cost a moment's thought. Deny sits at the far end and only turns red
1353
+ under the pointer.
1354
+
1355
+ Decided, it stops being a set of buttons and becomes a record of what was
1356
+ decided — live controls under a decision already made invite a second one
1357
+ that contradicts the first.
1358
+
1359
+ - **`kind: "approval"` on `TurnPart`,** and `onDecideApproval` on
1360
+ `<ChatTurnRow>`. Data like every other part: the tool it names is drawn for
1361
+ it, unrun, rather than passed in as an element.
1362
+
1363
+ - **`variant="outline"` on `<Button>`.** The gap between `primary`, which is
1364
+ filled, and `secondary`, which is naked until touched: a button that looks
1365
+ like one before you reach for it.
1366
+
1367
+ `secondary` reading as bold text has now caught this kit twice — the empty
1368
+ state's openers in 0.9.0, and an approval's Deny here. The second time it was
1369
+ a permission control that did not look like a control, so the gap is filled
1370
+ rather than worked around again.
1371
+
1372
+ - **`TurnPartUpdate`** — a part with everything optional but `kind` and `id`,
1373
+ which is what a stream and `updatePart` actually send. Saying a tool call
1374
+ finished is now `{ kind: "tool", id, state: "done" }` and nothing else.
1375
+ Repeating a field to satisfy a type is how one that was not meant to change
1376
+ gets overwritten with whatever was easiest to type — which is exactly what
1377
+ happened to an approval's title before this existed.
1378
+
1379
+ ## 0.19.0 — 2026-08-30
1380
+
1381
+ ### Changed
1382
+
1383
+ - **A question's corners nest.** A box's corner is now the corner of the thing
1384
+ inside it plus the gap between them — the rule that stops a rounded row
1385
+ inside a rounded card leaving a crescent of card between the two curves. It
1386
+ is the thing that reads as "not quite fitting" without anybody being able to
1387
+ name it.
1388
+
1389
+ Measured, from the inside out. The badge seeds the chain at 8, a row is 8
1390
+ plus its own 8 of padding, a card is 16 plus the 8 it puts around its rows,
1391
+ and the group is 24 plus the 16 it puts around its cards:
1392
+
1393
+ | | was | is |
1394
+ | --- | --- | --- |
1395
+ | badge | 8 | 8 |
1396
+ | option / field row | 8 | **16** |
1397
+ | card | 12 | **24** |
1398
+ | group | 24 | **40** |
1399
+
1400
+ Written as arithmetic rather than as four numbers —
1401
+ `--ick-question-radius-badge` seeds it and `-row`, `-card` and `-group` are
1402
+ `calc()` off it — so the chain cannot drift when one of the paddings changes.
1403
+ A test does the same sum and reports the whole chain when a link breaks.
1404
+
1405
+ The folded group row takes a card's corner, since that is what it is. The
1406
+ count beside it is a pill, which is outside the chain: a pill's corner is its
1407
+ own height, not something derived from what contains it.
1408
+
1409
+ ## 0.18.0 — 2026-08-30
1410
+
1411
+ ### Added
1412
+
1413
+ - **`<Sources>` and `<InlineCitation>`.** A numbered marker in the text and the
1414
+ list underneath.
1415
+
1416
+ **The citation marker and the highlight marker are the same interaction seen
1417
+ twice**, and this is where that pays. The kit already had a way of saying
1418
+ *this run of words is picked out* — the marker somebody draws over an answer
1419
+ to ask about it. A citation is that same statement made by the answer rather
1420
+ than by the reader, so it is drawn the same way rather than in a second
1421
+ visual language nobody has learned. Give `InlineCitation` the passage as its
1422
+ children and the passage is marked, on every line it wraps onto.
1423
+
1424
+ Pressing a marker **marks** the entry rather than scrolling to it. The list
1425
+ is already under the answer, and moving the page under somebody who clicked a
1426
+ marker in a sentence they were reading loses them the sentence.
1427
+
1428
+ The list is open by default: sources are the difference between an answer
1429
+ somebody can check and one they have to trust. An entry is a link only where
1430
+ there is somewhere to go — a keyboard lands on every link, and one that goes
1431
+ nowhere is a stop for nothing. Same for the marker, which is a `<button>`
1432
+ only when it has an `onSelect`.
1433
+
1434
+ ## 0.17.0 — 2026-08-30
1435
+
1436
+ ### Added
1437
+
1438
+ - **`<ChainOfThought>`.** How the answer was arrived at, step by step.
1439
+
1440
+ Three components now draw a sequence, and the line between them is the only
1441
+ reason there are three: `Reasoning` is prose, `TaskList` is a plan — known up
1442
+ front, fixed order, items changing state — and this is a **derivation**,
1443
+ which grows, and where each step follows from the one above it. That last
1444
+ word is what the line down the glyph column draws; a task list has no line
1445
+ between its items, because a plan's items do not follow from each other.
1446
+
1447
+ A step's `body` takes anything, including a `<Tool>` when the step was a tool
1448
+ call. While it thinks, the header carries the running step's label rather
1449
+ than a count — that is the question somebody watching is asking, and the
1450
+ reason to look at a folded chain at all.
1451
+
1452
+ Folds on the same terms as `Reasoning`: open while thinking, away once the
1453
+ answer starts, and overruled for good by anybody who touches it. Four
1454
+ components share `useDisclosure` now, and three share `StateGlyph`.
1455
+
1456
+ The connector came out of the first screenshot as a nine-pixel stub — the
1457
+ rail was only as tall as its glyph, so there was nothing for the line to span.
1458
+ It stretches to the step now.
1459
+
1460
+ ### Changed
1461
+
1462
+ - **The shimmer guard covers all three shimmers**, not two. A third copy of the
1463
+ same effect landed in `ChainOfThought`, which is exactly the drift the test
1464
+ was written for.
1465
+
1466
+ ## 0.16.0 — 2026-08-30
1467
+
1468
+ ### Added
1469
+
1470
+ - **A turn carries `parts`, and a `SendHandler` can stream them.** The agent
1471
+ tier existed and was unreachable: `Reasoning`, `Tool`, `TaskList` and
1472
+ `QuestionGroup` all worked, and nothing carried one into a conversation. An
1473
+ answer was one string, so a tool call, a plan and a block of reasoning either
1474
+ flattened into that string or never arrived.
1475
+
1476
+ A streamed item is now either a string — a delta of the answer's prose, as
1477
+ before — or a `TurnPart`, merged into `turn.parts` **by its id**. The merge is
1478
+ shallow, which is what makes streaming one bearable: send the state change on
1479
+ its own and the text that arrived before it is still there.
1480
+
1481
+ `<ChatTurnRow>` draws each kind with the component that owns it, in one block
1482
+ so the row's generous gap sits between the question and the answer — not
1483
+ between a tool call and the sentence it produced.
1484
+
1485
+ - **`updatePart(turnId, part)` on `useChatTurns`,** and `onAnswerQuestion` /
1486
+ `onEditQuestion` on `<ChatTurnRow>`. A question the assistant asked is
1487
+ answered by the person reading it, not by the stream, and that answer has to
1488
+ land somewhere. The row never keeps it — the parts are the host's.
1489
+
1490
+ - **`mergeParts`** is exported, for a host folding parts into state of its own.
1491
+
1492
+ ### Breaking
1493
+
1494
+ - **`ChatTurn` now has a required `parts` field.** Turns from `useChatTurns`
1495
+ always have it; code that builds a `ChatTurn` literal — a test, a story, a
1496
+ host's own state — needs `parts: []`.
1497
+
1498
+ - **`SendHandler`'s iterable widened** to `AsyncIterable<string | TurnPart>`.
1499
+ A handler that only yields strings is unaffected.
1500
+
1501
+ ## 0.15.0 — 2026-08-30
1502
+
1503
+ ### Added
1504
+
1505
+ - **`<TaskList>`.** What the agent means to do, what it is doing, and what it
1506
+ has finished.
1507
+
1508
+ **The order never changes.** A list that sorted itself as work progressed
1509
+ would move the line somebody is reading out from under them, and the sequence
1510
+ is half of what the list is saying — these steps, in this order. Only the
1511
+ glyphs change.
1512
+
1513
+ A list rather than a box, which is the difference from `Tool`: that is a
1514
+ record of one thing that ran and gets a surface; this is a sequence, and a
1515
+ surface around it would make it a thing beside the answer rather than part of
1516
+ it. Three inks down the list — what is finished recedes, what is queued is
1517
+ quieter still, and the one being worked on is the answer to "where is it up
1518
+ to".
1519
+
1520
+ `collapsible` folds it once every task is done, on the same terms as
1521
+ `Reasoning`: the list's preference, overruled for good by anybody who touches
1522
+ it. The running task carries `aria-current="step"`, so a screen reader can
1523
+ jump to it rather than counting down the list.
1524
+
1525
+ ### Breaking
1526
+
1527
+ - **`--ick-tool-glyph`, `--ick-tool-glyph-done`, `--ick-tool-glyph-error`,
1528
+ `--ick-tool-spinner`, `--ick-tool-spinner-track` and
1529
+ `--ick-tool-glyph-size` are gone.** The four states are shared with
1530
+ `TaskList` now, and so are their colours: `--ick-state-pending`,
1531
+ `--ick-state-running`, `--ick-state-done`, `--ick-state-error`,
1532
+ `--ick-state-track` and `--ick-state-size`. If you had restyled a tool call's
1533
+ glyph, the new names do the same job for both components.
1534
+
1535
+ Two copies of one drawing is how the two start to disagree — which the
1536
+ shimmer had already demonstrated one release earlier.
1537
+
1538
+ ## 0.14.1 — 2026-08-30
1539
+
1540
+ ### Fixed
1541
+
1542
+ - **The shimmer stood still for two thirds of every cycle.** With the gradient
1543
+ two and a half times the element and swept from `150%` to `-50%`, the bright
1544
+ point travels from one element-width left of the words to two widths right of
1545
+ them — so it is on the text for a third of the pass and off it for the rest.
1546
+ A shimmer that spends most of its time as a static grey line is doing the
1547
+ opposite of its job.
1548
+
1549
+ Sampled frame by frame over one period to confirm it rather than guess: two
1550
+ in every three frames were the word standing still.
1551
+
1552
+ It is `200%` swept `110%` → `-10%` now, which puts the bright stop on the
1553
+ left edge at one end and the right edge at the other, so it crosses the words
1554
+ and little else. 1.8s rather than 2.4.
1555
+
1556
+ Both `Loader`'s shimmer and `Reasoning`'s had it, and both are fixed. A test
1557
+ compares the two, since nothing else would make the next person fixing one of
1558
+ them look at the other.
1559
+
1560
+ ## 0.14.0 — 2026-08-30
1561
+
1562
+ ### Added
1563
+
1564
+ - **`<Reasoning>`.** What the model worked through before it answered. Open
1565
+ while it thinks, folded away once the answer starts — the one detail every
1566
+ kit shipping this has converged on, and it is right: thinking is worth
1567
+ watching while it happens and worth almost nothing afterwards, but it has to
1568
+ stay reachable, because the times it matters are exactly the times the answer
1569
+ looks wrong.
1570
+
1571
+ Folding is the block's preference, not something done to the reader. Open it
1572
+ and it stays open, however many times the state changes underneath.
1573
+
1574
+ It times itself when no `duration` is given — adjusted during render on a
1575
+ change of state rather than in an effect, which would mean a second pass
1576
+ every time the thinking stopped.
1577
+
1578
+ Prose, not a panel, which is the difference between this and `Tool`: a tool
1579
+ call is a record of something that ran and gets a box; this is the model
1580
+ talking to itself, and a box would give it a weight it has not earned next to
1581
+ the answer it is only explaining. A rule down the side says "aside" and stays
1582
+ out of the way.
1583
+
1584
+ The shimmering word is a real word in the button rather than a `<Loader>`.
1585
+ The loader is decorative and marks itself `aria-hidden`; drawn that way the
1586
+ button had no accessible name at all. Caught by the tests before it shipped,
1587
+ and three axe cases keep it that way.
1588
+
1589
+ ### Changed
1590
+
1591
+ - **`formatDuration` moved out of `Tool`** into its own module, and both it and
1592
+ `Reasoning` read it from there.
1593
+
1594
+ - **`useDisclosure`** holds the open/shut rule both blocks share: the host if it
1595
+ is controlling the row, then whoever clicked it, then the row's own
1596
+ preference. Derived rather than an effect, which is what lets a reader's
1597
+ decision outlive every state change after it.
1598
+
1599
+ ## 0.13.0 — 2026-08-29
1600
+
1601
+ ### Added
1602
+
1603
+ - **`questionAlign` on `<ChatTurnRow>`.** `end` by default — the composer is
1604
+ about to become the reader's own bubble, and those sit right. `stretch` fills
1605
+ the row instead, which is what an *opening* composer wants: on an empty
1606
+ conversation it is not a message on its way, it is the box under the openers,
1607
+ and a pill floating at the right edge of a centred block reads as unrelated
1608
+ to the block.
1609
+
1610
+ A flex child sizes to its content, so the input is told to fill as well —
1611
+ stretching the row on its own does nothing.
1612
+
1613
+ ### Changed
1614
+
1615
+ - **The badge on a chosen option is the accent taken down towards ink.** At
1616
+ full strength it was a shade off the tint under it: two yellows that close
1617
+ are not a badge on a row, they are one yellow with a rounded hole in it.
1618
+ Paper was tried first and was no better — pale on pale.
1619
+
1620
+ `--ick-marker-deep` mixes the marker with ink rather than stating a colour
1621
+ per theme, which is the point: ink is near-black in the light and near-white
1622
+ in the dark, so it comes out darker than the row in one and lighter in the
1623
+ other. One requirement — separate from the row — answered correctly twice.
1624
+
1625
+ Measured rather than eyeballed. 55% marker put the letter at 3.5:1; 45% is
1626
+ the first that clears 4.5. It ships at 40%: the letter reads 5.6:1 in the
1627
+ light and 16.3:1 in the dark, and the badge 5.1:1 and 4.0:1 against the row
1628
+ it sits on.
1629
+
1630
+ ## 0.12.1 — 2026-08-29
1631
+
1632
+ ### Fixed
1633
+
1634
+ - **Picking an option no longer draws a focus ring around it.** The option row
1635
+ rang on `:focus-within`, and a mouse click focuses a button — so choosing one
1636
+ outlined it, which is the exact thing `:focus-visible` exists to avoid.
1637
+ Measured: after a real click, `:focus-visible` was `false` and
1638
+ `:focus-within` `true`, with a 2px outline computed.
1639
+
1640
+ `:focus-within` was there for the "something else" row, which is a label
1641
+ around an input and so has no `:focus-visible` of its own. It is scoped to
1642
+ that row now. A keyboard still rings every row, and the row still lights up
1643
+ when its input is focused.
1644
+
1645
+ ## 0.12.0 — 2026-08-29
1646
+
1647
+ ### Added
1648
+
1649
+ - **`<Tool>`.** One tool call: what was run, what with, what came back, and
1650
+ which of four states it is in — queued, running, done, failed. Shut by
1651
+ default, because most of the time nobody cares.
1652
+
1653
+ **It opens itself when it failed**, since an error nobody can see has not
1654
+ been reported. Derived from the state rather than forced by an effect, so a
1655
+ call that fails later opens while one somebody deliberately shut stays shut —
1656
+ reopening a row under a reader's hands to show them something they dismissed
1657
+ is not help.
1658
+
1659
+ What it was given decides how it is drawn. A string is text: wrapping
1660
+ `"Belgrade, 24°C"` in a fence puts it in quotes with its newlines spelled
1661
+ out, which is worse than reading it. An object is JSON, in a `CodeBlock` with
1662
+ its copy button, which is most of why anybody opens the row. An element is
1663
+ somebody having decided for themselves, so it is left alone. A value JSON
1664
+ cannot hold prints something unhelpful rather than throwing.
1665
+
1666
+ The state is not carried by colour: the glyph changes shape, and the row says
1667
+ which state it is in in words that only a screen reader hears.
1668
+
1669
+ - **`--ick-danger`.** The first hue past ink, paper and the marker. The marker
1670
+ already means "this one", so a failed call wearing the accent for choice
1671
+ would say the wrong thing. A channel triplet like the rest, lifted in the
1672
+ dark, where the light red was nearly black on a near-black page.
1673
+
1674
+ ### Changed
1675
+
1676
+ - **`--ick-tool-code-fill`** points a fenced value inside a tool row at paper
1677
+ rather than at the sunken surface the row itself uses — otherwise the two are
1678
+ one grey with a hairline through them. Lighter than the row in the light,
1679
+ darker in the dark; different in both, which is the requirement.
1680
+
1681
+ ## 0.11.1 — 2026-08-29
1682
+
1683
+ ### Fixed
1684
+
1685
+ - **Every badge in a question sits on one vertical line.** The number over a
1686
+ question was 12px from the card's edge and the a/b/c beside its rows were
1687
+ 16, so the two did not line up. The rows already answered and the ones still
1688
+ to come were at 12 as well. All four are at 16 now.
1689
+
1690
+ - **A field row's focus edge stopped taking up room.** It was a 1px border,
1691
+ and a border is part of the box — which put every field row's badge a pixel
1692
+ to the right of every option row's. It is an inset shadow now: the same
1693
+ edge, drawn in no space.
1694
+
1695
+ A test does the sum the browser does — jsdom lays nothing out — and fails if
1696
+ any of the four rules drifts again.
1697
+
1698
+ ### Changed
1699
+
1700
+ - **A question still to come shows its number rather than an asterisk.** It is
1701
+ question three whether or not anybody has reached it, and saying so is what
1702
+ makes the list read as a list. White, since an upcoming row has no card
1703
+ under it: the badge is what stands off the group's own surface.
1704
+
1705
+ ## 0.11.0 — 2026-08-29
1706
+
1707
+ ### Added
1708
+
1709
+ - **The parts a question is built from are public.** `QuestionShell`,
1710
+ `QuestionOptionRow`, `QuestionFieldRow`, `QuestionOtherRow` and
1711
+ `QuestionBadge`.
1712
+
1713
+ The kit ships three question shapes, and three is not all of them. A fourth —
1714
+ pick one *and* say something, say — is now a composition rather than a fork,
1715
+ and it arrives already wearing the same tokens, focus behaviour and ARIA as
1716
+ the three that ship. Exporting the rows without the shell would have been
1717
+ half of it: the box, the header and the footer would still have been
1718
+ hand-rolled out of numbers that were tokens ten minutes earlier.
1719
+
1720
+ Being public API changed them. Each takes a `className` that is added to its
1721
+ own rather than replacing it, spreads the rest of its props onto the element
1722
+ it ends in, and forwards its ref to the thing worth having one for — the
1723
+ input, in the two rows that have one. `letter` is optional everywhere. And
1724
+ they are named for what they are from outside rather than for where they sat
1725
+ inside the card.
1726
+
1727
+ `QuestionShell` paints the card by default, since a shell standing on its own
1728
+ is the reason it is exported. `QuestionCard` passes `card={false}` — the box
1729
+ that morphs between the three states is its own, and two would nest. The card
1730
+ is now built out of the shell rather than repeating it, so the shipped
1731
+ component is the proof the exported one works.
1732
+
1733
+ ### Fixed
1734
+
1735
+ - **Two field rows with the same label no longer share an id.** The input's id
1736
+ was built out of the letter and the label, which is unique inside one card
1737
+ and not across two — and two inputs holding one id means the second row's
1738
+ label focuses the first row's input. It is `useId()` now, and an `id` of your
1739
+ own still wins.
1740
+
1741
+ - **The option row's tap no longer scales under reduced motion.** A 1% squash,
1742
+ but it was the one animation in the question rows that was not asking.
1743
+
1744
+ ## 0.10.1 — 2026-08-29
1745
+
1746
+ ### Fixed
1747
+
1748
+ - **A question card was clipping its own contents.** `.active` had padding and
1749
+ a 100% width but no `box-sizing`, so the padding was added *outside* the
1750
+ width and every option row and the Next button lost 16px to the card's
1751
+ `overflow: hidden`. Measured: rows 488px wide inside a 488px card, starting
1752
+ 8px in.
1753
+
1754
+ This package has no global `box-sizing` reset — one written inside a CSS
1755
+ module leaks into the host's page — so every box with padding has to say it
1756
+ itself. A test now reads the rules and fails if one stops.
1757
+
1758
+ - **The card is white.** `--ick-question-card` was `--ick-surface-raised`, a 5%
1759
+ mix, which put it within three percent of the ground it sits on: the same
1760
+ surface in a slightly different shade rather than a card. It is `--ick-surface`
1761
+ now, with the ground moved down to meet it. Dark keeps a raised value, since
1762
+ `--ick-surface` there is the page itself and a card painted with it would
1763
+ sink into the ground instead of sitting on it.
1764
+
1765
+ ## 0.10.0 — 2026-08-29
1766
+
1767
+ ### Added
1768
+
1769
+ - **`<QuestionCard>`, `<QuestionGroup>` and `<Chip>`.** A structured question
1770
+ inside a conversation — the assistant asks something with a shape to it, and
1771
+ the answer is picked or typed rather than written out. Three question shapes
1772
+ (`inputs`, `single`, `multi`), three card states that morph between each
1773
+ other, and a group that folds to one summary row once it is done with.
1774
+
1775
+ Brought across from another project and rebuilt on this kit's tokens, type
1776
+ and buttons. The accent doing the work is the marker yellow, on the badge of
1777
+ whatever is chosen and on the border of the field being typed into.
1778
+
1779
+ Two behaviours kept because they were right: a single-select waits a beat
1780
+ before committing, or the card is gone before anyone sees what they picked;
1781
+ and the "something else" row is a `<label>`, not a button, because an input
1782
+ inside a button is not reliably focusable.
1783
+
1784
+ Two changed. The collapsed row is one `<button>` naming what it does rather
1785
+ than a click handler on a `<div>` with another button inside it — clickable
1786
+ but not reachable by keyboard, and two tab stops for one action. And the
1787
+ letter badges are `aria-hidden`: the letter is a visual index, and in the
1788
+ tree it turns a field called "Their name" into "a Their name".
1789
+
1790
+ - **Question and chip tokens.** `--ick-question-*` for the three stacked
1791
+ surfaces, `--ick-badge-*`, `--ick-chip-*`.
1792
+
1793
+ ## 0.9.0 — 2026-08-29
1794
+
1795
+ ### Added
1796
+
1797
+ - **`<EmptyState>`.** What is on screen before anybody has asked: an icon, a
1798
+ title, a description and a row of openers, each optional and none drawn in
1799
+ place of what is left out. Openers need an `onSuggestion` — a chip that
1800
+ reports nowhere is a button that does nothing.
1801
+
1802
+ `title` renders as text rather than a heading. It sits inside a conversation
1803
+ the host already owns, and claiming a level in their document is not ours.
1804
+
1805
+ - **`<Loader>`.** `dots` for the gap between sending and the first word,
1806
+ `shimmer` for words standing in for something.
1807
+
1808
+ Silent by default: `useChatTurns` already announces that a response is
1809
+ coming, and a second live region means hearing it twice. `label` opts in.
1810
+
1811
+ - **`ChatTurnRow` shows the loader** between the question being sent and the
1812
+ first word landing. A sent question with a blank space under it reads as
1813
+ nothing having happened.
1814
+
1815
+ - **Two tokens**, `--ick-loader-dot` and `--ick-empty-pad`.
1816
+
1817
+ ## 0.8.0 — 2026-08-29
1818
+
1819
+ ### Added
1820
+
1821
+ - **`<AnswerActions>`.** Copy, regenerate and a verdict, under a settled
1822
+ answer. The input has had a hover row since the beginning and the answer had
1823
+ nothing, which is backwards — the answer is the part worth keeping.
1824
+
1825
+ Only what has somewhere to report is drawn. A control that calls nothing
1826
+ looks like a feature and behaves like a dead end. Pressing the verdict
1827
+ already given reports `null`, which is how somebody takes it back.
1828
+
1829
+ Copy confirms and announces itself, for the same reason the code block's
1830
+ does: a tick is a picture, and a reader who cannot see it is told nothing
1831
+ happened.
1832
+
1833
+ `reveal` makes the row invisible until hovered or focused — invisible, not
1834
+ absent: it keeps its space so nothing shifts, still hit-tests at zero
1835
+ opacity, and `:focus-within` brings it back for anyone arriving by keyboard.
1836
+
1837
+ - **`ChatTurnRow` renders it**, with `onRegenerate`, `onFeedback`, `feedback`
1838
+ and `answerActions`. They appear when the answer settles rather than while it
1839
+ arrives.
1840
+
1841
+ ## 0.7.1 — 2026-08-28
1842
+
1843
+ ### Added
1844
+
1845
+ - **`<Conversation anchorId>` and `anchorOffset`.** Hold an element at the top
1846
+ of the view instead of following the end of the content.
1847
+
1848
+ 0.7.0 only did the second, which is wrong for the way this kit works: a
1849
+ submitted message has to go to the top and stay there while the answer
1850
+ arrives underneath, so what is on screen is the question and its answer.
1851
+ Following the end stacks everything downwards instead, and the composer that
1852
+ appears when the answer settles ends up past the fold.
1853
+
1854
+ Point it at the turn that was just sent. It needs room to scroll into — an
1855
+ element cannot be brought to the top of a container that ends just below it —
1856
+ which is what a large bottom padding on the viewport is for.
1857
+
1858
+ With an anchor above the reader, scrolling *up* is how they return to it, so
1859
+ letting go is decided by distance rather than direction.
1860
+
1861
+ ## 0.7.0 — 2026-08-28
1862
+
1863
+ ### Added
1864
+
1865
+ - **`<Conversation>`.** The scroll container: it keeps up with an answer as it
1866
+ arrives, lets go the instant the reader scrolls away, and offers a button
1867
+ back. Three tokens for the gap and padding.
1868
+
1869
+ It follows the **end of the content**, not the bottom of the container.
1870
+ Those are only the same thing when nothing is padded below, and this kit's
1871
+ demo carries a screen-height pad so a turn can be pulled to the top —
1872
+ scrolling to the bottom there parks the answer above the fold in front of a
1873
+ blank screen. Measuring the content makes one behaviour right for both.
1874
+
1875
+ It reads intent from the **input** rather than the scroll event. A component
1876
+ watching scrolling cannot tell its own from the reader's, and ends up either
1877
+ dragging them back down mid-sentence or never following at all. A wheel
1878
+ upwards, `PageUp`, `Home`, or a drag away from the end, and it stops.
1879
+
1880
+ `ref` is forwarded to the viewport rather than the root, because a ref here
1881
+ is for scrolling and the root does not scroll. `viewportClassName` styles the
1882
+ scroller; `className` styles the box you lay out.
1883
+
1884
+ ## 0.6.0 — 2026-08-28
1885
+
1886
+ ### Added
1887
+
1888
+ - **`<CodeBlock>`.** The language, a copy button that confirms and announces
1889
+ itself, and code that scrolls sideways rather than widening the answer. The
1890
+ markdown renderer uses it for every fence; it is exported for use on its own.
1891
+
1892
+ Ten languages, chosen and measured: `lowlight/common` is 37 grammars and
1893
+ 51.6 KB gzipped — nearly the size of everything else here — for languages a
1894
+ chat will almost never show. These cost a quarter of that. A language outside
1895
+ the list renders unhighlighted rather than throwing.
1896
+
1897
+ The scheme is ink at four weights rather than a syntax palette, because this
1898
+ kit is ink, paper and one acid yellow. Six tokens make it a palette for
1899
+ anyone who wants one.
1900
+
1901
+ - **Code tokens.** `--ick-code-size`, `-leading`, `-comment`, `-keyword`,
1902
+ `-string`, `-number`, `-name`, `-attr`, `-addition`, `-deletion`.
1903
+
1904
+ ### Fixed
1905
+
1906
+ - **A code block keeps its own font.** `pre` and `code` are two of the elements
1907
+ a host app or docs tool styles without thinking about it, and an inherited
1908
+ family loses to any direct declaration — so the family is stated on the
1909
+ elements themselves. Ligatures are off with it: `--ick-font-mono` is a token,
1910
+ and a brand setting Fira Code would otherwise get `npm run dev -- --port`
1911
+ drawn as `dev —— ——port` while the clipboard hands over the real characters.
1912
+
1913
+ ### Dependencies
1914
+
1915
+ - `lowlight` and ten `highlight.js` grammars. 56.6 KB → 81.6 KB gzip.
1916
+
1917
+ ## 0.5.1 — 2026-08-28
1918
+
1919
+ ### Fixed
1920
+
1921
+ - **`<ChatHeader variant="bordered">` is opaque.** It was transparent with a
1922
+ hairline under it, so a sticky header had the conversation scrolling visibly
1923
+ through its own title. It now sits on `--ick-surface`, which is also what
1924
+ separates it from `glass`: a panel with a line under it, rather than a
1925
+ frosted pane.
1926
+
1927
+ Found by fixing the Storybook story rather than the component. The three
1928
+ materials were being shown on a blank page with nothing under them, where
1929
+ all three look identical — so the story now scrolls real content beneath a
1930
+ sticky header, and the bug was in the first frame of it.
1931
+
1932
+ Three tests read the rules out of the stylesheet, since jsdom does not paint
1933
+ and this is the kind of thing that comes back once nobody is looking.
1934
+
1935
+ ## 0.5.0 — 2026-08-28
1936
+
1937
+ ### Breaking
1938
+
1939
+ - **`TextHighlighter` renders its text as markdown.** An answer containing
1940
+ `*`, `#`, `` ` `` or `-` at the start of a line now renders as emphasis, a
1941
+ heading, code or a list rather than as those characters. If you were passing
1942
+ markdown and relying on it staying literal, escape it.
1943
+
1944
+ Raw HTML in the input is dropped rather than rendered. Model output is
1945
+ untrusted text, and there is no version of injecting it into the host's page
1946
+ that is worth the surface it opens.
1947
+
1948
+ ### Added
1949
+
1950
+ - **Markdown, inside the highlighter rather than around it.** Headings,
1951
+ emphasis, strikethrough, links, inline and fenced code, lists, blockquotes,
1952
+ tables, images and rules.
1953
+
1954
+ The design constraint was the marker. Internally the words stay a **flat
1955
+ array of tokens addressed by index** — hit-testing reads an index off the
1956
+ span under the pointer, the keyboard cursor walks the indices, a highlight's
1957
+ text is a run of them joined back. Markdown only decides which element each
1958
+ token is drawn inside, so a stroke that starts in plain text and ends inside
1959
+ `**bold**` is one run of indices like any other.
1960
+
1961
+ Fenced blocks are the exception: preformatted, so not tokenised, so not
1962
+ markable. `CodeBlock` will own them properly.
1963
+
1964
+ - **Markdown tokens.** `--ick-md-gap`, `-item-gap`, `-heading-space`,
1965
+ `-indent`, `-quote-indent`, `-rule` and `-code-fill`. The gaps are in `em`
1966
+ so they scale with the answer rather than drifting away from it.
1967
+
1968
+ ### Dependencies
1969
+
1970
+ - `unified`, `remark-parse` and `remark-gfm`, bundled rather than left to the
1971
+ consumer. The package went from 18.9 KB to 56.6 KB gzip; that is the price
1972
+ of markdown working on install with nothing to configure.
1973
+
1974
+ `react-markdown` was the obvious choice and is not used. Its feature is
1975
+ swapping *components*, and what this needs is every **text node** split into
1976
+ indexed spans — text nodes are strings, not components. It also parses
1977
+ inside its own render, which hands back the tokens a render too late for the
1978
+ keyboard cursor that needs them.
1979
+
1980
+ ## 0.4.0 — 2026-08-27
1981
+
1982
+ Additive.
1983
+
1984
+ ### Added
1985
+
1986
+ - **`<ChatTurnRow>`.** One turn: the question as a composer that has become a
1987
+ bubble, and the answer under it. It existed all along in the playground,
1988
+ which meant anyone installing the package had to rewrite the one thing the
1989
+ package is about — the README could only tell them to, and explain the memo
1990
+ they would need.
1991
+
1992
+ Not named `Message`. The user half is a live input that morphs into its own
1993
+ bubble, not a rendered record of what was typed, and that is the whole idea.
1994
+
1995
+ Memoised, and the memo is load-bearing: `useChatTurns` leaves untouched turns
1996
+ referentially identical, which only pays off if the rows act on it. Every
1997
+ callback takes the turn's id rather than being closed over per row, so a
1998
+ consumer can hoist them and not hand the memo a new prop each render.
1999
+
2000
+ - **Turn tokens.** `--ick-turn-gap`, and `--ick-answer-size` / `-leading` /
2001
+ `-tracking` / `-measure` for the answer. The measure is in `ch` rather than
2002
+ pixels, so a comfortable line length follows whatever font a brand sets.
2003
+
2004
+ ## 0.3.0 — 2026-08-27
2005
+
2006
+ Additive. Nothing in 0.2.0 changes behaviour.
2007
+
2008
+ ### Added
2009
+
2010
+ - **`<ChatHeader>`.** The chrome above a conversation: title, subtitle, avatar,
2011
+ back, and a row of actions. Three materials (`plain`, `glass`, `bordered`),
2012
+ three sizes (40 / 48 / 56px), and a centred arrangement for the native/mobile
2013
+ pattern.
2014
+
2015
+ Actions are described rather than passed as children:
2016
+
2017
+ ```tsx
2018
+ actions={[{ id: "share", label: "Share", icon: <Share2 size={16} />, onClick: share }]}
2019
+ ```
2020
+
2021
+ which is what lets `collapseActionsAt` fold them into a keyboard-navigable
2022
+ overflow menu when the header is narrow — a header cannot summarise children
2023
+ it cannot read. Anything with no icon-and-label shape goes in as `children`
2024
+ and stays put.
2025
+
2026
+ `label` is required, and a `count` is folded into it: the badge on the glyph
2027
+ is `aria-hidden`, so without that a reader would never learn the number.
2028
+
2029
+ - **Header tokens.** `--ick-header-*` for height, padding, gap, backdrop, blur,
2030
+ border, title, subtitle and the count badge — twelve, plus one that restates
2031
+ the backdrop for the dark theme, where a lighter wash still shows the text
2032
+ scrolling underneath.
2033
+
2034
+ ## 0.2.0 — 2026-08-26
2035
+
2036
+ The package became something other people can install: it has tests, it can be
2037
+ themed, and it can be used without a mouse.
2038
+
2039
+ ### Breaking
2040
+
2041
+ - **`ReplyThreadPopup` requires `onSendMessage`.** It used to be optional, and
2042
+ without it the component streamed placeholder copy about the Higgs boson —
2043
+ shipped to every consumer. A component has no answers of its own; that is the
2044
+ host app's job.
2045
+ ```tsx
2046
+ <ReplyThreadPopup
2047
+ onSendMessage={(message, quotedText, { signal }) => callYourApi(message, quotedText, signal)}
2048
+ …
2049
+ />
2050
+ ```
2051
+ - **`ChatInput` has no `variant` prop.** There were four; three had no call
2052
+ sites. Remove the prop.
2053
+ - **Components read `--ick-` custom properties.** The old unprefixed names
2054
+ (`--ink`, `--surface-hover`, `--font-geist-sans`, …) still resolve, as
2055
+ aliases, but they will go. A host that loads its own fonts should say so:
2056
+ ```css
2057
+ :root {
2058
+ --ick-font-sans: var(--font-geist-sans);
2059
+ --ick-font-mono: var(--font-geist-mono);
2060
+ }
2061
+ ```
2062
+ - **The kit now responds to a `.dark` class or `data-theme="dark"` on the root
2063
+ element,** and to `prefers-color-scheme` when neither is set. An app that
2064
+ already uses `.dark` for its own theme will find the kit following it. Pin it
2065
+ with `data-theme="light"` if that is not wanted.
2066
+ - `GlassButton` is deprecated in favour of `<Button variant="glass">`. It still
2067
+ works and renders identically — the wrapper translates the old size names.
2068
+
2069
+ ### Added
2070
+
2071
+ - `useChatTurns` — turn state, streaming and the reveal, in one hook. Takes an
2072
+ `onSend` returning a string, a promise or an async iterable, and hands back
2073
+ an `AbortSignal`.
2074
+ - A design token layer: three tiers, all prefixed `--ick-`, with a dark theme.
2075
+ See [theming.md](./theming.md).
2076
+ - Dark mode, following `prefers-color-scheme` unless told otherwise.
2077
+ - Keyboard operation throughout — a word cursor for making highlights, reachable
2078
+ controls for the ones already made, an escapable menu, and a real dialog.
2079
+ - Screen-reader announcements when an answer arrives, and `prefers-reduced-motion`
2080
+ honoured across the kit.
2081
+ - `announce()` — the shared live region, exported so a host can speak its own
2082
+ errors into it rather than adding a competing one.
2083
+
2084
+ ### Fixed
2085
+
2086
+ - **A finger could not scroll past an answer.** `touch-action: none` on every
2087
+ paragraph made the conversation a dead zone on a phone. Related: a scroll the
2088
+ browser took over was committed as a highlight, leaving a marker behind on
2089
+ whatever word the finger landed on.
2090
+ - **The package was styling the host's document.** `ReplyThreadPopup` rendered
2091
+ a `<style>` element with an unscoped `div::-webkit-scrollbar` rule, which hid
2092
+ the scrollbar on every div on the page for as long as a thread was open.
2093
+ - **`GlassButton` had no accessible name while loading.** The label was hidden
2094
+ with `visibility: hidden`, which removes it from the accessibility tree, and
2095
+ the spinner is `aria-hidden` — so it announced as a button called nothing.
2096
+ - `ChatInput` left a timer running past unmount.
2097
+ - Three performance passes, each measured: pointer crossings over a highlight
2098
+ went from ~28 DOM mutations to 0, opening the menu from 407 to 67, and
2099
+ streaming an answer stopped touching the turns above it at all (366 to 0).
2100
+
2101
+ ### Internal
2102
+
2103
+ - 167 tests and CI: lint, tests, build, a Storybook build and a dry-run pack.
2104
+ - Storybook, with a token table read live from the stylesheets.
2105
+ - Two guards that fail rather than rely on memory: one when a component is
2106
+ exported without a story, one when a literal colour appears outside the token
2107
+ file.
2108
+
2109
+ ## 0.1.0
2110
+
2111
+ Extracted from the website into a package.