@gemboss/ui 0.8.0 → 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 +181 -2
- package/catalog/index.html +316 -5
- package/gemboss-ui.css +118 -6
- package/package.json +12 -3
- package/src/app-shell.css +102 -0
- package/src/editor.css +1096 -0
- package/src/index.ts +5 -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/page.tsx +35 -0
- package/src/route-holding.tsx +65 -0
- package/src/shell.tsx +12 -7
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
|
|
@@ -2154,6 +2230,32 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
2154
2230
|
/* A phone has no ⌘ key, and the box needs every pixel the bar has left. */
|
|
2155
2231
|
@media (max-width: 460px) { .gemboss-topbar-search-kbd { display: none; } }
|
|
2156
2232
|
|
|
2233
|
+
/* ── Page width (0.8.1) ──────────────────────────────────────────────────────────────────────────────
|
|
2234
|
+
* Chris 04/10/2026: the rail on the left, and on the right a content column with ONE max width across every
|
|
2235
|
+
* service, not edge to edge. The admin's tiers (gemboss-shell.css, `.gemboss-w-*` around a Polaris Page), same
|
|
2236
|
+
* numbers; `page-width.test.tsx` fails if the two disagree:
|
|
2237
|
+
* .ui-page → --ui-content-max (900px, ui.css): lists, tables, card grids, dashboards
|
|
2238
|
+
* .ui-page--feed → 720px: read top to bottom (activity, logs, timelines)
|
|
2239
|
+
* .ui-page--form → 662px: one column of fields (Polaris narrowWidth)
|
|
2240
|
+
* .ui-page--canvas → 1440px: content measured to be wider than a reading column (admin: the funnel path)
|
|
2241
|
+
* .ui-page--full → no cap: a board or an editor that is the screen. Say why where you use it.
|
|
2242
|
+
* A cap only exists where it is SMALLER than the room it sits in. Right of the 240px rail with main's 24px padding,
|
|
2243
|
+
* 900 bites from 1188px up (1280 → 992 of room); 1440 bites only from ~1730px. Below that the column is simply the
|
|
2244
|
+
* room, so the tier never causes a horizontal scroll.
|
|
2245
|
+
* Here, not in ui.css: ui.css is exported verbatim into the admin under a 90 KB budget (89.8 KB at 0.7.3), and the
|
|
2246
|
+
* admin draws its tiers on Polaris' Page instead. Fixed px, not %: a % cap grows the line length with the monitor. */
|
|
2247
|
+
.ui-page {
|
|
2248
|
+
box-sizing: border-box;
|
|
2249
|
+
width: 100%;
|
|
2250
|
+
min-width: 0;
|
|
2251
|
+
max-width: var(--ui-content-max, 900px);
|
|
2252
|
+
margin-inline: auto;
|
|
2253
|
+
}
|
|
2254
|
+
.ui-page--feed { max-width: 720px; }
|
|
2255
|
+
.ui-page--form { max-width: 662px; }
|
|
2256
|
+
.ui-page--canvas { max-width: 1440px; }
|
|
2257
|
+
.ui-page--full { max-width: none; }
|
|
2258
|
+
|
|
2157
2259
|
/* ═══ @gemboss/ui src/auth.css ═══ */
|
|
2158
2260
|
/* The GemBoss sign-in screen — @gemboss/ui (28/09/2026).
|
|
2159
2261
|
*
|
|
@@ -3365,10 +3467,19 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
3365
3467
|
.ui-menu__sep { height: 1px; margin: var(--ui-space-1) 0; border: 0; background: var(--ui-border); }
|
|
3366
3468
|
/* Checkbox: the LABEL is the target; disabled turns both the label and the box to not-allowed. Focus ring in the
|
|
3367
3469
|
kit's own token (no --p-* here: this file is appended after the export's Polaris resolver runs). */
|
|
3368
|
-
.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); }
|
|
3369
3471
|
.ui-check > input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); cursor: inherit; }
|
|
3370
3472
|
.ui-check > input:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
|
|
3371
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; }
|
|
3372
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); }
|
|
3373
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); }
|
|
3374
3485
|
.ui-kv > dt { color: var(--ui-text-muted); }
|
|
@@ -3635,12 +3746,13 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
|
3635
3746
|
}
|
|
3636
3747
|
|
|
3637
3748
|
/* Radio: the checkbox's row with `type="radio"` — `<fieldset class="ui-choices"><legend class="ui-label">Plan</legend>
|
|
3638
|
-
<label class="ui-check"><input type="radio" name="plan" checked>
|
|
3639
|
-
|
|
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; }
|
|
3640
3752
|
.ui-choices > legend { padding: 0; }
|
|
3641
3753
|
|
|
3642
3754
|
/* Switch: a checkbox that reads as on / off, for a setting that applies at once (no Save button).
|
|
3643
|
-
`<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>`.
|
|
3644
3756
|
Track 32×20, thumb 16; the row stays the checkbox's 44px target. */
|
|
3645
3757
|
.ui-switch > input[type="checkbox"] {
|
|
3646
3758
|
appearance: none; -webkit-appearance: none; position: relative; width: 32px; height: 20px; margin: 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemboss/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "GemBoss design system for every surface in the fleet: the admin shell (topbar + rail + drawer) as React components, and the admin stylesheet exported as one CSS file.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"./shell.css": "./src/shell.css",
|
|
21
21
|
"./chat.css": "./src/chat.css",
|
|
22
22
|
"./chat-composer.css": "./src/chat-composer.css",
|
|
23
|
+
"./editor.css": "./src/editor.css",
|
|
23
24
|
"./auth.css": "./src/auth.css",
|
|
24
25
|
"./kit-plain.css": "./src/kit-plain.css",
|
|
25
26
|
"./kit-plain.js": "./src/kit-plain.js",
|
|
@@ -28,7 +29,8 @@
|
|
|
28
29
|
"./assistant.css": "./src/assistant.css",
|
|
29
30
|
"./portal.css": "./src/portal.css",
|
|
30
31
|
"./theme-dark.css": "./src/theme-dark.css",
|
|
31
|
-
"./icon.svg": "./src/icon.svg"
|
|
32
|
+
"./icon.svg": "./src/icon.svg",
|
|
33
|
+
"./next": "./src/next/index.ts"
|
|
32
34
|
},
|
|
33
35
|
"files": [
|
|
34
36
|
"src/**/*.ts",
|
|
@@ -39,7 +41,8 @@
|
|
|
39
41
|
"!src/**/*.test.*",
|
|
40
42
|
"gemboss-ui.css",
|
|
41
43
|
"catalog/index.html",
|
|
42
|
-
"README.md"
|
|
44
|
+
"README.md",
|
|
45
|
+
"CHANGELOG.md"
|
|
43
46
|
],
|
|
44
47
|
"scripts": {
|
|
45
48
|
"build:css": "node scripts/build-css.mjs",
|
|
@@ -49,8 +52,14 @@
|
|
|
49
52
|
"fleet": "node scripts/fleet.mjs"
|
|
50
53
|
},
|
|
51
54
|
"peerDependencies": {
|
|
55
|
+
"next": ">=15.3",
|
|
52
56
|
"react": ">=18"
|
|
53
57
|
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"next": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
54
63
|
"publishConfig": {
|
|
55
64
|
"access": "public"
|
|
56
65
|
},
|
package/src/app-shell.css
CHANGED
|
@@ -67,6 +67,82 @@
|
|
|
67
67
|
}
|
|
68
68
|
.gnav-item.active .gnav-count { background: var(--gemboss-primary); color: var(--gemboss-on-primary); }
|
|
69
69
|
|
|
70
|
+
/* ── Navigation pending (0.10.0): a click answers on the first frame ─────────────────────────────────────────────
|
|
71
|
+
* Chris 05/10/2026: content-radar's rail (the icon turns into a spinner the moment you click, sub items too) is the
|
|
72
|
+
* standard for every surface. Measured 04/10 on content-radar with a server ~280 ms away: nothing on screen changed
|
|
73
|
+
* for 321-503 ms after a click; with this, 6-8 ms.
|
|
74
|
+
*
|
|
75
|
+
* ONE state, three ways to set it, so a React rail, a Next rail and a server-rendered rail look the same:
|
|
76
|
+
* - `aria-busy="true"` on the item: RailItem / RailSubItem `pending` (React), kit-plain.js on a followed link;
|
|
77
|
+
* - a `.gnav-pending` child: NavLink from `@gemboss/ui/next` (Next's useLinkStatus is only readable INSIDE the
|
|
78
|
+
* link, so the link cannot set its own attribute; it renders this status text and `:has()` reads it).
|
|
79
|
+
* `:is()` is forgiving: a browser without `:has()` still honours the aria-busy half of each rule.
|
|
80
|
+
* The icon keeps its box (visibility, not display), so the label never moves. Here and not in shell.css: the
|
|
81
|
+
* admin's shell sheet has <1 KB of first-load headroom, and the admin answers a click with its own holding screen.
|
|
82
|
+
*/
|
|
83
|
+
.gnav-item:is([aria-busy="true"], :has(> .gnav-pending)),
|
|
84
|
+
.gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending)) { cursor: progress; }
|
|
85
|
+
.gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico > * { visibility: hidden; }
|
|
86
|
+
.gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico::after,
|
|
87
|
+
.gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after,
|
|
88
|
+
.gb-pending-mark {
|
|
89
|
+
content: ""; display: inline-block; vertical-align: middle; grid-area: 1 / 1; flex: none; box-sizing: border-box;
|
|
90
|
+
width: 20px; height: 20px; border-radius: 50%;
|
|
91
|
+
/* the item's own ink, so the spinner follows the active and hover colours */
|
|
92
|
+
border: 2px solid color-mix(in srgb, currentColor 18%, transparent); border-top-color: currentColor;
|
|
93
|
+
animation: gnav-spin 0.7s linear infinite;
|
|
94
|
+
}
|
|
95
|
+
/* 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
|
|
96
|
+
* it would be a third item in a space-between row and push a trailing count to the middle (review #3777). */
|
|
97
|
+
.gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after {
|
|
98
|
+
position: absolute; left: 22px; top: 50%; margin-top: -6px; width: 12px; height: 12px; border-width: 1.5px;
|
|
99
|
+
}
|
|
100
|
+
@keyframes gnav-spin { to { transform: rotate(360deg); } }
|
|
101
|
+
/* Slower, never off: it is the only sign that the click was heard. */
|
|
102
|
+
@media (prefers-reduced-motion: reduce) {
|
|
103
|
+
.gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico::after,
|
|
104
|
+
.gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after,
|
|
105
|
+
.gb-pending-mark { animation-duration: 2s; }
|
|
106
|
+
}
|
|
107
|
+
/* A link in the PAGE (a list row, a card, a tab) opts in with `data-gb-pending`: PendingLink (`@gemboss/ui/next`) or
|
|
108
|
+
* kit-plain.js put `span.gb-pending-mark` (role=status) inside it while the page loads. "inline" (default): 12px after
|
|
109
|
+
* the text. "corner": 16px in the top right corner, out of the flow, for a link that wraps a whole card (a grid card
|
|
110
|
+
* gains no column). Marked by attribute, not class: a surface that colours bare links with `a:not([class])` keeps it. */
|
|
111
|
+
a[data-gb-pending]:has(> .gb-pending-mark), a[data-gb-pending][aria-busy="true"] { cursor: progress; }
|
|
112
|
+
a[data-gb-pending] > .gb-pending-mark { display: inline-block; width: 12px; height: 12px; border-width: 1.5px; margin-left: 6px; vertical-align: -1px; }
|
|
113
|
+
a[data-gb-pending="corner"] { position: relative; }
|
|
114
|
+
a[data-gb-pending="corner"] > .gb-pending-mark { position: absolute; top: 12px; right: 12px; width: 16px; height: 16px; margin: 0; }
|
|
115
|
+
/* RouteHolding (./route-holding.tsx): a route group's loading boundary. The kit's skeleton lines in the content column,
|
|
116
|
+
* one shape for every route: a title bar, then four lines of falling length (the admin's holding screen). */
|
|
117
|
+
.gb-route-holding { padding-top: 4px; }
|
|
118
|
+
.gb-route-holding .ui-skeleton { gap: 16px; }
|
|
119
|
+
.gb-route-holding__heading { margin: 0 0 20px; font-size: 20px; font-weight: 650; line-height: 1.3; color: var(--gemboss-text); }
|
|
120
|
+
.gb-route-holding .ui-skeleton__line { height: 10px; }
|
|
121
|
+
.gb-route-holding .ui-skeleton__line.gb-route-holding__title { height: 20px; width: 34%; margin-bottom: 8px; }
|
|
122
|
+
.gb-route-holding .ui-skeleton__line:nth-last-child(4) { width: 46%; }
|
|
123
|
+
.gb-route-holding .ui-skeleton__line:nth-last-child(3) { width: 92%; }
|
|
124
|
+
.gb-route-holding .ui-skeleton__line:nth-last-child(2) { width: 78%; }
|
|
125
|
+
.gb-route-holding .ui-skeleton__line:nth-last-child(1) { width: 61%; }
|
|
126
|
+
/* RouteCover: the same frame over the content area from the click until the navigation commits. Shown only while
|
|
127
|
+
* `html[data-gb-navigating]` (set by the @gemboss/ui/next links while pending). No grace delay: the target is the new
|
|
128
|
+
* 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
|
|
129
|
+
* 80 ms fade is what a router cache hit (20-90 ms) sees of it: a faint, partial frame, not a full flash.
|
|
130
|
+
* z 5: above the outgoing page, below the rail, the topbar (300) and the assistant (290). */
|
|
131
|
+
.gb-route-cover { display: none; }
|
|
132
|
+
html[data-gb-navigating] .gb-route-cover {
|
|
133
|
+
display: block; position: fixed; inset: 56px 0 0 240px; z-index: 5; overflow: hidden; box-sizing: border-box;
|
|
134
|
+
padding: 24px; background: var(--gemboss-app-bg); pointer-events: none;
|
|
135
|
+
animation: gb-route-cover-in 0.08s ease-out both;
|
|
136
|
+
}
|
|
137
|
+
@keyframes gb-route-cover-in { from { opacity: 0; } to { opacity: 1; } }
|
|
138
|
+
@media (max-width: 760px) { html[data-gb-navigating] .gb-route-cover { left: 0; padding: 16px; } }
|
|
139
|
+
@media (prefers-reduced-motion: reduce) { html[data-gb-navigating] .gb-route-cover { animation-duration: 0s; } }
|
|
140
|
+
/* The status text a NavLink renders inside a rail item: read, not seen. */
|
|
141
|
+
.gnav-pending {
|
|
142
|
+
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
|
|
143
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
|
|
144
|
+
}
|
|
145
|
+
|
|
70
146
|
/* ── BrandLockup (./brand.tsx) ─────────────────────────────────────────────────────────────────
|
|
71
147
|
* The admin's `.gemboss-brand-lockup` box (40px tall, 8px padding, hover wash) around the wordmark's
|
|
72
148
|
* own proportions. Not that class itself: its ≤460px rule swaps in a background image at an
|
|
@@ -139,3 +215,29 @@
|
|
|
139
215
|
@media (pointer: coarse), (max-width: 47.9975em) { .gemboss-topbar-search { height: 44px; } }
|
|
140
216
|
/* A phone has no ⌘ key, and the box needs every pixel the bar has left. */
|
|
141
217
|
@media (max-width: 460px) { .gemboss-topbar-search-kbd { display: none; } }
|
|
218
|
+
|
|
219
|
+
/* ── Page width (0.8.1) ──────────────────────────────────────────────────────────────────────────────
|
|
220
|
+
* Chris 04/10/2026: the rail on the left, and on the right a content column with ONE max width across every
|
|
221
|
+
* service, not edge to edge. The admin's tiers (gemboss-shell.css, `.gemboss-w-*` around a Polaris Page), same
|
|
222
|
+
* numbers; `page-width.test.tsx` fails if the two disagree:
|
|
223
|
+
* .ui-page → --ui-content-max (900px, ui.css): lists, tables, card grids, dashboards
|
|
224
|
+
* .ui-page--feed → 720px: read top to bottom (activity, logs, timelines)
|
|
225
|
+
* .ui-page--form → 662px: one column of fields (Polaris narrowWidth)
|
|
226
|
+
* .ui-page--canvas → 1440px: content measured to be wider than a reading column (admin: the funnel path)
|
|
227
|
+
* .ui-page--full → no cap: a board or an editor that is the screen. Say why where you use it.
|
|
228
|
+
* A cap only exists where it is SMALLER than the room it sits in. Right of the 240px rail with main's 24px padding,
|
|
229
|
+
* 900 bites from 1188px up (1280 → 992 of room); 1440 bites only from ~1730px. Below that the column is simply the
|
|
230
|
+
* room, so the tier never causes a horizontal scroll.
|
|
231
|
+
* Here, not in ui.css: ui.css is exported verbatim into the admin under a 90 KB budget (89.8 KB at 0.7.3), and the
|
|
232
|
+
* admin draws its tiers on Polaris' Page instead. Fixed px, not %: a % cap grows the line length with the monitor. */
|
|
233
|
+
.ui-page {
|
|
234
|
+
box-sizing: border-box;
|
|
235
|
+
width: 100%;
|
|
236
|
+
min-width: 0;
|
|
237
|
+
max-width: var(--ui-content-max, 900px);
|
|
238
|
+
margin-inline: auto;
|
|
239
|
+
}
|
|
240
|
+
.ui-page--feed { max-width: 720px; }
|
|
241
|
+
.ui-page--form { max-width: 662px; }
|
|
242
|
+
.ui-page--canvas { max-width: 1440px; }
|
|
243
|
+
.ui-page--full { max-width: none; }
|