@gemboss/ui 0.8.1 → 0.12.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 +122 -0
- package/README.md +212 -3
- package/catalog/index.html +140 -7
- package/gemboss-ui.css +199 -69
- package/package.json +12 -3
- package/src/app-shell.css +89 -0
- package/src/editor.css +1099 -0
- package/src/gemboss-tokens.css +1 -1
- package/src/index.ts +2 -0
- package/src/kit-plain.css +39 -12
- package/src/kit-plain.js +118 -1
- package/src/nav-dwell.ts +6 -0
- package/src/next/index.ts +4 -0
- package/src/next/nav.tsx +142 -0
- package/src/route-holding.tsx +65 -0
- package/src/shell.tsx +12 -7
- package/src/theme-dark.css +1 -0
- package/src/ui.css +67 -54
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
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.12.0 (05/10/2026)
|
|
6
|
+
|
|
7
|
+
**A change you SEE on every surface, and on the admin's /admin console.** The kit now draws the admin's (Polaris')
|
|
8
|
+
density under a mouse; under a finger or below 768px every control keeps its 44px floor. Measured on admin.gemboss.ai,
|
|
9
|
+
the catalog drew 25 values the admin does not; it now draws 3, all colour (see "Not changed"). README "Admin density
|
|
10
|
+
(0.12.0)".
|
|
11
|
+
|
|
12
|
+
Changed (under a mouse, at 768px and wider)
|
|
13
|
+
- `ui-btn`: 28px tall (was 32), padding 0 12 (was 16), 12px (was 13). New `--ui-btn-h: 28px`; `--ui-control-h` stays 32
|
|
14
|
+
for `gemboss-field` and `ui-select` (Polaris TextField / Select). `ui-btn--icon` is 28 wide. The popup's own 28px rule
|
|
15
|
+
is gone (every button is 28). **A surface that raised `--ui-control-h` to grow its buttons must now set `--ui-btn-h`
|
|
16
|
+
as well** (gemcare `public/assets/inbox.css` sets `--ui-control-h: 44px` for that reason).
|
|
17
|
+
- ui.css (what the admin loads): buttons and segments are 44 at phone width with a mouse too (`max-width: 47.9975em`),
|
|
18
|
+
as they already were under a finger; they were 32 there.
|
|
19
|
+
- `ui-seg`: 28px group, labels 12px/550. `ui-tabs`: 28px (was 44 under every pointer), 12px/550 selected or not, 8px
|
|
20
|
+
apart (was 4); 44px under a finger or at phone width (in ui.css, so the plain export keeps it).
|
|
21
|
+
- `ui-page-head h1`: 20px/24px/700 (was 22/28/650).
|
|
22
|
+
- `ui-card`, `ui-table-wrap`: Polaris' drop under the card, `--ui-card-shadow` (shadow-100, `0 1px 0 rgba(26,26,26,.07)`).
|
|
23
|
+
The edge stays one 1px `--ui-border` line: Polaris paints a bevel inset on a ::before, which a full-bleed child would
|
|
24
|
+
cover in an inset shadow and which would not follow a surface's dark `--ui-border`. The admin console
|
|
25
|
+
(`.gemboss-admin-shell`) and `theme-dark.css` set the shadow to none.
|
|
26
|
+
- `ui-card__title`, `ui-section-head > h2`, `ui-empty__title`: 14px/20px/650 (were 14/18.2/600, 16/650, 16/600).
|
|
27
|
+
- `ui-table`: Polaris DataTable, measured on /app/analytics and Polaris 13.9.5 rendered: cells 8px 6px (a one-line row
|
|
28
|
+
36), header 12px 6px at 13px/450 (45; was 12/600), the first column 12px from the edge (was 8 x 12 everywhere).
|
|
29
|
+
- `ui-select` weight 450 (was 500); `ui-badge` weight 450 (was 550).
|
|
30
|
+
- `ui-tag`: Polaris Tag, 12px/450 on a 20px line, padding 0 6, radius 8 (was 11px/600 uppercase, letter-spaced).
|
|
31
|
+
`ui-tag--chip` keeps its 11px mono and its height (it already sat on the body's 20px line); its corner is 8 (was 6).
|
|
32
|
+
- `ui-metric--lg`: value 24/32 (was 24/1.1), label 13 (was 14). `ui-funnel__val` 20/24.
|
|
33
|
+
- The kit stops reading `--gemboss-fs-body`, `--gemboss-fs-h1/h2/h3` (the admin seller app's prose sizes, 14/24/20/16);
|
|
34
|
+
ui.css reads `--ui-fs-caption` where it read `--gemboss-fs-caption` (same 12px).
|
|
35
|
+
- `app-shell.css`: Polaris' heading reset (`h1…h6, p`: 1em, inherited weight, margin 0) INSIDE `.gemboss-platform-shell`,
|
|
36
|
+
for elements WITHOUT a class, never inside an article: `.ui-prose`, `[data-content]`, and the wrappers the fleet uses
|
|
37
|
+
for rendered HTML today, by name (`.md-body`, `.recap-md`, `.asst-md`, `.article-body`, any class containing
|
|
38
|
+
`prose` or `markdown` in any case). A new wrapper: mark it `ui-prose` or `data-content`. A bare `<h2>` or `<p>` in the shell becomes body text (13/20/450) with no margin, as in the
|
|
39
|
+
admin; `ui-page-head h1` / `p` lose the browser's margins the same way (the admin never had them). A heading with a
|
|
40
|
+
class keeps what it had.
|
|
41
|
+
|
|
42
|
+
Added
|
|
43
|
+
- `ui-chip` (filter chip: Polaris FilterPill's shape at 28px, `aria-pressed` / `aria-current`) and `ui-stack` (cards
|
|
44
|
+
16px apart, `--tight` 8) in kit-plain.css. For a new filter row use `ui-chip`; the older `.ui-chips > .ui-tag--chip`
|
|
45
|
+
(link and toggle) still draws as before and moves to `ui-chip` with each surface's PR.
|
|
46
|
+
- `ui-btn--sm` is the default size under a mouse now (both 28); its own size rule is gone. It still means "compact" in
|
|
47
|
+
one place: under a finger the admin (ui.css only) keeps it 28, as before; every other surface gets 44 from kit-plain.css.
|
|
48
|
+
- `ui-table`: the last column is 12px from the edge too (Polaris DataTable `:last-child`).
|
|
49
|
+
- visual-gate `--density` (scripts/visual-gate): one `density` check per width, `skip` when the page has no kit element
|
|
50
|
+
or was redirected; its selftest has the 28 / 32 control.
|
|
51
|
+
- `polaris-reference.json` + `packages/ui/scripts/density.mjs`: the density gate (README). `packages/ui/src/density.test.ts` runs it in
|
|
52
|
+
Chromium on the catalog and on the admin's own sheets, with negative controls (32px button, 44px under a mouse, bare
|
|
53
|
+
`<h2>`) and positive ones (sr-only, an article's headings, the 44px floor under a finger).
|
|
54
|
+
|
|
55
|
+
Not changed
|
|
56
|
+
- Colour: ink and primary `#1b1c1e` (Polaris `#303030`), the secondary button's fill. A separate change.
|
|
57
|
+
|
|
58
|
+
Surfaces: upgrade, then run `packages/ui/scripts/density.mjs` on your pages; what it still lists is your own CSS (GemFactory
|
|
59
|
+
`server/ui.mjs`, content-radar `v2.css`, taskdesk's segmented buttons are known).
|
|
60
|
+
|
|
61
|
+
## 0.10.1 (05/10/2026)
|
|
62
|
+
|
|
63
|
+
Editor kit step 1b (gemboss #3781): the visible fixes left over from the verbatim move of `editor.css` in 0.9.0. Each one
|
|
64
|
+
changes what the admin's three editors show; nothing else moves (computed style compared on 99 editor states at six
|
|
65
|
+
widths). README "Editor CSS (0.9.x)".
|
|
66
|
+
|
|
67
|
+
Changed
|
|
68
|
+
- `editor.css`, the Configure | Chat tab row: on the space scale. Row padding `var(--ui-space-2) var(--ui-space-3)`
|
|
69
|
+
(was 8px 10px), tab padding `var(--ui-space-2)` (was 7px). The temporary variables `--gemboss-ec-tabs-pad-x` and
|
|
70
|
+
`--gemboss-ec-tab-pad` are gone. Under a mouse a tab is 2px taller and the row 4px narrower inside.
|
|
71
|
+
- No iris for selection: `.gemboss-layer.is-sel` and `.gemboss-pageedit-chip` use `--gemboss-active-bg` /
|
|
72
|
+
`--gemboss-active-fg` / `--gemboss-divider`; `.gemboss-guest-keep` (the try door's Keep) is `--gemboss-primary`, hover
|
|
73
|
+
`--gemboss-primary-dark`, text `--gemboss-on-primary`. Iris (`--gemboss-accent`) stays for focus and links.
|
|
74
|
+
|
|
75
|
+
Added
|
|
76
|
+
- `@keyframes gemboss-flyout-in`, which `.gemboss-editor-menu` has named since before 0.9.0 and no sheet defined: the
|
|
77
|
+
⋯ More and Publish menus fade in with a 4px drop like the page picker, no fill mode, `animation: none` under
|
|
78
|
+
`prefers-reduced-motion`.
|
|
79
|
+
- `editor-css.test.ts`: no literal colour between 228° and 300° hue in `editor.css`.
|
|
80
|
+
|
|
81
|
+
Admin, same PR (not in the package): the theme editor's tabs wear `.gemboss-ec-tab` (36px to 44px under touch, as the
|
|
82
|
+
page and section editors), the Section Library loses its lilac borders, `SectionOutline.tsx` and `.gemboss-ec-panel`
|
|
83
|
+
(unreachable / unused) are deleted.
|
|
84
|
+
|
|
85
|
+
## 0.10.0 (05/10/2026)
|
|
86
|
+
|
|
87
|
+
Navigation pending: a click on a menu item answers on the first frame, and a page is fetched while the pointer rests on
|
|
88
|
+
its link (Chris, 05/10/2026, from content-radar's rail). README "Navigation pending".
|
|
89
|
+
|
|
90
|
+
Added
|
|
91
|
+
- `@gemboss/ui/next`, a new entry: `NavLink` (next/link that prefetches only on intent: pointer at rest 120 ms,
|
|
92
|
+
touchstart, focus; while its page loads it carries a `role="status"` mark that turns a rail item's icon into the
|
|
93
|
+
spinner), `PendingLink` (a link in the page with a small spinner, `mark="inline" | "corner"`), `PendingIcon`,
|
|
94
|
+
`NAV_DWELL_MS`. `next` >= 15.3 is an optional peer dependency; the main entry does not import it.
|
|
95
|
+
- `RailItem` and `RailSubItem` take `pending` (sets `aria-busy="true"`).
|
|
96
|
+
- `RouteHolding` (main entry, plain component): the one loading boundary a route group renders in its `loading.jsx`
|
|
97
|
+
(title bar + four lines in the content column, `role="status"`). Required wherever links are `NavLink`: without a
|
|
98
|
+
boundary, intent prefetch has nothing to fetch (content-radar 05/10/2026: new screen at 350-780 ms).
|
|
99
|
+
- `RouteCover` (main entry): the same frame over the content area from the click until the navigation commits, shown
|
|
100
|
+
by `html[data-gb-navigating]`, which the `@gemboss/ui/next` links set while pending (no delay, 80 ms fade).
|
|
101
|
+
- `app-shell.css`: the pending look. `.gnav-item` icon becomes a 20px spinner in the same box (the label does not
|
|
102
|
+
move), `.gnav-subitem` gets a 12px spinner in its indent before the label (out of the flow), `cursor: progress`, slower spin under reduced motion;
|
|
103
|
+
`a[data-gb-pending]` + `.gb-pending-mark` for page links; `.gnav-pending` status text.
|
|
104
|
+
- `kit-plain.js`: `initNav()`, which runs by itself when the script loads. Followed rail links (and
|
|
105
|
+
`a[data-gb-pending]`) are marked; links are prefetched (speculation rules where supported, else `<link rel="prefetch">`) after a 120 ms rest of the
|
|
106
|
+
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
|
|
107
|
+
already took; clears on `pageshow`, Escape, `navigateerror`, after 15 s. Opt out: `data-gb-nav="off"`,
|
|
108
|
+
`data-gb-prefetch="off"`. No prefetch inside a Next app or with Save-Data.
|
|
109
|
+
- Catalog section "Chờ điều hướng".
|
|
110
|
+
|
|
111
|
+
Measured, and why the prefetch is a speculation rule (05/10/2026, `_local/fleet/nav-pending/prefetch-reuse.txt`): a
|
|
112
|
+
`<link rel="prefetch">` of a page is reused by the click only when the page is cacheable (max-age > 0). Server-rendered
|
|
113
|
+
fleet pages send max-age=0 / no-cache, so the click fetched the page again, and a prefetch still in flight made the
|
|
114
|
+
click slower (1121 ms to the new page against 737 ms without one, through a 280 ms proxy). A speculation-rules prefetch
|
|
115
|
+
was reused under every Cache-Control tried; `nav-pending.test.tsx` holds both (one server request vs two).
|
|
116
|
+
|
|
117
|
+
Not changed
|
|
118
|
+
- The admin: it keeps its own holding screen and hover prefetch (`NavPending.tsx`, `AppShell.tsx`); none of the new
|
|
119
|
+
CSS or code is in its first load. Moving it over is a later change (README).
|
|
120
|
+
|
|
121
|
+
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 =
|
|
122
|
+
minor). Publish 0.9.0 first or together; check `npm view @gemboss/ui versions` and main before publishing either.
|
package/README.md
CHANGED
|
@@ -206,11 +206,57 @@ the admin `@import`s it, so it is the same file the admin's own chat is drawn wi
|
|
|
206
206
|
The toolbar row, the one-row layout, the engine picker's layout and Stop are in a second sheet,
|
|
207
207
|
`chat-composer.css` (also inside `gemboss-ui.css`): the admin imports it from its chat components rather than its
|
|
208
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
|
|
209
|
-
(`.gemboss-ec` width
|
|
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
|
|
210
210
|
are still the admin's. The one exception is the chat panel's touch / mouse target blocks: one rule per pointer
|
|
211
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
|
|
212
212
|
the surface.
|
|
213
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
|
+
|
|
249
|
+
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
|
+
|
|
251
|
+
- Tab row on the space scale: `padding: var(--ui-space-2) var(--ui-space-3)` on the row, `var(--ui-space-2)` on each
|
|
252
|
+
tab (were 8px 10px and 7px; the temporary `--gemboss-ec-tabs-pad-x` / `--gemboss-ec-tab-pad` are gone). Under a
|
|
253
|
+
mouse a tab is 2px taller; under touch the 44px floor decides.
|
|
254
|
+
- No iris for selection: Layers' selected row and the page editor's "Editing:" chip use `--gemboss-active-bg` /
|
|
255
|
+
`--gemboss-active-fg` / `--gemboss-divider`; the try door's Keep button is `--gemboss-primary` (hover
|
|
256
|
+
`--gemboss-primary-dark`) like Publish. Iris (`--gemboss-accent`) stays for focus and links.
|
|
257
|
+
- `gemboss-flyout-in` is defined (fade + 4px drop, as the page picker), so `.gemboss-editor-menu` animates; `none`
|
|
258
|
+
under `prefers-reduced-motion`.
|
|
259
|
+
|
|
214
260
|
## Plain-HTML behaviour: `kit-plain.js` (0.3.0)
|
|
215
261
|
|
|
216
262
|
Surfaces without React (GemFactory, gemcare, gempress, partner-radar) render the kit's markup on the server and need
|
|
@@ -251,6 +297,98 @@ false to keep the popup open (it gets the close value, and a guard that throws k
|
|
|
251
297
|
asserted in Chromium by `packages/ui/src/kit-plain.test.ts` (`npm run test:ui-browser`, run by the CI job that has
|
|
252
298
|
Chromium; `test:packages` excludes it); the catalog's "Hộp thoại" section runs it live.
|
|
253
299
|
|
|
300
|
+
## Navigation pending (0.10.0): a click answers on the first frame
|
|
301
|
+
|
|
302
|
+
Chris, 05/10/2026: content-radar's rail (click a menu item and its icon turns into a spinner, sub items too; rest the
|
|
303
|
+
pointer on one and its page is fetched ahead) is the standard for every surface. Measured 04/10 on content-radar with a
|
|
304
|
+
server ~280 ms away: a rail click left the screen unchanged for 321-503 ms; with this, 6-8 ms. The rule: **something
|
|
305
|
+
on screen changes within 100 ms of every navigation click.** A skeleton is optional (see below); this is not.
|
|
306
|
+
|
|
307
|
+
One state, drawn by `app-shell.css` (inside `gemboss-ui.css`), set three ways:
|
|
308
|
+
|
|
309
|
+
| Surface | What you write | What you get |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| 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 |
|
|
312
|
+
| React without Next | `<RailItem pending={loadingHref === href} … />`, `RailSubItem pending` | the same look (`aria-busy="true"`); prefetch is your router's |
|
|
313
|
+
| 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) |
|
|
314
|
+
|
|
315
|
+
Links in the page (a list row, a card, a tab): `PendingLink` in Next (`mark="corner"` for a link that wraps a whole
|
|
316
|
+
card), or `data-gb-pending` (`"inline"` / `"corner"`) on an `<a>` with kit-plain.js. `PendingIcon` (Next) swaps the icon of a
|
|
317
|
+
NavLink outside the rail (a tab with an icon) for the spinner; not inside PendingLink (it has its own) and not in a rail
|
|
318
|
+
item (already swapped).
|
|
319
|
+
|
|
320
|
+
kit-plain.js, exactly: same origin; a plain left click (no Cmd / Ctrl / Shift / Alt, not the middle button); no
|
|
321
|
+
`target` other than `_self`; no `download`; not a `#hash` (or `href="#"`) on the same page; nothing already called
|
|
322
|
+
`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
|
|
323
|
+
(`navigateerror`), the next followed link, and after 15 s at the latest. Opt out with `data-gb-nav="off"` on the link
|
|
324
|
+
or any ancestor (`<html>` for a whole page); `data-gb-prefetch="off"` keeps the spinner and skips the prefetch: put it
|
|
325
|
+
on any GET link that DOES something (sign out, export). It never prefetches in a Next app (`window.next`), nor once a
|
|
326
|
+
client router has taken a rail click over (any SPA): the router owns its links, and a document prefetch there is a
|
|
327
|
+
server request the click never uses. A PendingLink to the page already shown (`?tab=b`) gets its spinner but not the
|
|
328
|
+
RouteCover. Links NavLink renders carry
|
|
329
|
+
`data-gb-nav="next"` and are left to it.
|
|
330
|
+
|
|
331
|
+
`@gemboss/ui/next` is its own entry, and `next` (>= 15.3, for `useLinkStatus`) is an OPTIONAL peer: the main entry never
|
|
332
|
+
imports it (`kit-plain-bundles.test.ts` bundles it without next), so Vite and plain-HTML surfaces are unaffected. With
|
|
333
|
+
Next's prefetch on intent you also want `experimental.staleTimes` (`{ dynamic: 30, static: 180 }`, as the admin and
|
|
334
|
+
content-radar) so going back to a page is served from the router cache, and the loading boundary below is NOT optional;
|
|
335
|
+
a navigation that is not a link
|
|
336
|
+
(`router.push` from a select) is not covered: give that control its own pending state.
|
|
337
|
+
|
|
338
|
+
The admin is not on this yet: it answers a click with its own holding screen (`NavPending.tsx`: the clicked item takes
|
|
339
|
+
the selected pill at once, the content area is covered after 90 ms) and does its own hover prefetch, and its shell CSS
|
|
340
|
+
has under 1 KB of first-load headroom. Its rail already sets `aria-busy` on the pending hub, so moving it over is the
|
|
341
|
+
`app-shell.css` rules into `shell.css` plus `RailSubItem pending`.
|
|
342
|
+
|
|
343
|
+
### The loading boundary: REQUIRED with NavLink
|
|
344
|
+
|
|
345
|
+
**Every route group whose links are `NavLink` / `PendingLink` must have a `loading.jsx`, and it renders `RouteHolding`.**
|
|
346
|
+
Next prefetches a dynamic route only down to its nearest loading boundary. Without one, intent prefetch fetches
|
|
347
|
+
nothing useful and the click waits a whole server round trip with the old page on screen, spinner or not. Measured
|
|
348
|
+
05/10/2026 on content-radar (no `loading.*` file, server ~280 ms away): hover prefetches returned 0.3-1.2 KB, none
|
|
349
|
+
finished before the click, and the new screen took 350-780 ms although the rail spinner showed at 5-38 ms. gemwatcher
|
|
350
|
+
(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
|
|
351
|
+
from the other side: deleting `app/app/loading.tsx` cost 60-270 ms per click.
|
|
352
|
+
|
|
353
|
+
```jsx
|
|
354
|
+
// app/v2/loading.jsx: ONE file for the group, not one per page
|
|
355
|
+
import { RouteHolding } from "@gemboss/ui";
|
|
356
|
+
export default function Loading() { return <RouteHolding />; }
|
|
357
|
+
|
|
358
|
+
// app/v2/layout.jsx: ONE RouteCover beside the page, inside the shell
|
|
359
|
+
import { RouteCover } from "@gemboss/ui";
|
|
360
|
+
<AppShell …><PageContainer width={…}>{children}</PageContainer><RouteCover /></AppShell>
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The boundary alone is not enough for `t_route`: Next paints it only once a prefetch or the click's own request has come
|
|
364
|
+
back, one server round trip, and a 120 ms hover rarely lets the prefetch finish (content-radar through a 280 ms
|
|
365
|
+
proxy, 05/10/2026: ~300-350 ms with the boundary). `RouteCover` is the same frame already in the page: the
|
|
366
|
+
`@gemboss/ui/next` links set `html[data-gb-navigating]` while pending, and the cover shows over the content area (below
|
|
367
|
+
the topbar, right of the rail) from the click (80 ms fade) until the navigation commits, where the boundary or the page
|
|
368
|
+
takes over in the same place. It is the admin's holding screen (`.gemboss-route-shell`), minus the destination's name.
|
|
369
|
+
|
|
370
|
+
`RouteHolding` (and `RouteCover`) is one frame for every route: a title bar and four lines in the content column (`width` takes the
|
|
371
|
+
PageContainer tiers; `"none"` when the shell already caps the column; `title` when the boundary knows its one
|
|
372
|
+
destination), `role="status"` with a "Loading" label, no card edges. It is a plain component (no hooks), so a server
|
|
373
|
+
`loading.jsx` renders it, and the admin's first load does not carry it. Two numbers to check per surface (visual-gate
|
|
374
|
+
`--clicks`, or the PR's Playwright script): `t_feedback` (something changes) and `t_route` (the new screen, holding
|
|
375
|
+
frame included, is up), both within 100 ms of the click.
|
|
376
|
+
|
|
377
|
+
### Page skeletons: where a page has the conditions, not everywhere
|
|
378
|
+
|
|
379
|
+
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
|
|
380
|
+
kit's `.ui-skeleton` inside a page (a `<Suspense>` fallback around one slow block) when its shape is known before its
|
|
381
|
+
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
|
|
382
|
+
own. Always with the screen reader line:
|
|
383
|
+
|
|
384
|
+
```jsx
|
|
385
|
+
<div className="ui-skeleton" aria-busy="true">
|
|
386
|
+
<span className="ui-sr-only">Loading</span>
|
|
387
|
+
<span className="ui-skeleton__line" style={{ width: "40%" }} />
|
|
388
|
+
<span className="ui-skeleton__line" />
|
|
389
|
+
</div>
|
|
390
|
+
```
|
|
391
|
+
|
|
254
392
|
## Polaris parity (0.5.0)
|
|
255
393
|
|
|
256
394
|
The admin is Polaris; this package is how the rest of the fleet looks like it. Measured on 02/10/2026 across the six
|
|
@@ -258,8 +396,8 @@ surfaces on the package, the same gaps were filled six ways. What 0.5.0 settles,
|
|
|
258
396
|
|
|
259
397
|
| | Under a mouse | Under a thumb / ≤ 767px wide |
|
|
260
398
|
|---|---|---|
|
|
261
|
-
| Control height: `gemboss-field`, `ui-select
|
|
262
|
-
| `ui-btn--sm` | 28px | 44px |
|
|
399
|
+
| Control height: `gemboss-field`, `ui-select` | **32px** (Polaris TextField / Select), `--ui-control-h` | **44px** |
|
|
400
|
+
| `ui-btn`, `ui-btn--sm`, `ui-seg`, `ui-tabs` (0.12.0, was 32 / 28 / 32 / 44) | **28px** (Polaris Button), `--ui-btn-h` | **44px**, `ui-btn--sm` included |
|
|
263
401
|
| `ui-btn` inside a popup's head or foot (0.7.3) | 28px, 12px | 44px |
|
|
264
402
|
| Focus ring, every control | 2px iris `--ui-focus` (= `--gemboss-accent`, the admin's `--p-color-border-focus`) | same |
|
|
265
403
|
| Body text (`html`, `body`, at zero specificity) | 13px / 20px, weight 450, `--gemboss-text`, antialiased (Polaris bodyMd) | same |
|
|
@@ -275,6 +413,7 @@ New primitives, Polaris' shapes on GemBoss tokens (`kit-plain.css`; the admin us
|
|
|
275
413
|
| Help / error under a field | `ui-help`, `ui-error`; `aria-invalid="true"` paints the field | Labelled help text, InlineError |
|
|
276
414
|
| Radio group | `fieldset.ui-choices` + `label.ui-check > input[type=radio]` | ChoiceList |
|
|
277
415
|
| On / off that applies at once | `label.ui-check.ui-switch > input[role=switch]` | (admin toggle) |
|
|
416
|
+
| (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 | |
|
|
278
417
|
| Label for an icon button | `.ui-tooltip > button + .ui-tooltip__bubble[role=tooltip]` | Tooltip |
|
|
279
418
|
| Waiting | `ui-spinner` (20px), `--lg` (44px) | Spinner |
|
|
280
419
|
| Progress | `progress.ui-progress`, `--success|--critical` | ProgressBar small |
|
|
@@ -308,6 +447,74 @@ If your surface still sets any of these itself (a `body { font-size }`, a `min-h
|
|
|
308
447
|
cascade over the package's zero-specificity base. `packages/ui/src/kit-look.test.ts` measures the table above in
|
|
309
448
|
Chromium on the built CSS.
|
|
310
449
|
|
|
450
|
+
## Admin density (0.12.0)
|
|
451
|
+
|
|
452
|
+
05/10/2026, Chris: "UI GemFactory hình như mọi thứ to hơn bên gemboss". Measured the same day on admin.gemboss.ai, the
|
|
453
|
+
catalog and nine surfaces (raw data and matrix: `Gems/_local/fleet/ui-density-2026-10/`): about 94% of the gaps came from
|
|
454
|
+
the PACKAGE, not from the surfaces. It drew a 32px button at 13px where Polaris draws 28 at 12, a 22/28/650 page title
|
|
455
|
+
where Polaris draws 20/24/700, table cells padded 8 where Polaris pads 12, a bordered card where Polaris draws a shadow,
|
|
456
|
+
and an uppercase 11px tag. 0.12.0 draws the admin's numbers under a mouse and keeps the 44px floor under a finger or at
|
|
457
|
+
phone width (`(pointer: coarse), (max-width: 47.9975em)`, the topbar search's rule since 0.6.5), which is how a surface
|
|
458
|
+
both looks like the admin and passes Gems/CLAUDE.md §9.1.
|
|
459
|
+
|
|
460
|
+
| | Under a mouse, ≥ 768px | Under a finger / < 768px |
|
|
461
|
+
|---|---|---|
|
|
462
|
+
| `ui-btn` (every variant), `ui-btn--icon` | 28px, padding 0 12, 12px/550 (`--ui-btn-h`) | 44px |
|
|
463
|
+
| `ui-seg` group, `.is-text` label | 28px, 12px/550 | 44px |
|
|
464
|
+
| `ui-tabs` | 28px, padding 0 12, 12px/550 selected or not, 8px apart | 44px |
|
|
465
|
+
| `ui-page-head h1` | 20px / 24px / 700 (Polaris headingLg) | same |
|
|
466
|
+
| `ui-card`, `ui-table-wrap` | Polaris' shadow-100 under it (`--ui-card-shadow`), one 1px `--ui-border` edge, radius 12, padding 16 | same |
|
|
467
|
+
| `ui-card__title`, `ui-section-head > h2`, `ui-empty__title` | 14px / 20px / 650 (headingMd) | same |
|
|
468
|
+
| `ui-table` | Polaris DataTable: cells 8px 6px (row 36), `th` 12px 6px at 13px/450 (45), first and last column 12 from the edge | same |
|
|
469
|
+
| `ui-select` | weight 450 | 44px |
|
|
470
|
+
| `ui-badge` | 12px / 450 (its text span; 550 was the box) | same |
|
|
471
|
+
| `ui-tag` | Polaris Tag: 12px/450 on a 20px line, padding 0 6, radius 8, not uppercase | same |
|
|
472
|
+
| `ui-metric--lg` value / label, `ui-funnel__val` | 24/32/700 · 13px · 20/24/700 | same |
|
|
473
|
+
|
|
474
|
+
New in `kit-plain.css` (surfaces drew both by hand):
|
|
475
|
+
|
|
476
|
+
| Need | Markup |
|
|
477
|
+
|---|---|
|
|
478
|
+
| Filter chip (Polaris FilterPill's shape at the Button's density: dashed until applied) | `<div class="ui-chips"><button type="button" class="ui-chip" aria-pressed="true">Plus</button><a class="ui-chip" href="?c=us" aria-current="page">US</a></div>` |
|
|
479
|
+
| Cards on a page, 16px apart (Polaris BlockStack gap 400) | `<div class="ui-stack">…cards…</div>`, `ui-stack--tight` for 8 |
|
|
480
|
+
|
|
481
|
+
**Headings inside the shell.** In the admin a bare `<h2>` or `<p>` is body text with no margin: Polaris' global sheet
|
|
482
|
+
resets `h1…h6, p` (size 1em, regular weight, margin 0), and every heading that looks like one is a Text variant. `app-shell.css`
|
|
483
|
+
now does the same INSIDE `.gemboss-platform-shell` only, to elements without a class (a surface's `h2.x-title` keeps
|
|
484
|
+
what it had), and never inside an article the surface shows: mark post bodies, rendered markdown and previews with `ui-prose` or `data-content` (the wrappers already in the fleet are recognised by name too: `.md-body`, `.recap-md`, `.asst-md`, `.article-body`, and any class containing `prose` or `markdown`; the gate skips the same list) and they keep the browser's
|
|
485
|
+
heading scale and margins. A page heading is `ui-page-head`, a card or section heading `ui-card__title` /
|
|
486
|
+
`ui-section-head`. The kit no longer reads `--gemboss-fs-body` (14), `--gemboss-fs-h1/h2/h3`: those are the admin seller
|
|
487
|
+
app's own prose sizes; the kit is on Polaris' scale (`--ui-fs-body` 13, `--ui-fs-caption` 12, headings 14 / 20 / 24).
|
|
488
|
+
|
|
489
|
+
Not in 0.12.0 (a separate change): colour. The kit's ink and primary fill are `#1b1c1e`, Polaris' `#303030`; Polaris'
|
|
490
|
+
secondary button is a grey fill, the kit's a white one with a line.
|
|
491
|
+
|
|
492
|
+
### The density gate: `packages/ui/scripts/density.mjs` + `polaris-reference.json`
|
|
493
|
+
|
|
494
|
+
`polaris-reference.json` pins what the admin draws, role by role, with where each number was measured (admin.gemboss.ai
|
|
495
|
+
05/10/2026, Polaris 13.9.5 for Tag and the card bevel). `packages/ui/scripts/density.mjs` opens a page in Chromium, finds every
|
|
496
|
+
visible element of each role (the kit's selectors), reads computed style (text from the element that paints the first
|
|
497
|
+
glyph, as Polaris wraps its labels) and lists every value that is not the admin's. Two modes: `fine` (mouse, ≥ 768px)
|
|
498
|
+
is exact; `touch` (`--touch`, or narrower than 768) keeps typography and asks every control for ≥ 44px. Any `h1`–`h6`
|
|
499
|
+
that is not one of the admin's heading styles (20/24/700, 14/20/650, 13/20/650) is reported: that is a bare heading.
|
|
500
|
+
Not measured: sr-only text (1px), anything inside `.ui-prose` / `[data-content]`, closed `<details>`.
|
|
501
|
+
|
|
502
|
+
```
|
|
503
|
+
node scripts/visual-gate/visual-gate.mjs --url https://factory.gemboss.ai/ --density # in the official gate, every width
|
|
504
|
+
node packages/ui/scripts/density.mjs --catalog # the package's own catalog
|
|
505
|
+
node packages/ui/scripts/density.mjs --url https://factory.gemboss.ai/ --width 1280
|
|
506
|
+
node packages/ui/scripts/density.mjs --url https://factory.gemboss.ai/ --width 390 --touch
|
|
507
|
+
DENSITY_COOKIE='session=…' node packages/ui/scripts/density.mjs --url … --scope main --json
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Exit 1 when anything differs, 3 when nothing was measured; "not on this page" lists roles the page does not show
|
|
511
|
+
(never read that as a pass). In visual-gate, `--density` adds a `density` check per width (touch mode at <= 768, as the
|
|
512
|
+
gate's own tap checks) that is `skip`, not pass, when the page has no kit element. On a
|
|
513
|
+
surface page, run it after upgrading the package: what it still lists is the surface's own CSS. `packages/ui/src/density.test.ts`
|
|
514
|
+
runs it on the catalog (1280 mouse, 390 finger, 390 mouse) and on the admin's own sheets, with its controls: a
|
|
515
|
+
`.ui-btn{height:32px}` or `min-height:44px` under a mouse and a bare `<h2>` turn it red; sr-only text, an article's
|
|
516
|
+
heading and the 44px floor under a finger do not.
|
|
517
|
+
|
|
311
518
|
## Popup density (0.7.3)
|
|
312
519
|
|
|
313
520
|
The kit popup (`.ui-overlay > .ui-modal`, `<dialog class="ui-modal">`, `openModal`, `Dialog`) is drawn at Polaris
|
|
@@ -596,9 +803,11 @@ something the base cannot express, add a token or a prop here, and every surface
|
|
|
596
803
|
| `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). |
|
|
597
804
|
| `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. |
|
|
598
805
|
| `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. |
|
|
806
|
+
| `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. |
|
|
599
807
|
| `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. |
|
|
600
808
|
| `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). |
|
|
601
809
|
| `packages/ui/src/**/*.tsx` | the components, as TypeScript source. Next: add `@gemboss/ui` to `transpilePackages`. Vite compiles it as is. |
|
|
810
|
+
| `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". |
|
|
602
811
|
|
|
603
812
|
`gemboss-ui.css` is never committed and never edited. To change a colour or a radius, change
|
|
604
813
|
`packages/ui/src/gemboss-tokens.css` / `packages/ui/src/ui.css`; to change the rail, the topbar or the thread rail,
|