@gemboss/ui 0.8.1 → 0.10.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 +42 -0
- package/README.md +131 -1
- package/catalog/index.html +122 -2
- package/gemboss-ui.css +92 -6
- package/package.json +12 -3
- package/src/app-shell.css +76 -0
- package/src/editor.css +1096 -0
- package/src/index.ts +2 -0
- package/src/kit-plain.css +14 -4
- package/src/kit-plain.js +118 -1
- package/src/nav-dwell.ts +6 -0
- package/src/next/index.ts +4 -0
- package/src/next/nav.tsx +142 -0
- package/src/route-holding.tsx +65 -0
- package/src/shell.tsx +12 -7
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @gemboss/ui changelog
|
|
2
|
+
|
|
3
|
+
Versions up to 0.9.0 are described in the README, each in the section that introduced it.
|
|
4
|
+
|
|
5
|
+
## 0.10.0 (05/10/2026)
|
|
6
|
+
|
|
7
|
+
Navigation pending: a click on a menu item answers on the first frame, and a page is fetched while the pointer rests on
|
|
8
|
+
its link (Chris, 05/10/2026, from content-radar's rail). README "Navigation pending".
|
|
9
|
+
|
|
10
|
+
Added
|
|
11
|
+
- `@gemboss/ui/next`, a new entry: `NavLink` (next/link that prefetches only on intent: pointer at rest 120 ms,
|
|
12
|
+
touchstart, focus; while its page loads it carries a `role="status"` mark that turns a rail item's icon into the
|
|
13
|
+
spinner), `PendingLink` (a link in the page with a small spinner, `mark="inline" | "corner"`), `PendingIcon`,
|
|
14
|
+
`NAV_DWELL_MS`. `next` >= 15.3 is an optional peer dependency; the main entry does not import it.
|
|
15
|
+
- `RailItem` and `RailSubItem` take `pending` (sets `aria-busy="true"`).
|
|
16
|
+
- `RouteHolding` (main entry, plain component): the one loading boundary a route group renders in its `loading.jsx`
|
|
17
|
+
(title bar + four lines in the content column, `role="status"`). Required wherever links are `NavLink`: without a
|
|
18
|
+
boundary, intent prefetch has nothing to fetch (content-radar 05/10/2026: new screen at 350-780 ms).
|
|
19
|
+
- `RouteCover` (main entry): the same frame over the content area from the click until the navigation commits, shown
|
|
20
|
+
by `html[data-gb-navigating]`, which the `@gemboss/ui/next` links set while pending (no delay, 80 ms fade).
|
|
21
|
+
- `app-shell.css`: the pending look. `.gnav-item` icon becomes a 20px spinner in the same box (the label does not
|
|
22
|
+
move), `.gnav-subitem` gets a 12px spinner in its indent before the label (out of the flow), `cursor: progress`, slower spin under reduced motion;
|
|
23
|
+
`a[data-gb-pending]` + `.gb-pending-mark` for page links; `.gnav-pending` status text.
|
|
24
|
+
- `kit-plain.js`: `initNav()`, which runs by itself when the script loads. Followed rail links (and
|
|
25
|
+
`a[data-gb-pending]`) are marked; links are prefetched (speculation rules where supported, else `<link rel="prefetch">`) after a 120 ms rest of the
|
|
26
|
+
pointer or of keyboard focus, or on touchstart, once per URL; not at all once a client router took a rail click over. Skips modifier clicks, `target`, `download`, same-page hashes, other origins, clicks a handler
|
|
27
|
+
already took; clears on `pageshow`, Escape, `navigateerror`, after 15 s. Opt out: `data-gb-nav="off"`,
|
|
28
|
+
`data-gb-prefetch="off"`. No prefetch inside a Next app or with Save-Data.
|
|
29
|
+
- Catalog section "Chờ điều hướng".
|
|
30
|
+
|
|
31
|
+
Measured, and why the prefetch is a speculation rule (05/10/2026, `_local/fleet/nav-pending/prefetch-reuse.txt`): a
|
|
32
|
+
`<link rel="prefetch">` of a page is reused by the click only when the page is cacheable (max-age > 0). Server-rendered
|
|
33
|
+
fleet pages send max-age=0 / no-cache, so the click fetched the page again, and a prefetch still in flight made the
|
|
34
|
+
click slower (1121 ms to the new page against 737 ms without one, through a 280 ms proxy). A speculation-rules prefetch
|
|
35
|
+
was reused under every Cache-Control tried; `nav-pending.test.tsx` holds both (one server request vs two).
|
|
36
|
+
|
|
37
|
+
Not changed
|
|
38
|
+
- The admin: it keeps its own holding screen and hover prefetch (`NavPending.tsx`, `AppShell.tsx`); none of the new
|
|
39
|
+
CSS or code is in its first load. Moving it over is a later change (README).
|
|
40
|
+
|
|
41
|
+
Number: 0.9.0 is the editor kit (gemboss #3770, merged 05/10/2026, not yet published), so this is 0.10.0 (new API =
|
|
42
|
+
minor). Publish 0.9.0 first or together; check `npm view @gemboss/ui versions` and main before publishing either.
|
package/README.md
CHANGED
|
@@ -206,11 +206,46 @@ the admin `@import`s it, so it is the same file the admin's own chat is drawn wi
|
|
|
206
206
|
The toolbar row, the one-row layout, the engine picker's layout and Stop are in a second sheet,
|
|
207
207
|
`chat-composer.css` (also inside `gemboss-ui.css`): the admin imports it from its chat components rather than its
|
|
208
208
|
layout, so screens without a chat never load it. The catalog's "Thân chat" section is the markup to copy. NOT here, on purpose: the panel's own geometry
|
|
209
|
-
(`.gemboss-ec` width
|
|
209
|
+
(`.gemboss-ec` width; the editor columns are in `editor.css` since 0.9.0), attachments, action cards, the engine picker and work mode — those
|
|
210
210
|
are still the admin's. The one exception is the chat panel's touch / mouse target blocks: one rule per pointer
|
|
211
211
|
kind names every control of the panel, admin-only ones included (they match nothing elsewhere). The behaviour (streaming, Stop, Enter to send, IME, scroll-to-bottom) belongs to
|
|
212
212
|
the surface.
|
|
213
213
|
|
|
214
|
+
## Editor CSS (0.9.x)
|
|
215
|
+
|
|
216
|
+
The admin's three editors (theme, page, section) are drawn with `editor.css`, moved verbatim out of the admin's
|
|
217
|
+
`gemboss-shell.css` on 04/10/2026: the same file the admin loads, so a surface that writes the same markup gets the
|
|
218
|
+
same editor. It is a SEPARATE entry, not part of `gemboss-ui.css`: a surface with no editor does not download it.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import "@gemboss/ui/gemboss-ui.css";
|
|
222
|
+
import "@gemboss/ui/editor.css"; // only on the screens that show an editor
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
```html
|
|
226
|
+
<link rel="stylesheet" href="…/gemboss-ui.css"><link rel="stylesheet" href="…/editor.css">
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
| Piece | Classes |
|
|
230
|
+
|---|---|
|
|
231
|
+
| Frame | `.gemboss-editor` (fixed, full screen; `--embedded` fills its container) › `header.gemboss-editor-topbar` + `.gemboss-editor-body` › `.gemboss-ec` (left column, width `--gemboss-chat-w`) + `.gemboss-editor-resizer.ui-resizer` + `.gemboss-editor-preview-pane`. `--gemboss-editor-inset` (5px) is the gap and the outer padding. Toast `.gemboss-editor-toast`, status line `.gemboss-editor-statusbar`, phone pane switch `.gemboss-editor-mobile-tabs › .gemboss-editor-mobile-tab` |
|
|
232
|
+
| Top bar | `.gemboss-editor-topbar` (`--theme`, `--guest`) › `.gemboss-editor-tb-left` (`.gemboss-editor-back`, `.gemboss-editor-tb-name`) · `.gemboss-editor-tb-center` · `.gemboss-editor-tb-right` (`.gemboss-editor-tb-tools`, `.gemboss-editor-tb-ghost`, `.gemboss-editor-tb-undo`, `.gemboss-editor-chat-toggle`, `.gemboss-editor-savestate`). Publish split button `.gemboss-editor-tb-publish` / `.gemboss-editor-publish` (`-main`, `-caret`). Overflow `.gemboss-editor-overflow` (`-trigger`) › `.gemboss-editor-menu` › `.gemboss-editor-menu-item` (`-label`, `-val`, `-check`, `-ext`), `.gemboss-editor-menu-sep` |
|
|
233
|
+
| Page picker | `.gemboss-pagepicker` › `-trigger` (`-trigger-icon`, `-trigger-label`, `-caret`) + `-menu` › `-search` (`-search-icon`, `-search-input`), `-list` › `-item` (`-item-icon`, `-item-label`, `-check`, `-drill`, `-thumb`), `-back`, `-empty`, `-note` |
|
|
234
|
+
| Preview | `.gemboss-pv` › `.gemboss-pv-stage` › `.gemboss-pv-frame` (`.desktop` / `.tablet` / `.mobile`, `.gemboss-pv-fit` for scale-to-fit) › `iframe.gemboss-pv-iframe`; `.gemboss-pv-spin`; empty state `.gemboss-pv-empty` (`-art`, `-title`, `-sub`, `-cta`). Device toggle `.gemboss-pv-devices › button.gemboss-pv-device` |
|
|
235
|
+
| Left column | tab row `div.gemboss-ec-tabs[role=tablist] › button[role=tab][aria-selected]` (add `.gemboss-ec-tab` for the 44px pointer floors); Configure pane `.gemboss-ec-dock-body` |
|
|
236
|
+
| Inspector | `.gemboss-ei` (`--docked` inside the Configure pane) › `.gemboss-ei-head` (`-title`, `-x`, `-savedot`), `.gemboss-ei-tabs › .gemboss-ei-tab`, `.gemboss-ei-body`, `.gemboss-ei-foot`. Groups `.gemboss-ei-group › .gemboss-ei-grouphead` (collapsible) and `.gemboss-ei-gh` / `.gemboss-ei-grp`. Field `.gemboss-ei-fld › label + control + .gemboss-ei-hint` |
|
|
237
|
+
| 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` |
|
|
238
|
+
| 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`) |
|
|
239
|
+
| 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` |
|
|
240
|
+
| 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` |
|
|
241
|
+
| Try door bar | `.gemboss-editor-topbar--guest` › `.gemboss-guest-brand` (`.gemboss-guest-mark`), `-note`, `-keep`, `-signin` |
|
|
242
|
+
|
|
243
|
+
Not here, on purpose: the body modes that hide the admin's own topbar and rail while an editor is open
|
|
244
|
+
(`gemboss-editor-fullscreen`, `-just-exited`, `-layer-open`) and admin-only extensions such as the product studio's
|
|
245
|
+
resizer. The behaviour (resizing, tabs, menus, the inspector's writes) belongs to the surface for now: the React
|
|
246
|
+
components come in steps 2 to 4 (`EditorFrame` + `EditorTopbar`; `PanelTabs`, `PanelHeader`, `InspectorGroup`, `Field`
|
|
247
|
+
and the controls; `LayerList`, `EditorDrawer`). Step 5 moves GemFactory's Builder onto them.
|
|
248
|
+
|
|
214
249
|
## Plain-HTML behaviour: `kit-plain.js` (0.3.0)
|
|
215
250
|
|
|
216
251
|
Surfaces without React (GemFactory, gemcare, gempress, partner-radar) render the kit's markup on the server and need
|
|
@@ -251,6 +286,98 @@ false to keep the popup open (it gets the close value, and a guard that throws k
|
|
|
251
286
|
asserted in Chromium by `packages/ui/src/kit-plain.test.ts` (`npm run test:ui-browser`, run by the CI job that has
|
|
252
287
|
Chromium; `test:packages` excludes it); the catalog's "Hộp thoại" section runs it live.
|
|
253
288
|
|
|
289
|
+
## Navigation pending (0.10.0): a click answers on the first frame
|
|
290
|
+
|
|
291
|
+
Chris, 05/10/2026: content-radar's rail (click a menu item and its icon turns into a spinner, sub items too; rest the
|
|
292
|
+
pointer on one and its page is fetched ahead) is the standard for every surface. Measured 04/10 on content-radar with a
|
|
293
|
+
server ~280 ms away: a rail click left the screen unchanged for 321-503 ms; with this, 6-8 ms. The rule: **something
|
|
294
|
+
on screen changes within 100 ms of every navigation click.** A skeleton is optional (see below); this is not.
|
|
295
|
+
|
|
296
|
+
One state, drawn by `app-shell.css` (inside `gemboss-ui.css`), set three ways:
|
|
297
|
+
|
|
298
|
+
| Surface | What you write | What you get |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| Next.js App Router | `import { NavLink, PendingLink } from "@gemboss/ui/next"` then `<RailItem as={NavLink} href="/x" icon={…} label="X" />`, `<RailSubItem as={NavLink} href="/x/y">Y</RailSubItem>` | prefetch when the pointer or keyboard focus rests 120 ms, or on touchstart (never on sight, never on a sweep or a Tab run); while the page loads, the icon becomes the spinner (sub item: 12px in its indent, before the label), wait cursor, a `role="status"` "Loading" for screen readers |
|
|
301
|
+
| React without Next | `<RailItem pending={loadingHref === href} … />`, `RailSubItem pending` | the same look (`aria-busy="true"`); prefetch is your router's |
|
|
302
|
+
| Plain HTML (GemFactory, taskdesk, gemcare, gempress) | load `kit-plain.js`; nothing else | it attaches itself: a followed `.gnav-item` / `.gnav-subitem` link gets `aria-busy="true"`; a link the pointer or keyboard focus rests on for 120 ms, or a finger touches, is prefetched once: a speculation rule in Chromium (the click reuses it whatever the Cache-Control), else `<link rel="prefetch">` (reused only for a cacheable page) |
|
|
303
|
+
|
|
304
|
+
Links in the page (a list row, a card, a tab): `PendingLink` in Next (`mark="corner"` for a link that wraps a whole
|
|
305
|
+
card), or `data-gb-pending` (`"inline"` / `"corner"`) on an `<a>` with kit-plain.js. `PendingIcon` (Next) swaps the icon of a
|
|
306
|
+
NavLink outside the rail (a tab with an icon) for the spinner; not inside PendingLink (it has its own) and not in a rail
|
|
307
|
+
item (already swapped).
|
|
308
|
+
|
|
309
|
+
kit-plain.js, exactly: same origin; a plain left click (no Cmd / Ctrl / Shift / Alt, not the middle button); no
|
|
310
|
+
`target` other than `_self`; no `download`; not a `#hash` (or `href="#"`) on the same page; nothing already called
|
|
311
|
+
`preventDefault` (a client router, a popup; the kit listens on the window, so it sees the page's own delegated handlers). The mark comes off on `pageshow` (back/forward cache), Escape, the browser's stop
|
|
312
|
+
(`navigateerror`), the next followed link, and after 15 s at the latest. Opt out with `data-gb-nav="off"` on the link
|
|
313
|
+
or any ancestor (`<html>` for a whole page); `data-gb-prefetch="off"` keeps the spinner and skips the prefetch: put it
|
|
314
|
+
on any GET link that DOES something (sign out, export). It never prefetches in a Next app (`window.next`), nor once a
|
|
315
|
+
client router has taken a rail click over (any SPA): the router owns its links, and a document prefetch there is a
|
|
316
|
+
server request the click never uses. A PendingLink to the page already shown (`?tab=b`) gets its spinner but not the
|
|
317
|
+
RouteCover. Links NavLink renders carry
|
|
318
|
+
`data-gb-nav="next"` and are left to it.
|
|
319
|
+
|
|
320
|
+
`@gemboss/ui/next` is its own entry, and `next` (>= 15.3, for `useLinkStatus`) is an OPTIONAL peer: the main entry never
|
|
321
|
+
imports it (`kit-plain-bundles.test.ts` bundles it without next), so Vite and plain-HTML surfaces are unaffected. With
|
|
322
|
+
Next's prefetch on intent you also want `experimental.staleTimes` (`{ dynamic: 30, static: 180 }`, as the admin and
|
|
323
|
+
content-radar) so going back to a page is served from the router cache, and the loading boundary below is NOT optional;
|
|
324
|
+
a navigation that is not a link
|
|
325
|
+
(`router.push` from a select) is not covered: give that control its own pending state.
|
|
326
|
+
|
|
327
|
+
The admin is not on this yet: it answers a click with its own holding screen (`NavPending.tsx`: the clicked item takes
|
|
328
|
+
the selected pill at once, the content area is covered after 90 ms) and does its own hover prefetch, and its shell CSS
|
|
329
|
+
has under 1 KB of first-load headroom. Its rail already sets `aria-busy` on the pending hub, so moving it over is the
|
|
330
|
+
`app-shell.css` rules into `shell.css` plus `RailSubItem pending`.
|
|
331
|
+
|
|
332
|
+
### The loading boundary: REQUIRED with NavLink
|
|
333
|
+
|
|
334
|
+
**Every route group whose links are `NavLink` / `PendingLink` must have a `loading.jsx`, and it renders `RouteHolding`.**
|
|
335
|
+
Next prefetches a dynamic route only down to its nearest loading boundary. Without one, intent prefetch fetches
|
|
336
|
+
nothing useful and the click waits a whole server round trip with the old page on screen, spinner or not. Measured
|
|
337
|
+
05/10/2026 on content-radar (no `loading.*` file, server ~280 ms away): hover prefetches returned 0.3-1.2 KB, none
|
|
338
|
+
finished before the click, and the new screen took 350-780 ms although the rail spinner showed at 5-38 ms. gemwatcher
|
|
339
|
+
(a SPA that draws the new screen's frame first and fills it after) "jumps at once": 8-30 ms. The admin measured the same
|
|
340
|
+
from the other side: deleting `app/app/loading.tsx` cost 60-270 ms per click.
|
|
341
|
+
|
|
342
|
+
```jsx
|
|
343
|
+
// app/v2/loading.jsx: ONE file for the group, not one per page
|
|
344
|
+
import { RouteHolding } from "@gemboss/ui";
|
|
345
|
+
export default function Loading() { return <RouteHolding />; }
|
|
346
|
+
|
|
347
|
+
// app/v2/layout.jsx: ONE RouteCover beside the page, inside the shell
|
|
348
|
+
import { RouteCover } from "@gemboss/ui";
|
|
349
|
+
<AppShell …><PageContainer width={…}>{children}</PageContainer><RouteCover /></AppShell>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The boundary alone is not enough for `t_route`: Next paints it only once a prefetch or the click's own request has come
|
|
353
|
+
back, one server round trip, and a 120 ms hover rarely lets the prefetch finish (content-radar through a 280 ms
|
|
354
|
+
proxy, 05/10/2026: ~300-350 ms with the boundary). `RouteCover` is the same frame already in the page: the
|
|
355
|
+
`@gemboss/ui/next` links set `html[data-gb-navigating]` while pending, and the cover shows over the content area (below
|
|
356
|
+
the topbar, right of the rail) from the click (80 ms fade) until the navigation commits, where the boundary or the page
|
|
357
|
+
takes over in the same place. It is the admin's holding screen (`.gemboss-route-shell`), minus the destination's name.
|
|
358
|
+
|
|
359
|
+
`RouteHolding` (and `RouteCover`) is one frame for every route: a title bar and four lines in the content column (`width` takes the
|
|
360
|
+
PageContainer tiers; `"none"` when the shell already caps the column; `title` when the boundary knows its one
|
|
361
|
+
destination), `role="status"` with a "Loading" label, no card edges. It is a plain component (no hooks), so a server
|
|
362
|
+
`loading.jsx` renders it, and the admin's first load does not carry it. Two numbers to check per surface (visual-gate
|
|
363
|
+
`--clicks`, or the PR's Playwright script): `t_feedback` (something changes) and `t_route` (the new screen, holding
|
|
364
|
+
frame included, is up), both within 100 ms of the click.
|
|
365
|
+
|
|
366
|
+
### Page skeletons: where a page has the conditions, not everywhere
|
|
367
|
+
|
|
368
|
+
A skeleton shaped like one page is optional (Chris: "cái có cái không tuỳ điều kiện nhưng dần sẽ áp dụng hết"). Use the
|
|
369
|
+
kit's `.ui-skeleton` inside a page (a `<Suspense>` fallback around one slow block) when its shape is known before its
|
|
370
|
+
data (a list, a table, a card grid), and keep it to that shape: one that does not match the page is a layout jump of its
|
|
371
|
+
own. Always with the screen reader line:
|
|
372
|
+
|
|
373
|
+
```jsx
|
|
374
|
+
<div className="ui-skeleton" aria-busy="true">
|
|
375
|
+
<span className="ui-sr-only">Loading</span>
|
|
376
|
+
<span className="ui-skeleton__line" style={{ width: "40%" }} />
|
|
377
|
+
<span className="ui-skeleton__line" />
|
|
378
|
+
</div>
|
|
379
|
+
```
|
|
380
|
+
|
|
254
381
|
## Polaris parity (0.5.0)
|
|
255
382
|
|
|
256
383
|
The admin is Polaris; this package is how the rest of the fleet looks like it. Measured on 02/10/2026 across the six
|
|
@@ -275,6 +402,7 @@ New primitives, Polaris' shapes on GemBoss tokens (`kit-plain.css`; the admin us
|
|
|
275
402
|
| Help / error under a field | `ui-help`, `ui-error`; `aria-invalid="true"` paints the field | Labelled help text, InlineError |
|
|
276
403
|
| Radio group | `fieldset.ui-choices` + `label.ui-check > input[type=radio]` | ChoiceList |
|
|
277
404
|
| On / off that applies at once | `label.ui-check.ui-switch > input[role=switch]` | (admin toggle) |
|
|
405
|
+
| (0.9.0) Long label text | put the text in an element: `label.ui-check > input + span`. A long unbroken run (URL, `a/b/c/d`) then breaks inside the label in a block, a column, `ui-choices` or a switch. Not covered: bare text after the input (anonymous flex item), and a label in a flex ROW (it keeps its min-content so short labels are never split): let that row wrap | |
|
|
278
406
|
| Label for an icon button | `.ui-tooltip > button + .ui-tooltip__bubble[role=tooltip]` | Tooltip |
|
|
279
407
|
| Waiting | `ui-spinner` (20px), `--lg` (44px) | Spinner |
|
|
280
408
|
| Progress | `progress.ui-progress`, `--success|--critical` | ProgressBar small |
|
|
@@ -596,9 +724,11 @@ something the base cannot express, add a token or a prop here, and every surface
|
|
|
596
724
|
| `packages/ui/src/shell.css` | the SHARED SHELL (rail, topbar, main, mobile drawer, the assistant's thread rail, popover surface, account chip). The admin's `gemboss-shell.css` @imports it on line 2; the export refuses a shared-shell rule in the admin's sheet or an admin-only rule here (one owner). |
|
|
597
725
|
| `packages/ui/src/kit-plain.css` | primitives for surfaces that render plain HTML, not React (0.2.1): menu, key/value list, icon button; 0.3.0 adds `ui-link` / `ui-prose`, the media card (`ui-card--media`, `__media`, `__body`, `__foot`) and the popup's `ui-modal__body` / `__foot`. Shipped inside `gemboss-ui.css`; the admin does not import it (its first-load CSS budget has ~0.1 KB headroom).; (0.4.0) report pages: delta, large KPI grid, bar list, column chart, legend, funnel, card grid, section head. |
|
|
598
726
|
| `packages/ui/src/kit-plain.js` | 0.3.0: the behaviour those surfaces each wrote by hand: `gembossUI.openModal` (Escape, backdrop, Tab trap, stacking, ask-before-close, focus back), `toast` (text, error tone), `initMenus` (outside click, item, blur, Escape closes the menu before the popup); 0.5.0: `confirm`, `initShell`, `initPasswordFields` / `resetPasswordFields`. A classic script: inline it or `<script src>`; see "Plain-HTML behaviour" above. |
|
|
727
|
+
| `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. |
|
|
599
728
|
| `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. |
|
|
600
729
|
| `packages/ui/src/icon.svg` | 0.5.0: the GemBoss app icon, for `<link rel="icon" type="image/svg+xml">`. Must stay byte-identical to `apps/admin/src/app/icon.svg` (`kit-look.test.ts` checks). |
|
|
601
730
|
| `packages/ui/src/**/*.tsx` | the components, as TypeScript source. Next: add `@gemboss/ui` to `transpilePackages`. Vite compiles it as is. |
|
|
731
|
+
| `packages/ui/src/next/` | 0.10.0, `@gemboss/ui/next`: `NavLink`, `PendingLink`, `PendingIcon`, `NAV_DWELL_MS`. Needs `next` >= 15.3 (optional peer; only this entry imports it). See "Navigation pending". |
|
|
602
732
|
|
|
603
733
|
`gemboss-ui.css` is never committed and never edited. To change a colour or a radius, change
|
|
604
734
|
`packages/ui/src/gemboss-tokens.css` / `packages/ui/src/ui.css`; to change the rail, the topbar or the thread rail,
|
package/catalog/index.html
CHANGED
|
@@ -26,8 +26,8 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
26
26
|
|
|
27
27
|
@media (max-width:760px){.cat{grid-template-columns:1fr;padding:16px}.cat-nav{position:static;flex-direction:row;flex-wrap:wrap}}
|
|
28
28
|
</style></head><body>
|
|
29
|
-
<div class="cat"><nav class="cat-nav" aria-label="Mục"><a href="#tokens">Token</a><a href="#buttons">Nút</a><a href="#fields">Ô nhập</a><a href="#choice">Tab, nhóm nút, thẻ</a><a href="#surfaces">Thẻ, số liệu, bảng, trạng thái trống</a><a href="#report">Báo cáo</a><a href="#resizer">Thanh kéo giữa hai cột</a><a href="#modal">Hộp thoại, toast, menu (JS thuần)</a><a href="#shell">Khung app</a><a href="#page">Bề rộng nội dung</a><a href="#portal">Cổng công khai: tab trên topbar, tìm kiếm, sáng / tối, người dẫn đầu</a><a href="#auth">Đăng nhập</a><a href="#chat">Thân chat</a><a href="#assistant">Trợ lý: cột phiên</a><a href="#icons">Icon</a></nav>
|
|
30
|
-
<main><header class="cat-head"><h1>@gemboss/ui 0.
|
|
29
|
+
<div class="cat"><nav class="cat-nav" aria-label="Mục"><a href="#tokens">Token</a><a href="#buttons">Nút</a><a href="#fields">Ô nhập</a><a href="#choice">Tab, nhóm nút, thẻ</a><a href="#surfaces">Thẻ, số liệu, bảng, trạng thái trống</a><a href="#report">Báo cáo</a><a href="#resizer">Thanh kéo giữa hai cột</a><a href="#modal">Hộp thoại, toast, menu (JS thuần)</a><a href="#shell">Khung app</a><a href="#nav">Chờ điều hướng</a><a href="#page">Bề rộng nội dung</a><a href="#portal">Cổng công khai: tab trên topbar, tìm kiếm, sáng / tối, người dẫn đầu</a><a href="#auth">Đăng nhập</a><a href="#chat">Thân chat</a><a href="#assistant">Trợ lý: cột phiên</a><a href="#icons">Icon</a></nav>
|
|
30
|
+
<main><header class="cat-head"><h1>@gemboss/ui 0.10.0</h1><p>Ngôn ngữ thiết kế chung của các công cụ GemBoss. Markup dưới mỗi ví dụ là đúng thứ package render ra; bề mặt không dùng React (HTML tĩnh, Python) chép markup này và nạp <code>gemboss-ui.css</code>. Dựng từ gemboss <code>527a9099c443</code>.</p></header>
|
|
31
31
|
<section id="tokens"><h2>Token</h2><p>Đổi giao diện một bề mặt bằng cách đặt lại token, không viết đè class. Tên và giá trị đọc thẳng từ gemboss-ui.css.</p><div class="cat-swatches"><div class="cat-swatch"><i style="background:var(--gemboss-primary)"></i><code>--gemboss-primary</code><span>#1b1c1e</span></div><div class="cat-swatch"><i style="background:var(--gemboss-primary-dark)"></i><code>--gemboss-primary-dark</code><span>#000000</span></div><div class="cat-swatch"><i style="background:var(--gemboss-primary-soft)"></i><code>--gemboss-primary-soft</code><span>#f2f3f5</span></div><div class="cat-swatch"><i style="background:var(--gemboss-accent)"></i><code>--gemboss-accent</code><span>#5661f5</span></div><div class="cat-swatch"><i style="background:var(--gemboss-accent-strong)"></i><code>--gemboss-accent-strong</code><span>#3a44de</span></div><div class="cat-swatch"><i style="background:var(--gemboss-accent-soft)"></i><code>--gemboss-accent-soft</code><span>#eef0fe</span></div><div class="cat-swatch"><i style="background:var(--gemboss-text-strong)"></i><code>--gemboss-text-strong</code><span>#1b1c1e</span></div><div class="cat-swatch"><i style="background:var(--gemboss-text)"></i><code>--gemboss-text</code><span>#1b1c1e</span></div><div class="cat-swatch"><i style="background:var(--gemboss-text-muted)"></i><code>--gemboss-text-muted</code><span>#686b71</span></div><div class="cat-swatch"><i style="background:var(--gemboss-surface)"></i><code>--gemboss-surface</code><span>#ffffff</span></div><div class="cat-swatch"><i style="background:var(--gemboss-canvas)"></i><code>--gemboss-canvas</code><span>#ffffff</span></div><div class="cat-swatch"><i style="background:var(--gemboss-preview-canvas)"></i><code>--gemboss-preview-canvas</code><span>#eceef1</span></div><div class="cat-swatch"><i style="background:var(--gemboss-rail-bg)"></i><code>--gemboss-rail-bg</code><span>#ffffff</span></div><div class="cat-swatch"><i style="background:var(--gemboss-panel-bg)"></i><code>--gemboss-panel-bg</code><span>#ffffff</span></div><div class="cat-swatch"><i style="background:var(--gemboss-divider)"></i><code>--gemboss-divider</code><span>#e5e7ea</span></div><div class="cat-swatch"><i style="background:var(--gemboss-divider-soft)"></i><code>--gemboss-divider-soft</code><span>#edeff2</span></div><div class="cat-swatch"><i style="background:var(--gemboss-hover)"></i><code>--gemboss-hover</code><span>#eceef2</span></div><div class="cat-swatch"><i style="background:var(--gemboss-active-bg)"></i><code>--gemboss-active-bg</code><span>#f0f1f4</span></div><div class="cat-swatch"><i style="background:var(--gemboss-active-fg)"></i><code>--gemboss-active-fg</code><span>#1b1c1e</span></div><div class="cat-swatch"><i style="background:var(--gemboss-danger)"></i><code>--gemboss-danger</code><span>#d72c0d</span></div><div class="cat-swatch"><i style="background:var(--gemboss-danger-strong)"></i><code>--gemboss-danger-strong</code><span>#b42318</span></div><div class="cat-swatch"><i style="background:var(--gemboss-danger-soft)"></i><code>--gemboss-danger-soft</code><span>#fff0e9</span></div><div class="cat-swatch"><i style="background:var(--gemboss-danger-border)"></i><code>--gemboss-danger-border</code><span>#f5c9bc</span></div><div class="cat-swatch"><i style="background:var(--gemboss-success)"></i><code>--gemboss-success</code><span>#007f5f</span></div><div class="cat-swatch"><i style="background:var(--gemboss-success-strong)"></i><code>--gemboss-success-strong</code><span>#0a6a4c</span></div><div class="cat-swatch"><i style="background:var(--gemboss-success-soft)"></i><code>--gemboss-success-soft</code><span>#e3f1df</span></div><div class="cat-swatch"><i style="background:var(--gemboss-success-border)"></i><code>--gemboss-success-border</code><span>#b7dfcd</span></div><div class="cat-swatch"><i style="background:var(--gemboss-warning)"></i><code>--gemboss-warning</code><span>#9a6700</span></div><div class="cat-swatch"><i style="background:var(--gemboss-warning-soft)"></i><code>--gemboss-warning-soft</code><span>#fff8e0</span></div><div class="cat-swatch"><i style="background:var(--gemboss-warning-border)"></i><code>--gemboss-warning-border</code><span>#f5d569</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-bg)"></i><code>--gemboss-dark-bg</code><span>#0a0a0c</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-rail)"></i><code>--gemboss-dark-rail</code><span>#0e0e11</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-surface)"></i><code>--gemboss-dark-surface</code><span>#16161a</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-surface-2)"></i><code>--gemboss-dark-surface-2</code><span>#1e1e24</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-divider)"></i><code>--gemboss-dark-divider</code><span>#2a2a31</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-text)"></i><code>--gemboss-dark-text</code><span>#ededf0</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-muted)"></i><code>--gemboss-dark-muted</code><span>#9a9aa4</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-bg)"></i><code>--gemboss-field-bg</code><span>#fdfdfd</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-bg-hover)"></i><code>--gemboss-field-bg-hover</code><span>#fafafa</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-ink)"></i><code>--gemboss-field-ink</code><span>#303030</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-placeholder)"></i><code>--gemboss-field-placeholder</code><span>#616161</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-line)"></i><code>--gemboss-field-line</code><span>#8a8a8a</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-line-hover)"></i><code>--gemboss-field-line-hover</code><span>#616161</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-line-active)"></i><code>--gemboss-field-line-active</code><span>#1a1a1a</span></div><div class="cat-swatch"><i style="background:var(--gemboss-chip-ink)"></i><code>--gemboss-chip-ink</code><span>#303633</span></div><div class="cat-swatch"><i style="background:var(--gemboss-chip-line)"></i><code>--gemboss-chip-line</code><span>#cfd6ce</span></div><div class="cat-swatch"><i style="background:var(--gemboss-chip-line-hover)"></i><code>--gemboss-chip-line-hover</code><span>#aab4ad</span></div><div class="cat-swatch"><i style="background:var(--gemboss-chip-bg-hover)"></i><code>--gemboss-chip-bg-hover</code><span>#f6f7f5</span></div><div class="cat-swatch"><i style="background:var(--gemboss-on-primary)"></i><code>--gemboss-on-primary</code><span>#ffffff</span></div><div class="cat-swatch"><i style="background:var(--gemboss-app-bg)"></i><code>--gemboss-app-bg</code><span>#ffffff</span></div></div><table class="ui-table cat-scale"><thead><tr><th>Token</th><th>Giá trị</th><th>Mẫu</th></tr></thead><tbody><tr><td><code>--gemboss-fs-micro</code></td><td>11px</td><td style="font-size:var(--gemboss-fs-micro)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-caption</code></td><td>12px</td><td style="font-size:var(--gemboss-fs-caption)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-body</code></td><td>14px</td><td style="font-size:var(--gemboss-fs-body)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-body-lg</code></td><td>16px</td><td style="font-size:var(--gemboss-fs-body-lg)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-h3</code></td><td>16px</td><td style="font-size:var(--gemboss-fs-h3)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-h2</code></td><td>20px</td><td style="font-size:var(--gemboss-fs-h2)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-h1</code></td><td>24px</td><td style="font-size:var(--gemboss-fs-h1)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-display</code></td><td>30px</td><td style="font-size:var(--gemboss-fs-display)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-radius-xs</code></td><td>4px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-xs)"></i></td></tr><tr><td><code>--gemboss-radius-sm</code></td><td>6px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-sm)"></i></td></tr><tr><td><code>--gemboss-radius-md</code></td><td>8px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-md)"></i></td></tr><tr><td><code>--gemboss-radius-lg</code></td><td>12px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-lg)"></i></td></tr><tr><td><code>--gemboss-radius-xl</code></td><td>16px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-xl)"></i></td></tr><tr><td><code>--gemboss-radius-2xl</code></td><td>20px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-2xl)"></i></td></tr><tr><td><code>--gemboss-radius-full</code></td><td>999px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-full)"></i></td></tr><tr><td><code>--gemboss-shadow-resting</code></td><td class="cat-small">0 1px 2px rgba(31, 30, 29, 0.04), 0 2px 6px rgba(31, 30, 29, 0.06)</td><td><i class="cat-radius" style="box-shadow:var(--gemboss-shadow-resting);border:0"></i></td></tr><tr><td><code>--gemboss-shadow-raised</code></td><td class="cat-small">0 4px 16px rgba(31, 30, 29, 0.10)</td><td><i class="cat-radius" style="box-shadow:var(--gemboss-shadow-raised);border:0"></i></td></tr><tr><td><code>--gemboss-shadow-overlay</code></td><td class="cat-small">0 16px 48px rgba(31, 30, 29, 0.18)</td><td><i class="cat-radius" style="box-shadow:var(--gemboss-shadow-overlay);border:0"></i></td></tr></tbody></table></section>
|
|
32
32
|
<section id="buttons"><h2>Nút</h2><p>Một lớp <code>ui-btn</code>, ba biến thể, tông đỏ, một cỡ nhỏ. Cao 32px dưới chuột (bằng ô nhập và ô chọn), 44px dưới ngón tay và ở khổ điện thoại, cỡ nhỏ cũng vậy. Dùng được trên <code><button></code> lẫn <code><a></code> (căn giữa sẵn).</p><figure class="cat-demo"><figcaption>Mặc định · chính · ghost · nhỏ · tắt</figcaption><div class="cat-live"><div class="cat-row"><button type="button" class="ui-btn">Lưu nháp</button><button type="button" class="ui-btn ui-btn--primary">Xuất bản</button><button type="button" class="ui-btn ui-btn--ghost">Huỷ</button><button type="button" class="ui-btn ui-btn--sm">Nhỏ</button><button type="button" class="ui-btn ui-btn--primary" disabled>Đang lưu…</button><a class="ui-btn" href="#buttons">Link dạng nút</a></div></div><details class="cat-code"><summary>Markup</summary><pre><code><div class="cat-row">
|
|
33
33
|
<button type="button" class="ui-btn">Lưu nháp</button>
|
|
@@ -752,6 +752,126 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
752
752
|
</div>
|
|
753
753
|
</main>
|
|
754
754
|
</div></code></pre></details></figure></section>
|
|
755
|
+
<section id="nav"><h2>Chờ điều hướng</h2><p>Bấm một mục rail là có phản hồi ở khung hình đầu tiên: icon thành spinner trong đúng ô 20px (chữ không xê dịch), mục con có spinner 12px ở phần thụt lề ngay trước chữ, con trỏ chờ. Một trạng thái, ba cách đặt: <code>RailItem pending</code> / <code>RailSubItem pending</code> (React, thành <code>aria-busy="true"</code>); <code>RailItem as={NavLink}</code> của <code>@gemboss/ui/next</code> (đọc <code>useLinkStatus</code> của Next, tải trước khi chuột hoặc focus bàn phím dừng 120 ms, hoặc khi chạm); trang HTML thuần nạp <code>kit-plain.js</code> là tự có, kể cả tải trước (speculation rules ở Chromium, không có thì <code><link rel="prefetch"></code>). Link trong nội dung trang thêm <code>data-gb-pending</code> ("inline" hoặc "corner").</p><figure class="cat-demo cat-screen"><figcaption>RailItem pending + RailSubItem pending</figcaption><iframe title="RailItem pending + RailSubItem pending" style="height:300px" srcdoc="<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="../gemboss-ui.css"></head><body><div class="gemboss-platform-shell"><header class="gemboss-platform-topbar"><div class="gemboss-topbar-start"><button type="button" class="gemboss-mobile-menu-btn" aria-label="Open menu" aria-expanded="false" data-label-open="Open menu" data-label-close="Close menu" aria-controls=":R0:"><svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M3 5.5h14M3 10h14M3 14.5h14" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"></path></svg></button><a class="gb-brand" href="#" aria-label="GemFactory home"><svg class="gb-brand-gem" viewBox="0 0 32 40" width="20.8" height="26" aria-hidden="true" focusable="false"><g transform="translate(0,0.8) scale(0.31)"><polygon points="26,14 74,14 66,50 34,50" fill="#5661F5" fill-opacity="0.92"></polygon><path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="currentColor" stroke-width="8.75" stroke-linejoin="round"></path><path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="currentColor" stroke-width="5.45" stroke-linejoin="round"></path></g></svg><span class="gb-brand-name">GemFactory</span></a></div><div class="gemboss-topbar-center"></div><div class="gemboss-topbar-end"></div></header><nav class="icon-nav" aria-label="Main navigation" id=":R0:"><div class="gnav-scroll"><div class="gnav-group"><a class="gnav-item" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3.5" width="4" height="13" rx="1.2"></rect><rect x="8" y="3.5" width="4" height="9" rx="1.2"></rect><rect x="13" y="3.5" width="4" height="6" rx="1.2"></rect></g></svg></span><span class="gnav-label">Board</span></a></div><div class="gnav-group"><a class="gnav-item" aria-busy="true" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="8.25" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="13.5" width="13" height="3.5" rx="1"></rect></g></svg></span><span class="gnav-label">Ghép trang (đang mở)</span></a></div><div class="gnav-group active has-active-child"><a class="gnav-item active" aria-current="page" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 16.5h14"></path><rect x="4.5" y="9" width="2.8" height="5.5" rx=".7"></rect><rect x="8.6" y="5" width="2.8" height="9.5" rx=".7"></rect><rect x="12.7" y="11" width="2.8" height="3.5" rx=".7"></rect></g></svg></span><span class="gnav-label">Báo cáo</span></a><ul class="gnav-sub"><li><a class="gnav-subitem active" aria-current="page" href="#">Tổng quan</a></li><li><a class="gnav-subitem" aria-busy="true" href="#">Theo kênh (đang mở)</a></li></ul></div></div></nav><button type="button" class="gemboss-mobile-nav-backdrop" aria-label="Close menu" tabindex="-1"></button><main class="gemboss-platform-main"><div style="padding:24px"><h1 style="margin:0">Tổng quan</h1></div></main></div></body></html>"></iframe><details class="cat-code"><summary>Markup</summary><pre><code><div class="gemboss-platform-shell">
|
|
756
|
+
<header class="gemboss-platform-topbar">
|
|
757
|
+
<div class="gemboss-topbar-start">
|
|
758
|
+
<button type="button" class="gemboss-mobile-menu-btn" aria-label="Open menu" aria-expanded="false" data-label-open="Open menu" data-label-close="Close menu" aria-controls=":R0:">
|
|
759
|
+
<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true">
|
|
760
|
+
<path d="M3 5.5h14M3 10h14M3 14.5h14" stroke="currentColor" stroke-width="1.7" stroke-linecap="round">
|
|
761
|
+
</path>
|
|
762
|
+
</svg>
|
|
763
|
+
</button>
|
|
764
|
+
<a class="gb-brand" href="#" aria-label="GemFactory home">
|
|
765
|
+
<svg class="gb-brand-gem" viewBox="0 0 32 40" width="20.8" height="26" aria-hidden="true" focusable="false">
|
|
766
|
+
<g transform="translate(0,0.8) scale(0.31)">
|
|
767
|
+
<polygon points="26,14 74,14 66,50 34,50" fill="#5661F5" fill-opacity="0.92">
|
|
768
|
+
</polygon>
|
|
769
|
+
<path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="currentColor" stroke-width="8.75" stroke-linejoin="round">
|
|
770
|
+
</path>
|
|
771
|
+
<path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="currentColor" stroke-width="5.45" stroke-linejoin="round">
|
|
772
|
+
</path>
|
|
773
|
+
</g>
|
|
774
|
+
</svg>
|
|
775
|
+
<span class="gb-brand-name">GemFactory</span>
|
|
776
|
+
</a>
|
|
777
|
+
</div>
|
|
778
|
+
<div class="gemboss-topbar-center">
|
|
779
|
+
</div>
|
|
780
|
+
<div class="gemboss-topbar-end">
|
|
781
|
+
</div>
|
|
782
|
+
</header>
|
|
783
|
+
<nav class="icon-nav" aria-label="Main navigation" id=":R0:">
|
|
784
|
+
<div class="gnav-scroll">
|
|
785
|
+
<div class="gnav-group">
|
|
786
|
+
<a class="gnav-item" href="#">
|
|
787
|
+
<span class="gnav-ico">
|
|
788
|
+
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false">
|
|
789
|
+
<g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
790
|
+
<rect x="3" y="3.5" width="4" height="13" rx="1.2">
|
|
791
|
+
</rect>
|
|
792
|
+
<rect x="8" y="3.5" width="4" height="9" rx="1.2">
|
|
793
|
+
</rect>
|
|
794
|
+
<rect x="13" y="3.5" width="4" height="6" rx="1.2">
|
|
795
|
+
</rect>
|
|
796
|
+
</g>
|
|
797
|
+
</svg>
|
|
798
|
+
</span>
|
|
799
|
+
<span class="gnav-label">Board</span>
|
|
800
|
+
</a>
|
|
801
|
+
</div>
|
|
802
|
+
<div class="gnav-group">
|
|
803
|
+
<a class="gnav-item" aria-busy="true" href="#">
|
|
804
|
+
<span class="gnav-ico">
|
|
805
|
+
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false">
|
|
806
|
+
<g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
807
|
+
<rect x="3.5" y="3" width="13" height="3.5" rx="1">
|
|
808
|
+
</rect>
|
|
809
|
+
<rect x="3.5" y="8.25" width="13" height="3.5" rx="1">
|
|
810
|
+
</rect>
|
|
811
|
+
<rect x="3.5" y="13.5" width="13" height="3.5" rx="1">
|
|
812
|
+
</rect>
|
|
813
|
+
</g>
|
|
814
|
+
</svg>
|
|
815
|
+
</span>
|
|
816
|
+
<span class="gnav-label">Ghép trang (đang mở)</span>
|
|
817
|
+
</a>
|
|
818
|
+
</div>
|
|
819
|
+
<div class="gnav-group active has-active-child">
|
|
820
|
+
<a class="gnav-item active" aria-current="page" href="#">
|
|
821
|
+
<span class="gnav-ico">
|
|
822
|
+
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false">
|
|
823
|
+
<g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
824
|
+
<path d="M3 16.5h14">
|
|
825
|
+
</path>
|
|
826
|
+
<rect x="4.5" y="9" width="2.8" height="5.5" rx=".7">
|
|
827
|
+
</rect>
|
|
828
|
+
<rect x="8.6" y="5" width="2.8" height="9.5" rx=".7">
|
|
829
|
+
</rect>
|
|
830
|
+
<rect x="12.7" y="11" width="2.8" height="3.5" rx=".7">
|
|
831
|
+
</rect>
|
|
832
|
+
</g>
|
|
833
|
+
</svg>
|
|
834
|
+
</span>
|
|
835
|
+
<span class="gnav-label">Báo cáo</span>
|
|
836
|
+
</a>
|
|
837
|
+
<ul class="gnav-sub">
|
|
838
|
+
<li>
|
|
839
|
+
<a class="gnav-subitem active" aria-current="page" href="#">Tổng quan</a>
|
|
840
|
+
</li>
|
|
841
|
+
<li>
|
|
842
|
+
<a class="gnav-subitem" aria-busy="true" href="#">Theo kênh (đang mở)</a>
|
|
843
|
+
</li>
|
|
844
|
+
</ul>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
</nav>
|
|
848
|
+
<button type="button" class="gemboss-mobile-nav-backdrop" aria-label="Close menu" tabindex="-1">
|
|
849
|
+
</button>
|
|
850
|
+
<main class="gemboss-platform-main">
|
|
851
|
+
<div style="padding:24px">
|
|
852
|
+
<h1 style="margin:0">Tổng quan</h1>
|
|
853
|
+
</div>
|
|
854
|
+
</main>
|
|
855
|
+
</div></code></pre></details></figure><figure class="cat-demo"><figcaption>Link trong trang đang mở (<code>data-gb-pending</code>) <span>PendingLink hoặc kit-plain.js chèn spinner vào trong link</span></figcaption><div class="cat-live"><div class="cat-row"><a href="#" data-gb-pending="inline" aria-busy="true">Bài 412: Hero split<span class="gb-pending-mark" role="status"><span class="ui-sr-only">Loading</span></span></a></div></div><details class="cat-code"><summary>Markup</summary><pre><code><div class="cat-row">
|
|
856
|
+
<a href="#" data-gb-pending="inline" aria-busy="true">Bài 412: Hero split<span class="gb-pending-mark" role="status">
|
|
857
|
+
<span class="ui-sr-only">Loading</span>
|
|
858
|
+
</span>
|
|
859
|
+
</a>
|
|
860
|
+
</div></code></pre></details></figure><figure class="cat-demo"><figcaption>Ranh giới tải của một nhóm route (<code>RouteHolding</code> trong <code>loading.jsx</code>) <span>BẮT BUỘC khi dùng NavLink: không có nó thì tải trước không lấy được gì</span></figcaption><div class="cat-live"><div class="gb-route-holding" role="status" aria-busy="true"><span class="ui-sr-only">Loading</span><div class="ui-skeleton" aria-hidden="true"><span class="ui-skeleton__line gb-route-holding__title"></span><span class="ui-skeleton__line"></span><span class="ui-skeleton__line"></span><span class="ui-skeleton__line"></span><span class="ui-skeleton__line"></span></div></div></div><details class="cat-code"><summary>Markup</summary><pre><code><div class="gb-route-holding" role="status" aria-busy="true">
|
|
861
|
+
<span class="ui-sr-only">Loading</span>
|
|
862
|
+
<div class="ui-skeleton" aria-hidden="true">
|
|
863
|
+
<span class="ui-skeleton__line gb-route-holding__title">
|
|
864
|
+
</span>
|
|
865
|
+
<span class="ui-skeleton__line">
|
|
866
|
+
</span>
|
|
867
|
+
<span class="ui-skeleton__line">
|
|
868
|
+
</span>
|
|
869
|
+
<span class="ui-skeleton__line">
|
|
870
|
+
</span>
|
|
871
|
+
<span class="ui-skeleton__line">
|
|
872
|
+
</span>
|
|
873
|
+
</div>
|
|
874
|
+
</div></code></pre></details></figure></section>
|
|
755
875
|
<section id="page"><h2>Bề rộng nội dung</h2><p>Rail bên trái, bên phải là một cột có bề rộng tối đa theo bậc của admin: <code>ui-page</code> 900px (<code>--ui-content-max</code>: danh sách, bảng, lưới card, dashboard), <code>--feed</code> 720, <code>--form</code> 662, <code>--canvas</code> 1440 (chỉ khi nội dung đo được là rộng hơn một cột chữ), <code>--full</code> không giới hạn (board, editor; ghi lý do). React: một <code>PageContainer width="form"</code> ngay trong <code>AppShell</code>. Một cap chỉ có tác dụng khi nó NHỎ HƠN chỗ trống thật: ở 1280 chỗ trống là 992. Hai khung dưới vẽ ở 1440, kéo ngang để xem hết.</p><figure class="cat-demo cat-screen"><figcaption>PageContainer width="default" ở 1440: cột 900</figcaption><div class="cat-wide"><iframe title="PageContainer width="default" ở 1440: cột 900" style="height:300px;width:1440px" srcdoc="<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="../gemboss-ui.css"></head><body><div class="gemboss-platform-shell"><header class="gemboss-platform-topbar"><div class="gemboss-topbar-start"><button type="button" class="gemboss-mobile-menu-btn" aria-label="Open menu" aria-expanded="false" data-label-open="Open menu" data-label-close="Close menu" aria-controls=":R0:"><svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M3 5.5h14M3 10h14M3 14.5h14" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"></path></svg></button><a class="gb-brand" href="#" aria-label="GemFactory home"><svg class="gb-brand-gem" viewBox="0 0 32 40" width="20.8" height="26" aria-hidden="true" focusable="false"><g transform="translate(0,0.8) scale(0.31)"><polygon points="26,14 74,14 66,50 34,50" fill="#5661F5" fill-opacity="0.92"></polygon><path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="currentColor" stroke-width="8.75" stroke-linejoin="round"></path><path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="currentColor" stroke-width="5.45" stroke-linejoin="round"></path></g></svg><span class="gb-brand-name">GemFactory</span></a></div><div class="gemboss-topbar-center"></div><div class="gemboss-topbar-end"></div></header><nav class="icon-nav" aria-label="Main navigation" id=":R0:"><div class="gnav-scroll"><div class="gnav-heading">Kho</div><div class="gnav-group active"><a class="gnav-item active" aria-current="page" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3.5" width="4" height="13" rx="1.2"></rect><rect x="8" y="3.5" width="4" height="9" rx="1.2"></rect><rect x="13" y="3.5" width="4" height="6" rx="1.2"></rect></g></svg></span><span class="gnav-label">Board</span></a></div><div class="gnav-group"><a class="gnav-item" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="8.25" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="13.5" width="13" height="3.5" rx="1"></rect></g></svg></span><span class="gnav-label">Ghép trang</span></a></div><div class="gnav-group"><a class="gnav-item" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3.2 17.3 16H2.7z"></path><path d="M10 8.2v3.6M10 13.9v.1"></path></g></svg></span><span class="gnav-label">Framework thiếu</span><span class="gnav-count" aria-label="3 mục đang mở">3</span></a></div></div></nav><button type="button" class="gemboss-mobile-nav-backdrop" aria-label="Close menu" tabindex="-1"></button><main class="gemboss-platform-main"><div class="ui-page"><h1 style="margin:0 0 16px">Board</h1><div class="ui-card"><div class="ui-card__title">#412 Hero split</div><p class="ui-muted">Cột 900px, căn giữa trong chỗ trống bên phải rail.</p></div></div></main></div></body></html>"></iframe></div><details class="cat-code"><summary>Markup</summary><pre><code><div class="gemboss-platform-shell">
|
|
756
876
|
<header class="gemboss-platform-topbar">
|
|
757
877
|
<div class="gemboss-topbar-start">
|
package/gemboss-ui.css
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* GemBoss admin design system — GENERATED — do not edit; regenerate from gemboss:
|
|
3
3
|
* node apps/admin/scripts/export-ui-css.mjs --out <path> (npm run export:ui-css)
|
|
4
|
-
* gemboss commit:
|
|
5
|
-
* sources sha256:
|
|
4
|
+
* gemboss commit: 527a9099c443fa986e3844442da5497d4623e8a3
|
|
5
|
+
* sources sha256: cf447622a2b99d887e6fa00ecfbc2c11423bbafafb1273411820018baed1e77e
|
|
6
6
|
* contents: 1 allow-listed rule from apps/admin/src/app/globals.css + packages/ui/src/gemboss-tokens.css
|
|
7
7
|
* (verbatim) + packages/ui/src/ui.css (verbatim) + packages/ui/src/gemboss-field.css (verbatim)
|
|
8
8
|
* + 181 rules from packages/ui/src/shell.css (the shared shell)
|
|
@@ -2081,6 +2081,82 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
2081
2081
|
}
|
|
2082
2082
|
.gnav-item.active .gnav-count { background: var(--gemboss-primary); color: var(--gemboss-on-primary); }
|
|
2083
2083
|
|
|
2084
|
+
/* ── Navigation pending (0.10.0): a click answers on the first frame ─────────────────────────────────────────────
|
|
2085
|
+
* Chris 05/10/2026: content-radar's rail (the icon turns into a spinner the moment you click, sub items too) is the
|
|
2086
|
+
* standard for every surface. Measured 04/10 on content-radar with a server ~280 ms away: nothing on screen changed
|
|
2087
|
+
* for 321-503 ms after a click; with this, 6-8 ms.
|
|
2088
|
+
*
|
|
2089
|
+
* ONE state, three ways to set it, so a React rail, a Next rail and a server-rendered rail look the same:
|
|
2090
|
+
* - `aria-busy="true"` on the item: RailItem / RailSubItem `pending` (React), kit-plain.js on a followed link;
|
|
2091
|
+
* - a `.gnav-pending` child: NavLink from `@gemboss/ui/next` (Next's useLinkStatus is only readable INSIDE the
|
|
2092
|
+
* link, so the link cannot set its own attribute; it renders this status text and `:has()` reads it).
|
|
2093
|
+
* `:is()` is forgiving: a browser without `:has()` still honours the aria-busy half of each rule.
|
|
2094
|
+
* The icon keeps its box (visibility, not display), so the label never moves. Here and not in shell.css: the
|
|
2095
|
+
* admin's shell sheet has <1 KB of first-load headroom, and the admin answers a click with its own holding screen.
|
|
2096
|
+
*/
|
|
2097
|
+
.gnav-item:is([aria-busy="true"], :has(> .gnav-pending)),
|
|
2098
|
+
.gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending)) { cursor: progress; }
|
|
2099
|
+
.gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico > * { visibility: hidden; }
|
|
2100
|
+
.gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico::after,
|
|
2101
|
+
.gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after,
|
|
2102
|
+
.gb-pending-mark {
|
|
2103
|
+
content: ""; display: inline-block; vertical-align: middle; grid-area: 1 / 1; flex: none; box-sizing: border-box;
|
|
2104
|
+
width: 20px; height: 20px; border-radius: 50%;
|
|
2105
|
+
/* the item's own ink, so the spinner follows the active and hover colours */
|
|
2106
|
+
border: 2px solid color-mix(in srgb, currentColor 18%, transparent); border-top-color: currentColor;
|
|
2107
|
+
animation: gnav-spin 0.7s linear infinite;
|
|
2108
|
+
}
|
|
2109
|
+
/* A sub item has no icon box: a 12px spinner in its 44px indent, just before the label, out of the flow. At the row's end
|
|
2110
|
+
* it would be a third item in a space-between row and push a trailing count to the middle (review #3777). */
|
|
2111
|
+
.gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after {
|
|
2112
|
+
position: absolute; left: 22px; top: 50%; margin-top: -6px; width: 12px; height: 12px; border-width: 1.5px;
|
|
2113
|
+
}
|
|
2114
|
+
@keyframes gnav-spin { to { transform: rotate(360deg); } }
|
|
2115
|
+
/* Slower, never off: it is the only sign that the click was heard. */
|
|
2116
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2117
|
+
.gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico::after,
|
|
2118
|
+
.gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after,
|
|
2119
|
+
.gb-pending-mark { animation-duration: 2s; }
|
|
2120
|
+
}
|
|
2121
|
+
/* A link in the PAGE (a list row, a card, a tab) opts in with `data-gb-pending`: PendingLink (`@gemboss/ui/next`) or
|
|
2122
|
+
* kit-plain.js put `span.gb-pending-mark` (role=status) inside it while the page loads. "inline" (default): 12px after
|
|
2123
|
+
* the text. "corner": 16px in the top right corner, out of the flow, for a link that wraps a whole card (a grid card
|
|
2124
|
+
* gains no column). Marked by attribute, not class: a surface that colours bare links with `a:not([class])` keeps it. */
|
|
2125
|
+
a[data-gb-pending]:has(> .gb-pending-mark), a[data-gb-pending][aria-busy="true"] { cursor: progress; }
|
|
2126
|
+
a[data-gb-pending] > .gb-pending-mark { display: inline-block; width: 12px; height: 12px; border-width: 1.5px; margin-left: 6px; vertical-align: -1px; }
|
|
2127
|
+
a[data-gb-pending="corner"] { position: relative; }
|
|
2128
|
+
a[data-gb-pending="corner"] > .gb-pending-mark { position: absolute; top: 12px; right: 12px; width: 16px; height: 16px; margin: 0; }
|
|
2129
|
+
/* RouteHolding (./route-holding.tsx): a route group's loading boundary. The kit's skeleton lines in the content column,
|
|
2130
|
+
* one shape for every route: a title bar, then four lines of falling length (the admin's holding screen). */
|
|
2131
|
+
.gb-route-holding { padding-top: 4px; }
|
|
2132
|
+
.gb-route-holding .ui-skeleton { gap: 16px; }
|
|
2133
|
+
.gb-route-holding__heading { margin: 0 0 20px; font-size: 20px; font-weight: 650; line-height: 1.3; color: var(--gemboss-text); }
|
|
2134
|
+
.gb-route-holding .ui-skeleton__line { height: 10px; }
|
|
2135
|
+
.gb-route-holding .ui-skeleton__line.gb-route-holding__title { height: 20px; width: 34%; margin-bottom: 8px; }
|
|
2136
|
+
.gb-route-holding .ui-skeleton__line:nth-last-child(4) { width: 46%; }
|
|
2137
|
+
.gb-route-holding .ui-skeleton__line:nth-last-child(3) { width: 92%; }
|
|
2138
|
+
.gb-route-holding .ui-skeleton__line:nth-last-child(2) { width: 78%; }
|
|
2139
|
+
.gb-route-holding .ui-skeleton__line:nth-last-child(1) { width: 61%; }
|
|
2140
|
+
/* RouteCover: the same frame over the content area from the click until the navigation commits. Shown only while
|
|
2141
|
+
* `html[data-gb-navigating]` (set by the @gemboss/ui/next links while pending). No grace delay: the target is the new
|
|
2142
|
+
* screen within 100 ms of the click, and a 40 ms grace measured 120-132 ms on content-radar's rail (05/10/2026). The
|
|
2143
|
+
* 80 ms fade is what a router cache hit (20-90 ms) sees of it: a faint, partial frame, not a full flash.
|
|
2144
|
+
* z 5: above the outgoing page, below the rail, the topbar (300) and the assistant (290). */
|
|
2145
|
+
.gb-route-cover { display: none; }
|
|
2146
|
+
html[data-gb-navigating] .gb-route-cover {
|
|
2147
|
+
display: block; position: fixed; inset: 56px 0 0 240px; z-index: 5; overflow: hidden; box-sizing: border-box;
|
|
2148
|
+
padding: 24px; background: var(--gemboss-app-bg); pointer-events: none;
|
|
2149
|
+
animation: gb-route-cover-in 0.08s ease-out both;
|
|
2150
|
+
}
|
|
2151
|
+
@keyframes gb-route-cover-in { from { opacity: 0; } to { opacity: 1; } }
|
|
2152
|
+
@media (max-width: 760px) { html[data-gb-navigating] .gb-route-cover { left: 0; padding: 16px; } }
|
|
2153
|
+
@media (prefers-reduced-motion: reduce) { html[data-gb-navigating] .gb-route-cover { animation-duration: 0s; } }
|
|
2154
|
+
/* The status text a NavLink renders inside a rail item: read, not seen. */
|
|
2155
|
+
.gnav-pending {
|
|
2156
|
+
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
|
|
2157
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2084
2160
|
/* ── BrandLockup (./brand.tsx) ─────────────────────────────────────────────────────────────────
|
|
2085
2161
|
* The admin's `.gemboss-brand-lockup` box (40px tall, 8px padding, hover wash) around the wordmark's
|
|
2086
2162
|
* own proportions. Not that class itself: its ≤460px rule swaps in a background image at an
|
|
@@ -3391,10 +3467,19 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
3391
3467
|
.ui-menu__sep { height: 1px; margin: var(--ui-space-1) 0; border: 0; background: var(--ui-border); }
|
|
3392
3468
|
/* Checkbox: the LABEL is the target; disabled turns both the label and the box to not-allowed. Focus ring in the
|
|
3393
3469
|
kit's own token (no --p-* here: this file is appended after the export's Polaris resolver runs). */
|
|
3394
|
-
.ui-check { display: inline-flex; align-items: center; gap: var(--ui-space-2); min-height: 44px; cursor: pointer; font-size: var(--ui-fs-body); color: var(--ui-text); }
|
|
3470
|
+
.ui-check { display: inline-flex; align-items: center; gap: var(--ui-space-2); min-height: 44px; max-width: 100%; overflow-wrap: break-word; cursor: pointer; font-size: var(--ui-fs-body); color: var(--ui-text); }
|
|
3395
3471
|
.ui-check > input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); cursor: inherit; }
|
|
3396
3472
|
.ui-check > input:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
|
|
3397
3473
|
.ui-check:has(> input:disabled) { opacity: .5; cursor: not-allowed; }
|
|
3474
|
+
/* 0.9.0: a long run with no spaces (a URL, "clinical/premium/…/utility") breaks inside the label instead of widening
|
|
3475
|
+
the page: gemwatcher Operations overflowed 69px at 320 on one switch description (#375). Three parts: the label is
|
|
3476
|
+
capped at its parent (max-width above), its TEXT child may shrink (min-width 0, the last child only so a badge in
|
|
3477
|
+
front keeps its width), and break-word breaks the run once it cannot fit a line.
|
|
3478
|
+
break-word, NOT anywhere: anywhere also drops the label's min-content, and a flex row then squeezes a short label
|
|
3479
|
+
("Lock" beside a long band name in GemFactory) into "Lo/ck". Two cases stay the surface's job: the text must be an
|
|
3480
|
+
element (`input + span`; bare text is an anonymous flex item min-width cannot reach), and in a flex ROW the label
|
|
3481
|
+
keeps its min-content, so that row must wrap. */
|
|
3482
|
+
.ui-check > :last-child:not(input) { min-width: 0; }
|
|
3398
3483
|
.ui-label { display: block; margin: 0 0 var(--ui-space-1); font-size: var(--ui-fs-caption); font-weight: 600; color: var(--ui-text-muted); }
|
|
3399
3484
|
.ui-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--ui-space-2) var(--ui-space-4); margin: 0; font-size: var(--ui-fs-body); }
|
|
3400
3485
|
.ui-kv > dt { color: var(--ui-text-muted); }
|
|
@@ -3661,12 +3746,13 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
|
3661
3746
|
}
|
|
3662
3747
|
|
|
3663
3748
|
/* Radio: the checkbox's row with `type="radio"` — `<fieldset class="ui-choices"><legend class="ui-label">Plan</legend>
|
|
3664
|
-
<label class="ui-check"><input type="radio" name="plan" checked>
|
|
3665
|
-
|
|
3749
|
+
<label class="ui-check"><input type="radio" name="plan" checked><span>Basic</span></label>…</fieldset>`.
|
|
3750
|
+
One minmax(0, 1fr) column: an auto column grows to the longest unbroken word in any label (0.9.0). */
|
|
3751
|
+
.ui-choices { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; margin: 0; padding: 0; border: 0; min-width: 0; }
|
|
3666
3752
|
.ui-choices > legend { padding: 0; }
|
|
3667
3753
|
|
|
3668
3754
|
/* Switch: a checkbox that reads as on / off, for a setting that applies at once (no Save button).
|
|
3669
|
-
`<label class="ui-check ui-switch"><input type="checkbox" role="switch" checked>
|
|
3755
|
+
`<label class="ui-check ui-switch"><input type="checkbox" role="switch" checked><span>Send Monday digest</span></label>`.
|
|
3670
3756
|
Track 32×20, thumb 16; the row stays the checkbox's 44px target. */
|
|
3671
3757
|
.ui-switch > input[type="checkbox"] {
|
|
3672
3758
|
appearance: none; -webkit-appearance: none; position: relative; width: 32px; height: 20px; margin: 0;
|