inline-chat-kit 0.51.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,309 @@ The versions before 1.0 follow the pre-release convention: **a breaking change
6
6
  or new public API bumps the minor**, and the patch is for fixes. Anything that would break an
7
7
  existing install is called out under **Breaking**, with what to do about it.
8
8
 
9
+ ## 0.54.0 — 2026-09-12
10
+
11
+ Everything here came out of using the kit on a phone. It is better there than
12
+ it was, and the README now says plainly that a phone is not what it is for —
13
+ see *Where it is meant to run*.
14
+
15
+ ### Fixed
16
+
17
+ - **A sent message travels to the top instead of cutting to it.** Sending adds
18
+ a whole turn at once, so the content grew by its height in a single frame and
19
+ the scroll was assigned its new position in that same frame. Traced at
20
+ 1120×680 across two answers: three moves over 24px in the whole session and
21
+ every one of them a single frame — 154px, **416px**, 172px. The 416 was the
22
+ send.
23
+
24
+ The reasoning block folding away at the end was never the problem: it was
25
+ already gradual (−29, −58, −206 across frames) and moved the scroll by
26
+ nothing. Now the one move that is a decision is eased and the thousand that
27
+ are tracking stay instant, because easing the arrival of text would mean the
28
+ line being read slides for a third of a second. Measured after: `178 → 206 →
29
+ 244 → 287 → 326 → 358 → 384`, largest single step 42px.
30
+
31
+ - **The room under the last turn is measured rather than guessed.** A turn can
32
+ only be brought to the top if there is room under it to scroll into, and that
33
+ room was a `99vh` typed into the demo's stylesheet. At the end of the scroll:
34
+ 673px of it on desktop, 836px on a phone, and the last turn **61px above the
35
+ top edge** — a completely blank screen at the bottom of every conversation.
36
+
37
+ `Conversation` takes a `tail` prop now, `"auto"` by default, and the
38
+ measurement is `screen - anchorOffset - (the anchored turn's top to the end
39
+ of the content)`. That makes the end of the scroll exactly where the anchored
40
+ turn sits at the anchor, so coming to rest and running out of scroll are the
41
+ same place and the room is spent holding the turn up rather than left over
42
+ underneath it. It also explains why chasing an answer costs nothing: the
43
+ content grows and the room shrinks by the same pixel, so the scroll height
44
+ does not move and neither does the turn at the top.
45
+
46
+ - **A short answer leaves its question at the top too.** The room was being
47
+ sized correctly and then not rested at: resting still meant the end of the
48
+ *content*, which agrees with the end of the scroll only while the answer is
49
+ longer than the screen. Below that the difference was left unspent with the
50
+ turn stranded mid-view — 575px from the top in a 1174px window, with 475px
51
+ still to scroll.
52
+
53
+ - **`endOfContent` counted the conversation's top padding twice.** A
54
+ conversation ending at 729 read as ending at 829, so the composer came to
55
+ rest 100px above the `endOffset` it was given. See **Breaking**.
56
+
57
+ - **Only a control that folds lets go of the end.** Releasing the follow on any
58
+ press meant clicking into the composer released it too, so when an answer
59
+ settled there was nothing to bring the view back — and settling is also when
60
+ the reasoning block folds itself away, so the content shrank by its height
61
+ and everything above dropped into view. It read as the conversation jumping
62
+ to show an older message.
63
+
64
+ - **An attachment's remove button is always there on a phone.** It sat under a
65
+ wash over the whole square, revealed on `:hover`, and there is nothing to
66
+ hover with — so it was drawn, in the accessibility tree, present to every
67
+ test that asks whether it is *there*, and unreachable. Under
68
+ `@media (hover: none)` it is a button in the top-right corner instead:
69
+ measured 28×28, 4px in from the corner of the 64px square, and a press at its
70
+ middle lands on it. The wash stays on a pointer, where it is the better
71
+ answer.
72
+
73
+ - **A picture attachment was never actually drawn.** `background-image`
74
+ computed to `none` in both engines: the URL went into an unquoted `url()`,
75
+ and a bracket inside it closes the `url()` early, so the browser threw the
76
+ whole declaration away with no error anywhere. An SVG data URL says
77
+ `fill="url(#g)"`, and `encodeURIComponent` does not touch brackets — so the
78
+ stories showed a row of blank squares under the heading "pictures — they show
79
+ themselves" for as long as they had existed, and any host whose image URL
80
+ contains a bracket had the same.
81
+
82
+ - **The reply thread stays on the screen.** It hung off the phrase from a fixed
83
+ top and grew downwards with nothing stopping it: at 390×844 with a passage
84
+ 556px down, the panel ended at 920 after one reply — 76px past the bottom
85
+ edge, and every message after it pushed more out of reach. `placePanel`
86
+ returns a `maxHeight` now and stops following a phrase down once there would
87
+ be less than 260px left. A window short enough had this at any width.
88
+
89
+ - **The disclosure row's fades work at any height.** They required
90
+ `clientHeight >= 630`, the thread's old fixed ceiling written again as a
91
+ number, so with the height coming from the screen they would have stopped
92
+ appearing exactly where they are most needed — the short panel is the one
93
+ whose content does not fit.
94
+
95
+ - **`transition: all` on the glass button.** 400ms, on what the composer's mic,
96
+ plus and send are made of, and the X on an attachment. Among the properties
97
+ it was animating: `backdrop-filter`, a blur re-composited every frame for
98
+ four hundred milliseconds, and the same blur before and after. Named now —
99
+ colour and shadow at 200ms, the lift at 160ms.
100
+
101
+ - **`:hover` no longer moves anything on a touchscreen.** The glass button's
102
+ lift and the artifact card's were ungated, and a touchscreen fires a hover on
103
+ tap and leaves it there until the next tap somewhere else — so they stood a
104
+ pixel or two high for as long as you did not touch anything. Both are inside
105
+ `@media (hover: hover) and (pointer: fine)`. The colour and shadow stay on
106
+ touch: those say "this is the one under your finger", which is true.
107
+
108
+ ### Added
109
+
110
+ - **A bottom sheet on a phone, for the reply thread.** Below 760px it comes up
111
+ from the bottom instead of hanging off the phrase, with a grabber and a drag
112
+ to put it away — the same sheet the artifact pane becomes, with the same
113
+ tokens. Capping its height alone would have turned it into a letterbox: 289px
114
+ of thread in the lower third, under a phrase you can no longer see. It grows
115
+ from the bottom edge with the thread rather than standing at a fixed 88%,
116
+ because one reply is what a thread usually has and a sheet that is mostly
117
+ white reads as something that failed to load.
118
+
119
+ - **The type scale rises on a touch device, and only where it earns it.**
120
+ `@media (pointer: coarse)` puts the prose and anything you type into at 16px
121
+ and caps everything else at 14. iOS zooms the page in to any editable whose
122
+ text computes under 16px and does not zoom back out; the answer is to stop
123
+ being under 16px rather than to fight the viewport.
124
+
125
+ | | before | after | mouse |
126
+ | --- | --- | --- | --- |
127
+ | prose | 12px | 16px | 12px |
128
+ | composer | 12px | 16px | 12px |
129
+ | header, chips | 13–14px | 14px | 13–14px |
130
+
131
+ - **`--ick-field-size`** — the size text somebody types into draws at, which is
132
+ not the same idea as a step on the type scale even when they share a number.
133
+ `max(1rem, …)` under a coarse pointer: a floor, not an override, so a theme
134
+ at 18px keeps 18 and a theme at 13 gets 16.
135
+
136
+ - **`--ick-icon`** — icons sized from the text beside them. Around fifty
137
+ `size={13..16}` call sites were fixed pixels, so raising the text left every
138
+ one behind. `1.15em` on touch, and `auto` elsewhere, which on an inline SVG
139
+ means its own width and height attributes — so a pointer keeps exactly the
140
+ sizes the call sites asked for.
141
+
142
+ - **`--ick-control-xs` … `--ick-control-xl`** — `Button`'s five heights, as
143
+ tokens. They were pixels in its stylesheet, flattered 13px text, and are
144
+ tight around 14: the tool row was 16px of text in a 26px box. 28/32/38/44/52
145
+ on touch against 24/28/32/40/48. The look changes; the hit area does not —
146
+ that is `--ick-touch-target`, and it has always been 44px without anything
147
+ moving.
148
+
149
+ - **`tail` on `<Conversation>`** — `"auto"` or a number of pixels. See
150
+ **Fixed**, and **Breaking** for what it supersedes.
151
+
152
+ - **`npm run reach-check`** — what a thumb can reach on a device with no hover,
153
+ in both engines at 390×844, against Storybook rather than the demo (the demo
154
+ holds one attachment; the question needs two side by side, because these
155
+ buttons grow an invisible 44px box under `pointer: coarse` and two of those
156
+ 6px apart would have the wrong one answering).
157
+
158
+ - **`npm run follow-check:tall`** — the same follow check in a 1400px window.
159
+ The short window made every answer taller than the screen, which is the one
160
+ regime where two different numbers agree, so an assertion that there is
161
+ nowhere further to scroll passed while being unable to fail.
162
+
163
+ - **`motionRules.test.ts`** — two rules read out of the stylesheets, because
164
+ neither is visible on screen: no `transition: all`, and no `:hover` that
165
+ moves something outside a pointer query. A screenshot is taken with no
166
+ pointer, so the second one can never appear in a picture.
167
+
168
+ ### Changed
169
+
170
+ - **`zoom-check` measures font sizes rather than a proxy for them.** It used to
171
+ assert that `maximum-scale=1` was in place at the instant focus landed, for a
172
+ hook that locked the host's viewport while a field had focus. No engine
173
+ outside a real iPhone implements zoom-on-focus, so the check observed a stand-in
174
+ and twice went green over a version that still zoomed. A font size is the
175
+ input to the platform's own rule, is the same number in every engine, and is
176
+ either under 16 or not.
177
+
178
+ ### Breaking
179
+
180
+ - **`endOffset` on `<Conversation>` now means what it says.** `endOfContent`
181
+ added the content wrapper's `offsetTop` to the last turn's, and both are
182
+ measured from the same positioned ancestor — so it counted the viewport's
183
+ padding twice and every value was effectively `endOffset + padding`. If you
184
+ set it, the composer will now rest closer to the bottom edge than it did by
185
+ exactly your conversation's top padding. **What to do:** raise `endOffset` by
186
+ that padding to keep what you had. The demo went from `24` to `120`.
187
+
188
+ - **`--ick-conversation-tail` is written by the component.** `<Conversation>`
189
+ measures the room its anchor needs and sets the token on the content wrapper,
190
+ which beats a value set in a stylesheet. **What to do:** nothing, if you were
191
+ not setting it. If you were, pass `tail={<px>}` instead — a number takes the
192
+ measurement over, and the token remains the value used before the first
193
+ measurement.
194
+
195
+ - **The type scale is different on a touch device.** Nothing renamed and
196
+ nothing removed, but a phone or tablet now draws the prose and the composer
197
+ at 16px, the rest at 14, icons at `1.15em`, and controls 4–8px taller. **What
198
+ to do:** nothing, unless you want the old sizes, in which case re-declare the
199
+ `--ick-text-*`, `--ick-answer-size`, `--ick-composer-size`, `--ick-field-size`,
200
+ `--ick-icon` and `--ick-control-*` tokens inside your own
201
+ `@media (pointer: coarse)` block.
202
+
203
+ ## 0.53.0 — 2026-09-03
204
+
205
+ ### Changed
206
+
207
+ - **The artifact card moves instead of changing colour.** Pointing at it lifted
208
+ nothing and washed it grey — `--ick-inset`, ink at about 4% — while the card
209
+ sits on a ground that is ink at 5%. So the hover slid the card *towards* the
210
+ colour of the thing it is meant to be raised off, and it read as smudged
211
+ rather than raised. It now travels 2px up under the pointer with the shadow
212
+ thrown further, and 1px down under the press, and its surface never changes.
213
+
214
+ Measured, in both themes: rest 0, hover −2, press +1, with the shadow's total
215
+ throw going 28 → 47 → 9 and the background one value throughout. The press
216
+ did not work in the first cut — `.card:active` lost to
217
+ `.artifact:not([data-open]) .card:hover` four specificity points to two, so
218
+ the card stayed lifted under the finger. A press that does nothing looks like
219
+ a press arriving a frame late, which is why this was found by the
220
+ measurement and not by looking.
221
+
222
+ - **Nothing is drawn for the open card any more.** It had a 2px ink ring. Next
223
+ to a whole pane holding the same document, that was a second answer to a
224
+ question already answered, and on its own it read as a selected row in a
225
+ list. What marks it now is that it stops lifting under the pointer — there is
226
+ nowhere for it to go — and `aria-expanded`, which says it outright to anybody
227
+ not using one. The card is the only `aria-expanded` in the kit without an
228
+ `aria-controls` beside it, because what it opens is not its to render.
229
+
230
+ - **The pane opens and closes instead of appearing.** `ChatLayout` rendered the
231
+ pane the instant it had one: the conversation jumped narrower, the pane was
232
+ simply there, and closing it was the same in reverse. Both now animate, and
233
+ the two halves are deliberately not the same — the room opens flat and the
234
+ pane arrives with a spring in it, because `width` is what the conversation is
235
+ laid out against and any overshoot there re-wraps every line of the answer
236
+ twice on the way past. Leaving is quicker and in the other order.
237
+
238
+ The document inside is laid out **once**, at the width it ends up at, and the
239
+ slot uncovers it: measured at 369.59px held across every frame of an entrance
240
+ in which the room travelled 409px and the conversation followed it.
241
+
242
+ ### Added
243
+
244
+ - **`--ick-shadow-float-lift` and `--ick-shadow-float-press`** — the same card
245
+ pointed at, and pressed. Stated per theme like `--ick-shadow-float`, and for
246
+ the same reason: a dark shadow has more work to do on a near-black page and
247
+ still must not cut the card out of it.
248
+
249
+ - **`npm run showcase:artifact`** — records the card and the pane, and measures
250
+ what the recording is meant to show. The numbers are the half that is not
251
+ visible at speed.
252
+
253
+ ### Breaking
254
+
255
+ - **`--ick-artifact-surface-hover`, `--ick-artifact-open-edge` and
256
+ `--ick-artifact-open-edge-width` are gone.** They were the grey hover fill
257
+ and the open card's ring, neither of which is drawn any more. If you were
258
+ overriding them, there is nothing to override; the hover is now
259
+ `--ick-shadow-float-lift` and the open state draws nothing.
260
+
261
+ - **`--ick-artifact-pane-width` and `--ick-artifact-pane-width-wide` are in
262
+ `cqi`, not `%`.** `max(360px, 33cqi)` rather than `max(360px, 33%)`. The
263
+ slot's own width is what animates, so a percentage of it would be a
264
+ percentage of a number moving from zero — the pane would re-wrap its text on
265
+ every frame of its own entrance. Container units are measured off
266
+ `ChatLayout`, which is not moving. Same numbers in the same places: measured
267
+ at 370px in a 1120px layout and 475px in a 1440px one, unchanged from before.
268
+
269
+ If you place an `ArtifactPane` **without** `ChatLayout`, give its parent
270
+ `container-type: inline-size` — outside a container, `cqi` falls back to the
271
+ viewport.
272
+
273
+ ## 0.52.1 — 2026-09-03
274
+
275
+ ### Fixed
276
+
277
+ - **The composer's editor was 16px wider than the box that clips it.**
278
+ `.editor` had `width: 100%` and 8px of padding either side with no
279
+ `box-sizing`, so its border box overflowed `.editorWrap` — which clips —
280
+ and the right edge of the text was cut. `scrollWidth` 292 against
281
+ `clientWidth` 276 on a 276px composer.
282
+
283
+ This kit has no global `box-sizing` reset on purpose, because a reset inside
284
+ a CSS module leaks into the host page; the rule is that every padded box
285
+ states its own. This one had not, and it had been that way unnoticed.
286
+
287
+ Found by the new visual QA pass on its first run, which reported it on 25
288
+ separate stories.
289
+
290
+ ## 0.52.0 — 2026-09-02
291
+
292
+ ### Added
293
+
294
+ - **`--ick-tray` and `--ick-tray-thumb`** — a segmented control's recessed
295
+ track and the raised thumb in it. Every chat has one somewhere, and until now
296
+ the demo carried the colours in variables of its own, so the control had no
297
+ background anywhere else.
298
+
299
+ The thumb has to be **lighter than its tray in both themes**, and the two get
300
+ there from opposite ends: in light it goes all the way to paper, in dark pure
301
+ white is too loud so it is a wash of ink instead. Stated per theme rather
302
+ than mixed, because a mix cannot reverse.
303
+
304
+ Measured from painted pixels rather than declared values — with alpha
305
+ involved, `getComputedStyle` reports what was written, not what was drawn:
306
+
307
+ | | tray | thumb | |
308
+ | --- | --- | --- | --- |
309
+ | light | `rgb(236)` | `rgb(253)` | 1.16:1 |
310
+ | dark | `rgb(36)` | `rgb(72)` | 1.70:1 |
311
+
9
312
  ## 0.51.0 — 2026-09-02
10
313
 
11
314
  ### Added
package/README.md CHANGED
@@ -10,6 +10,42 @@ answers you can draw on with a marker,
10
10
  syntax-highlighted code blocks, a scroll container that keeps up with an
11
11
  answer, and reply-in-thread popups.
12
12
 
13
+ ## Where it is meant to run
14
+
15
+ **Desktop and tablet.** That is what this is designed for, tuned for, and what
16
+ the checks measure.
17
+
18
+ It runs on a phone and does not break. The artifact pane becomes a bottom
19
+ sheet, so does a reply thread; every control is reachable by a thumb; nothing
20
+ zooms when a field takes focus; the type scale rises so reading text is 16px
21
+ and the icons rise with it. There are guards for each of those, in two engines,
22
+ at 390×844 — `npm run zoom-check` and `npm run reach-check`.
23
+
24
+ **But it is not tuned for a phone, and the inline idea itself is weaker there.**
25
+ Worth saying plainly rather than leaving you to discover it.
26
+
27
+ The argument for an inline composer is that you see your question and its
28
+ answer at once, because the thing you type into *is* the message. A phone gives
29
+ you about half a screen once the keyboard is up — and the browser will not tell
30
+ you how much. So the platform takes away the one thing the design exists for.
31
+
32
+ Four more that no amount of CSS reaches:
33
+
34
+ | | |
35
+ | --- | --- |
36
+ | the keyboard's height | the browser does not report it reliably; a native scroll view is simply told |
37
+ | text selection | the system's own selection UI fights a marker drawn in prose — this kit ships a selection-mode toggle *because* of that |
38
+ | scroll physics | rubber-banding and momentum belong to the browser, not to you |
39
+ | haptics | no API at all on iOS Safari |
40
+
41
+ If you need a phone-first chat, treat this as a specification for one rather
42
+ than an implementation of one. The parts that would carry over are the ones
43
+ with no DOM in them — `useChatTurns`, `turnParts`, `placePanel` — and they are
44
+ plain TypeScript on purpose.
45
+
46
+ Desktop **applications** are a different story: an Electron or Tauri renderer
47
+ is a browser, so this works there today with nothing changed.
48
+
13
49
  ## Install
14
50
 
15
51
  ```bash
@@ -32,7 +32,14 @@ export interface ArtifactCardProps extends Omit<HTMLAttributes<HTMLDivElement>,
32
32
  lines?: number;
33
33
  /** Open it. Without one the card is a record rather than a control. */
34
34
  onOpen?: (id: string) => void;
35
- /** True while its pane is the one on screen. */
35
+ /**
36
+ * True while its pane is the one on screen.
37
+ *
38
+ * Nothing is drawn for it. The pane standing beside the conversation is
39
+ * already the answer, and a ring on the card was a second one — so what
40
+ * marks it is that it no longer lifts under the pointer, and
41
+ * `aria-expanded`, which says it outright to anybody not using a pointer.
42
+ */
36
43
  open?: boolean;
37
44
  }
38
45
  /**
@@ -19,6 +19,15 @@ export interface ChatLayoutProps extends HTMLAttributes<HTMLDivElement> {
19
19
  * changes it belongs in the pane's own header, where somebody can find it.
20
20
  */
21
21
  pane?: (state: PaneState) => ReactNode;
22
+ /**
23
+ * Close it — dragged down, or the conversation behind it pressed.
24
+ *
25
+ * Only used on a phone, where the pane is a sheet. Both of those are ways
26
+ * out that belong to the *layout* rather than to the pane: the pane owns its
27
+ * close button, the layout owns the gesture and the ground around it. Leave
28
+ * it out and the sheet has neither, which is a sheet a thumb cannot dismiss.
29
+ */
30
+ onDismiss?: () => void;
22
31
  }
23
32
  /**
24
33
  * Where the pane goes, decided once.
@@ -32,4 +41,4 @@ export interface ChatLayoutProps extends HTMLAttributes<HTMLDivElement> {
32
41
  * What is left open is the part that actually differs — what is *in* the pane.
33
42
  * See `ArtifactPane`.
34
43
  */
35
- export declare function ChatLayout({ children, pane, className, ...rest }: ChatLayoutProps): import("react").JSX.Element;
44
+ export declare function ChatLayout({ children, pane, onDismiss, className, ...rest }: ChatLayoutProps): import("react").JSX.Element;
@@ -30,6 +30,57 @@ export interface ConversationProps extends HTMLAttributes<HTMLDivElement> {
30
30
  * neatly underneath and out of sight.
31
31
  */
32
32
  anchorOffset?: number;
33
+ /**
34
+ * How much room is left under the last turn when the view is at the end, in
35
+ * pixels.
36
+ *
37
+ * Zero puts the last line flush with the bottom edge, which is right for a
38
+ * transcript and wrong for this kit: the last turn *is* the composer — the
39
+ * input is the message — so flush means the thing you type into is jammed
40
+ * against the edge of the screen the moment an answer finishes. On a phone
41
+ * that is also where the browser's own chrome lives.
42
+ */
43
+ endOffset?: number;
44
+ /**
45
+ * The empty room under the last turn, which is what lets a turn be brought
46
+ * to the top at all.
47
+ *
48
+ * An element cannot be scrolled to the top of a container that ends just
49
+ * below it — so `anchorId` only works if there is somewhere to scroll into.
50
+ * That room used to be a guess written in the host's stylesheet, `99vh`,
51
+ * and a guess is wrong in both directions at once: too much of it and the
52
+ * reader can scroll a whole screen into nothing (measured: 697px of blank
53
+ * in a 680px view, with the last turn above the top edge), too little and
54
+ * the newest message cannot reach the top.
55
+ *
56
+ * `"auto"` measures it instead:
57
+ *
58
+ * screen - anchorOffset - (the anchored turn's top to the end) - padding
59
+ *
60
+ * and the second term is the whole point. Measuring the **last turn's own
61
+ * height** instead — which this did first — leaves everything between the
62
+ * anchor and the end unaccounted for: the last turn is the composer, 44px
63
+ * of it, so an answer that already filled the screen still got 536px of
64
+ * room under it to scroll into. What the room is for is lifting the
65
+ * anchored turn to the top, and what stands between that turn and the
66
+ * bottom edge is the whole stack under it.
67
+ *
68
+ * Sized this way, the end of the scroll lands exactly where the anchored
69
+ * turn sits at the anchor. So the view comes to rest **at** the end of the
70
+ * scroll and there is nowhere further to go — the room is spent holding the
71
+ * turn up, not left over underneath. It shrinks as the answer grows, which
72
+ * is also why an answer does not have to be chased: the content grows and
73
+ * the room shrinks by the same pixel, so the scroll height does not move
74
+ * and neither does the turn at the top.
75
+ *
76
+ * Never below `endOffset`, so a turn taller than the screen still rests
77
+ * with the composer clear of the bottom edge instead of jammed against it.
78
+ *
79
+ * Zero unless this conversation anchors, because the room exists for
80
+ * anchoring. A number takes it over, in pixels; so does the
81
+ * `--ick-conversation-tail` token, which this writes.
82
+ */
83
+ tail?: "auto" | number;
33
84
  /** Switch the whole thing off and it is a plain scroll container. */
34
85
  follow?: boolean;
35
86
  /**
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Where the reply panel goes, as arithmetic.
3
+ *
4
+ * A function rather than ten lines inside the render, and the reason is that
5
+ * it could not otherwise be tested. The panel's geometry reaches the DOM
6
+ * through Motion's `animate` prop, and Motion animations never complete under
7
+ * jsdom — so a test that reads `panel.style.left` reads the `initial` value
8
+ * for ever, which is the anchor. The first guard written for this passed
9
+ * against the broken version because of it.
10
+ */
11
+ export interface Anchor {
12
+ left: number;
13
+ top: number;
14
+ width: number;
15
+ }
16
+ export interface Placement {
17
+ x: number;
18
+ y: number;
19
+ width: number;
20
+ /**
21
+ * The room it has between where it starts and the bottom of the screen.
22
+ *
23
+ * The panel hangs off a phrase and grows downwards as the thread fills, and
24
+ * nothing used to stop it growing past the bottom edge. Measured on a 390px
25
+ * phone with a passage 556px down: after one reply the panel ended at 920
26
+ * in an 844px screen, and every message after that pushed more of it
27
+ * further out of reach. This is what it may use, so the thread scrolls
28
+ * inside it instead of running off the end of the world.
29
+ */
30
+ maxHeight: number;
31
+ }
32
+ /**
33
+ * **Fit first, then place.** That order is the whole of it.
34
+ *
35
+ * The version before this widened to 480 whatever the screen was, then ran two
36
+ * clamps in a row: push the left edge in to the padding, then push the right
37
+ * edge in to `screenWidth - padding`. For a box that does not fit, the second
38
+ * always undoes the first — on a 390px phone it finished at `390 - 24 - 480`,
39
+ * so the panel sat at **x = −114** with its heading off the side of the world.
40
+ * Clamping a box into a space smaller than itself has no answer; the box has
41
+ * to be made smaller first.
42
+ */
43
+ export declare function placePanel(anchor: Anchor, screenWidth: number, screenHeight?: number): Placement;