@gemboss/ui 0.8.0 → 0.10.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 ADDED
@@ -0,0 +1,42 @@
1
+ # @gemboss/ui changelog
2
+
3
+ Versions up to 0.9.0 are described in the README, each in the section that introduced it.
4
+
5
+ ## 0.10.0 (05/10/2026)
6
+
7
+ Navigation pending: a click on a menu item answers on the first frame, and a page is fetched while the pointer rests on
8
+ its link (Chris, 05/10/2026, from content-radar's rail). README "Navigation pending".
9
+
10
+ Added
11
+ - `@gemboss/ui/next`, a new entry: `NavLink` (next/link that prefetches only on intent: pointer at rest 120 ms,
12
+ touchstart, focus; while its page loads it carries a `role="status"` mark that turns a rail item's icon into the
13
+ spinner), `PendingLink` (a link in the page with a small spinner, `mark="inline" | "corner"`), `PendingIcon`,
14
+ `NAV_DWELL_MS`. `next` >= 15.3 is an optional peer dependency; the main entry does not import it.
15
+ - `RailItem` and `RailSubItem` take `pending` (sets `aria-busy="true"`).
16
+ - `RouteHolding` (main entry, plain component): the one loading boundary a route group renders in its `loading.jsx`
17
+ (title bar + four lines in the content column, `role="status"`). Required wherever links are `NavLink`: without a
18
+ boundary, intent prefetch has nothing to fetch (content-radar 05/10/2026: new screen at 350-780 ms).
19
+ - `RouteCover` (main entry): the same frame over the content area from the click until the navigation commits, shown
20
+ by `html[data-gb-navigating]`, which the `@gemboss/ui/next` links set while pending (no delay, 80 ms fade).
21
+ - `app-shell.css`: the pending look. `.gnav-item` icon becomes a 20px spinner in the same box (the label does not
22
+ move), `.gnav-subitem` gets a 12px spinner in its indent before the label (out of the flow), `cursor: progress`, slower spin under reduced motion;
23
+ `a[data-gb-pending]` + `.gb-pending-mark` for page links; `.gnav-pending` status text.
24
+ - `kit-plain.js`: `initNav()`, which runs by itself when the script loads. Followed rail links (and
25
+ `a[data-gb-pending]`) are marked; links are prefetched (speculation rules where supported, else `<link rel="prefetch">`) after a 120 ms rest of the
26
+ pointer or of keyboard focus, or on touchstart, once per URL; not at all once a client router took a rail click over. Skips modifier clicks, `target`, `download`, same-page hashes, other origins, clicks a handler
27
+ already took; clears on `pageshow`, Escape, `navigateerror`, after 15 s. Opt out: `data-gb-nav="off"`,
28
+ `data-gb-prefetch="off"`. No prefetch inside a Next app or with Save-Data.
29
+ - Catalog section "Chờ điều hướng".
30
+
31
+ Measured, and why the prefetch is a speculation rule (05/10/2026, `_local/fleet/nav-pending/prefetch-reuse.txt`): a
32
+ `<link rel="prefetch">` of a page is reused by the click only when the page is cacheable (max-age > 0). Server-rendered
33
+ fleet pages send max-age=0 / no-cache, so the click fetched the page again, and a prefetch still in flight made the
34
+ click slower (1121 ms to the new page against 737 ms without one, through a 280 ms proxy). A speculation-rules prefetch
35
+ was reused under every Cache-Control tried; `nav-pending.test.tsx` holds both (one server request vs two).
36
+
37
+ Not changed
38
+ - The admin: it keeps its own holding screen and hover prefetch (`NavPending.tsx`, `AppShell.tsx`); none of the new
39
+ CSS or code is in its first load. Moving it over is a later change (README).
40
+
41
+ Number: 0.9.0 is the editor kit (gemboss #3770, merged 05/10/2026, not yet published), so this is 0.10.0 (new API =
42
+ minor). Publish 0.9.0 first or together; check `npm view @gemboss/ui versions` and main before publishing either.
package/README.md CHANGED
@@ -40,6 +40,55 @@ Surfaces with no React (gemcare, gempress) use the CSS alone and write the same
40
40
  `nav.icon-nav > .gnav-scroll > .gnav-group > a.gnav-item > span.gnav-ico + span.gnav-label`.
41
41
  `packages/ui/src/shell.tsx` is the reference.
42
42
 
43
+ ## Page width (0.8.1)
44
+
45
+ Chris, 04/10/2026: the rail on the left, and on the right a content column with the same max width on every
46
+ service, not edge to edge. The tiers are the admin's (`gemboss-shell.css`, `.gemboss-w-*` around a Polaris Page),
47
+ same numbers; `page-width.test.tsx` fails if the admin and the package disagree.
48
+
49
+ | Tier | Class | Max width | For | Admin |
50
+ |---|---|---|---|---|
51
+ | `default` | `ui-page` | `--ui-content-max` (900px) | lists, tables, card grids, dashboards, detail pages | bare Page, `.gemboss-w-gallery`, `.gemboss-w-dashboard` |
52
+ | `feed` | `ui-page ui-page--feed` | 720px | read top to bottom: activity, logs, timelines, a thread | `.gemboss-w-feed` |
53
+ | `form` | `ui-page ui-page--form` | 662px | one column of fields: settings, a create form | `.gemboss-w-form` (Polaris narrowWidth) |
54
+ | `canvas` | `ui-page ui-page--canvas` | 1440px | content measured to be wider than a column of text: a horizontal path, a wide comparison table | `.gemboss-w-canvas` (funnel path, 864px needed) |
55
+ | `full` | `ui-page ui-page--full` | none | a board or an editor that IS the screen. Write the reason next to it. | the editor (fixed, its own frame) |
56
+
57
+ React: one `PageContainer` directly inside `AppShell` caps every screen. Pass the CURRENT route's tier; a page
58
+ cannot widen a cap its parent set, so a screen that needs `canvas` or `full` gets it here, not from a second
59
+ wrapper of its own.
60
+
61
+ ```tsx
62
+ <AppShell rail={rail}>
63
+ <PageContainer width={route === "settings" ? "form" : route === "board" ? "full" : "default"}>{page}</PageContainer>
64
+ </AppShell>
65
+ ```
66
+
67
+ `as="section"` changes the tag. `pageWidthClass("form")` returns the class string for a server template.
68
+ `PageContainer` lives in its own plain module (`packages/ui/src/page.tsx`), not in `shell.tsx`, and `AppShell` has no width
69
+ prop: the admin imports the shell pieces from `shell.tsx`, a client module its bundler keeps whole, and the
70
+ admin's first-load shell budget had no room for even this much JS (240.0 / 240 KB, 04/10/2026).
71
+
72
+ Plain HTML (GemFactory, gemcare, gempress): one wrapper as the first child of main.
73
+
74
+ ```html
75
+ <main class="gemboss-platform-main">
76
+ <div class="ui-page ui-page--form">…</div>
77
+ </main>
78
+ ```
79
+
80
+ Delete the surface's own cap when it moves onto a tier (`maxWidth: 1160` on each gemwatcher screen, a
81
+ `max-width` on a `.container`): two caps on one column means the narrower one wins and nobody can tell which.
82
+
83
+ **A cap only exists where it is SMALLER than the room it sits in.** Right of the 240px rail, inside main's 24px
84
+ padding, the room is `viewport − 288`: 992 at 1280, 1152 at 1440, 1632 at 1920. So 900 bites at all three, and
85
+ 1440 (`canvas`) bites only from about 1730px; below that the column is just the room. Before you quote a width,
86
+ check `cap < viewport − rail − padding` at the width people actually use (the admin shipped two caps, 1040 then
87
+ 998, that were wider than the room at 1277 and did nothing there, 10/09/2026). The column is never wider than
88
+ the room, so no tier makes a page scroll sideways; at ≤760px there is no rail and the column is `viewport − 32`.
89
+
90
+ The page must reset `body { margin: 0 }` itself: `gemboss-ui.css` does not (the admin gets it from Polaris).
91
+
43
92
  ## The sign-in screen
44
93
 
45
94
  ```tsx
@@ -157,11 +206,46 @@ the admin `@import`s it, so it is the same file the admin's own chat is drawn wi
157
206
  The toolbar row, the one-row layout, the engine picker's layout and Stop are in a second sheet,
158
207
  `chat-composer.css` (also inside `gemboss-ui.css`): the admin imports it from its chat components rather than its
159
208
  layout, so screens without a chat never load it. The catalog's "Thân chat" section is the markup to copy. NOT here, on purpose: the panel's own geometry
160
- (`.gemboss-ec` width, editor columns), attachments, action cards, the engine picker and work mode — those
209
+ (`.gemboss-ec` width; the editor columns are in `editor.css` since 0.9.0), attachments, action cards, the engine picker and work mode — those
161
210
  are still the admin's. The one exception is the chat panel's touch / mouse target blocks: one rule per pointer
162
211
  kind names every control of the panel, admin-only ones included (they match nothing elsewhere). The behaviour (streaming, Stop, Enter to send, IME, scroll-to-bottom) belongs to
163
212
  the surface.
164
213
 
214
+ ## Editor CSS (0.9.x)
215
+
216
+ The admin's three editors (theme, page, section) are drawn with `editor.css`, moved verbatim out of the admin's
217
+ `gemboss-shell.css` on 04/10/2026: the same file the admin loads, so a surface that writes the same markup gets the
218
+ same editor. It is a SEPARATE entry, not part of `gemboss-ui.css`: a surface with no editor does not download it.
219
+
220
+ ```ts
221
+ import "@gemboss/ui/gemboss-ui.css";
222
+ import "@gemboss/ui/editor.css"; // only on the screens that show an editor
223
+ ```
224
+
225
+ ```html
226
+ <link rel="stylesheet" href="…/gemboss-ui.css"><link rel="stylesheet" href="…/editor.css">
227
+ ```
228
+
229
+ | Piece | Classes |
230
+ |---|---|
231
+ | Frame | `.gemboss-editor` (fixed, full screen; `--embedded` fills its container) › `header.gemboss-editor-topbar` + `.gemboss-editor-body` › `.gemboss-ec` (left column, width `--gemboss-chat-w`) + `.gemboss-editor-resizer.ui-resizer` + `.gemboss-editor-preview-pane`. `--gemboss-editor-inset` (5px) is the gap and the outer padding. Toast `.gemboss-editor-toast`, status line `.gemboss-editor-statusbar`, phone pane switch `.gemboss-editor-mobile-tabs › .gemboss-editor-mobile-tab` |
232
+ | Top bar | `.gemboss-editor-topbar` (`--theme`, `--guest`) › `.gemboss-editor-tb-left` (`.gemboss-editor-back`, `.gemboss-editor-tb-name`) · `.gemboss-editor-tb-center` · `.gemboss-editor-tb-right` (`.gemboss-editor-tb-tools`, `.gemboss-editor-tb-ghost`, `.gemboss-editor-tb-undo`, `.gemboss-editor-chat-toggle`, `.gemboss-editor-savestate`). Publish split button `.gemboss-editor-tb-publish` / `.gemboss-editor-publish` (`-main`, `-caret`). Overflow `.gemboss-editor-overflow` (`-trigger`) › `.gemboss-editor-menu` › `.gemboss-editor-menu-item` (`-label`, `-val`, `-check`, `-ext`), `.gemboss-editor-menu-sep` |
233
+ | Page picker | `.gemboss-pagepicker` › `-trigger` (`-trigger-icon`, `-trigger-label`, `-caret`) + `-menu` › `-search` (`-search-icon`, `-search-input`), `-list` › `-item` (`-item-icon`, `-item-label`, `-check`, `-drill`, `-thumb`), `-back`, `-empty`, `-note` |
234
+ | Preview | `.gemboss-pv` › `.gemboss-pv-stage` › `.gemboss-pv-frame` (`.desktop` / `.tablet` / `.mobile`, `.gemboss-pv-fit` for scale-to-fit) › `iframe.gemboss-pv-iframe`; `.gemboss-pv-spin`; empty state `.gemboss-pv-empty` (`-art`, `-title`, `-sub`, `-cta`). Device toggle `.gemboss-pv-devices › button.gemboss-pv-device` |
235
+ | Left column | tab row `div.gemboss-ec-tabs[role=tablist] › button[role=tab][aria-selected]` (add `.gemboss-ec-tab` for the 44px pointer floors); Configure pane `.gemboss-ec-dock-body` |
236
+ | Inspector | `.gemboss-ei` (`--docked` inside the Configure pane) › `.gemboss-ei-head` (`-title`, `-x`, `-savedot`), `.gemboss-ei-tabs › .gemboss-ei-tab`, `.gemboss-ei-body`, `.gemboss-ei-foot`. Groups `.gemboss-ei-group › .gemboss-ei-grouphead` (collapsible) and `.gemboss-ei-gh` / `.gemboss-ei-grp`. Field `.gemboss-ei-fld › label + control + .gemboss-ei-hint` |
237
+ | Controls | text / select `.gemboss-field.gemboss-ei-inp` (+ `.gemboss-ei-suffix`, `.gemboss-ei-mono`), segmented `.gemboss-ei-seg`, colour `.gemboss-ei-color` / `.gemboss-ei-swatches › .gemboss-ei-sw` / `.gemboss-ei-swatch`, range `.gemboss-ei-range` + `.gemboss-ei-rangeval`, spacing box `.gemboss-ei-edgebox › .gemboss-ei-edge`, image `.gemboss-ei-img` (`-thumb`, `-ctl`, `-actions`, `-grid`, `-cell`), repeater `.gemboss-ei-rep-item` (`-head`, `-acts`, `-add`), blocks `.gemboss-ei-block` (`-head`, `-x`), add `.gemboss-ei-addrow` / `.gemboss-ei-addchip`, element tree `.gemboss-ei-tree › .gemboss-ei-tnode` (`-tname`, `-tacts`, `-tadd`), notes `.gemboss-ei-note`, `.gemboss-ei-oos` (`-h`), `.gemboss-ei-empty`, drag `.gemboss-ei-drag` / `.gemboss-ei-dragmask` |
238
+ | Layers | `.gemboss-layers` › `.gemboss-layer-head` + `.gemboss-layers-list` › `.gemboss-layer` (`.is-sel`) › `.gemboss-layer-main` (`.gemboss-layer-type`) + `.gemboss-layer-acts` (`.gemboss-layer-del`) |
239
+ | Settings drawer | `.gemboss-settings` › `.gemboss-picker-head` + `.gemboss-settings-body` › `.gemboss-settings-field`, `-grouphead`, `-gen`, `-note`, `-error`, `-handle`; `.gemboss-settings-foot` (`-save`, `-cancel`); backdrop `.gemboss-picker-backdrop`, empty `.gemboss-picker-empty` |
240
+ | Page / section editor bar | `.gemboss-pageedit` (`-top`, `-title`, `-kind`, `-status`, `-standing`, `-scope`, `-modes`, `-notice`, `-chip`, `-pv`), `.gemboss-sec-type`, section insert `.gemboss-sec-insert-*`, draft preview `.gemboss-page-draft-stage` / `-status`, `.gemboss-page-section-content`, global styles bundle `.gemboss-bundle-look` |
241
+ | Try door bar | `.gemboss-editor-topbar--guest` › `.gemboss-guest-brand` (`.gemboss-guest-mark`), `-note`, `-keep`, `-signin` |
242
+
243
+ Not here, on purpose: the body modes that hide the admin's own topbar and rail while an editor is open
244
+ (`gemboss-editor-fullscreen`, `-just-exited`, `-layer-open`) and admin-only extensions such as the product studio's
245
+ resizer. The behaviour (resizing, tabs, menus, the inspector's writes) belongs to the surface for now: the React
246
+ components come in steps 2 to 4 (`EditorFrame` + `EditorTopbar`; `PanelTabs`, `PanelHeader`, `InspectorGroup`, `Field`
247
+ and the controls; `LayerList`, `EditorDrawer`). Step 5 moves GemFactory's Builder onto them.
248
+
165
249
  ## Plain-HTML behaviour: `kit-plain.js` (0.3.0)
166
250
 
167
251
  Surfaces without React (GemFactory, gemcare, gempress, partner-radar) render the kit's markup on the server and need
@@ -202,6 +286,98 @@ false to keep the popup open (it gets the close value, and a guard that throws k
202
286
  asserted in Chromium by `packages/ui/src/kit-plain.test.ts` (`npm run test:ui-browser`, run by the CI job that has
203
287
  Chromium; `test:packages` excludes it); the catalog's "Hộp thoại" section runs it live.
204
288
 
289
+ ## Navigation pending (0.10.0): a click answers on the first frame
290
+
291
+ Chris, 05/10/2026: content-radar's rail (click a menu item and its icon turns into a spinner, sub items too; rest the
292
+ pointer on one and its page is fetched ahead) is the standard for every surface. Measured 04/10 on content-radar with a
293
+ server ~280 ms away: a rail click left the screen unchanged for 321-503 ms; with this, 6-8 ms. The rule: **something
294
+ on screen changes within 100 ms of every navigation click.** A skeleton is optional (see below); this is not.
295
+
296
+ One state, drawn by `app-shell.css` (inside `gemboss-ui.css`), set three ways:
297
+
298
+ | Surface | What you write | What you get |
299
+ |---|---|---|
300
+ | Next.js App Router | `import { NavLink, PendingLink } from "@gemboss/ui/next"` then `<RailItem as={NavLink} href="/x" icon={…} label="X" />`, `<RailSubItem as={NavLink} href="/x/y">Y</RailSubItem>` | prefetch when the pointer or keyboard focus rests 120 ms, or on touchstart (never on sight, never on a sweep or a Tab run); while the page loads, the icon becomes the spinner (sub item: 12px in its indent, before the label), wait cursor, a `role="status"` "Loading" for screen readers |
301
+ | React without Next | `<RailItem pending={loadingHref === href} … />`, `RailSubItem pending` | the same look (`aria-busy="true"`); prefetch is your router's |
302
+ | Plain HTML (GemFactory, taskdesk, gemcare, gempress) | load `kit-plain.js`; nothing else | it attaches itself: a followed `.gnav-item` / `.gnav-subitem` link gets `aria-busy="true"`; a link the pointer or keyboard focus rests on for 120 ms, or a finger touches, is prefetched once: a speculation rule in Chromium (the click reuses it whatever the Cache-Control), else `<link rel="prefetch">` (reused only for a cacheable page) |
303
+
304
+ Links in the page (a list row, a card, a tab): `PendingLink` in Next (`mark="corner"` for a link that wraps a whole
305
+ card), or `data-gb-pending` (`"inline"` / `"corner"`) on an `<a>` with kit-plain.js. `PendingIcon` (Next) swaps the icon of a
306
+ NavLink outside the rail (a tab with an icon) for the spinner; not inside PendingLink (it has its own) and not in a rail
307
+ item (already swapped).
308
+
309
+ kit-plain.js, exactly: same origin; a plain left click (no Cmd / Ctrl / Shift / Alt, not the middle button); no
310
+ `target` other than `_self`; no `download`; not a `#hash` (or `href="#"`) on the same page; nothing already called
311
+ `preventDefault` (a client router, a popup; the kit listens on the window, so it sees the page's own delegated handlers). The mark comes off on `pageshow` (back/forward cache), Escape, the browser's stop
312
+ (`navigateerror`), the next followed link, and after 15 s at the latest. Opt out with `data-gb-nav="off"` on the link
313
+ or any ancestor (`<html>` for a whole page); `data-gb-prefetch="off"` keeps the spinner and skips the prefetch: put it
314
+ on any GET link that DOES something (sign out, export). It never prefetches in a Next app (`window.next`), nor once a
315
+ client router has taken a rail click over (any SPA): the router owns its links, and a document prefetch there is a
316
+ server request the click never uses. A PendingLink to the page already shown (`?tab=b`) gets its spinner but not the
317
+ RouteCover. Links NavLink renders carry
318
+ `data-gb-nav="next"` and are left to it.
319
+
320
+ `@gemboss/ui/next` is its own entry, and `next` (>= 15.3, for `useLinkStatus`) is an OPTIONAL peer: the main entry never
321
+ imports it (`kit-plain-bundles.test.ts` bundles it without next), so Vite and plain-HTML surfaces are unaffected. With
322
+ Next's prefetch on intent you also want `experimental.staleTimes` (`{ dynamic: 30, static: 180 }`, as the admin and
323
+ content-radar) so going back to a page is served from the router cache, and the loading boundary below is NOT optional;
324
+ a navigation that is not a link
325
+ (`router.push` from a select) is not covered: give that control its own pending state.
326
+
327
+ The admin is not on this yet: it answers a click with its own holding screen (`NavPending.tsx`: the clicked item takes
328
+ the selected pill at once, the content area is covered after 90 ms) and does its own hover prefetch, and its shell CSS
329
+ has under 1 KB of first-load headroom. Its rail already sets `aria-busy` on the pending hub, so moving it over is the
330
+ `app-shell.css` rules into `shell.css` plus `RailSubItem pending`.
331
+
332
+ ### The loading boundary: REQUIRED with NavLink
333
+
334
+ **Every route group whose links are `NavLink` / `PendingLink` must have a `loading.jsx`, and it renders `RouteHolding`.**
335
+ Next prefetches a dynamic route only down to its nearest loading boundary. Without one, intent prefetch fetches
336
+ nothing useful and the click waits a whole server round trip with the old page on screen, spinner or not. Measured
337
+ 05/10/2026 on content-radar (no `loading.*` file, server ~280 ms away): hover prefetches returned 0.3-1.2 KB, none
338
+ finished before the click, and the new screen took 350-780 ms although the rail spinner showed at 5-38 ms. gemwatcher
339
+ (a SPA that draws the new screen's frame first and fills it after) "jumps at once": 8-30 ms. The admin measured the same
340
+ from the other side: deleting `app/app/loading.tsx` cost 60-270 ms per click.
341
+
342
+ ```jsx
343
+ // app/v2/loading.jsx: ONE file for the group, not one per page
344
+ import { RouteHolding } from "@gemboss/ui";
345
+ export default function Loading() { return <RouteHolding />; }
346
+
347
+ // app/v2/layout.jsx: ONE RouteCover beside the page, inside the shell
348
+ import { RouteCover } from "@gemboss/ui";
349
+ <AppShell …><PageContainer width={…}>{children}</PageContainer><RouteCover /></AppShell>
350
+ ```
351
+
352
+ The boundary alone is not enough for `t_route`: Next paints it only once a prefetch or the click's own request has come
353
+ back, one server round trip, and a 120 ms hover rarely lets the prefetch finish (content-radar through a 280 ms
354
+ proxy, 05/10/2026: ~300-350 ms with the boundary). `RouteCover` is the same frame already in the page: the
355
+ `@gemboss/ui/next` links set `html[data-gb-navigating]` while pending, and the cover shows over the content area (below
356
+ the topbar, right of the rail) from the click (80 ms fade) until the navigation commits, where the boundary or the page
357
+ takes over in the same place. It is the admin's holding screen (`.gemboss-route-shell`), minus the destination's name.
358
+
359
+ `RouteHolding` (and `RouteCover`) is one frame for every route: a title bar and four lines in the content column (`width` takes the
360
+ PageContainer tiers; `"none"` when the shell already caps the column; `title` when the boundary knows its one
361
+ destination), `role="status"` with a "Loading" label, no card edges. It is a plain component (no hooks), so a server
362
+ `loading.jsx` renders it, and the admin's first load does not carry it. Two numbers to check per surface (visual-gate
363
+ `--clicks`, or the PR's Playwright script): `t_feedback` (something changes) and `t_route` (the new screen, holding
364
+ frame included, is up), both within 100 ms of the click.
365
+
366
+ ### Page skeletons: where a page has the conditions, not everywhere
367
+
368
+ A skeleton shaped like one page is optional (Chris: "cái có cái không tuỳ điều kiện nhưng dần sẽ áp dụng hết"). Use the
369
+ kit's `.ui-skeleton` inside a page (a `<Suspense>` fallback around one slow block) when its shape is known before its
370
+ data (a list, a table, a card grid), and keep it to that shape: one that does not match the page is a layout jump of its
371
+ own. Always with the screen reader line:
372
+
373
+ ```jsx
374
+ <div className="ui-skeleton" aria-busy="true">
375
+ <span className="ui-sr-only">Loading</span>
376
+ <span className="ui-skeleton__line" style={{ width: "40%" }} />
377
+ <span className="ui-skeleton__line" />
378
+ </div>
379
+ ```
380
+
205
381
  ## Polaris parity (0.5.0)
206
382
 
207
383
  The admin is Polaris; this package is how the rest of the fleet looks like it. Measured on 02/10/2026 across the six
@@ -226,6 +402,7 @@ New primitives, Polaris' shapes on GemBoss tokens (`kit-plain.css`; the admin us
226
402
  | Help / error under a field | `ui-help`, `ui-error`; `aria-invalid="true"` paints the field | Labelled help text, InlineError |
227
403
  | Radio group | `fieldset.ui-choices` + `label.ui-check > input[type=radio]` | ChoiceList |
228
404
  | On / off that applies at once | `label.ui-check.ui-switch > input[role=switch]` | (admin toggle) |
405
+ | (0.9.0) Long label text | put the text in an element: `label.ui-check > input + span`. A long unbroken run (URL, `a/b/c/d`) then breaks inside the label in a block, a column, `ui-choices` or a switch. Not covered: bare text after the input (anonymous flex item), and a label in a flex ROW (it keeps its min-content so short labels are never split): let that row wrap | |
229
406
  | Label for an icon button | `.ui-tooltip > button + .ui-tooltip__bubble[role=tooltip]` | Tooltip |
230
407
  | Waiting | `ui-spinner` (20px), `--lg` (44px) | Spinner |
231
408
  | Progress | `progress.ui-progress`, `--success|--critical` | ProgressBar small |
@@ -547,9 +724,11 @@ something the base cannot express, add a token or a prop here, and every surface
547
724
  | `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). |
548
725
  | `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. |
549
726
  | `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. |
550
- | `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. All inside `gemboss-ui.css`; the admin @imports `chat.css` and `chat-composer.css` itself. |
727
+ | `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. |
728
+ | `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. |
551
729
  | `packages/ui/src/icon.svg` | 0.5.0: the GemBoss app icon, for `<link rel="icon" type="image/svg+xml">`. Must stay byte-identical to `apps/admin/src/app/icon.svg` (`kit-look.test.ts` checks). |
552
730
  | `packages/ui/src/**/*.tsx` | the components, as TypeScript source. Next: add `@gemboss/ui` to `transpilePackages`. Vite compiles it as is. |
731
+ | `packages/ui/src/next/` | 0.10.0, `@gemboss/ui/next`: `NavLink`, `PendingLink`, `PendingIcon`, `NAV_DWELL_MS`. Needs `next` >= 15.3 (optional peer; only this entry imports it). See "Navigation pending". |
553
732
 
554
733
  `gemboss-ui.css` is never committed and never edited. To change a colour or a radius, change
555
734
  `packages/ui/src/gemboss-tokens.css` / `packages/ui/src/ui.css`; to change the rail, the topbar or the thread rail,