@gemboss/ui 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,6 +40,55 @@ Surfaces with no React (gemcare, gempress) use the CSS alone and write the same
40
40
  `nav.icon-nav > .gnav-scroll > .gnav-group > a.gnav-item > span.gnav-ico + span.gnav-label`.
41
41
  `packages/ui/src/shell.tsx` is the reference.
42
42
 
43
+ ## Page width (0.8.1)
44
+
45
+ Chris, 04/10/2026: the rail on the left, and on the right a content column with the same max width on every
46
+ service, not edge to edge. The tiers are the admin's (`gemboss-shell.css`, `.gemboss-w-*` around a Polaris Page),
47
+ same numbers; `page-width.test.tsx` fails if the admin and the package disagree.
48
+
49
+ | Tier | Class | Max width | For | Admin |
50
+ |---|---|---|---|---|
51
+ | `default` | `ui-page` | `--ui-content-max` (900px) | lists, tables, card grids, dashboards, detail pages | bare Page, `.gemboss-w-gallery`, `.gemboss-w-dashboard` |
52
+ | `feed` | `ui-page ui-page--feed` | 720px | read top to bottom: activity, logs, timelines, a thread | `.gemboss-w-feed` |
53
+ | `form` | `ui-page ui-page--form` | 662px | one column of fields: settings, a create form | `.gemboss-w-form` (Polaris narrowWidth) |
54
+ | `canvas` | `ui-page ui-page--canvas` | 1440px | content measured to be wider than a column of text: a horizontal path, a wide comparison table | `.gemboss-w-canvas` (funnel path, 864px needed) |
55
+ | `full` | `ui-page ui-page--full` | none | a board or an editor that IS the screen. Write the reason next to it. | the editor (fixed, its own frame) |
56
+
57
+ React: one `PageContainer` directly inside `AppShell` caps every screen. Pass the CURRENT route's tier; a page
58
+ cannot widen a cap its parent set, so a screen that needs `canvas` or `full` gets it here, not from a second
59
+ wrapper of its own.
60
+
61
+ ```tsx
62
+ <AppShell rail={rail}>
63
+ <PageContainer width={route === "settings" ? "form" : route === "board" ? "full" : "default"}>{page}</PageContainer>
64
+ </AppShell>
65
+ ```
66
+
67
+ `as="section"` changes the tag. `pageWidthClass("form")` returns the class string for a server template.
68
+ `PageContainer` lives in its own plain module (`packages/ui/src/page.tsx`), not in `shell.tsx`, and `AppShell` has no width
69
+ prop: the admin imports the shell pieces from `shell.tsx`, a client module its bundler keeps whole, and the
70
+ admin's first-load shell budget had no room for even this much JS (240.0 / 240 KB, 04/10/2026).
71
+
72
+ Plain HTML (GemFactory, gemcare, gempress): one wrapper as the first child of main.
73
+
74
+ ```html
75
+ <main class="gemboss-platform-main">
76
+ <div class="ui-page ui-page--form">…</div>
77
+ </main>
78
+ ```
79
+
80
+ Delete the surface's own cap when it moves onto a tier (`maxWidth: 1160` on each gemwatcher screen, a
81
+ `max-width` on a `.container`): two caps on one column means the narrower one wins and nobody can tell which.
82
+
83
+ **A cap only exists where it is SMALLER than the room it sits in.** Right of the 240px rail, inside main's 24px
84
+ padding, the room is `viewport − 288`: 992 at 1280, 1152 at 1440, 1632 at 1920. So 900 bites at all three, and
85
+ 1440 (`canvas`) bites only from about 1730px; below that the column is just the room. Before you quote a width,
86
+ check `cap < viewport − rail − padding` at the width people actually use (the admin shipped two caps, 1040 then
87
+ 998, that were wider than the room at 1277 and did nothing there, 10/09/2026). The column is never wider than
88
+ the room, so no tier makes a page scroll sideways; at ≤760px there is no rail and the column is `viewport − 32`.
89
+
90
+ The page must reset `body { margin: 0 }` itself: `gemboss-ui.css` does not (the admin gets it from Polaris).
91
+
43
92
  ## The sign-in screen
44
93
 
45
94
  ```tsx
@@ -85,17 +134,27 @@ A live run is never thrown off screen (`assistant/dock.ts` holds that rule). The
85
134
  its top edge and slides along the bottom, by pointer or keyboard, and remembers both under
86
135
  `storagePrefix`.
87
136
 
88
- The side panel is opt-in: `<AssistantHost allowSide>` puts a "Pin to the side" button in the popup
89
- header, and the panel carries "Float as a popup" back. It stands under the topbar on the right, and
137
+ The side panel is the DEFAULT shape since 0.8.0 (Chris, 04/10/2026): a browser that never chose opens the assistant
138
+ there, and `allowSide` defaults to true — a surface whose layout cannot make room passes `allowSide={false}` and gets
139
+ the popup. The popup header has "Pin to the side", and the panel carries "Float as a popup" back; the choice is kept
140
+ under `<prefix>.shape` (the older `floatShape` key recorded the old default for everyone, so it is not read). The rail
141
+ link reopens in that shape unless the person last worked full screen. A popup opens as tall as the window allows,
142
+ 12px under the topbar (it was min(480, viewport − 176)); a height the person dragged to is kept, and dragging it back
143
+ to the top returns to "as tall as the window allows".
144
+
145
+ **Upgrading to 0.8.0:** a host that never passed `allowSide` now offers the panel. Its page must make room — render
146
+ inside `AppShell` (`.gemboss-platform-main` takes the margin) or pad by `var(--gb-assistant-side-w)` while
147
+ `<prefix>-side` is on `<body>` — or pass `allowSide={false}`. A full-screen "shrink" handler should call `reopen()`
148
+ (back to the working shape), not `openDock()`, which records "popup" as the person's choice. It stands under the topbar on the right, and
90
149
  the page moves over for it instead of being covered: the provider puts `<prefix>-side` on `<body>` and
91
150
  the width in `--gb-assistant-side-w` (`SIDE_WIDTH_VAR`), and `.gemboss-platform-main` takes that as
92
151
  `margin-right`. A surface with its own layout reads the same variable. Its left edge is a
93
152
  `.ui-resizer` (drag, or ←/→, Shift for 64px, Home/End), clamped to 380–720px and never leaving the page
94
153
  less than 480px; the width is remembered. Panel and page only fit side by side from 1100px (rail 240
95
- + 380 + 480). Between 761 and 1099 the panel covers the content area and is not offered; on a phone
96
- it takes the screen. The status bar, a busy ×, leaving full screen mid-run and full
154
+ + 380 + 480). Between 761 and 1099 the panel covers the content area and is not offered — the host
155
+ withdraws `allowSide` there (`SIDE_FITS_QUERY`), so even the default opens the popup; on a phone it takes the screen. The status bar, a busy ×, leaving full screen mid-run and full
97
156
  screen's own way back ("Back to the side panel") all return to whichever shape the person last
98
- worked in (`floatShape`, remembered across reloads), so a pinned panel never pops back as a popup.
157
+ worked in (`floatShape`, remembered across reloads under `<prefix>.shape`), so a pinned panel never pops back as a popup.
99
158
  The host registers `allowSide` with the provider, and every way into the panel checks it, including
100
159
  a "side" remembered in storage: a host without it gets the popup.
101
160
 
@@ -201,6 +260,7 @@ surfaces on the package, the same gaps were filled six ways. What 0.5.0 settles,
201
260
  |---|---|---|
202
261
  | Control height: `gemboss-field`, `ui-select`, `ui-btn`, `ui-seg` | **32px** (Polaris TextField / Select), `--ui-control-h` | **44px**, `ui-btn--sm` included |
203
262
  | `ui-btn--sm` | 28px | 44px |
263
+ | `ui-btn` inside a popup's head or foot (0.7.3) | 28px, 12px | 44px |
204
264
  | Focus ring, every control | 2px iris `--ui-focus` (= `--gemboss-accent`, the admin's `--p-color-border-focus`) | same |
205
265
  | Body text (`html`, `body`, at zero specificity) | 13px / 20px, weight 450, `--gemboss-text`, antialiased (Polaris bodyMd) | same |
206
266
  | Kit text (tabs, table, notice, pager, empty, page-head lead) | `--ui-fs-body`, 13px | 16px in fields (iOS zoom) |
@@ -248,6 +308,42 @@ If your surface still sets any of these itself (a `body { font-size }`, a `min-h
248
308
  cascade over the package's zero-specificity base. `packages/ui/src/kit-look.test.ts` measures the table above in
249
309
  Chromium on the built CSS.
250
310
 
311
+ ## Popup density (0.7.3)
312
+
313
+ The kit popup (`.ui-overlay > .ui-modal`, `<dialog class="ui-modal">`, `openModal`, `Dialog`) is drawn at Polaris
314
+ Modal's density, the popup the admin opens. Chris, 04/10/2026, on GemFactory's detail popup: the admin's buttons are
315
+ smaller and save room (GemFeedback #1099). Measured at 1280 with a mouse:
316
+
317
+ | | Polaris Modal (admin) | 0.7.1 / 0.7.3 | 0.7.3 |
318
+ |---|---|---|---|
319
+ | × | 28×28 | 44×44 at every pointer | **28×28**; 44×44 under a thumb or ≤ 767px wide (head stays 53) |
320
+ | Head | 53px, padding 12/16 | 61px, padding 16/20 | **53px**, padding 12/16, `align-items: center` |
321
+ | Title | 14px / 650 | 15px / 620 | **14px / 650** |
322
+ | `ui-btn` in `__head` / `__foot` | 28 tall, 12px, padding 0/12 | 32, 13px, 0/16 | **28, 12px, 0/12**; 44 under a thumb or ≤ 767px |
323
+ | Body / foot padding | 16 / 16 | 16/20 / 12/20 | **16 / 12/16** |
324
+ | Scrim | rgba(0,0,0,.5), no blur | .6, blur 2px | **.5, no blur** (`::backdrop` too) |
325
+
326
+ Popup only: `--ui-control-h` stays 32, and a button outside a popup is still 32 tall.
327
+
328
+ `.ui-modal__body` and `.ui-modal__foot` moved from `kit-plain.css` to `ui.css`. The admin loads `ui.css` and not
329
+ `kit-plain.css`, so before 0.7.3 its kit popups drew a body with no padding and two foot buttons touching
330
+ (AutopilotConfirmModal). The scrolling layout of a tall popup stays in `kit-plain.css`.
331
+
332
+ A large popup can keep its subtitle and its own actions in the kit head instead of drawing a second bar
333
+ (`kit-plain.css`):
334
+
335
+ ```html
336
+ <div class="ui-modal__head">
337
+ <div class="ui-modal__title">Mega menu<span class="ui-modal__subtitle">chrome/header</span></div>
338
+ <div class="ui-modal__actions"><button class="ui-btn">Copy</button><button class="ui-btn ui-btn--icon" aria-label="More">…</button></div>
339
+ <button type="button" class="ui-modal__close" aria-label="Close">…</button>
340
+ </div>
341
+ ```
342
+
343
+ `packages/ui/src/modal-density.test.ts` measures the table in Chromium at 1280 (mouse), 390 and under a finger, on
344
+ `gemboss-ui.css` and on the admin's sheets alone. `UI_MODAL_ROOT=<a 0.7.1 tree>` runs it against the old CSS, where
345
+ it is red.
346
+
251
347
  ## Resizable panels
252
348
 
253
349
  `ui-resizer` is the seam a person drags between two panels; `ui-resizing` on `<body>` is the drag in
@@ -428,6 +524,31 @@ surface computes every number and ratio; the kit only draws. Markup for each is
428
524
  - `ui-grid--2` / `--3`: card grid that drops to one column; `ui-section-head`: an `h2` + note between page head and
429
525
  card titles.
430
526
 
527
+ ## Long lists: pages or "load more" (0.7.3)
528
+
529
+ *Recommendation, not a rule: Claude, 04/10/2026, after Chris asked it to decide for GemFactory.*
530
+
531
+ | List | Pattern | Why |
532
+ |---|---|---|
533
+ | A work list: items with an #ID, filters, a review queue, links people send each other (GemFactory board) | **Pages**, `ui-pager` with numbers, position in the URL (`?p=3`) | Back returns to the same place, a link opens the same page, the total is visible. Infinite scroll loses all three. |
534
+ | One column of a Kanban (GemFactory), or a feed read top to bottom | **Load more**, `ui-more` | Nobody jumps to page 9 of one column or of a feed. |
535
+
536
+ `ui-pager`: `<a rel="prev">` · page links · `<span class="ui-pager__gap" aria-hidden="true">…</span>` · the current page as
537
+ `<a aria-current="page">` (filled) · `<a rel="next">` · `<span class="ui-pager__range">201–250 of 655</span>` (pushed right).
538
+ On the first or last page the edge is `<a role="link" aria-disabled="true">` with no `href`: a screen reader still reads a
539
+ link and says it is unavailable (a `<span aria-disabled>` has no role, so it reads as plain text). It is muted and does
540
+ not react to hover. Page links are at least 32x32, 44x44 under a finger. Arrow and word are two flex items, hence the 4px
541
+ gap. Under 576px only Previous · the current page · Next stay and the range wraps under them (eleven 44px targets
542
+ took three to four rows at 320). The old form `<span aria-current="page">2 / 9</span>` keeps its muted look. The surface computes which numbers to
543
+ show (first, last, current ± 1, a gap only where it hides two or more pages: GemFactory `pageWindow`).
544
+
545
+ `ui-more`: one centred row. Idle: a `ui-btn` ("Show 24 more"). Loading: `<span class="ui-spinner" aria-hidden="true">` +
546
+ `<span role="status">Loading…</span>`. Error: `<span role="alert">Couldn't load.</span>` + "Try again". End: a line. As an
547
+ IntersectionObserver sentinel it has height from its padding; zero the padding and it has none.
548
+
549
+ `.ui-prose` links are at `:where()` specificity, so a component's own link rule (`ui-pager`, `ui-seg`, `ui-tabs`) wins
550
+ inside a page-wide `.ui-prose` (GemFactory wraps every page in it; its page numbers were drawn as blue underlined text).
551
+
431
552
  ## The catalog: every token, class and component, with its markup
432
553
 
433
554
  `npm run catalog` builds `catalog/index.html` (also shipped in the npm tarball, and built by `prepack`):
@@ -475,7 +596,7 @@ something the base cannot express, add a token or a prop here, and every surface
475
596
  | `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). |
476
597
  | `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. |
477
598
  | `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. |
478
- | `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. All inside `gemboss-ui.css`; the admin @imports `chat.css` and `chat-composer.css` itself. |
599
+ | `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. |
479
600
  | `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). |
480
601
  | `packages/ui/src/**/*.tsx` | the components, as TypeScript source. Next: add `@gemboss/ui` to `transpilePackages`. Vite compiles it as is. |
481
602