@gemboss/ui 0.4.2 → 0.6.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/README.md +102 -17
- package/catalog/index.html +74 -9
- package/gemboss-ui.css +335 -60
- package/package.json +6 -3
- package/src/app-shell.css +22 -1
- package/src/assistant.css +44 -21
- package/src/auth.tsx +3 -0
- package/src/chat-composer.css +1 -1
- package/src/chat.css +1 -1
- package/src/gemboss-field.css +3 -3
- package/src/gemboss-tokens.css +1 -1
- package/src/icon.svg +1 -0
- package/src/kit-plain.css +223 -4
- package/src/kit-plain.js +115 -7
- package/src/launcher.tsx +16 -7
- package/src/shell.css +1 -0
- package/src/shell.tsx +3 -1
- package/src/ui.css +33 -25
package/gemboss-ui.css
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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: 47291357064c6877d0ebe7d118b6a1b52b1d9d22
|
|
5
|
+
* sources sha256: 22617cdf59a7330f1285221022e5341d94e5b5e74cf917ce6f863c85733505e1
|
|
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)
|
|
9
9
|
* Polaris --p-* references are resolved to their fallbacks; declarations with no fallback are dropped
|
|
10
10
|
* (0 dropped).
|
|
11
11
|
*/
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
327
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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; }
|
|
@@ -390,8 +396,8 @@
|
|
|
390
396
|
`.gemboss-admin-page-head` (01/10/2026, Chris: một chủ sở hữu; admin dùng bản này). Cố ý không đặt margin cho h1
|
|
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
|
-
.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:
|
|
399
|
+
.ui-page-head h1 { color: var(--ui-text-strong); font-size: 22px; line-height: 28px; font-weight: 650; letter-spacing: -0.01em; } /* own line-height: the 20px body line overlapped a wrapped title (0.5.1) */
|
|
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(--
|
|
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,19 +440,20 @@ 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(--
|
|
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
|
|
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
|
-
|
|
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:
|
|
447
|
-
padding:
|
|
448
|
-
|
|
449
|
-
}
|
|
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: color-mix(in srgb, var(--ui-text) 8%, transparent); color: var(--ui-text-muted);
|
|
456
|
+
} /* a tint of the ink, not --ui-surface-2: that is the table row hover, and a badge vanished on hover (0.5.1) */
|
|
450
457
|
|
|
451
458
|
/* ── Modal ────────────────────────────────────────────────────────────────────
|
|
452
459
|
Rendered INLINE (not portaled): a portal escapes the .gemboss-admin-shell token
|
|
@@ -476,7 +483,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
476
483
|
}
|
|
477
484
|
.ui-modal__close svg { width: 20px; height: 20px; }
|
|
478
485
|
.ui-modal__close:hover { background: var(--ui-surface-2); color: var(--ui-text); }
|
|
479
|
-
.ui-modal__close:focus-visible { outline: 2px solid var(--ui-
|
|
486
|
+
.ui-modal__close:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
|
|
480
487
|
|
|
481
488
|
/* `size="lg"` — the screen-shaped dialog. Only the HOOK lives here: this sheet is imported by the
|
|
482
489
|
root layout, so every rule in it is paid on every screen, and the geometry of a dialog with one
|
|
@@ -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
|
|
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,12 +525,13 @@ 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:
|
|
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; }
|
|
525
532
|
.ui-seg :is(button, a) + :is(button, a) { border-left: 1px solid var(--ui-border); }
|
|
526
533
|
.ui-seg :is(button, a):hover { background: var(--ui-surface-2); color: var(--ui-text); }
|
|
534
|
+
.ui-seg :is(button, a):focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
|
|
527
535
|
/* Lọc / chế độ xem bằng LINK (trang server-render, chọn = đổi URL): `<a aria-current="page">` là ô đang chọn. */
|
|
528
536
|
.ui-seg a { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; color: var(--ui-text-muted); }
|
|
529
537
|
.ui-seg a[aria-current="page"],
|
|
@@ -702,7 +710,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
702
710
|
position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
|
|
703
711
|
min-height: 44px; padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
|
|
704
712
|
border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;
|
|
705
|
-
font-family: inherit; font-size: var(--
|
|
713
|
+
font-family: inherit; font-size: var(--ui-fs-body); font-weight: 500; white-space: nowrap;
|
|
706
714
|
color: var(--ui-text-muted);
|
|
707
715
|
transition: color var(--gemboss-transition-fast) ease, background-color var(--gemboss-transition-fast) ease;
|
|
708
716
|
}
|
|
@@ -728,7 +736,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
728
736
|
overflow-x: auto; max-width: 100%;
|
|
729
737
|
background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
|
|
730
738
|
}
|
|
731
|
-
.ui-table { width: 100%; border-collapse: collapse; font-size: var(--
|
|
739
|
+
.ui-table { width: 100%; border-collapse: collapse; font-size: var(--ui-fs-body); color: var(--ui-text); }
|
|
732
740
|
.ui-table th,
|
|
733
741
|
.ui-table td {
|
|
734
742
|
padding: var(--ui-space-2) var(--ui-space-3); text-align: left; vertical-align: middle;
|
|
@@ -752,7 +760,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
752
760
|
.ui-empty {
|
|
753
761
|
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--ui-space-2);
|
|
754
762
|
padding: var(--ui-space-4); padding-block: 40px; text-align: center;
|
|
755
|
-
color: var(--ui-text-muted); font-size: var(--
|
|
763
|
+
color: var(--ui-text-muted); font-size: var(--ui-fs-body);
|
|
756
764
|
}
|
|
757
765
|
.ui-empty__title { margin: 0; font-size: var(--gemboss-fs-h3); font-weight: 600; color: var(--ui-text-strong); }
|
|
758
766
|
.ui-empty__body { margin: 0; max-width: 48ch; line-height: 1.5; }
|
|
@@ -811,7 +819,7 @@ input[type="search"]:is(.gemboss-field, .gemboss-field__bare) { -webkit-appearan
|
|
|
811
819
|
.gemboss-field:hover:not(:disabled),
|
|
812
820
|
.gemboss-control:hover:not(:disabled) { border-color: var(--gemboss-field-line-hover); background-color: var(--gemboss-field-bg-hover); }
|
|
813
821
|
.gemboss-field:focus-visible,
|
|
814
|
-
.gemboss-control:focus-visible { outline: 2px solid #
|
|
822
|
+
.gemboss-control:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 1px; border-color: var(--gemboss-field-line-active); }
|
|
815
823
|
.gemboss-field:disabled,
|
|
816
824
|
.gemboss-control:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
817
825
|
/* Polaris' own breakpoint for the same switch: under it a field reads 16px, so iOS does not zoom. */
|
|
@@ -840,7 +848,7 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
|
|
|
840
848
|
rows, the two places a field is made of parts. A media query does not outrank anything; source
|
|
841
849
|
order decided it, and the fix is to say it again after. (/code-review, 23/09/2026.) */
|
|
842
850
|
@media (max-width: 47.9975em) { .gemboss-field--group { min-height: 44px; } }
|
|
843
|
-
.gemboss-field--group:focus-within { outline: 2px solid #
|
|
851
|
+
.gemboss-field--group:focus-within { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 1px; border-color: var(--gemboss-field-line-active); }
|
|
844
852
|
/* The colour swatch in a group: FLUSH to the group's left edge and full height — it covers the group's
|
|
845
853
|
1px border top and bottom and its 12px left padding, so it is the whole 44px target the panel's rule
|
|
846
854
|
asks for, rounded by the group's own corner (overflow: hidden), and never clipped. A 44px swatch
|
|
@@ -849,7 +857,7 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
|
|
|
849
857
|
/* ↑ the right edge is what keeps a WHITE swatch visible: without it the block melted into the field (measured). */
|
|
850
858
|
.gemboss-field--group input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
|
|
851
859
|
.gemboss-field--group input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 0; }
|
|
852
|
-
.gemboss-field__bare { flex: 1; min-width: 0; align-self: stretch; padding: 0 12px 0 0; border: 0; border-radius: 0; outline: none; background: transparent; color: inherit; font: inherit; }
|
|
860
|
+
.gemboss-field__bare { flex: 1; min-width: 0; align-self: stretch; margin-block: -1px; /* over the group's 1px border: the input is the 44px target, not 42 (0.5.1) */ padding: 0 12px 0 0; border: 0; border-radius: 0; outline: none; background: transparent; color: inherit; font: inherit; }
|
|
853
861
|
/* …and the placeholder, which "draws nothing" does not cover: the surfaces that used to declare
|
|
854
862
|
their own (the /admin search rows, the settings and palette rows) stopped when they stopped
|
|
855
863
|
owning the box, and without this every bare field would fall back to the browser's grey. */
|
|
@@ -1188,6 +1196,10 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
|
|
|
1188
1196
|
.gnav-item:hover {
|
|
1189
1197
|
background: var(--gemboss-nav-hover);
|
|
1190
1198
|
}
|
|
1199
|
+
.gnav-item:focus-visible {
|
|
1200
|
+
outline: 2px solid var(--gemboss-accent);
|
|
1201
|
+
outline-offset: -2px;
|
|
1202
|
+
}
|
|
1191
1203
|
.gnav-item.active {
|
|
1192
1204
|
color: var(--gemboss-active-fg);
|
|
1193
1205
|
font-weight: 600;
|
|
@@ -2002,6 +2014,19 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
2002
2014
|
* these elements wins, wherever it sits in the cascade. Re-theme with the token, not a body rule.
|
|
2003
2015
|
* The admin does not load this file, so its own rendering is untouched. */
|
|
2004
2016
|
:where(html, body, button) { font-family: var(--gemboss-font-sans); }
|
|
2017
|
+
/* …and the rest of Polaris' body text (02/10/2026): bodyMd, 13px on a 20px line, weight 450, the text colour,
|
|
2018
|
+
* antialiased. With the face alone, every surface picked its own size — measured on the sign-in pages that day:
|
|
2019
|
+
* 14, 15 and 16px bodies, one in Lora serif — so every line outside a kit class read differently from the admin.
|
|
2020
|
+
* Same (0,0,0) as above: a surface that sets its own body text still wins, and should delete that rule.
|
|
2021
|
+
* BODY only, never `html`: the root size is what `rem` means, and gemwatcher alone has ~200 rem lengths that a 13px
|
|
2022
|
+
* root would shrink by a fifth (review #3665). Polaris does the same: html stays at the browser's 100%. */
|
|
2023
|
+
:where(body) {
|
|
2024
|
+
font-size: 13px; line-height: 20px; font-weight: 450; color: var(--gemboss-text);
|
|
2025
|
+
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
2026
|
+
}
|
|
2027
|
+
/* Headings get their own line (Polaris heading line-heights are ~1.25-1.4): inheriting the body's 20px made a wrapped 22px
|
|
2028
|
+
* title overlap itself (gempress, 0.5.1). */
|
|
2029
|
+
:where(h1, h2, h3) { letter-spacing: -0.01em; line-height: 1.3; }
|
|
2005
2030
|
|
|
2006
2031
|
.gemboss-topbar-start { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
2007
2032
|
.gemboss-topbar-center { display: flex; justify-content: center; min-width: 0; }
|
|
@@ -2011,6 +2036,10 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
2011
2036
|
.gemboss-topbar-center { flex: 1 1 auto; justify-content: flex-start; }
|
|
2012
2037
|
/* No actions: the empty slot would still take one flex `gap` (8px) off the search at 320px. */
|
|
2013
2038
|
.gemboss-topbar-end:empty { display: none; }
|
|
2039
|
+
/* Only the search gives. With `min-width: 0` on all three slots, flex shrank them all: the actions slot gave way and its
|
|
2040
|
+
* buttons (justify flex-end) spilled LEFT over the search (two 44px targets, assistant + account, overlapped
|
|
2041
|
+
* partner-radar's search by 15px at 320 and 360), and the start slot squeezed the gem mark to 16px. */
|
|
2042
|
+
.gemboss-topbar-start, .gemboss-topbar-end { flex: 0 0 auto; }
|
|
2014
2043
|
}
|
|
2015
2044
|
|
|
2016
2045
|
.gnav-heading {
|
|
@@ -2051,7 +2080,11 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
2051
2080
|
overflow: hidden; text-overflow: ellipsis; min-width: 0;
|
|
2052
2081
|
}
|
|
2053
2082
|
/* The admin shows only its mark below 460px; so does this. */
|
|
2054
|
-
@media (max-width: 460px) {
|
|
2083
|
+
@media (max-width: 460px) {
|
|
2084
|
+
.gb-brand-name { display: none; }
|
|
2085
|
+
/* The mark alone is ~21px wide: a 44px target, centred (visual-gate tap_target_link on content-radar at 320, 0.5.1). */
|
|
2086
|
+
.gb-brand { min-width: 44px; height: 44px; justify-content: center; }
|
|
2087
|
+
}
|
|
2055
2088
|
|
|
2056
2089
|
/* ── AccountMenu (./account.tsx): its rules live in shell.css since 29/09/2026, because the admin
|
|
2057
2090
|
* renders AccountMenu too now and loads shell.css, not this file. */
|
|
@@ -3162,40 +3195,63 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
3162
3195
|
.gb-dock-side .gb-dock-sidegrip, .gb-dock-pinside { display: none; }
|
|
3163
3196
|
}
|
|
3164
3197
|
|
|
3165
|
-
/* ── AssistantLauncher
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3198
|
+
/* ── AssistantLauncher: the button that opens the assistant. NOT floating (03/10/2026) ────────────────────────────────
|
|
3199
|
+
0.4.2 floated it bottom-right (z 280). visual-gate on partner-radar prod, 03/10: `content_covered` red at 5 of 6 widths
|
|
3200
|
+
on every page measured (/, /apps, /sources): a fixed button always sits on whatever text scrolls under it, and at
|
|
3201
|
+
scroll 0 that is a table column. GemCommunity had dropped its floating buttons for the same reason (#239). Chris chose
|
|
3202
|
+
"bỏ nút nổi". It now lives where nothing scrolls under it:
|
|
3203
|
+
· `--rail` wide screens: the last row of the rail (`railBottom`), a full-width primary button. Hidden ≤760px,
|
|
3204
|
+
where the rail is an off-canvas drawer nobody sees until they open it.
|
|
3205
|
+
· `--topbar` phones (≤760px): a 44px icon button in the topbar's `actions`, left of the account chip. Hidden above.
|
|
3206
|
+
The top-right corner stays the ACCOUNT. A surface renders both; exactly one is displayed at any width. */
|
|
3169
3207
|
.gb-assistant-launcher {
|
|
3170
|
-
position: fixed;
|
|
3171
|
-
right: calc(20px + env(safe-area-inset-right, 0px));
|
|
3172
|
-
bottom: calc(20px + env(safe-area-inset-bottom, 0px));
|
|
3173
|
-
z-index: 280;
|
|
3174
3208
|
display: inline-flex;
|
|
3175
3209
|
align-items: center;
|
|
3176
3210
|
gap: 8px;
|
|
3177
|
-
min-height: 44px;
|
|
3178
|
-
padding: 0 16px;
|
|
3179
3211
|
border: 0;
|
|
3180
|
-
border-radius: 999px;
|
|
3181
|
-
background: var(--gemboss-primary, #1b1c1e);
|
|
3182
|
-
color: var(--gemboss-on-primary, #fff);
|
|
3183
|
-
font: 600 14px/1 var(--gemboss-font-sans, system-ui, sans-serif);
|
|
3184
|
-
box-shadow: var(--gemboss-shadow-raised, 0 6px 18px rgba(0, 0, 0, 0.18));
|
|
3185
3212
|
cursor: pointer;
|
|
3213
|
+
font-family: var(--gemboss-font-sans, system-ui, sans-serif);
|
|
3186
3214
|
}
|
|
3187
|
-
.gb-assistant-launcher:hover { background: var(--gemboss-primary-dark, #000); }
|
|
3188
|
-
.gb-assistant-launcher:focus-visible { outline: 2px solid var(--gemboss-accent, #5661f5); outline-offset: 2px; }
|
|
3189
3215
|
.gb-assistant-launcher svg { flex: 0 0 auto; }
|
|
3216
|
+
.gb-assistant-launcher:focus-visible { outline: 2px solid var(--gemboss-accent, #5661f5); outline-offset: 2px; }
|
|
3217
|
+
.gb-assistant-launcher--rail {
|
|
3218
|
+
width: 100%;
|
|
3219
|
+
min-height: 44px; /* a control, not a list link: §9.1 asks 44 (visual-gate red at 768 with 36) */
|
|
3220
|
+
margin: 8px 0 4px;
|
|
3221
|
+
padding: 0 12px;
|
|
3222
|
+
border-radius: var(--gemboss-radius-md, 8px);
|
|
3223
|
+
background: var(--gemboss-primary, #1b1c1e);
|
|
3224
|
+
color: var(--gemboss-on-primary, #fff);
|
|
3225
|
+
font-size: 13px;
|
|
3226
|
+
font-weight: 600;
|
|
3227
|
+
line-height: 1;
|
|
3228
|
+
text-align: left;
|
|
3229
|
+
}
|
|
3230
|
+
.gb-assistant-launcher--rail:hover { background: var(--gemboss-primary-dark, #000); }
|
|
3231
|
+
.gb-assistant-launcher--rail .gb-assistant-launcher-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3190
3232
|
.gb-assistant-launcher-kbd {
|
|
3191
3233
|
font: inherit; font-size: 11px; opacity: 0.7;
|
|
3192
3234
|
border: 1px solid currentColor; border-radius: 5px; padding: 1px 5px;
|
|
3193
3235
|
}
|
|
3236
|
+
.gb-assistant-launcher--topbar {
|
|
3237
|
+
display: none;
|
|
3238
|
+
position: relative; /* holds the clipped label: an absolute child of a static button escapes to the nearest positioned box */
|
|
3239
|
+
justify-content: center;
|
|
3240
|
+
flex: 0 0 auto;
|
|
3241
|
+
width: 44px;
|
|
3242
|
+
height: 44px;
|
|
3243
|
+
padding: 0;
|
|
3244
|
+
border-radius: var(--gemboss-radius-md, 8px);
|
|
3245
|
+
background: transparent;
|
|
3246
|
+
color: var(--gemboss-text, #1b1c1e);
|
|
3247
|
+
}
|
|
3248
|
+
.gb-assistant-launcher--topbar:hover, .gb-assistant-launcher--topbar:active { background: var(--gemboss-hover, #eceef2); }
|
|
3249
|
+
.gb-assistant-launcher--topbar .gb-assistant-launcher-label, .gb-assistant-launcher--topbar .gb-assistant-launcher-kbd {
|
|
3250
|
+
position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
|
|
3251
|
+
}
|
|
3194
3252
|
@media (max-width: 760px) {
|
|
3195
|
-
.gb-assistant-launcher {
|
|
3196
|
-
.gb-assistant-launcher
|
|
3197
|
-
position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
|
|
3198
|
-
}
|
|
3253
|
+
.gb-assistant-launcher--rail { display: none; }
|
|
3254
|
+
.gb-assistant-launcher--topbar { display: inline-flex; }
|
|
3199
3255
|
}
|
|
3200
3256
|
|
|
3201
3257
|
/* ═══ @gemboss/ui src/kit-plain.css ═══ */
|
|
@@ -3278,7 +3334,7 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
3278
3334
|
.ui-link:hover,
|
|
3279
3335
|
.ui-prose a:not([class]):hover { color: var(--gemboss-accent, #5661f5); text-decoration-thickness: 2px; }
|
|
3280
3336
|
.ui-link:focus-visible,
|
|
3281
|
-
.ui-prose a:not([class]):focus-visible { outline: 2px solid var(--ui-focus, #
|
|
3337
|
+
.ui-prose a:not([class]):focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 2px; border-radius: 2px; }
|
|
3282
3338
|
|
|
3283
3339
|
.ui-card--media { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
3284
3340
|
.ui-card__media {
|
|
@@ -3287,7 +3343,7 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
3287
3343
|
}
|
|
3288
3344
|
.ui-card__media > img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
3289
3345
|
button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
3290
|
-
.ui-card__media:focus-visible { outline: 2px solid var(--ui-focus, #
|
|
3346
|
+
.ui-card__media:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: -2px; }
|
|
3291
3347
|
/* justify-items: start: a tag or a button in the body keeps its own width instead of stretching across the card. */
|
|
3292
3348
|
.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; }
|
|
3293
3349
|
.ui-card__foot {
|
|
@@ -3297,8 +3353,8 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
|
3297
3353
|
}
|
|
3298
3354
|
|
|
3299
3355
|
/* Popup body and foot: `<div class="ui-modal__body">…</div><div class="ui-modal__foot"><button class="ui-btn">…</button></div>`.
|
|
3300
|
-
|
|
3301
|
-
hand (GemFactory: 16/20 and a hairline above the actions). kit-plain.js's openModal writes exactly this shape. */
|
|
3356
|
+
ui.css never styled these, and the admin does not load this file: its Modal.tsx children carry their own padding
|
|
3357
|
+
(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. */
|
|
3302
3358
|
.ui-modal__body { padding: 16px 20px; }
|
|
3303
3359
|
.ui-modal__foot {
|
|
3304
3360
|
display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
|
|
@@ -3416,6 +3472,225 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
|
3416
3472
|
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. */
|
|
3417
3473
|
@media (pointer: coarse), (max-width: 47.9975em) { .ui-seg :is(button, a) { height: 44px; min-height: 44px; } }
|
|
3418
3474
|
|
|
3475
|
+
/* ── Polaris parity: the primitives every surface was drawing for itself (02/10/2026) ──────────────────────
|
|
3476
|
+
An audit of the six surfaces on the package (gemwatcher, GemFactory, gempress, gemcare, partner-radar,
|
|
3477
|
+
content-radar) found the same gaps re-filled six ways: no destructive button (four surfaces fell back to the
|
|
3478
|
+
browser's `confirm()`), one-tone badges, banners as bare colour blocks, hand-made radio / switch / tooltip /
|
|
3479
|
+
spinner, and help and error text under a field in five sizes. The admin has all of them from Polaris; this is
|
|
3480
|
+
the same shape in plain CSS, with Polaris' measurements and GemBoss' tokens (the danger / success / warning
|
|
3481
|
+
set the kit's tags and notices already use). The admin does not load this file. */
|
|
3482
|
+
|
|
3483
|
+
/* Destructive button: `ui-btn ui-btn--critical` (red text on the default button) and
|
|
3484
|
+
`ui-btn ui-btn--primary ui-btn--critical` (red fill), Polaris' `tone="critical"` in both variants. Red text is
|
|
3485
|
+
--gemboss-danger-strong: 7.3:1 on white, 5.7:1 on the hover ground (shell.css says why not --gemboss-danger). */
|
|
3486
|
+
.ui-btn--critical { color: var(--gemboss-danger-strong); }
|
|
3487
|
+
.ui-btn--critical:hover { background: var(--gemboss-danger-soft); color: var(--gemboss-danger-strong); }
|
|
3488
|
+
.ui-btn--primary.ui-btn--critical { background: var(--gemboss-danger); border-color: var(--gemboss-danger); color: var(--gemboss-on-primary); }
|
|
3489
|
+
.ui-btn--primary.ui-btn--critical:hover { background: var(--gemboss-danger-strong); border-color: var(--gemboss-danger-strong); color: var(--gemboss-on-primary); }
|
|
3490
|
+
.ui-btn--critical:focus-visible { outline-color: var(--gemboss-danger-strong); }
|
|
3491
|
+
|
|
3492
|
+
/* Under a thumb, and at phone WIDTH (a desktop browser at 320px reports pointer: fine), EVERY kit button is a 44px
|
|
3493
|
+
control, the compact one included: §9.1 has no density exception, and GemFactory, gempress and gemcare each
|
|
3494
|
+
restated this floor for `--sm` themselves. ui.css's coarse block leaves `--sm` alone for the admin's own
|
|
3495
|
+
settings page; this file is not loaded there. */
|
|
3496
|
+
@media (pointer: coarse), (max-width: 47.9975em) {
|
|
3497
|
+
.ui-btn, .ui-btn.ui-btn--sm { min-height: 44px; }
|
|
3498
|
+
.ui-select.ui-select { min-height: 44px; }
|
|
3499
|
+
/* A target is an AREA: the compact icon button (28 wide) and an icon segment (30) grow sideways as well. */
|
|
3500
|
+
.ui-btn--icon.ui-btn--sm, .ui-seg :is(button, a):not(.is-text) { min-width: 44px; }
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
/* 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).
|
|
3504
|
+
`ui-badge--dot` puts a status dot before the label (filled = done, the tone's colour). */
|
|
3505
|
+
.ui-badge--success { background: var(--gemboss-success-soft); color: var(--gemboss-success-strong); }
|
|
3506
|
+
.ui-badge--warning { background: var(--gemboss-warning-soft); color: var(--gemboss-warning); }
|
|
3507
|
+
.ui-badge--critical { background: var(--gemboss-danger-soft); color: var(--gemboss-danger-strong); }
|
|
3508
|
+
.ui-badge--info { background: var(--gemboss-accent-soft); color: var(--gemboss-accent-strong); }
|
|
3509
|
+
.ui-badge--dot::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3510
|
+
|
|
3511
|
+
/* Banner: Polaris' in-page Banner. A tone strip with an icon and a title, a body, optional actions, optional
|
|
3512
|
+
dismiss. `ui-notice` stays for a one-line note; a banner is for something the person must read or act on.
|
|
3513
|
+
<div class="ui-banner ui-banner--warning" role="status">
|
|
3514
|
+
<div class="ui-banner__head"><svg class="ui-banner__icon" …/><h3 class="ui-banner__title">Slack token expired</h3>
|
|
3515
|
+
<button type="button" class="ui-banner__close" aria-label="Dismiss">…</button></div>
|
|
3516
|
+
<div class="ui-banner__body"><p>Monday's digest will not send.</p>
|
|
3517
|
+
<div class="ui-banner__actions"><a class="ui-btn" href>Reconnect</a></div></div></div>
|
|
3518
|
+
No tone = neutral. role="status" / "alert" only when it appears after the page loaded. */
|
|
3519
|
+
.ui-banner {
|
|
3520
|
+
--ui-banner-bg: var(--ui-surface-2); --ui-banner-fg: var(--ui-text);
|
|
3521
|
+
border-radius: var(--ui-radius-lg); overflow: hidden; background: var(--ui-surface); color: var(--ui-text);
|
|
3522
|
+
box-shadow: 0 0 0 1px var(--ui-border), 0 3px 1px -1px rgba(26, 26, 26, 0.07);
|
|
3523
|
+
font-size: var(--ui-fs-body); line-height: 20px;
|
|
3524
|
+
}
|
|
3525
|
+
.ui-banner--success { --ui-banner-bg: var(--gemboss-success-soft); --ui-banner-fg: var(--gemboss-success-strong); }
|
|
3526
|
+
.ui-banner--warning { --ui-banner-bg: var(--gemboss-warning-soft); --ui-banner-fg: var(--gemboss-warning); }
|
|
3527
|
+
.ui-banner--critical { --ui-banner-bg: var(--gemboss-danger-soft); --ui-banner-fg: var(--gemboss-danger-strong); }
|
|
3528
|
+
.ui-banner--info { --ui-banner-bg: var(--gemboss-accent-soft); --ui-banner-fg: var(--gemboss-accent-strong); }
|
|
3529
|
+
.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); }
|
|
3530
|
+
.ui-banner__icon { flex: none; width: 20px; height: 20px; fill: currentColor; }
|
|
3531
|
+
.ui-banner__title { flex: 1 1 auto; min-width: 0; margin: 0; font-size: var(--ui-fs-body); font-weight: 650; line-height: 20px; }
|
|
3532
|
+
.ui-banner__close {
|
|
3533
|
+
flex: none; display: inline-grid; place-items: center; width: 32px; height: 32px; margin: -6px -8px -6px 0;
|
|
3534
|
+
border: 0; border-radius: var(--ui-radius-md); background: transparent; color: inherit; cursor: pointer;
|
|
3535
|
+
}
|
|
3536
|
+
.ui-banner__close:hover { background: color-mix(in srgb, currentColor 10%, transparent); }
|
|
3537
|
+
.ui-banner__close:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
|
|
3538
|
+
.ui-banner__close svg { width: 16px; height: 16px; }
|
|
3539
|
+
.ui-banner__body { padding: var(--ui-space-3) var(--ui-space-4); }
|
|
3540
|
+
.ui-banner__body > :first-child { margin-top: 0; }
|
|
3541
|
+
.ui-banner__body > :last-child { margin-bottom: 0; }
|
|
3542
|
+
.ui-banner__actions { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); margin-top: var(--ui-space-3); }
|
|
3543
|
+
@media (pointer: coarse), (max-width: 47.9975em) { .ui-banner__close { width: 44px; height: 44px; margin: -12px -12px -12px 0; } }
|
|
3544
|
+
|
|
3545
|
+
/* Help and error text under a field, Polaris' Labelled help text and InlineError:
|
|
3546
|
+
<label class="ui-label" for="x">Domain</label><input id="x" class="gemboss-field gemboss-field--tall"
|
|
3547
|
+
aria-describedby="x-help x-err" aria-invalid="true"><p class="ui-help" id="x-help">Without https://</p>
|
|
3548
|
+
<p class="ui-error" id="x-err">This store is already tracked.</p>
|
|
3549
|
+
aria-invalid paints the field in the critical tone; the message says what to do, not only what is wrong. */
|
|
3550
|
+
.ui-help { margin: var(--ui-space-1) 0 0; font-size: var(--ui-fs-body); line-height: 20px; color: var(--ui-text-muted); }
|
|
3551
|
+
.ui-error:empty { display: none; } /* a placeholder kept for aria-describedby draws no icon */
|
|
3552
|
+
.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); }
|
|
3553
|
+
.ui-error::before {
|
|
3554
|
+
content: ""; flex: none; width: 16px; height: 16px; margin-top: 2px; background: currentColor;
|
|
3555
|
+
-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;
|
|
3556
|
+
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;
|
|
3557
|
+
}
|
|
3558
|
+
.gemboss-field[aria-invalid="true"], .gemboss-field--group:has([aria-invalid="true"]),
|
|
3559
|
+
.ui-select[aria-invalid="true"], .ui-textarea[aria-invalid="true"] {
|
|
3560
|
+
border-color: var(--gemboss-danger-strong); background-color: var(--gemboss-danger-soft);
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
/* Radio: the checkbox's row with `type="radio"` — `<fieldset class="ui-choices"><legend class="ui-label">Plan</legend>
|
|
3564
|
+
<label class="ui-check"><input type="radio" name="plan" checked> Basic</label>…</fieldset>`. */
|
|
3565
|
+
.ui-choices { display: grid; gap: 0; margin: 0; padding: 0; border: 0; min-width: 0; }
|
|
3566
|
+
.ui-choices > legend { padding: 0; }
|
|
3567
|
+
|
|
3568
|
+
/* Switch: a checkbox that reads as on / off, for a setting that applies at once (no Save button).
|
|
3569
|
+
`<label class="ui-check ui-switch"><input type="checkbox" role="switch" checked> Send Monday digest</label>`.
|
|
3570
|
+
Track 32×20, thumb 16; the row stays the checkbox's 44px target. */
|
|
3571
|
+
.ui-switch > input[type="checkbox"] {
|
|
3572
|
+
appearance: none; -webkit-appearance: none; position: relative; width: 32px; height: 20px; margin: 0;
|
|
3573
|
+
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;
|
|
3574
|
+
}
|
|
3575
|
+
.ui-switch > input[type="checkbox"]::before {
|
|
3576
|
+
content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
|
|
3577
|
+
background: var(--gemboss-surface); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); transition: transform var(--gemboss-transition-fast) ease;
|
|
3578
|
+
}
|
|
3579
|
+
.ui-switch > input[type="checkbox"]:checked { background: var(--ui-accent); }
|
|
3580
|
+
.ui-switch > input[type="checkbox"]:checked::before { transform: translateX(12px); }
|
|
3581
|
+
@media (prefers-reduced-motion: reduce) { .ui-switch > input[type="checkbox"], .ui-switch > input[type="checkbox"]::before { transition: none; } }
|
|
3582
|
+
|
|
3583
|
+
/* Tooltip: a SHORT label for an icon-only control, shown on hover and keyboard focus. Not for anything the person
|
|
3584
|
+
needs to act on (a touch screen never hovers).
|
|
3585
|
+
<span class="ui-tooltip"><button class="ui-btn ui-btn--icon" aria-describedby="t1" aria-label="Archive">…</button>
|
|
3586
|
+
<span class="ui-tooltip__bubble" role="tooltip" id="t1">Archive</span></span> `--end` aligns it right. */
|
|
3587
|
+
.ui-tooltip { position: relative; display: inline-flex; }
|
|
3588
|
+
.ui-tooltip__bubble {
|
|
3589
|
+
position: absolute; z-index: 30; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
|
|
3590
|
+
width: max-content; max-width: 240px; padding: 4px 8px; border-radius: var(--ui-radius-md);
|
|
3591
|
+
background: var(--ui-surface); color: var(--ui-text); font-size: var(--ui-fs-body); line-height: 20px; font-weight: 450;
|
|
3592
|
+
box-shadow: 0 8px 16px -4px rgba(26, 26, 26, 0.22), 0 0 0 1px var(--ui-border);
|
|
3593
|
+
opacity: 0; visibility: hidden; pointer-events: none; transition: opacity var(--gemboss-transition-fast) ease;
|
|
3594
|
+
}
|
|
3595
|
+
.ui-tooltip--end .ui-tooltip__bubble { left: auto; right: 0; transform: none; }
|
|
3596
|
+
.ui-tooltip:hover > .ui-tooltip__bubble, .ui-tooltip:focus-within > .ui-tooltip__bubble { opacity: 1; visibility: visible; }
|
|
3597
|
+
|
|
3598
|
+
/* Spinner: `<span class="ui-spinner" role="status"><span class="ui-sr-only">Loading…</span></span>`, 20px
|
|
3599
|
+
(Polaris small); `--lg` is 44px. Reduced motion turns it slower, never off: it is the only sign of waiting. */
|
|
3600
|
+
.ui-spinner {
|
|
3601
|
+
display: inline-block; flex: none; width: 20px; height: 20px; box-sizing: border-box; border-radius: 50%;
|
|
3602
|
+
border: 2px solid color-mix(in srgb, var(--ui-text) 18%, transparent); border-top-color: var(--ui-text);
|
|
3603
|
+
animation: ui-spin 0.7s linear infinite; vertical-align: middle;
|
|
3604
|
+
}
|
|
3605
|
+
.ui-spinner--lg { width: 44px; height: 44px; border-width: 4px; }
|
|
3606
|
+
@keyframes ui-spin { to { transform: rotate(360deg); } }
|
|
3607
|
+
@media (prefers-reduced-motion: reduce) { .ui-spinner { animation-duration: 2s; } }
|
|
3608
|
+
|
|
3609
|
+
/* Progress: `<progress class="ui-progress" value="37" max="100" aria-label="Import">37%</progress>`, 8px, the
|
|
3610
|
+
primary fill (Polaris' small ProgressBar). Tones: `--success`, `--critical`. */
|
|
3611
|
+
.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); }
|
|
3612
|
+
.ui-progress::-webkit-progress-bar { background: var(--gemboss-divider); border-radius: inherit; }
|
|
3613
|
+
.ui-progress::-webkit-progress-value { background: currentColor; border-radius: inherit; }
|
|
3614
|
+
.ui-progress::-moz-progress-bar { background: currentColor; border-radius: inherit; }
|
|
3615
|
+
.ui-progress--success { color: var(--gemboss-success); }
|
|
3616
|
+
.ui-progress--critical { color: var(--gemboss-danger); }
|
|
3617
|
+
|
|
3618
|
+
/* Wider popup: `openModal({ className: "ui-modal--wide" })`, Polaris' default Modal (620px). The kit's own 460 is
|
|
3619
|
+
Polaris' small step plus one; a form with two columns or a table needs this one. */
|
|
3620
|
+
.ui-modal--wide { width: min(620px, 100%); }
|
|
3621
|
+
|
|
3622
|
+
/* Native popup: `<dialog class="ui-modal">` opened with `showModal()`, for React (and other) surfaces that do not load
|
|
3623
|
+
kit-plain.js. The browser does what openModal does by hand: Escape closes it, the page behind is inert, it sits in
|
|
3624
|
+
the top layer above the shell's topbar. Same head / body / foot markup; `::backdrop` is the kit's overlay. gemcare
|
|
3625
|
+
and content-radar already used `<dialog>`, each with its own width, padding and backdrop (02/10/2026: four shapes,
|
|
3626
|
+
420 to 760 wide). Close on a backdrop click yourself: a click whose target IS the dialog landed outside the panel.
|
|
3627
|
+
`<dialog class="ui-modal" aria-labelledby="t"><div class="ui-modal__head"><div class="ui-modal__title" id="t">…</div>
|
|
3628
|
+
<button type="button" class="ui-modal__close" aria-label="Close">…</button></div><div class="ui-modal__body">…</div>
|
|
3629
|
+
<div class="ui-modal__foot">…</div></dialog>` */
|
|
3630
|
+
dialog.ui-modal { padding: 0; max-width: calc(100% - 40px); max-height: calc(100dvh - 40px); color: var(--ui-text); }
|
|
3631
|
+
dialog.ui-modal[open] { display: flex; flex-direction: column; }
|
|
3632
|
+
dialog.ui-modal::backdrop { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px); }
|
|
3633
|
+
|
|
3634
|
+
/* ── 0.5.1: what the first surfaces on 0.5.0 still drew themselves (GemFactory, content-radar, 03/10/2026) ─────────── */
|
|
3635
|
+
|
|
3636
|
+
/* At phone width a short compact button ("Bỏ") was 44 tall but 41 wide: a target is an area. */
|
|
3637
|
+
@media (pointer: coarse), (max-width: 47.9975em) { .ui-btn.ui-btn--sm { min-width: 44px; } }
|
|
3638
|
+
|
|
3639
|
+
/* A long label in a badge ("Đã dùng 1 lần + 4 bài cùng nguồn · 2 Hot News") wraps instead of running out of its card. */
|
|
3640
|
+
.ui-badge--wrap { white-space: normal; overflow-wrap: anywhere; line-height: 16px; }
|
|
3641
|
+
|
|
3642
|
+
/* Page head with actions on the right, wrapping under the title when narrow:
|
|
3643
|
+
`<div class="ui-page-head ui-page-head--actions"><div><h1>…</h1><p>…</p></div><div class="ui-page-head__actions">
|
|
3644
|
+
<button class="ui-btn ui-btn--primary">…</button></div></div>`. */
|
|
3645
|
+
.ui-page-head--actions { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 12px 16px; }
|
|
3646
|
+
.ui-page-head--actions > :first-child { min-width: 0; }
|
|
3647
|
+
.ui-page-head__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2); }
|
|
3648
|
+
|
|
3649
|
+
/* Chips that TOGGLE (a filter applied in place, no URL): `<div class="ui-chips"><button type="button" class="ui-tag
|
|
3650
|
+
ui-tag--chip" aria-pressed="true">CRO</button>…`. Same box as the link chips (ui.css); pressed = the link's current. */
|
|
3651
|
+
.ui-chips > button.ui-tag--chip {
|
|
3652
|
+
min-height: 28px; padding: 4px 12px; border-radius: var(--ui-radius-md, 8px); cursor: pointer;
|
|
3653
|
+
font-family: inherit; font-size: var(--gemboss-fs-caption, 12px); color: var(--ui-text);
|
|
3654
|
+
}
|
|
3655
|
+
.ui-chips > button.ui-tag--chip:hover { background: var(--ui-surface-2); border-color: var(--ui-text-muted); }
|
|
3656
|
+
.ui-chips > button.ui-tag--chip:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 1px; }
|
|
3657
|
+
.ui-chips > button.ui-tag--chip[aria-pressed="true"] { background: var(--ui-accent-soft); border-color: var(--ui-accent); color: var(--ui-accent-strong); font-weight: 600; }
|
|
3658
|
+
@media (pointer: coarse), (max-width: 47.9975em) { .ui-chips > button.ui-tag--chip { min-height: 44px; } }
|
|
3659
|
+
|
|
3660
|
+
/* Chat at phone WIDTH (gemwatcher's assistant, measured 03/10/2026): the composer read 14px (iOS zooms the page on tap
|
|
3661
|
+
below 16) and "new chat" was 35px wide under a fine pointer. shell.css raises the latter under `pointer: coarse`; a
|
|
3662
|
+
desktop window at 390px reports fine. Here and not in chat.css / shell.css: the admin's first-load budget. */
|
|
3663
|
+
@media (max-width: 47.9975em) {
|
|
3664
|
+
/* Class doubled: chat.css is appended AFTER this file in gemboss-ui.css, so a same-weight rule there would win. */
|
|
3665
|
+
.gemboss-ec-input.gemboss-ec-input textarea { font-size: 16px; }
|
|
3666
|
+
.gemboss-assistant-collapse, .gemboss-assistant-newchat { min-height: 44px; min-width: 44px; }
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
/* Segmented buttons that say their state the accessible way: `aria-pressed="true"` looks like `.active` (ui-tabs reads
|
|
3670
|
+
aria-selected already; gemcare had to set both on every group). */
|
|
3671
|
+
.ui-seg button[aria-pressed="true"] { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
|
|
3672
|
+
|
|
3673
|
+
/* Read-only field: visibly not editable (the admin's field has no such state; gempress kept its own `.field[readonly]`).
|
|
3674
|
+
The attribute doubled = (0,3,0), so it also holds over the field's own hover rule, which comes earlier. */
|
|
3675
|
+
:is(.gemboss-field, .ui-textarea)[readonly][readonly] { background: var(--ui-surface-2); border-color: var(--ui-border); cursor: default; }
|
|
3676
|
+
|
|
3677
|
+
/* A popup whose body and foot sit inside one <form>: the form takes no box, so `.ui-modal > .ui-modal__body` (the
|
|
3678
|
+
scrolling body) and the pinned foot still lay out as the popup's own children (gemcare's escalate form, 0.5.1). */
|
|
3679
|
+
.ui-modal > form { display: contents; }
|
|
3680
|
+
.ui-modal > form > .ui-modal__head, .ui-modal > form > .ui-modal__foot { flex: none; }
|
|
3681
|
+
.ui-modal > form > .ui-modal__body { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
3682
|
+
|
|
3683
|
+
/* A metric that FILTERS the view below it, as a toggle: `<button type="button" class="ui-metric" aria-pressed="true">`
|
|
3684
|
+
(gemwatcher's Radar cards: as links to the same hash, Cmd-click opened an unfiltered tab, Space did nothing and the
|
|
3685
|
+
chosen card was told by colour alone). A metric link that is the current view: `a.ui-metric[aria-current="page"]`.
|
|
3686
|
+
Selected = iris edge, the shell's "this one" colour. */
|
|
3687
|
+
button.ui-metric { font: inherit; text-align: left; color: inherit; cursor: pointer; }
|
|
3688
|
+
button.ui-metric:hover { border-color: var(--ui-text-muted); }
|
|
3689
|
+
button.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
|
|
3690
|
+
/* Class doubled: the chosen card is usually the one under the pointer, and `button.ui-metric:hover` (0,2,1) would
|
|
3691
|
+
repaint its edge grey the moment it is clicked (measured on gemwatcher Radar). */
|
|
3692
|
+
.ui-metric.ui-metric:is([aria-pressed="true"], [aria-current="page"]) { border-color: var(--gemboss-accent); box-shadow: inset 0 0 0 1px var(--gemboss-accent); }
|
|
3693
|
+
|
|
3419
3694
|
/* ═══ @gemboss/ui src/chat.css ═══ */
|
|
3420
3695
|
/*
|
|
3421
3696
|
* The chat BODY — what an answer looks like, wherever a GemBoss surface puts a chat (01/10/2026).
|
|
@@ -3774,7 +4049,7 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
|
3774
4049
|
* chat can sit in a 380px sidebar — and capped in height so five thumbnails read as a shortlist, not
|
|
3775
4050
|
* five screens. The link around it is the tap target: keyboard focus has to be visible on it. */
|
|
3776
4051
|
.gemboss-md-imglink { display: inline-block; max-width: 100%; margin: 4px 0 8px; border-radius: 8px; line-height: 0; }
|
|
3777
|
-
.gemboss-md-imglink:focus-visible { outline: 2px solid var(--gemboss-focus, #
|
|
4052
|
+
.gemboss-md-imglink:focus-visible { outline: 2px solid var(--gemboss-focus, var(--ui-focus, #5661f5)); outline-offset: 2px; }
|
|
3778
4053
|
.gemboss-md-img {
|
|
3779
4054
|
display: block;
|
|
3780
4055
|
max-width: 100%;
|
|
@@ -3960,4 +4235,4 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
|
3960
4235
|
.gemboss-ec-stop svg { width: 12px; height: 12px; fill: currentColor; }
|
|
3961
4236
|
.gemboss-ec-stop:disabled { opacity: 0.6; cursor: default; }
|
|
3962
4237
|
.gemboss-ec-stop:hover { filter: brightness(1.08); }
|
|
3963
|
-
.gemboss-ec-stop:focus-visible { outline: 2px solid var(--gemboss-focus, #
|
|
4238
|
+
.gemboss-ec-stop:focus-visible { outline: 2px solid var(--gemboss-focus, var(--ui-focus, #5661f5)); outline-offset: 2px; }
|