@gemboss/ui 0.4.1 → 0.5.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/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: b05e167c5874f1ccbc73b215c8f30e38038a974b
5
- * sources sha256: cbbd2b3684dc3552bfa6914f7d61ae06fba666b0811a95a005303c0c6cb6f6d1
4
+ * gemboss commit: 5a8d401f4c0a723a8ed69bbbe8871a0323fd66cd
5
+ * sources sha256: e74fceb32d9769c9079a6fc1d3cda8d911188ce0b0825965f85ebf185d9200fb
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
  * + 180 rules from packages/ui/src/shell.css (the shared shell)
@@ -96,7 +96,7 @@
96
96
  and reading as "the font keeps shrinking". New rule: pick a step here, never a
97
97
  raw px. Floor is 12px (--fs-caption); 11px (--fs-micro) is reserved for bold
98
98
  micro-badges/count-pills ONLY — nothing below that. Body default is 14px, one
99
- step up from Polaris bodySm(13) which the app had been leaning on for prose. */
99
+ step up from Polaris bodyMd (13px; bodySm is 12) which the app had been leaning on for prose. The kit (ui.css) reads --ui-fs-body, 13px, because it sits beside Polaris components. */
100
100
  --gemboss-fs-micro: 11px; /* bold count-pills / status chips only */
101
101
  --gemboss-fs-caption: 12px; /* metadata, timestamps, secondary labels */
102
102
  --gemboss-fs-body: 14px; /* default body / reading text */
@@ -218,7 +218,10 @@
218
218
  --ui-success-fg: var(--gemboss-success);
219
219
  --ui-warning-fg: var(--gemboss-primary-dark);
220
220
  --ui-danger-fg: var(--gemboss-danger-strong);
221
- --ui-focus: var(--gemboss-primary, #1b1c1e);
221
+ /* ONE focus ring for every control (02/10/2026): the admin's, iris — gemboss-shell.css points Polaris'
222
+ --p-color-border-focus at --gemboss-accent. Before, kit buttons, tabs and checks drew black, fields
223
+ and selects #005bd3 (Polaris' default, which the export shipped), and the admin drew iris. */
224
+ --ui-focus: var(--gemboss-accent, #5661f5);
222
225
  --ui-radius-sm: 6px;
223
226
  --ui-radius-md: 8px;
224
227
  --ui-radius-lg: 12px;
@@ -226,7 +229,10 @@
226
229
  --ui-space-2: 8px;
227
230
  --ui-space-3: 12px;
228
231
  --ui-space-4: 16px;
229
- --ui-control-h: 34px;
232
+ /* ONE control height under a mouse: Polaris' TextField/Select (32px). Field, select, button and
233
+ segment share it, so a toolbar row is one height (was 32 / 36 / 34 / 36). Under a thumb every
234
+ control is 44px (the coarse block below; kit-plain.css adds phone WIDTH for non-admin surfaces). */
235
+ --ui-control-h: 32px;
230
236
  --ui-fs-body: 13px;
231
237
  --ui-fs-caption: 12px;
232
238
  /* Content column width for the seller app shell — ONE source of truth for every wide surface
@@ -323,10 +329,10 @@
323
329
  Its BOX is the text field's (29/09/2026, Chris): same --gemboss-field-* tokens for ground, line and
324
330
  ink, same hover, same focus ring as `.gemboss-field`. Before, a grey --ui-surface-2 select stood next
325
331
  to a bordered white field in the same form (gempress editor, admin forms), and `:focus` removed the
326
- outline and only recoloured the border, so a keyboard user saw almost nothing. Height stays 36px
327
- (--ui-control-h), because a select sits in toolbars beside 36px `.ui-btn`s. */
332
+ outline and only recoloured the border, so a keyboard user saw almost nothing. Height is
333
+ --ui-control-h, the button's and the field's, because a select sits in toolbars beside both. */
328
334
  .ui-select {
329
- height: 36px; padding: 0 32px 0 12px; max-width: 100%;
335
+ height: var(--ui-control-h); padding: 0 32px 0 12px; max-width: 100%;
330
336
  background: var(--gemboss-field-bg) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 20 20'><path fill='%239a9aa4' d='M5 7l5 5 5-5z'/></svg>") no-repeat right 10px center;
331
337
  border: 1px solid var(--gemboss-field-line); border-radius: var(--ui-radius-md);
332
338
  color: var(--gemboss-field-ink); font-family: inherit; font-size: var(--ui-fs-body); font-weight: 500;
@@ -338,7 +344,7 @@
338
344
  }
339
345
  .ui-select:focus { outline: none; }
340
346
  .ui-select:focus-visible:not(:disabled) {
341
- outline: 2px solid #005bd3; outline-offset: 1px;
347
+ outline: 2px solid var(--ui-focus); outline-offset: 1px;
342
348
  border-color: var(--gemboss-field-line-active);
343
349
  }
344
350
  .ui-select:disabled { opacity: .5; cursor: not-allowed; }
@@ -349,7 +355,7 @@
349
355
  bordered mono token (was gemboss-vault-chip). `accent` tone for the highlighted state. */
350
356
  .ui-tag {
351
357
  display: inline-flex; align-items: center; gap: 6px;
352
- font-size: 10px; font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase;
358
+ font-size: var(--gemboss-fs-micro, 11px); font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase;
353
359
  padding: 1px 6px; border-radius: 6px; background: var(--ui-surface-2); color: var(--ui-text-muted);
354
360
  }
355
361
  .ui-tag--accent { background: var(--adm-accent-soft, rgba(20,22,26,.05)); color: var(--adm-accent-fg, var(--ui-accent)); }
@@ -371,7 +377,7 @@
371
377
  font-family: inherit; font-size: var(--gemboss-fs-caption, 12px); color: var(--ui-text); text-decoration: none;
372
378
  }
373
379
  .ui-chips > a.ui-tag--chip:hover { background: var(--ui-surface-2); border-color: var(--ui-text-muted); }
374
- .ui-chips > a.ui-tag--chip:focus-visible { outline: 2px solid #005bd3; outline-offset: 1px; }
380
+ .ui-chips > a.ui-tag--chip:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
375
381
  /* Danh sách LỌC dài (chuyên môn, hạng…) dùng ui-chips thay ui-seg: ui-seg không xuống dòng nên tràn ngang ở 320px.
376
382
  Ô đang chọn = `aria-current="page"` (lọc là một URL), cùng tông với ui-seg đang chọn. */
377
383
  .ui-chips > a.ui-tag--chip[aria-current="page"] { background: var(--ui-accent-soft); border-color: var(--ui-accent); color: var(--ui-accent-strong); font-weight: 600; }
@@ -391,7 +397,7 @@
391
397
  và không dùng flex: admin không reset margin tiêu đề, nên đổi ở đây là đổi khoảng cách của 14 trang admin. */
392
398
  .ui-page-head { margin-bottom: 20px; }
393
399
  .ui-page-head h1 { color: var(--ui-text-strong); font-size: 22px; font-weight: 650; letter-spacing: -0.01em; }
394
- .ui-page-head p { color: var(--ui-text-muted); font-size: 14px; margin-top: 4px; }
400
+ .ui-page-head p { color: var(--ui-text-muted); font-size: var(--ui-fs-body); margin-top: 4px; }
395
401
 
396
402
  /* ── Thanh công cụ: `<div class="ui-toolbar">` — ô tìm giãn, điều khiển bám phải, một hàng gọn, xuống dòng khi hẹp.
397
403
  Đưa lên từ admin `.gemboss-admin-toolbar`. Chỉ BỐ CỤC: ô tìm là `gemboss-field gemboss-field--group`. */
@@ -426,7 +432,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
426
432
  /* ── Thông báo: `<div class="ui-notice ui-notice--warning">…</div>`. Lỗi tải, cảnh báo cấu hình, việc cần người
427
433
  làm. Mỗi tông mang màu chữ riêng (nền tông luôn sáng, kể cả trong admin tối). Không có tông = ghi chú trung tính.
428
434
  role="status"/"alert" CHỈ khi thông báo xuất hiện sau khi trang đã tải (vùng live); có sẵn lúc tải thì không role. */
429
- .ui-notice { padding: 12px 16px; border-radius: var(--ui-radius-md); border: 1px solid var(--ui-border); background: var(--ui-surface-2); color: var(--ui-text); font-size: var(--gemboss-fs-body); }
435
+ .ui-notice { padding: 12px 16px; border-radius: var(--ui-radius-md); border: 1px solid var(--ui-border); background: var(--ui-surface-2); color: var(--ui-text); font-size: var(--ui-fs-body); }
430
436
  .ui-notice--warning { background: var(--gemboss-warning-soft); border-color: var(--gemboss-warning-border); color: var(--gemboss-warning); }
431
437
  .ui-notice--danger { background: var(--gemboss-danger-soft); border-color: var(--gemboss-danger-border); color: var(--gemboss-danger-strong); }
432
438
  .ui-notice--success { background: var(--gemboss-success-soft); border-color: var(--gemboss-success-border); color: var(--gemboss-success-strong); }
@@ -434,18 +440,19 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
434
440
  /* ── Phân trang: `<nav class="ui-pager" aria-label="Trang"><a href rel="prev"><span aria-hidden="true">‹</span> Trước</a><span aria-current="page">2 / 9</span>
435
441
  <a href rel="next">Sau ›</a></nav>`. Link ≥ 32px (≥ 24px WCAG 2.5.8), 44px dưới ngón tay (khối coarse). */
436
442
  .ui-pager { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 16px 0; }
437
- .ui-pager > a { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); color: var(--ui-text); text-decoration: none; font-size: var(--gemboss-fs-body); }
443
+ .ui-pager > a { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); color: var(--ui-text); text-decoration: none; font-size: var(--ui-fs-body); }
438
444
  .ui-pager > a:hover { background: var(--ui-surface-2); }
439
- .ui-pager > a:focus-visible { outline: 2px solid #005bd3; outline-offset: 1px; }
445
+ .ui-pager > a:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
440
446
  .ui-pager > [aria-current="page"] { color: var(--ui-text-muted); font-size: var(--gemboss-fs-caption); }
441
447
 
442
448
  /* ── Badge ────────────────────────────────────────────────────────────────────
443
- Uppercase mono pill for a status/label (was gemboss-admin-storage-badge). */
449
+ Polaris' Badge (02/10/2026; was an uppercase mono pill): 12px/550, padding 2/8, radius 8, the secondary
450
+ fill. The admin's /admin console draws it too (components/ui/Badge.tsx). Tones (`--success|--warning|--critical|
451
+ --info`, `--dot`) are in kit-plain.css, for the non-React surfaces; the seller app uses Polaris <Badge tone>. */
444
452
  .ui-badge {
445
- display: inline-flex; align-items: center;
446
- font-size: 11px; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase;
447
- padding: 1px 8px; border-radius: 6px; background: var(--ui-surface-2); color: var(--ui-text-muted);
448
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
453
+ display: inline-flex; align-items: center; gap: 4px;
454
+ font-size: var(--ui-fs-caption); line-height: 16px; font-weight: 550; white-space: nowrap;
455
+ padding: 2px 8px; border-radius: var(--ui-radius-md); background: var(--ui-surface-2); color: var(--ui-text-muted);
449
456
  }
450
457
 
451
458
  /* ── Modal ────────────────────────────────────────────────────────────────────
@@ -500,7 +507,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
500
507
  }
501
508
  .ui-textarea:focus { outline: none; }
502
509
  .ui-textarea:focus-visible:not(:disabled) {
503
- outline: 2px solid #005bd3; outline-offset: 1px;
510
+ outline: 2px solid var(--ui-focus); outline-offset: 1px;
504
511
  border-color: var(--gemboss-field-line-active);
505
512
  }
506
513
  .ui-textarea:disabled { opacity: .5; cursor: not-allowed; }
@@ -518,7 +525,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
518
525
  selected segment uses the accent-soft state. */
519
526
  .ui-seg { display: inline-flex; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); overflow: hidden; }
520
527
  .ui-seg :is(button, a) {
521
- display: inline-grid; place-items: center; width: 36px; height: 36px;
528
+ display: inline-grid; place-items: center; width: calc(var(--ui-control-h) - 2px); height: calc(var(--ui-control-h) - 2px);
522
529
  border: 0; background: transparent; color: var(--ui-text-muted); cursor: pointer; font-family: inherit;
523
530
  }
524
531
  .ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
@@ -702,7 +709,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
702
709
  position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
703
710
  min-height: 44px; padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
704
711
  border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;
705
- font-family: inherit; font-size: var(--gemboss-fs-body); font-weight: 500; white-space: nowrap;
712
+ font-family: inherit; font-size: var(--ui-fs-body); font-weight: 500; white-space: nowrap;
706
713
  color: var(--ui-text-muted);
707
714
  transition: color var(--gemboss-transition-fast) ease, background-color var(--gemboss-transition-fast) ease;
708
715
  }
@@ -728,7 +735,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
728
735
  overflow-x: auto; max-width: 100%;
729
736
  background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
730
737
  }
731
- .ui-table { width: 100%; border-collapse: collapse; font-size: var(--gemboss-fs-body); color: var(--ui-text); }
738
+ .ui-table { width: 100%; border-collapse: collapse; font-size: var(--ui-fs-body); color: var(--ui-text); }
732
739
  .ui-table th,
733
740
  .ui-table td {
734
741
  padding: var(--ui-space-2) var(--ui-space-3); text-align: left; vertical-align: middle;
@@ -752,7 +759,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
752
759
  .ui-empty {
753
760
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--ui-space-2);
754
761
  padding: var(--ui-space-4); padding-block: 40px; text-align: center;
755
- color: var(--ui-text-muted); font-size: var(--gemboss-fs-body);
762
+ color: var(--ui-text-muted); font-size: var(--ui-fs-body);
756
763
  }
757
764
  .ui-empty__title { margin: 0; font-size: var(--gemboss-fs-h3); font-weight: 600; color: var(--ui-text-strong); }
758
765
  .ui-empty__body { margin: 0; max-width: 48ch; line-height: 1.5; }
@@ -811,7 +818,7 @@ input[type="search"]:is(.gemboss-field, .gemboss-field__bare) { -webkit-appearan
811
818
  .gemboss-field:hover:not(:disabled),
812
819
  .gemboss-control:hover:not(:disabled) { border-color: var(--gemboss-field-line-hover); background-color: var(--gemboss-field-bg-hover); }
813
820
  .gemboss-field:focus-visible,
814
- .gemboss-control:focus-visible { outline: 2px solid #005bd3; outline-offset: 1px; border-color: var(--gemboss-field-line-active); }
821
+ .gemboss-control:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 1px; border-color: var(--gemboss-field-line-active); }
815
822
  .gemboss-field:disabled,
816
823
  .gemboss-control:disabled { opacity: 0.5; cursor: not-allowed; }
817
824
  /* Polaris' own breakpoint for the same switch: under it a field reads 16px, so iOS does not zoom. */
@@ -840,7 +847,7 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
840
847
  rows, the two places a field is made of parts. A media query does not outrank anything; source
841
848
  order decided it, and the fix is to say it again after. (/code-review, 23/09/2026.) */
842
849
  @media (max-width: 47.9975em) { .gemboss-field--group { min-height: 44px; } }
843
- .gemboss-field--group:focus-within { outline: 2px solid #005bd3; outline-offset: 1px; border-color: var(--gemboss-field-line-active); }
850
+ .gemboss-field--group:focus-within { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 1px; border-color: var(--gemboss-field-line-active); }
844
851
  /* The colour swatch in a group: FLUSH to the group's left edge and full height — it covers the group's
845
852
  1px border top and bottom and its 12px left padding, so it is the whole 44px target the panel's rule
846
853
  asks for, rounded by the group's own corner (overflow: hidden), and never clipped. A 44px swatch
@@ -2002,6 +2009,17 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
2002
2009
  * these elements wins, wherever it sits in the cascade. Re-theme with the token, not a body rule.
2003
2010
  * The admin does not load this file, so its own rendering is untouched. */
2004
2011
  :where(html, body, button) { font-family: var(--gemboss-font-sans); }
2012
+ /* …and the rest of Polaris' body text (02/10/2026): bodyMd, 13px on a 20px line, weight 450, the text colour,
2013
+ * antialiased. With the face alone, every surface picked its own size — measured on the sign-in pages that day:
2014
+ * 14, 15 and 16px bodies, one in Lora serif — so every line outside a kit class read differently from the admin.
2015
+ * Same (0,0,0) as above: a surface that sets its own body text still wins, and should delete that rule.
2016
+ * BODY only, never `html`: the root size is what `rem` means, and gemwatcher alone has ~200 rem lengths that a 13px
2017
+ * root would shrink by a fifth (review #3665). Polaris does the same: html stays at the browser's 100%. */
2018
+ :where(body) {
2019
+ font-size: 13px; line-height: 20px; font-weight: 450; color: var(--gemboss-text);
2020
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
2021
+ }
2022
+ :where(h1, h2, h3) { letter-spacing: -0.01em; }
2005
2023
 
2006
2024
  .gemboss-topbar-start { display: flex; align-items: center; gap: 8px; min-width: 0; }
2007
2025
  .gemboss-topbar-center { display: flex; justify-content: center; min-width: 0; }
@@ -3162,6 +3180,42 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
3162
3180
  .gb-dock-side .gb-dock-sidegrip, .gb-dock-pinside { display: none; }
3163
3181
  }
3164
3182
 
3183
+ /* ── AssistantLauncher (02/10/2026): the floating button that opens the assistant, bottom-right ──────────────────────
3184
+ Top-right is the account corner (AccountMenu). Below the dock (290/320) and the topbar (300), above page content; the
3185
+ phone drawer (1000) covers it. 44px floor at every width; text + shortcut on wide screens, the icon alone on phones
3186
+ (the label stays the accessible name). Clears the iPhone home indicator with safe-area insets. */
3187
+ .gb-assistant-launcher {
3188
+ position: fixed;
3189
+ right: calc(20px + env(safe-area-inset-right, 0px));
3190
+ bottom: calc(20px + env(safe-area-inset-bottom, 0px));
3191
+ z-index: 280;
3192
+ display: inline-flex;
3193
+ align-items: center;
3194
+ gap: 8px;
3195
+ min-height: 44px;
3196
+ padding: 0 16px;
3197
+ border: 0;
3198
+ border-radius: 999px;
3199
+ background: var(--gemboss-primary, #1b1c1e);
3200
+ color: var(--gemboss-on-primary, #fff);
3201
+ font: 600 14px/1 var(--gemboss-font-sans, system-ui, sans-serif);
3202
+ box-shadow: var(--gemboss-shadow-raised, 0 6px 18px rgba(0, 0, 0, 0.18));
3203
+ cursor: pointer;
3204
+ }
3205
+ .gb-assistant-launcher:hover { background: var(--gemboss-primary-dark, #000); }
3206
+ .gb-assistant-launcher:focus-visible { outline: 2px solid var(--gemboss-accent, #5661f5); outline-offset: 2px; }
3207
+ .gb-assistant-launcher svg { flex: 0 0 auto; }
3208
+ .gb-assistant-launcher-kbd {
3209
+ font: inherit; font-size: 11px; opacity: 0.7;
3210
+ border: 1px solid currentColor; border-radius: 5px; padding: 1px 5px;
3211
+ }
3212
+ @media (max-width: 760px) {
3213
+ .gb-assistant-launcher { min-width: 52px; min-height: 52px; padding: 0; justify-content: center; }
3214
+ .gb-assistant-launcher-label, .gb-assistant-launcher-kbd {
3215
+ position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
3216
+ }
3217
+ }
3218
+
3165
3219
  /* ═══ @gemboss/ui src/kit-plain.css ═══ */
3166
3220
  /* kit-plain.css — kit primitives that only the NON-React surfaces use (GemFactory, gemcare, gempress write the
3167
3221
  markup by hand). They ship inside gemboss-ui.css (scripts/build-css.mjs `own`), but the admin does NOT import
@@ -3242,7 +3296,7 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
3242
3296
  .ui-link:hover,
3243
3297
  .ui-prose a:not([class]):hover { color: var(--gemboss-accent, #5661f5); text-decoration-thickness: 2px; }
3244
3298
  .ui-link:focus-visible,
3245
- .ui-prose a:not([class]):focus-visible { outline: 2px solid var(--ui-focus, #005bd3); outline-offset: 2px; border-radius: 2px; }
3299
+ .ui-prose a:not([class]):focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 2px; border-radius: 2px; }
3246
3300
 
3247
3301
  .ui-card--media { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
3248
3302
  .ui-card__media {
@@ -3251,7 +3305,7 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
3251
3305
  }
3252
3306
  .ui-card__media > img { display: block; width: 100%; height: 100%; object-fit: cover; }
3253
3307
  button.ui-card__media, a.ui-card__media { cursor: pointer; }
3254
- .ui-card__media:focus-visible { outline: 2px solid var(--ui-focus, #005bd3); outline-offset: -2px; }
3308
+ .ui-card__media:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: -2px; }
3255
3309
  /* justify-items: start: a tag or a button in the body keeps its own width instead of stretching across the card. */
3256
3310
  .ui-card__body { display: grid; justify-items: start; gap: var(--ui-space-2, 8px); padding: var(--ui-space-3, 12px) var(--ui-space-4, 16px); min-width: 0; }
3257
3311
  .ui-card__foot {
@@ -3261,8 +3315,8 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3261
3315
  }
3262
3316
 
3263
3317
  /* Popup body and foot: `<div class="ui-modal__body">…</div><div class="ui-modal__foot"><button class="ui-btn">…</button></div>`.
3264
- The admin's Modal.tsx pads its own children, so ui.css never styled these; every non-React surface padded them by
3265
- hand (GemFactory: 16/20 and a hairline above the actions). kit-plain.js's openModal writes exactly this shape. */
3318
+ ui.css never styled these, and the admin does not load this file: its Modal.tsx children carry their own padding
3319
+ (or none). Every non-React surface padded them by hand (GemFactory: 16/20 and a hairline above the actions). kit-plain.js's openModal writes exactly this shape. */
3266
3320
  .ui-modal__body { padding: 16px 20px; }
3267
3321
  .ui-modal__foot {
3268
3322
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
@@ -3380,6 +3434,165 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3380
3434
  thoại trình duyệt máy tính vẫn báo `fine`, nên nâng theo khổ như ui-btn--icon ở trên. */
3381
3435
  @media (pointer: coarse), (max-width: 47.9975em) { .ui-seg :is(button, a) { height: 44px; min-height: 44px; } }
3382
3436
 
3437
+ /* ── Polaris parity: the primitives every surface was drawing for itself (02/10/2026) ──────────────────────
3438
+ An audit of the six surfaces on the package (gemwatcher, GemFactory, gempress, gemcare, partner-radar,
3439
+ content-radar) found the same gaps re-filled six ways: no destructive button (four surfaces fell back to the
3440
+ browser's `confirm()`), one-tone badges, banners as bare colour blocks, hand-made radio / switch / tooltip /
3441
+ spinner, and help and error text under a field in five sizes. The admin has all of them from Polaris; this is
3442
+ the same shape in plain CSS, with Polaris' measurements and GemBoss' tokens (the danger / success / warning
3443
+ set the kit's tags and notices already use). The admin does not load this file. */
3444
+
3445
+ /* Destructive button: `ui-btn ui-btn--critical` (red text on the default button) and
3446
+ `ui-btn ui-btn--primary ui-btn--critical` (red fill), Polaris' `tone="critical"` in both variants. Red text is
3447
+ --gemboss-danger-strong: 7.3:1 on white, 5.7:1 on the hover ground (shell.css says why not --gemboss-danger). */
3448
+ .ui-btn--critical { color: var(--gemboss-danger-strong); }
3449
+ .ui-btn--critical:hover { background: var(--gemboss-danger-soft); color: var(--gemboss-danger-strong); }
3450
+ .ui-btn--primary.ui-btn--critical { background: var(--gemboss-danger); border-color: var(--gemboss-danger); color: var(--gemboss-on-primary); }
3451
+ .ui-btn--primary.ui-btn--critical:hover { background: var(--gemboss-danger-strong); border-color: var(--gemboss-danger-strong); color: var(--gemboss-on-primary); }
3452
+ .ui-btn--critical:focus-visible { outline-color: var(--gemboss-danger-strong); }
3453
+
3454
+ /* Under a thumb, and at phone WIDTH (a desktop browser at 320px reports pointer: fine), EVERY kit button is a 44px
3455
+ control, the compact one included: §9.1 has no density exception, and GemFactory, gempress and gemcare each
3456
+ restated this floor for `--sm` themselves. ui.css's coarse block leaves `--sm` alone for the admin's own
3457
+ settings page; this file is not loaded there. */
3458
+ @media (pointer: coarse), (max-width: 47.9975em) {
3459
+ .ui-btn, .ui-btn.ui-btn--sm { min-height: 44px; }
3460
+ .ui-select.ui-select { min-height: 44px; }
3461
+ /* A target is an AREA: the compact icon button (28 wide) and an icon segment (30) grow sideways as well. */
3462
+ .ui-btn--icon.ui-btn--sm, .ui-seg :is(button, a):not(.is-text) { min-width: 44px; }
3463
+ }
3464
+
3465
+ /* Badge tones: `<span class="ui-badge ui-badge--success">Active</span>`. Polaris' tones on GemBoss' soft grounds (Polaris' `attention` is `--warning` here: the fleet has one amber).
3466
+ `ui-badge--dot` puts a status dot before the label (filled = done, the tone's colour). */
3467
+ .ui-badge--success { background: var(--gemboss-success-soft); color: var(--gemboss-success-strong); }
3468
+ .ui-badge--warning { background: var(--gemboss-warning-soft); color: var(--gemboss-warning); }
3469
+ .ui-badge--critical { background: var(--gemboss-danger-soft); color: var(--gemboss-danger-strong); }
3470
+ .ui-badge--info { background: var(--gemboss-accent-soft); color: var(--gemboss-accent-strong); }
3471
+ .ui-badge--dot::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
3472
+
3473
+ /* Banner: Polaris' in-page Banner. A tone strip with an icon and a title, a body, optional actions, optional
3474
+ dismiss. `ui-notice` stays for a one-line note; a banner is for something the person must read or act on.
3475
+ <div class="ui-banner ui-banner--warning" role="status">
3476
+ <div class="ui-banner__head"><svg class="ui-banner__icon" …/><h3 class="ui-banner__title">Slack token expired</h3>
3477
+ <button type="button" class="ui-banner__close" aria-label="Dismiss">…</button></div>
3478
+ <div class="ui-banner__body"><p>Monday's digest will not send.</p>
3479
+ <div class="ui-banner__actions"><a class="ui-btn" href>Reconnect</a></div></div></div>
3480
+ No tone = neutral. role="status" / "alert" only when it appears after the page loaded. */
3481
+ .ui-banner {
3482
+ --ui-banner-bg: var(--ui-surface-2); --ui-banner-fg: var(--ui-text);
3483
+ border-radius: var(--ui-radius-lg); overflow: hidden; background: var(--ui-surface); color: var(--ui-text);
3484
+ box-shadow: 0 0 0 1px var(--ui-border), 0 3px 1px -1px rgba(26, 26, 26, 0.07);
3485
+ font-size: var(--ui-fs-body); line-height: 20px;
3486
+ }
3487
+ .ui-banner--success { --ui-banner-bg: var(--gemboss-success-soft); --ui-banner-fg: var(--gemboss-success-strong); }
3488
+ .ui-banner--warning { --ui-banner-bg: var(--gemboss-warning-soft); --ui-banner-fg: var(--gemboss-warning); }
3489
+ .ui-banner--critical { --ui-banner-bg: var(--gemboss-danger-soft); --ui-banner-fg: var(--gemboss-danger-strong); }
3490
+ .ui-banner--info { --ui-banner-bg: var(--gemboss-accent-soft); --ui-banner-fg: var(--gemboss-accent-strong); }
3491
+ .ui-banner__head { display: flex; align-items: center; gap: var(--ui-space-2); min-height: 32px; padding: 6px var(--ui-space-3); background: var(--ui-banner-bg); color: var(--ui-banner-fg); }
3492
+ .ui-banner__icon { flex: none; width: 20px; height: 20px; fill: currentColor; }
3493
+ .ui-banner__title { flex: 1 1 auto; min-width: 0; margin: 0; font-size: var(--ui-fs-body); font-weight: 650; line-height: 20px; }
3494
+ .ui-banner__close {
3495
+ flex: none; display: inline-grid; place-items: center; width: 32px; height: 32px; margin: -6px -8px -6px 0;
3496
+ border: 0; border-radius: var(--ui-radius-md); background: transparent; color: inherit; cursor: pointer;
3497
+ }
3498
+ .ui-banner__close:hover { background: color-mix(in srgb, currentColor 10%, transparent); }
3499
+ .ui-banner__close:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
3500
+ .ui-banner__close svg { width: 16px; height: 16px; }
3501
+ .ui-banner__body { padding: var(--ui-space-3) var(--ui-space-4); }
3502
+ .ui-banner__body > :first-child { margin-top: 0; }
3503
+ .ui-banner__body > :last-child { margin-bottom: 0; }
3504
+ .ui-banner__actions { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); margin-top: var(--ui-space-3); }
3505
+ @media (pointer: coarse), (max-width: 47.9975em) { .ui-banner__close { width: 44px; height: 44px; margin: -12px -12px -12px 0; } }
3506
+
3507
+ /* Help and error text under a field, Polaris' Labelled help text and InlineError:
3508
+ <label class="ui-label" for="x">Domain</label><input id="x" class="gemboss-field gemboss-field--tall"
3509
+ aria-describedby="x-help x-err" aria-invalid="true"><p class="ui-help" id="x-help">Without https://</p>
3510
+ <p class="ui-error" id="x-err">This store is already tracked.</p>
3511
+ aria-invalid paints the field in the critical tone; the message says what to do, not only what is wrong. */
3512
+ .ui-help { margin: var(--ui-space-1) 0 0; font-size: var(--ui-fs-body); line-height: 20px; color: var(--ui-text-muted); }
3513
+ .ui-error:empty { display: none; } /* a placeholder kept for aria-describedby draws no icon */
3514
+ .ui-error { display: flex; gap: 6px; margin: var(--ui-space-1) 0 0; font-size: var(--ui-fs-body); line-height: 20px; color: var(--gemboss-danger-strong); }
3515
+ .ui-error::before {
3516
+ content: ""; flex: none; width: 16px; height: 16px; margin-top: 2px; background: currentColor;
3517
+ -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/></svg>") center / contain no-repeat;
3518
+ mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/></svg>") center / contain no-repeat;
3519
+ }
3520
+ .gemboss-field[aria-invalid="true"], .gemboss-field--group:has([aria-invalid="true"]),
3521
+ .ui-select[aria-invalid="true"], .ui-textarea[aria-invalid="true"] {
3522
+ border-color: var(--gemboss-danger-strong); background-color: var(--gemboss-danger-soft);
3523
+ }
3524
+
3525
+ /* Radio: the checkbox's row with `type="radio"` — `<fieldset class="ui-choices"><legend class="ui-label">Plan</legend>
3526
+ <label class="ui-check"><input type="radio" name="plan" checked> Basic</label>…</fieldset>`. */
3527
+ .ui-choices { display: grid; gap: 0; margin: 0; padding: 0; border: 0; min-width: 0; }
3528
+ .ui-choices > legend { padding: 0; }
3529
+
3530
+ /* Switch: a checkbox that reads as on / off, for a setting that applies at once (no Save button).
3531
+ `<label class="ui-check ui-switch"><input type="checkbox" role="switch" checked> Send Monday digest</label>`.
3532
+ Track 32×20, thumb 16; the row stays the checkbox's 44px target. */
3533
+ .ui-switch > input[type="checkbox"] {
3534
+ appearance: none; -webkit-appearance: none; position: relative; width: 32px; height: 20px; margin: 0;
3535
+ border-radius: var(--gemboss-radius-full); background: var(--gemboss-field-line); /* 3.5:1 on white, WCAG 1.4.11 */ transition: background-color var(--gemboss-transition-fast) ease;
3536
+ }
3537
+ .ui-switch > input[type="checkbox"]::before {
3538
+ content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
3539
+ background: var(--gemboss-surface); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); transition: transform var(--gemboss-transition-fast) ease;
3540
+ }
3541
+ .ui-switch > input[type="checkbox"]:checked { background: var(--ui-accent); }
3542
+ .ui-switch > input[type="checkbox"]:checked::before { transform: translateX(12px); }
3543
+ @media (prefers-reduced-motion: reduce) { .ui-switch > input[type="checkbox"], .ui-switch > input[type="checkbox"]::before { transition: none; } }
3544
+
3545
+ /* Tooltip: a SHORT label for an icon-only control, shown on hover and keyboard focus. Not for anything the person
3546
+ needs to act on (a touch screen never hovers).
3547
+ <span class="ui-tooltip"><button class="ui-btn ui-btn--icon" aria-describedby="t1" aria-label="Archive">…</button>
3548
+ <span class="ui-tooltip__bubble" role="tooltip" id="t1">Archive</span></span> `--end` aligns it right. */
3549
+ .ui-tooltip { position: relative; display: inline-flex; }
3550
+ .ui-tooltip__bubble {
3551
+ position: absolute; z-index: 30; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
3552
+ width: max-content; max-width: 240px; padding: 4px 8px; border-radius: var(--ui-radius-md);
3553
+ background: var(--ui-surface); color: var(--ui-text); font-size: var(--ui-fs-body); line-height: 20px; font-weight: 450;
3554
+ box-shadow: 0 8px 16px -4px rgba(26, 26, 26, 0.22), 0 0 0 1px var(--ui-border);
3555
+ opacity: 0; visibility: hidden; pointer-events: none; transition: opacity var(--gemboss-transition-fast) ease;
3556
+ }
3557
+ .ui-tooltip--end .ui-tooltip__bubble { left: auto; right: 0; transform: none; }
3558
+ .ui-tooltip:hover > .ui-tooltip__bubble, .ui-tooltip:focus-within > .ui-tooltip__bubble { opacity: 1; visibility: visible; }
3559
+
3560
+ /* Spinner: `<span class="ui-spinner" role="status"><span class="ui-sr-only">Loading…</span></span>`, 20px
3561
+ (Polaris small); `--lg` is 44px. Reduced motion turns it slower, never off: it is the only sign of waiting. */
3562
+ .ui-spinner {
3563
+ display: inline-block; flex: none; width: 20px; height: 20px; box-sizing: border-box; border-radius: 50%;
3564
+ border: 2px solid color-mix(in srgb, var(--ui-text) 18%, transparent); border-top-color: var(--ui-text);
3565
+ animation: ui-spin 0.7s linear infinite; vertical-align: middle;
3566
+ }
3567
+ .ui-spinner--lg { width: 44px; height: 44px; border-width: 4px; }
3568
+ @keyframes ui-spin { to { transform: rotate(360deg); } }
3569
+ @media (prefers-reduced-motion: reduce) { .ui-spinner { animation-duration: 2s; } }
3570
+
3571
+ /* Progress: `<progress class="ui-progress" value="37" max="100" aria-label="Import">37%</progress>`, 8px, the
3572
+ primary fill (Polaris' small ProgressBar). Tones: `--success`, `--critical`. */
3573
+ .ui-progress { appearance: none; -webkit-appearance: none; display: block; width: 100%; height: 8px; border: 0; border-radius: var(--gemboss-radius-full); overflow: hidden; background: var(--gemboss-divider); color: var(--ui-accent); }
3574
+ .ui-progress::-webkit-progress-bar { background: var(--gemboss-divider); border-radius: inherit; }
3575
+ .ui-progress::-webkit-progress-value { background: currentColor; border-radius: inherit; }
3576
+ .ui-progress::-moz-progress-bar { background: currentColor; border-radius: inherit; }
3577
+ .ui-progress--success { color: var(--gemboss-success); }
3578
+ .ui-progress--critical { color: var(--gemboss-danger); }
3579
+
3580
+ /* Wider popup: `openModal({ className: "ui-modal--wide" })`, Polaris' default Modal (620px). The kit's own 460 is
3581
+ Polaris' small step plus one; a form with two columns or a table needs this one. */
3582
+ .ui-modal--wide { width: min(620px, 100%); }
3583
+
3584
+ /* Native popup: `<dialog class="ui-modal">` opened with `showModal()`, for React (and other) surfaces that do not load
3585
+ kit-plain.js. The browser does what openModal does by hand: Escape closes it, the page behind is inert, it sits in
3586
+ the top layer above the shell's topbar. Same head / body / foot markup; `::backdrop` is the kit's overlay. gemcare
3587
+ and content-radar already used `<dialog>`, each with its own width, padding and backdrop (02/10/2026: four shapes,
3588
+ 420 to 760 wide). Close on a backdrop click yourself: a click whose target IS the dialog landed outside the panel.
3589
+ `<dialog class="ui-modal" aria-labelledby="t"><div class="ui-modal__head"><div class="ui-modal__title" id="t">…</div>
3590
+ <button type="button" class="ui-modal__close" aria-label="Close">…</button></div><div class="ui-modal__body">…</div>
3591
+ <div class="ui-modal__foot">…</div></dialog>` */
3592
+ dialog.ui-modal { padding: 0; max-width: calc(100% - 40px); max-height: calc(100dvh - 40px); color: var(--ui-text); }
3593
+ dialog.ui-modal[open] { display: flex; flex-direction: column; }
3594
+ dialog.ui-modal::backdrop { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px); }
3595
+
3383
3596
  /* ═══ @gemboss/ui src/chat.css ═══ */
3384
3597
  /*
3385
3598
  * The chat BODY — what an answer looks like, wherever a GemBoss surface puts a chat (01/10/2026).
@@ -3491,9 +3704,14 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3491
3704
  border: 1px solid transparent;
3492
3705
  padding: 6px 8px; font-size: var(--gemboss-fs-body); font-family: inherit;
3493
3706
  /* Starts at one line; GemChat's auto-grow JS expands it up to ~3 lines (max-height),
3494
- then scrolls inside (ChatGPT/Gemini-style). overflow-y is hidden until the JS flips
3495
- it to auto past the cap, so a 1–2 line message never shows a (rounding) scrollbar. */
3496
- min-height: 0; max-height: 84px; line-height: 1.5; overflow-y: hidden;
3707
+ then scrolls inside (ChatGPT/Gemini-style), and writes overflow-y itself (hidden until
3708
+ the cap, auto past it). The DEFAULT here is auto, for every surface that has no such
3709
+ JS: it was hidden, and there a message taller than the box could not be scrolled at
3710
+ all — wheel and touch do nothing under overflow:hidden. Measured 02/10/2026 in Chromium:
3711
+ wheel scrolled 0/9 overflowing boxes with hidden, 9/9 with auto; a box whose text
3712
+ fits showed no overflow at 13.5 / 14 / 15px (no rounding scrollbar). Found on
3713
+ partner-radar's assistant: 264px of text in a 75px box, unreadable. */
3714
+ min-height: 0; max-height: 84px; line-height: 1.5; overflow-y: auto;
3497
3715
  scrollbar-width: thin; scrollbar-color: rgba(31,30,29,0.22) transparent; /* thin, not the OS bar */
3498
3716
  }
3499
3717
  .gemboss-ec-input textarea::-webkit-scrollbar { width: 7px; }
@@ -3733,7 +3951,7 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3733
3951
  * chat can sit in a 380px sidebar — and capped in height so five thumbnails read as a shortlist, not
3734
3952
  * five screens. The link around it is the tap target: keyboard focus has to be visible on it. */
3735
3953
  .gemboss-md-imglink { display: inline-block; max-width: 100%; margin: 4px 0 8px; border-radius: 8px; line-height: 0; }
3736
- .gemboss-md-imglink:focus-visible { outline: 2px solid var(--gemboss-focus, #005bd3); outline-offset: 2px; }
3954
+ .gemboss-md-imglink:focus-visible { outline: 2px solid var(--gemboss-focus, var(--ui-focus, #5661f5)); outline-offset: 2px; }
3737
3955
  .gemboss-md-img {
3738
3956
  display: block;
3739
3957
  max-width: 100%;
@@ -3919,4 +4137,4 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3919
4137
  .gemboss-ec-stop svg { width: 12px; height: 12px; fill: currentColor; }
3920
4138
  .gemboss-ec-stop:disabled { opacity: 0.6; cursor: default; }
3921
4139
  .gemboss-ec-stop:hover { filter: brightness(1.08); }
3922
- .gemboss-ec-stop:focus-visible { outline: 2px solid var(--gemboss-focus, #005bd3); outline-offset: 2px; }
4140
+ .gemboss-ec-stop:focus-visible { outline: 2px solid var(--gemboss-focus, var(--ui-focus, #5661f5)); outline-offset: 2px; }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@gemboss/ui",
3
- "version": "0.4.1",
3
+ "version": "0.5.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",
7
7
  "sideEffects": [
8
- "*.css"
8
+ "*.css",
9
+ "./src/kit-plain.js"
9
10
  ],
10
11
  "main": "./src/index.ts",
11
12
  "types": "./src/index.ts",
@@ -24,13 +25,15 @@
24
25
  "./kit-plain.js": "./src/kit-plain.js",
25
26
  "./assistant": "./src/assistant/index.ts",
26
27
  "./assistant/dock": "./src/assistant/dock.ts",
27
- "./assistant.css": "./src/assistant.css"
28
+ "./assistant.css": "./src/assistant.css",
29
+ "./icon.svg": "./src/icon.svg"
28
30
  },
29
31
  "files": [
30
32
  "src/**/*.ts",
31
33
  "src/**/*.tsx",
32
34
  "src/*.css",
33
35
  "src/kit-plain.js",
36
+ "src/icon.svg",
34
37
  "!src/**/*.test.*",
35
38
  "gemboss-ui.css",
36
39
  "catalog/index.html",
package/src/app-shell.css CHANGED
@@ -13,6 +13,17 @@
13
13
  * these elements wins, wherever it sits in the cascade. Re-theme with the token, not a body rule.
14
14
  * The admin does not load this file, so its own rendering is untouched. */
15
15
  :where(html, body, button) { font-family: var(--gemboss-font-sans); }
16
+ /* …and the rest of Polaris' body text (02/10/2026): bodyMd, 13px on a 20px line, weight 450, the text colour,
17
+ * antialiased. With the face alone, every surface picked its own size — measured on the sign-in pages that day:
18
+ * 14, 15 and 16px bodies, one in Lora serif — so every line outside a kit class read differently from the admin.
19
+ * Same (0,0,0) as above: a surface that sets its own body text still wins, and should delete that rule.
20
+ * BODY only, never `html`: the root size is what `rem` means, and gemwatcher alone has ~200 rem lengths that a 13px
21
+ * root would shrink by a fifth (review #3665). Polaris does the same: html stays at the browser's 100%. */
22
+ :where(body) {
23
+ font-size: 13px; line-height: 20px; font-weight: 450; color: var(--gemboss-text);
24
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
25
+ }
26
+ :where(h1, h2, h3) { letter-spacing: -0.01em; }
16
27
 
17
28
  .gemboss-topbar-start { display: flex; align-items: center; gap: 8px; min-width: 0; }
18
29
  .gemboss-topbar-center { display: flex; justify-content: center; min-width: 0; }
package/src/assistant.css CHANGED
@@ -494,3 +494,39 @@
494
494
  .gb-dock-side { left: 240px; width: auto; }
495
495
  .gb-dock-side .gb-dock-sidegrip, .gb-dock-pinside { display: none; }
496
496
  }
497
+
498
+ /* ── AssistantLauncher (02/10/2026): the floating button that opens the assistant, bottom-right ──────────────────────
499
+ Top-right is the account corner (AccountMenu). Below the dock (290/320) and the topbar (300), above page content; the
500
+ phone drawer (1000) covers it. 44px floor at every width; text + shortcut on wide screens, the icon alone on phones
501
+ (the label stays the accessible name). Clears the iPhone home indicator with safe-area insets. */
502
+ .gb-assistant-launcher {
503
+ position: fixed;
504
+ right: calc(20px + env(safe-area-inset-right, 0px));
505
+ bottom: calc(20px + env(safe-area-inset-bottom, 0px));
506
+ z-index: 280;
507
+ display: inline-flex;
508
+ align-items: center;
509
+ gap: 8px;
510
+ min-height: 44px;
511
+ padding: 0 16px;
512
+ border: 0;
513
+ border-radius: 999px;
514
+ background: var(--gemboss-primary, #1b1c1e);
515
+ color: var(--gemboss-on-primary, #fff);
516
+ font: 600 14px/1 var(--gemboss-font-sans, system-ui, sans-serif);
517
+ box-shadow: var(--gemboss-shadow-raised, 0 6px 18px rgba(0, 0, 0, 0.18));
518
+ cursor: pointer;
519
+ }
520
+ .gb-assistant-launcher:hover { background: var(--gemboss-primary-dark, #000); }
521
+ .gb-assistant-launcher:focus-visible { outline: 2px solid var(--gemboss-accent, #5661f5); outline-offset: 2px; }
522
+ .gb-assistant-launcher svg { flex: 0 0 auto; }
523
+ .gb-assistant-launcher-kbd {
524
+ font: inherit; font-size: 11px; opacity: 0.7;
525
+ border: 1px solid currentColor; border-radius: 5px; padding: 1px 5px;
526
+ }
527
+ @media (max-width: 760px) {
528
+ .gb-assistant-launcher { min-width: 52px; min-height: 52px; padding: 0; justify-content: center; }
529
+ .gb-assistant-launcher-label, .gb-assistant-launcher-kbd {
530
+ position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
531
+ }
532
+ }
@@ -107,4 +107,4 @@
107
107
  .gemboss-ec-stop svg { width: 12px; height: 12px; fill: currentColor; }
108
108
  .gemboss-ec-stop:disabled { opacity: 0.6; cursor: default; }
109
109
  .gemboss-ec-stop:hover { filter: brightness(1.08); }
110
- .gemboss-ec-stop:focus-visible { outline: 2px solid var(--gemboss-focus, #005bd3); outline-offset: 2px; }
110
+ .gemboss-ec-stop:focus-visible { outline: 2px solid var(--gemboss-focus, var(--ui-focus, #5661f5)); outline-offset: 2px; }
package/src/chat.css CHANGED
@@ -108,9 +108,14 @@
108
108
  border: 1px solid transparent;
109
109
  padding: 6px 8px; font-size: var(--gemboss-fs-body); font-family: inherit;
110
110
  /* Starts at one line; GemChat's auto-grow JS expands it up to ~3 lines (max-height),
111
- then scrolls inside (ChatGPT/Gemini-style). overflow-y is hidden until the JS flips
112
- it to auto past the cap, so a 1–2 line message never shows a (rounding) scrollbar. */
113
- min-height: 0; max-height: 84px; line-height: 1.5; overflow-y: hidden;
111
+ then scrolls inside (ChatGPT/Gemini-style), and writes overflow-y itself (hidden until
112
+ the cap, auto past it). The DEFAULT here is auto, for every surface that has no such
113
+ JS: it was hidden, and there a message taller than the box could not be scrolled at
114
+ all — wheel and touch do nothing under overflow:hidden. Measured 02/10/2026 in Chromium:
115
+ wheel scrolled 0/9 overflowing boxes with hidden, 9/9 with auto; a box whose text
116
+ fits showed no overflow at 13.5 / 14 / 15px (no rounding scrollbar). Found on
117
+ partner-radar's assistant: 264px of text in a 75px box, unreadable. */
118
+ min-height: 0; max-height: 84px; line-height: 1.5; overflow-y: auto;
114
119
  scrollbar-width: thin; scrollbar-color: rgba(31,30,29,0.22) transparent; /* thin, not the OS bar */
115
120
  }
116
121
  .gemboss-ec-input textarea::-webkit-scrollbar { width: 7px; }
@@ -350,7 +355,7 @@
350
355
  * chat can sit in a 380px sidebar — and capped in height so five thumbnails read as a shortlist, not
351
356
  * five screens. The link around it is the tap target: keyboard focus has to be visible on it. */
352
357
  .gemboss-md-imglink { display: inline-block; max-width: 100%; margin: 4px 0 8px; border-radius: 8px; line-height: 0; }
353
- .gemboss-md-imglink:focus-visible { outline: 2px solid var(--gemboss-focus, #005bd3); outline-offset: 2px; }
358
+ .gemboss-md-imglink:focus-visible { outline: 2px solid var(--gemboss-focus, var(--ui-focus, #5661f5)); outline-offset: 2px; }
354
359
  .gemboss-md-img {
355
360
  display: block;
356
361
  max-width: 100%;
@@ -50,7 +50,7 @@ input[type="search"]:is(.gemboss-field, .gemboss-field__bare) { -webkit-appearan
50
50
  .gemboss-field:hover:not(:disabled),
51
51
  .gemboss-control:hover:not(:disabled) { border-color: var(--p-color-input-border-hover, var(--gemboss-field-line-hover)); background-color: var(--p-color-input-bg-surface-hover, var(--gemboss-field-bg-hover)); }
52
52
  .gemboss-field:focus-visible,
53
- .gemboss-control:focus-visible { outline: 2px solid var(--p-color-border-focus, #005bd3); outline-offset: 1px; border-color: var(--p-color-input-border-active, var(--gemboss-field-line-active)); }
53
+ .gemboss-control:focus-visible { outline: 2px solid var(--p-color-border-focus, var(--ui-focus, #5661f5)); outline-offset: 1px; border-color: var(--p-color-input-border-active, var(--gemboss-field-line-active)); }
54
54
  .gemboss-field:disabled,
55
55
  .gemboss-control:disabled { opacity: 0.5; cursor: not-allowed; }
56
56
  /* Polaris' own breakpoint for the same switch: under it a field reads 16px, so iOS does not zoom. */
@@ -79,7 +79,7 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
79
79
  rows, the two places a field is made of parts. A media query does not outrank anything; source
80
80
  order decided it, and the fix is to say it again after. (/code-review, 23/09/2026.) */
81
81
  @media (max-width: 47.9975em) { .gemboss-field--group { min-height: 44px; } }
82
- .gemboss-field--group:focus-within { outline: 2px solid var(--p-color-border-focus, #005bd3); outline-offset: 1px; border-color: var(--p-color-input-border-active, var(--gemboss-field-line-active)); }
82
+ .gemboss-field--group:focus-within { outline: 2px solid var(--p-color-border-focus, var(--ui-focus, #5661f5)); outline-offset: 1px; border-color: var(--p-color-input-border-active, var(--gemboss-field-line-active)); }
83
83
  /* The colour swatch in a group: FLUSH to the group's left edge and full height — it covers the group's
84
84
  1px border top and bottom and its 12px left padding, so it is the whole 44px target the panel's rule
85
85
  asks for, rounded by the group's own corner (overflow: hidden), and never clipped. A 44px swatch