@gemboss/ui 0.13.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +512 -1
- package/README.md +426 -14
- package/catalog/editor.html +1027 -0
- package/catalog/index.html +1215 -90
- package/gemboss-ui.css +3 -4698
- package/package.json +24 -5
- package/parts/components.css +5 -0
- package/parts/core.css +5 -0
- package/parts/shell.css +5 -0
- package/src/app-shell.css +1 -256
- package/src/assistant/engine.tsx +607 -0
- package/src/assistant/host.tsx +1 -0
- package/src/assistant/index.ts +1 -0
- package/src/assistant-engine.css +1 -0
- package/src/assistant.css +1 -558
- package/src/auth.css +1 -606
- package/src/auth.tsx +1 -0
- package/src/board-move.ts +194 -0
- package/src/chat-composer.css +1 -110
- package/src/chat.css +1 -432
- package/src/collections.css +1 -350
- package/src/collections.tsx +170 -40
- package/src/dialog.tsx +3 -135
- package/src/editor/controls.tsx +207 -0
- package/src/editor/css-url.ts +11 -0
- package/src/editor/frame.tsx +243 -0
- package/src/editor/index.ts +52 -0
- package/src/editor/layers.tsx +136 -0
- package/src/editor/panel.tsx +189 -0
- package/src/editor/panes.tsx +45 -0
- package/src/editor/sortable.tsx +79 -0
- package/src/editor/swatch.ts +45 -0
- package/src/editor/tree.tsx +61 -0
- package/src/editor-layers-plain.js +1 -0
- package/src/editor-plain.js +1 -0
- package/src/editor.css +1 -1099
- package/src/gemboss-field.css +1 -112
- package/src/gemboss-tokens.css +1 -164
- package/src/icons.tsx +2 -0
- package/src/index-table.tsx +20 -56
- package/src/index.ts +16 -6
- package/src/kit-plain.css +1 -499
- package/src/kit-plain.js +1 -575
- package/src/launcher.tsx +1 -0
- package/src/menu-layer.ts +94 -0
- package/src/menu.tsx +28 -2
- package/src/portal.css +1 -139
- package/src/portal.tsx +1 -0
- package/src/reactions-data.ts +59 -0
- package/src/reactions.css +1 -0
- package/src/reactions.tsx +140 -0
- package/src/resource-list-select.tsx +73 -0
- package/src/search-dialog.tsx +146 -0
- package/src/segmented-click.ts +20 -0
- package/src/segmented.tsx +98 -0
- package/src/selection.tsx +79 -0
- package/src/shell.css +1 -1079
- package/src/theme-dark.css +1 -66
- package/src/theme-script.ts +10 -0
- package/src/theme.tsx +5 -5
- package/src/ui.css +1 -610
package/README.md
CHANGED
|
@@ -11,7 +11,10 @@ npm install @gemboss/ui
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
|
-
|
|
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`;
|
|
@@ -242,9 +365,10 @@ import "@gemboss/ui/editor.css"; // only on the screens that show an editor
|
|
|
242
365
|
|
|
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
|
-
resizer. The
|
|
246
|
-
|
|
247
|
-
and the
|
|
368
|
+
resizer. The frame and its top bar are React components since 0.14.0 ("Editor frame" below), with their behaviour for
|
|
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
|
|
|
@@ -257,6 +381,185 @@ Changes since the verbatim move, in 0.10.1 (05/10/2026, step 1b, the only ones t
|
|
|
257
381
|
- `gemboss-flyout-in` is defined (fade + 4px drop, as the page picker), so `.gemboss-editor-menu` animates; `none`
|
|
258
382
|
under `prefers-reduced-motion`.
|
|
259
383
|
|
|
384
|
+
## Editor frame (0.14.0): `EditorFrame`, `EditorTopbar`
|
|
385
|
+
|
|
386
|
+
The frame every editor is drawn in: the admin's theme, page and section editors render these components since 0.14.0
|
|
387
|
+
(measured on a production build before and after: the same DOM and the same computed style, see CHANGELOG), and
|
|
388
|
+
GemFactory's Builder is next. FRAME ONLY: the chat, the Configure dock, the canvas, the page picker, the menus and Publish
|
|
389
|
+
belong to the editor and arrive as slots; the package holds no data logic and no state.
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
import "@gemboss/ui/editor.css";
|
|
393
|
+
import { DeviceToggle, EditorBack, EditorChatToggle, EditorFrame, EditorPaneTabs, EditorSaveState, EditorTopbar } from "@gemboss/ui/editor";
|
|
394
|
+
|
|
395
|
+
<EditorFrame
|
|
396
|
+
top={<EditorTopbar variant="theme" back={<EditorBack as={Link} href="/" />} title="Builder"
|
|
397
|
+
chatToggle={<EditorChatToggle collapsed={collapsed} onToggle={toggle} />}
|
|
398
|
+
left={<EditorSaveState state={saveState} />}
|
|
399
|
+
center={<DeviceToggle device={device} onChange={setDevice} />}
|
|
400
|
+
right={<>{tools}{moreMenu}{publish}</>}
|
|
401
|
+
status={message ? { state: "error", message } : null} />}
|
|
402
|
+
split={{ width, min: 300, max, collapsed, onPointerDown, onKeyDown }}
|
|
403
|
+
left={<div className="gemboss-ec">…</div>}
|
|
404
|
+
preview={<Canvas />}
|
|
405
|
+
right={presetsOpen ? <Presets /> : null}
|
|
406
|
+
mobilePane={pane}
|
|
407
|
+
>
|
|
408
|
+
<EditorPaneTabs value={pane} onChange={setPane} />
|
|
409
|
+
{drawersAndPopups}
|
|
410
|
+
</EditorFrame>
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
| Component | Draws | Notes |
|
|
414
|
+
|---|---|---|
|
|
415
|
+
| `EditorFrame` | `div.gemboss-editor` › `top` · `.gemboss-editor-body` (`left` · seam · `.gemboss-editor-preview-pane`) · `children` | `split` writes `--gemboss-chat-w` (0 while `collapsed`, plus `.is-chat-collapsed`) and draws the seam (`role=separator`, `aria-valuenow/min/max`, `tabIndex 0`); the drag and the keys are the caller's (`onPointerDown`, `onKeyDown`). `right` is the right column: `.gemboss-editor-dock`, docked over the preview's right edge, 320px (max 90%), first in the pane. `mobilePane` is `data-mobile-pane` on the body. `embedded` fills a container instead of the viewport. Refs and extra classes for body and pane. |
|
|
416
|
+
| `EditorPaneTabs` | `.gemboss-editor-mobile-tabs` › two `[role=tab]` | The phone's Chat / Preview switch (shown at 760px and below). Put it first in the frame's `children`. `labels` renames them. |
|
|
417
|
+
| `EditorTopbar` | `header.gemboss-editor-topbar` › `-tb-left` (`back`, `chatToggle`, `title`, `left`) · `-tb-center` · `-tb-right`; then the status line | `variant`: `theme` (page picker + split Publish), `page` (`.gemboss-pageedit-top`: a title, a type chip, a status), `guest` (the try door). `status` draws `.gemboss-editor-statusbar.<state>` under the bar while its message is not empty. |
|
|
418
|
+
| `EditorBack` | `.gemboss-editor-back` ← | A `<button type="button">`, or `as={Link}` / `as="a"` with `href`. Named "Back" for a screen reader (`label`). |
|
|
419
|
+
| `EditorChatToggle` | `.gemboss-editor-chat-toggle` | Shows / hides the left column; filled while open, pressed when collapsed. |
|
|
420
|
+
| `EditorSaveState` | `.gemboss-editor-savestate` (`.is-saving` with the spinner, `.is-saved`) | `idle` draws nothing, or `idleText` dimmed (the section editor's "Autosaves"). Texts and titles are props. |
|
|
421
|
+
| `DeviceToggle`, `MonitorGlyph`, `PhoneGlyph` | `.gemboss-pv-devices` › `.gemboss-pv-device` | Desktop / tablet / mobile. Moved from the admin unchanged. |
|
|
422
|
+
|
|
423
|
+
One module per piece behind one entry, so a bundler gives each editor only what it draws (measured: one module for all
|
|
424
|
+
of it cost the page and section editors about 1 KB of code they never ran). Not in `@gemboss/ui` itself: like
|
|
425
|
+
`editor.css`, only a screen with an editor pays for it.
|
|
426
|
+
|
|
427
|
+
The geometry comes from `editor.css`. What the admin got from its own sheet (the left column sized by `--gemboss-chat-w`,
|
|
428
|
+
stacked above the preview at 1000px and below, the preview taking the rest) is in `editor.css` as ZERO-specificity
|
|
429
|
+
`:where(…)` rules since 0.14.0: anything more specific wins (the admin's own rules do, so its look did not move), and a
|
|
430
|
+
page that has only the package still lays out.
|
|
431
|
+
|
|
432
|
+
### Plain HTML: `editor-plain.js`
|
|
433
|
+
|
|
434
|
+
Write the markup the components render (copy it from `catalog/editor.html`, where it is rendered from the components and
|
|
435
|
+
runs), load the behaviour AFTER `kit-plain.js`, and call `gembossUI.initEditorFrame()`:
|
|
436
|
+
|
|
437
|
+
```html
|
|
438
|
+
<link rel="stylesheet" href="…/@gemboss/ui/gemboss-ui.css"><link rel="stylesheet" href="…/@gemboss/ui/src/editor.css">
|
|
439
|
+
…
|
|
440
|
+
<script src="…/@gemboss/ui/src/kit-plain.js"></script>
|
|
441
|
+
<script src="…/@gemboss/ui/src/editor-plain.js"></script>
|
|
442
|
+
<script>
|
|
443
|
+
gembossUI.initEditorFrame(); // every .gemboss-editor, or initEditorFrame(root)
|
|
444
|
+
addEventListener("gemboss:editor-device", (e) => setCanvas(e.detail.device));
|
|
445
|
+
</script>
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
| Wires | Markup | Event on the editor (bubbles) |
|
|
449
|
+
|---|---|---|
|
|
450
|
+
| The seam: pointer drag (main button of the primary pointer: mouse, touch, pen), ←/→ 16px (Shift 64), Home / End; clamped to the BOX, `min(aria-valuemax, body width − 360 − 5)`, never below `aria-valuemin`; the CHOSEN width is kept apart, so a box that narrows clamps it and widening gives it back, and reopening the column clamps to the box as it is then; `ui-resizing` on `<body>` while dragging; remembered when the body has `data-gb-storage="<key>"` | `.gemboss-editor-resizer` (the frame's) | `gemboss:editor-split` `{ width }` |
|
|
451
|
+
| Hide / show the left column | `.gemboss-editor-chat-toggle` | `gemboss:editor-chat` `{ collapsed }` |
|
|
452
|
+
| Device | `.gemboss-pv-devices .gemboss-pv-device` (`data-device`, or the `title`) | `gemboss:editor-device` `{ device }` |
|
|
453
|
+
| Phone pane | `.gemboss-editor-mobile-tab` (`data-pane`, or Chat first / Preview second) | `gemboss:editor-pane` `{ pane }` |
|
|
454
|
+
| ⋯ / Publish ▾ menus | a trigger with `aria-haspopup="menu"`, then a `.gemboss-editor-menu[hidden]` right after it: opens 6px under the trigger, right edges aligned, at least 8px inside the window; one at a time; a press outside, an item, the window losing focus (a press in the canvas iframe) and a resize close or re-place it; Escape closes the menu only (capture phase, like `initMenus`; a popup behind it stays open; not while an IME composes) | `gemboss:editor-menu` `{ open, menu }` |
|
|
455
|
+
|
|
456
|
+
Without `kit-plain.js` it logs what is missing and defines nothing (`kit-plain.js` owns `gembossUI`). Its own file for
|
|
457
|
+
the same reason as `editor.css`: a page with no editor does not load it.
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
## Settings panel (0.17.0): `PanelTabs`, `PanelHeader`, `InspectorGroup`, `Field` and the controls
|
|
461
|
+
|
|
462
|
+
Step 3 of the editor kit. The pieces every editor's settings panel is made of: the admin's Configure column, its element
|
|
463
|
+
inspector and its schema form render these since 0.17.0 (measured on a production build before and after: the same DOM
|
|
464
|
+
and the same computed style, see CHANGELOG). Same entry as the frame (`@gemboss/ui/editor`), look from `editor.css`.
|
|
465
|
+
Controls take a value and give one back; nothing in the package knows what is edited, what schema a value came from or
|
|
466
|
+
where it is saved.
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
import "@gemboss/ui/editor.css";
|
|
470
|
+
import { ColorControl, EdgeBox, Field, ImageControl, InspectorGroup, PanelAction, PanelHeader, PanelTabs, RangeControl, Seg } from "@gemboss/ui/editor";
|
|
471
|
+
|
|
472
|
+
<PanelHeader title="Hero" subtitle="hero-banner" actions={<PanelAction label="Duplicate section" onClick={dup}>⧉</PanelAction>} />
|
|
473
|
+
<PanelTabs variant="inspector" value={tab} onChange={setTab} tabs={[{ id: "content", label: "Content" }, { id: "design", label: "Design" }]} />
|
|
474
|
+
<InspectorGroup title="Layout" note="all screens">
|
|
475
|
+
<Field label="Width"><Seg label="Width" value={width} onChange={setWidth} options={[{ value: "full", label: "Full" }, { value: "contained", label: "Contained" }]} /></Field>
|
|
476
|
+
<Field label="Background" htmlFor="bg"><ColorControl id="bg" label="Background" value={bg} onChange={setBg} /></Field>
|
|
477
|
+
<Field label="Opacity" htmlFor="op" labelValue={`${op}%`}><RangeControl id="op" min={0} max={100} value={op} onChange={setOp} /></Field>
|
|
478
|
+
<Field label="Padding"><EdgeBox label="Padding" values={pad} onChange={(side, raw) => setPad({ ...pad, [side]: raw })} onCommit={save} /></Field>
|
|
479
|
+
<Field label="Image" htmlFor="img"><ImageControl id="img" value={url} onChange={setUrl} actions={<ChipButton onClick={openLibrary}>Library</ChipButton>} /></Field>
|
|
480
|
+
</InspectorGroup>
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
| Component | Draws | Behaviour |
|
|
484
|
+
|---|---|---|
|
|
485
|
+
| `PanelTabs` | `variant="column"`: `.gemboss-ec-tabs` › `.gemboss-ec-tab` (Configure · Chat, `icon` before the label). `variant="inspector"`: `.gemboss-ei-tabs` › `.gemboss-ei-tab(.on)` | `role=tablist` / `tab`, `aria-selected`, `data-value` (the tab's id, for plain HTML), `aria-controls` from a tab's `controls`. `roving` (default on): only the selected tab is a Tab stop, ←/→ move the selection and the focus, wrapping. The admin's Configure · Chat row passes `roving={false}` (both tabs Tab stops, no arrows: how it always behaved). |
|
|
486
|
+
| `PanelHeader`, `PanelAction` | `header.gemboss-ei-head` › `.gemboss-ei-title` (+ `small` subtitle) · `.gemboss-ei-act` buttons · `.gemboss-ei-x` ✕ | ✕ only with `onClose` (a docked panel has none); `onDragStart` adds `.gemboss-ei-drag` and takes the mouse-down of a floating panel. |
|
|
487
|
+
| `InspectorGroup` | `.gemboss-ei-grp` › `button.gemboss-ei-gh` (▾ / ▸, 11px / 700 uppercase, `note` as `.gemboss-ei-gh-note`) + the body | `aria-expanded`, `data-open`; closed, CSS hides everything after the header (the body stays mounted). Controlled (`open` + `onOpenChange`, which receives the NEXT state) or not (`defaultOpen`). `collapsible={false}`: the schema form's static group, `section.gemboss-ei-group` › `h4.gemboss-ei-grouphead` + `hint`. |
|
|
488
|
+
| `Field` | `.gemboss-ei-fld` › `label` (13px / 500; `htmlFor`; `labelValue` as `.gemboss-ei-rangeval` at its right edge) · control · `hint` | No label when `label` is undefined / null. `className` (the admin's `togrow`), `style`. |
|
|
489
|
+
| `Seg` | `.gemboss-ei-seg[role=group]` › `button[aria-pressed][data-value]` (`.on`) | A click hands back the option's value; `label` names the group; `style` on the group. |
|
|
490
|
+
| `ColorControl` | `.gemboss-ei-color` › `input[type=color].gemboss-ei-swatch` + text box | The swatch commits on every change; the text box keeps a draft and commits on blur (every blur) or Enter. Anything but a 6-digit hex disables the swatch, with a title saying why (`swatchState`). |
|
|
491
|
+
| `RangeControl` | `input[type=range].gemboss-ei-range` | Every change, as a number (drag, arrows, Home / End). Show the value with `Field`'s `labelValue`. |
|
|
492
|
+
| `EdgeBox` | `.gemboss-ei-edgebox` › four `.gemboss-ei-edge` (box + T / R / B / L) | `onChange(side, raw)` per keystroke, `onCommit()` on blur; Enter blurs. Boxes named "<label> top / right / bottom / left". `narrow` (default) adds `gemboss-field--narrow`; `placeholders`, `ids` per side. What an empty edge means is the caller's. |
|
|
493
|
+
| `ImageControl`, `ChipButton` | `.gemboss-ei-img` › 52px thumbnail (— when empty) · URL box · `.gemboss-ei-img-actions` (`actions`, then Clear while there is a value) | Commits on blur / Enter only when the box changed; Clear commits "". The box's text can be the caller's (`draft` + `onDraftChange`) to put a picked URL in it at once. The thumbnail's `url()` is quoted (`cssUrl`, 0.20.1), so a URL with ")" or a space still shows. |
|
|
494
|
+
| `cssUrl` | | `url("…")` for any URL: quoted, `"` and `\` escaped, a control character as a CSS hex escape. Plain TypeScript (also `@gemboss/ui/editor/css-url`), so a server component can call it; `gembossUI.cssUrl` in editor-plain.js writes the same. Use it for your own image cells (the admin's library grid does). |
|
|
495
|
+
| `swatchState` | | The one rule for what `<input type="color">` can show. Also `@gemboss/ui/editor/swatch` (plain TypeScript, for node). |
|
|
496
|
+
|
|
497
|
+
Two modules behind the entry (`panel`, `controls`), both `"use client"`. Kept in the admin, on purpose: the schema
|
|
498
|
+
(`FieldSpec`), value shapes and clamping (FieldRenderer), the theme job's media library and image generation, the
|
|
499
|
+
per-breakpoint spacing model. Since 0.20.0 (#3846) the schema's On / Off toggles and the inspector's Visibility (Show /
|
|
500
|
+
Hide) rows are `Seg` too, so they say `aria-pressed` like every other segmented row.
|
|
501
|
+
|
|
502
|
+
### Plain HTML: `initEditorPanel()`
|
|
503
|
+
|
|
504
|
+
The same markup (copy it from `catalog/editor.html`), then `gembossUI.initEditorPanel()` (editor-plain.js, after
|
|
505
|
+
`kit-plain.js`; the whole document, or `initEditorPanel(element)`). By delegation, each with an event on the element
|
|
506
|
+
named (it bubbles):
|
|
507
|
+
|
|
508
|
+
| Wires | Markup | Event |
|
|
509
|
+
|---|---|---|
|
|
510
|
+
| Tabs: select (`aria-selected`, `.on`, the tab's `aria-controls` panel shown and its siblings' hidden); ←/→ when the row is roving (its tabs carry `tabindex`) | `.gemboss-ec-tabs` / `.gemboss-ei-tabs` › `[role=tab]` (`data-value`, or the text) | `gemboss:editor-tab` `{ tab, index }` on the tab |
|
|
511
|
+
| Fold / unfold a group | `.gemboss-ei-gh` | `gemboss:editor-group` `{ open }` on the header |
|
|
512
|
+
| Press a Seg option | `.gemboss-ei-seg button` (`data-value`, or the text) | `gemboss:editor-seg` `{ value }` on the group |
|
|
513
|
+
| Colour: the swatch writes the box; the box commits on blur / Enter and the swatch follows (off for a non-hex) | `.gemboss-ei-color` | `gemboss:editor-color` `{ value }` |
|
|
514
|
+
| Range: the label's `.gemboss-ei-rangeval` follows, with its `data-suffix` | `input.gemboss-ei-range` with an `id` and a `label[for]` | `gemboss:editor-range` `{ value }` on the input |
|
|
515
|
+
| Edges: Enter blurs; a box that loses focus sends all four | `.gemboss-ei-edgebox` | `gemboss:editor-edges` `{ t, r, b, l }` |
|
|
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 }` |
|
|
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
|
+
|
|
260
563
|
## Plain-HTML behaviour: `kit-plain.js` (0.3.0)
|
|
261
564
|
|
|
262
565
|
Surfaces without React (GemFactory, gemcare, gempress, partner-radar) render the kit's markup on the server and need
|
|
@@ -461,6 +764,7 @@ both looks like the admin and passes Gems/CLAUDE.md §9.1.
|
|
|
461
764
|
|---|---|---|
|
|
462
765
|
| `ui-btn` (every variant), `ui-btn--icon` | 28px, padding 0 12, 12px/550 (`--ui-btn-h`) | 44px |
|
|
463
766
|
| `ui-seg` group, `.is-text` label | 28px, 12px/550 | 44px |
|
|
767
|
+
| `ui-seg--fill` (0.20.0): the row's width, segments equal; `ui-seg-n` a count inside a segment. EVERY "pick one" switch in the admin is a `ui-seg`, drawn by `Segmented` (`@gemboss/ui/segmented`); the editor kit's `Seg` and `DeviceToggle` too | same | same |
|
|
464
768
|
| `ui-tabs` | 28px, padding 0 12, 12px/550 selected or not, 8px apart | 44px |
|
|
465
769
|
| `ui-page-head h1` | 20px / 24px / 700 (Polaris headingLg) | same |
|
|
466
770
|
| `ui-card`, `ui-table-wrap` | Polaris' shadow-100 under it (`--ui-card-shadow`), one 1px `--ui-border` edge, radius 12, padding 16 | same |
|
|
@@ -712,6 +1016,23 @@ import { Avatar, Dialog, Leaderboard, Podium, SearchDialog, ThemeToggle, themeSc
|
|
|
712
1016
|
(after the name, e.g. a level) and `id` (a `#me` anchor). `title` may be left out on a page whose own
|
|
713
1017
|
`<h1>` names the board; pass `label` then. Screen-reader text uses `.gb-sr-only`, styled in portal.css.
|
|
714
1018
|
|
|
1019
|
+
## Reactions (0.21.0): one emoji set and one chip for the fleet
|
|
1020
|
+
|
|
1021
|
+
Chris 06/10/2026: "reaction các thứ comment nên chuẩn hoá một kiểu, bên gemcare cũng có, nên đưa vào gemboss ui". GemCare's
|
|
1022
|
+
team chat built them first (`public/assets/inbox.js`); this is that, moved here so GemFeedback's requests and comments,
|
|
1023
|
+
and GemCare's chat, draw and accept the same thing.
|
|
1024
|
+
|
|
1025
|
+
| Piece | What |
|
|
1026
|
+
|---|---|
|
|
1027
|
+
| `@gemboss/ui/reactions-data` (plain TS, also re-exported from the root) | `REACTION_GROUPS` (GemCare's 137 emoji in 4 groups, each with search words), `REACTION_EMOJI`, `isReactionEmoji` (the server's check: refuse anything else), `searchReactions`, `reactionLabel` (names, two then `+n`), `toggleReaction` (the optimistic guess drawn before the server answers), `QUICK_REACTIONS` (👍 ✅), type `Reaction` = `{ emoji, names, mine }` |
|
|
1028
|
+
| `Reactions` (React, client) | the chips (emoji + NAMES of who put it; yours outlined + filled, `aria-pressed`) and "Add reaction", which opens `ReactionPicker` in flow under the row. `canReact={false}`: chips only; no chips and no right to react: nothing |
|
|
1029
|
+
| `ReactionPicker` | search, "Frequently used" (this browser's last 9, `localStorage`, else the quick pair), the groups; arrows move in the grid, Enter in the search picks the first, Escape closes and returns focus |
|
|
1030
|
+
| `reactions.css` (its own entry, NOT inside `gemboss-ui.css`: link it where reactions are drawn, as `collections.css`) | `ui-reactions`, `ui-reactions__row`, `ui-reaction` (+ `is-mine`), `__emoji`, `__names`, `ui-reaction-add`, `ui-reaction-picker` (`__search`, `__body`, `__head`, `__grid`, `__btn`, `__empty`) |
|
|
1031
|
+
|
|
1032
|
+
Every target is 44px (a control, CLAUDE.md §9.1); the pill you see is 26px, transparent padding carries the rest, so a
|
|
1033
|
+
message with reactions does not grow by a full line. A plain-HTML surface writes the same classes (markup at the top of
|
|
1034
|
+
`reactions.css`, and the catalog's "Reactions" section).
|
|
1035
|
+
|
|
715
1036
|
## Report pages (0.4.0)
|
|
716
1037
|
|
|
717
1038
|
Plain CSS in `kit-plain.css` for a numbers page written by hand (first user: gemnews `/admin`, 03/10/2026; that page moved to GemCommunity the same day, so gemnews no longer takes the package). The
|
|
@@ -759,14 +1080,14 @@ silently: a `slice` prints "+N more" or "See all N". A table renders at most 100
|
|
|
759
1080
|
|---|---|---|---|---|
|
|
760
1081
|
| 1 | Data table | `DataTable` | `ui-table-wrap > table.ui-table(.ui-table--sticky-first, --compact)`; `tfoot.ui-table__totals`, `.ui-table__footer` | DataTable |
|
|
761
1082
|
| 2 | Index table | `IndexTable` (client) | `div.ui-index > [div.ui-index__sortbar] + [div.ui-bulkbar] + ui-table-wrap > table.ui-table.ui-table--index`; `tr[data-href]`, `td.ui-table__check`, `td.ui-table__primary > a.ui-table__title + span.ui-sub`, `td.ui-table__actions` | IndexTable |
|
|
762
|
-
| 3 | Resource list | `ResourceList` + `ResourceItem` | `ul.ui-list > li.ui-list__item > .ui-list__media + .ui-list__body(a.ui-list__title, p.ui-list__sub, p.ui-list__meta, .ui-list__badges) + .ui-list__aside + .ui-list__actions` | ResourceList |
|
|
763
|
-
| 4 | Compact list | `CompactList` (`variant="activity"`) | `ul.ui-list.ui-list--compact > li.ui-list__item > a/button/div.ui-list__row`; timeline: `ui-list--activity`, `.ui-list__dot(--tone)`, `.ui-list__detail`, `time` | ActionList |
|
|
1083
|
+
| 3 | Resource list | `ResourceList` + `ResourceItem`; with selection `SelectableResourceList` (client, 0.16.0) | `ul.ui-list > li.ui-list__item > .ui-list__media + .ui-list__body(a.ui-list__title, p.ui-list__sub, p.ui-list__meta, .ui-list__badges) + .ui-list__aside + .ui-list__actions`; selectable: `div.ui-resource > div.ui-bulkbar + ul.ui-list`, `label.ui-list__check > input[data-gb-select]` first in `__media` | ResourceList |
|
|
1084
|
+
| 4 | Compact list | `CompactList` (`variant="activity"`, `wrap`) | `ul.ui-list.ui-list--compact > li.ui-list__item > a/button/div.ui-list__row`; timeline: `ui-list--activity`, `.ui-list__dot(--tone)`, `.ui-list__detail`, `.ui-list__content` (0.15.0), `time`; `ui-list--wrap` (0.15.0) | ActionList |
|
|
764
1085
|
| 5 | Card grid | `CardGrid min={200 \| 240 \| 320}` + `MediaCard` | `div.ui-cardgrid[data-min] > article.ui-card.ui-card--media > a.ui-card__media + .ui-card__menu + .ui-card__body > a.ui-card__link` | InlineGrid + Card |
|
|
765
|
-
| 6 | Board | `Board` | `div.ui-board > section.ui-board__col > header.ui-board__head + ol.ui-board__list + details.ui-board__more` | none |
|
|
1086
|
+
| 6 | Board | `Board` (`onMove`, `hasMore` / `moreHref` / `onLoadMore`: 0.18.0) | `div.ui-board > section.ui-board__col[data-col] > header.ui-board__head + ol.ui-board__list > li[data-id]` + `details.ui-board__more` (client paging) or `div.ui-more.ui-board__next > a[data-gb-more]` (server paging) | none |
|
|
766
1087
|
| | States | `CollectionState kind="empty \| no-match \| error"`, `CollectionSkeleton pattern=…`, `state="busy"` | `ui-empty(--no-match)`, `ui-coll-error`, `ui-skeleton--table/--list/--compact/--grid/--board`, `aria-busy="true"` | EmptyState, EmptySearchResult |
|
|
767
1088
|
|
|
768
|
-
`./collections` is a plain module (a server component renders it, render functions and all); `IndexTable`
|
|
769
|
-
|
|
1089
|
+
`./collections` is a plain module (a server component renders it, render functions and all); `IndexTable` and
|
|
1090
|
+
`SelectableResourceList` (0.16.0) are client modules. Their props are plain data (rows of rendered cells, `sortHref` strings) so a server page can still render them;
|
|
770
1091
|
the callbacks (`onSort`, `onSelectionChange`, a bulk action's `onAction`) need a client parent. Neither module reaches
|
|
771
1092
|
the admin's first load (`kit-plain-bundles.test.ts` checks the shell import carries none of their code).
|
|
772
1093
|
|
|
@@ -777,9 +1098,56 @@ the admin's first load (`kit-plain-bundles.test.ts` checks the shell import carr
|
|
|
777
1098
|
two or more in-place editors is not linked (only its title is). HTML: `tr[data-href]` + kit-plain.js; React:
|
|
778
1099
|
`IndexTable` rows with `href`; a resource list's title link covers its row (no script).
|
|
779
1100
|
- **One visible button per row at most**, the rest in the ⋯ menu (`ui-menu`). **Never shown only on hover.**
|
|
1101
|
+
- **An open ⋯ menu is never cut by its frame (0.18.0).** It floats in the browser's top layer (`popover="manual"`,
|
|
1102
|
+
`position: fixed` against its button): below the button, above it when the room below is short and the room above is
|
|
1103
|
+
larger, 8px from the viewport's edges, scrolling inside when taller than the room. It closes when a scroll moves its
|
|
1104
|
+
button (the page, or a frame around the menu) or the window resizes; a scroll inside the list or elsewhere (a log
|
|
1105
|
+
following its output) does not count. Tab order, Escape (focus back on ⋯) and "a click in the
|
|
1106
|
+
menu is not a click on the row" are unchanged: the list is still the details' child. React `Menu` does it itself;
|
|
1107
|
+
plain HTML gets it from kit-plain.js (`initMenuLayer()`, runs by itself). No popover API (Safari < 17) or no script:
|
|
1108
|
+
the absolute list, as before. GemFactory Members (#197) is the case: at 820 / 900 / 1280 the last rows' menus were
|
|
1109
|
+
half hidden inside the table's scrolling frame.
|
|
1110
|
+
- **A resource list in a frame of 400 or less (0.15.0)** keeps the body beside the media, with all the width right of
|
|
1111
|
+
it; the aside and the actions share ONE line under the body (aside left, actions right, from the body's edge); the
|
|
1112
|
+
media spans both lines; a row without media gives the body the whole width; a body that is only its title keeps
|
|
1113
|
+
them on its line. The row's direct children are the four slots (a checkbox goes in `__media`). So a long
|
|
1114
|
+
subtitle and a body of many meta lines both get shorter than in 0.13.0 (`collections.test.ts` checks the geometry
|
|
1115
|
+
at 320 / 390 / 768 for both, with the 0.13.0 rule and the `flex: 1 1 0` patch as negative controls).
|
|
1116
|
+
- **A compact list wraps when its labels carry data (0.15.0).** `CompactList wrap` (`ui-list--wrap`): the label and an
|
|
1117
|
+
activity's detail wrap instead of ending in "…", because the cut text's `title` is mouse-only. A side rail of short
|
|
1118
|
+
labels keeps one line. `CompactItem.body` (activity only) is the event's own text under them (`.ui-list__content`,
|
|
1119
|
+
body size, never cut; paragraphs as `<p>`); `labelText` puts the full label in `title`.
|
|
780
1120
|
- **Below 768 of its container an index table becomes a list** (title line; the other cells on one caption line,
|
|
781
1121
|
`data-label` prefixes, `data-condensed="hide"` drops; checkbox and actions kept; rows ≥ 44); sort moves to the chips
|
|
782
1122
|
of `ui-index__sortbar`. **A data table never folds:** it scrolls in its frame with `--sticky-first`.
|
|
1123
|
+
- **A data table reads from the top of its rows (0.16.0)**, as Polaris DataTable (`verticalAlign` "top"): one-line
|
|
1124
|
+
cells sit on the first line of a three-line row, and a 24px cell link is pulled up by half its extra height so its
|
|
1125
|
+
text lines up (`calc((1lh - 24px) / 2)`). The index table keeps `middle`. The row header (`th.ui-table__rowhead`)
|
|
1126
|
+
wraps like any cell (Polaris `firstColumn`) with Polaris's first-column width as a floor (9.0625rem; the frame
|
|
1127
|
+
scrolls rather than squeezing it to a word a line); `truncate` or `ui-clamp-2` when it must stay short. A column's
|
|
1128
|
+
`minWidth` (DataTable) is the floor of a column whose cells wrap, and replaces the row header's (`0` for a row
|
|
1129
|
+
header that is always short: a date, an id). A link alone on its
|
|
1130
|
+
line is 24px in the row header and in a list inside a cell (`li > a`), as `td > a` already was.
|
|
1131
|
+
- **A media card is 16:10, 4:3 or 1:1 (0.18.0)** (`MediaCard ratio`, `data-ratio="4/3" | "1/1"`): 1:1 for a square
|
|
1132
|
+
picture (a moodboard, a product shot, a social post). No other ratio: any other value draws 16:10.
|
|
1133
|
+
- **A board column of more than a few dozen cards pages on the SERVER (0.18.0).** The 0.13.0 board puts every card in
|
|
1134
|
+
the HTML behind nested `<details>` (content-radar ideas reaches 2,000). With `hasMore` the column shows the cards the
|
|
1135
|
+
server sent (`boardShown(page)`: 12, then 24 more a page, Chris 05/10/2026) and ends in ONE "Show 24 more":
|
|
1136
|
+
`moreHref` is this page with the column one page further (`?col=ideas&page=2`, cumulative, so no script, Back and a
|
|
1137
|
+
sent link all work; `moreHref` alone means there is more, `hasMore: false` is the last page). On a plain-HTML page
|
|
1138
|
+
(`fetchMore`, i.e. `a[data-gb-more]`) kit-plain.js fetches it, appends the cards whose `data-id` are not on the board
|
|
1139
|
+
yet, takes the next link and the count, puts the URL in the address bar, says "24 more cards in <column>" and focuses
|
|
1140
|
+
the first new card ("Couldn't load." + Try again on failure); never on a board React owns. React: `linkAs` (a Next `Link` with `moreLinkProps={{ scroll: false }}`) or
|
|
1141
|
+
`onLoadMore(columnId)` + `loading`. Cards need ids: `items: [{ id, card, name? }]` instead of `cards`.
|
|
1142
|
+
- **Moving cards between columns is optional (0.18.0)** (`Board onMove(cardId, toColumn, toIndex)`; plain HTML
|
|
1143
|
+
`.ui-board[data-gb-move]` + `li[data-id][tabindex="0"][draggable="true"]`, event `gb:move` { id, from, to, index },
|
|
1144
|
+
cancelable: not cancelled, kit-plain.js moves the `li` and the counts). Drag with a mouse, or Tab to a card, Space,
|
|
1145
|
+
arrows (↑ ↓ in the column, ← → to the next one), Space or Enter to drop, Escape to put it back, each step said in the
|
|
1146
|
+
board's live region. While held, the card has `data-gb-lifted="key"` or `"drag"` (a surface may change its layout for a
|
|
1147
|
+
keyboard hold, never inside dragstart: Chrome cancels the drag), the target column `data-gb-over`, the landing line
|
|
1148
|
+
`data-gb-drop="before|after"` on a card. Nothing moves in the DOM until the drop, and a drop where it was reports nothing. Without
|
|
1149
|
+
`onMove` the board stays read-only (no tab stops, no drag). A phone cannot drag: keep a way that is not dragging (a
|
|
1150
|
+
select, a ⋯ "Move to").
|
|
783
1151
|
- **At 1280 a table fits its page frame.** Fix in this order: a `canvas` page if the content really is wide; move
|
|
784
1152
|
low-priority columns to the detail page; fold attributes into the `ui-sub` line.
|
|
785
1153
|
- **No zebra, no uppercase header, no card per row, no grid that changes its column count per viewport** (auto-fill
|
|
@@ -787,7 +1155,22 @@ the admin's first load (`kit-plain-bundles.test.ts` checks the shell import carr
|
|
|
787
1155
|
- **A checkbox column only with bulk actions.** The bulk bar covers the header (the table does not move), carries its
|
|
788
1156
|
own select-all, Shift+click takes a range, Escape clears. HTML: `input[data-gb-select]` / `[data-gb-select-all]`,
|
|
789
1157
|
`gembossUI.selection(root)`, event `gb:selection`.
|
|
790
|
-
- **
|
|
1158
|
+
- **A resource list selects the same way (0.16.0):** `SelectableResourceList` (`items`, `bulkActions`, optional
|
|
1159
|
+
controlled `selected` + `onSelectionChange`, `selectedLabel`, `linkAs`). The checkbox is first in the media slot
|
|
1160
|
+
(`label.ui-list__check`: 24, 44 under a finger, pulled into the row's padding so a 64px row stays 64). One
|
|
1161
|
+
difference from the table: the bar is the list's header from the start ("0 selected", actions disabled), because a
|
|
1162
|
+
list has no header to cover and a bar that appears on the first check pushes the rows under the finger (talent-radar
|
|
1163
|
+
/pool measured 74px at 1280, 194px at 390). The whole row still opens the item. HTML: the same `data-gb-select`
|
|
1164
|
+
hooks in a `.ui-resource` (kit-plain.js enables the action buttons from 1 instead of showing the bar; an action with
|
|
1165
|
+
its own rule sets its button on `gb:selection`, which fires after). collections-gate reports a list with selection
|
|
1166
|
+
boxes and no bar (`select-without-bulk`). An open ⋯ menu or dialog takes Escape first; a closed one kept in a row
|
|
1167
|
+
does not.
|
|
1168
|
+
- **A sort control is 44 × 44 under a finger (0.18.0)**: the header button grows to 44 tall under `pointer: coarse`
|
|
1169
|
+
and to 44 wide under `pointer: coarse` or at 768 and below (a short label like "ID" was 44 × 30 on an iPad); the
|
|
1170
|
+
chips below 768 have the same floor. Under a mouse the header keeps its Polaris size.
|
|
1171
|
+
- **Sort lives in the URL** (`th[aria-sort] > a.ui-table__sort`); in React the sort links (header and the chips below
|
|
1172
|
+
768) are `linkAs`, the same element as the row titles (0.16.0), so a Next `Link` sorts without reloading the page;
|
|
1173
|
+
`sortLinkProps={{ scroll: false }}` keeps the scroll position (a sort is the same page in another order); `button.ui-table__sort` sorts a small table in place
|
|
791
1174
|
(`table[data-gb-sort="client"]`, by `data-sort-value`) or fires `gb:sort` for the page (`data-gb-sort="event"`); a React table owns its buttons and kit-plain.js leaves them alone.
|
|
792
1175
|
- **States** (Gems/CLAUDE.md 9.2): empty (one main action) · no match (filters and header stay, "Clear filters") ·
|
|
793
1176
|
loading (a skeleton in the pattern's shape: a table keeps its real header, rows keep their height) · reloading
|
|
@@ -872,11 +1255,16 @@ something the base cannot express, add a token or a prop here, and every surface
|
|
|
872
1255
|
|
|
873
1256
|
| Path | What |
|
|
874
1257
|
|---|---|
|
|
875
|
-
| `
|
|
876
|
-
| `
|
|
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). |
|
|
877
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. |
|
|
878
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. |
|
|
879
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. |
|
|
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". |
|
|
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`. |
|
|
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`. |
|
|
880
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. |
|
|
881
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. |
|
|
882
1270
|
| `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). |
|
|
@@ -888,12 +1276,36 @@ something the base cannot express, add a token or a prop here, and every surface
|
|
|
888
1276
|
change `packages/ui/src/shell.css` (since 29/09/2026: before that it lived in the admin's stylesheet and was extracted).
|
|
889
1277
|
The admin and the next published version both carry it.
|
|
890
1278
|
|
|
1279
|
+
## Minified on publish, weighed per file (0.19.0)
|
|
1280
|
+
|
|
1281
|
+
The published tarball carries every stylesheet (`gemboss-ui.css`, `src/*.css`) and the two plain scripts
|
|
1282
|
+
(`packages/ui/src/kit-plain.js`, `packages/ui/src/editor-plain.js`) MINIFIED; the comments stay here in the sources. `gemboss-ui.css` keeps its
|
|
1283
|
+
generated header (the gemboss commit; GemFactory's `/api/health` reads it). Next and Vite minify what they bundle anyway;
|
|
1284
|
+
the surfaces that serve the files as they come out of `node_modules` (GemFactory, taskdesk, gemcare, gempress) did not.
|
|
1285
|
+
Measured on this change: `gemboss-ui.css` 78.1 KB gzip as written, 21.9 KB minified; `kit-plain.js` 17.8 → 8.5 KB.
|
|
1286
|
+
|
|
1287
|
+
`npm publish` cannot minify in place (`exports` points at `src/`, which the admin compiles from), so the release goes
|
|
1288
|
+
through a staged copy: `packages/ui/scripts/stage.mjs` copies exactly what npm would pack into `.pack/`, minifying with esbuild
|
|
1289
|
+
(`legalComments: none`, no target: rewrites that compute the same style only). `npm pack` / `npm publish` run straight
|
|
1290
|
+
in `packages/ui` are refused by `prepack`.
|
|
1291
|
+
|
|
1292
|
+
The weight gate, `packages/ui/scripts/weight-gate.mjs`, reads the staged copy: gzip (level 6) of each minified file against its
|
|
1293
|
+
ceiling in `weight-budget.json` (measured + 512 bytes). Red when a file is over its ceiling, more than 1 KB under it
|
|
1294
|
+
(lower it: a ceiling above the truth constrains nothing), still carries comments or indentation (not minified; measured by esbuild, so a `/*` in a string does not count), or is shipped without
|
|
1295
|
+
a ceiling. CI runs `node packages/ui/scripts/weight-gate.mjs --ci`: build, stage into a temp dir, gate, then the
|
|
1296
|
+
negative controls (+5 KB of real rules in `ui.css` red; +20 KB of comment green; the unminified sources red; one
|
|
1297
|
+
unminified `kit-plain.js` red; a stylesheet with no ceiling red; a ceiling for a file not shipped red; a stale ceiling
|
|
1298
|
+
red; a `/*` inside a string is not a comment, green). Ceilings go down by themselves; raising one is not how a PR
|
|
1299
|
+
gets green (CLAUDE.md §9.4: shared weight only shrinks): it takes the reason in the PR and Chris's OK, and a new
|
|
1300
|
+
component goes in its own entry. `packages/ui/scripts/pack-lib.mjs` holds what the three scripts share (pack list, minifier, staging).
|
|
1301
|
+
|
|
891
1302
|
## Release
|
|
892
1303
|
|
|
893
1304
|
```bash
|
|
894
1305
|
cd packages/ui
|
|
895
|
-
npm version
|
|
896
|
-
npm
|
|
1306
|
+
npm version minor # or patch / major
|
|
1307
|
+
npm run stage # builds gemboss-ui.css + catalogs, minifies into .pack/, runs the weight gate
|
|
1308
|
+
npm publish .pack --auth-type=web # publish the STAGED copy (npm pack .pack for a local tarball)
|
|
897
1309
|
```
|
|
898
1310
|
|
|
899
1311
|
Publish from a worktree at the MERGED commit on `main`, not from a branch, so the registry and
|