@gemboss/ui 0.21.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,151 @@
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
+
5
150
  ## 0.21.0 (07/10/2026)
6
151
 
7
152
  **Reactions: one emoji set and one chip for the fleet.** GemCare's team chat reactions, moved into the kit so GemFeedback
@@ -68,8 +213,13 @@ Fixed
68
213
  - `.ui-seg svg` filled a stroke icon solid (`fill: currentColor` over `fill="none"`): the monitor and phone glyphs drew
69
214
  as blocks. A `fill="none"` svg keeps its outline.
70
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
+
71
219
  Removed
72
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
+
73
223
  ## 0.19.0 (06/10/2026)
74
224
 
75
225
  **The published CSS and plain scripts are minified, and each file has a wire-weight ceiling.** README "Minified on
package/README.md CHANGED
@@ -11,7 +11,10 @@ npm install @gemboss/ui
11
11
  ```
12
12
 
13
13
  ```tsx
14
- import "@gemboss/ui/gemboss-ui.css";
14
+ // The core, once, in the root layout, FIRST and in this order (0.23.0; "Load only what you use" below):
15
+ import "@gemboss/ui/core.css";
16
+ import "@gemboss/ui/components.css";
17
+ import "@gemboss/ui/shell.css";
15
18
  import { AppShell, RailItem, RailGroup, RailHeading, BrandLockup } from "@gemboss/ui";
16
19
 
17
20
  <AppShell
@@ -40,6 +43,88 @@ Surfaces with no React (gemcare, gempress) use the CSS alone and write the same
40
43
  `nav.icon-nav > .gnav-scroll > .gnav-group > a.gnav-item > span.gnav-ico + span.gnav-label`.
41
44
  `packages/ui/src/shell.tsx` is the reference.
42
45
 
46
+ ## Load only what you use: the core and the entries (0.23.0)
47
+
48
+ Every surface used to load `gemboss-ui.css` whole and use 14–21% of it (measured 06/10/2026 on twelve surfaces). Since
49
+ 0.23.0 the CSS comes in parts (`packages/ui/scripts/parts.mjs` is the list): a CORE every surface loads once, and ENTRIES that
50
+ come with the component that draws them. A React component imports its own entry (`auth.tsx` imports `auth.css`), so
51
+ a Next or Vite surface downloads an entry only on the routes that render it, and a component it never imports takes
52
+ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as published:
53
+
54
+ | Part | Import (Node / bundler) | File in the tarball | What | gzip | Who loads it |
55
+ |---|---|---|---|---:|---|
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 |
58
+ | 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
+ | auth | `@gemboss/ui/auth.css` | `packages/ui/src/auth.css` | the sign-in screen | 2.6 KB | `AuthLayout` & co. import it |
60
+ | assistant | `@gemboss/ui/assistant.css` | `packages/ui/src/assistant.css` | the dock and the launcher | 2.2 KB | `AssistantHost`, the launcher import it |
61
+ | portal | `@gemboss/ui/portal.css` | `packages/ui/src/portal.css` | `TopbarNav`, `SearchDialog`, `Avatar`, `Leaderboard`, `Podium`, the `ThemeToggle` button | 2.1 KB | those components import it |
62
+ | theme-dark | `@gemboss/ui/theme-dark.css` | `packages/ui/src/theme-dark.css` | the dark palette | 0.6 KB | `ThemeToggle` and `themeScript` (`theme-script.ts`, without portal.css) import it |
63
+ | collections | `@gemboss/ui/collections.css` | `packages/ui/src/collections.css` | tables, lists, grids, boards | 4.6 KB | `IndexTable`, `ResourceList`, `Board`… import it |
64
+ | editor | `@gemboss/ui/editor.css` | `packages/ui/src/editor.css` | the editors | 7.9 KB | the `@gemboss/ui/editor` components import it |
65
+ | reactions | `@gemboss/ui/reactions.css` | `packages/ui/src/reactions.css` | the emoji reactions and their picker | 0.8 KB | `Reactions` / `ReactionPicker` import it |
66
+ | 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 |
67
+ | 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 surface (no package component draws it) |
68
+ | bundle | `@gemboss/ui/gemboss-ui.css` | `gemboss-ui.css` | core + components + shell + auth + assistant + chat + portal + theme-dark | 21.7 KB | compatibility only (see CHANGELOG 0.23.0) |
69
+
70
+ 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).
71
+
72
+ **Next.js / Vite.** The core's three imports go at the top of the root layout, above any component import, in the
73
+ order above. Entries need nothing: the components bring them. A page that writes chat markup itself imports
74
+ `@gemboss/ui/chat.css`. Drop `gemboss-ui.css` IN THE SAME BUMP: kept beside the parts, the entries a component brings
75
+ load a second time, after your own CSS, and a second copy of a rule you override wins again.
76
+
77
+ ```tsx
78
+ // app/layout.tsx
79
+ import "@gemboss/ui/core.css";
80
+ import "@gemboss/ui/components.css";
81
+ import "@gemboss/ui/shell.css";
82
+ import "./globals.css"; // your own CSS after the core
83
+ ```
84
+
85
+ Add `"@gemboss/ui"` to `experimental.optimizePackageImports` in `next.config` (the admin does, 0.23.0): a SERVER
86
+ component that imports from the index otherwise collects the CSS of every module the index re-exports (the admin's
87
+ `/login`, which imports `AuthHelp` in its server page, picked up portal.css and collections.css under webpack until it did).
88
+
89
+ Which bundler drops what (measured 06/10/2026): Next 16 (Turbopack) leaves out the entry of a component a route does
90
+ not render. content-radar on 0.23.0 with the core: `/v2` 38.5 → 32.3 KB gzip of CSS, `/login` 31.2 → 25.4 (auth.css
91
+ only there, portal.css nowhere). esbuild keeps the CSS of every module the index reaches even when it drops their JS, so
92
+ through `@gemboss/ui` it ships every entry the index re-exports; and esbuild with no CSS loader now stops at a
93
+ component's `import "./x.css"`: give it `loader: { ".css": "empty" }` (you link the CSS yourself, as GemFactory's
94
+ gbui.mjs and this package's tests do) or `"css"`.
95
+
96
+ **Plain HTML** (a server that serves the files out of `node_modules`): link the core, then only the entries the page
97
+ draws.
98
+
99
+ ```html
100
+ <link rel="stylesheet" href="/ui/core.css"> <!-- require.resolve("@gemboss/ui/core.css") -->
101
+ <link rel="stylesheet" href="/ui/components.css"> <!-- require.resolve("@gemboss/ui/components.css") -->
102
+ <link rel="stylesheet" href="/ui/shell.css"> <!-- require.resolve("@gemboss/ui/shell.css") -->
103
+ <link rel="stylesheet" href="/ui/auth.css"> <!-- the sign-in page only -->
104
+ <link rel="stylesheet" href="/ui/collections.css"> <!-- list pages only -->
105
+ ```
106
+
107
+ **Order.** The core loads FIRST, and its three parts in that order: the top lines of the root layout, or the first three
108
+ `<link>`s. `shell.css` restyles the field look at phone width, and entries restyle the kit with BEM modifiers at the
109
+ kit's own weight (`.ui-empty--no-match` after `.ui-empty`): both are written to follow what they modify, which keeps
110
+ every rule at the weight your own CSS can override. After the core, the entries may land in any order:
111
+ `packages/ui/scripts/cascade-check.mjs` loads the catalog with the entries (collections.css and editor.css included) after the core,
112
+ in order and reversed, and holds every element's computed style to the bundle's at 1280 and 390 (`parts-cascade.test.ts`,
113
+ CI; negative control: two entries that only agree by order). An entry loaded BEFORE the core is not supported: measured,
114
+ `.ui-empty--no-match` loses its 32px padding to `.ui-empty`. Two kit-plain.css rules that overrode shell.css by coming
115
+ 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
116
+ weight, so they still follow it.
117
+
118
+ **A new component** gets its own entry: a file in `src/`, listed in `ENTRIES` in `packages/ui/scripts/parts.mjs`, exported,
119
+ imported by the component that draws it, with a ceiling in `weight-budget.json`. It never goes into the core or into
120
+ `gemboss-ui.css`: `packages/ui/scripts/parts-gate.mjs` (CI) holds both frozen, fails on a stylesheet nobody classified, and on a
121
+ component that draws an entry's class without importing it.
122
+
123
+ **The admin** is unchanged: it has Polaris and its own order, so it imports the sources one by one
124
+ (`gemboss-tokens.css`, `ui.css`, and `shell-source.css` from its `gemboss-shell.css`; `@gemboss/ui/shell.css` is the
125
+ built part now). Its editor, assistant and sign-in modules already imported the same entry files the components now
126
+ import, so nothing moves.
127
+
43
128
  ## Page width (0.8.1)
44
129
 
45
130
  Chris, 04/10/2026: the rail on the left, and on the right a content column with the same max width on every
@@ -187,6 +272,44 @@ Its styles are the `gemboss-assistant-*` rules in `packages/ui/src/shell.css` (t
187
272
  @imports too), and they reach you through `gemboss-ui.css` together with `gemboss-field.css`. The admin passes a second tab ("Talk to a person", gemcare) through `secondTab` and its button
188
273
  through `fullActions`; a surface without those leaves them out.
189
274
 
275
+ ## The engine picker (0.25.0): `EnginePicker`, `BridgeNotice`
276
+
277
+ Which engine answers the assistant, and the bridge on the user's machine, without spending the chat's space on it
278
+ (Chris 07/10/2026: content-radar's assistant spent close to half its frame on an ENGINE / MODEL / EFFORT row, an
279
+ "outdated bridge" block and the install command). The admin's shape: a chip in the composer row opens a popup.
280
+
281
+ ```tsx
282
+ import { EnginePicker, BridgeNotice, useEnginePicker } from "@gemboss/ui/assistant"; // brings assistant-engine.css
283
+
284
+ const picker = useEnginePicker(); // open + tab, shared with the notice
285
+ <EnginePicker {...picker.props}
286
+ engines={[{ id: "claude", label: "Claude Code", note: "Your Claude subscription" }, { id: "server", label: "Server" }]}
287
+ engine={engine} onEngineChange={setEngine}
288
+ models={[{ id: "", label: "Default" }, { id: "opus", label: "Opus" }]} model={model} onModelChange={setModel}
289
+ efforts={[{ id: "", label: "Default" }, { id: "high", label: "High" }]} effort={effort} onEffortChange={setEffort}
290
+ bridge={{ state: "outdated", version: "8", latest: "10", autostart: true, detail: "Codex cannot see the radar tools." }}
291
+ setup={{ command: installCommand, pairingCode, note: "The panel reconnects in a few seconds." }}
292
+ statusActions={<button className="ui-btn" onClick={recheck}>Check again</button>} />
293
+
294
+ // Only when a turn FAILED because of the bridge, one line in the stream:
295
+ <BridgeNotice onOpenSetup={() => picker.openTab("setup")}>The bridge on your machine is not responding.</BridgeNotice>
296
+ ```
297
+
298
+ - **Chip**: engine name, the model when it is not the default (`id: ""`), a dot for `bridge.state`: green `running`,
299
+ amber `outdated`, red `offline` / `unpaired`, hollow `checking`. Its accessible name says the state in words.
300
+ - **Popup**: a non-modal dialog in the top layer (placed like the kit's menus, `placeFloating`), so the assistant's frame
301
+ cannot cut it. Tabs: Engine always; Setup when `setup` is given; Status when `bridge` is given. ←/→/Home/End move
302
+ between tabs; ↑/↓ move between engines (one Tab stop). Escape with focus in the panel, or a model / effort pick from
303
+ the buttons, closes it and gives focus back to the chip; an outside click closes. An engine pick and a pick from a
304
+ select (more than six models, or grouped ones) keep it open. Closing goes back to the Engine tab. 44px targets under a coarse pointer and at phone width.
305
+ - **Not in the chat**: no standing warning, no command in the conversation. The amber dot on the chip and on the Setup
306
+ tab is the warning. `BridgeNotice` is for a turn that actually failed, once.
307
+ - The package knows no bridge: the surface polls its own, computes `outdated`, builds the command, and owns the
308
+ behaviour of every callback. `EnginePanel` is the same tabs without the chip (catalog, a settings page).
309
+
310
+ Tests: `packages/ui/src/assistant/engine.test.tsx` (markup, styles, its own entry), `packages/ui/src/assistant/engine-browser.test.tsx`
311
+ (Chromium: top layer through a clipping frame, keys, focus, copy, Open setup, 44px; with negative controls).
312
+
190
313
  ## The chat body
191
314
 
192
315
  What an answer looks like, on any surface that puts a chat somewhere: `chat.css` (inside `gemboss-ui.css`;
@@ -243,8 +366,9 @@ import "@gemboss/ui/editor.css"; // only on the screens that show an editor
243
366
  Not here, on purpose: the body modes that hide the admin's own topbar and rail while an editor is open
244
367
  (`gemboss-editor-fullscreen`, `-just-exited`, `-layer-open`) and admin-only extensions such as the product studio's
245
368
  resizer. The frame and its top bar are React components since 0.14.0 ("Editor frame" below), with their behaviour for
246
- plain HTML in `editor-plain.js`; the panel and its controls are components since 0.17.0 ("Settings panel" below, step 3); step 4 is `LayerList`,
247
- `EditorDrawer`. Step 5 moves GemFactory's Builder onto them.
369
+ plain HTML in `editor-plain.js`; the panel and its controls are components since 0.17.0 ("Settings panel" below, step 3); the
370
+ layers, the element outline and the drawer since 0.22.0 ("Layers and drawer" below, step 4). Step 5 moves GemFactory's
371
+ Builder onto them.
248
372
 
249
373
  Changes since the verbatim move, in 0.10.1 (05/10/2026, step 1b, the only ones that change what the admin shows; CHANGELOG):
250
374
 
@@ -391,6 +515,51 @@ named (it bubbles):
391
515
  | Edges: Enter blurs; a box that loses focus sends all four | `.gemboss-ei-edgebox` | `gemboss:editor-edges` `{ t, r, b, l }` |
392
516
  | Image: commits on blur / Enter when changed; the thumbnail follows; a chip with `data-gb-image-clear` empties it | `.gemboss-ei-img` | `gemboss:editor-image` `{ value }` |
393
517
 
518
+ ## Layers and drawer (0.22.0): `LayerList`, `SortableLayerList`, `LayerTree`, `EditorDrawer`
519
+
520
+ The page editor's Layers, Page settings and Edit content drawers and the element inspector's Elements outline, as
521
+ components (`@gemboss/ui/editor`). The admin draws those four with them since 0.22.0, measured before and after on a
522
+ production build: the same computed style and box, the markup differing only in `data-id` on each row, `type="button"`
523
+ on the buttons the components draw, and `aria-current` on the selected layer (CHANGELOG has the numbers).
524
+
525
+ ```tsx
526
+ import { EditorDrawer, LayerList, LayerTree, SortableLayerList } from "@gemboss/ui/editor";
527
+
528
+ {layersOpen ? (
529
+ <EditorDrawer side="left" label="Page layers" title="Layers" onClose={() => setLayersOpen(false)}>
530
+ <SortableLayerList items={sections.map((s) => ({ id: s.id, title: s.type, subtitle: s.heading, hidden: s.hidden }))}
531
+ selectedId={picked} onSelect={(it) => setPicked(it.id)}
532
+ onMove={(it, to) => moveSection(it.id, to)} onToggleHidden={(it, hidden) => setHidden(it.id, hidden)}
533
+ actions={(it) => <button aria-label="Delete" onClick={() => remove(it.id)}>✕</button>} empty="No sections yet" />
534
+ </EditorDrawer>
535
+ ) : null}
536
+ ```
537
+
538
+ | Component | Markup | Behaviour |
539
+ |---|---|---|
540
+ | `LayerList` | `.gemboss-layers-list` › `.gemboss-layer[data-id]` (`.is-sel`) › `button.gemboss-layer-main` (`.gemboss-layer-type` · `.gemboss-layer-head`) · `.gemboss-layer-acts` | A click selects (`onSelect`; the selected button says `aria-current`). Rows are 34px, 50px with a subtitle. `empty` shows in `.gemboss-picker-empty`. `onToggleHidden`: an eye first among the actions (named "Hide <name>", `aria-pressed` = hidden, the row faded). `depth` per item indents it (`indent` px per level, default 14). Stateless. |
541
+ | `SortableLayerList` | the same, each row `draggable` with a `.gemboss-layer-grip` | `onMove(item, to)` (`to` = the index after the move): drag a row (faded while dragged, a 2px line where it lands), or Alt+↑ / Alt+↓ on its button (the focus stays on the moved row, also when `onMove` answers later). The drag is kept by id. Its own module (`"use client"`): a list that does not reorder does not carry it. |
542
+ | `LayerTree` | `ul.gemboss-ei-tree[role=tree]` › `li.gemboss-ei-tnode[role=treeitem][data-id]` (`.on`, level / position / selection) › `button.gemboss-ei-tname` (+ `small`) · `.gemboss-ei-tacts` · `below` | A click on the name calls `onSelect`; toggling it off is the caller's. Actions show on hover, on the selected row and on focus. 27px rows, 14px per level. |
543
+ | `EditorDrawer` | `.gemboss-picker-backdrop` + `aside.gemboss-settings` (`side="right"`, min(380px, 100%)) or `aside.gemboss-layers` (`side="left"`, 300px) › `before` · `.gemboss-picker-head` (title, ✕) · children · `.gemboss-settings-foot` (`footer`) | ✕ and the backdrop call `onClose`. Mounted while open. The body is the caller's: `.gemboss-settings-body` › `label.gemboss-settings-field` (`span`, control, `small`). |
544
+
545
+ Drag to reorder is pointer only (HTML drag and drop does not start from touch); a list that must reorder on a phone
546
+ keeps ↑ / ↓ buttons in its `actions`, as the admin's does. Kept as the admin had them: the drawer has no Escape and
547
+ does not move the focus into itself or back; the admin's Layers do not drag and have no eye (they move with ↑ / ↓).
548
+
549
+ ### Plain HTML: `initEditorLayers()`
550
+
551
+ The same markup (copy it from `catalog/editor.html`), then `gembossUI.initEditorLayers()` (`editor-layers-plain.js`, its own file:
552
+ load it only where layers or drawers are, after `kit-plain.js`; the whole document, or `initEditorLayers(element)`). A drawer stays in the document while closed:
553
+ `hidden` on the aside and on the backdrop before it, an `id` on the aside.
554
+
555
+ | Wires | Markup | Event |
556
+ |---|---|---|
557
+ | Select a layer (`.is-sel`, `aria-current`) | `.gemboss-layer-main` | `gemboss:editor-layer` `{ id, index }` on the row |
558
+ | Hide / show (`aria-pressed`, `.is-hidden`, the eye's slash) | `.gemboss-layer-eye` | `gemboss:editor-layer-hidden` `{ id, hidden }` |
559
+ | Reorder: drag a row, or Alt+↑ / Alt+↓ on its button; the row moves in the document and keeps the focus | `.gemboss-layer[draggable=true]` | `gemboss:editor-layer-move` `{ id, from, to }` |
560
+ | Select an outline node; a second click clears it | `.gemboss-ei-tree .gemboss-ei-tname` | `gemboss:editor-tree` `{ id }` (`null` when cleared) |
561
+ | Open a drawer (closing any other open one) / close it (✕ in its head, `data-gb-drawer-close`, the backdrop) | `[data-gb-drawer="<aside id>"]` | `gemboss:editor-drawer` `{ id, open }` on the aside |
562
+
394
563
  ## Plain-HTML behaviour: `kit-plain.js` (0.3.0)
395
564
 
396
565
  Surfaces without React (GemFactory, gemcare, gempress, partner-radar) render the kit's markup on the server and need
@@ -1086,13 +1255,15 @@ something the base cannot express, add a token or a prop here, and every surface
1086
1255
 
1087
1256
  | Path | What |
1088
1257
  |---|---|
1089
- | `gemboss-ui.css` | built at stage time and shipped minified (0.19.0): `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`) |
1090
- | `packages/ui/src/shell.css` | the SHARED SHELL (rail, topbar, main, mobile drawer, the assistant's thread rail, popover surface, account chip). 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). |
1258
+ | `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"). |
1259
+ | `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`) |
1260
+ | `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). |
1091
1261
  | `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. |
1092
1262
  | `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. |
1093
1263
  | `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. |
1094
- | `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". |
1264
+ | `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". |
1095
1265
  | `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`. |
1266
+ | `packages/ui/src/editor-layers-plain.js` | 0.22.0: `gembossUI.initEditorLayers()`, the layers', the outline's and the drawers' behaviour for plain HTML. Load after `kit-plain.js`. |
1096
1267
  | `catalog/editor.html` | 0.14.0: the frame rendered from the components and running (`packages/ui/scripts/build-catalog-editor.mjs`), linked from `catalog/index.html`. |
1097
1268
  | `packages/ui/src/editor.css` | 0.9.0: the editors (frame, top bar, preview, Configure dock, inspector and controls, Layers, settings drawer, page picker, menus). A separate entry, NOT inside `gemboss-ui.css`; the admin imports it from the editor components, never from a layout. See "Editor CSS" above. |
1098
1269
  | `packages/ui/src/chat.css`, `chat-composer.css`, `assistant.css`, `auth.css`, `app-shell.css` | the chat body, the composer, the assistant frame, the sign-in screen, the topbar slots + base text + the page width tiers (`ui-page`, 0.8.1). All inside `gemboss-ui.css`; the admin @imports `chat.css` and `chat-composer.css` itself. |