@gemboss/ui 0.25.0 → 0.32.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
@@ -2,6 +2,282 @@
2
2
 
3
3
  Versions up to 0.9.0 are described in the README, each in the section that introduced it.
4
4
 
5
+ ## 0.32.0 (09/10/2026)
6
+
7
+ **The bridge, once** (assistant sync, Chris 08/10/2026: "mọi service có panel này đồng bộ style + code dùng gemboss ui").
8
+ content-radar, partner-radar and search-radar each carried ~300 lines for the bridge on the user's machine, copied one
9
+ from the other, and reviews fixed one copy at a time. README "The bridge".
10
+
11
+ Added
12
+ - `createBridgeClient({ port, codeKey, app, tokenPath?, tokenSince? })`: `probe`, `detect` (Chrome's Local Network Access
13
+ permission read first; asked from a click only), `ensureCode`, `installCommand`, `dataToken` (reused until under an hour
14
+ is left; its error is the turn's), `turn(body, { signal, version })` (code + token; errors marked `atBridge` with the
15
+ status, so `bridgeTurnError` reads 401 as "another code" and a dead connection as "gone"), `control("stop" | "remove")`.
16
+ - `readTurnEvents(res, onEvent, { atBridge })` (the turn's `data:` events, whole across chunks, heartbeats skipped; a
17
+ read that dies is the bridge's only for the bridge's stream; a stream that ends without `done` throws
18
+ `BRIDGE_CLOSED_EARLY` / `SERVER_CLOSED_EARLY` instead of showing half an answer as done; an early exit cancels the
19
+ reader) and `postTurn(url, body)` for the surface's own route.
20
+ - `useBridge(client, { open, wantsLocal, pickerOpen, statusUrl, onDetected })`: probes while open, the server's engines
21
+ and the bridge together, merged (`engines`); a 3 s poll while a teammate installs; again on focus / tab / permission
22
+ change, also once paired, one at a time, and dropped when the user acted while it was out (an epoch); `lost()`,
23
+ `pair(code)`, `allowAccess()`, `control(kind)` (401 → Not connected, no answer → lost), `recheck()`. `local` is decided
24
+ after mount (no hydration mismatch on the dev machine); the client is pinned to the first render; a pairing code that
25
+ storage refuses lives in memory, so the command and the turns carry the same one.
26
+ - `bridgePickerProps(b, { latest, busy, controls, onControl, outdatedDetail, installNote, labels })` → `EnginePicker`'s
27
+ `bridge`, `setup` (per case: Allow / unblock / `BridgePairForm` + command / install / update) and `statusActions`
28
+ (Check again, Stop, Remove). `BRIDGE_SETUP_EN`, `bridgeControlNote`, `pickerBridgeState`, `mergeBridgeEngines`.
29
+ - `useEnginePicker().close()`: closes and puts focus on the chip at once (Stop / Remove before a confirm opens); also
30
+ `focusChip()`, `chipRef`, and `EnginePicker chipRef`. Every surface did it with `querySelector(".gb-engine-chip")`.
31
+ - `AssistantChat` / `ChatMarkdown` `linkPolicy(href, label, target)`: one prop for every link in an answer (written, bare,
32
+ a picture from another host): `true` = the kit's link, `false` = its words, a node = drawn as given; `target` is what
33
+ the kit would link (normalised) or null, for a policy that draws its own link. Replaces the
34
+ `ownLink` + `renderLink` pair used to turn links into text (kept; `linkPolicy` wins when both are given). A claimed
35
+ token carries `own: true`. React only (`chat-plain.js` is at its weight ceiling; gemcare keeps the pair).
36
+
37
+ Changed
38
+ - `AssistantChat` goes back to the end when a user message is ADDED, wherever it was sent from (a page's button, the
39
+ 📎 upload): before, only a send from its own composer did, and partner-radar scrolled by hand (#968). A note or an
40
+ answer arriving while the reader is up leaves them there, and so does an edit that drops a user message or swaps its id
41
+ (negative controls in chat-browser.test.tsx; the 0.31.0 pinKey fails the new test: 1717px from the end).
42
+ - `BridgePairForm` says "Pairing…" and takes one press at a time while the probe runs (up to 8 s).
43
+ - `chat-plain.js`: a token its `ownLink` claims carries `own: true`, like the React parser's.
44
+
45
+ No stylesheet changed; chat-plain.js stays under its weight ceiling. New tests: bridge.test.tsx (14),
46
+ bridge-browser.test.tsx (8, Chromium, fake bridge on test port 8797 answered by the router: open, poll, lost, pair,
47
+ permission, Stop, a stale probe after Stop, Stop answered 401), chat.test.tsx (+2), chat-browser.test.tsx (+1),
48
+ engine-browser.test.tsx (+1). Review #3952 (in this version): probes ordered by an epoch and one at a time, a stream
49
+ without `done` is an error, the reader is cancelled on an early exit, `linkPolicy` gets the checked target, the
50
+ README's `onDetected` keeps a "your machine only" engine, a stored-code fallback in memory, `local` after mount, the
51
+ client pinned, Stop's 401 is Not connected, the pinKey counts additions only.
52
+
53
+ ## 0.31.0 (09/10/2026)
54
+
55
+ The assistant header groups its controls (Chris 08/10/2026: "cái button clear history cần đưa lên topbar để tiết kiệm
56
+ không gian nhưng trên đó nhiều icon rồi nên cần gom nhóm"). One change in the package, every surface follows.
57
+
58
+ - `AssistantHost` header: four controls, + New chat, ⤢ Open full screen, ⋯ More, × Close. "Pin to the side" /
59
+ "Float as a popup" and "Shrink to a bar" (minimise) are rows of ⋯ now, not icons. At 1440 the header held five.
60
+ - `onClearHistory` (+ `canClearHistory`): a "Clear history" row (danger) in ⋯, in the panel header and in full screen's
61
+ chrome. The surface confirms (it knows what is cleared). Before this, content-radar spent a 44px row of its body on it.
62
+ - `menuItems` + `AssistantMenuItem`: a surface's own rows at the top of ⋯.
63
+ - ⋯ wears the fleet's row (`ui-menu__item`) and surface (`gemboss-popover-surface`) from shell.css, which the admin
64
+ loads; its placement is in assistant.css (`.gb-dock-menu*`), because the kit's `.ui-menu` rules are in kit-plain.css,
65
+ which the admin does not load. Escape closes ⋯ only (focus back on ⋯; the surface's "Escape hides the assistant" does
66
+ not fire on the same key; an Escape meant for a modal elsewhere is left to it); a press outside or the window losing
67
+ focus closes it; pressing ⋯ does not drag the popup. A row closes ⋯ and focuses it BEFORE its handler, so a confirm
68
+ the handler opens returns focus to ⋯, not to <body>. No `menu` / `menuitem` roles (no arrow keys, like the kit's Menu);
69
+ the rows do not inherit the popup header's "Drag this bar…" tooltip.
70
+ - Labels `more`, `clearHistory`. Tests: `assistant/header-menu.browser.test.ts` (Chromium, the admin's stylesheets only).
71
+
72
+ `SearchDialog` for a surface whose topbar centre IS the search (content-radar, 08/10/2026: "click opens a popup like
73
+ other surfaces"):
74
+
75
+ - `trigger="field"`: the door is the topbar's search box (TopbarSearch's `gemboss-topbar-search`: 36px, 44px under a
76
+ finger or at phone width, the hint at the right, `hint` prop, ⌘K by default), a link that opens the popup. `icon`
77
+ (default) is the 36px magnifier as before.
78
+ - `SearchItem.external`: a result outside the surface opens in a new tab (link or Enter) and the popup closes behind it,
79
+ marked ↗. Before, every result navigated the tab, so a collected post took the person off the surface.
80
+ - A search that throws says "Search failed: <message>" instead of "Nothing matches.".
81
+ - `onNavigate(href)`: a result inside the surface (Enter, plain click) goes through it instead of a full reload, so a
82
+ Next surface keeps a running assistant answer (content-radar passes `router.push`). ⌘ / Ctrl / Shift / Alt-click is
83
+ left to the browser and keeps the popup open.
84
+ - `minChars`: fewer characters say "Type at least N characters." instead of "Nothing matches.".
85
+ - Escape closes the popup in one press and is marked handled: a surface's own window listener ("Escape hides the
86
+ assistant") no longer fired on the same key, and the search box's clear-the-words Escape no longer took the first press.
87
+ - The `field` door has no aria-label: its name is the words on it (WCAG 2.5.3).
88
+ - Closing forgets the answer, and an answer still on its way when it closed: reopened, the popup drew the previous results
89
+ and highlight for a frame, and Enter in that frame opened the previous highlight. Tests: `search-dialog.browser.test.ts` (with a negative control: the reopen case
90
+ goes red without the reset).
91
+
92
+ `.ui-btn--on` (ui.css): the ON look for a plain button (accent-soft fill, accent border, accent-strong text, the kit's
93
+ selected look), for a CTA whose thing is already in effect ("Scheduled · Fri 09:00").
94
+
95
+ ## 0.30.3 (09/10/2026)
96
+
97
+ - The inspector under a finger (`pointer: coarse`): its tabs (`.gemboss-ei-tab`, were 27px) and group heads
98
+ (`.gemboss-ei-gh`, were 13px) are at least 44px tall, and its colour swatches (`.gemboss-ei-sw`) are 44x44 (were 30x30)
99
+ and wrap in a narrow column. Measured on the funnel canvas's theme Settings tab at 320 / 390 / 768 with touch; the theme
100
+ editor's inspector is the same component. Mouse sizes are unchanged. (The lockfile's entry for the package was still
101
+ 0.30.1; it now says 0.30.3.)
102
+
103
+ ## 0.30.2 (09/10/2026)
104
+
105
+ The admin's chat moves onto the package's composer and engine picker (gemboss admin, assistant sync).
106
+
107
+ - `ChatComposer` `canSend`: overrides the empty-box rule for the action. The admin sends a reference picture with no words,
108
+ and holds send while an upload runs, without disabling the text box. React only: `chat-plain.js` sits at its weight
109
+ ceiling (8698 B gzip) and no plain-HTML surface needs it yet.
110
+ - `ChatComposer` `stopDisabled`: Stop stays in the slot, disabled, while a stop is on its way (the admin's turn can take
111
+ seconds to abort; a second press was a silent no-op).
112
+ - chat-composer.css / chat.css: the admin's own engine chip rules are deleted (`.gemboss-ec-enginepick`, `-enginepick-btn`,
113
+ `-chiplabel`, `-chipsub`, `-chipmark` and their density and 44px-target rules). The admin now draws `EnginePicker` like
114
+ every surface (`gb-engine-*`, assistant-engine.css); nothing in the fleet wears those classes any more.
115
+
116
+ ## 0.30.1 (09/10/2026)
117
+
118
+ - `LayerList` / `SortableLayerList` under a finger (`pointer: coarse`): the row's action buttons are 44x44 (were 26x26,
119
+ the mouse size) and the row's pick button is at least 44px tall. Where the name and the buttons do not fit side by side
120
+ (the 300px Layers drawer), the row wraps and the buttons take a line of their own, so a name is never drawn under them.
121
+ Measured on the funnel canvas's section list at 768 with touch. Mouse sizes are unchanged.
122
+
123
+ ## 0.30.0 (09/10/2026)
124
+
125
+ **What gemform had and the package did not** (Chris 08/10/2026: "cái page login của gemform cái background bên dưới logo
126
+ cũng đẹp… trong gemform có cái gì mà gemboss ui không có thì chuẩn hoá đăng ký vào gemboss ui"). gemform
127
+ (gemlabs `toolkits/gemform`) was inventoried screen by screen; three things in it were shared patterns the package lacked.
128
+ Everything else it draws (shell, KPIs, cards, tables with bulk actions, filters, pager, switches, banners, key-value
129
+ lists, skeletons) the package already has, and gemform moves onto those. README "The sign-in screen", "Mini charts",
130
+ "Setup guide".
131
+
132
+ Added
133
+ - `AuthLayout backdrop`: `"facet"` (default, the markup and pixels of 0.29.2), `"grid"` (gemform's ground: a 34px iris
134
+ grid fading out from the panel's top-left corner, on an ink gradient), `"none"`. Tokens `--gb-panel-grid-bg`,
135
+ `--gb-panel-grid-line`, `--gb-panel-grid-size`, `--gb-panel-grid-fade` on `.gb-auth`; classes `gb-auth-panel--grid` /
136
+ `--none` for a page that writes the markup. Drawn only where the panel is (900 wide AND 520 tall).
137
+ - `Sparkline` + `sparklinePaths` and `ShareBar`, a new entry `minichart.css` (0.5 KB gzip). Six hand-drawn sparklines in
138
+ the fleet (admin x3, talent-radar, search-radar, gemwatcher, gemform); the baseline is 0 for counts unless fixed.
139
+ - `SetupGuide`, a new entry `setup-guide.css` (0.9 KB gzip): the admin's Setup guide (StoreChecklistCard,
140
+ `gemboss-setup-*`) with the kit's tokens, all native `<details>` (no script for plain HTML). The admin keeps its own
141
+ until it moves onto this one in a PR of its own.
142
+ - The catalog draws the sign-in screens at 1280 (at the catalog's column width the panel never showed), the grid beside
143
+ the facet, the mini charts in "Báo cáo", and a "Hướng dẫn thiết lập" section.
144
+
145
+ Neither new entry is in `gemboss-ui.css` or the core (parts gate); the admin imports none of them. The grid's rules are
146
+ in auth.css (+0.2 KB gzip there and in the bundle). `apps/admin/scripts/check-token-fallbacks-agree.mjs` now reads
147
+ minichart.css and setup-guide.css too.
148
+
149
+ Review #3925 (in this version): class names are written out (no `ui-sharebar__seg--${tone}` stem: the admin's orphan-rule
150
+ gate went red on it); ShareBar grows by share, so fractions fill the track; a sparse array's holes are gaps; SetupGuide
151
+ decides the open step once, draws a step without a body as a plain row, fills a `<progress>` instead of a styled width,
152
+ renders nothing for no steps, remembers a fold (`rememberKey`), takes a `group` name; the reduced-motion block names the
153
+ head's chevron with its own selector (`setup-guide-browser.test.tsx` reads the computed transition, with the weak
154
+ selector as negative control); the specificity test computes specificity instead of matching strings.
155
+
156
+ ## 0.29.2 (08/10/2026)
157
+
158
+ Fixes the rollout to search-radar (gemlabs #943) and content-radar (#944) found in the chat body. `chat-plain.js` does the
159
+ same as the components, held to them by chat-plain.test.ts.
160
+
161
+ - `ChatMarkdown` / `parseInline`: a bare `https://…` (and `<https://…>`) is a link again, as the surfaces' own renderers
162
+ drew it. GitHub's reading of where it ends: trailing `. , : ; ! ? _ ~ ' "` and a `)` or `]` with no partner inside the
163
+ URL go back to the sentence (linear time: a URL followed by 50 000 `)` parses in milliseconds). It also ends at `*`,
164
+ CJK text and full-width / typographic punctuation, and one glued to a word (`foohttps://…`) is not a link. Same
165
+ allowlist as a written link (`linkTarget`, then `new URL`: http(s) with a host), so `https://.`, `ftp:`, `javascript:`
166
+ stay text. New export `bareUrl`.
167
+ - `ChatMarkdown` / `parseBlocks`: `####` to `######` are headings at the bubble's lowest step (`level: 3`, an `h5` with
168
+ `gemboss-md-h3`) instead of four hashes in front of a sentence. `#######` and `#tag` stay text; a heading line with no
169
+ words (`#### `) is dropped instead of drawing an empty heading.
170
+ - `ImageHost.host` is optional: an entry with no host is the page's OWN origin (`imageHosts={[{ path: "/api/img/" }]}`),
171
+ for a root-relative picture (content-radar's `/api/img/…`). Checked after the browser's normalisation: the resolved
172
+ path must start with `path`; `../`, `%2e%2e`, `\`, an encoded slash or backslash, a double encoding (`%25`), a `;` path
173
+ parameter (`..;/`), `//host` and `/\host` are refused, and the src drawn is the resolved path. `path` must be a
174
+ directory below the root (`/x/`; `/` and `/x` are ignored). An entry WITH a host never opens relative paths.
175
+ - chat.css: the send circle is `.gemboss-ec-toolbar > button[type="submit"]` (the toolbar's own child, same specificity).
176
+ `.gemboss-ec-input button[type="submit"]` also reached the engine popup, a DOM descendant of the composer even in the
177
+ top layer, and drew a surface's Pair button there as the 44px filled circle (five surfaces wrote `type="button"` to
178
+ dodge it). Test of the forbidden combination in chat.test.tsx, and the popup measured in Chromium against the same
179
+ button outside the composer (chat-browser.test.tsx, chat-plain.test.ts), each with the old selector as its negative.
180
+
181
+ ## 0.29.1 (08/10/2026)
182
+
183
+ - `ChatThread` and plain `chat.thread`: keep following when a native scroll event from their own write-to-end arrives
184
+ after new content. The increased content gap no longer masquerades as the reader scrolling up. Wheel, key, touch and
185
+ direct programmatic moves up still unpin the thread.
186
+
187
+ ## 0.29.0 (08/10/2026)
188
+
189
+ **The assistant's chat body as components** (Chris 08/10/2026: "đồng bộ hết giao diện của AI assistant… mọi service có
190
+ panel này đồng bộ style + code dùng gemboss ui"). Until now the package drew the frame, the conversation list and the
191
+ engine picker; every surface wrote the bubbles, steps, starters and composer by hand on chat.css's classes, with its own
192
+ markdown renderer (five of them). The surface now hands in data and actions. README "The chat body".
193
+
194
+ Added
195
+ - `@gemboss/ui/assistant`: `AssistantChat` (thread + empty state + composer, from `messages`), and its pieces
196
+ `ChatThread` (role=log, follows the end only while the reader is there, aria-busy), `ChatMessage`, `ChatBubble`,
197
+ `ChatMarkdown` (the admin's parser and renderer: tables, headings demoted, fenced code that folds, safe links, pictures
198
+ only from fleet hosts, no raw HTML; `ownLink` / `renderLink` for a surface's own scheme), `ChatSteps` (rows print and
199
+ stop; kinds step / thinking / trace / steer), `ChatPending` (the one heartbeat; says its label once to a screen
200
+ reader), `ChatStarters` (they wrap), `ChatMeta` (Copy · engine · seconds · cost), `ChatError` (a fix: sentence,
201
+ command, Copy, the original folded), `ChatNote`, `ChatComposer` (a DIV, not a form; Enter sends, Shift+Enter and an IME's
202
+ Enter do not; one 44px slot that is send or stop; grows to 84px; `data-density` full → compact → icon → tight, measured),
203
+ `ChatAttach`, `PaperclipIcon`, `useComposerDensity`, `ChatLabelsProvider`, `CHAT_EN`.
204
+ - `useChatTurns()`: the conversation's state with an id per message; a turn is written BY ITS ID (`start`, `write`,
205
+ `step`, `finish`, `fail`, `note`), so a note landing mid-turn or the next turn opening cannot take its text (the
206
+ "patch the last element" bug of partner-radar #929 / content-radar #937). `bridgeTurnError(e)` + `atBridge(e, status)`:
207
+ only a failure AT the bridge (its call threw, or it answered 401: pairing code rejected) is the bridge's; its 400 / 503
208
+ and failures on the way to the server are the turn's.
209
+ - `parseBlocks`, `parseInline`, `linkTarget`, `imageSource`, `FLEET_IMAGE_HOSTS`: the markdown, testable without React.
210
+ - `EngineMark`, `ENGINE_MARK_ART`, `isEngineMarkId`: each engine's mark (from the admin).
211
+ - `EnginePicker`: `EngineChoice.mark` (the chip shows the engine's mark instead of its name when the composer is too
212
+ narrow), `EngineChoice.models` + `onPick(engine, model)` (every engine with its own models, one click picks both: the
213
+ admin's popup), `problem` (a red line on the Engine tab), `disabled` (the chip itself, while a switch is in flight).
214
+ - `assistant-chat.css`, a new entry (0.9 KB gzip), imported by the chat components; not in `gemboss-ui.css`.
215
+ chat.tsx imports chat.css and chat-composer.css too (they were "the surface links it").
216
+ - `chat-plain.js` (after `kit-plain.js`, and `engine-plain.js` when the page has the picker): `gembossUI.chat` with
217
+ `markdown`, `message`, `render`, `thread`, `steps`, `pending`, `starters`, `meta`, `error`, `note`, `composer`, `attach`,
218
+ `turnError`, `atBridge`, and `gembossUI.engineMark`. Same markup as the components, element by element (chat-plain.test.ts).
219
+ - `AssistantChat` `title` / `subtitle`: the chat's own heading where the frame has no title bar (full screen).
220
+ - `useFullChromeRoom(ref)`: how far full screen's floated buttons reach into an element (`--gb-chat-chrome-w` / `-h`,
221
+ `data-chrome`), for a surface arranging the pieces itself; `AssistantChat` uses it.
222
+ - Exports `./assistant/markdown` (the parser) and `./assistant/engine-mark-art` (the artwork): no JSX, no CSS, so a node
223
+ test or a server module imports them. The admin's `components/chat/markdown-blocks.ts` and `lib/engine-mark-art.ts`
224
+ now re-export these (one owner; the admin's tests read the package's code). A fence with a tag like `c#`, `c++`,
225
+ `objective-c` is a fence (it opened nothing, and its closing fence swallowed the rest of the answer as code).
226
+
227
+ Fixed
228
+ - Full screen: the frame's floated buttons (Back to the popup and a surface's `fullActions`) covered the first message of
229
+ a chat with no heading (content-radar /v2/assistant at 1280). `AssistantChat` measures how far they reach into it and
230
+ makes room (`data-gb-dock-chrome` on the host's button row is what it measures).
231
+ - On a phone, a `fullActions` button with no icon lost its words and showed an empty circle (assistant-chat.css).
232
+ - `BridgeNotice`'s "Open setup" was a 16px box with a 24px overlay, which a measuring tool reads as 16: its own box is now
233
+ 24px with a mouse and 44px under touch / at phone width (negative margins keep the line 16px). A long sentence no longer
234
+ wraps away from its dot (dot | sentence, the action under the sentence).
235
+ - `engine-plain.js`: the chip carries the engine's mark (`mark`) when chat-plain.js is on the page.
236
+
237
+ Review #3910 (all in this version): the log unpins at the reader's first move up (wheel, finger, keys), not 24px later, and
238
+ goes back to the end on a send or another conversation (`ChatThread pinKey`; plain: `log.toEnd()` / composer `thread`);
239
+ `turns.step` keeps a field an update leaves out; a bridge `TimeoutError` is the bridge's; clicking the pick already in
240
+ force changes nothing; a step for an engine with no mark keeps its ✓ / ✕; message keys never mix ids and positions;
241
+ plain `render()` keeps a message's element when its look did not change, the heartbeat's clock, and the folds the reader
242
+ opened; the full-screen room is measured again when the buttons themselves change size.
243
+
244
+ ## 0.28.0 (08/10/2026)
245
+
246
+ **Action card: an assistant proposes, a person applies.** The look of GemFeedback's merge and board-action cards (Chris
247
+ 08/10/2026, "follow theo gemboss ui"), so every assistant in the fleet asks before a change the same way. README "Action card".
248
+
249
+ Added
250
+ - `ActionCard`, `ActionCardSection`, `ActionCardItem`, `ActionChange` (React), `action-card.css` (its own entry
251
+ `./action-card.css`, loaded by the component; `gemboss-ui.css` unchanged).
252
+ - Catalog section "Thẻ hành động".
253
+ - `packages/ui/weight-budget.json`: a ceiling for `packages/ui/src/action-card.css`.
254
+ ## 0.27.0 (08/10/2026)
255
+
256
+ **The engine picker for plain HTML**, for gemcare's two assistants (inbox, Reports), which have no React.
257
+
258
+ Added
259
+ - `engine-plain.js` (new entry, after `kit-plain.js`): `gembossUI.enginePicker(host, options)` → `{ update, openTab, open,
260
+ close, isOpen, destroy, element, panel }` and `gembossUI.bridgeNotice(options)`. Same markup as `EnginePicker` (one
261
+ stylesheet, `assistant-engine.css`), same behaviour; a redraw patches in place. README "Plain HTML:
262
+ `gembossUI.enginePicker()`". 5.3 KB gzip minified, its own ceiling.
263
+ - `gembossUI.placeFloating(layer, anchor, end)` in kit-plain.js: the menus' placement, one plain copy (floatMenu and
264
+ engine-plain.js use it).
265
+
266
+ Changed
267
+ - `EnginePicker`: an Escape that closes the panel is marked handled (`preventDefault`) also when focus is outside the
268
+ panel. A surface whose Escape hides the assistant and checks `defaultPrevented` waits for the next one; the
269
+ window-capture markers the surfaces added (gemlabs #922, gemfeedback #66) are no longer needed. Not an Escape it does
270
+ not own: an IME composition (keyCode 229), or one in a modal opened from the panel; and focus entering such a modal
271
+ closes the panel, which would otherwise sit above it in the top layer.
272
+
273
+ ## 0.26.0 (08/10/2026)
274
+
275
+ **A tablet glyph to draw with** (the funnel canvas's "All sizes": each page at desktop, tablet and phone widths).
276
+
277
+ Added
278
+ - `TabletGlyph` in `@gemboss/ui/editor`, beside `MonitorGlyph` and `PhoneGlyph`. `DeviceToggle` draws it for its Tablet
279
+ button (it carried the same SVG inline), so there is one tablet.
280
+
5
281
  ## 0.25.0 (07/10/2026)
6
282
 
7
283
  **The assistant's engine picker** (Chris 07/10/2026): a chip in the composer row, as in the admin, that opens a popup