@gemboss/ui 0.35.7 → 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 +290 -0
- package/README.md +178 -6
- package/catalog/editor.html +33 -15
- package/catalog/index.html +2020 -122
- package/gemboss-ui.css +3 -3
- package/package.json +15 -2
- package/parts/components.css +2 -2
- package/parts/core.css +1 -1
- package/parts/shell.css +1 -1
- package/src/assistant-chat.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/brand.tsx +9 -3
- package/src/chat-composer.css +1 -1
- package/src/chat.css +1 -1
- package/src/collections.css +1 -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/icons.tsx +2 -0
- package/src/index.ts +9 -1
- package/src/kit-plain.js +1 -1
- package/src/service-switcher-plain.js +1 -0
- package/src/service-switcher.css +1 -0
- package/src/service-switcher.tsx +222 -0
- package/src/services.ts +91 -0
- package/src/shell.css +1 -1
- package/src/toggle-bar.tsx +60 -0
- package/src/ui.css +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,296 @@
|
|
|
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
|
+
|
|
212
|
+
## 0.36.1 (15/10/2026)
|
|
213
|
+
|
|
214
|
+
**The package reads only tokens it declares.** It read eight `--gemboss-*` tokens it never declared, each with a
|
|
215
|
+
fallback, so nothing failed: the browser painted the fallback hex. That hex is light-theme only, and in one place it
|
|
216
|
+
was wrong in light too. gemcare's copy of gemboss-ui.css failed its token gate on six of them (15/10/2026).
|
|
217
|
+
|
|
218
|
+
Fixed
|
|
219
|
+
- Editor inspector repeater (`.gemboss-ei-rep-*`): the rows and the add button had a `#2a2a3a` border on the white
|
|
220
|
+
panel, and the move/remove buttons hovered to that same near-black behind `#1b1c1e` glyphs. Now `--gemboss-divider`
|
|
221
|
+
and `--gemboss-hover`, like the rest of the panel.
|
|
222
|
+
- Chat markdown in dark mode outside a bubble: table headers, code blocks, inline code and images kept `#f7f7f7`; quotes
|
|
223
|
+
and h3 kept `#616161`. They now read `--gemboss-primary-soft`, `--gemboss-divider` and `--gemboss-text-muted`, which
|
|
224
|
+
`theme-dark.css` redefines. In light the shift is a hair (header `#f7f7f7` → `#f2f3f5`, border `#e3e3e3` → `#e5e7ea`);
|
|
225
|
+
the code-language label goes `#8a8a8a` → `#686b71`.
|
|
226
|
+
- `.gemboss-ec-stop`: `--gemboss-danger` with the send button's `--gemboss-on-primary` ink. Light is unchanged (white
|
|
227
|
+
on `#d72c0d`, 4.9:1); dark is `#0d0d12` on `#ff6b5a` (6.9:1), where white would be 2.8:1.
|
|
228
|
+
- `.gemboss-page-draft-status button` border: `--gemboss-divider` (was `#ccc`), as kit buttons. Lifted board card
|
|
229
|
+
(`[data-gb-lifted]`): `--gemboss-shadow-raised`.
|
|
230
|
+
|
|
231
|
+
Unchanged on screen
|
|
232
|
+
- Focus rings read `var(--ui-focus, #5661f5)` (the undeclared `--gemboss-focus` in front of it is gone); the trace
|
|
233
|
+
`code` font is the literal mono stack; the assistant thread rail's selected row is the literal `rgba(0,0,0,0.07)`
|
|
234
|
+
the export already wrote for it (`--gemboss-active-bg` is lighter than the row's `--gemboss-hover`, so it would
|
|
235
|
+
read as less selected than a hovered row).
|
|
236
|
+
|
|
237
|
+
Tests: `tokens-declared.test.ts` (every `var(--gemboss-*)` in the package's CSS and JS names a token the package
|
|
238
|
+
declares; only the editor's runtime `--gemboss-pv-*` and `--gemboss-editor-topbar-h` are exempt). On 0.36.0 it fails
|
|
239
|
+
with 30 reads of eight tokens. `chat-plain.test.ts` (0.35.4) removed the bubble rules and expected a dark answer to fall
|
|
240
|
+
under 3:1; with theme tokens underneath it no longer does, so that control now holds it at 4.5:1 or more (6.54 at its
|
|
241
|
+
weakest). The seller's bubble, light and dark, still falls under 3:1 without its rules.
|
|
242
|
+
|
|
243
|
+
## 0.36.0 (15/10/2026)
|
|
244
|
+
|
|
245
|
+
**ServiceSwitcher: the logo goes home, the button beside it opens every other Gem service.** Chris 15/10/2026: on an
|
|
246
|
+
internal service, a click on the logo goes home, and beside it a dropdown to the other Gem services, ordered and organised,
|
|
247
|
+
working on a phone, a full panel when needed: a grid of each service's logo with a line under it saying what it is for.
|
|
248
|
+
README "ServiceSwitcher".
|
|
249
|
+
|
|
250
|
+
Added
|
|
251
|
+
- `ServiceSwitcher` (client): a nine-dot button after the brand (`aria-haspopup="dialog"`, `aria-expanded`; 32px, 44 under a
|
|
252
|
+
finger or at phone width) opening a native `<dialog class="gb-svc">` through `useModalDialog`. Desktop: under the topbar
|
|
253
|
+
from the brand's left edge, `min(760px, 100vw - 24px)` wide, at most 80% of the height. ≤640px: the whole screen, two
|
|
254
|
+
columns, `env(safe-area-inset-*)`. A search box (name, purpose, group; Enter follows the first match), the groups in
|
|
255
|
+
order, tiles that are links to each home (same tab); the current service's tile is `aria-current="page"` with the accent
|
|
256
|
+
ring and still links home. Arrows by place on screen, Home / End, one tile in the Tab order; it opens on the current tile.
|
|
257
|
+
- `BrandLockup switcher={{ current, services?, labels?, allHref?, search? }}`: the logo link as before, the button after it.
|
|
258
|
+
Without the prop the lockup renders exactly as before.
|
|
259
|
+
- `GEM_SERVICES`, `SERVICE_GROUPS`, `SERVICE_TINTS`, `groupServices()` and the types `GemService`, `ServiceId`,
|
|
260
|
+
`ServiceGroup`, `ServiceGroupId`, `ServiceTint` (src/services.ts, plain data, also `@gemboss/ui/services`): sixteen
|
|
261
|
+
services in six groups, in the order a store is built and run: GemBoss; Build the store (GemFactory, GemMedia, GemForm);
|
|
262
|
+
Read the market (GemWatcher, GemMarket, GemCapture, Search Radar); Content and community (GemPress, Content Radar,
|
|
263
|
+
GemCommunity, GemCommunity News); Customers (GemCare, GemFeedback); Team and partners (Partner Radar, Taskdesk).
|
|
264
|
+
- `SERVICES_HEALTH_URL` (GemFactory's `/services`), the panel's default foot link.
|
|
265
|
+
- `service-switcher.css`: its own entry `./service-switcher.css`, loaded by the component; `gemboss-ui.css` and the core
|
|
266
|
+
unchanged. Nine tint hues as `--gb-svc-*` tokens on `:root`, lighter under `:root[data-theme="dark"]`, always mixed with
|
|
267
|
+
the theme's own surface and text.
|
|
268
|
+
- `Icon name="apps"`: nine dots, drawn as `more` draws its three (the set is 45 icons).
|
|
269
|
+
- Plain HTML: `service-switcher-plain.js` (`@gemboss/ui/service-switcher-plain.js`, its own entry, in `sideEffects`),
|
|
270
|
+
`gembossUI.initServiceSwitcher(options?)` and `gembossUI.GEM_SERVICES`. A placeholder
|
|
271
|
+
`<span data-gb-service-switcher="<service id>" hidden>` right after `.gb-brand` is replaced by the component's button and
|
|
272
|
+
panel, the same DOM (asserted node for node against the React component in Chromium), so `service-switcher.css` draws it
|
|
273
|
+
unchanged, with the same behaviour. Options are the component's props (`labels`, `allHref`, `search`, `services`,
|
|
274
|
+
`className`, `current`) plus `root`. Idempotent; a call in `<head>` draws the rest at `DOMContentLoaded`. Adds to
|
|
275
|
+
`gembossUI` (after `kit-plain.js`, which it never replaces; alone it defines it), closes an open kit menu when the panel
|
|
276
|
+
opens. The catalogue is a literal block held to `services.ts` + `icons.tsx` by `service-switcher-plain-data.test.ts`
|
|
277
|
+
(`UPDATE_PLAIN_CATALOGUE=1` writes it). README "ServiceSwitcher" › "Plain HTML". `kit-plain.js` is unchanged.
|
|
278
|
+
- Catalog section "Chuyển dịch vụ" (the panel open at 1280, in dark, at 390, and a live plain-HTML frame running
|
|
279
|
+
`service-switcher-plain.js`); the shell section's lockup carries the switcher.
|
|
280
|
+
- `packages/ui/weight-budget.json`: ceilings for `packages/ui/src/service-switcher.css` and
|
|
281
|
+
`packages/ui/src/service-switcher-plain.js` (new entries; no existing ceiling raised).
|
|
282
|
+
- Tests: `service-switcher.test.tsx` (catalogue, markup, BrandLockup with and without it, the CSS reaching every class),
|
|
283
|
+
`service-switcher-browser.test.tsx` (Chromium, in `test:ui-browser`), `css-on-demand.test.ts` rows for the switcher,
|
|
284
|
+
BrandLockup and services.ts; `service-switcher-plain-data.test.ts` (Node: the plain catalogue against the TypeScript,
|
|
285
|
+
with its negative control, and the packaging) and `service-switcher-plain-browser.test.ts` (Chromium, in
|
|
286
|
+
`test:ui-browser`: DOM parity with the component, behaviour, phone, beside kit-plain.js, its own contract).
|
|
287
|
+
|
|
288
|
+
Changed
|
|
289
|
+
- `BrandLockup`'s module imports the switcher, so it brings `service-switcher.css` (1.8 KB gzip) and the switcher's code
|
|
290
|
+
wherever a bundler keeps what is imported, also without the prop.
|
|
291
|
+
- The phone topbar with a switcher in it: under 360px the search's floor drops from 96 to 56px (the bar has no other
|
|
292
|
+
room: ☰, mark, button, search, assistant, account). A bar without the switcher is unchanged.
|
|
293
|
+
|
|
294
|
+
|
|
5
295
|
## 0.35.7 (15/10/2026)
|
|
6
296
|
|
|
7
297
|
**The rail's scrollbar runs down the rail's edge.** The 12px side gutter was padding on `.icon-nav`, so the scroll box
|
package/README.md
CHANGED
|
@@ -62,12 +62,15 @@ 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 |
|
|
68
70
|
| assistant-chat | `@gemboss/ui/assistant-chat.css` | `packages/ui/src/assistant-chat.css` | the chat pieces the package draws (0.29.0): column, starters that wrap, meta, errors, notes, paperclip | 0.9 KB | `AssistantChat` and its pieces import it |
|
|
69
71
|
| minichart | `@gemboss/ui/minichart.css` | `packages/ui/src/minichart.css` | `Sparkline`, `ShareBar` (0.30.0) | 0.5 KB | those components import it |
|
|
70
72
|
| setup-guide | `@gemboss/ui/setup-guide.css` | `packages/ui/src/setup-guide.css` | `SetupGuide` (0.30.0) | 0.9 KB | `SetupGuide` imports it |
|
|
73
|
+
| service-switcher | `@gemboss/ui/service-switcher.css` | `packages/ui/src/service-switcher.css` | `ServiceSwitcher`: the button after the brand and its panel of Gem services (0.36.0) | 1.8 KB | `ServiceSwitcher` imports it, and `BrandLockup` through it (README "ServiceSwitcher") |
|
|
71
74
|
| bundle | `@gemboss/ui/gemboss-ui.css` | `gemboss-ui.css` | core + components + shell + auth + assistant + chat + portal + theme-dark | 21.9 KB | compatibility only (see CHANGELOG 0.23.0) |
|
|
72
75
|
|
|
73
76
|
The core alone is 14.7 KB against 21.7 for the bundle (each part opens with a short header naming the gemboss commit, kept through minify, so a surface serving the parts can still say which commit its CSS is); a list page adds collections (4.6), a sign-in page auth (2.6).
|
|
@@ -639,7 +642,7 @@ import "@gemboss/ui/editor.css"; // only on the screens that show an editor
|
|
|
639
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` |
|
|
640
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`) |
|
|
641
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` |
|
|
642
|
-
| 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` |
|
|
643
646
|
| Try door bar | `.gemboss-editor-topbar--guest` › `.gemboss-guest-brand` (`.gemboss-guest-mark`), `-note`, `-keep`, `-signin` |
|
|
644
647
|
|
|
645
648
|
Not here, on purpose: the body modes that hide the admin's own topbar and rail while an editor is open
|
|
@@ -696,7 +699,7 @@ import { DeviceToggle, EditorBack, EditorChatToggle, EditorFrame, EditorPaneTabs
|
|
|
696
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. |
|
|
697
700
|
| `EditorBack` | `.gemboss-editor-back` ← | A `<button type="button">`, or `as={Link}` / `as="a"` with `href`. Named "Back" for a screen reader (`label`). |
|
|
698
701
|
| `EditorChatToggle` | `.gemboss-editor-chat-toggle` | Shows / hides the left column; filled while open, pressed when collapsed. |
|
|
699
|
-
| `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). |
|
|
700
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). |
|
|
701
704
|
|
|
702
705
|
One module per piece behind one entry, so a bundler gives each editor only what it draws (measured: one module for all
|
|
@@ -870,7 +873,11 @@ gembossUI.initPasswordFields(); // the sign-in's reveal button; resetPasswordFie
|
|
|
870
873
|
|
|
871
874
|
In a bundled app (Next, Vite) load it once with a bare `import "@gemboss/ui/kit-plain.js";` in a client module: the
|
|
872
875
|
package lists the file in `sideEffects`, so the bundler keeps it (before 0.5.0 it dropped it and `gembossUI` was
|
|
873
|
-
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
|
|
874
881
|
buttons cannot be named. For `initShell`, put `data-label-open` / `data-label-close` on the ☰ button to have its
|
|
875
882
|
name follow the state; for `initPasswordFields`, `data-label-show` / `data-label-hide` on the reveal button.
|
|
876
883
|
|
|
@@ -1203,6 +1210,166 @@ import { Icon, SettingsDialog } from "@gemboss/ui";
|
|
|
1203
1210
|
`packages/ui/src/settings-dialog-browser.test.tsx` measures it in Chromium; its negative control runs without
|
|
1204
1211
|
`settings-dialog.css` and finds no 280px rail. Catalog section "Hộp cài đặt".
|
|
1205
1212
|
|
|
1213
|
+
## ServiceSwitcher (0.36.0): the logo goes home, the button beside it opens the other Gem services
|
|
1214
|
+
|
|
1215
|
+
Chris 15/10/2026: on an internal service, a click on the logo goes home (or reloads it), and beside it there should be a
|
|
1216
|
+
way to switch to the other Gem services, "ordered and organised logically", working on a phone, opening when needed as a
|
|
1217
|
+
full panel: a grid of each service's logo with a line under it saying what it is for.
|
|
1218
|
+
|
|
1219
|
+
```tsx
|
|
1220
|
+
import { AppShell, BrandLockup, ServiceSwitcher } from "@gemboss/ui";
|
|
1221
|
+
|
|
1222
|
+
<AppShell brand={<BrandLockup name="GemFactory" href="/" switcher={{ current: "gemfactory" }} />} … />
|
|
1223
|
+
|
|
1224
|
+
// A surface with a logo of its own (the GemBoss admin draws an image): the button right after it.
|
|
1225
|
+
<><Logo /><ServiceSwitcher current="gemboss" /></>
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
**When.** On the topbar of an INTERNAL service, beside its logo. Not on a public surface (GemFeedback's board,
|
|
1229
|
+
GemCommunity, a storefront): the panel names internal tools and links to their admin pages.
|
|
1230
|
+
|
|
1231
|
+
| | |
|
|
1232
|
+
| --- | --- |
|
|
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) |
|
|
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 |
|
|
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 |
|
|
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-*)` |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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) |
|
|
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 |
|
|
1243
|
+
|
|
1244
|
+
The groups, in order, are how a store is built and then run (`SERVICE_GROUPS`, services.ts):
|
|
1245
|
+
|
|
1246
|
+
| Group | Services |
|
|
1247
|
+
| --- | --- |
|
|
1248
|
+
| GemBoss | GemBoss (the seller app and the ops console) |
|
|
1249
|
+
| Build the store | GemFactory, GemMedia, GemForm, GemX |
|
|
1250
|
+
| Read the market | GemWatcher, GemMarket, GemPixel, GemCapture, Search Radar |
|
|
1251
|
+
| Content and community | GemPress, Content Radar, GemCommunity, GemCommunity News |
|
|
1252
|
+
| Customers | GemCare, GemFeedback |
|
|
1253
|
+
| Team and partners | Partner Radar, Taskdesk |
|
|
1254
|
+
|
|
1255
|
+
**Adding a service** (or moving one): one entry in `GEM_SERVICES` (`packages/ui/src/services.ts`), in its group, at its
|
|
1256
|
+
place in that group's story: `{ id, name, url, purpose, group, icon, tint }`. Its id joins `ServiceId`; the purpose is one
|
|
1257
|
+
plain sentence of at most 60 characters; the icon is a name of the set (draw a missing one in icons.tsx, by the set's
|
|
1258
|
+
rules); the tint one of `SERVICE_TINTS`, not the same as the tile before it. `service-switcher.test.tsx` checks all of that,
|
|
1259
|
+
and the homes as listed. Then run `UPDATE_PLAIN_CATALOGUE=1 npx vitest run packages/ui/src/service-switcher-plain-data.test.ts`:
|
|
1260
|
+
it writes the plain script's copy of the list (below), and without it that test is red. Every surface gets it at its next
|
|
1261
|
+
bump. A server reads the list from `@gemboss/ui/services`.
|
|
1262
|
+
|
|
1263
|
+
`packages/ui/src/service-switcher-browser.test.tsx` measures it in Chromium (where it sits at 1280, Escape / ✕ / backdrop,
|
|
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
|
|
1269
|
+
"Chuyển dịch vụ", and the shell section's lockup carries it.
|
|
1270
|
+
|
|
1271
|
+
### Plain HTML: `gembossUI.initServiceSwitcher()` (0.36.0)
|
|
1272
|
+
|
|
1273
|
+
For a page with no React (GemCapture's ops admin, GemPress's admin, GemCare, Taskdesk): the server writes a placeholder
|
|
1274
|
+
right after its `.gb-brand`, and `service-switcher-plain.js` draws the component's button and panel in its place, the
|
|
1275
|
+
same DOM (classes, ARIA, order), so `service-switcher.css` draws it unchanged, and the same behaviour. Its own file, not
|
|
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).
|
|
1279
|
+
|
|
1280
|
+
```html
|
|
1281
|
+
<head>
|
|
1282
|
+
<link rel="stylesheet" href="/ui/service-switcher.css"> <!-- require.resolve("@gemboss/ui/service-switcher.css") -->
|
|
1283
|
+
</head>
|
|
1284
|
+
…
|
|
1285
|
+
<div class="gemboss-topbar-start">
|
|
1286
|
+
<a class="gb-brand" href="/admin" aria-label="GemCapture home">…</a>
|
|
1287
|
+
<span data-gb-service-switcher="gemcapture" hidden></span> <!-- the value: this service's id (services.ts) -->
|
|
1288
|
+
</div>
|
|
1289
|
+
…
|
|
1290
|
+
<script src="/ui/kit-plain.js"></script> <!-- @gemboss/ui/kit-plain.js, if the page has it: FIRST -->
|
|
1291
|
+
<script src="/ui/service-switcher-plain.js"></script> <!-- @gemboss/ui/service-switcher-plain.js -->
|
|
1292
|
+
<script>gembossUI.initServiceSwitcher();</script>
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
```js
|
|
1296
|
+
gembossUI.initServiceSwitcher({ // every option is optional, and the component's prop of the same name
|
|
1297
|
+
labels: { trigger: "Chuyển dịch vụ", title: "Dịch vụ Gem", close: "Đóng", search: "Tìm dịch vụ",
|
|
1298
|
+
noMatch: "Không có dịch vụ nào khớp “{query}”.", all: "Mọi dịch vụ và tình trạng" },
|
|
1299
|
+
allHref: null, // the foot link; default GemFactory's /services; null draws none
|
|
1300
|
+
search: false, // the search box (default true)
|
|
1301
|
+
services: gembossUI.GEM_SERVICES.filter((s) => s.group !== "team"), // default every service
|
|
1302
|
+
className: "…", // added to the button's class
|
|
1303
|
+
current: "gemcapture", // instead of the placeholder's value
|
|
1304
|
+
root: element, // where to look for placeholders (default the document)
|
|
1305
|
+
}); // → [{ button, dialog, open(), close() }] for what it drew
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
| | |
|
|
1309
|
+
| --- | --- |
|
|
1310
|
+
| The placeholder | any element with `data-gb-service-switcher="<service id>"`, right after the brand; `hidden`, so it takes no room (nor the topbar's gap) before the script runs or where it never does. It is REPLACED by `button.gb-svc-trigger` + `dialog.gb-svc`: the button has to be the brand's next sibling, as `BrandLockup` draws it, for `.gb-brand + .gb-svc-trigger` (4px from the logo; at ≤460 40 wide and touching it, so the phone bar fits at 360) and for the panel to hang from the brand's left edge. A wrapper around the button would break all three |
|
|
1311
|
+
| Calls | idempotent: a placeholder is drawn once (it is gone after), so a second call draws nothing, and markup added later (`root`) is drawn by the next call. Called while the page still loads (in `<head>`), it draws what is parsed and the rest at `DOMContentLoaded`. Each switcher gets ids of its own |
|
|
1312
|
+
| `gembossUI` | a classic script that ADDS to it: after `kit-plain.js` when the page has it, because `kit-plain.js` defines nothing when `gembossUI` already exists (loaded the other way round it says so in the console). On its own it defines `gembossUI` with `initServiceSwitcher` and `GEM_SERVICES` only. With `kit-plain.js`, opening the panel closes an open kit menu (`closeMenus`: an open account menu would take the first Escape) |
|
|
1313
|
+
| Behaviour | the component's, item for item (table above): `showModal()`, Escape / ✕ / a press that starts on the backdrop, focus back to the button, opens on the current tile, search (Enter follows the first match, ↓ to the tiles), arrows by place on screen, Home / End, ↑ to the search box, one tile in the Tab order, a followed tile closes it. Strings are text |
|
|
1314
|
+
| The list | `GEM_SERVICES` as data inside the script, with the paths of the icons it names, held to `services.ts` + `icons.tsx` character for character by `service-switcher-plain-data.test.ts` (red when either moves without it; `UPDATE_PLAIN_CATALOGUE=1` writes it). A literal copy and a test, as the other plain scripts carry their TypeScript twins: the published file is the source minified, with no build step that could write it. A list of your own (`services`) uses the icons the catalogue names |
|
|
1315
|
+
| Inline | it holds no closing script tag, so a server can inline it (`readFileSync(require.resolve("@gemboss/ui/service-switcher-plain.js"))`), as it does `kit-plain.js` |
|
|
1316
|
+
|
|
1317
|
+
`packages/ui/src/service-switcher-plain-browser.test.ts` (Chromium, in `test:ui-browser`) renders the React component and
|
|
1318
|
+
this script for the same props on the same shell and compares the two DOMs node for node (generated ids aside): closed,
|
|
1319
|
+
open, filtered, nothing matching, after the keys, closed again, and with labels, no foot, no search and a list of its own;
|
|
1320
|
+
its negative control changes one attribute and the comparison sees it. Then the component's own checks on the plain page
|
|
1321
|
+
(1280, Escape / ✕ / backdrop, search, Enter, keys, 390 and 320 full screen, the bar at 320 / 360 / 390, kit-plain.js
|
|
1322
|
+
beside it, idempotence, `<head>`, alone, the wrong order). Catalog section "Chuyển dịch vụ", last frame: the plain page,
|
|
1323
|
+
live.
|
|
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
|
+
|
|
1206
1373
|
## Resizable panels
|
|
1207
1374
|
|
|
1208
1375
|
`ui-resizer` is the seam a person drags between two panels; `ui-resizing` on `<body>` is the drag in
|
|
@@ -1259,7 +1426,8 @@ const open = dock.presentation !== "hidden";
|
|
|
1259
1426
|
- No provider and no state: pass `hidden` while your assistant is open.
|
|
1260
1427
|
- The shortcut is a hint only: the surface owns the key binding.
|
|
1261
1428
|
- The phone topbar (≤760px) gives in this order when it is short: the search (to a 96px floor), then the brand name
|
|
1262
|
-
(ellipsis). The actions never shrink, so the account chip stays on screen (0.6.1; `topbar.test.tsx`).
|
|
1429
|
+
(ellipsis). The actions never shrink, so the account chip stays on screen (0.6.1; `topbar.test.tsx`). With a
|
|
1430
|
+
`ServiceSwitcher` in the bar (0.36.0), under 360px the search goes below its floor, to 56px: README "ServiceSwitcher".
|
|
1263
1431
|
- 0.6.0, not a patch: an existing `<AssistantLauncher>` call changes meaning (no longer fixed; the default copy is the
|
|
1264
1432
|
rail one, hidden on phones). A surface on `^0.5.x` must move to `^0.6.0` in the same PR that places the two copies.
|
|
1265
1433
|
|
|
@@ -1284,12 +1452,13 @@ import { AppShell, BrandLockup, AccountMenu, AccountMenuItem, Icon, RailItem } f
|
|
|
1284
1452
|
```
|
|
1285
1453
|
|
|
1286
1454
|
- `BrandLockup`: the admin wordmark's gem and proportions (26px tall, 13.3px/700 name) with your
|
|
1287
|
-
product's name. The name hides below 460px, as the admin's wordmark does.
|
|
1455
|
+
product's name. The name hides below 460px, as the admin's wordmark does. `switcher={{ current: "gemfactory" }}`
|
|
1456
|
+
(0.36.0) adds the button that opens every other Gem service right after it: README "ServiceSwitcher".
|
|
1288
1457
|
- `AccountMenu`: the admin's 32px initials chip and popover (same CSS). A `<details>`, so it opens
|
|
1289
1458
|
without JavaScript on a page that never hydrates; with React it also closes on an outside click and
|
|
1290
1459
|
on Escape. `AccountMenuItem` is a link (`href`), a POST form (`formAction`), or a button.
|
|
1291
1460
|
- `TopbarSearch` (0.6.3): see "Topbar search" below.
|
|
1292
|
-
- `Icon`: the GemBoss set,
|
|
1461
|
+
- `Icon`: the GemBoss set, 45 icons on a 20px grid with 1.5px strokes, drawn to sit next to Polaris.
|
|
1293
1462
|
`ICON_NAMES` lists them. Internal tools cannot use `@shopify/polaris-icons`: its licence only covers
|
|
1294
1463
|
applications that integrate with Shopify. Draw a missing one HERE, not in the surface: every rail
|
|
1295
1464
|
icon must keep `fill="none"` on its inner `<g>`, because `.gnav-ico svg { fill: currentColor }`
|
|
@@ -1314,6 +1483,8 @@ The admin's "Search ⌘K" box for every surface, as a real form (works with no J
|
|
|
1314
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.
|
|
1315
1484
|
- Shortcut on a page without React: `gembossUI.initSearch()` from `kit-plain.js` (⌘K / Ctrl+K and "/" focus it,
|
|
1316
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.
|
|
1317
1488
|
|
|
1318
1489
|
## Public portal pieces (0.7.0)
|
|
1319
1490
|
|
|
@@ -1690,6 +1861,7 @@ something the base cannot express, add a token or a prop here, and every surface
|
|
|
1690
1861
|
| `packages/ui/src/editor/` | 0.14.0, `@gemboss/ui/editor`: `EditorFrame`, `EditorPaneTabs`, `EditorTopbar`, `EditorBack`, `EditorChatToggle`, `EditorSaveState`, `DeviceToggle`. See "Editor frame". 0.17.0: `PanelTabs`, `PanelHeader`, `PanelAction`, `InspectorGroup`, `Field`, `Seg`, `ColorControl`, `RangeControl`, `EdgeBox`, `ImageControl`, `ChipButton`, `swatchState` (`./editor/swatch` too); 0.20.1 `cssUrl`. See "Settings panel". 0.22.0: `LayerList`, `SortableLayerList`, `LayerTree`, `EditorDrawer`. See "Layers and drawer". |
|
|
1691
1862
|
| `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`. |
|
|
1692
1863
|
| `packages/ui/src/editor-layers-plain.js` | 0.22.0: `gembossUI.initEditorLayers()`, the layers', the outline's and the drawers' behaviour for plain HTML. Load after `kit-plain.js`. |
|
|
1864
|
+
| `packages/ui/src/service-switcher-plain.js` | 0.36.0: `gembossUI.initServiceSwitcher()`, the ServiceSwitcher drawn and driven for plain HTML (`@gemboss/ui/service-switcher-plain.js`). After `kit-plain.js` when the page has it; works alone. See "ServiceSwitcher". |
|
|
1693
1865
|
| `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`. |
|
|
1694
1866
|
| `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. |
|
|
1695
1867
|
| `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. |
|