@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 +207 -0
- package/README.md +71 -11
- package/catalog/editor.html +33 -15
- package/catalog/index.html +143 -7
- package/gemboss-ui.css +3 -3
- package/package.json +9 -2
- package/parts/components.css +2 -2
- package/parts/core.css +1 -1
- package/parts/shell.css +1 -1
- package/src/auth-field.css +1 -0
- package/src/auth-field.tsx +74 -0
- package/src/auth-sso-plain.js +1 -0
- package/src/auth.css +1 -1
- package/src/auth.tsx +64 -1
- package/src/editor/controls.tsx +105 -4
- package/src/editor/frame.tsx +8 -4
- package/src/editor/index.ts +7 -3
- package/src/editor/panel.tsx +34 -10
- package/src/editor/repeater.tsx +244 -0
- package/src/editor/sortable.tsx +50 -24
- package/src/editor/tiles.tsx +51 -0
- package/src/editor-repeater.css +1 -0
- package/src/editor-tiles.css +1 -0
- package/src/editor.css +1 -1
- package/src/index.ts +3 -1
- package/src/kit-plain.js +1 -1
- package/src/service-switcher-plain.js +1 -1
- package/src/service-switcher.css +1 -1
- package/src/service-switcher.tsx +10 -3
- package/src/services.ts +4 -2
- package/src/toggle-bar.tsx +60 -0
- package/src/ui.css +1 -1
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`
|
|
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).
|
|
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
|
|
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
|
|
1259
|
-
|
|
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
|
|
1268
|
-
|
|
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
|
|