@gemboss/ui 0.36.1 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,213 @@
2
2
 
3
3
  Versions up to 0.9.0 are described in the README, each in the section that introduced it.
4
4
 
5
+ ## 0.39.0 (19/10/2026)
6
+
7
+ **A field of real pages for the sign-in panel, and an entrance for the screen.** Chris, on admin.gemboss.ai/login:
8
+ "animation ko quá lố nhưng thể hiện được những kỹ thuật, có chiều sâu như ở ngoài website". gemboss.ai opens on a
9
+ tilted field of real pages; the sign-in panel can now do the same, quieter.
10
+
11
+ Added
12
+ - **`AuthPageField`** (`auth-field.tsx`, its own entry `auth-field.css`, 1.1 KB gzipped): five columns of page thumbnails
13
+ the surface serves (portrait, 3:4) on a plane tipped back in perspective, drifting 100 to 130 seconds a lap, neighbours
14
+ the other way; a veil of the panel's ink keeps the words readable; a fine pointer leans the plane by at most 3 degrees.
15
+ Each column is dealt at least five pages so a lap never shows a gap. Images are lazy and live inside the panel, so a
16
+ screen without the panel (under 900 wide or 520 tall) never fetches them.
17
+ - **`AuthLayout` `art`**: anything drawn on the panel under its words. Pair it with `backdrop="none"`.
18
+ - **`className="gb-auth--alive"`** (with `AuthPageField` loaded): the mark, the name and the tagline, then each field,
19
+ then the panel's words rise 12px as they fade in, 0.7s each, all done by 1.1s; one light passes over the mark.
20
+
21
+ Nothing changes for a surface that does not opt in. Under `prefers-reduced-motion: reduce` the field is a still picture
22
+ and nothing rises.
23
+
24
+ **And "Continue with GemBoss" for the internal tools** (Chris, 16/10/2026: one sign-in for the fleet, "có cả continue
25
+ với gemboss hoặc nhập mật khẩu"; protocol in gemboss docs/fleet-sso.md).
26
+
27
+ Added
28
+ - **`AuthContinueWithGemBoss`** `{ service, next }`: put it first inside `AuthLayout`, above the form. An outline button
29
+ with the gem (the password form keeps the one dark button), then "or use the password". It links to GemBoss's door
30
+ for this tool; once the tool has set `gb_sso=1` (a browser that came in this way) the page continues by itself,
31
+ except over a reason the tool sent back (`?sso=not_member|invalid|off`, shown under the button) or `?password=1`.
32
+ - **`gemBossSignInUrl(service, next)`**, **`continuesByItself(cookie, search)`**: the same two rules, for a page that
33
+ writes the markup itself.
34
+ - **`auth-sso-plain.js`** (`@gemboss/ui/auth-sso-plain.js`, 1.1 KB gzipped, its own entry: kit-plain.js had no room
35
+ under its ceiling): `gembossUI.initContinueWithGemBoss()` for a sign-in page with no React. The page writes
36
+ `<a class="gb-auth-sso-btn" data-gb-sso="<tool>" data-next="/admin">` inside a `.gb-auth-sso`; the script points it at
37
+ GemBoss, fills in the gem, the label, the reason and "or use the password", and continues by itself under the same rules.
38
+ - auth.css: `.gb-auth-sso-btn`, `.gb-auth-sso-why`, `.gb-auth-sso .gb-auth-or` (auth.css 3,013 B gzip, under its ceiling).
39
+
40
+ **The save pill reads on the light bar.** `EditorSaveState`'s colours were the old dark topbar's: measured on the page editor's
41
+ bar (white) 19/10/2026, "✓ Draft saved" read 1.35:1 and "Saving…" 1:1, white on white. They are on the tokens now:
42
+ "Saving…" in `--gemboss-text-muted` on the bar (5.3:1), "✓ Draft saved" in `--gemboss-success-strong` on
43
+ `--gemboss-success-soft` (5.6:1), the spinner `--gemboss-divider` with a `currentColor` arc. The idle words ("Autosaves", the
44
+ page editor's "Draft saved · <when>") were the text at opacity .55, 3.31:1: they are `is-idle` now, the muted token (5.3:1), and
45
+ the inline `style` is gone from the markup. The theme and section editors' pills change with it.
46
+
47
+ **`EditorSaveState` can be unannounced.** `live` (default `true`, so every editor reads as before): `live={false}` draws the same
48
+ words and title without `role=status` / `aria-live`, for a surface that speaks through a live region of its own, in the page
49
+ from its first render and saying only what changed (the admin's page editor: "Saving…", "Saved just now", "Your last change
50
+ was not saved."). Two live regions for one pill announced it twice, and a region mounted with its first words is not read.
51
+
52
+ **The page and section editors' bar keeps its free space beside its centre.** Above 1000px `.gemboss-pageedit-top` is
53
+ `auto fit-content(100%) auto` with the default `justify-content` (it was `auto auto auto` with `space-between`, which the theme
54
+ editor keeps): the free space goes into the left and right tracks, half each, instead of between the three clusters. The
55
+ centre and the right sit exactly where they were (measured 19/10/2026 at 1024-1920 on the page editor's bar, the Studio's and
56
+ the section editor's). What changes is the left track: it has room past its content, which is where the admin's page editor
57
+ draws its save pill (in its own sheet, adding no width to any track). The page editor's left cluster declares its gap as
58
+ `--gemboss-tb-gap` (12px), so a child can hand it back; a bar with another gap sets the property.
59
+
60
+ **Toggles that are each on or off.** `ToggleBar` (from `@gemboss/ui` and `@gemboss/ui/toggle-bar`, 17/10/2026, Chris: the panel
61
+ was missing pickers; research `_local/gemboss/research/pickers-2026-10-16/pickers.md` §13 slice 8): a row of buttons drawn as
62
+ `.ui-seg`, any number pressed at once (`aria-pressed`), `role="toolbar"` with one Tab stop and ← → Home End along it. `keepFocus`
63
+ leaves the focus where it is on a press, so a text box keeps its selection. The admin's text toolbar (Bold, Italic, Underline,
64
+ Highlight, Accent color, Warning color, Link) draws through it. Not a switch: "pick one of these" stays `Segmented`.
65
+
66
+ Added
67
+ - `ToggleBar` (`toggle-bar.tsx`, a client module of its own: `segmented.tsx` stays free of hooks for the server components that
68
+ import `Segmented`).
69
+
70
+ **A segmented option can be a glyph with a name.** `Seg` options take `name` (17/10/2026): the button's accessible name and
71
+ its hover title, for a label that is an arrow (the admin's gradient direction: ↓ → ↘ ↗, named Down, Right, Down right, Up right).
72
+
73
+ **A choice of shapes is picture tiles.** `ChoiceTiles` (from `@gemboss/ui/editor`, 16/10/2026, Chris: the panel was missing
74
+ pickers; research `_local/gemboss/research/pickers-2026-10-16/pickers.md` §4, pattern P9): each option a tile with its picture
75
+ (`picture`, or a rectangle `shape` of w×h drawn to fit 26px) and its name, 4 to a row, the lit one `aria-pressed`; 56px tall,
76
+ past 44px under a finger. The admin draws an image's ratio with it (Original, 1:1, 4:5, 4:3, 3:2, 16:9, 9:16 + a Custom box).
77
+
78
+ Added
79
+ - `ChoiceTiles` (`editor/tiles.tsx`) and its own part `@gemboss/ui/editor-tiles.css` (parts.mjs, export, weight ceiling 1,000 B).
80
+ - `ChoiceTiles` sizes its tiles to fit (as many to a row as fit at 72px: 4 in a 350px panel, 3 on a phone, 2 wide ones for a choice of two) and a
81
+ name wraps to two lines instead of being cut ("Main image and thumbnails"): the admin's layout tiles (17/10/2026).
82
+
83
+ **A setting that is a list reads as rows.** `RepeaterList` + `useKeyedRows` (from `@gemboss/ui/editor`, 16/10/2026, Chris: the
84
+ panel was missing pickers; research `_local/gemboss/research/pickers-2026-10-16/pickers.md` §9). Each row: a grip to drag, its
85
+ name (folds it), ↑ ↓, duplicate, ✕; more than one row opens folded and a row added or duplicated opens; the count beside the
86
+ list's name; Alt+↑/↓ moves a row and keeps the focus. Rows are kept by key, so a row's typing follows it when it moves.
87
+
88
+ Added
89
+ - `RepeaterList` (`touch`: 44px buttons with a mouse too), `useKeyedRows` (`patch(key, fn)` writes a field by the row's key onto its
90
+ latest version; a refused write, `commit` answering false, puts the stored list back; the stored list is read by content),
91
+ `useRowDrag`, `dropIndex` (`editor/repeater.tsx`, `editor/sortable.tsx`).
92
+ - `@gemboss/ui/editor-repeater.css`: its own part, imported by `RepeaterList`.
93
+
94
+ Changed
95
+ - `SortableLayerList` drags through `useRowDrag`, the same rule as `RepeaterList` (behaviour unchanged; its browser tests pass).
96
+
97
+ Moved
98
+ - `.gemboss-ei-rep*` out of `editor.css` into `editor-repeater.css` (editor.css 8.4 → 8.2 KB gzip).
99
+
100
+ **The settings panel reads like the rest of the admin.** Chris 16/10/2026: the panel's controls looked bigger than the kit's
101
+ and Polaris's. Measured (a countdown's Settings tab, 1280, a mouse): every control was Polaris-sized already (field 32/13,
102
+ chips 26-28/12, buttons 28); the panel was long, 3,363px, because each setting was a label line over its control (70px a
103
+ setting) and 22% of the height was help lines. Research of GemPages, PageFly and Replo:
104
+ `_local/gemboss/research/inspector-density-2026-10-16.md`.
105
+
106
+ Changed, inside an inspector group (`InspectorGroup`) only; a plugin's settings form is unchanged:
107
+ - `Field` takes `row`: label left (2 parts), control right (3 parts), for a one-value setting. GemPages draws a setting in 52px
108
+ this way. The admin sets it on toggles, selects, chip sets of up to three, numbers (the unit beside the box), ranges and
109
+ colours. Value controls (chips + box + slider), longer chip sets, text, dates, spacing, images and lists keep the label
110
+ above: their parts need the panel's width (measured: "None 1 2 3 4" was cut in the control column). A countdown's
111
+ Settings tab went from 3,363px to 2,197px.
112
+ - `Field`'s `hint` with a label is behind a ? beside the label (`button.gemboss-ei-help`, named "About <label>", described by
113
+ the help line): a click, Enter or a tap shows it as the line under the control, again hides it. Outside a group the
114
+ ? is not drawn and the line shows, as before. The line is still one node (`p.gemboss-ei-hint.gemboss-ei-help-text`).
115
+ - Labels are quiet: 13px / 450, secondary (Polaris bodyMd).
116
+ - Group titles (`.gemboss-ei-gh`) are Polaris headingSm everywhere: 13px / 650, sentence case, the text colour. They were
117
+ 10-11px uppercase with letter spacing, the old vault look.
118
+
119
+ Removed
120
+ - `.gemboss-pageedit-modes`: no source in the fleet draws it.
121
+ - `.gemboss-ei-img-grid` / `.gemboss-ei-img-cell`: the image field's inline library grid. The admin's image field opens the
122
+ media picker since gemboss #4474 (16/10/2026) and nothing in the fleet draws the grid any more.
123
+
124
+ ## 0.38.0 (15/10/2026)
125
+
126
+ **A setting you can click as well as type.** `ValueControl` (from `@gemboss/ui/editor`): a number's named values as chips, then
127
+ its box with the unit and, for a bounded range, a slider. Chris 15/10/2026: "có option để vừa bấm được vừa nhập được, lười thì bấm".
128
+ Research of GemPages, PageFly, Replo and Shopify: Shopify's reference themes never show a bare number box for a style; GemPages and
129
+ PageFly both left border width one.
130
+
131
+ Added
132
+ - `ValueControl`: props `presets` (`{ value, label }`, smallest first), `text` (what the box holds), `suffix`, `slider`
133
+ (`{ min, max, step, value }`), `note`, and `onPreset` / `onText` / `onCommit` / `onSlide` / `onKeyDown`. The chips are the kit's
134
+ segmented control (`Seg`): the one whose value the box holds lights, a value of its own lights none. The caller owns the value.
135
+ - `.gemboss-ei-value`, `.gemboss-ei-value-row`, `.gemboss-ei-value-box`: chips on top, then the 72px box with its unit and the
136
+ slider filling the rest of the row.
137
+
138
+ ## 0.37.1 (16/10/2026)
139
+
140
+ **Three things the ServiceSwitcher rollout found on the 16 services.** A sliver of the search's first letter at 320
141
+ (GemMedia), the panel opening 6px over a header of the surface's own (GemNews's admin), and a React hydration warning
142
+ about `<html>` on the Next surfaces that import `kit-plain.js` (talent-radar, post-studio: 1 load in 3).
143
+
144
+ Fixed
145
+ - **The search under its floor is its icon.** In a bar with the switcher, under 360px, the search goes below its 96px
146
+ floor (0.36.0), and its words did not fit: measured in Chromium with the switcher, the assistant and the account, the box
147
+ is 56 / 58 / 78 wide at 300 / 320 / 340 and showed 0 / 2 / 22px of its words (a cut letter). Below 96 wide it is now the
148
+ search icon alone, centred (a container query on the centre slot, `gb-svc-search`, set only in that bar under 360).
149
+ Both topbar searches: `TopbarSearch`'s input covers the box, placeholder unpainted, name still its `aria-label`, the
150
+ icon stepping aside while it is focused or holds words; `SearchDialog trigger="field"`'s words stay the link's name,
151
+ visually hidden. At 359 and up, and in a bar without the switcher, every number is unchanged (30 of 36 measured rows
152
+ identical; the 6 that change are those three widths, both searches).
153
+ - **…and the box is a target again there.** `TopbarSearch`'s input had the 0-2px left of the box at 300 / 320, so a tap at
154
+ the box's centre landed on the icon and focused nothing (typing went nowhere). It now lands in the input.
155
+ - **The panel hangs under the bar that holds the button**, the closest `.gemboss-platform-topbar`, `header` or
156
+ `[role="banner"]` around it (never above the button's own bottom), else under the button. Only the kit's topbar was
157
+ looked for, so on GemNews's admin (its own 56px `<header>`, the plain script) the panel was placed from the 32px button
158
+ and opened at y=50, over the header's last 6px; now 62. The kit's topbar is unchanged: 62 at 1280. The GemBoss admin's
159
+ `/admin` console draws its own 56px `<header class="gemboss-admin-topbar">` too, so it moves the same way (50 → 62,
160
+ read off its CSS, not measured); the seller shell's bar is the kit's topbar, unchanged. The React component and
161
+ `service-switcher-plain.js` share the selector (`BAR`) and the rule, held equal by a test.
162
+ - **`kit-plain.js` writes nothing on `<html>` as it loads.** The `data-gb-menus="float"` flag (kit-plain.css: an open
163
+ list nobody has floated yet stays unpainted, 0.18.0) now goes on in the capture phase of the click that opens a menu,
164
+ which runs before the browser opens the details (Enter and Space on a summary are that click too), and on any opening
165
+ after; it is put back if something took it off. A client module that imports the script can run before React hydrates
166
+ `<html>`, and React then reported the attribute. Measured on a Next 16.2.6 / React 19.2.6 app (`next dev`) with the
167
+ script made to run before hydration (`next/script` `beforeInteractive`, the order an import lands in 1 load in 3 on
168
+ post-studio; imported from a client module alone it ran after hydration 30 of 30 loads there): 0.36.1 warned on 20 of 20 loads ("A tree hydrated but some attributes of the server rendered HTML
169
+ didn't match"), 0.37.1 on 0 of 20; a click on its menu then sets the flag and floats it, 20 of 20. Plain pages keep
170
+ what the flag is for: the first frame after the opening click paints the list hidden, never inside its frame (20 of 20
171
+ by mouse and by Enter; without the flag it is inside its frame 20 of 20). post-studio's `suppressHydrationWarning` on
172
+ `<html>` is not needed for this any more.
173
+
174
+ Added
175
+ - Two services in the catalogue (`GEM_SERVICES`, `ServiceId`, the plain-HTML copy), both live since 15/10/2026: **GemX**
176
+ (`gemx`, https://x.gemboss.ai, "Tests and personalization across every store.", Build the store, after GemForm, icon
177
+ `sliders`, tint amber) and **GemPixel** (`gempixel`, https://pixel.gemboss.ai, "Events every store sends, and whether they
178
+ add up.", Read the market, after GemMarket, icon `activity`, tint blue). The panel now has 18 tiles. Their own admins can
179
+ pass `switcher={{ current: "gemx" }}` / `"gempixel"` once they are on this version.
180
+
181
+ Changed
182
+ - A menu opened by code (`details.open = true`) before any menu has been clicked may show inside its frame for one frame
183
+ before it floats, as every menu did before 0.18.0. A menu served open is floated as the script loads, as before.
184
+ - Weight (gzip of the minified file): `service-switcher.css` 1815 → 2040, `service-switcher-plain.js` 4559 → 4583,
185
+ `kit-plain.js` 8704 → 8742. No ceiling raised.
186
+
187
+ Tests: `service-switcher-browser.test.tsx` (Chromium): both searches at 300 / 320 / 340 in a bar with the switcher (no
188
+ words painted, icon centred within 1px, the centre of the box its target, the name kept, nothing wider than the screen),
189
+ a tap at 320 types into `TopbarSearch`, and 359 / 390 / 320-without-the-switcher unchanged; negative control: 0.36.1's
190
+ sheet paints 2px of the words at 320 and the centre tap lands on the icon. `service-switcher-plain-browser.test.ts`: the
191
+ panel 6px under GemNews's header, plain and React on the same pixel; 62 on the kit's topbar, both; 6px under the button in
192
+ no bar, both; negative control: 0.36.1's selector opens it at the button's bottom + 6, over the header.
193
+ `service-switcher-plain-data.test.ts`: one `BAR` in both files. `collections-board.test.ts`: no `data-gb-menus` until a
194
+ menu opens, the first frame after a mouse / Enter / Space opening never inside the frame (negative control: a kit that
195
+ never sets the flag paints it there); a React development build hydrating the whole document after kit-plain.js ran logs
196
+ no hydration warning and its menu floats with the flag (negative control: the flag set at load, 0.36.1, warns).
197
+
198
+ Also in this release: 0.37.0 (#4425), merged and not published until now (npm has 0.36.1).
199
+
200
+ ## 0.37.0 (15/10/2026)
201
+
202
+ **A spacing box's side can be dragged.** `EdgeBox` takes `onScrub(side, dx, keys)` and `onScrubEnd(side, moved)`: with them,
203
+ each side's letter (T · R · B · L) is a drag row, as Figma's and Webflow's number labels are. The package reports how far the
204
+ pointer moved and which keys are held; the caller turns that into a value and saves once when the drag lets go.
205
+
206
+ Added
207
+ - `.gemboss-ei-edge-scrub`: the letter as a 24px row the box's width (WCAG 2.5.8), `cursor: ew-resize`, hover fill
208
+ `--gemboss-hover`. A mouse or a pen drags; a finger scrolls the panel (`touch-action: pan-y`) and types in the box, which is
209
+ 44px under a finger. A press that moves under 2px is a click (`moved: false`).
210
+ - Without `onScrub` the letter is the plain `<span>` it was: nothing changes for other callers.
211
+
5
212
  ## 0.36.1 (15/10/2026)
6
213
 
7
214
  **The package reads only tokens it declares.** It read eight `--gemboss-*` tokens it never declared, each with a
package/README.md CHANGED
@@ -62,6 +62,8 @@ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as
62
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
63
  | collections | `@gemboss/ui/collections.css` | `packages/ui/src/collections.css` | tables, lists, grids, boards | 4.6 KB | `IndexTable`, `ResourceList`, `Board`… import it |
64
64
  | editor | `@gemboss/ui/editor.css` | `packages/ui/src/editor.css` | the editors | 7.9 KB | the `@gemboss/ui/editor` components import it |
65
+ | editor-repeater | `@gemboss/ui/editor-repeater.css` | `packages/ui/src/editor-repeater.css` | `RepeaterList`: a setting that is a list, as rows | 0.9 KB | `RepeaterList` imports it |
66
+ | editor-tiles | `@gemboss/ui/editor-tiles.css` | `packages/ui/src/editor-tiles.css` | `ChoiceTiles`: a closed choice of shapes as picture tiles | 0.5 KB | `ChoiceTiles` imports it |
65
67
  | reactions | `@gemboss/ui/reactions.css` | `packages/ui/src/reactions.css` | the emoji reactions and their picker | 0.8 KB | `Reactions` / `ReactionPicker` import it |
66
68
  | 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
69
  | 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 chat components (0.29.0); a page writing the markup links them |
@@ -640,7 +642,7 @@ import "@gemboss/ui/editor.css"; // only on the screens that show an editor
640
642
  | Controls | text / select `.gemboss-field.gemboss-ei-inp` (+ `.gemboss-ei-suffix`, `.gemboss-ei-mono`), segmented `.gemboss-ei-seg`, colour `.gemboss-ei-color` / `.gemboss-ei-swatches › .gemboss-ei-sw` / `.gemboss-ei-swatch`, range `.gemboss-ei-range` + `.gemboss-ei-rangeval`, spacing box `.gemboss-ei-edgebox › .gemboss-ei-edge`, image `.gemboss-ei-img` (`-thumb`, `-ctl`, `-actions`, `-grid`, `-cell`), repeater `.gemboss-ei-rep-item` (`-head`, `-acts`, `-add`), blocks `.gemboss-ei-block` (`-head`, `-x`), add `.gemboss-ei-addrow` / `.gemboss-ei-addchip`, element tree `.gemboss-ei-tree › .gemboss-ei-tnode` (`-tname`, `-tacts`, `-tadd`), notes `.gemboss-ei-note`, `.gemboss-ei-oos` (`-h`), `.gemboss-ei-empty`, drag `.gemboss-ei-drag` / `.gemboss-ei-dragmask` |
641
643
  | Layers | `.gemboss-layers` › `.gemboss-layer-head` + `.gemboss-layers-list` › `.gemboss-layer` (`.is-sel`) › `.gemboss-layer-main` (`.gemboss-layer-type`) + `.gemboss-layer-acts` (`.gemboss-layer-del`) |
642
644
  | Settings drawer | `.gemboss-settings` › `.gemboss-picker-head` + `.gemboss-settings-body` › `.gemboss-settings-field`, `-grouphead`, `-gen`, `-note`, `-error`, `-handle`; `.gemboss-settings-foot` (`-save`, `-cancel`); backdrop `.gemboss-picker-backdrop`, empty `.gemboss-picker-empty` |
643
- | Page / section editor bar | `.gemboss-pageedit` (`-top`, `-title`, `-kind`, `-status`, `-standing`, `-scope`, `-modes`, `-notice`, `-chip`, `-pv`), `.gemboss-sec-type`, section insert `.gemboss-sec-insert-*`, draft preview `.gemboss-page-draft-stage` / `-status`, `.gemboss-page-section-content`, global styles bundle `.gemboss-bundle-look` |
645
+ | Page / section editor bar | `.gemboss-pageedit` (`-top`, `-title`, `-kind`, `-status`, `-standing`, `-scope`, `-modes`, `-notice`, `-chip`, `-pv`; the left cluster's gap is `--gemboss-tb-gap`, 12px, for a child that hands it back; above 1000px the bar's tracks are `auto fit-content(100%) auto`, so its free space sits beside the centre), `.gemboss-sec-type`, section insert `.gemboss-sec-insert-*`, draft preview `.gemboss-page-draft-stage` / `-status`, `.gemboss-page-section-content`, global styles bundle `.gemboss-bundle-look` |
644
646
  | Try door bar | `.gemboss-editor-topbar--guest` › `.gemboss-guest-brand` (`.gemboss-guest-mark`), `-note`, `-keep`, `-signin` |
645
647
 
646
648
  Not here, on purpose: the body modes that hide the admin's own topbar and rail while an editor is open
@@ -697,7 +699,7 @@ import { DeviceToggle, EditorBack, EditorChatToggle, EditorFrame, EditorPaneTabs
697
699
  | `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. |
698
700
  | `EditorBack` | `.gemboss-editor-back` ← | A `<button type="button">`, or `as={Link}` / `as="a"` with `href`. Named "Back" for a screen reader (`label`). |
699
701
  | `EditorChatToggle` | `.gemboss-editor-chat-toggle` | Shows / hides the left column; filled while open, pressed when collapsed. |
700
- | `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. |
702
+ | `EditorSaveState` | `.gemboss-editor-savestate` (`.is-idle`, `.is-saving` with the spinner, `.is-saved`) | `idle` draws nothing, or `idleText` as `.is-idle` (muted, 5.3:1 on the bar; the section editor's "Autosaves", the page editor's "Draft saved · <when>"). `.is-saved` is hidden at 760px and below. Texts and titles are props. Saving and saved are a polite live region (`role=status`) unless `live={false}`: for a surface with its own live region, present from its first render, that says only what changed (the admin's page editor). |
701
703
  | `DeviceToggle`, `MonitorGlyph`, `TabletGlyph`, `PhoneGlyph` | `.gemboss-pv-devices` › `.gemboss-pv-device` | Desktop / tablet / mobile. Moved from the admin unchanged; `TabletGlyph` exported 0.26.0 (it was inline in the toggle). |
702
704
 
703
705
  One module per piece behind one entry, so a bundler gives each editor only what it draws (measured: one module for all
@@ -871,7 +873,11 @@ gembossUI.initPasswordFields(); // the sign-in's reveal button; resetPasswordFie
871
873
 
872
874
  In a bundled app (Next, Vite) load it once with a bare `import "@gemboss/ui/kit-plain.js";` in a client module: the
873
875
  package lists the file in `sideEffects`, so the bundler keeps it (before 0.5.0 it dropped it and `gembossUI` was
874
- undefined in production builds). Use `confirm` instead of `window.confirm()`: the browser's box has no GemBoss look, no critical tone, and its
876
+ undefined in production builds). Loading it writes nothing on `<html>` (0.37.1): the `data-gb-menus` flag kit-plain.css
877
+ reads (an open menu's list stays unpainted until the script floats it) goes on in the capture phase of the click that
878
+ opens a menu, before the browser opens it. Up to 0.36.1 the script set it as it loaded, and a client module can run
879
+ before React hydrates `<html>`: React reported an attribute the server did not render (talent-radar, post-studio: 1
880
+ load in 3). `suppressHydrationWarning` on `<html>` is not needed for it any more; drop it unless something else writes there. Use `confirm` instead of `window.confirm()`: the browser's box has no GemBoss look, no critical tone, and its
875
881
  buttons cannot be named. For `initShell`, put `data-label-open` / `data-label-close` on the ☰ button to have its
876
882
  name follow the state; for `initPasswordFields`, `data-label-show` / `data-label-hide` on the reveal button.
877
883
 
@@ -1227,21 +1233,21 @@ GemCommunity, a storefront): the panel names internal tools and links to their a
1227
1233
  | Props | `current` (a `ServiceId`, required): the service this page belongs to. `services` (default `GEM_SERVICES`; another list keeps the groups' order). `labels`: `trigger` ("Switch service", the button's name and tooltip), `title` ("Gem services"), `close`, `search` ("Find a service"), `noMatch` ("No service matches “{query}”."), `all`. `allHref`: the foot link, default `SERVICES_HEALTH_URL` (GemFactory's `/services`, every service and its health); `null` draws no foot. `search` (default true). `className` (the button). `BrandLockup switcher={…}` takes the same, less `className`. Every prop is plain data, so a server component can pass it (BrandLockup is not a client module; ServiceSwitcher is) |
1228
1234
  | The button | `button.gb-svc-trigger`, the nine-dot `apps` icon (new in the set), `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls` the panel. 32px under a mouse, 4px from the brand; 44 under a finger or at phone width; ≤460px, where the brand is its 44px mark alone, 40 wide and touching it |
1229
1235
  | The panel | `<dialog class="gb-svc">` through the kit's `useModalDialog` (as `Dialog`, `Sheet`, `SettingsDialog`): Escape (one press, also from the search box), ✕ (the kit popup's `ui-modal__close`), a press that starts on the backdrop; the page behind is inert and holds still; focus goes back to the button. It opens on the current service's tile (not on ✕, not on the search box, which would pull up a phone's keyboard). Each opening starts with every service shown |
1230
- | Where | desktop: under the topbar, from the brand's left edge (measured when it opens, kept inside the window), `min(760px, 100vw - 24px)` wide, at most 80% of the height, its body scrolling. ≤640px: the whole screen, a head row (title + ✕), two columns, padded by `env(safe-area-inset-*)` |
1236
+ | Where | desktop: 6px under the bar that holds the button, from the brand's left edge (measured when it opens, kept inside the window), `min(760px, 100vw - 24px)` wide, at most 80% of the height, its body scrolling. The bar (0.37.1) is the closest `.gemboss-platform-topbar`, `header` or `[role="banner"]` around the button (62 at 1280 on the kit's 56px topbar; GemNews's own 56px `<header>` the same, where 0.36.1 measured from the 32px button and opened at 50, over the header's last 6px); with none, 6px under the button. Both implementations, one selector (`service-switcher-plain-data.test.ts`). ≤640px: the whole screen, a head row (title + ✕), two columns, padded by `env(safe-area-inset-*)` |
1231
1237
  | A tile | an `<a href>` to the service's home, same tab: a 40px mark (a kit `Icon` on a square tinted per service), the name (600), what it is for (muted, two lines at most). The current service's tile carries `aria-current="page"` and the accent ring, and still links to its own home. A followed tile closes the panel (a page back from the back/forward cache does not come back with it open); a new-tab click leaves it |
1232
1238
  | Search | name, purpose, id and group title; the groups keep their order; "No service matches “…”." when nothing does; Enter follows the first match ("med⏎" is GemMedia); ↓ goes to the first tile |
1233
1239
  | Keys | arrows move by place on screen (↓ from a tile is the one under it, also across groups), Home / End the first and the last, ↑ from the first row the search box. One tile is in the Tab order at a time |
1234
1240
  | Tints | `data-tint` on the tile, one of `SERVICE_TINTS` (iris, blue, teal, green, amber, orange, rose, violet, slate). A hue is never painted as it is: the mark is the hue mixed into the theme's surface (14%, 22% in dark), the glyph the hue mixed with the theme's text, so one value reads in light and in `data-theme="dark"` (lighter hues there). Measured in Chromium, both themes: every glyph ≥ 3:1 on its mark, names and purposes ≥ 4.5:1 |
1235
1241
  | CSS | `service-switcher.css`, its own entry, 1.8 KB gzip, imported by the component. `BrandLockup` imports the component, so a surface that renders `BrandLockup` gets the entry (and the switcher's code) even without the prop: 1.8 KB of CSS, and 5.8 KB gzip of JS for brand.tsx's module graph against 0.5 before (most of it the icon set and the popup hook, which a surface with a rail already has) |
1236
- | The topbar | `.gemboss-topbar-start` holds ☰, the brand and the button. ≤460 the button is 40 wide and touches the mark, so a full bar (search at its 96px floor, the assistant, the account) still fits at 360. Under 360 nothing else can give: in a bar that holds the switcher, the search's floor drops from 96 to 56 (still a box to tap), rather than the account leaving the screen |
1242
+ | The topbar | `.gemboss-topbar-start` holds ☰, the brand and the button. ≤460 the button is 40 wide and touches the mark, so a full bar (search at its 96px floor, the assistant, the account) still fits at 360. Under 360 nothing else can give: in a bar that holds the switcher, the search's floor drops from 96 to 56 (still a box to tap), rather than the account leaving the screen. 0.37.1: narrower than 96 (a container query on the centre slot, only in that bar under 360) the box is the search icon alone, centred: its words would show a few px (2 at 320). `TopbarSearch`: the input covers the box, its placeholder unpainted, its name its `aria-label`, the icon stepping aside while it is focused or holds words. `SearchDialog trigger="field"`: the words stay the link's name, visually hidden. Measured at 300 / 320 / 340 (56 / 58 / 78 wide); at 359 and up, and in a bar without the switcher, nothing changes |
1237
1243
 
1238
1244
  The groups, in order, are how a store is built and then run (`SERVICE_GROUPS`, services.ts):
1239
1245
 
1240
1246
  | Group | Services |
1241
1247
  | --- | --- |
1242
1248
  | GemBoss | GemBoss (the seller app and the ops console) |
1243
- | Build the store | GemFactory, GemMedia, GemForm |
1244
- | Read the market | GemWatcher, GemMarket, GemCapture, Search Radar |
1249
+ | Build the store | GemFactory, GemMedia, GemForm, GemX |
1250
+ | Read the market | GemWatcher, GemMarket, GemPixel, GemCapture, Search Radar |
1245
1251
  | Content and community | GemPress, Content Radar, GemCommunity, GemCommunity News |
1246
1252
  | Customers | GemCare, GemFeedback |
1247
1253
  | Team and partners | Partner Radar, Taskdesk |
@@ -1255,8 +1261,11 @@ it writes the plain script's copy of the list (below), and without it that test
1255
1261
  bump. A server reads the list from `@gemboss/ui/services`.
1256
1262
 
1257
1263
  `packages/ui/src/service-switcher-browser.test.tsx` measures it in Chromium (where it sits at 1280, Escape / ✕ / backdrop,
1258
- search, keys, a phone at 390 and 320, the topbar at 320 / 360 / 390 / 461, the tints in both themes, kit-plain.js beside
1259
- it); its negative control runs without `service-switcher.css` and finds the browser's centred dialog. Catalog section
1264
+ search, keys, a phone at 390 and 320, the topbar at 320 / 360 / 390 / 461, the search under its floor at 300 / 320 / 340
1265
+ for both searches, the tints in both themes, kit-plain.js beside it); its negative controls run without
1266
+ `service-switcher.css` (the browser's centred dialog) and with 0.36.1's sheet (2px of the search's words at 320).
1267
+ `service-switcher-plain-browser.test.ts` measures where the panel hangs, both implementations: the kit's topbar, GemNews's
1268
+ own header, no bar. Catalog section
1260
1269
  "Chuyển dịch vụ", and the shell section's lockup carries it.
1261
1270
 
1262
1271
  ### Plain HTML: `gembossUI.initServiceSwitcher()` (0.36.0)
@@ -1264,8 +1273,9 @@ it); its negative control runs without `service-switcher.css` and finds the brow
1264
1273
  For a page with no React (GemCapture's ops admin, GemPress's admin, GemCare, Taskdesk): the server writes a placeholder
1265
1274
  right after its `.gb-brand`, and `service-switcher-plain.js` draws the component's button and panel in its place, the
1266
1275
  same DOM (classes, ARIA, order), so `service-switcher.css` draws it unchanged, and the same behaviour. Its own file, not
1267
- `kit-plain.js` (which has 485 bytes gzip left under its ceiling and stays as it was): 4.5 KB gzip, loaded only where the
1268
- switcher is.
1276
+ `kit-plain.js` (which had 485 bytes gzip left under its ceiling at 0.36.0): 4.5 KB gzip, loaded only where the switcher is.
1277
+ A page with its own header (not the kit's topbar) needs nothing more: the panel hangs under the `header` or
1278
+ `[role="banner"]` around the button (0.37.1).
1269
1279
 
1270
1280
  ```html
1271
1281
  <head>
@@ -1312,6 +1322,54 @@ its negative control changes one attribute and the comparison sees it. Then the
1312
1322
  beside it, idempotence, `<head>`, alone, the wrong order). Catalog section "Chuyển dịch vụ", last frame: the plain page,
1313
1323
  live.
1314
1324
 
1325
+ ## Repeater rows (16/10/2026): `RepeaterList`, `useKeyedRows`
1326
+
1327
+ A setting that is a list (an accordion's items, a pricing table's plans, choice cards) is the element's content. The admin's
1328
+ Canvas panel and page editor drew it as boxes always open, moved one place at a time with ↑ ↓; PageFly and GemPages draw it as
1329
+ rows (research `_local/gemboss/research/pickers-2026-10-16/pickers.md` §9, P11). `RepeaterList` is that, for both admin
1330
+ surfaces, with its own part (`editor-repeater.css`, the rules that were in `editor.css` plus the new ones).
1331
+
1332
+ ```tsx
1333
+ import { RepeaterList, useKeyedRows } from "@gemboss/ui/editor";
1334
+
1335
+ const list = useKeyedRows(items, (next) => save({ items: next })); // a stable key per row; the whole list written per change
1336
+ <RepeaterList label="Questions" keys={list.keys} title={(i) => list.rows[i].question}
1337
+ body={(i) => { const key = list.keys[i]; return <QuestionFields row={list.rows[i]} onChange={(field, v) => list.patch(key, (row) => ({ ...row, [field]: v }))} />; }}
1338
+ onMove={list.move} onRemove={list.remove} onDuplicate={list.duplicate} onAdd={() => list.add({ question: "", answer: "" })} />
1339
+ ```
1340
+
1341
+ | Part | Markup | Behaviour |
1342
+ |---|---|---|
1343
+ | list | `.gemboss-ei-fld.gemboss-ei-rep` › `label` (+ `.gemboss-ei-rep-count`) · `hint` · `.gemboss-ei-rep-list` · `button.gemboss-ei-rep-add` | the count beside the name; "No items yet." (or `empty`) without rows; `canAdd={false}` turns add and duplicate off |
1344
+ | row | `.gemboss-ei-rep-item[data-key]` (`.is-dragging`, `data-drop=before\|after`) › `.gemboss-ei-rep-head` › `.gemboss-ei-rep-grip` · `button.gemboss-ei-rep-toggle[aria-expanded]` (`.gemboss-ei-rep-title`) · `.gemboss-ei-rep-acts` (↑ ↓ duplicate ✕) | drag the grip (a 2px line where it lands, `useRowDrag`, the Layers rule); the name folds and opens the row; Alt+↑/↓ on it moves the row and keeps the focus; more than one row opens folded, a row added or duplicated later opens |
1345
+ | body | `.gemboss-ei-rep-body[role=group]` | drawn only while the row is open |
1346
+
1347
+ `useKeyedRows(value, commit)` keeps a key per row that moves, duplicates and is removed with it, so a row's open state and its
1348
+ controls' unsaved typing follow it (measured in `editor-repeater-browser.test.tsx`, with the same list keyed by index as the
1349
+ negative control). It shows a written list at once; `commit` may answer (a Promise of true / false) and a refusal puts the
1350
+ stored list back; the stored list is read by content (a new `[]` each render is no change); one of its own writes coming
1351
+ back is not a change; any other stored list (an undo) replaces what is shown. A field writes with `patch(key, fn)`: by the
1352
+ row's key and onto its latest version, so a control that commits late (a colour, a slider) after a move changes the right row
1353
+ and after a remove changes none. `touch` makes the buttons 44px with a mouse too (the page editor's content panel).
1354
+ Drag is pointer only (HTML drag and drop does not start from a touch); ↑ ↓ stay, 44px under a finger (`editor.css`).
1355
+
1356
+ ## Choice tiles (16/10/2026): `ChoiceTiles`
1357
+
1358
+ A closed choice whose options are shapes (an image's ratio, a layout) as picture tiles with their name under them, as GemPages'
1359
+ layout selector draws them: the seller sees the shape before choosing it.
1360
+
1361
+ ```tsx
1362
+ import { ChoiceTiles } from "@gemboss/ui/editor";
1363
+
1364
+ <ChoiceTiles label="Aspect ratio" value={ratio} onChange={save}
1365
+ options={[{ value: "original", label: "Original", picture: <ImageIcon /> }, { value: "1-1", label: "1:1", shape: { w: 1, h: 1 } },
1366
+ { value: "16-9", label: "16:9", shape: { w: 16, h: 9 } }]} />
1367
+ ```
1368
+
1369
+ `.gemboss-ei-tiles[role=group]` › `button.gemboss-ei-tile[aria-pressed][data-value]` › `.gemboss-ei-tile-pic` (the picture, or a
1370
+ rectangle drawn to fit 26px: wide by its width, tall by its height) · `.gemboss-ei-tile-label`. 4 to a row, 56px tall. The caller
1371
+ owns the value; a click calls `onChange`. Its own part (`editor-tiles.css`).
1372
+
1315
1373
  ## Resizable panels
1316
1374
 
1317
1375
  `ui-resizer` is the seam a person drags between two panels; `ui-resizing` on `<body>` is the drag in
@@ -1425,6 +1483,8 @@ The admin's "Search ⌘K" box for every surface, as a real form (works with no J
1425
1483
  full 44 (0.6.6): the box is a form, so the input is what a finger has to hit. ⌘K hint hidden on focus and below 460px.
1426
1484
  - Shortcut on a page without React: `gembossUI.initSearch()` from `kit-plain.js` (⌘K / Ctrl+K and "/" focus it,
1427
1485
  Escape leaves it). "/" is ignored while another field has focus.
1486
+ - In a bar that holds the ServiceSwitcher, under 360px, the box may go below its 96px floor; there it is its icon alone
1487
+ (0.37.1, `service-switcher.css`; README "ServiceSwitcher" › The topbar). `SearchDialog trigger="field"` the same.
1428
1488
 
1429
1489
  ## Public portal pieces (0.7.0)
1430
1490