@gemboss/ui 0.13.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +512 -1
  2. package/README.md +426 -14
  3. package/catalog/editor.html +1027 -0
  4. package/catalog/index.html +1215 -90
  5. package/gemboss-ui.css +3 -4698
  6. package/package.json +24 -5
  7. package/parts/components.css +5 -0
  8. package/parts/core.css +5 -0
  9. package/parts/shell.css +5 -0
  10. package/src/app-shell.css +1 -256
  11. package/src/assistant/engine.tsx +607 -0
  12. package/src/assistant/host.tsx +1 -0
  13. package/src/assistant/index.ts +1 -0
  14. package/src/assistant-engine.css +1 -0
  15. package/src/assistant.css +1 -558
  16. package/src/auth.css +1 -606
  17. package/src/auth.tsx +1 -0
  18. package/src/board-move.ts +194 -0
  19. package/src/chat-composer.css +1 -110
  20. package/src/chat.css +1 -432
  21. package/src/collections.css +1 -350
  22. package/src/collections.tsx +170 -40
  23. package/src/dialog.tsx +3 -135
  24. package/src/editor/controls.tsx +207 -0
  25. package/src/editor/css-url.ts +11 -0
  26. package/src/editor/frame.tsx +243 -0
  27. package/src/editor/index.ts +52 -0
  28. package/src/editor/layers.tsx +136 -0
  29. package/src/editor/panel.tsx +189 -0
  30. package/src/editor/panes.tsx +45 -0
  31. package/src/editor/sortable.tsx +79 -0
  32. package/src/editor/swatch.ts +45 -0
  33. package/src/editor/tree.tsx +61 -0
  34. package/src/editor-layers-plain.js +1 -0
  35. package/src/editor-plain.js +1 -0
  36. package/src/editor.css +1 -1099
  37. package/src/gemboss-field.css +1 -112
  38. package/src/gemboss-tokens.css +1 -164
  39. package/src/icons.tsx +2 -0
  40. package/src/index-table.tsx +20 -56
  41. package/src/index.ts +16 -6
  42. package/src/kit-plain.css +1 -499
  43. package/src/kit-plain.js +1 -575
  44. package/src/launcher.tsx +1 -0
  45. package/src/menu-layer.ts +94 -0
  46. package/src/menu.tsx +28 -2
  47. package/src/portal.css +1 -139
  48. package/src/portal.tsx +1 -0
  49. package/src/reactions-data.ts +59 -0
  50. package/src/reactions.css +1 -0
  51. package/src/reactions.tsx +140 -0
  52. package/src/resource-list-select.tsx +73 -0
  53. package/src/search-dialog.tsx +146 -0
  54. package/src/segmented-click.ts +20 -0
  55. package/src/segmented.tsx +98 -0
  56. package/src/selection.tsx +79 -0
  57. package/src/shell.css +1 -1079
  58. package/src/theme-dark.css +1 -66
  59. package/src/theme-script.ts +10 -0
  60. package/src/theme.tsx +5 -5
  61. package/src/ui.css +1 -610
package/CHANGELOG.md CHANGED
@@ -2,6 +2,516 @@
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.25.0 (07/10/2026)
6
+
7
+ **The assistant's engine picker** (Chris 07/10/2026): a chip in the composer row, as in the admin, that opens a popup
8
+ with Engine · Setup · Status tabs. Engine, model, effort, the bridge's status and its install command leave the chat.
9
+
10
+ Added
11
+ - `EnginePicker`, `EnginePanel`, `BridgeNotice`, `useEnginePicker` (+ types `EngineChoice`, `Choice`, `BridgeStatus`,
12
+ `BridgeSetup`, `BridgeState`, `EnginePickerTab`) in `@gemboss/ui/assistant`. README "The engine picker".
13
+ - `assistant-engine.css`, a new entry (1.9 KB gzip), imported by the component; not in `gemboss-ui.css`.
14
+ - `placeFloating(layer, anchor, end)` in `menu-layer.ts`: the menus' placement, now shared with the picker's panel
15
+ (`floatMenu` calls it; same geometry).
16
+
17
+ ## 0.24.0 (07/10/2026)
18
+
19
+ **The Studio canvas in the editors' frame** (gemboss #3856): the pieces the admin's Studio needed from the editor kit.
20
+
21
+ Added
22
+ - `EditorChatToggle` takes a `name` ("Hide <name>"; default "chat panel"): a column that is not a chat says so.
23
+
24
+ Changed
25
+ - A folded editor column (`.is-chat-collapsed`) is `visibility: hidden`: at width 0 its fields stayed in the Tab order.
26
+
27
+ Removed
28
+ - `.gemboss-pageedit-status` and `.gemboss-pageedit-kind` looks: the page editor draws Polaris' Badge, as the funnel beside it does
29
+ (`.gemboss-pageedit-status` stays on a wrapper as a hook, `data-status`, with no look).
30
+
31
+ ## 0.23.0 (07/10/2026)
32
+
33
+ **The CSS comes in parts: a core every surface loads, and entries that come with the components that draw them.**
34
+ README "Load only what you use" has the table and the imports for Next and for plain HTML.
35
+
36
+ Added
37
+ - `@gemboss/ui/core.css` (reset + tokens, 1.0 KB gzip), `@gemboss/ui/components.css` (the kit, the field look,
38
+ kit-plain.css; 8.2 KB), `@gemboss/ui/shell.css` (the shell + app-shell.css; 5.5 KB): built into `parts/` from the
39
+ same sections as `gemboss-ui.css`, each opening with a header that names the gemboss commit (kept through minify).
40
+ The core is 14.7 KB against 21.7 for the bundle. Like `gemboss-ui.css`, the parts exist after `build-css.mjs` (the
41
+ tarball has them; in this repo, build first).
42
+ - Components import their own entry: `AuthLayout` & co. → `auth.css`; `AssistantHost`, the launcher → `assistant.css`;
43
+ `TopbarNav`, `SearchDialog`, `Avatar`, `Leaderboard`, `Podium`, `ThemeToggle` → `portal.css`; `ThemeToggle` /
44
+ `themeScript` (now in `theme-script.ts`, so a root layout with only the script does not take portal.css) → `theme-dark.css`; the collections components → `collections.css`; the editor components → `editor.css`.
45
+ A component a surface never imports takes its CSS out of the build with it (`sideEffects`).
46
+ - The editor kit's step 4 (#3850: `LayerList`, `SortableLayerList`, `LayerTree` in `layers.tsx`, `sortable.tsx`, `tree.tsx`)
47
+ imports `editor.css` like the rest of the editor; `editor-layers-plain.js` is minified and weighed like the other plain
48
+ scripts (1.8 KB gzip).
49
+ - `Reactions` (#3861, born an entry) imports `reactions.css` itself too; the parts gate found it unclassified on the
50
+ rebase, which is the gate doing its job.
51
+
52
+ Changed
53
+ - `@gemboss/ui/shell.css` is the built shell PART (was `packages/ui/src/shell.css`, which only the admin imported). The source is
54
+ `@gemboss/ui/shell-source.css`; the admin's `gemboss-shell.css` imports that, same file as before.
55
+ - `SearchDialog` moved to its own module (`search-dialog.tsx`, same export from `@gemboss/ui`), so a surface that only
56
+ opens a `Dialog` does not get `portal.css`.
57
+ - kit-plain.css → app-shell.css: `.ui-menu__item { display: flex … }` and the thread rail's 44px buttons at phone width
58
+ (`.gemboss-assistant-collapse`, `-newchat`). Both override a shell.css rule by coming after it; kit-plain.css sat
59
+ after shell.css in `gemboss-ui.css` but is in `components.css` now, which loads before it. Same rules, same weight.
60
+ - `gemboss-ui.css` holds the same parts as 0.19.0, in the parts' order (kit-plain.css now sits before the shell).
61
+ Computed style of every element of both catalogs at 1280 and 390, 0.19.0 bundle vs 0.23.0 bundle: 0 differences.
62
+
63
+ Measured on surfaces (tarball installed locally, nothing committed there), gzip of the package's CSS a page loads:
64
+ - content-radar (Next 16): `/v2` 38.5 KB on 0.19.0 → 32.3 with the core; `/login` 31.2 → 25.4; `/v2/accounts` 43.1 →
65
+ 36.9. Keeping `gemboss-ui.css` on 0.23.0 instead costs +1.9 KB (`/v2` 40.4): the launcher brings assistant.css twice.
66
+ - taskdesk (plain HTML): 22.2 KB (the minified bundle) → 15.0 with the core; every element's computed style the same and
67
+ screenshots byte-identical at 1280 and 390.
68
+ - GemFactory sign-in (plain HTML): 22.2 KB → 17.7 with the core + auth.css; screenshots byte-identical at 1280 and 390.
69
+
70
+ Upgrading
71
+ - A surface that bundles the package with esbuild needs a CSS loader (`loader: { ".css": "empty" }` if it links the CSS
72
+ itself): esbuild stops at a component's own `import "./auth.css"` without one. GemFactory's gbui.mjs has it already.
73
+
74
+ The compatibility bundle, and the road out
75
+ - `gemboss-ui.css` stays, minified, FROZEN at the 0.19.0 set: a surface that links it does not break, and a new
76
+ component never goes into it (`parts-gate.mjs`, CI).
77
+ - 1. Now: every surface moves to the core + entries on its next bump (Next and Vite: three imports in the root layout and
78
+ drop `gemboss-ui.css` in the same bump; plain HTML: link the core and the entries each page draws).
79
+ 2. A search across the fleet's repos for `gemboss-ui.css` shows who still loads it (06/10/2026: content-radar,
80
+ partner-radar, search-radar, talent-radar, GemCommunity admin, GemFeedback, gemwatcher, GemFactory, taskdesk).
81
+ 3. When nobody does, it is removed in its own release, with this note pointing at it. Not before.
82
+
83
+ Checks
84
+ - `packages/ui/scripts/parts-gate.mjs` (CI, with `--selftest`): the core and the bundle frozen; every stylesheet in `src/`
85
+ classified; every entry exported; a component that draws an entry's class imports it; the built bundle holds exactly
86
+ the frozen sections.
87
+ - `parts-cascade.test.ts` (`test:ui-browser`): the catalog with the parts (the core first, then the entries in order and
88
+ reversed, collections.css and editor.css included) paints what the bundle paints, element for element, at 1280 and
89
+ 390; two planted entries that only agree by order turn it red. The core loads first by contract (README "Order"):
90
+ an entry before it loses its modifiers (`.ui-empty--no-match`), measured.
91
+ - Ceilings for the three parts in `weight-budget.json`.
92
+
93
+ ## 0.22.0 (07/10/2026)
94
+
95
+ **The layers, the element outline and the drawer as components (editor kit, step 4 of 5).** Nothing changes on screen
96
+ or in behaviour in the admin: its page editor's Layers, Page settings and Edit content drawers and its element
97
+ inspector's Elements outline now render these, measured equal on a production build (below). README "Layers and
98
+ drawer".
99
+
100
+ Added
101
+ - `@gemboss/ui/editor`, three more modules behind the entry: `layers` (`LayerList`, `EditorDrawer`), `sortable`
102
+ ("use client": `SortableLayerList`) and `tree` (`LayerTree`).
103
+ - `LayerList`: the page's sections as rows (34px, 50 with a subtitle); select (`aria-current`), the caller's actions,
104
+ an empty line; off until asked: an eye to hide / show (`onToggleHidden`, `aria-pressed`), indent per `depth`.
105
+ - `SortableLayerList`: the same rows, reordered by drag (a grip, the dragged row faded, a drop line) and Alt+↑ / Alt+↓
106
+ (the focus kept on the moved row, also when `onMove` answers after a server write); the drag is kept by id. Its own
107
+ module, so the admin's Layers (no reordering) do not carry it. For GemFactory's Builder (step 5).
108
+ - `LayerTree`: an indented outline, `role="tree"` of `treeitem`s with level, position and selection.
109
+ - `EditorDrawer`: backdrop + a side panel (`right`: `.gemboss-settings`, `left`: `.gemboss-layers`), head with ✕,
110
+ `before`, body, `footer`.
111
+ - `editor-layers-plain.js` (its own file and its own weight ceiling, 2,315 B gzip; editor-plain.js is unchanged):
112
+ `gembossUI.initEditorLayers()`: select, hide, drag and Alt+↑/↓ (the row moves in the document),
113
+ outline select / clear, drawers opened by `data-gb-drawer` and closed by ✕ and the backdrop; an event for each.
114
+ - editor.css: the optional pieces (`.gemboss-layer-grip`, `.is-dragging`, `[data-drop]`, `.is-hidden`,
115
+ `.gemboss-layer-eye`) and `[hidden]` for a drawer a plain page keeps in the document.
116
+ - `catalog/editor.html`: Layers (drag, hide, select, indent), Page settings and the outline, running, at 1280 and 390.
117
+
118
+ Measured in the admin (two production builds, before = main f23a60729 with the #3848 fixes (0.20.1) and after, pinned SENTRY_RELEASE /
119
+ GIT_COMMIT_SHA / GEMBOSS_BUILD_STAMP, served by `next start` on the same local data; every write the harness could cause
120
+ was held)
121
+ - DOM and computed style (+ ::before / ::after, box, control values) of every element, and :hover / :focus-visible /
122
+ :active forced on every control of the editor's columns and drawers, for 13 states (page editor: Layers open, a layer
123
+ selected, focus on a row's actions, closed with ✕, closed from the backdrop, ↓ pressed with the write held; Edit
124
+ content drawer; Page settings drawer, typing in it; theme editor inspector, Elements: the outline, a node picked, "Add
125
+ inside" open, a node picked and cleared) at 320, 390, 768 (touch), 1280, 1366, 1920: 78 captures, 22,888 element
126
+ renders, 10,620 forced-state checks. 0 differences in computed style or box. In markup, the intended ones only (720):
127
+ `data-id` on each layer row and outline node, `type="button"` on the buttons the components draw (no form around
128
+ them), `aria-current="true"` on the selected layer's button. Not the change: inline styles serialised by the server or
129
+ by the client (computed equal), the logo's per-port URL, the canvas's loading spinner (four states re-captured with
130
+ the canvas settled: equal), and the forced states of the chat column's work-mode select, which differ between two
131
+ captures of the SAME base build too. Negative controls (one declaration laid over the after build: the layer button's
132
+ padding, the outline name's, the drawer head's) are reported in 3 of 3 states.
133
+ - Re-checked after rebasing onto main with 0.20.0 (#3846) and 0.20.1 (#3848): the 13 states at 1280 and 390, 26 captures,
134
+ 7,612 element renders, 3,540 forced-state checks: again 0 differences in computed style or box, markup as above.
135
+ - First load (brotli), against main with 0.20.1: the shell 239,674 → 239,655 B (−19); page editor +693 (LayerList and
136
+ EditorDrawer; SortableLayerList is not in it), campaign editor +327, theme editor +314, product studio +198, section
137
+ editor +125, plugin editor −12. Package weight gate: editor.css 8.1 / 8.4 KB gzip, editor-plain.js unchanged,
138
+ editor-layers-plain.js 1.8 / 2.3 KB.
139
+ - Behaviour the admin does not use (drag, Alt+↑/↓, the eye, indent) and the plain-HTML side are asserted in Chromium:
140
+ editor-layers-browser.test.tsx (React and `initEditorLayers()`, with negative controls).
141
+
142
+ Known, kept as it was (this step moves markup, not behaviour)
143
+ - The admin's Layers do not drag and have no eye: they move with ↑ / ↓ buttons, which also work on touch (HTML drag and
144
+ drop does not start from a touch). Using `onMove` there is a change of its own.
145
+ - The drawers have no Escape, and do not move the focus into themselves or back to their opener.
146
+ - `DesignControls` (the inspector's Design tab body) is not moved: moving it changes how it looks.
147
+ - The theme editor's "Sections on this page" list (ThemeSectionList) keeps its own bordered rows (inline styles); drawn
148
+ as a LayerList it would look different.
149
+
150
+ ## 0.21.0 (07/10/2026)
151
+
152
+ **Reactions: one emoji set and one chip for the fleet.** GemCare's team chat reactions, moved into the kit so GemFeedback
153
+ (requests, comments) and GemCare draw and accept the same thing (Chris 06/10/2026). README "Reactions".
154
+
155
+ Added
156
+ - `@gemboss/ui/reactions-data`: `REACTION_GROUPS` (137 emoji, GemCare's set unchanged), `REACTION_EMOJI`,
157
+ `isReactionEmoji`, `searchReactions`, `reactionLabel`, `toggleReaction`, `QUICK_REACTIONS`, type `Reaction`.
158
+ - `Reactions` and `ReactionPicker` (client components), `reactions.css` (its own entry `./reactions.css`, not inside `gemboss-ui.css`: the shared sheet stays as it was).
159
+ - Icon `reaction` (a face with a plus).
160
+ - Catalog section "Reactions".
161
+ - `packages/ui/weight-budget.json`: a ceiling for `packages/ui/src/reactions.css`; `gemboss-ui.css` unchanged.
162
+
163
+ ## 0.20.1 (07/10/2026)
164
+
165
+ **Two faults the admin's settings panel had before step 3, fixed.** Nothing else changes on screen. (The third listed
166
+ under 0.17.0, Show / Hide and On / Off rows without `aria-pressed`, went with 0.20.0, #3846.)
167
+
168
+ Fixed
169
+ - The inspector's groups that arrive folded (the shared frame's: Motion, …) opened on the SECOND click: the admin's
170
+ toggle flipped the stored value, and a flip of "unset" is "closed", where the group already was. It now stores what
171
+ the click asks for (`InspectorGroup`'s `onOpenChange(next)`). Reproduced on a production build first (theme editor,
172
+ Hero banner, Motion: `aria-expanded` false after one click, true after two), the same steps open it in one click after.
173
+ - The image thumbnail's `url()` is quoted (`cssUrl`, new export, plain TypeScript, also `./editor/css-url`), in
174
+ `ImageControl` and in the admin's library grid: unquoted, a URL holding ")" or a space ended the token, the
175
+ declaration was dropped and the thumbnail stayed empty. `"` and `\` get a backslash and a control character a CSS hex
176
+ escape; `initEditorPanel()` used JSON.stringify, whose `\n` CSS reads as the letter n, and now writes the same as
177
+ `cssUrl` (`gembossUI.cssUrl`).
178
+
179
+ Known, kept as it was
180
+ - The inspector remembers open / closed per group TITLE, so two groups with the same title (the Design tab's
181
+ Typography and an element frame's Typography) share it.
182
+
183
+ ## 0.20.0 (07/10/2026)
184
+
185
+ **One switch, one component.** The admin drew "pick one of these" nine ways: a dark 44px pill (the Studio's
186
+ Funnel · Canvas · Page and the funnel lens copied from it), a dark joined group (the canvas's device, the campaign
187
+ editor's mode), a grey track with a raised white tab (the inspector's `.gemboss-ei-seg`, which 0.17.0 moved here as
188
+ `Seg`), a brand-fill joined group (post-purchase, twice), separate tinted boxes (`DeviceToggle`), and this package's own
189
+ `.ui-seg`, which no editor used. Two of them sat side by side in one bar (Chris 06/10: "switch canvas mode page mode đó
190
+ cũng dùng khác nhau là không được"). Now there is one look, `.ui-seg`, and one component.
191
+
192
+ Added
193
+ - `Segmented` / `IconSegmented` (`@gemboss/ui/segmented`, also from `@gemboss/ui`): moved from the admin
194
+ (`components/ui/Segmented.tsx` re-exports it). Options take a ReactNode label, `title`, `ariaLabel`, `disabled`; the group
195
+ takes `fill`, a hook `className` and `style`; each button carries `data-value`. Its own plain module: a surface that draws
196
+ a switch does not load the shell.
197
+ - `.ui-seg--fill` (the row's width, segments equal), `.ui-seg-n` (a count inside a segment, read by eye; the segment's
198
+ `aria-label` says the sentence), a disabled segment (.45, `not-allowed`).
199
+
200
+ Changed
201
+ - `Seg` (editor/controls) is `Segmented` stretched to its row. `gemboss-ei-seg` stays on the group as the hook
202
+ `editor-plain.js` finds; the pressed button is `.active` (the driver toggles `.active` and, for markup written before,
203
+ `.on`).
204
+ - `DeviceToggle` is a `.ui-seg` (28px under a mouse, 44px under a finger). `.gemboss-pv-devices` / `.gemboss-pv-device`
205
+ stay as hooks; inside the switch the segment's size wins over the icon-button sizes those classes carry elsewhere
206
+ (undo, redo, layers, ⋯). Each device button has an `aria-label`.
207
+ - `.is-text` segments lay their children in a row: a label and its count, or two icons, stacked in the grid.
208
+ - A disabled segment keeps the picked one's fill (a switch that is saving still says which option is on; review #3846).
209
+ - `.ui-seg--fill` outranks kit-plain.css's `.is-text { flex: none }` (gemboss-ui.css carries it after ui.css): at equal
210
+ weight the segments kept their own widths outside the admin (review #3846).
211
+
212
+ Fixed
213
+ - `.ui-seg svg` filled a stroke icon solid (`fill: currentColor` over `fill="none"`): the monitor and phone glyphs drew
214
+ as blocks. A `fill="none"` svg keeps its outline.
215
+
216
+ - A folded editor column (`.is-chat-collapsed`) is `visibility: hidden`: at width 0 its fields stayed in the Tab order.
217
+ - `EditorChatToggle` takes a `name` ("Hide <name>"; default "chat panel"): a column that is not a chat says so.
218
+
219
+ Removed
220
+ - The `.gemboss-ei-seg` look from `editor.css`.
221
+ - `.gemboss-pageedit-status` and `.gemboss-pageedit-kind` (the page editor draws Polaris' Badge, as the funnel beside it does).
222
+
223
+ ## 0.19.0 (06/10/2026)
224
+
225
+ **The published CSS and plain scripts are minified, and each file has a wire-weight ceiling.** README "Minified on
226
+ publish, weighed per file".
227
+
228
+ Changed
229
+ - The tarball carries every stylesheet and `kit-plain.js` / `editor-plain.js` minified (esbuild, comments out; the
230
+ sources keep them). `gemboss-ui.css` keeps its generated header. gzip, as measured on this change:
231
+ `gemboss-ui.css` 78.1 → 21.9 KB, `kit-plain.js` 17.8 → 8.5 KB, `editor.css` 25.8 → 7.9 KB, `collections.css`
232
+ 12.4 → 4.6 KB, `editor-plain.js` 7.5 → 3.6 KB. A surface that serves the files from `node_modules` as they are
233
+ (GemFactory, taskdesk, gemcare, gempress) sends the smaller file on its next bump with no change of its own; Next and
234
+ Vite surfaces minified already and see no difference.
235
+ - Release: `npm run stage` then `npm publish .pack --auth-type=web`. `npm pack` / `npm publish` straight in
236
+ `packages/ui` are refused (they would ship the unminified sources).
237
+ - Computed style is unchanged: every element and `::before` / `::after` of `catalog/index.html` and
238
+ `catalog/editor.html`, all frames, at 1280 and 390, compared source vs minified copy. The only text that differs is in
239
+ custom properties (`0.18` → `.18`, `'Inter'` → `"Inter"`), whose computed value is their token text; every property
240
+ that reads them computes the same.
241
+
242
+ Checks
243
+ - `packages/ui/scripts/weight-gate.mjs` (CI: `--ci`) replaces the export test's raw 90 KB cap: gzip of each minified file against
244
+ `weight-budget.json` (measured + 512 B); red over the ceiling, more than 1 KB under it, with comments left, or without
245
+ a ceiling. Negative controls run with it in CI. `fleet.mjs` compares main and npm minified on both sides.
246
+ - `esbuild` is a pinned root devDependency (0.21.5, the version already installed): the minifier decides the bytes.
247
+
248
+ ## 0.18.0 (06/10/2026)
249
+
250
+ **Five collections gaps surface migrations found** (GemFactory Members #197 and its board, content-radar ideas,
251
+ GemCommunity admin Content #270). README "Collections" has the rules.
252
+
253
+ Fixed
254
+ - A sort button (IndexTable / DataTable header, `button|a.ui-table__sort`) is 44 WIDE under a finger and at phone width,
255
+ not only 44 tall: "ID" and "Views" were ~30 wide on an iPad in landscape (GemCommunity admin Content #270). Under a
256
+ mouse above 768 nothing changes. The sort chips below 768 already had it (`ui-chip`).
257
+ - An open ⋯ menu (`Menu`, `details.ui-menu`) is no longer cut by a scrolling frame. Its list floats in the top layer
258
+ (`popover="manual"`, `position: fixed` against the button): below it, or above when the room below is short and the
259
+ room above is larger; 8px from the viewport's edges; scrolls inside when taller than the room; closes when a scroll
260
+ moves its button (the page, or a frame around it; not a scroll inside the list or of something elsewhere) or the
261
+ window resizes. One page-wide pair of listeners, however many menus; a menu already open when the script loads is
262
+ floated, not hidden. Tab order, Escape and row clicks are unchanged (the list is still the details' child).
263
+ React `Menu` (new `menu-layer.ts`, floats in its own summary click, sets nothing page-wide) and kit-plain.js
264
+ (`initMenuLayer()`, runs by itself) carry the same code. No popover
265
+ API or no script: the 0.17 absolute list. GemFactory Members at 820 / 900 / 1280 showed half of the last rows' menus
266
+ and asked for a scroll inside the table.
267
+
268
+ Added
269
+ - `MediaCard ratio="1/1"` (`data-ratio="1/1"`) beside 16:10 and 4:3, for square pictures (content-radar's moodboard).
270
+ No free ratio; an unknown value draws 16:10.
271
+ - Board server paging: `BoardColumn.hasMore` (default: true when `moreHref` is set), `moreHref`, `loading`,
272
+ `Board onLoadMore(columnId)`, `linkAs`, `moreLinkProps`, `fetchMore` (plain HTML only), `boardShown(page)` (12, then 24
273
+ a page; a missing or non-numeric page is page 1). A server-paged column renders the cards it was given and
274
+ ONE `ui-more` "Show N more" (`a[data-gb-more]`), not the nested `<details>` that need every card in the HTML.
275
+ With `fetchMore` (`a[data-gb-more]`) kit-plain.js fetches the link and appends the cards not yet on the board (by
276
+ `data-id`), takes the next link, the count and the URL, announces and focuses the first new card; "Couldn't load." +
277
+ Try again on failure. A paged column whose loaded cards all moved away keeps its list and its link.
278
+ - Board moves, optional: `Board onMove(cardId, toColumn, toIndex)`; plain HTML `.ui-board[data-gb-move]` with
279
+ `li[data-id][tabindex="0"][draggable="true"]` and the `gb:move` event (cancelable; not cancelled, the `li` moves).
280
+ Drag, or Space / arrows / Space / Escape on a focused card, every step said in `[data-gb-live]`; the held card is
281
+ `data-gb-lifted="key|drag"` (no `aria-pressed`: a list item does not take it). `board-move.ts`
282
+ (React, through a callback ref: `collections.tsx` stays a plain module) and kit-plain.js carry the same code; a page
283
+ with both drives a React board once.
284
+ - `BoardColumn.items` (`{ id, card, name? }`): cards with ids, needed by moves and server paging. `cards` still works.
285
+ `Board id` is now the root's DOM id too (kit-plain.js reads a fetched page's column from the board with the same id).
286
+ - Review #3845 (before merge): a drag that left the board and came back to the same slot was cancelled; kit-plain.js
287
+ would have fetched into a board React owns; a menu closed on any scroll on the page. Each has a test that fails on the
288
+ first commit of this PR.
289
+ - collections-gate counts boards (`seen.boards`): a page that is only a board (content-radar ideas) read "NOT MEASURED".
290
+ - Catalog: a movable board (live with kit-plain.js), a server-paged board, a 1:1 card grid, a table to open the last
291
+ row's ⋯ in.
292
+
293
+ Checks
294
+ - `collections-board.test.ts` (new, in `test:ui-browser`): the last row's menu whole in the viewport at 820 / 900 /
295
+ 1280 / 390, plain HTML and React, above its button near the bottom of the screen, keyboard (Enter, Tab, Escape back
296
+ to ⋯), closes on page scroll and resize but not on its own scroll, an item click does not open the row; negative:
297
+ without the popover API the same menus are cut (plain and React). Ratios 1.6 / 1.33 / 1 / fallback 1.6; negative:
298
+ without the 1/1 rule the square card is 16:10. A 2,000-card column server-rendered = 12 `li` + one link (the
299
+ client-paged board: 2,000 `li`); kit-plain fetch in place to the last page (36 → 60 → 80, no reload, URL, count,
300
+ focus, live text), failure + Try again, no-script link navigates, React `onLoadMore`. Moves: keyboard and drag on
301
+ plain HTML (`gb:move` detail, DOM, counts, focus), Escape and a drop in place report nothing, a cancelled `gb:move`
302
+ leaves the DOM to the page, React `onMove` (keyboard + drag), React + kit-plain on one page fire once; negative: a
303
+ board without `onMove` has no tab stops or drag and moves nothing. collections-gate clean on all three.
304
+
305
+ ## 0.17.0 (06/10/2026)
306
+
307
+ **The settings panel and its controls as components (editor kit, step 3 of 5).** Nothing changes on screen or in
308
+ behaviour in the admin: its Configure column, element inspector and schema form now render these, measured equal on a
309
+ production build (below). README "Settings panel (0.17.0)".
310
+
311
+ Added
312
+ - `@gemboss/ui/editor`, two more modules behind the entry (`panel`, `controls`, both `"use client"`):
313
+ `PanelTabs` (`column`: Configure · Chat; `inspector`: Content · Design · …; roving ←/→ by default, `roving={false}`
314
+ for a row of plain Tab stops; `controls` per tab for `aria-controls`), `PanelHeader` + `PanelAction`, `InspectorGroup` (collapsible, controlled or not;
315
+ `collapsible={false}` is the schema form's static group), `Field` (label, `labelValue`, hint), `Seg`, `ColorControl`,
316
+ `RangeControl`, `EdgeBox`, `ImageControl` + `ChipButton`, and `swatchState` (moved from the admin, which re-exports it;
317
+ also `@gemboss/ui/editor/swatch`, plain TypeScript).
318
+ - `editor-plain.js`: `gembossUI.initEditorPanel()`: tabs (+ arrows on a roving row, `aria-controls` panels), group
319
+ fold, Seg, colour (swatch and box kept in step, swatch off for a non-hex), range (label value + `data-suffix`), edges
320
+ (all four on blur), image (thumbnail, `data-gb-image-clear`); an event for each.
321
+ - editor.css: `.gemboss-ei-gh-note`, the group header's aside (the declarations the inspector had inline).
322
+ - `catalog/editor.html`: the panel with every control, rendered from the components and running, at 1280 and 390.
323
+
324
+ Measured in the admin (two production builds, before = main 23644fabb and after, pinned SENTRY_RELEASE / GIT_COMMIT_SHA /
325
+ GEMBOSS_BUILD_STAMP, served by `next start` on the same local data; every write the harness could cause was held)
326
+ - DOM and computed style (+ ::before / ::after, box, control values) of every element, and :hover / :focus-visible /
327
+ :active forced on every control of the panel, for 19 states (theme editor inspector: Design, groups folded, Content,
328
+ Advanced, ←/→ on the tabs, typing in an edge box, mobile breakpoint, busy with every control disabled; Global styles:
329
+ default, slider dragged, colour typed, swatch clicked; page editor Configure, edge committed with Enter; section editor:
330
+ hero, image slot, slider, slider moved with the keys, image URL committed) at 320, 390, 768 (touch), 1280, 1366,
331
+ 1920: 114 captures, 41,506 element renders, 18,018 forced-state checks. 0 differences in computed style or box. In
332
+ markup, the intended ones: the inspector's tabs gain `type="button"` (no form around them), the group note's inline
333
+ style became `.gemboss-ei-gh-note` (computed equal), and (after review) every tab and Seg button carries
334
+ `data-value`, which no stylesheet selects; re-captured at 1280 and 390 after it, the same. Two more that are not the change: the brand mark's
335
+ `url()` resolves to each server's own port, and one forced `:active` in Global styles differs between two captures of
336
+ the SAME build too. A negative control (one declaration laid over the after build) is reported in 10 of 19 states.
337
+ - Server-rendered markup of the admin's controls before and after with the same props (FieldControl of every kind,
338
+ FieldRenderer schemas, the inspector's Seg, EdgeBox, ImageField, ElementPropField; disabled, values, hints): 330
339
+ renders: 282 byte-identical, 20 identical but for attribute order (the element edge box), 28 identical but for the
340
+ added `data-value`, 0 different. A changed aria-label laid over one render is caught.
341
+ - First load (brotli): the shell 239,475 → 239,439 B (−36); theme editor +659, page editor +875, section editor +991,
342
+ campaign editor +653, product studio +644; plugin editor −32. Budget gate: shell 233.8 / 237 KB.
343
+
344
+ Known, kept as it was (this step moves markup, not behaviour)
345
+ - The inspector's groups that arrive folded (the shared frame's) stay folded on the first click and open on the second:
346
+ the toggle flips the stored value from "unset" to "closed".
347
+ - The schema form's Show / Hide rows and the inspector's Visibility rows have no `aria-pressed`, so they are not `Seg`.
348
+ - The image thumbnail's `url(…)` is unquoted in the React control, as in the admin (a URL with ")" or a space shows no
349
+ thumbnail); `initEditorPanel()` quotes it.
350
+ - `InspectorGroup collapsible={false}` (schema form) and the inspector's group differ a little (letter-spacing .08em vs
351
+ .1em, divider above vs below), as in the admin.
352
+
353
+ ## 0.16.0 (06/10/2026)
354
+ **Four collections fixes a surface migration asked for**, each patched by hand on a surface until now (partner-radar
355
+ `SortNav` + `cells.js`, search-radar `SortNav`, talent-radar's bar apart from its list). README "Collections" has the
356
+ rules.
357
+
358
+ Fixed
359
+ - Sort links are `linkAs` (IndexTable header and the sort chips below 768; DataTable header), the element the row
360
+ titles already used. 0.13.0 drew them as plain `<a>`, so on a Next surface every sort reloaded the whole page (frame,
361
+ open filters, scroll): partner-radar /apps and search-radar /keywords wrapped the table in a `SortNav` that caught the
362
+ click. New `sortLinkProps` (plain data) go to the sort links only: `{ scroll: false }` for a Next `Link` keeps the
363
+ scroll position, while opening a row still scrolls.
364
+ - Data table cells sit at the TOP of their row (`vertical-align: top`), as Polaris DataTable (its `verticalAlign`
365
+ defaults to "top", Polaris 13.9.5 Cell.js). A 24px cell link is pulled up by `calc((1lh - 24px) / 2)` so its text is on
366
+ the line of the cells beside it (top-aligned, the 24px box put it 2-4px low). The index table keeps `middle`.
367
+ In collections.css, not ui.css: the admin loads ui.css on every screen and draws no ui-table.
368
+ - The row header (`th.ui-table__rowhead`) wraps (Polaris `firstColumn`), with Polaris's first-column width as a floor
369
+ (9.0625rem) so a narrow frame scrolls instead of squeezing it to a word a line; `truncate` / `ui-clamp-2` cut it when
370
+ it must stay short. A link alone on its line is 24px in the row header and in a list inside a cell (`li > a`), as
371
+ `td > a` was. 0.13.0 kept ui.css's `nowrap` and matched `td > a` only: partner-radar wrapped 17 cells on five pages in a
372
+ `CELL()` box and set 24px by hand on 12 links (`cells.js`).
373
+ - `ResourceItem selected` no longer puts `aria-selected` on the `li` (a listitem does not take it; jsx-a11y
374
+ `role-supports-aria-props`): with a checkbox the box says it, without one (an item open beside the list) it is
375
+ `aria-current="true"`. kit-plain.js sets aria-selected on table rows only.
376
+ - Escape clears a selection unless a menu or dialog in the table / list is OPEN and shown: a closed `<dialog>` or a
377
+ hidden `role=menu` kept in a row used to swallow every Escape (IndexTable, kit-plain.js, and the new list; review
378
+ #3831).
379
+
380
+ Added
381
+ - `SelectableResourceList` (client module `resource-list-select.tsx`): a resource list with selection and bulk actions,
382
+ the IndexTable's rules: a checkbox first in each item's media slot, Shift+click range (anchor by id), Escape clears
383
+ (an open ⋯ menu or dialog takes it first), the actions get the ids in the list's order, the whole row still opens
384
+ the item. The bar is the list's header from the start ("0 selected", actions disabled): a list has no header to cover,
385
+ and a bar that appears on the first check pushed talent-radar /pool's rows 74px (1280) / 194px (390) under the finger.
386
+ `ResourceList` stays plain (server). `ResourceItem.check` is the slot it fills; an item's `id` is the selection key
387
+ only, never the `li`'s DOM id. Loading keeps each row's box (`CollectionSkeleton check` for a list), so the media
388
+ does not move when rows arrive. The selection rules and the bar are one module (`selection.tsx`) that IndexTable now
389
+ uses too.
390
+ - `CollectionColumn.minWidth` (DataTable): the column's floor, for cells that wrap (Polaris `firstColumnMinWidth`, any
391
+ column); replaces the row header's floor, and `0` removes it for a row header that is always short (a date, an id).
392
+ - Plain HTML: `div.ui-resource > div.ui-bulkbar + ul.ui-list`, `label.ui-list__check > input[data-gb-select]`;
393
+ kit-plain.js drives a `.ui-resource` like a `.ui-index` (action buttons enabled from 1 instead of the bar shown;
394
+ `gb:selection` fires after, for an action with a rule of its own).
395
+ - collections-gate: `select-without-bulk` also reports a resource list with selection boxes (in `__media`, or
396
+ `data-gb-select`) and no bar; a checklist's done box or a switch is not a selection box.
397
+ - Catalog: a multi-line job table at the catalog width and at 390, a selectable list (two selected) and at 390.
398
+
399
+ Measured (gemlabs partner-radar on the local proof DB, `next dev`, published 0.13.0 vs the 0.16.0 tarball with
400
+ `SortNav` and `cells.js` deleted, `CELL(n)` floors moved to `minWidth: n`, `style={LINK24}` removed from 12 links; same
401
+ rows; 320 / 390 / 768 / 1280)
402
+ | Page | rows whose cells' first lines differ > 1.5px | links < 24 outside a sentence | gate | page overflow |
403
+ |---|---|---|---|---|
404
+ | /ops (24 rows) | 24 → 3 (the 3: a status pill, its text 4px into its own padding) | 0 → 0 | 0 → 0 | none |
405
+ | /sources (193) | 183 → 3 (same pill) at 320-768, 175 → 3 at 1280 | 0 → 0 | 0 → 0 | none |
406
+ | /feed (27) | 26 → 0 | 0 → 0 | 0 → 0 | none |
407
+ | /competitors (93) | 80 → 0 | 0 → 0 | 0 → 0 | none |
408
+ | /search?q=shop (60) | 60 / 53 → 0 | 0 → 0 | 0 → 0 | none |
409
+
410
+ Total row height: /search 7,318 → 5,448 at 320, /sources 18,426 → 15,440 at 320-768; /feed +2% and /competitors +4% at
411
+ 320 / 390 (a row header now wraps at its 145 floor instead of running one line wide). No table wider than its frame at
412
+ 1280. Density gate: same count of differences or fewer on every page (competitors 48 → 36 at 1280).
413
+ Sort: /apps (partner-radar) and /keywords (search-radar) without `SortNav`, a header link at 1280 and a chip at 390:
414
+ 0 document requests, the page's state kept, scroll 120 → 120. Negative: published 0.13.0 without `SortNav`: 1 document
415
+ request, state lost, scroll 120 → 0.
416
+
417
+ Checks
418
+ - `collections.test.ts`: sort links go through `linkAs` with `sortLinkProps` (header, chip, DataTable; the row title
419
+ keeps linkAs without them), negative: no `linkAs` = the browser navigates; first lines of a three-line row header, a
420
+ plain cell, a link cell and a number on one line, negatives with the top rule or the link rule cut out; the row header
421
+ wraps in a 560 frame, keeps its 145 floor at 390 (the frame scrolls), `minWidth` replaces it, a `truncate` column stays
422
+ one line, negative: the 0.15.0 nowrap overflows the frame and its link is a bare inline box; SelectableResourceList:
423
+ bar from the start, rows do not move on the first check, Shift range, ids to the action, select-all + Escape, a row
424
+ click opens the item through linkAs, Escape with a menu open keeps the selection, boxes 24 / 44 aligned with the
425
+ bar's, rows stay 64; kit-plain on `.ui-resource`; the gate on a list with and without its bar.
426
+ - Admin first load: the names the admin imports from `@gemboss/ui` bundle to the same bytes (7,967 minified, 2,685
427
+ gzip) before and after; ui.css, gemboss-ui.css and the admin's other stylesheets unchanged (this release touches
428
+ collections.css, the collections modules and kit-plain.js, none of which the admin loads).
429
+
430
+ ## 0.15.0 (06/10/2026)
431
+
432
+ **Resource list in a narrow frame, and a body + wrap for the compact list.** Two package bugs a surface migration
433
+ (partner-radar, gemlabs #895) found in the 0.13.0 collections. README "Collections" has the rules.
434
+
435
+ Fixed
436
+ - Resource list when the LIST's frame is 400 or less. 0.13.0 wrapped the flex row, so the body's max-content (a long
437
+ subtitle) pushed it under the media, and the aside and the actions took a line each: partner-radar /hot rows went
438
+ from 221 (its old table) to 284 at 390. Now a grid with one rule for every row: the body stays beside the media
439
+ with all the width right of it; the aside and the actions share one line under the body (aside left, actions
440
+ right); the media spans both lines; without media the body takes the whole row. The `flex: 1 1 0` patch tried on
441
+ partner-radar fixed /hot but made /ugc-operators taller (308 → 332 at 320), because the actions sat beside the
442
+ body and narrowed it; the grid does neither. A body that is only its title keeps its aside and actions on that line
443
+ (one band, never taller than 0.13.0). Gaps are margins, so a row without media needs no `:has()`. The row's direct
444
+ children are the four slots (a selection checkbox goes inside `__media`); the loading skeleton keeps its own row.
445
+ Frames wider than 400 are unchanged.
446
+
447
+ Added
448
+ - `CompactItem.body` (activity): the event's own text under the label and the detail, `.ui-list__content` (body
449
+ size, wraps, never cut; `<p>` margins reset).
450
+ - `CompactList wrap` (`ui-list--wrap`, either variant): the label and the detail wrap instead of ending in "…". For a
451
+ list whose label carries data a finger must read (the cut text's `title` is mouse-only).
452
+ Under a finger the wrapped row is padded 12 + 20 + 12, so a one-line label stays centred in its 44px row.
453
+ - `CompactItem.labelText`: the full label in `title` (left out under `wrap`, where nothing is cut). `CompactListProps`
454
+ is exported.
455
+ - Catalog: the two row shapes at 390, and a timeline with body + wrap at 390.
456
+
457
+ Measured (gemlabs partner-radar on a local proof DB, `next dev`, 0.13.0 vs the 0.15.0 tarball, same rows; median /
458
+ total of all rows, px)
459
+ | Page | 320 | 390 | 768 | 1280 |
460
+ |---|---|---|---|---|
461
+ | /hot (100 rows) | 284 → **191** (28,747 → 20,123) | 284 → **191** (28,003 → 19,187) | 143 = 143 | 107 = 107 |
462
+ | /ugc-operators (40 rows) | 308 → **267** (12,067 → 10,427) | 284 → **243** (11,243 → 9,603) | 199 = 199 | 159 = 159 |
463
+
464
+ Row by row: 0 rows taller at any width; every row lower at 320 and 390; identical at 768 and 1280. collections-gate:
465
+ 0 broken, page overflow 0, at every width. The four partner-radar timelines on `CompactList variant="activity" wrap`
466
+ with `body` instead of its own `.pr-tl-*` (profile with 4 events, profile with 15 jobs, an organization): every item
467
+ the same height as before at all four widths, 0 labels or details cut, 0 `.pr-tl-*` left.
468
+
469
+ Checks
470
+ - `collections.test.ts`: geometry of both row shapes (avatar + long subtitle + score + 44px link; no media + many meta
471
+ lines + badges + score + button + ⋯) at 320 / 390 / 768: body beside the media and as wide as the room, aside and
472
+ actions on one line under the body, the row exactly as tall as that layout asks, nothing sideways; no taller than
473
+ 0.13.0 at any width and lower at 390; a title-only row stays one band; the skeleton keeps its block beside the bars.
474
+ Negative controls: the 0.13.0 rule and the `flex: 1 1 0` patch, planted in the
475
+ same CSS, break the checks. Compact list: with `wrap` nothing is cut at 320 / 390 / 768, the body follows the detail
476
+ at 13px, the time stays top right; without `wrap` the same label is one line and cut (control).
477
+ - Admin first load: the names the admin imports from `@gemboss/ui` bundle to the same bytes (7,967 minified,
478
+ 2,685 gzip) before and after; `gemboss-ui.css`, `ui.css` and the other admin stylesheets are unchanged (this release
479
+ touches `collections.css` and `collections.tsx` only, which the admin does not load).
480
+
481
+ ## 0.14.0 (06/10/2026)
482
+
483
+ **The editor frame as components (editor kit, step 2 of 5).** Nothing changes on screen in the admin: its theme, page and
484
+ section editors now render these, measured equal on a production build (below). README "Editor frame (0.14.0)".
485
+
486
+ Added
487
+ - `@gemboss/ui/editor` (its own entry; two modules, `frame` and `panes`, see README): `EditorFrame` (top · body with left column, seam and preview
488
+ pane · children; `split`, `right` docked over the preview, `mobilePane`, `embedded`), `EditorPaneTabs`, `EditorTopbar`
489
+ (`theme` / `page` / `guest`; back, chat toggle, title, three clusters, status line), `EditorBack`, `EditorChatToggle`,
490
+ `EditorSaveState`, and `DeviceToggle` / `MonitorGlyph` / `PhoneGlyph` (moved from the admin unchanged).
491
+ - `editor-plain.js`: `gembossUI.initEditorFrame()` for plain HTML (seam drag + keys clamped to the box, chat toggle,
492
+ device, phone pane, ⋯ / Publish ▾ menus; an event for each). Load after `kit-plain.js`.
493
+ - `catalog/editor.html`: the frame rendered from the components and running; `catalog/index.html` links it.
494
+ - editor.css: `.gemboss-editor-dock` (the right column; the declarations the theme editor's Global styles dock had
495
+ inline), its pane as containing block, and the frame's base geometry as zero-specificity `:where(…)` rules (left
496
+ column sized by `--gemboss-chat-w`, stacked at 1000px and below; preview takes the rest) so a page with only the
497
+ package lays out. In the admin every one of those properties is set more specifically, so none applies there.
498
+
499
+ Measured in the admin (two production builds of e3f3f92ec, before and after, pinned SENTRY_RELEASE / GIT_COMMIT_SHA /
500
+ GEMBOSS_BUILD_STAMP, served by `next start`)
501
+ - DOM and computed style (+ ::before / ::after and box) of every element on the page, and :hover / :focus-visible /
502
+ :active forced on every frame control, for 122 states of the three editors (default, Configure, chat collapsed,
503
+ device mobile / tablet, ⋯ open, Publish ▾ open, page picker open, status line, Global styles, phone Preview pane,
504
+ Layers, page settings, section insert menu) at 320, 390, 768 (touch), 1280, 1366, 1920: 27,606 element renders,
505
+ 4,080 forced-state checks, 0 differences in computed style or box. The one markup change is the intended one (the
506
+ Global styles dock's inline style became its class, 6 states, computed style equal). Two states differed by the
507
+ canvas still loading at the moment of capture, one in each direction; re-captured, 0. A negative control (one
508
+ declaration laid over the after build) is reported (52 differences on one screen).
509
+ - The theme top bar and the try door's bar rendered on the server before and after with the same props (662
510
+ combinations of save state, chat toggle, status line, tools, device): byte-identical markup.
511
+ - First load: the shell's raw bytes are unchanged (the webpack runtime 7,981 to 7,979 bytes; brotli +1 B from four
512
+ lazy-chunk hashes). Editor routes: theme +684 B, page +681 B, section +625 B, A/B campaign editor +680 B brotli
513
+ (CSS +106 B of each, the dock and the `:where` geometry); product studio +107 B (the same CSS).
514
+
5
515
  ## 0.13.0 (06/10/2026)
6
516
 
7
517
  **Collections: six patterns for tables, lists, grids and boards** (Chris 05/10/2026), each with its states. The CSS is
@@ -46,7 +556,8 @@ Changed (under a mouse, at 768px and wider)
46
556
  - `ui-btn`: 28px tall (was 32), padding 0 12 (was 16), 12px (was 13). New `--ui-btn-h: 28px`; `--ui-control-h` stays 32
47
557
  for `gemboss-field` and `ui-select` (Polaris TextField / Select). `ui-btn--icon` is 28 wide. The popup's own 28px rule
48
558
  is gone (every button is 28). **A surface that raised `--ui-control-h` to grow its buttons must now set `--ui-btn-h`
49
- as well** (gemcare `public/assets/inbox.css` sets `--ui-control-h: 44px` for that reason).
559
+ as well**. (This line used to name gemcare's `inbox.css` as such a surface: that rule was removed on 03/10/2026, and
560
+ gemcare takes 0.12.0 with no override, 28 under a mouse and 44 under a finger, measured on its inbox 06/10/2026, gemcare #312.)
50
561
  - ui.css (what the admin loads): buttons and segments are 44 at phone width with a mouse too (`max-width: 47.9975em`),
51
562
  as they already were under a finger; they were 32 there.
52
563
  - `ui-seg`: 28px group, labels 12px/550. `ui-tabs`: 28px (was 44 under every pointer), 12px/550 selected or not, 8px