@gemboss/ui 0.48.0 → 0.50.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 (64) hide show
  1. package/CHANGELOG.md +260 -0
  2. package/README.md +127 -11
  3. package/catalog/editor.html +1 -1
  4. package/catalog/index.html +10 -10
  5. package/gemboss-ui.css +3 -3
  6. package/package.json +22 -2
  7. package/parts/components.css +3 -3
  8. package/parts/core.css +1 -1
  9. package/parts/shell.css +2 -2
  10. package/src/app-shell.css +1 -1
  11. package/src/assistant/chat.tsx +50 -6
  12. package/src/assistant/host.tsx +19 -7
  13. package/src/assistant/index.ts +1 -1
  14. package/src/assistant/sessions.tsx +2 -1
  15. package/src/assistant-chat.css +1 -1
  16. package/src/assistant.css +1 -1
  17. package/src/auth-signin-plain.js +1 -0
  18. package/src/auth-sso-plain.js +1 -1
  19. package/src/auth.tsx +105 -8
  20. package/src/briefing-card.css +1 -1
  21. package/src/collections.css +1 -1
  22. package/src/create-shell.css +1 -0
  23. package/src/create-shell.tsx +221 -0
  24. package/src/editor-insert.css +1 -0
  25. package/src/editor.css +1 -1
  26. package/src/empty-stage.css +1 -0
  27. package/src/empty-stage.tsx +31 -0
  28. package/src/icons.tsx +2 -0
  29. package/src/index-table.tsx +7 -4
  30. package/src/index.ts +16 -2
  31. package/src/kit-plain.css +1 -1
  32. package/src/kit-plain.js +1 -1
  33. package/src/language-switcher.tsx +10 -41
  34. package/src/list-page.css +1 -0
  35. package/src/list-page.tsx +239 -0
  36. package/src/menu-extras.css +1 -0
  37. package/src/menu-keys.ts +199 -0
  38. package/src/menu-layer.ts +2 -1
  39. package/src/menu-plain.js +1 -0
  40. package/src/menu.css +1 -0
  41. package/src/menu.tsx +83 -35
  42. package/src/pager-extras.css +1 -0
  43. package/src/pager.css +1 -0
  44. package/src/pager.tsx +88 -0
  45. package/src/portal.css +1 -1
  46. package/src/segmented.tsx +12 -2
  47. package/src/selection.tsx +3 -1
  48. package/src/session-rail-plain.js +1 -1
  49. package/src/settings-dialog.tsx +29 -2
  50. package/src/shell.css +1 -1
  51. package/src/stage-path.css +1 -0
  52. package/src/stage-path.tsx +89 -0
  53. package/src/stage.css +1 -0
  54. package/src/stage.tsx +52 -0
  55. package/src/starting-point.css +1 -0
  56. package/src/starting-point.tsx +93 -0
  57. package/src/status-list.css +1 -0
  58. package/src/status-list.tsx +65 -0
  59. package/src/status-matrix.css +1 -0
  60. package/src/status-matrix.tsx +131 -0
  61. package/src/team-chat.css +1 -1
  62. package/src/translation-row.css +1 -0
  63. package/src/translation-row.tsx +113 -0
  64. package/src/ui.css +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,266 @@ Versions up to 0.9.0 are described in the README, each in the section that intro
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.50.0 (24/10/2026)
8
+
9
+ **Continue with GemBoss says which name the page is on** (24/10/2026). Every internal tool answers to a flat name and an .ops
10
+ name; GemBoss sent the token to the one registered URL, so a person on the other name signed in there and the name they used
11
+ kept asking (review of gemlabs #1163).
12
+
13
+ Changed
14
+ - `gemBossSignInUrl(service, next, issuer, host?)` adds `&host=`; `AuthContinueWithGemBoss` passes the browser's
15
+ `location.host` once mounted (the server-rendered first paint links without it), and auth-sso-plain.js always does.
16
+ GemBoss only CHOOSES among the tool's registered URLs with it (`FLEET_SSO_SERVICES[...].alias`, apps/admin).
17
+ - `AuthSignIn` renders `method="post"` always and takes `action`: on a server-rendered page a press before React loaded was a
18
+ native GET, with the password in the URL, history and server logs (measured 24/10/2026: 8 Next sign-in pages in gemlabs).
19
+ - `?sso=not_member` now reads "Your GemBoss account is not a member here. Ask an admin to add your email." It said "Use the
20
+ password", but since 23/10/2026 both doors check the same member list, so the password door refuses that person too.
21
+
22
+ **StartingPoint: a chat door where the person asks** (24/10/2026). The chat door's picture always drew Gem asking, the person
23
+ answering and a "Drafting" tile, so a door that asks Gem a question (the empty Funnels page's "Which funnel should I build?")
24
+ read as Gem drafting pages. `chat.from: "me"` puts the person's question first and Gem's answer under it; `chat.step` names the
25
+ tile. Both optional: a door that passes neither draws as before.
26
+
27
+ **AuthSignIn: the whole sign-in form of a fleet tool** (23/10/2026). Every tool wrote its own form around the kit's pieces, and
28
+ the words drifted: "Your lark email", "Team password", "Access password", placeholders of dots, buttons saying `Enter →`, `Vào`,
29
+ `Continue`, `Log in` (measured on the live sign-in pages of 60 services, 23/10/2026: one of them, GemPress's, had the whole
30
+ standard). Chris: "cái này chắc phải đưa vào gemboss ui".
31
+
32
+ Added
33
+ - `AuthSignIn` (auth.tsx): Continue with GemBoss (with `service`), then Email, Password with its eye, the error and Sign in, in
34
+ the kit's words and with no placeholder. The tool passes `service`, `next`, `onSignIn({ email, password })` (returns the
35
+ server's sentence on a refusal, nothing once it has moved the browser on; a throw reads as the network) and an optional `foot`.
36
+ `SIGN_IN_OFFLINE` is that network sentence.
37
+ - `auth-signin-plain.js` (`@gemboss/ui/auth-signin-plain.js`), its own weight entry: `gembossUI.initSignIn()` fills an empty
38
+ `<form data-gb-signin>` with the same markup, for a page with no React. Loaded after auth-sso-plain.js. `data-submit="json"`
39
+ posts `{email, password}` and follows `data-next`; otherwise the browser posts the form and the server's sentence comes back
40
+ in `data-error`. A form written without `method` is posted (a GET would put the password in the URL). A redirect the server
41
+ answers with is followed as a page, never read as success. With `data-sso` but no auth-sso-plain.js it draws no button and
42
+ says so in the console. Its eye stops its click at the button, so kit-plain.js's delegated `initPasswordFields()` does not flip
43
+ it a second time.
44
+ - `initContinueWithGemBoss()` (auth-sso-plain.js) can be called again: a link it already set up gets no second click handler.
45
+ - `auth-signin-browser.test.ts`: the React and the plain form compared element by element in Chromium (a word changed on one
46
+ side is red), what each does on submit, the eye with kit-plain.js on the page; two negative controls.
47
+
48
+ **ListPage: the frame of a list page** (23/10/2026, step 3 of the list standard, Gems/CLAUDE.md §13). Every admin list page
49
+ drew its own frame on Polaris `Page`, and its states its own way: five kinds of empty state, four labels on a retry, a skeleton
50
+ of 3 or 4 lines or a spinner or nothing (survey of 14 pages, 23/10/2026).
51
+
52
+ Added
53
+ - `ListPage` (+ `ListPageAction`, `ListPageState`): the header (a way back, the title and its badge, one line under it, the page's
54
+ primary button, secondary actions), notices, a toolbar, and ONE card holding the list or the state it is in (`state`:
55
+ `loading` placeholder rows, `empty` the page's own empty state, `no-match` with Clear filters, `error` with Try again).
56
+ Polaris Page's numbers as the admin draws it, measured at the six widths. From a page column of 560px two secondary actions
57
+ are buttons and the rest are behind ⋯; under it, and below 768px, all of them. Below 768 the header is two rows and its
58
+ controls 44px. Its CSS is an entry, `list-page.css` (`@gemboss/ui/list-page.css`, imported by list-page.tsx).
59
+ `list-page-browser.test.ts`, four cases with negative controls.
60
+ - `Icon` `arrowLeft` (ListPage's way back, the arrow Polaris' Page draws there).
61
+ - ListPage, steps 3c and 3d (Pages, Themes, Plugins, Media, Offers, Funnels): `card={false}` for a list that draws its own cards
62
+ (its own empty stands on the page; loading, no match and error are still boxed), `cardProps` (stay on the card in every
63
+ state: a drop zone), `cardMeta` (beside the card's heading), and `cardLead`: rows at the top of the card in every state, the
64
+ state's body after them (Funnels' Website row, which does not wait for the list's read); a page with a lead always has its
65
+ card, so the lead stays the same element. Notices or a toolbar that draw nothing take no gap. The page's status region
66
+ ("Loading…") is one element in every state and only its words change (one added with its words in it is often not read out).
67
+ `list-page-browser.test.ts` cases 6–8, with negative controls.
68
+ - ListPage, step 3e (Tests): an `external` action (a new tab, a download) is a plain link, as a button and as a ⋯ row, not the
69
+ app's `linkAs`: Next prefetches its Link, so the Tests page's Export CSV built its file on every visit. Case 5.
70
+ - ListPage, step 3f (Products): `actionGroups` (Polaris Page's name), choices of one kind always behind ⋯ at every width, a
71
+ title and the one that is on marked `active` (Products' Auto-refresh: Every day, Off). Case 9, with a negative control.
72
+ - `MenuItem` `checked`: a choice (`menuitemradio`, `aria-checked`) with a check before the one that is on and the same space
73
+ before the others; `MenuGroup`: a titled `role="group"` of such rows. Their CSS is in the `menu-extras.css` entry (the core
74
+ is frozen). list-page-browser.test.ts now loads menu-extras.css, as the admin does through menu.tsx.
75
+ - A closed ⋯ has no layout (`menu-extras.css`: `.ui-menu:not([open]) > .ui-menu__list { display: none }`). A closed <details>
76
+ only skips painting: asked for geometry, its rows were laid out, so the admin's phone-width gate read a closed ListPage ⋯ at
77
+ 320px as rows cut by the left edge, and find-in-page matched them and opened the menu. menu-browser.test.ts case 23.
78
+ - `Pager` (+ `pageWindow`): the numbered pager (`ui-pager`, README "Long lists") as a React component, links (`href`, through
79
+ `linkAs`) or, for a list paged in state, buttons (`onPage`), `busy` while a page is on its way. With Chris's OK (24/10/2026)
80
+ the pager's rules moved out of kit-plain.css into `pager.css`, just before it in the core (same rules, same order, no new
81
+ weight; the cascade check paints 0 boxes differently); the buttons' look is the new `pager-extras.css` entry.
82
+ `pager-browser.test.ts`, four cases with negative controls. The admin's Products list uses it.
83
+
84
+ ### Before: the row ⋯ (step 2)
85
+
86
+ **The admin's row ⋯ is the kit's Menu** (23/10/2026, step 2 of 3: `RowMenu` in apps/admin swaps Polaris's Popover + ActionList for
87
+ `Menu`; step 3 is the "list page" component).
88
+
89
+ Added
90
+ - `Menu` takes `disabled` and `loading`: the ⋯ takes no click and no key and leaves the Tab order (`aria-disabled`, drawn by
91
+ ui.css); `loading` puts a ring in place of the dots and `aria-busy`. The ring (`.ui-menu__busy`) is in a new entry,
92
+ `menu-extras.css` (`@gemboss/ui/menu-extras.css`, imported by menu.tsx): menu.css is in the core and the frozen
93
+ gemboss-ui.css had no bytes left (+24 B gzip over), and the admin loads no kit-plain.css (its `.ui-spinner`). Turned off
94
+ while open, it closes, with focus that was in it back on ⋯; opened while off by anything but its button (find-in-page, a
95
+ script), it closes again. `menu-browser.test.ts` case 20, with a negative control.
96
+ - A long hint wraps (`menu-extras.css`): in a column of at most 32ch, under the label when the row has no room beside it, and
97
+ the list is never wider than the screen less 16px. One nowrap line of 80 characters made the list ~600px wide and ran it
98
+ past a phone screen. A short hint stays on one line right of the label. `menu-browser.test.ts` case 21.
99
+
100
+ Changed
101
+ - Escape that closes an open React menu from the page-wide listener (focus on the ⋯, or nowhere in the menu) is taken
102
+ (default prevented), as kit-plain.js already takes it, so a dialog listening after it does not close on the same Escape.
103
+ `menu-browser.test.ts` case 22.
104
+
105
+ **The ⋯ menu is ready to be the admin's row menu** (23/10/2026, step 1 of 3: the kit only; the admin's `RowMenu` moves onto it next).
106
+
107
+ Added
108
+ - `menu.css`, its own file (`@gemboss/ui/menu.css`), imported by `menu.tsx`: the `.ui-menu` rules (placement, the floated list,
109
+ the summary marker, `--end`), the hint, the separator, the disabled row and the icon button (`ui-btn--icon`, with its 44px
110
+ rules under a finger), moved out of `kit-plain.css` unchanged. The admin loads `ui.css` and the shell, never `kit-plain.css`,
111
+ so a `Menu` there had no placement and no square ⋯. The core carries the file where the rules stood, just before
112
+ `kit-plain.css` (`packages/ui/scripts/parts.mjs` CORE and the frozen copy in `packages/ui/scripts/parts-gate.mjs`: a split, not new weight), so
113
+ `parts/components.css` and `gemboss-ui.css` hold the same rules in the same order.
114
+ - A menu's semantics and keys, `menu-keys.ts`, shared by `Menu` and `LanguageSwitcher` (whose own key handler and `openMenu`
115
+ it replaces). The list is `role="menu"` named by the trigger's label, rows `menuitem` with `tabindex="-1"`, the trigger
116
+ `aria-haspopup`. An opening by a click, Enter or Space lands on the first row that can act (↓ on ⋯ the first, ↑ the last;
117
+ a script's `open` moves no focus), with or without the popover API. On a row (a field in the list keeps its own keys):
118
+ ↑ ↓ wrap and skip disabled rows; Home End; Space acts on a link row; Escape closes with focus back on ⋯ and is taken; Tab
119
+ closes and goes on. A chosen row runs with focus already back on ⋯ (a popup it opens returns there), then the menu
120
+ closes, link rows too. One menu open at a time, whatever opened the next one (a key is not a click outside); a menu that
121
+ holds the one opening stays open.
122
+ - `menu-plain.js` (`@gemboss/ui/menu-plain.js`), the same for plain markup: a SECOND script after `kit-plain.js`, which has no
123
+ room for it under its weight ceiling. It adds the roles the markup leaves out and runs by itself. **A plain surface that
124
+ wants a menu's keys adds this script** (or `import "@gemboss/ui/menu-plain.js"` after the kit-plain.js import); without it
125
+ a plain menu works as before.
126
+ - `menu-browser.test.ts` (`test:ui-browser`): 19 cases on `Menu` and on plain markup, each with a negative control: roles,
127
+ keys, where an opening lands, choosing a row, one open, a menu in a table row that opens on click, the last row of a
128
+ scrolling table, Escape and a selection, the admin's own sheets + `menu.css` against the core, a field in the list, a menu
129
+ in a menu or the account menu, a menu with no list, a popup's Tab trap, a prevented opening, no popover API, and a React
130
+ Menu on a page that also runs kit-plain.js + menu-plain.js, Tab from an open menu's ⋯, an Escape something else took, and
131
+ the plain LanguageSwitcher beside menu-plain.js. `plain-ssr.test.ts` (`test:packages`): every `*-plain.js` loads in Node
132
+ without throwing.
133
+ - Review of #5179: Tab from the ⋯ of an open menu closes it and focus goes on (its rows are out of the Tab order, so the list
134
+ stayed open over the page); the page-wide Escape leaves an Escape something else took (`defaultPrevented`: a dialog, a
135
+ combobox) alone; a close clears the one-open mark an opening left (an opening closed before its toggle let the next one skip
136
+ the rule); menu-plain.js leaves the plain LanguageSwitcher (`.gb-lang`, its own keys and checked-row focus) to
137
+ language-switcher-plain.js and only closes it with the others (two scripts put focus twice).
138
+ - editor-insert.css: the insert menu's "Ask AI: …" row (E2), in the accent colour, highlighted with the accent wash when
139
+ selected, and one line however long the request.
140
+ - Four pieces GemPress's redesign (approved by Chris 23/10/2026) drew with its own CSS, each its own entry, presentational
141
+ (no state, no inline style) and written by hand from plain HTML with the classes listed at the top of its file (README "Review
142
+ pieces"):
143
+ - `StagePath` (`stage-path.css`): stages that unlock in order, an `<ol>` of cards with a number (a green ✓ when
144
+ `done >= total`), the name, "3 / 35 pages approved" and a `<progress>` bar; the current stage (default the first not done)
145
+ has an ink outline and an ink number, `aria-current="step"`. `stagePathDone`.
146
+ - `StatusMatrix` (`status-matrix.css`): rows × columns on the kit table, each cell "live / total" with what waits under it
147
+ ("5 draft · 2 changed"), toned all live (green) or has drafts (amber), "—" where there is nothing to count.
148
+ `statusMatrixTone`.
149
+ - `TranslationRow` (`translation-row.css`): one string under review, key + state badge + who, source beside target (one
150
+ column under 900px or in a row narrower than 600px), the checks and the surface's action; `current` draws an ink border
151
+ and a 3px ink bar; focusable (`tabIndex={-1}`) for a J / K list. The editable control is the surface's (`target`, a node or
152
+ a function given the ids for `aria-describedby`). `TRANSLATION_STATE_LABELS`, `translationRowId`.
153
+ - `StatusList` (`status-list.css`): a quiet system-health strip, a dot (success / warning / danger, or an empty ring for
154
+ muted), a bold name and one muted sentence, a link when it has `href`.
155
+ - `CreateIcon`, `CreateStepper`, `CreateRail`, `CreateDoors`, `CreateFilter`, `CreateGrid`, `CreateCard`, `CreateParts`,
156
+ `CreatePath` (`create-shell.css`, its own entry): the create shell, one shape for making a page, a funnel or a section inside
157
+ the frame each already opens in: an icon tile and "Choose → Make it yours → Gem builds it" in the header, a rail with the doors
158
+ (ask the assistant, the library, blank: at most three) and chip questions ("Who it is for": cold, warm, ready, each with its
159
+ colour's dot; "What it does"), cards with a Stage picture, who it is for, a name, one line and what is inside. `CREATE_AUDIENCE`
160
+ holds the words for the three temperatures (23/10/2026, drawings A1/A3/A4).
161
+
162
+ Changed
163
+ - shell.css is the one owner of the menu row: `.gemboss-account-menu-item, .ui-menu__item` keep their shared declarations,
164
+ the account row stays `display: block`, and `.ui-menu__item` is the flex row (label, then the hint on the right; `gap: 8px`,
165
+ nowrap, the small radius) that app-shell.css used to lay over it. app-shell.css drops its `.ui-menu__item` rule. A plain
166
+ `details.ui-menu` on the core: 0 differences in 2.3 million computed values against main (bundle and parts, built and
167
+ minified; 1280 and 390, mouse and finger, light, dark, the `data-gb-menus` flag). In the admin, which loads shell.css and
168
+ not app-shell.css, the assistant dock's ⋯ rows become that flex row too (their boxes unchanged); the account menu's rows
169
+ do not change.
170
+ - `initMenus()` (kit-plain.js) puts focus back on a menu's button BEFORE a chosen row's own handler, then closes the menu as
171
+ before. Still opt-in: a page that never calls it keeps its menus open on a chosen row. `closeMenus(except)` leaves open a
172
+ menu that holds `except` (a kit menu in an open account menu).
173
+ - kit-plain.js: a popup's Tab trap leaves `tabindex="-1"` out (a menu's rows): counted as its last control, Tab from the real
174
+ last one left the popup.
175
+ - `escapeTaken` (selection.tsx, kit-plain.js): a menu or a dialog takes Escape when it is SHOWN (`checkVisibility`), not when
176
+ it has a box. A closed `<details>`' content keeps its boxes, so with every row's ⋯ list `role="menu"` the box test would
177
+ have handed every Escape in a selectable table to its closed menus.
178
+ - `Menu`: one page-wide set of listeners for every mounted menu (`registerMenu`: outside click, Escape, the float, one open),
179
+ not a click and a keydown listener per menu; the summary opens the menu itself with or without the popover API.
180
+ - kit-plain.js stays under its ceiling (8 784 B gzip of 9 189); menu-plain.js weighs 1 047 B (ceiling 1 559).
181
+ - collections-board.test.ts: the keyboard case loads menu-plain.js and walks the rows with ↓ (it walked them with Tab); the
182
+ two "no popover API" negatives open the menu by script, since a click now focuses its first row and, without the top
183
+ layer, that focus scrolls the table's frame to it (menu-browser.test.ts case 15 holds the click).
184
+
185
+ ## 0.49.0 (23/10/2026)
186
+
187
+ Fixed
188
+ - collections.css: a folded index row's primary cell wraps, as the fold always meant to. Its `white-space: normal` was
189
+ (0,2,1) and lost to the fold's own nowrap on every cell (0,2,2), so free text under a title (GemPress's "missing: …"
190
+ note) and an unlinked title stayed on one line, widened the row and scrolled the list sideways (384px at 320 on
191
+ GemPress). A linked title (`.ui-table__title`) and its second line keep their own nowrap and are cut as before; a token
192
+ too long for the row (a URL, a slug) breaks anywhere. A cell whose content fits is unchanged (gemcare People at
193
+ 320/390/768, 50 rows: no row taller).
194
+ - ui.css: `.ui-sr-only` is pinned on the inline axis (`left: 0`; also portal.css `.gb-sr-only`, team-chat.css
195
+ `.gb-tc-sr`, shell.css `.gemboss-assistant-sronly`, assistant.css `.gb-dock-sr`). It stood where it would have been in the
196
+ flow, so inside a frame that scrolls sideways (a table, a board, a segmented row) it could stand far past the frame and push
197
+ the PAGE sideways: the "Actions" header of a 2105px IndexTable, 792px at 1280; search-radar /aeo 428px; a bare
198
+ `ui-table-wrap` on a page with ui.css only, 1109px. It keeps its line, so a screen reader's cursor stays by the row.
199
+ (#5176 had made `.ui-table-wrap` `position: relative` instead and put `noreferrer` on a row's Cmd-click; both are withdrawn
200
+ before release: a positioned frame paints over an earlier dropdown or sticky bar with no z-index, and the referrer is the
201
+ page's to set, see the README's collection rules.)
202
+ - shell.css: the topbar's actions column never goes narrower than what it holds (`minmax(max-content, 1fr)`, and
203
+ `max-content` under 1120px), and the search column gives way instead (`minmax(0, …)`). With a 200px minimum the admin's
204
+ right-hand chips (~512px) spilled left over the search: 160px at 1280, 239 at 1121 (22/10/2026). The brand column is
205
+ unchanged (a surface's tabs there keep their scroller); with room to spare the search is centred as before. The
206
+ `max-width: 1040px` topbar rule, which the base rule after it always overrode, is removed.
207
+
208
+ Added
209
+ - `@gemboss/ui/editor-insert.css` (23/10/2026), its own entry: the page editor's insert menu, the + on a picked element (admin
210
+ `InsertElementMenu`). It is a list to search, with the row in focus described beside it, 32px rows (44px under a finger), and a
211
+ sheet along the bottom under 640px. It's not in editor.css because every editor surface loads that.
212
+ - IndexTable `flush` (root `.ui-index--flush`): inside a card that is already the frame, as DataTable's and ResourceList's. The
213
+ table drops its own border, radius and shadow; the bulk bar covers the 37px header and draws its rule; folded, the bar sits on
214
+ the first row and the sort chips are inset 12px like the rows. partner-radar and talent-radar drew two frames (23/10/2026).
215
+ - `Segmented` takes `scroll`: text tabs keep one line and the group scrolls sideways inside itself when it is wider than its
216
+ room, what kit-plain.css already does for plain surfaces. Set as layout styles on the component (gemboss-ui.css had no
217
+ bytes left under its weight ceiling); opt-in; first user the admin's Plugins filter (five tabs broke to three lines each at
218
+ 390, 22/10/2026).
219
+ - `AssistantHost` takes `waiting`: the chat's last answer ends on choices only the person can make. The status bar and the
220
+ header say "Waiting on you" with an amber dot (`--gemboss-warning`, set on the dot: the bundle is at its gzip ceiling), ahead of "Done" and "Ready". It is a prop, not dock
221
+ state, so the plain frame's reducer copy is unchanged. New label `waiting`.
222
+ - assistant-chat.css `.gemboss-ec-decide` / `-decide-label`: the box that holds a waiting question's choices over the message box
223
+ (22/10/2026).
224
+ - `Stage`, `StageWindow`, `StageTile`, `StageBubble`, `StageLine` (`stage.css`, its own entry): the one way the app draws a
225
+ picture, taken from gemboss.ai's mega menu: a night-blue stage lit with iris, a dot grid fading out, glass tiles (one picked,
226
+ white and glowing), white windows lifted off it showing a real screenshot. Its colours are named once on `.ui-stage`
227
+ (an illustration may light itself, website/DESIGN-LANGUAGE.md §3). A stage is hidden from screen readers unless
228
+ `decorative={false}` (a panel holding words a reader needs); `StageWindow` then takes `alt` for its screenshot.
229
+ - `EmptyStage` (`empty-stage.css`, its own entry): an area with nothing in it yet, centred like an admin empty state: a
230
+ Stage picture drawn from the area's own nouns, a name, one line, the page's own buttons (`.ui-empty-stage__row` lays tiles
231
+ side by side). Replaces the picture-less empty states (22/10/2026).
232
+ - `StartingPoint` (`starting-point.css`, its own entry): "Choose your starting point", the same three doors wherever something new
233
+ is made: ask the assistant (a two-line chat on the stage), a template (two real pages fanned on the stage, `images`), blank (an
234
+ empty window with "+ Add a section"). Each door is a picture, a name, one line and a button; at most three. Learned from
235
+ Replo's Plans, Tasks, Skills and reports (22/10/2026).
236
+ - `ChatStarter` takes `sub` (one line under the label) and `icon`; a list where any starter has either is drawn as a column
237
+ of cards (`.gb-chat-starters-cards`: icon in an iris tile, label, the line under it). Pills are unchanged.
238
+ - `firstBlank(text)` and `selectBlank(textarea, text)`: a starter with `fill` now selects the first `[…]` in its prompt, so
239
+ typing replaces it ("Build a funnel for [which product?]"); with no blank the caret goes to the end (22/10/2026).
240
+
241
+ Changed
242
+ - collections.css: an index table's header is the data table's (the surface, 13px regular, 12px above and below: 45 tall,
243
+ opaque) and its cells 8px 6px (a one-line row 37), taken from the data table's rules in ui.css rather than restated. It was Polaris IndexTable's grey band, 12px / 550, 36 tall, over 6px cells: two
244
+ looks for one thing, the heavier one on the lists people open (Chris 23/10/2026: "GemPress's table reads lighter than
245
+ Funnels"). Its bulk bar is 46 (the wrap's top line + the header), 45 flush, 38 folded below 768, wherever in the index
246
+ it sits; a resource list's stays 38. The admin's unselectable Polaris IndexTables take the same (gemboss #5185).
247
+ - `SessionRail` (and `sessionRail` in session-rail-plain.js): a conversation's `preview` follows its time on the same line,
248
+ "3 hr ago · Nothing is published yet…", one line cut with an ellipsis (`.gemboss-assistant-thread-meta`, shell.css). The
249
+ admin now sends the answer's closing sentence as `preview`, how the conversation ended (23/10/2026, learned from Replo).
250
+ - editor.css: the page editor's column (`.gemboss-pageedit .gemboss-ec-dock-body`: its section list and the element settings) is
251
+ 44px tall under a finger and at a phone's width only; under a mouse from 48em its buttons, fields and selects are the kit's own
252
+ sizes, as the same settings panel on the canvas (they were 44 at every width: 44 against 32 for the same panel, 22/10/2026).
253
+ - editor.css: the page editor's icon buttons (`.gemboss-pageedit .gemboss-pv-device`: ↶ ↷, ⋯) are 44 × 44 under a finger and
254
+ at a phone's width only; under a mouse from 48em they are the kit's 28 × 28, as on every other editor bar (they were 44 at
255
+ every width, 21/10/2026).
256
+
257
+ **The briefing card leaves its picture out when the card is too narrow to hold it** (Market, 22/10/2026: with the assistant pinned beside the page the
258
+ card was 693px wide and 510px tall, the gem under the buttons).
259
+
260
+ Changed
261
+ - `.ui-briefing` is a size container (`container-type: inline-size`) and `@container (max-width: 631px)` hides `.ui-briefing__art`. 631 is the
262
+ content width the picture's row needs (380 text + 32 gap + 220 picture = 632) less one: a container query measures the content box, so it
263
+ is the card's 720px at the desktop padding. It is the CARD that is measured, not the screen: the same card is wide on a desktop and
264
+ narrow beside a pinned panel. Measured in Chromium at a 1440px window: picture beside the text at 720px and wider, none at 719px and
265
+ narrower, and the card 258px tall at 693px instead of about 430. `briefing-card.test.tsx` holds the rule.
266
+
7
267
  ## 0.48.0 (21/10/2026)
8
268
 
9
269
  **LanguageSwitcher: what its first three surfaces had to work around** (21/10/2026; GemCommunity #333, GemNews #2,
package/README.md CHANGED
@@ -54,7 +54,8 @@ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as
54
54
  | Part | Import (Node / bundler) | File in the tarball | What | gzip | Who loads it |
55
55
  |---|---|---|---|---:|---|
56
56
  | core | `@gemboss/ui/core.css` | `parts/core.css` | the box-sizing reset + the tokens | 1.0 KB | every surface, first |
57
- | components | `@gemboss/ui/components.css` | `parts/components.css` | the kit (`ui.css`), the field look, the plain-HTML primitives (`kit-plain.css`: menu, chips, report pieces, popup body) | 8.2 KB | every surface, second |
57
+ | components | `@gemboss/ui/components.css` | `parts/components.css` | the kit (`ui.css`), the field look, the ⋯ menu and the icon button (`menu.css`), the plain-HTML primitives (`kit-plain.css`: chips, report pieces, popup body) | 8.2 KB | every surface, second |
58
+ | menu | `@gemboss/ui/menu.css` | `packages/ui/src/menu.css` | the ⋯ menu and its icon button (23/10/2026, moved out of `kit-plain.css`, same rules): part of the core (components, just before `kit-plain.css`, where its rules stood) AND its own file | 0.6 KB | `Menu` imports it, so a surface without the core draws the menu: the admin loads `ui.css` and the shell, never `kit-plain.css` |
58
59
  | shell | `@gemboss/ui/shell.css` | `parts/shell.css` | rail, topbar, main, mobile drawer, the thread rail, topbar slots, base text, page widths (`app-shell.css`) | 5.5 KB | every surface, third |
59
60
  | auth | `@gemboss/ui/auth.css` | `packages/ui/src/auth.css` | the sign-in screen (+ the grid backdrop, 0.30.0) | 2.8 KB | `AuthLayout` & co. import it |
60
61
  | assistant | `@gemboss/ui/assistant.css` | `packages/ui/src/assistant.css` | the dock and the launcher | 2.2 KB | `AssistantHost`, the launcher import it |
@@ -66,6 +67,7 @@ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as
66
67
  | editor | `@gemboss/ui/editor.css` | `packages/ui/src/editor.css` | the editors | 7.9 KB | the `@gemboss/ui/editor` components import it |
67
68
  | editor-repeater | `@gemboss/ui/editor-repeater.css` | `packages/ui/src/editor-repeater.css` | `RepeaterList`: a setting that is a list, as rows | 0.9 KB | `RepeaterList` imports it |
68
69
  | editor-tiles | `@gemboss/ui/editor-tiles.css` | `packages/ui/src/editor-tiles.css` | `ChoiceTiles`: a closed choice of shapes as picture tiles | 0.5 KB | `ChoiceTiles` imports it |
70
+ | editor-insert | `@gemboss/ui/editor-insert.css` | `packages/ui/src/editor-insert.css` | the page editor's insert menu (the + on a picked element) | 0.6 KB | admin `InsertElementMenu` imports it |
69
71
  | reactions | `@gemboss/ui/reactions.css` | `packages/ui/src/reactions.css` | the emoji reactions and their picker | 0.8 KB | `Reactions` / `ReactionPicker` import it |
70
72
  | assistant-engine | `@gemboss/ui/assistant-engine.css` | `packages/ui/src/assistant-engine.css` | the engine picker (chip + Engine/Setup/Status popup) and the bridge notice | 1.9 KB | `EnginePicker` / `EnginePanel` / `BridgeNotice` import it |
71
73
  | chat | `@gemboss/ui/chat.css` + `chat-composer.css` | `packages/ui/src/chat.css`, `packages/ui/src/chat-composer.css` | the chat body, the composer | 3.0 + 0.9 KB | the chat components (0.29.0); a page writing the markup links them |
@@ -82,6 +84,10 @@ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as
82
84
  | progress-ring | `@gemboss/ui/progress-ring.css` | `packages/ui/src/progress-ring.css` | `ProgressRing`: "3 of 5" as a ring (0.43.0) | 0.2 KB | `ProgressRing` imports it |
83
85
  | spot-art | `@gemboss/ui/spot-art.css` | `packages/ui/src/spot-art.css` | `SpotArt`: six brand pictures for pauses and empty states (0.43.0) | 0.4 KB | `SpotArt` imports it |
84
86
  | logo-sheet | `@gemboss/ui/logo-sheet.css` | `packages/ui/src/logo-sheet.css` | `LogoSheet`: one logo as a person judges it, the mark on a checkerboard, the lockup on a light and a dark header, the favicon at its real 32px (unreleased) | 0.5 KB | `LogoSheet` imports it |
87
+ | stage-path | `@gemboss/ui/stage-path.css` | `packages/ui/src/stage-path.css` | `StagePath`: stages that unlock in order, each a card with its number (✓ when done), its count and a thin bar; the current one in ink (unreleased) | 0.7 KB | `StagePath` imports it; a plain-HTML page links it (README "Review pieces") |
88
+ | status-matrix | `@gemboss/ui/status-matrix.css` | `packages/ui/src/status-matrix.css` | `StatusMatrix`: rows × columns of "live / total" cells on the kit table, all live or has drafts (unreleased) | 0.3 KB | `StatusMatrix` imports it; a plain-HTML page links it |
89
+ | translation-row | `@gemboss/ui/translation-row.css` | `packages/ui/src/translation-row.css` | `TranslationRow`: one string under review, source beside target, the checks and the action (unreleased) | 0.6 KB | `TranslationRow` imports it; a plain-HTML page links it |
90
+ | status-list | `@gemboss/ui/status-list.css` | `packages/ui/src/status-list.css` | `StatusList`: a quiet system-health strip, a dot, a name and one sentence each (unreleased) | 0.5 KB | `StatusList` imports it; a plain-HTML page links it |
85
91
  | bundle | `@gemboss/ui/gemboss-ui.css` | `gemboss-ui.css` | core + components + shell + auth + assistant + chat + portal + theme-dark | 21.9 KB | compatibility only (see CHANGELOG 0.23.0) |
86
92
 
87
93
  The core alone is 14.7 KB against 21.7 for the bundle (each part opens with a short header naming the gemboss commit, kept through minify, so a surface serving the parts can still say which commit its CSS is); a list page adds collections (4.6), a sign-in page auth (2.6).
@@ -130,7 +136,7 @@ in order and reversed, and holds every element's computed style to the bundle's
130
136
  CI; negative control: two entries that only agree by order). An entry loaded BEFORE the core is not supported: measured,
131
137
  `.ui-empty--no-match` loses its 32px padding to `.ui-empty`. Two kit-plain.css rules that overrode shell.css by coming
132
138
  after it (the menu row's flex, the thread rail's 44px buttons at phone width) moved to the end of app-shell.css, same
133
- weight, so they still follow it.
139
+ weight, so they still follow it. (The menu row's flex is shell.css's own since 23/10/2026: one owner for the row.)
134
140
 
135
141
  **A new component** gets its own entry: a file in `src/`, listed in `ENTRIES` in `packages/ui/scripts/parts.mjs`, exported,
136
142
  imported by the component that draws it, with a ceiling in `weight-budget.json`. It never goes into the core or into
@@ -142,6 +148,37 @@ component that draws an entry's class without importing it.
142
148
  built part now). Its editor, assistant and sign-in modules already imported the same entry files the components now
143
149
  import, so nothing moves.
144
150
 
151
+ ## ListPage (23/10/2026): the frame of a list page
152
+
153
+ One component for a page that lists one kind of thing (the admin's Rules first; the other lists move one PR at a time):
154
+
155
+ ```tsx
156
+ <ListPage title="Rules" subtitle="12 rules · 9 on." back={{ href: "/app/content", label: "Types" }} linkAs={Link}
157
+ primaryAction={<Button variant="primary">New rule</Button>}
158
+ secondaryActions={[{ content: "Simulator", onAction }, { content: "Export CSV", url: "/export" }]}
159
+ notices={<Banner …/>} toolbar={<ListToolbar …/>}
160
+ state={saved === null ? (unread ? "error" : "loading") : saved.length ? "ready" : "empty"}
161
+ empty={<EmptyStage …/>} noMatch={{ onClear }} error={{ title: "Your rules couldn’t be read.", message, onRetry }}
162
+ cardTitle="Your rules" after={<PlaybookLibrary …/>}>
163
+ {rows}
164
+ </ListPage>
165
+ ```
166
+
167
+ - The page says WHICH state its list is in; the kit draws it, the same on every list: placeholder rows while loading, the
168
+ page's own empty state, "Nothing matches" + Clear filters, a read that failed + Try again. A change that failed over a list
169
+ that IS there is a notice, not the error state.
170
+ - `primaryAction` is the page's own button (so a Publish button that counts changes stays itself). Secondary actions are data:
171
+ two stand as buttons from a 560px page column, the rest are behind ⋯; under 560 or below 768px, all are behind ⋯.
172
+ - `cardLead`: rows at the top of the card in every state, before the state's loading, empty or error (a row that does not
173
+ wait for the list's read, as Funnels' Website). A page with a lead always has its card, so the row stays the same element.
174
+ `card={false}` is for a list that draws its own cards (a grid); `cardProps` stay on the card in every state (a drop zone).
175
+ - `actionGroups`: choices of one kind (a cadence, a view) behind ⋯ at every width, titled, the one that is on `active`; never
176
+ header buttons. The rows are `MenuItem checked` in a `MenuGroup` (`menuitemradio`, a check before the chosen one).
177
+ - Numbers: Polaris Page in the admin (title 20/24/700 on a 28px row, the line under it 12/16 muted 2px below, actions 28px 8
178
+ apart, 24px above and under the header from 768, 16 below). Below 768 the header is two rows and its controls are 44px.
179
+ Colours are Polaris' where Polaris is loaded (`--p-color-text`, `--p-color-text-secondary`, `--p-color-bg-fill-tertiary`),
180
+ the kit's tokens elsewhere.
181
+
145
182
  ## Page width (0.8.1)
146
183
 
147
184
  Chris, 04/10/2026: the rail on the left, and on the right a content column with the same max width on every
@@ -924,6 +961,16 @@ gembossUI.initShell(); // AppShell markup written by hand: ☰ drawer,
924
961
  gembossUI.initPasswordFields(); // the sign-in's reveal button; resetPasswordFields() on sign-out
925
962
  ```
926
963
 
964
+ Menus (23/10/2026): a menu's keys are a SECOND script, `menu-plain.js`, loaded after this one (`<script src>`, inlined, or
965
+ `import "@gemboss/ui/menu-plain.js"` after the kit-plain.js import): kit-plain.js has no room for them under its weight
966
+ ceiling. It runs by itself and gives every `details.ui-menu` what the React `Menu` does (README "Menu"): the roles its
967
+ markup leaves out, the keys on a row (↑ ↓ wrapping past disabled rows, Home End, Escape with focus back on ⋯, Tab leaves;
968
+ ↓ / ↑ on ⋯ open it), the first row focused on an opening by a click, Enter or Space, one menu open at a time. Without it a
969
+ plain menu works as before (Tab walks its rows). `initMenus()` (opt-in, as before) closes a menu on a chosen row, now with
970
+ focus back on ⋯ BEFORE the row's own handler, and on an outside click, the window's blur and Escape from anywhere; a page
971
+ that never calls it keeps its menus open on a chosen row. Both scripts return at once where there is no DOM, so a Next
972
+ client module that imports them renders on the server (`plain-ssr.test.ts`).
973
+
927
974
  In a bundled app (Next, Vite) load it once with a bare `import "@gemboss/ui/kit-plain.js";` in a client module: the
928
975
  package lists the file in `sideEffects`, so the bundler keeps it (before 0.5.0 it dropped it and `gembossUI` was
929
976
  undefined in production builds). Loading it writes nothing on `<html>` (0.37.1): the `data-gb-menus` flag kit-plain.css
@@ -1663,9 +1710,23 @@ import { AppShell, BrandLockup, AccountMenu, AccountMenuItem, Icon, RailItem } f
1663
1710
  outranks an attribute on the `<svg>` itself and paints an outline icon solid.
1664
1711
  - `Menu` / `MenuItem` / `MenuSeparator` (0.2.1): the "⋯" action menu. Same `<details>` rule as the account
1665
1712
  menu (works as plain markup; with React it closes on an outside click, on Escape with focus back on the
1666
- trigger, and after a button row's handler). Rows share the account menu's row style. `MenuItem` is a link
1667
- (`href`) or a button; `disabled` on a link drops the href and sets `aria-disabled`; `className` is added
1713
+ trigger, and after a row is chosen). Rows share the account menu's row style. `MenuItem` is a link
1714
+ (`href`, `target`, `rel`) or a button; `disabled` on a link drops the href and sets `aria-disabled`; `className` is added
1668
1715
  to the row's class.
1716
+ A menu (23/10/2026, `menu-keys.ts`, shared with `LanguageSwitcher`; `menu-plain.js` after kit-plain.js for plain markup):
1717
+
1718
+ | | |
1719
+ |---|---|
1720
+ | Semantics | the list is `role="menu"` named by `label` (the trigger's name), the trigger `aria-haspopup="menu"`, each row `role="menuitem"` with `tabindex="-1"`: the arrows move between rows, Tab does not |
1721
+ | Opening | a click, Enter or Space on ⋯ puts focus on the first row that can act; ↓ on ⋯ opens on the first, ↑ on the last; a script's `open = true` moves no focus |
1722
+ | Keys | on a ROW (a field or a checkbox in the list keeps its own): ↑ ↓ wrap and skip disabled rows (`disabled`, `aria-disabled="true"`); Home / End; Enter and Space act on the row (Space on a link row too); Escape closes with focus back on ⋯ and is taken (a popup around the menu stays open); Tab / Shift+Tab close it and focus goes on from ⋯ |
1723
+ | Choosing | focus goes back on ⋯ BEFORE the row's handler (a popup it opens returns focus there, not to a hidden row; a handler that moves focus on purpose keeps it), then the menu closes, link rows included. Plain markup: with `initMenus()`, opt-in as before |
1724
+ | One open | opening a menu closes every other open `details.ui-menu`, by a click, a key or a script (a key is not a click outside); a menu that holds the one opening (a menu in a menu, a kit menu in the account menu) stays open |
1725
+ | Listeners | one page-wide set for every mounted `Menu` (`registerMenu`): outside click, Escape, the float on its toggle, one open. A 100-row table has 100 Menus and one set of listeners, not 200 |
1726
+ | In a row that opens on click | `IndexTable` rows with `href` and `tr[data-href]` leave the menu be: opening, moving and choosing never follow the row |
1727
+ | CSS | `menu.css`, imported by `menu.tsx`: placement, the floated list, hint, separator, disabled row, the square ⋯ (`ui-btn--icon`). The surface and the row are shell.css's (which the admin loads too): `.ui-menu__item` a flex row (label, then the hint on the right; the account menu's `.gemboss-account-menu-item` stays a block), 32px under a mouse, 44 under a finger or below 768. So a page with `ui.css` + `shell.css` + `menu.css` and no `app-shell.css` (the admin), in either order, draws what the core draws |
1728
+
1729
+ `menu-browser.test.ts` runs each of these on `Menu` and on plain markup with kit-plain.js, each with a negative control.
1669
1730
 
1670
1731
  ## Topbar search (0.6.3)
1671
1732
 
@@ -2602,6 +2663,41 @@ thumb and at phone width.
2602
2663
  ]} />
2603
2664
  ```
2604
2665
 
2666
+ ## Review pieces (unreleased, after 0.49.0): `StagePath`, `StatusMatrix`, `TranslationRow`, `StatusList`
2667
+
2668
+ GemPress's redesign (Chris approved it 23/10/2026) drew four things with its own CSS (`gp-unlock`, `gp-cov`, `gp-str`, `gp-sys`)
2669
+ that are not about GemPress: a path of stages that unlock in order, a map of "how much is live" per row and column, one string
2670
+ under review, and a strip of system health. GemNews and GemCommunity translations review the same way, and every service has a
2671
+ health line to show, so they live here. Each is its own entry, presentational and server-renderable (no state, no effect, no
2672
+ inline style), painted by tokens (the dark palette follows), and usable from plain HTML: link `@gemboss/ui/<name>.css` beside the
2673
+ core and write the classes listed at the top of that file. The tests hold those lists to the components' markup.
2674
+
2675
+ | Piece | What it draws |
2676
+ | --- | --- |
2677
+ | `StagePath` | `stages` (`{ key, name, done, total, unit?, href? }`) as an `<ol>` of cards in a grid (`repeat(auto-fit, minmax(190px, 1fr))`): a number badge (a green ✓ once `done >= total`; a stage with nothing in it is done), the name (a link when `href`, the whole card its target), "3 / 35 pages approved" and a thin `<progress>` bar (hidden from a screen reader: the words say it). `current` (default the first stage not done, `null` for none) has an ink outline and an ink number, and is `aria-current="step"`. `label` names the list (default "Unlock path"); `caption` and `format` make the words the surface's. `stagePathDone` is the rule, pure. |
2678
+ | `StatusMatrix` | the kit table (`ui-table-wrap` + `ui-table`) with `columns` across and `rows` down, each row's name its `th scope="row"` (a link when `href`). A cell is "**live** / total" with a second line for what waits ("5 draft · 2 changed", "3 missing"); its tone is all live (green number) or has drafts (amber number), never colour alone. A `null` or absent cell, or a row whose `total` is 0, is "—". `rowHeading`, `totalHeading` (a column of the rows' totals, e.g. the source language), `caption` (what the numbers mean, under the table), `format`, `labels`. `statusMatrixTone` is the rule, pure. |
2679
+ | `TranslationRow` | one string: a head (`keyName` in code, `state` as a kit badge: `draft` "To review", `approved`, `stale` "Source changed", `missing` "Not translated"; `by`), the `source` beside the `target` (two columns; one under 900px, or when the row is narrower than 600px: it is a size container), and a foot with `checks` (`{ ok, text }`: a green "✓ …" or the problems in amber words) on the left and the surface's `action` on the right. The editable control is the surface's: `target` is a node, or a function given the ids of the key, the source and the checks so the field can say `aria-describedby`. `current` draws an ink border and a 3px ink bar inside the left edge (`aria-current`). The row is `tabIndex={-1}`: a J / K list moves focus to it (pass `id`, or find it by `data-*` the row passes through), and it is never a Tab stop. `as="li"` inside an `<ol>`. |
2680
+ | `StatusList` | `items` (`{ key, label, text, tone, href? }`) as a grid (`repeat(auto-fill, minmax(260px, 1fr))`): a dot, a bold name, one muted sentence (a link when `href`). `ok`, `warning`, `critical` fill the dot with the success, warning and danger tokens; `muted` is an empty ring (off, not applicable); an unknown tone is muted. |
2681
+ | `stage-path.css`, `status-matrix.css`, `translation-row.css`, `status-list.css` | one entry each, loaded by its component; `status-matrix.css` needs the core's table (`ui.css`), and `TranslationRow`'s badge tones are the core's (`kit-plain.css`, in `components.css`) |
2682
+
2683
+ **Quiet when fine, a banner is for when a person must act.** The status list is the line a page ends on: nothing in it asks for
2684
+ anything. Something a person must do today goes in a `ui-banner` above the page, not in an amber dot at the bottom of it.
2685
+
2686
+ ```tsx
2687
+ import { StagePath, StatusMatrix, TranslationRow, StatusList } from "@gemboss/ui";
2688
+
2689
+ <StagePath label="Unlock path" stages={[
2690
+ { key: "frame", name: "Header & footer", done: 2, total: 2, unit: "parts" },
2691
+ { key: "pages", name: "Pages", done: 3, total: 35, unit: "pages", href: "#/review?stage=pages" },
2692
+ ]} />
2693
+ <StatusMatrix rowHeading="Collection" totalHeading="EN" caption="Live / published." columns={[{ key: "vi", label: "VI" }]}
2694
+ rows={[{ key: "pages", label: "Pages", total: 35, cells: { vi: { live: 30, draft: 5, changed: 2 } } }]} />
2695
+ <TranslationRow as="li" id="vi-hero" keyName="hero.title" state="draft" source="Ship in 3 days" sourceLang="en" by="gpt-4o" current={at === 0}
2696
+ target={(ids) => <textarea className="ui-textarea" lang="vi" aria-describedby={`${ids.source} ${ids.checks}`} value={text} onChange={…} />}
2697
+ checks={{ ok: true, text: "Numbers and tags match" }} action={<button className="ui-btn ui-btn--sm" onClick={approve}>Approve</button>} />
2698
+ <StatusList label="System" items={[{ key: "backups", label: "Backups", text: "nightly · last 22 Oct", tone: "ok", href: "#/settings" }]} />
2699
+ ```
2700
+
2605
2701
  ## Collections (0.13.0): tables, lists, grids, boards
2606
2702
 
2607
2703
  Chris settled these on 05/10/2026 after an inventory of 136 hand-made collections on 11 surfaces (86 tables scrolling
@@ -2643,17 +2739,28 @@ the admin's first load (`kit-plain-bundles.test.ts` checks the shell import carr
2643
2739
 
2644
2740
  ### The rules, and what checks them
2645
2741
 
2742
+ - **One header for both tables (23/10/2026).** An index table's header is the data table's: the surface, 13px regular, 12px
2743
+ above and below (45 tall), opaque so the sticky headers hide what scrolls under them; its cells 8px 6px. The index table
2744
+ takes these from the data table's rules (ui.css), so the two cannot drift. Polaris draws an index header as a grey 12px /
2745
+ 550 band over 6px cells; the admin's unselectable Polaris IndexTables (all of them on 23/10/2026) take the kit's instead
2746
+ (gemboss-shell.css, #5185).
2747
+
2646
2748
  - **The whole row opens the item; the title stays a real link** (Cmd-click, a screen reader). A click on a checkbox, a
2647
2749
  button, the ⋯ menu, a select or a second link does only that; so does a click that ends a text selection. A row with
2648
2750
  two or more in-place editors is not linked (only its title is). HTML: `tr[data-href]` + kit-plain.js; React:
2649
2751
  `IndexTable` rows with `href`; a resource list's title link covers its row (no script).
2752
+ - **The referrer is the page's to set, not the row's.** A row opens with `noopener` and the browser's default policy
2753
+ (`strict-origin-when-cross-origin`): another site learns this site's origin, never the path. A surface whose rows open
2754
+ other sites and must send nothing sets it once for every link: `Referrer-Policy: same-origin` (header) or
2755
+ `<meta name="referrer" content="same-origin">`. Per-link `noreferrer` in the kit was tried (#5176, then #5180's first
2756
+ draft) and dropped: it missed ways in (a Cmd-click straight on the title is the browser's) and every other link type.
2650
2757
  - **One visible button per row at most**, the rest in the ⋯ menu (`ui-menu`). **Never shown only on hover.**
2651
2758
  - **An open ⋯ menu is never cut by its frame (0.18.0).** It floats in the browser's top layer (`popover="manual"`,
2652
2759
  `position: fixed` against its button): below the button, above it when the room below is short and the room above is
2653
2760
  larger, 8px from the viewport's edges, scrolling inside when taller than the room. It closes when a scroll moves its
2654
2761
  button (the page, or a frame around the menu) or the window resizes; a scroll inside the list or elsewhere (a log
2655
- following its output) does not count. Tab order, Escape (focus back on ⋯) and "a click in the
2656
- menu is not a click on the row" are unchanged: the list is still the details' child. React `Menu` does it itself;
2762
+ following its output) does not count. The keys (README "Menu", 23/10/2026: arrows, Escape with focus back on ⋯) and "a
2763
+ click in the menu is not a click on the row" are unchanged: the list is still the details' child. React `Menu` does it itself;
2657
2764
  plain HTML gets it from kit-plain.js (`initMenuLayer()`, runs by itself). No popover API (Safari < 17) or no script:
2658
2765
  the absolute list, as before. GemFactory Members (#197) is the case: at 820 / 900 / 1280 the last rows' menus were
2659
2766
  half hidden inside the table's scrolling frame.
@@ -2667,7 +2774,8 @@ the admin's first load (`kit-plain-bundles.test.ts` checks the shell import carr
2667
2774
  activity's detail wrap instead of ending in "…", because the cut text's `title` is mouse-only. A side rail of short
2668
2775
  labels keeps one line. `CompactItem.body` (activity only) is the event's own text under them (`.ui-list__content`,
2669
2776
  body size, never cut; paragraphs as `<p>`); `labelText` puts the full label in `title`.
2670
- - **Below 768 of its container an index table becomes a list** (title line; the other cells on one caption line,
2777
+ - **Below 768 of its container an index table becomes a list** (title line: a linked title is cut, an unlinked one and free
2778
+ text under it wrap, a long token breaks; the other cells on one caption line,
2671
2779
  `data-label` prefixes, `data-condensed="hide"` drops; checkbox and actions kept; rows ≥ 44); sort moves to the chips
2672
2780
  of `ui-index__sortbar`. **A data table never folds:** it scrolls in its frame with `--sticky-first`.
2673
2781
  - **A data table reads from the top of its rows (0.16.0)**, as Polaris DataTable (`verticalAlign` "top"): one-line
@@ -2678,7 +2786,8 @@ the admin's first load (`kit-plain-bundles.test.ts` checks the shell import carr
2678
2786
  `minWidth` (DataTable) is the floor of a column whose cells wrap, and replaces the row header's (`0` for a row
2679
2787
  header that is always short: a date, an id). A link alone on its
2680
2788
  line is 24px in the row header and in a list inside a cell (`li > a`), as `td > a` already was.
2681
- - **A data table in a card is `flush` (0.34.0)**, as a resource list in a card: the card is the frame, so the wrap drops its
2789
+ - **A data table or an index table in a card is `flush`** (DataTable 0.34.0; IndexTable unreleased, its bulk bar
2790
+ then lies on the card's top edge), as a resource list in a card: the card is the frame, so the wrap drops its
2682
2791
  own border, radius and shadow (two frames otherwise). The last row has no bottom rule, row header included. The card
2683
2792
  clips its corners (`overflow: hidden`, as GemWatcher's Panel): the last row's hover, the totals and the footer are square.
2684
2793
  - **A media card is 16:10, 4:3 or 1:1 (0.18.0)** (`MediaCard ratio`, `data-ratio="4/3" | "1/1"`): 1:1 for a square
@@ -2766,6 +2875,11 @@ gap. Under 576px only Previous · the current page · Next stay and the range wr
2766
2875
  took three to four rows at 320). The old form `<span aria-current="page">2 / 9</span>` keeps its muted look. The surface computes which numbers to
2767
2876
  show (first, last, current ± 1, a gap only where it hides two or more pages: GemFactory `pageWindow`).
2768
2877
 
2878
+ React: `<Pager page pageCount range href={(n) => \`?p=${n}\`} linkAs={Link} />` draws that markup (`pageWindow` is exported
2879
+ too). A list that pages in STATE (its search and filters not in the URL, as the admin's Products) passes `onPage` instead of
2880
+ `href` and gets buttons with the same look (pager-extras.css), Previous / Next marked `data-rel`; `busy` while a page is on its
2881
+ way. The rules are `pager.css` (in the core, moved out of kit-plain.css 24/10/2026 with Chris's OK) and `pager-extras.css`.
2882
+
2769
2883
  `ui-more`: one centred row. Idle: a `ui-btn` ("Show 24 more"). Loading: `<span class="ui-spinner" aria-hidden="true">` +
2770
2884
  `<span role="status">Loading…</span>`. Error: `<span role="alert">Couldn't load.</span>` + "Try again". End: a line. As an
2771
2885
  IntersectionObserver sentinel it has height from its padding; zero the padding and it has none.
@@ -2817,10 +2931,12 @@ something the base cannot express, add a token or a prop here, and every surface
2817
2931
  | Path | What |
2818
2932
  |---|---|
2819
2933
  | `parts/core.css`, `parts/components.css`, `parts/shell.css` | 0.23.0: the core, built by `packages/ui/scripts/build-css.mjs` from the same sections as `gemboss-ui.css` (see "Load only what you use"). |
2820
- | `gemboss-ui.css` | built at stage time and shipped minified (0.19.0); since 0.23.0 the COMPATIBILITY BUNDLE, frozen: `apps/admin/scripts/export-ui-css.mjs` (tokens + kit + field + `packages/ui/src/shell.css`, with Polaris `--p-*` resolved to their fallbacks) + `packages/ui/src/app-shell.css` + `auth.css` + `assistant.css` + `kit-plain.css` + `chat.css` + `chat-composer.css` (`packages/ui/scripts/build-css.mjs`) |
2934
+ | `gemboss-ui.css` | built at stage time and shipped minified (0.19.0); since 0.23.0 the COMPATIBILITY BUNDLE, frozen: `apps/admin/scripts/export-ui-css.mjs` (tokens + kit + field + `packages/ui/src/shell.css`, with Polaris `--p-*` resolved to their fallbacks) + `packages/ui/src/app-shell.css` + `auth.css` + `assistant.css` + `menu.css` + `kit-plain.css` + `chat.css` + `chat-composer.css` (`packages/ui/scripts/build-css.mjs`) |
2821
2935
  | `packages/ui/src/shell.css` | the SHARED SHELL (rail, topbar, main, mobile drawer, the assistant's thread rail, popover surface, account chip). Exported as `@gemboss/ui/shell-source.css` since 0.23.0 (`@gemboss/ui/shell.css` is the built part). The admin's `gemboss-shell.css` @imports it on line 2; the export refuses a shared-shell rule in the admin's sheet or an admin-only rule here (one owner). |
2822
- | `packages/ui/src/kit-plain.css` | primitives for surfaces that render plain HTML, not React (0.2.1): menu, key/value list, icon button; 0.3.0 adds `ui-link` / `ui-prose`, the media card (`ui-card--media`, `__media`, `__body`, `__foot`) and the popup's `ui-modal__body` / `__foot`. Shipped inside `gemboss-ui.css`; the admin does not import it (its first-load CSS budget has ~0.1 KB headroom).; (0.4.0) report pages: delta, large KPI grid, bar list, column chart, legend, funnel, card grid, section head. |
2823
- | `packages/ui/src/kit-plain.js` | 0.3.0: the behaviour those surfaces each wrote by hand: `gembossUI.openModal` (Escape, backdrop, Tab trap, stacking, ask-before-close, focus back), `toast` (text, error tone), `initMenus` (outside click, item, blur, Escape closes the menu before the popup); 0.5.0: `confirm`, `initShell`, `initPasswordFields` / `resetPasswordFields`. A classic script: inline it or `<script src>`; see "Plain-HTML behaviour" above. |
2936
+ | `packages/ui/src/menu-plain.js` | 23/10/2026: `gembossUI.initMenuKeys()`, a menu's roles and keys and one menu open at a time for plain HTML (`@gemboss/ui/menu-plain.js`), the same as `menu-keys.ts`. After `kit-plain.js`; runs by itself. See "Plain-HTML behaviour". |
2937
+ | `packages/ui/src/menu.css` | 23/10/2026: the ⋯ menu and its icon button, moved out of `kit-plain.css` (same rules, same order in the core). Imported by `Menu`; see "Menu". |
2938
+ | `packages/ui/src/kit-plain.css` | primitives for surfaces that render plain HTML, not React (0.2.1): key/value list (the menu and the icon button are `menu.css` since 23/10/2026); 0.3.0 adds `ui-link` / `ui-prose`, the media card (`ui-card--media`, `__media`, `__body`, `__foot`) and the popup's `ui-modal__body` / `__foot`. Shipped inside `gemboss-ui.css`; the admin does not import it (its first-load CSS budget has ~0.1 KB headroom).; (0.4.0) report pages: delta, large KPI grid, bar list, column chart, legend, funnel, card grid, section head. |
2939
+ | `packages/ui/src/kit-plain.js` | 0.3.0: the behaviour those surfaces each wrote by hand: `gembossUI.openModal` (Escape, backdrop, Tab trap, stacking, ask-before-close, focus back), `toast` (text, error tone), `initMenus` (outside click, item with focus back on ⋯ first, blur, Escape closes the menu before the popup); 0.5.0: `confirm`, `initShell`, `initPasswordFields` / `resetPasswordFields`. A classic script: inline it or `<script src>`; see "Plain-HTML behaviour" above. |
2824
2940
  | `packages/ui/src/collections.css` | 0.13.0: tables, lists, grids, boards and their states (see "Collections"). A separate entry, NOT inside `gemboss-ui.css`; the admin does not load it. |
2825
2941
  | `packages/ui/src/editor/` | 0.14.0, `@gemboss/ui/editor`: `EditorFrame`, `EditorPaneTabs`, `EditorTopbar`, `EditorBack`, `EditorChatToggle`, `EditorSaveState`, `DeviceToggle`. See "Editor frame". 0.17.0: `PanelTabs`, `PanelHeader`, `PanelAction`, `InspectorGroup`, `Field`, `Seg`, `ColorControl`, `RangeControl`, `EdgeBox`, `ImageControl`, `ChipButton`, `swatchState` (`./editor/swatch` too); 0.20.1 `cssUrl`. See "Settings panel". 0.22.0: `LayerList`, `SortableLayerList`, `LayerTree`, `EditorDrawer`. See "Layers and drawer". |
2826
2942
  | `packages/ui/src/editor-plain.js` | 0.14.0: `gembossUI.initEditorFrame()`, the frame's behaviour for plain HTML; 0.17.0: `gembossUI.initEditorPanel()`, the settings panel's. Load after `kit-plain.js`. |
@@ -8,7 +8,7 @@
8
8
  .cat-code summary{padding:10px 16px;font-size:13px;cursor:pointer;color:var(--gemboss-text-muted);border-top:1px solid var(--gemboss-divider);min-height:24px}
9
9
  .cat-code pre,.cat-api{margin:0;padding:12px 16px;overflow:auto;background:#0f1012;color:#e8e8e6;font-size:12px;line-height:1.5;border-radius:0}
10
10
  .cat-api{border-radius:12px;margin:0 0 16px}</style></head><body><div class="cat">
11
- <h1>@gemboss/ui 0.48.0 · Editor</h1>
11
+ <h1>@gemboss/ui 0.50.0 · Editor</h1>
12
12
  <p>Khung của mọi editor: <code>EditorFrame</code> (cột trái · khe kéo · preview · cột phải dock trên mép phải preview) và <code>EditorTopbar</code> (← · tiêu đề · cụm giữa · cụm phải · dòng trạng thái). Là đúng khung của theme / page / section editor trong admin. Gói chỉ vẽ KHUNG; chat, Configure, canvas, page picker, menu, Publish là của editor, đi vào bằng slot. Trang HTML thuần nạp <code>editor.css</code> + <code>editor-plain.js</code> rồi gọi <code>gembossUI.initEditorFrame()</code>: kéo khe (chuột, chạm, phím mũi tên / Home / End), đổi thiết bị, mở ⋯ và Publish ▾, ẩn cột trái, Chat | Preview trên điện thoại. <a href="index.html">← Catalog</a></p>
13
13
  <h2>React</h2><pre class="cat-api"><code>import &quot;@gemboss/ui/editor.css&quot;;
14
14
  import { EditorFrame, EditorTopbar, EditorBack, EditorChatToggle, EditorPaneTabs, EditorSaveState, DeviceToggle } from &quot;@gemboss/ui/editor&quot;;