@gemboss/ui 0.35.2 → 0.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,169 @@
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.36.1 (15/10/2026)
6
+
7
+ **The package reads only tokens it declares.** It read eight `--gemboss-*` tokens it never declared, each with a
8
+ fallback, so nothing failed: the browser painted the fallback hex. That hex is light-theme only, and in one place it
9
+ was wrong in light too. gemcare's copy of gemboss-ui.css failed its token gate on six of them (15/10/2026).
10
+
11
+ Fixed
12
+ - Editor inspector repeater (`.gemboss-ei-rep-*`): the rows and the add button had a `#2a2a3a` border on the white
13
+ panel, and the move/remove buttons hovered to that same near-black behind `#1b1c1e` glyphs. Now `--gemboss-divider`
14
+ and `--gemboss-hover`, like the rest of the panel.
15
+ - Chat markdown in dark mode outside a bubble: table headers, code blocks, inline code and images kept `#f7f7f7`; quotes
16
+ and h3 kept `#616161`. They now read `--gemboss-primary-soft`, `--gemboss-divider` and `--gemboss-text-muted`, which
17
+ `theme-dark.css` redefines. In light the shift is a hair (header `#f7f7f7` → `#f2f3f5`, border `#e3e3e3` → `#e5e7ea`);
18
+ the code-language label goes `#8a8a8a` → `#686b71`.
19
+ - `.gemboss-ec-stop`: `--gemboss-danger` with the send button's `--gemboss-on-primary` ink. Light is unchanged (white
20
+ on `#d72c0d`, 4.9:1); dark is `#0d0d12` on `#ff6b5a` (6.9:1), where white would be 2.8:1.
21
+ - `.gemboss-page-draft-status button` border: `--gemboss-divider` (was `#ccc`), as kit buttons. Lifted board card
22
+ (`[data-gb-lifted]`): `--gemboss-shadow-raised`.
23
+
24
+ Unchanged on screen
25
+ - Focus rings read `var(--ui-focus, #5661f5)` (the undeclared `--gemboss-focus` in front of it is gone); the trace
26
+ `code` font is the literal mono stack; the assistant thread rail's selected row is the literal `rgba(0,0,0,0.07)`
27
+ the export already wrote for it (`--gemboss-active-bg` is lighter than the row's `--gemboss-hover`, so it would
28
+ read as less selected than a hovered row).
29
+
30
+ Tests: `tokens-declared.test.ts` (every `var(--gemboss-*)` in the package's CSS and JS names a token the package
31
+ declares; only the editor's runtime `--gemboss-pv-*` and `--gemboss-editor-topbar-h` are exempt). On 0.36.0 it fails
32
+ with 30 reads of eight tokens. `chat-plain.test.ts` (0.35.4) removed the bubble rules and expected a dark answer to fall
33
+ 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
34
+ weakest). The seller's bubble, light and dark, still falls under 3:1 without its rules.
35
+
36
+ ## 0.36.0 (15/10/2026)
37
+
38
+ **ServiceSwitcher: the logo goes home, the button beside it opens every other Gem service.** Chris 15/10/2026: on an
39
+ internal service, a click on the logo goes home, and beside it a dropdown to the other Gem services, ordered and organised,
40
+ 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.
41
+ README "ServiceSwitcher".
42
+
43
+ Added
44
+ - `ServiceSwitcher` (client): a nine-dot button after the brand (`aria-haspopup="dialog"`, `aria-expanded`; 32px, 44 under a
45
+ finger or at phone width) opening a native `<dialog class="gb-svc">` through `useModalDialog`. Desktop: under the topbar
46
+ from the brand's left edge, `min(760px, 100vw - 24px)` wide, at most 80% of the height. ≤640px: the whole screen, two
47
+ columns, `env(safe-area-inset-*)`. A search box (name, purpose, group; Enter follows the first match), the groups in
48
+ order, tiles that are links to each home (same tab); the current service's tile is `aria-current="page"` with the accent
49
+ ring and still links home. Arrows by place on screen, Home / End, one tile in the Tab order; it opens on the current tile.
50
+ - `BrandLockup switcher={{ current, services?, labels?, allHref?, search? }}`: the logo link as before, the button after it.
51
+ Without the prop the lockup renders exactly as before.
52
+ - `GEM_SERVICES`, `SERVICE_GROUPS`, `SERVICE_TINTS`, `groupServices()` and the types `GemService`, `ServiceId`,
53
+ `ServiceGroup`, `ServiceGroupId`, `ServiceTint` (src/services.ts, plain data, also `@gemboss/ui/services`): sixteen
54
+ services in six groups, in the order a store is built and run: GemBoss; Build the store (GemFactory, GemMedia, GemForm);
55
+ Read the market (GemWatcher, GemMarket, GemCapture, Search Radar); Content and community (GemPress, Content Radar,
56
+ GemCommunity, GemCommunity News); Customers (GemCare, GemFeedback); Team and partners (Partner Radar, Taskdesk).
57
+ - `SERVICES_HEALTH_URL` (GemFactory's `/services`), the panel's default foot link.
58
+ - `service-switcher.css`: its own entry `./service-switcher.css`, loaded by the component; `gemboss-ui.css` and the core
59
+ unchanged. Nine tint hues as `--gb-svc-*` tokens on `:root`, lighter under `:root[data-theme="dark"]`, always mixed with
60
+ the theme's own surface and text.
61
+ - `Icon name="apps"`: nine dots, drawn as `more` draws its three (the set is 45 icons).
62
+ - Plain HTML: `service-switcher-plain.js` (`@gemboss/ui/service-switcher-plain.js`, its own entry, in `sideEffects`),
63
+ `gembossUI.initServiceSwitcher(options?)` and `gembossUI.GEM_SERVICES`. A placeholder
64
+ `<span data-gb-service-switcher="<service id>" hidden>` right after `.gb-brand` is replaced by the component's button and
65
+ panel, the same DOM (asserted node for node against the React component in Chromium), so `service-switcher.css` draws it
66
+ unchanged, with the same behaviour. Options are the component's props (`labels`, `allHref`, `search`, `services`,
67
+ `className`, `current`) plus `root`. Idempotent; a call in `<head>` draws the rest at `DOMContentLoaded`. Adds to
68
+ `gembossUI` (after `kit-plain.js`, which it never replaces; alone it defines it), closes an open kit menu when the panel
69
+ opens. The catalogue is a literal block held to `services.ts` + `icons.tsx` by `service-switcher-plain-data.test.ts`
70
+ (`UPDATE_PLAIN_CATALOGUE=1` writes it). README "ServiceSwitcher" › "Plain HTML". `kit-plain.js` is unchanged.
71
+ - Catalog section "Chuyển dịch vụ" (the panel open at 1280, in dark, at 390, and a live plain-HTML frame running
72
+ `service-switcher-plain.js`); the shell section's lockup carries the switcher.
73
+ - `packages/ui/weight-budget.json`: ceilings for `packages/ui/src/service-switcher.css` and
74
+ `packages/ui/src/service-switcher-plain.js` (new entries; no existing ceiling raised).
75
+ - Tests: `service-switcher.test.tsx` (catalogue, markup, BrandLockup with and without it, the CSS reaching every class),
76
+ `service-switcher-browser.test.tsx` (Chromium, in `test:ui-browser`), `css-on-demand.test.ts` rows for the switcher,
77
+ BrandLockup and services.ts; `service-switcher-plain-data.test.ts` (Node: the plain catalogue against the TypeScript,
78
+ with its negative control, and the packaging) and `service-switcher-plain-browser.test.ts` (Chromium, in
79
+ `test:ui-browser`: DOM parity with the component, behaviour, phone, beside kit-plain.js, its own contract).
80
+
81
+ Changed
82
+ - `BrandLockup`'s module imports the switcher, so it brings `service-switcher.css` (1.8 KB gzip) and the switcher's code
83
+ wherever a bundler keeps what is imported, also without the prop.
84
+ - The phone topbar with a switcher in it: under 360px the search's floor drops from 96 to 56px (the bar has no other
85
+ room: ☰, mark, button, search, assistant, account). A bar without the switcher is unchanged.
86
+
87
+
88
+ ## 0.35.7 (15/10/2026)
89
+
90
+ **The rail's scrollbar runs down the rail's edge.** The 12px side gutter was padding on `.icon-nav`, so the scroll box
91
+ (`.gnav-scroll`) ended 12px short of the rail's border and its bar ran down the middle of the gutter, right against the
92
+ count pills. GemFactory's rail is long enough to scroll and showed it (Chris 15/10/2026: "cái vertical scroll bar nó
93
+ không đúng vị trí"); the admin's rail rarely scrolls, which is why nobody saw it there. Measured on GemFactory's own
94
+ rail at 1280: scroll box 12–227, rail 0–239.
95
+
96
+ Fixed
97
+ - The gutter pads `.gnav-scroll` (`padding-inline: 12px`) and `.gnav-bottom` (`padding: 0 12px 12px`) instead of
98
+ `.icon-nav` (now `padding: 12px 0 0`). The scroll box spans the rail (0–239), so the bar sits on the edge; every row
99
+ stays where it was (12–227 at 1280, 12–287 in the 300px phone drawer), and the bottom part too.
100
+
101
+ Note
102
+ - `Rail` holds `RailScroll` and `RailBottom`. Something placed straight inside `Rail` (not in either) no longer gets the
103
+ 12px gutter from the rail; no surface in this repo or GemFactory does that.
104
+
105
+ Tests: `rail-scrollbar-browser.test.tsx` (Chromium, built gemboss-ui.css, real `AppShell` on a rail that scrolls: the
106
+ scroll box spans the rail and rows keep their gutter at 1280 and in the open drawer at 390; negative control: the 0.35.6
107
+ rule appended over the sheet must fail). `launcher.test.tsx` measured "a full row" as the rail's width minus the rail's
108
+ own padding, which is no longer where the gutter is: it now measures the row box the launcher sits in, and also holds the
109
+ launcher to a real rail item's width (both 215 at every desktop width, as before).
110
+
111
+ ## 0.35.6 (14/10/2026)
112
+
113
+ **A link inside bold is a bold link.** Bold and italic were drawn as plain text whatever they held, so a link or a code
114
+ span inside them reached the reader as raw brackets and a path: `**[Proof listicle](/app/funnels?open=cmp_2): B won.**`
115
+ drew `[Proof listicle](/app/funnels?open=cmp_2): B won.` in bold. A model bolds the line that leads a list item, and that is
116
+ the line it puts its link on (the weekly market loop's report in GemBoss admin, 14/10/2026).
117
+
118
+ Fixed
119
+ - `ChatMarkdown` and `chat-plain.js` draw the inside of `**…**` and `*…*` as inline markdown: one `<strong>` holding the
120
+ link and the text around it, as GitHub draws it. A link the parser refuses is still refused, its words kept; plain
121
+ emphasis is drawn as before. One level deep: the inside of `**…**` / `*…*` holds no `*`.
122
+ - A link inside `<strong>` is as bold as the words around it: `strong > .gemboss-ec-md-link { font-weight: inherit }`
123
+ (its own `600` drew it thinner than the 700 around it).
124
+ - `parseInline` is unchanged: a `bold` / `italic` token still carries its inside as written. A renderer of its own (the
125
+ GemBoss admin's `MarkdownLite`) draws the same by rendering that text through `parseInline` again.
126
+
127
+ Changed
128
+ - A picture inside emphasis (`**![logo](…)**`) was bold text with its brackets; it now follows the rules it follows
129
+ anywhere: drawn from a fleet host, else a link labelled with its alt.
130
+
131
+ ## 0.35.5 (14/10/2026)
132
+
133
+ **The ← Back is a plain arrow when it is a button.** `EditorBack` draws a `<button>` unless it is given a link (`as={Link}`),
134
+ and `.gemboss-editor-back` set its size and colours but not the button's own border and background: as a button it wore
135
+ the browser's default, a 2px outset border on grey. Seen in the Studio's bar, whose way out became a ← on 14/10/2026.
136
+
137
+ Fixed
138
+ - `.gemboss-editor-back` resets `border`, `background`, `padding` and `font-family`; hover, active and focus are unchanged.
139
+
140
+ ## 0.35.4 (13/10/2026)
141
+
142
+ **What the seller wrote reads on their own bubble, and an answer reads in dark theme.** The admin chat draws the seller's
143
+ words as markdown, inside the dark user bubble, with the answer bubble's colours: a pasted link was black on near-black
144
+ (1.23:1), and a `code`, a quote, a heading, a table or a code block 1.07 to 2.75:1. Those markdown parts read three tokens
145
+ no theme defines (`--gemboss-text-subdued`, `--gemboss-border`, `--gemboss-surface-subdued`), so the answer bubble in dark
146
+ theme had the same fault (1.12 to 2.89:1).
147
+
148
+ Fixed
149
+ - `.gemboss-ec-msg.user .gemboss-ec-md-link` takes the bubble's own text colour, underlined in it: 17:1 in both themes.
150
+ - The seller's bubble, and the answer bubble in dark theme, derive the three tokens from their own text colour: every part
151
+ 9.4:1 or more. A light answer bubble keeps the old values exactly.
152
+ - A chat link has a focus ring in its own colour (`outline: 2px solid currentColor`), not the browser's default.
153
+
154
+ Also in this release: 0.35.3 (#4118), merged and not published until now.
155
+
156
+ ## 0.35.3 (12/10/2026)
157
+
158
+ **An inspector field's controls are a finger's size.** A picked element's own panel on the funnel canvas (Settings |
159
+ Advanced) drew its controls small on a phone: number boxes, selects and toggles 21 to 27px tall, the sides-link and clear
160
+ buttons 16 to 20px (measured 12/10/2026 at 320 and 390 with touch). The tabs, group heads and swatches were 44px already
161
+ (0.30.3); the fields were not.
162
+
163
+ Fixed
164
+ - Under `(pointer: coarse)`: every input (not a checkbox or radio), select and button inside a `.gemboss-ei-fld` has
165
+ `min-height: 44px`, and a button or a colour well (`input[type=color]`) `min-width: 44px`. A mouse sees no change. A
166
+ section's fields in the same inspector get it too.
167
+
5
168
  ## 0.35.2 (12/10/2026)
6
169
 
7
170
  **A folded index table's title is a 24px target.** visual-gate on partner-radar's list pages (/companies, /invites, /posts,
package/README.md CHANGED
@@ -68,6 +68,7 @@ its CSS out of the build with it (`sideEffects`). gzip of the minified file, as
68
68
  | 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
69
  | minichart | `@gemboss/ui/minichart.css` | `packages/ui/src/minichart.css` | `Sparkline`, `ShareBar` (0.30.0) | 0.5 KB | those components import it |
70
70
  | setup-guide | `@gemboss/ui/setup-guide.css` | `packages/ui/src/setup-guide.css` | `SetupGuide` (0.30.0) | 0.9 KB | `SetupGuide` imports it |
71
+ | 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
72
  | 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
73
 
73
74
  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).
@@ -1203,6 +1204,114 @@ import { Icon, SettingsDialog } from "@gemboss/ui";
1203
1204
  `packages/ui/src/settings-dialog-browser.test.tsx` measures it in Chromium; its negative control runs without
1204
1205
  `settings-dialog.css` and finds no 280px rail. Catalog section "Hộp cài đặt".
1205
1206
 
1207
+ ## ServiceSwitcher (0.36.0): the logo goes home, the button beside it opens the other Gem services
1208
+
1209
+ 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
1210
+ way to switch to the other Gem services, "ordered and organised logically", working on a phone, opening when needed as a
1211
+ full panel: a grid of each service's logo with a line under it saying what it is for.
1212
+
1213
+ ```tsx
1214
+ import { AppShell, BrandLockup, ServiceSwitcher } from "@gemboss/ui";
1215
+
1216
+ <AppShell brand={<BrandLockup name="GemFactory" href="/" switcher={{ current: "gemfactory" }} />} … />
1217
+
1218
+ // A surface with a logo of its own (the GemBoss admin draws an image): the button right after it.
1219
+ <><Logo /><ServiceSwitcher current="gemboss" /></>
1220
+ ```
1221
+
1222
+ **When.** On the topbar of an INTERNAL service, beside its logo. Not on a public surface (GemFeedback's board,
1223
+ GemCommunity, a storefront): the panel names internal tools and links to their admin pages.
1224
+
1225
+ | | |
1226
+ | --- | --- |
1227
+ | 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
+ | 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
+ | The panel | `<dialog class="gb-svc">` through the kit's `useModalDialog` (as `Dialog`, `Sheet`, `SettingsDialog`): Escape (one press, also from the search box), ✕ (the kit popup's `ui-modal__close`), a press that starts on the backdrop; the page behind is inert and holds still; focus goes back to the button. It opens on the current service's tile (not on ✕, not on the search box, which would pull up a phone's keyboard). Each opening starts with every service shown |
1230
+ | Where | desktop: under the topbar, from the brand's left edge (measured when it opens, kept inside the window), `min(760px, 100vw - 24px)` wide, at most 80% of the height, its body scrolling. ≤640px: the whole screen, a head row (title + ✕), two columns, padded by `env(safe-area-inset-*)` |
1231
+ | 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
+ | 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
+ | 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
+ | 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
+ | 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 |
1237
+
1238
+ The groups, in order, are how a store is built and then run (`SERVICE_GROUPS`, services.ts):
1239
+
1240
+ | Group | Services |
1241
+ | --- | --- |
1242
+ | 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 |
1245
+ | Content and community | GemPress, Content Radar, GemCommunity, GemCommunity News |
1246
+ | Customers | GemCare, GemFeedback |
1247
+ | Team and partners | Partner Radar, Taskdesk |
1248
+
1249
+ **Adding a service** (or moving one): one entry in `GEM_SERVICES` (`packages/ui/src/services.ts`), in its group, at its
1250
+ place in that group's story: `{ id, name, url, purpose, group, icon, tint }`. Its id joins `ServiceId`; the purpose is one
1251
+ 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
1252
+ rules); the tint one of `SERVICE_TINTS`, not the same as the tile before it. `service-switcher.test.tsx` checks all of that,
1253
+ and the homes as listed. Then run `UPDATE_PLAIN_CATALOGUE=1 npx vitest run packages/ui/src/service-switcher-plain-data.test.ts`:
1254
+ 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
1255
+ bump. A server reads the list from `@gemboss/ui/services`.
1256
+
1257
+ `packages/ui/src/service-switcher-browser.test.tsx` measures it in Chromium (where it sits at 1280, Escape / ✕ / backdrop,
1258
+ search, keys, a phone at 390 and 320, the topbar at 320 / 360 / 390 / 461, the tints in both themes, kit-plain.js beside
1259
+ it); its negative control runs without `service-switcher.css` and finds the browser's centred dialog. Catalog section
1260
+ "Chuyển dịch vụ", and the shell section's lockup carries it.
1261
+
1262
+ ### Plain HTML: `gembossUI.initServiceSwitcher()` (0.36.0)
1263
+
1264
+ For a page with no React (GemCapture's ops admin, GemPress's admin, GemCare, Taskdesk): the server writes a placeholder
1265
+ right after its `.gb-brand`, and `service-switcher-plain.js` draws the component's button and panel in its place, the
1266
+ same DOM (classes, ARIA, order), so `service-switcher.css` draws it unchanged, and the same behaviour. Its own file, not
1267
+ `kit-plain.js` (which has 485 bytes gzip left under its ceiling and stays as it was): 4.5 KB gzip, loaded only where the
1268
+ switcher is.
1269
+
1270
+ ```html
1271
+ <head>
1272
+ <link rel="stylesheet" href="/ui/service-switcher.css"> <!-- require.resolve("@gemboss/ui/service-switcher.css") -->
1273
+ </head>
1274
+ …
1275
+ <div class="gemboss-topbar-start">
1276
+ <a class="gb-brand" href="/admin" aria-label="GemCapture home">…</a>
1277
+ <span data-gb-service-switcher="gemcapture" hidden></span> <!-- the value: this service's id (services.ts) -->
1278
+ </div>
1279
+ …
1280
+ <script src="/ui/kit-plain.js"></script> <!-- @gemboss/ui/kit-plain.js, if the page has it: FIRST -->
1281
+ <script src="/ui/service-switcher-plain.js"></script> <!-- @gemboss/ui/service-switcher-plain.js -->
1282
+ <script>gembossUI.initServiceSwitcher();</script>
1283
+ ```
1284
+
1285
+ ```js
1286
+ gembossUI.initServiceSwitcher({ // every option is optional, and the component's prop of the same name
1287
+ labels: { trigger: "Chuyển dịch vụ", title: "Dịch vụ Gem", close: "Đóng", search: "Tìm dịch vụ",
1288
+ noMatch: "Không có dịch vụ nào khớp “{query}”.", all: "Mọi dịch vụ và tình trạng" },
1289
+ allHref: null, // the foot link; default GemFactory's /services; null draws none
1290
+ search: false, // the search box (default true)
1291
+ services: gembossUI.GEM_SERVICES.filter((s) => s.group !== "team"), // default every service
1292
+ className: "…", // added to the button's class
1293
+ current: "gemcapture", // instead of the placeholder's value
1294
+ root: element, // where to look for placeholders (default the document)
1295
+ }); // → [{ button, dialog, open(), close() }] for what it drew
1296
+ ```
1297
+
1298
+ | | |
1299
+ | --- | --- |
1300
+ | 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 |
1301
+ | 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 |
1302
+ | `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) |
1303
+ | 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 |
1304
+ | 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 |
1305
+ | 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` |
1306
+
1307
+ `packages/ui/src/service-switcher-plain-browser.test.ts` (Chromium, in `test:ui-browser`) renders the React component and
1308
+ this script for the same props on the same shell and compares the two DOMs node for node (generated ids aside): closed,
1309
+ open, filtered, nothing matching, after the keys, closed again, and with labels, no foot, no search and a list of its own;
1310
+ its negative control changes one attribute and the comparison sees it. Then the component's own checks on the plain page
1311
+ (1280, Escape / ✕ / backdrop, search, Enter, keys, 390 and 320 full screen, the bar at 320 / 360 / 390, kit-plain.js
1312
+ beside it, idempotence, `<head>`, alone, the wrong order). Catalog section "Chuyển dịch vụ", last frame: the plain page,
1313
+ live.
1314
+
1206
1315
  ## Resizable panels
1207
1316
 
1208
1317
  `ui-resizer` is the seam a person drags between two panels; `ui-resizing` on `<body>` is the drag in
@@ -1259,7 +1368,8 @@ const open = dock.presentation !== "hidden";
1259
1368
  - No provider and no state: pass `hidden` while your assistant is open.
1260
1369
  - The shortcut is a hint only: the surface owns the key binding.
1261
1370
  - 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`).
1371
+ (ellipsis). The actions never shrink, so the account chip stays on screen (0.6.1; `topbar.test.tsx`). With a
1372
+ `ServiceSwitcher` in the bar (0.36.0), under 360px the search goes below its floor, to 56px: README "ServiceSwitcher".
1263
1373
  - 0.6.0, not a patch: an existing `<AssistantLauncher>` call changes meaning (no longer fixed; the default copy is the
1264
1374
  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
1375
 
@@ -1284,12 +1394,13 @@ import { AppShell, BrandLockup, AccountMenu, AccountMenuItem, Icon, RailItem } f
1284
1394
  ```
1285
1395
 
1286
1396
  - `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.
1397
+ product's name. The name hides below 460px, as the admin's wordmark does. `switcher={{ current: "gemfactory" }}`
1398
+ (0.36.0) adds the button that opens every other Gem service right after it: README "ServiceSwitcher".
1288
1399
  - `AccountMenu`: the admin's 32px initials chip and popover (same CSS). A `<details>`, so it opens
1289
1400
  without JavaScript on a page that never hydrates; with React it also closes on an outside click and
1290
1401
  on Escape. `AccountMenuItem` is a link (`href`), a POST form (`formAction`), or a button.
1291
1402
  - `TopbarSearch` (0.6.3): see "Topbar search" below.
1292
- - `Icon`: the GemBoss set, 39 icons on a 20px grid with 1.5px strokes, drawn to sit next to Polaris.
1403
+ - `Icon`: the GemBoss set, 45 icons on a 20px grid with 1.5px strokes, drawn to sit next to Polaris.
1293
1404
  `ICON_NAMES` lists them. Internal tools cannot use `@shopify/polaris-icons`: its licence only covers
1294
1405
  applications that integrate with Shopify. Draw a missing one HERE, not in the surface: every rail
1295
1406
  icon must keep `fill="none"` on its inner `<g>`, because `.gnav-ico svg { fill: currentColor }`
@@ -1690,6 +1801,7 @@ something the base cannot express, add a token or a prop here, and every surface
1690
1801
  | `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
1802
  | `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
1803
  | `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`. |
1804
+ | `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
1805
  | `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
1806
  | `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
1807
  | `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. |
@@ -8,7 +8,7 @@
8
8
  .cat-code summary{padding:10px 16px;font-size:13px;cursor:pointer;color:var(--gemboss-text-muted);border-top:1px solid var(--gemboss-divider);min-height:24px}
9
9
  .cat-code pre,.cat-api{margin:0;padding:12px 16px;overflow:auto;background:#0f1012;color:#e8e8e6;font-size:12px;line-height:1.5;border-radius:0}
10
10
  .cat-api{border-radius:12px;margin:0 0 16px}</style></head><body><div class="cat">
11
- <h1>@gemboss/ui 0.35.2 · Editor</h1>
11
+ <h1>@gemboss/ui 0.36.1 · Editor</h1>
12
12
  <p>Khung của mọi editor: <code>EditorFrame</code> (cột trái · khe kéo · preview · cột phải dock trên mép phải preview) và <code>EditorTopbar</code> (← · tiêu đề · cụm giữa · cụm phải · dòng trạng thái). Là đúng khung của theme / page / section editor trong admin. Gói chỉ vẽ KHUNG; chat, Configure, canvas, page picker, menu, Publish là của editor, đi vào bằng slot. Trang HTML thuần nạp <code>editor.css</code> + <code>editor-plain.js</code> rồi gọi <code>gembossUI.initEditorFrame()</code>: kéo khe (chuột, chạm, phím mũi tên / Home / End), đổi thiết bị, mở ⋯ và Publish ▾, ẩn cột trái, Chat | Preview trên điện thoại. <a href="index.html">← Catalog</a></p>
13
13
  <h2>React</h2><pre class="cat-api"><code>import &quot;@gemboss/ui/editor.css&quot;;
14
14
  import { EditorFrame, EditorTopbar, EditorBack, EditorChatToggle, EditorPaneTabs, EditorSaveState, DeviceToggle } from &quot;@gemboss/ui/editor&quot;;