@gemboss/ui 0.13.0 → 0.21.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 +362 -1
- package/README.md +253 -12
- package/catalog/editor.html +611 -0
- package/catalog/index.html +998 -25
- package/gemboss-ui.css +3 -4698
- package/package.json +15 -4
- package/src/app-shell.css +1 -256
- package/src/assistant.css +1 -558
- package/src/auth.css +1 -606
- 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 +169 -40
- package/src/editor/controls.tsx +206 -0
- package/src/editor/css-url.ts +11 -0
- package/src/editor/frame.tsx +242 -0
- package/src/editor/index.ts +43 -0
- package/src/editor/panel.tsx +188 -0
- package/src/editor/panes.tsx +44 -0
- package/src/editor/swatch.ts +45 -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 +19 -56
- package/src/index.ts +12 -4
- package/src/kit-plain.css +1 -499
- package/src/kit-plain.js +1 -575
- package/src/menu-layer.ts +87 -0
- package/src/menu.tsx +28 -2
- package/src/portal.css +1 -139
- package/src/reactions-data.ts +59 -0
- package/src/reactions.css +1 -0
- package/src/reactions.tsx +139 -0
- package/src/resource-list-select.tsx +72 -0
- package/src/segmented-click.ts +20 -0
- package/src/segmented.tsx +98 -0
- package/src/selection.tsx +78 -0
- package/src/shell.css +1 -1079
- package/src/theme-dark.css +1 -66
- package/src/ui.css +1 -610
package/README.md
CHANGED
|
@@ -242,9 +242,9 @@ import "@gemboss/ui/editor.css"; // only on the screens that show an editor
|
|
|
242
242
|
|
|
243
243
|
Not here, on purpose: the body modes that hide the admin's own topbar and rail while an editor is open
|
|
244
244
|
(`gemboss-editor-fullscreen`, `-just-exited`, `-layer-open`) and admin-only extensions such as the product studio's
|
|
245
|
-
resizer. The
|
|
246
|
-
|
|
247
|
-
|
|
245
|
+
resizer. The frame and its top bar are React components since 0.14.0 ("Editor frame" below), with their behaviour for
|
|
246
|
+
plain HTML in `editor-plain.js`; the panel and its controls are components since 0.17.0 ("Settings panel" below, step 3); step 4 is `LayerList`,
|
|
247
|
+
`EditorDrawer`. Step 5 moves GemFactory's Builder onto them.
|
|
248
248
|
|
|
249
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
250
|
|
|
@@ -257,6 +257,140 @@ Changes since the verbatim move, in 0.10.1 (05/10/2026, step 1b, the only ones t
|
|
|
257
257
|
- `gemboss-flyout-in` is defined (fade + 4px drop, as the page picker), so `.gemboss-editor-menu` animates; `none`
|
|
258
258
|
under `prefers-reduced-motion`.
|
|
259
259
|
|
|
260
|
+
## Editor frame (0.14.0): `EditorFrame`, `EditorTopbar`
|
|
261
|
+
|
|
262
|
+
The frame every editor is drawn in: the admin's theme, page and section editors render these components since 0.14.0
|
|
263
|
+
(measured on a production build before and after: the same DOM and the same computed style, see CHANGELOG), and
|
|
264
|
+
GemFactory's Builder is next. FRAME ONLY: the chat, the Configure dock, the canvas, the page picker, the menus and Publish
|
|
265
|
+
belong to the editor and arrive as slots; the package holds no data logic and no state.
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
import "@gemboss/ui/editor.css";
|
|
269
|
+
import { DeviceToggle, EditorBack, EditorChatToggle, EditorFrame, EditorPaneTabs, EditorSaveState, EditorTopbar } from "@gemboss/ui/editor";
|
|
270
|
+
|
|
271
|
+
<EditorFrame
|
|
272
|
+
top={<EditorTopbar variant="theme" back={<EditorBack as={Link} href="/" />} title="Builder"
|
|
273
|
+
chatToggle={<EditorChatToggle collapsed={collapsed} onToggle={toggle} />}
|
|
274
|
+
left={<EditorSaveState state={saveState} />}
|
|
275
|
+
center={<DeviceToggle device={device} onChange={setDevice} />}
|
|
276
|
+
right={<>{tools}{moreMenu}{publish}</>}
|
|
277
|
+
status={message ? { state: "error", message } : null} />}
|
|
278
|
+
split={{ width, min: 300, max, collapsed, onPointerDown, onKeyDown }}
|
|
279
|
+
left={<div className="gemboss-ec">…</div>}
|
|
280
|
+
preview={<Canvas />}
|
|
281
|
+
right={presetsOpen ? <Presets /> : null}
|
|
282
|
+
mobilePane={pane}
|
|
283
|
+
>
|
|
284
|
+
<EditorPaneTabs value={pane} onChange={setPane} />
|
|
285
|
+
{drawersAndPopups}
|
|
286
|
+
</EditorFrame>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
| Component | Draws | Notes |
|
|
290
|
+
|---|---|---|
|
|
291
|
+
| `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. |
|
|
292
|
+
| `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. |
|
|
293
|
+
| `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. |
|
|
294
|
+
| `EditorBack` | `.gemboss-editor-back` ← | A `<button type="button">`, or `as={Link}` / `as="a"` with `href`. Named "Back" for a screen reader (`label`). |
|
|
295
|
+
| `EditorChatToggle` | `.gemboss-editor-chat-toggle` | Shows / hides the left column; filled while open, pressed when collapsed. |
|
|
296
|
+
| `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. |
|
|
297
|
+
| `DeviceToggle`, `MonitorGlyph`, `PhoneGlyph` | `.gemboss-pv-devices` › `.gemboss-pv-device` | Desktop / tablet / mobile. Moved from the admin unchanged. |
|
|
298
|
+
|
|
299
|
+
One module per piece behind one entry, so a bundler gives each editor only what it draws (measured: one module for all
|
|
300
|
+
of it cost the page and section editors about 1 KB of code they never ran). Not in `@gemboss/ui` itself: like
|
|
301
|
+
`editor.css`, only a screen with an editor pays for it.
|
|
302
|
+
|
|
303
|
+
The geometry comes from `editor.css`. What the admin got from its own sheet (the left column sized by `--gemboss-chat-w`,
|
|
304
|
+
stacked above the preview at 1000px and below, the preview taking the rest) is in `editor.css` as ZERO-specificity
|
|
305
|
+
`:where(…)` rules since 0.14.0: anything more specific wins (the admin's own rules do, so its look did not move), and a
|
|
306
|
+
page that has only the package still lays out.
|
|
307
|
+
|
|
308
|
+
### Plain HTML: `editor-plain.js`
|
|
309
|
+
|
|
310
|
+
Write the markup the components render (copy it from `catalog/editor.html`, where it is rendered from the components and
|
|
311
|
+
runs), load the behaviour AFTER `kit-plain.js`, and call `gembossUI.initEditorFrame()`:
|
|
312
|
+
|
|
313
|
+
```html
|
|
314
|
+
<link rel="stylesheet" href="…/@gemboss/ui/gemboss-ui.css"><link rel="stylesheet" href="…/@gemboss/ui/src/editor.css">
|
|
315
|
+
…
|
|
316
|
+
<script src="…/@gemboss/ui/src/kit-plain.js"></script>
|
|
317
|
+
<script src="…/@gemboss/ui/src/editor-plain.js"></script>
|
|
318
|
+
<script>
|
|
319
|
+
gembossUI.initEditorFrame(); // every .gemboss-editor, or initEditorFrame(root)
|
|
320
|
+
addEventListener("gemboss:editor-device", (e) => setCanvas(e.detail.device));
|
|
321
|
+
</script>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
| Wires | Markup | Event on the editor (bubbles) |
|
|
325
|
+
|---|---|---|
|
|
326
|
+
| 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 }` |
|
|
327
|
+
| Hide / show the left column | `.gemboss-editor-chat-toggle` | `gemboss:editor-chat` `{ collapsed }` |
|
|
328
|
+
| Device | `.gemboss-pv-devices .gemboss-pv-device` (`data-device`, or the `title`) | `gemboss:editor-device` `{ device }` |
|
|
329
|
+
| Phone pane | `.gemboss-editor-mobile-tab` (`data-pane`, or Chat first / Preview second) | `gemboss:editor-pane` `{ pane }` |
|
|
330
|
+
| ⋯ / 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 }` |
|
|
331
|
+
|
|
332
|
+
Without `kit-plain.js` it logs what is missing and defines nothing (`kit-plain.js` owns `gembossUI`). Its own file for
|
|
333
|
+
the same reason as `editor.css`: a page with no editor does not load it.
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
## Settings panel (0.17.0): `PanelTabs`, `PanelHeader`, `InspectorGroup`, `Field` and the controls
|
|
337
|
+
|
|
338
|
+
Step 3 of the editor kit. The pieces every editor's settings panel is made of: the admin's Configure column, its element
|
|
339
|
+
inspector and its schema form render these since 0.17.0 (measured on a production build before and after: the same DOM
|
|
340
|
+
and the same computed style, see CHANGELOG). Same entry as the frame (`@gemboss/ui/editor`), look from `editor.css`.
|
|
341
|
+
Controls take a value and give one back; nothing in the package knows what is edited, what schema a value came from or
|
|
342
|
+
where it is saved.
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
import "@gemboss/ui/editor.css";
|
|
346
|
+
import { ColorControl, EdgeBox, Field, ImageControl, InspectorGroup, PanelAction, PanelHeader, PanelTabs, RangeControl, Seg } from "@gemboss/ui/editor";
|
|
347
|
+
|
|
348
|
+
<PanelHeader title="Hero" subtitle="hero-banner" actions={<PanelAction label="Duplicate section" onClick={dup}>⧉</PanelAction>} />
|
|
349
|
+
<PanelTabs variant="inspector" value={tab} onChange={setTab} tabs={[{ id: "content", label: "Content" }, { id: "design", label: "Design" }]} />
|
|
350
|
+
<InspectorGroup title="Layout" note="all screens">
|
|
351
|
+
<Field label="Width"><Seg label="Width" value={width} onChange={setWidth} options={[{ value: "full", label: "Full" }, { value: "contained", label: "Contained" }]} /></Field>
|
|
352
|
+
<Field label="Background" htmlFor="bg"><ColorControl id="bg" label="Background" value={bg} onChange={setBg} /></Field>
|
|
353
|
+
<Field label="Opacity" htmlFor="op" labelValue={`${op}%`}><RangeControl id="op" min={0} max={100} value={op} onChange={setOp} /></Field>
|
|
354
|
+
<Field label="Padding"><EdgeBox label="Padding" values={pad} onChange={(side, raw) => setPad({ ...pad, [side]: raw })} onCommit={save} /></Field>
|
|
355
|
+
<Field label="Image" htmlFor="img"><ImageControl id="img" value={url} onChange={setUrl} actions={<ChipButton onClick={openLibrary}>Library</ChipButton>} /></Field>
|
|
356
|
+
</InspectorGroup>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
| Component | Draws | Behaviour |
|
|
360
|
+
|---|---|---|
|
|
361
|
+
| `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). |
|
|
362
|
+
| `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. |
|
|
363
|
+
| `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`. |
|
|
364
|
+
| `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`. |
|
|
365
|
+
| `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. |
|
|
366
|
+
| `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`). |
|
|
367
|
+
| `RangeControl` | `input[type=range].gemboss-ei-range` | Every change, as a number (drag, arrows, Home / End). Show the value with `Field`'s `labelValue`. |
|
|
368
|
+
| `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. |
|
|
369
|
+
| `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. |
|
|
370
|
+
| `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). |
|
|
371
|
+
| `swatchState` | | The one rule for what `<input type="color">` can show. Also `@gemboss/ui/editor/swatch` (plain TypeScript, for node). |
|
|
372
|
+
|
|
373
|
+
Two modules behind the entry (`panel`, `controls`), both `"use client"`. Kept in the admin, on purpose: the schema
|
|
374
|
+
(`FieldSpec`), value shapes and clamping (FieldRenderer), the theme job's media library and image generation, the
|
|
375
|
+
per-breakpoint spacing model. Since 0.20.0 (#3846) the schema's On / Off toggles and the inspector's Visibility (Show /
|
|
376
|
+
Hide) rows are `Seg` too, so they say `aria-pressed` like every other segmented row.
|
|
377
|
+
|
|
378
|
+
### Plain HTML: `initEditorPanel()`
|
|
379
|
+
|
|
380
|
+
The same markup (copy it from `catalog/editor.html`), then `gembossUI.initEditorPanel()` (editor-plain.js, after
|
|
381
|
+
`kit-plain.js`; the whole document, or `initEditorPanel(element)`). By delegation, each with an event on the element
|
|
382
|
+
named (it bubbles):
|
|
383
|
+
|
|
384
|
+
| Wires | Markup | Event |
|
|
385
|
+
|---|---|---|
|
|
386
|
+
| 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 |
|
|
387
|
+
| Fold / unfold a group | `.gemboss-ei-gh` | `gemboss:editor-group` `{ open }` on the header |
|
|
388
|
+
| Press a Seg option | `.gemboss-ei-seg button` (`data-value`, or the text) | `gemboss:editor-seg` `{ value }` on the group |
|
|
389
|
+
| 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 }` |
|
|
390
|
+
| 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 |
|
|
391
|
+
| Edges: Enter blurs; a box that loses focus sends all four | `.gemboss-ei-edgebox` | `gemboss:editor-edges` `{ t, r, b, l }` |
|
|
392
|
+
| Image: commits on blur / Enter when changed; the thumbnail follows; a chip with `data-gb-image-clear` empties it | `.gemboss-ei-img` | `gemboss:editor-image` `{ value }` |
|
|
393
|
+
|
|
260
394
|
## Plain-HTML behaviour: `kit-plain.js` (0.3.0)
|
|
261
395
|
|
|
262
396
|
Surfaces without React (GemFactory, gemcare, gempress, partner-radar) render the kit's markup on the server and need
|
|
@@ -461,6 +595,7 @@ both looks like the admin and passes Gems/CLAUDE.md §9.1.
|
|
|
461
595
|
|---|---|---|
|
|
462
596
|
| `ui-btn` (every variant), `ui-btn--icon` | 28px, padding 0 12, 12px/550 (`--ui-btn-h`) | 44px |
|
|
463
597
|
| `ui-seg` group, `.is-text` label | 28px, 12px/550 | 44px |
|
|
598
|
+
| `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
599
|
| `ui-tabs` | 28px, padding 0 12, 12px/550 selected or not, 8px apart | 44px |
|
|
465
600
|
| `ui-page-head h1` | 20px / 24px / 700 (Polaris headingLg) | same |
|
|
466
601
|
| `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 +847,23 @@ import { Avatar, Dialog, Leaderboard, Podium, SearchDialog, ThemeToggle, themeSc
|
|
|
712
847
|
(after the name, e.g. a level) and `id` (a `#me` anchor). `title` may be left out on a page whose own
|
|
713
848
|
`<h1>` names the board; pass `label` then. Screen-reader text uses `.gb-sr-only`, styled in portal.css.
|
|
714
849
|
|
|
850
|
+
## Reactions (0.21.0): one emoji set and one chip for the fleet
|
|
851
|
+
|
|
852
|
+
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
|
|
853
|
+
team chat built them first (`public/assets/inbox.js`); this is that, moved here so GemFeedback's requests and comments,
|
|
854
|
+
and GemCare's chat, draw and accept the same thing.
|
|
855
|
+
|
|
856
|
+
| Piece | What |
|
|
857
|
+
|---|---|
|
|
858
|
+
| `@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 }` |
|
|
859
|
+
| `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 |
|
|
860
|
+
| `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 |
|
|
861
|
+
| `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`) |
|
|
862
|
+
|
|
863
|
+
Every target is 44px (a control, CLAUDE.md §9.1); the pill you see is 26px, transparent padding carries the rest, so a
|
|
864
|
+
message with reactions does not grow by a full line. A plain-HTML surface writes the same classes (markup at the top of
|
|
865
|
+
`reactions.css`, and the catalog's "Reactions" section).
|
|
866
|
+
|
|
715
867
|
## Report pages (0.4.0)
|
|
716
868
|
|
|
717
869
|
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 +911,14 @@ silently: a `slice` prints "+N more" or "See all N". A table renders at most 100
|
|
|
759
911
|
|---|---|---|---|---|
|
|
760
912
|
| 1 | Data table | `DataTable` | `ui-table-wrap > table.ui-table(.ui-table--sticky-first, --compact)`; `tfoot.ui-table__totals`, `.ui-table__footer` | DataTable |
|
|
761
913
|
| 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 |
|
|
914
|
+
| 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 |
|
|
915
|
+
| 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
916
|
| 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 |
|
|
917
|
+
| 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
918
|
| | 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
919
|
|
|
768
|
-
`./collections` is a plain module (a server component renders it, render functions and all); `IndexTable`
|
|
769
|
-
|
|
920
|
+
`./collections` is a plain module (a server component renders it, render functions and all); `IndexTable` and
|
|
921
|
+
`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
922
|
the callbacks (`onSort`, `onSelectionChange`, a bulk action's `onAction`) need a client parent. Neither module reaches
|
|
771
923
|
the admin's first load (`kit-plain-bundles.test.ts` checks the shell import carries none of their code).
|
|
772
924
|
|
|
@@ -777,9 +929,56 @@ the admin's first load (`kit-plain-bundles.test.ts` checks the shell import carr
|
|
|
777
929
|
two or more in-place editors is not linked (only its title is). HTML: `tr[data-href]` + kit-plain.js; React:
|
|
778
930
|
`IndexTable` rows with `href`; a resource list's title link covers its row (no script).
|
|
779
931
|
- **One visible button per row at most**, the rest in the ⋯ menu (`ui-menu`). **Never shown only on hover.**
|
|
932
|
+
- **An open ⋯ menu is never cut by its frame (0.18.0).** It floats in the browser's top layer (`popover="manual"`,
|
|
933
|
+
`position: fixed` against its button): below the button, above it when the room below is short and the room above is
|
|
934
|
+
larger, 8px from the viewport's edges, scrolling inside when taller than the room. It closes when a scroll moves its
|
|
935
|
+
button (the page, or a frame around the menu) or the window resizes; a scroll inside the list or elsewhere (a log
|
|
936
|
+
following its output) does not count. Tab order, Escape (focus back on ⋯) and "a click in the
|
|
937
|
+
menu is not a click on the row" are unchanged: the list is still the details' child. React `Menu` does it itself;
|
|
938
|
+
plain HTML gets it from kit-plain.js (`initMenuLayer()`, runs by itself). No popover API (Safari < 17) or no script:
|
|
939
|
+
the absolute list, as before. GemFactory Members (#197) is the case: at 820 / 900 / 1280 the last rows' menus were
|
|
940
|
+
half hidden inside the table's scrolling frame.
|
|
941
|
+
- **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
|
|
942
|
+
it; the aside and the actions share ONE line under the body (aside left, actions right, from the body's edge); the
|
|
943
|
+
media spans both lines; a row without media gives the body the whole width; a body that is only its title keeps
|
|
944
|
+
them on its line. The row's direct children are the four slots (a checkbox goes in `__media`). So a long
|
|
945
|
+
subtitle and a body of many meta lines both get shorter than in 0.13.0 (`collections.test.ts` checks the geometry
|
|
946
|
+
at 320 / 390 / 768 for both, with the 0.13.0 rule and the `flex: 1 1 0` patch as negative controls).
|
|
947
|
+
- **A compact list wraps when its labels carry data (0.15.0).** `CompactList wrap` (`ui-list--wrap`): the label and an
|
|
948
|
+
activity's detail wrap instead of ending in "…", because the cut text's `title` is mouse-only. A side rail of short
|
|
949
|
+
labels keeps one line. `CompactItem.body` (activity only) is the event's own text under them (`.ui-list__content`,
|
|
950
|
+
body size, never cut; paragraphs as `<p>`); `labelText` puts the full label in `title`.
|
|
780
951
|
- **Below 768 of its container an index table becomes a list** (title line; the other cells on one caption line,
|
|
781
952
|
`data-label` prefixes, `data-condensed="hide"` drops; checkbox and actions kept; rows ≥ 44); sort moves to the chips
|
|
782
953
|
of `ui-index__sortbar`. **A data table never folds:** it scrolls in its frame with `--sticky-first`.
|
|
954
|
+
- **A data table reads from the top of its rows (0.16.0)**, as Polaris DataTable (`verticalAlign` "top"): one-line
|
|
955
|
+
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
|
|
956
|
+
text lines up (`calc((1lh - 24px) / 2)`). The index table keeps `middle`. The row header (`th.ui-table__rowhead`)
|
|
957
|
+
wraps like any cell (Polaris `firstColumn`) with Polaris's first-column width as a floor (9.0625rem; the frame
|
|
958
|
+
scrolls rather than squeezing it to a word a line); `truncate` or `ui-clamp-2` when it must stay short. A column's
|
|
959
|
+
`minWidth` (DataTable) is the floor of a column whose cells wrap, and replaces the row header's (`0` for a row
|
|
960
|
+
header that is always short: a date, an id). A link alone on its
|
|
961
|
+
line is 24px in the row header and in a list inside a cell (`li > a`), as `td > a` already was.
|
|
962
|
+
- **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
|
|
963
|
+
picture (a moodboard, a product shot, a social post). No other ratio: any other value draws 16:10.
|
|
964
|
+
- **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
|
|
965
|
+
the HTML behind nested `<details>` (content-radar ideas reaches 2,000). With `hasMore` the column shows the cards the
|
|
966
|
+
server sent (`boardShown(page)`: 12, then 24 more a page, Chris 05/10/2026) and ends in ONE "Show 24 more":
|
|
967
|
+
`moreHref` is this page with the column one page further (`?col=ideas&page=2`, cumulative, so no script, Back and a
|
|
968
|
+
sent link all work; `moreHref` alone means there is more, `hasMore: false` is the last page). On a plain-HTML page
|
|
969
|
+
(`fetchMore`, i.e. `a[data-gb-more]`) kit-plain.js fetches it, appends the cards whose `data-id` are not on the board
|
|
970
|
+
yet, takes the next link and the count, puts the URL in the address bar, says "24 more cards in <column>" and focuses
|
|
971
|
+
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
|
|
972
|
+
`onLoadMore(columnId)` + `loading`. Cards need ids: `items: [{ id, card, name? }]` instead of `cards`.
|
|
973
|
+
- **Moving cards between columns is optional (0.18.0)** (`Board onMove(cardId, toColumn, toIndex)`; plain HTML
|
|
974
|
+
`.ui-board[data-gb-move]` + `li[data-id][tabindex="0"][draggable="true"]`, event `gb:move` { id, from, to, index },
|
|
975
|
+
cancelable: not cancelled, kit-plain.js moves the `li` and the counts). Drag with a mouse, or Tab to a card, Space,
|
|
976
|
+
arrows (↑ ↓ in the column, ← → to the next one), Space or Enter to drop, Escape to put it back, each step said in the
|
|
977
|
+
board's live region. While held, the card has `data-gb-lifted="key"` or `"drag"` (a surface may change its layout for a
|
|
978
|
+
keyboard hold, never inside dragstart: Chrome cancels the drag), the target column `data-gb-over`, the landing line
|
|
979
|
+
`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
|
|
980
|
+
`onMove` the board stays read-only (no tab stops, no drag). A phone cannot drag: keep a way that is not dragging (a
|
|
981
|
+
select, a ⋯ "Move to").
|
|
783
982
|
- **At 1280 a table fits its page frame.** Fix in this order: a `canvas` page if the content really is wide; move
|
|
784
983
|
low-priority columns to the detail page; fold attributes into the `ui-sub` line.
|
|
785
984
|
- **No zebra, no uppercase header, no card per row, no grid that changes its column count per viewport** (auto-fill
|
|
@@ -787,7 +986,22 @@ the admin's first load (`kit-plain-bundles.test.ts` checks the shell import carr
|
|
|
787
986
|
- **A checkbox column only with bulk actions.** The bulk bar covers the header (the table does not move), carries its
|
|
788
987
|
own select-all, Shift+click takes a range, Escape clears. HTML: `input[data-gb-select]` / `[data-gb-select-all]`,
|
|
789
988
|
`gembossUI.selection(root)`, event `gb:selection`.
|
|
790
|
-
- **
|
|
989
|
+
- **A resource list selects the same way (0.16.0):** `SelectableResourceList` (`items`, `bulkActions`, optional
|
|
990
|
+
controlled `selected` + `onSelectionChange`, `selectedLabel`, `linkAs`). The checkbox is first in the media slot
|
|
991
|
+
(`label.ui-list__check`: 24, 44 under a finger, pulled into the row's padding so a 64px row stays 64). One
|
|
992
|
+
difference from the table: the bar is the list's header from the start ("0 selected", actions disabled), because a
|
|
993
|
+
list has no header to cover and a bar that appears on the first check pushes the rows under the finger (talent-radar
|
|
994
|
+
/pool measured 74px at 1280, 194px at 390). The whole row still opens the item. HTML: the same `data-gb-select`
|
|
995
|
+
hooks in a `.ui-resource` (kit-plain.js enables the action buttons from 1 instead of showing the bar; an action with
|
|
996
|
+
its own rule sets its button on `gb:selection`, which fires after). collections-gate reports a list with selection
|
|
997
|
+
boxes and no bar (`select-without-bulk`). An open ⋯ menu or dialog takes Escape first; a closed one kept in a row
|
|
998
|
+
does not.
|
|
999
|
+
- **A sort control is 44 × 44 under a finger (0.18.0)**: the header button grows to 44 tall under `pointer: coarse`
|
|
1000
|
+
and to 44 wide under `pointer: coarse` or at 768 and below (a short label like "ID" was 44 × 30 on an iPad); the
|
|
1001
|
+
chips below 768 have the same floor. Under a mouse the header keeps its Polaris size.
|
|
1002
|
+
- **Sort lives in the URL** (`th[aria-sort] > a.ui-table__sort`); in React the sort links (header and the chips below
|
|
1003
|
+
768) are `linkAs`, the same element as the row titles (0.16.0), so a Next `Link` sorts without reloading the page;
|
|
1004
|
+
`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
1005
|
(`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
1006
|
- **States** (Gems/CLAUDE.md 9.2): empty (one main action) · no match (filters and header stay, "Clear filters") ·
|
|
793
1007
|
loading (a skeleton in the pattern's shape: a table keeps its real header, rows keep their height) · reloading
|
|
@@ -872,11 +1086,14 @@ something the base cannot express, add a token or a prop here, and every surface
|
|
|
872
1086
|
|
|
873
1087
|
| Path | What |
|
|
874
1088
|
|---|---|
|
|
875
|
-
| `gemboss-ui.css` | built at
|
|
1089
|
+
| `gemboss-ui.css` | built at stage time and shipped minified (0.19.0): `apps/admin/scripts/export-ui-css.mjs` (tokens + kit + field + `packages/ui/src/shell.css`, with Polaris `--p-*` resolved to their fallbacks) + `packages/ui/src/app-shell.css` + `auth.css` + `assistant.css` + `kit-plain.css` + `chat.css` + `chat-composer.css` (`packages/ui/scripts/build-css.mjs`) |
|
|
876
1090
|
| `packages/ui/src/shell.css` | the SHARED SHELL (rail, topbar, main, mobile drawer, the assistant's thread rail, popover surface, account chip). The admin's `gemboss-shell.css` @imports it on line 2; the export refuses a shared-shell rule in the admin's sheet or an admin-only rule here (one owner). |
|
|
877
1091
|
| `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
1092
|
| `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
1093
|
| `packages/ui/src/collections.css` | 0.13.0: tables, lists, grids, boards and their states (see "Collections"). A separate entry, NOT inside `gemboss-ui.css`; the admin does not load it. |
|
|
1094
|
+
| `packages/ui/src/editor/` | 0.14.0, `@gemboss/ui/editor`: `EditorFrame`, `EditorPaneTabs`, `EditorTopbar`, `EditorBack`, `EditorChatToggle`, `EditorSaveState`, `DeviceToggle`. See "Editor frame". 0.17.0: `PanelTabs`, `PanelHeader`, `PanelAction`, `InspectorGroup`, `Field`, `Seg`, `ColorControl`, `RangeControl`, `EdgeBox`, `ImageControl`, `ChipButton`, `swatchState` (`./editor/swatch` too); 0.20.1 `cssUrl`. See "Settings panel". |
|
|
1095
|
+
| `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`. |
|
|
1096
|
+
| `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
1097
|
| `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
1098
|
| `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
1099
|
| `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 +1105,36 @@ something the base cannot express, add a token or a prop here, and every surface
|
|
|
888
1105
|
change `packages/ui/src/shell.css` (since 29/09/2026: before that it lived in the admin's stylesheet and was extracted).
|
|
889
1106
|
The admin and the next published version both carry it.
|
|
890
1107
|
|
|
1108
|
+
## Minified on publish, weighed per file (0.19.0)
|
|
1109
|
+
|
|
1110
|
+
The published tarball carries every stylesheet (`gemboss-ui.css`, `src/*.css`) and the two plain scripts
|
|
1111
|
+
(`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
|
|
1112
|
+
generated header (the gemboss commit; GemFactory's `/api/health` reads it). Next and Vite minify what they bundle anyway;
|
|
1113
|
+
the surfaces that serve the files as they come out of `node_modules` (GemFactory, taskdesk, gemcare, gempress) did not.
|
|
1114
|
+
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.
|
|
1115
|
+
|
|
1116
|
+
`npm publish` cannot minify in place (`exports` points at `src/`, which the admin compiles from), so the release goes
|
|
1117
|
+
through a staged copy: `packages/ui/scripts/stage.mjs` copies exactly what npm would pack into `.pack/`, minifying with esbuild
|
|
1118
|
+
(`legalComments: none`, no target: rewrites that compute the same style only). `npm pack` / `npm publish` run straight
|
|
1119
|
+
in `packages/ui` are refused by `prepack`.
|
|
1120
|
+
|
|
1121
|
+
The weight gate, `packages/ui/scripts/weight-gate.mjs`, reads the staged copy: gzip (level 6) of each minified file against its
|
|
1122
|
+
ceiling in `weight-budget.json` (measured + 512 bytes). Red when a file is over its ceiling, more than 1 KB under it
|
|
1123
|
+
(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
|
|
1124
|
+
a ceiling. CI runs `node packages/ui/scripts/weight-gate.mjs --ci`: build, stage into a temp dir, gate, then the
|
|
1125
|
+
negative controls (+5 KB of real rules in `ui.css` red; +20 KB of comment green; the unminified sources red; one
|
|
1126
|
+
unminified `kit-plain.js` red; a stylesheet with no ceiling red; a ceiling for a file not shipped red; a stale ceiling
|
|
1127
|
+
red; a `/*` inside a string is not a comment, green). Ceilings go down by themselves; raising one is not how a PR
|
|
1128
|
+
gets green (CLAUDE.md §9.4: shared weight only shrinks): it takes the reason in the PR and Chris's OK, and a new
|
|
1129
|
+
component goes in its own entry. `packages/ui/scripts/pack-lib.mjs` holds what the three scripts share (pack list, minifier, staging).
|
|
1130
|
+
|
|
891
1131
|
## Release
|
|
892
1132
|
|
|
893
1133
|
```bash
|
|
894
1134
|
cd packages/ui
|
|
895
|
-
npm version
|
|
896
|
-
npm
|
|
1135
|
+
npm version minor # or patch / major
|
|
1136
|
+
npm run stage # builds gemboss-ui.css + catalogs, minifies into .pack/, runs the weight gate
|
|
1137
|
+
npm publish .pack --auth-type=web # publish the STAGED copy (npm pack .pack for a local tarball)
|
|
897
1138
|
```
|
|
898
1139
|
|
|
899
1140
|
Publish from a worktree at the MERGED commit on `main`, not from a branch, so the registry and
|