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.
- package/CHANGELOG.md +2111 -0
- package/LICENSE +21 -0
- package/README.md +1430 -0
- package/dist/AnswerActions/AnswerActions.d.ts +35 -0
- package/dist/Approval/Approval.d.ts +42 -0
- package/dist/Artifact/ArtifactCard.d.ts +45 -0
- package/dist/Artifact/ArtifactPane.d.ts +50 -0
- package/dist/Artifact/ChatLayout.d.ts +35 -0
- package/dist/Artifact/useArtifacts.d.ts +21 -0
- package/dist/Attachments/Attachments.d.ts +50 -0
- package/dist/Branch/Branch.d.ts +28 -0
- package/dist/Button/Button.d.ts +23 -0
- package/dist/ChainOfThought/ChainOfThought.d.ts +49 -0
- package/dist/ChatHeader/ChatHeader.d.ts +89 -0
- package/dist/ChatInput/AddCardsOverlay.d.ts +11 -0
- package/dist/ChatInput/ChatInput.d.ts +118 -0
- package/dist/ChatInput/HoverActionsRow.d.ts +13 -0
- package/dist/ChatInput/MorphGlyph.d.ts +15 -0
- package/dist/ChatTurnRow/ChatTurnRow.d.ts +115 -0
- package/dist/Chip/Chip.d.ts +7 -0
- package/dist/CodeBlock/CodeBlock.d.ts +24 -0
- package/dist/CodeBlock/grammars.d.ts +16 -0
- package/dist/CodeBlock/highlight.d.ts +38 -0
- package/dist/Context/Context.d.ts +36 -0
- package/dist/Conversation/Conversation.d.ts +64 -0
- package/dist/CustomCursor/CustomCursor.d.ts +1 -0
- package/dist/EmptyState/EmptyState.d.ts +24 -0
- package/dist/GlassButton/GlassButton.d.ts +19 -0
- package/dist/InlineCitation/InlineCitation.d.ts +30 -0
- package/dist/Loader/Loader.d.ts +23 -0
- package/dist/QuestionCard/QuestionCard.d.ts +32 -0
- package/dist/QuestionCard/parts.d.ts +84 -0
- package/dist/QuestionCard/types.d.ts +54 -0
- package/dist/QuestionGroup/QuestionGroup.d.ts +87 -0
- package/dist/Reasoning/Reasoning.d.ts +37 -0
- package/dist/ReplyThreadPopup/ReplyThreadPopup.d.ts +18 -0
- package/dist/Sources/Sources.d.ts +46 -0
- package/dist/SystemMessage/SystemMessage.d.ts +39 -0
- package/dist/TaskList/TaskList.d.ts +42 -0
- package/dist/TextHighlighter/TextHighlighter.d.ts +17 -0
- package/dist/Tool/Tool.d.ts +41 -0
- package/dist/announce/announce.d.ts +27 -0
- package/dist/disclosure/DisclosureBody.d.ts +22 -0
- package/dist/disclosure/DisclosureHeader.d.ts +42 -0
- package/dist/disclosure/useDisclosure.d.ts +30 -0
- package/dist/duration/formatDuration.d.ts +9 -0
- package/dist/grammars-B19jp7qm.js +3181 -0
- package/dist/grammars-B19jp7qm.js.map +1 -0
- package/dist/index.d.ts +80 -0
- package/dist/inline-chat-kit.css +2 -0
- package/dist/inline-chat-kit.js +5314 -0
- package/dist/inline-chat-kit.js.map +1 -0
- package/dist/markdown/parse.d.ts +105 -0
- package/dist/markdown/parseMarkdown.d.ts +47 -0
- package/dist/radiusCorrection/useCorrectedRadius.d.ts +24 -0
- package/dist/reducedMotion/reducedMotion.d.ts +3 -0
- package/dist/stateGlyph/StateGlyph.d.ts +23 -0
- package/dist/turnParts/turnParts.d.ts +156 -0
- package/dist/useChatTurns/useChatTurns.d.ts +127 -0
- package/dist/voice/useVoiceInput.d.ts +79 -0
- package/package.json +95 -0
- 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.
|