@gemboss/ui 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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: c41e1cabde7bcdfa7747fdc2c88d5afbdf632cc6
5
- * sources sha256: 55d17952dd57f9e4a7fb14d4e12dc1c65236c97c26f2c7c40f86e5e04f233e1a
4
+ * gemboss commit: be1da9b1254eb62cd68a2e65f026c94b4d7258cf
5
+ * sources sha256: d4f46aabf4df141636ddacc91c119762a521312a30154ca0e6d82412cb99491c
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)
@@ -214,6 +214,10 @@
214
214
  --ui-accent-soft: var(--gemboss-active-bg, #f0f1f4); /* tinted bg for a selected/active state */
215
215
  --ui-accent-strong: var(--gemboss-active-fg, #1b1c1e); /* readable accent text ON --ui-accent-soft */
216
216
  --ui-text-strong: var(--gemboss-text-strong, #1b1c1e);
217
+ /* Chữ theo tông (ui-metric, …). Nền tối của admin ánh xạ lại bên dưới để còn đọc được. */
218
+ --ui-success-fg: var(--gemboss-success);
219
+ --ui-warning-fg: var(--gemboss-primary-dark);
220
+ --ui-danger-fg: var(--gemboss-danger-strong);
217
221
  --ui-focus: var(--gemboss-primary, #1b1c1e);
218
222
  --ui-radius-sm: 6px;
219
223
  --ui-radius-md: 8px;
@@ -273,6 +277,10 @@
273
277
  --ui-accent-soft: var(--adm-accent-soft);
274
278
  --ui-accent-strong: var(--adm-accent-fg);
275
279
  --ui-text-strong: var(--adm-text-strong);
280
+ /* Tông trên nền tối #16161a: pha 70% với trắng ⇒ tương phản 6,2 / 9,5 / 4,8 (đo 01/10/2026, review #3609). */
281
+ --ui-success-fg: color-mix(in srgb, var(--gemboss-success) 70%, #ffffff);
282
+ --ui-warning-fg: color-mix(in srgb, #c99a2e 70%, #ffffff);
283
+ --ui-danger-fg: color-mix(in srgb, var(--gemboss-danger-strong) 70%, #ffffff);
276
284
  --ui-focus: var(--adm-accent);
277
285
  }
278
286
 
@@ -303,6 +311,11 @@
303
311
  padding: var(--ui-space-4); color: var(--ui-text);
304
312
  }
305
313
  .ui-card__title { font-size: 14px; font-weight: 600; margin: 0 0 var(--ui-space-2); }
314
+ /* Đầu thẻ có nút: `<div class="ui-card__head"><h2 class="ui-card__title">…</h2><button class="ui-btn">…</button></div>`.
315
+ Tiêu đề trái, hành động phải, xuống dòng khi hẹp (01/10/2026: partner-radar rel-head / agr). */
316
+ .ui-card__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--ui-space-2); margin-bottom: var(--ui-space-2); }
317
+ .ui-card__head > .ui-card__title { margin: 0; }
318
+ .ui-card__head > :last-child:not(.ui-card__title) { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); }
306
319
 
307
320
  /* ── Select ──────────────────────────────────────────────────────────────────
308
321
  Native <select> reskinned; custom caret as an inline SVG data-URI (caret color
@@ -324,7 +337,7 @@
324
337
  background-color: var(--gemboss-field-bg-hover);
325
338
  }
326
339
  .ui-select:focus { outline: none; }
327
- .ui-select:focus-visible {
340
+ .ui-select:focus-visible:not(:disabled) {
328
341
  outline: 2px solid #005bd3; outline-offset: 1px;
329
342
  border-color: var(--gemboss-field-line-active);
330
343
  }
@@ -347,6 +360,85 @@
347
360
  }
348
361
  .ui-tag--chip.ui-tag--accent { color: var(--adm-accent-fg, var(--ui-accent)); border-color: var(--ui-accent); background: transparent; }
349
362
 
363
+ /* Danh sách link dạng chip: `<div class="ui-chips"><a class="ui-tag ui-tag--chip" href>…</a>…</div>` (01/10/2026,
364
+ partner-radar /competitors: "đã tích hợp với GemPages"). `.ui-tag--chip` vốn là nhãn TĨNH cao ~19px, dưới mức 24px
365
+ của link trong danh sách (WCAG 2.5.8, §9.1) — visual-gate đo đúng chỗ đó đỏ ở 320 / 390 / 768. Chip là link thì cao
366
+ ≥ 28px, chữ thường (không mono), có hover + focus-visible; dưới ngón tay là điều khiển, lên 44px (khối coarse dưới).
367
+ Khe 8px: hai target nhỏ hơn 44px phải cách nhau ≥ 8px. */
368
+ .ui-chips { display: flex; flex-wrap: wrap; gap: var(--ui-space-2, 8px); margin: 0; padding: 0; list-style: none; }
369
+ .ui-chips > a.ui-tag--chip {
370
+ min-height: 28px; padding: 4px 12px; border-radius: var(--ui-radius-md, 8px);
371
+ font-family: inherit; font-size: var(--gemboss-fs-caption, 12px); color: var(--ui-text); text-decoration: none;
372
+ }
373
+ .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; }
375
+ /* 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
+ Ô đang chọn = `aria-current="page"` (lọc là một URL), cùng tông với ui-seg đang chọn. */
377
+ .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; }
378
+
379
+ /* ── Tông màu cho tag: `ui-tag--success | --warning | --danger` (token ngữ nghĩa có sẵn). Trạng thái, nguồn,
380
+ kết quả kiểm — thay cho màu viết cứng ở từng surface. */
381
+ .ui-tag--success { background: var(--gemboss-success-soft); color: var(--gemboss-success-strong, var(--gemboss-success)); }
382
+ .ui-tag--warning { background: var(--gemboss-warning-soft); color: var(--gemboss-warning); }
383
+ .ui-tag--danger { background: var(--gemboss-danger-soft); color: var(--gemboss-danger-strong, var(--gemboss-danger)); }
384
+
385
+ /* ── Chữ phụ: `<p class="ui-muted">`, `<span class="ui-muted">` — mô tả, chú thích, dòng meta. Cỡ caption, màu muted.
386
+ Trước đây mỗi surface tự viết `.muted` (partner-radar: 180 chỗ). */
387
+ .ui-muted { color: var(--ui-text-muted); font-size: var(--gemboss-fs-caption); }
388
+
389
+ /* ── Tiêu đề trang: `<div class="ui-page-head"><h1>…</h1><p>…</p></div>`. Đưa lên NGUYÊN VĂN từ admin
390
+ `.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
+ 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
+ .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: 14px; margin-top: 4px; }
395
+
396
+ /* ── 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
+ Đưa lên từ admin `.gemboss-admin-toolbar`. Chỉ BỐ CỤC: ô tìm là `gemboss-field gemboss-field--group`. */
398
+ .ui-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-bottom: 20px; }
399
+
400
+ /* ── Ô số liệu: `<div class="ui-metrics"><div class="ui-metric"><span class="ui-metric__val">12</span>
401
+ <span class="ui-metric__lbl">Nhãn</span></div></div>`; tông `ui-metric--success | --warning | --danger`.
402
+ Đưa lên từ admin `.gemboss-ec-metric` (GemChat). Khác `ui-stat` (viên đếm nhỏ, có thể bấm làm bộ lọc). */
403
+ .ui-metrics { display: flex; flex-wrap: wrap; gap: 8px; margin: 2px 0; }
404
+ .ui-metric { display: flex; flex-direction: column; gap: 1px; min-width: 78px; padding: 8px 12px; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: 12px; }
405
+ /* Cả ô là link (`<a class="ui-metric" href>`): vùng chạm là cả ô (~52px), không phải con số 20px bên trong
406
+ (visual-gate đỏ ở partner-radar /ops 01/10/2026 khi link nằm trong __val). */
407
+ a.ui-metric { text-decoration: none; color: inherit; }
408
+ a.ui-metric:hover { border-color: var(--ui-text-muted); }
409
+ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
410
+ .ui-metric__val { font-size: 17px; font-weight: 700; color: var(--ui-text); font-variant-numeric: tabular-nums; line-height: 1.15; }
411
+ .ui-metric__lbl { font-size: var(--gemboss-fs-caption); font-weight: 500; color: var(--ui-text-muted); }
412
+ .ui-metric--success { border-color: color-mix(in srgb, var(--gemboss-success) 40%, transparent); }
413
+ .ui-metric--success .ui-metric__val { color: var(--ui-success-fg); }
414
+ .ui-metric--warning { border-color: color-mix(in srgb, #c99a2e 40%, transparent); }
415
+ .ui-metric--warning .ui-metric__val { color: var(--ui-warning-fg); }
416
+ .ui-metric--danger { border-color: color-mix(in srgb, var(--gemboss-danger-strong) 40%, transparent); }
417
+ .ui-metric--danger .ui-metric__val { color: var(--ui-danger-fg); }
418
+
419
+ /* ── Cặp nhãn – giá trị: `<dl class="ui-dl"><div><dt>Quốc gia</dt><dd>IL</dd></div>…</dl>` — dữ kiện của một hồ sơ.
420
+ Lưới tự giãn: mỗi ô ≥ 160px, xuống dòng khi hẹp. */
421
+ .ui-dl { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px 16px; margin: 0; }
422
+ .ui-dl > div { min-width: 0; }
423
+ .ui-dl dt { font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
424
+ .ui-dl dd { margin: 4px 0 0; font-weight: 600; color: var(--ui-text); overflow-wrap: anywhere; }
425
+
426
+ /* ── 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
+ 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
+ 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); }
430
+ .ui-notice--warning { background: var(--gemboss-warning-soft); border-color: var(--gemboss-warning-border); color: var(--gemboss-warning); }
431
+ .ui-notice--danger { background: var(--gemboss-danger-soft); border-color: var(--gemboss-danger-border); color: var(--gemboss-danger-strong); }
432
+ .ui-notice--success { background: var(--gemboss-success-soft); border-color: var(--gemboss-success-border); color: var(--gemboss-success-strong); }
433
+
434
+ /* ── 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
+ <a href rel="next">Sau ›</a></nav>`. Link ≥ 32px (≥ 24px WCAG 2.5.8), 44px dưới ngón tay (khối coarse). */
436
+ .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); }
438
+ .ui-pager > a:hover { background: var(--ui-surface-2); }
439
+ .ui-pager > a:focus-visible { outline: 2px solid #005bd3; outline-offset: 1px; }
440
+ .ui-pager > [aria-current="page"] { color: var(--ui-text-muted); font-size: var(--gemboss-fs-caption); }
441
+
350
442
  /* ── Badge ────────────────────────────────────────────────────────────────────
351
443
  Uppercase mono pill for a status/label (was gemboss-admin-storage-badge). */
352
444
  .ui-badge {
@@ -391,24 +483,50 @@
391
483
  consumer belongs in that consumer's chunk (see SettingsDialog.module.css). */
392
484
  .ui-modal--lg { position: relative; }
393
485
 
486
+ /* The kit's multi-line field joins the ONE field family (01/10/2026, Chris: "form control đồng bộ"):
487
+ same input-border token, hover and focus ring as `gemboss-field` and `.ui-select`. Before, it drew a
488
+ lighter --ui-border box at 14px with no focus ring, so on GemFactory's compose form a textarea sat
489
+ beside selects of a darker border and text fields of a third look (measured on prod). */
394
490
  .ui-textarea {
395
491
  width: 100%; min-height: 96px; resize: vertical; box-sizing: border-box;
396
- background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md);
397
- padding: 12px; color: var(--ui-text); font-size: 14px; font-family: inherit; outline: none;
492
+ background: var(--gemboss-field-bg);
493
+ border: 1px solid var(--gemboss-field-line); border-radius: var(--ui-radius-md);
494
+ padding: 12px; color: var(--gemboss-field-ink); font-size: var(--ui-fs-body); line-height: 1.5; font-family: inherit;
495
+ }
496
+ .ui-textarea::placeholder { color: var(--gemboss-field-placeholder); }
497
+ .ui-textarea:hover:not(:disabled) {
498
+ border-color: var(--gemboss-field-line-hover);
499
+ background-color: var(--gemboss-field-bg-hover);
500
+ }
501
+ .ui-textarea:focus { outline: none; }
502
+ .ui-textarea:focus-visible:not(:disabled) {
503
+ outline: 2px solid #005bd3; outline-offset: 1px;
504
+ border-color: var(--gemboss-field-line-active);
505
+ }
506
+ .ui-textarea:disabled { opacity: .5; cursor: not-allowed; }
507
+ /* On a PHONE-WIDTH screen every field of the family reads 16px (iOS zooms the page on tap below that)
508
+ and a select is a 44px target — by WIDTH, as `gemboss-field` does, because a desktop browser at 320px
509
+ still reports pointer: fine. Measured on GemFactory at 390px before this: select 13px, textarea 14px. */
510
+ @media (max-width: 47.9975em) {
511
+ .ui-select.ui-select, .ui-textarea { font-size: 16px; }
512
+ /* Height only for the full select: `--slim` is a density decision (table rows), as the coarse block keeps it. */
513
+ .ui-select:not(.ui-select--slim) { min-height: 44px; }
398
514
  }
399
- .ui-textarea:focus { border-color: var(--ui-accent); }
400
515
 
401
516
  /* ── Segmented control ────────────────────────────────────────────────────────
402
517
  Grouped toggle: icon squares (.ui-seg button) or text tabs (.is-text). The
403
518
  selected segment uses the accent-soft state. */
404
519
  .ui-seg { display: inline-flex; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); overflow: hidden; }
405
- .ui-seg button {
520
+ .ui-seg :is(button, a) {
406
521
  display: inline-grid; place-items: center; width: 36px; height: 36px;
407
522
  border: 0; background: transparent; color: var(--ui-text-muted); cursor: pointer; font-family: inherit;
408
523
  }
409
- .ui-seg button.is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
410
- .ui-seg button + button { border-left: 1px solid var(--ui-border); }
411
- .ui-seg button:hover { background: var(--ui-surface-2); color: var(--ui-text); }
524
+ .ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
525
+ .ui-seg :is(button, a) + :is(button, a) { border-left: 1px solid var(--ui-border); }
526
+ .ui-seg :is(button, a):hover { background: var(--ui-surface-2); color: var(--ui-text); }
527
+ /* Lọc / chế độ xem bằng LINK (trang server-render, chọn = đổi URL): `<a aria-current="page">` là ô đang chọn. */
528
+ .ui-seg a { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; color: var(--ui-text-muted); }
529
+ .ui-seg a[aria-current="page"],
412
530
  .ui-seg button.active { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
413
531
  .ui-seg svg { width: 16px; height: 16px; fill: currentColor; }
414
532
 
@@ -490,7 +608,9 @@
490
608
  decision, not an oversight; it gets its own measurement pass or none. */
491
609
  .ui-btn:not(.ui-btn--sm),
492
610
  .ui-stat.is-clickable,
493
- .ui-seg button { min-height: 44px; }
611
+ .ui-chips > a.ui-tag--chip,
612
+ .ui-pager > a,
613
+ .ui-seg :is(button, a) { min-height: 44px; }
494
614
  /* A clickable stat whose label is one short token ("7d" on /admin/gem-intelligence) measured 40x44
495
615
  in the 13/09/2026 sweep: the height floor held, the width did not. */
496
616
  .ui-stat.is-clickable { min-width: 44px; justify-content: center; }
@@ -499,7 +619,7 @@
499
619
  .ui-select.ui-select,
500
620
  .ui-select--slim.ui-select--slim { min-height: 44px; }
501
621
  /* The icon-only segment is 36 WIDE as well — a target is an area. */
502
- .ui-seg button { min-width: 44px; }
622
+ .ui-seg :is(button, a) { min-width: 44px; }
503
623
  }
504
624
 
505
625
  /* ── Skeleton ─────────────────────────────────────────────────────────────────
@@ -523,6 +643,46 @@
523
643
 
524
644
  }
525
645
 
646
+ /* ── Resizer: the seam you drag between two panels ────────────────────────────
647
+ One look for every split in the fleet: the admin's editors (chat | preview) today; made for gemcare's
648
+ inbox columns too (they adopt it at their next ui:sync). The element is the drag target; its GEOMETRY (a flex item, a grid edge, an absolutely
649
+ placed strip) belongs to the surface. While a drag runs the surface puts `ui-resizing` on <body>
650
+ (and may put `dragging` on the seam itself to keep its grip lit).
651
+ Markup: <div class="ui-resizer" role="separator" aria-orientation="vertical" tabindex="0"
652
+ aria-label="…" aria-valuenow aria-valuemin aria-valuemax>. Pointer events, not mouse events (touch
653
+ is a pointer); arrow keys move it — a focusable separator that ignores the keyboard is INERT. */
654
+ /* `touch-action: none` is what makes a FINGER drag reach this element at all. `preventDefault()` on
655
+ `pointerdown` does not stop panning: the browser hands the gesture to the nearest scroller and sends
656
+ `pointercancel`, so the seam jumps once and stops. */
657
+ .ui-resizer { cursor: col-resize; position: relative; touch-action: none; outline: none; }
658
+ /* The grip pill is invisible at rest (a clean seam) and fades in over the zone, on keyboard focus,
659
+ and while a drag runs. Focus shows the pill rather than a ring: on a 5px seam a ring is a smear. */
660
+ .ui-resizer::before {
661
+ content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
662
+ width: 4px; height: 44px; border-radius: var(--gemboss-radius-full);
663
+ background: var(--gemboss-primary);
664
+ opacity: 0;
665
+ transition: opacity 0.14s ease, height 0.14s ease;
666
+ }
667
+ .ui-resizer:hover::before,
668
+ .ui-resizer:focus-visible::before,
669
+ .ui-resizer.dragging::before,
670
+ body.ui-resizing .ui-resizer::before { opacity: 1; height: 52px; }
671
+ /* A FINGER NEEDS MORE THAN 5px, and the line must still look like 5px. WCAG 2.5.8 puts the floor for
672
+ this kind of target at 24px, so the HIT area grows and the visible seam does not — widening the
673
+ element itself would also move the columns. Only where the pointer is coarse. */
674
+ @media (pointer: coarse) {
675
+ .ui-resizer::after {
676
+ content: ""; position: absolute; top: 0; bottom: 0; left: -14px; right: -14px;
677
+ touch-action: none; /* the band must claim the gesture too, or the scroller still wins */
678
+ z-index: 2; /* above the panels either side, transparent */
679
+ }
680
+ }
681
+ body.ui-resizing, body.ui-resizing * { cursor: col-resize !important; user-select: none !important; }
682
+ /* While dragging, a full-viewport transparent shield sits above everything — iframes included — so the
683
+ pointer stream is never captured by a cross-document frame (the classic "stuck cursor"). */
684
+ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999; cursor: col-resize; }
685
+
526
686
  /* ── Tabs · Table · Empty state ───────────────────────────────────────────────
527
687
  Plain-CSS primitives for surfaces that are NOT React (gemcare's vanilla dashboard reads this
528
688
  file through `npm run export:ui-css`). The React app has Polaris <Tabs>/<DataTable>/<EmptyState>
@@ -537,7 +697,8 @@
537
697
  display: flex; gap: var(--ui-space-1); border-bottom: 1px solid var(--ui-border);
538
698
  overflow-x: auto; scrollbar-width: none;
539
699
  }
540
- .ui-tabs > button {
700
+ .ui-tabs > button,
701
+ .ui-tabs > a {
541
702
  position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
542
703
  min-height: 44px; padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
543
704
  border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;
@@ -545,9 +706,15 @@
545
706
  color: var(--ui-text-muted);
546
707
  transition: color var(--gemboss-transition-fast) ease, background-color var(--gemboss-transition-fast) ease;
547
708
  }
548
- .ui-tabs > button:hover { color: var(--ui-text); background: var(--ui-surface-2); }
549
- .ui-tabs > button:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
709
+ .ui-tabs > button:hover,
710
+ .ui-tabs > a:hover { color: var(--ui-text); background: var(--ui-surface-2); }
711
+ .ui-tabs > button:focus-visible,
712
+ .ui-tabs > a:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
713
+ .ui-tabs > a { text-decoration: none; }
714
+ /* Tab bằng link (mỗi tab là một URL): `<nav class="ui-tabs"><a aria-current="page">` — cùng hình với tab nút. */
715
+ .ui-tabs > a[aria-current="page"],
550
716
  .ui-tabs > button[aria-selected="true"] { color: var(--ui-text-strong); font-weight: 600; }
717
+ .ui-tabs > a[aria-current="page"]::after,
551
718
  .ui-tabs > button[aria-selected="true"]::after {
552
719
  content: ""; position: absolute; left: var(--ui-space-3); right: var(--ui-space-3); bottom: 0;
553
720
  height: 2px; border-radius: var(--gemboss-radius-full); background: var(--ui-accent);
@@ -573,6 +740,12 @@
573
740
  .ui-table tbody tr:last-child > td { border-bottom: 0; }
574
741
  .ui-table tbody tr:hover { background: var(--ui-surface-2); }
575
742
  .ui-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
743
+ /* Dòng phụ dưới nội dung chính của một ô (lớp, nguồn, ngày…): `<td><a>Loox</a><span class="ui-sub">Cộng sinh</span></td>`.
744
+ Trước đây mỗi surface tự viết `.muted` riêng trong ô bảng (partner-radar, gemwatcher). */
745
+ /* Link nằm thẳng trong ô (tên bản ghi dẫn sang trang chi tiết) là link trong DANH SÁCH: ≥ 24px (WCAG 2.5.8, §9.1).
746
+ Chữ 13px cho hộp 17px; visual-gate đo đỏ ở partner-radar /competitors 01/10/2026. inline-flex giữ nó trên dòng. */
747
+ .ui-table td > a { display: inline-flex; align-items: center; min-height: 24px; }
748
+ .ui-table .ui-sub { display: block; margin-top: 2px; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); font-weight: 400; }
576
749
 
577
750
  /* Empty state: `<div class="ui-empty"><p class="ui-empty__title">…</p><p class="ui-empty__body">…</p>
578
751
  <button class="ui-btn">…</button></div>`. Says what is missing and what to do next. */
@@ -618,6 +791,10 @@
618
791
  border: 1px solid var(--gemboss-field-line);
619
792
  border-radius: 8px;
620
793
  }
794
+ /* WebKit draws `input[type=search]` as `-webkit-appearance: searchfield`: its own padding and rounding, so in Safari
795
+ (macOS and iOS, the platform the 16px rule exists for) a search box did not match the fields beside it. Same box for
796
+ every text type; the clear button stays. (GemFactory #98 review, 01/10/2026.) */
797
+ input[type="search"]:is(.gemboss-field, .gemboss-field__bare) { -webkit-appearance: none; appearance: none; }
621
798
  .gemboss-field::placeholder { color: var(--gemboss-field-placeholder); }
622
799
  /* A NARROW field — the inspector's T/R/B/L boxes and its number spinners are 40-68px wide, and the
623
800
  shared 12px side padding left 38px of content box for a three-digit value (measured in Chromium:
@@ -732,7 +909,8 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
732
909
  z-index: 320;
733
910
  overflow: hidden;
734
911
  }
735
- .gemboss-account-menu-item {
912
+ .gemboss-account-menu-item,
913
+ .ui-menu__item {
736
914
  display: block;
737
915
  width: 100%;
738
916
  min-height: 44px;
@@ -747,15 +925,18 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
747
925
  cursor: pointer;
748
926
  transition: background var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
749
927
  }
750
- .gemboss-account-menu-item:hover {
928
+ .gemboss-account-menu-item:hover,
929
+ .ui-menu__item:hover {
751
930
  background: var(--gemboss-hover);
752
931
  }
753
- .gemboss-account-menu-item:focus-visible {
932
+ .gemboss-account-menu-item:focus-visible,
933
+ .ui-menu__item:focus-visible {
754
934
  outline: 2px solid var(--gemboss-primary);
755
935
  outline-offset: -2px;
756
936
  background: var(--gemboss-hover);
757
937
  }
758
- .gemboss-account-menu-item.danger {
938
+ .gemboss-account-menu-item.danger,
939
+ .ui-menu__item--danger {
759
940
  color: var(--gemboss-danger-strong);
760
941
  }
761
942
  .gemboss-account-chip {
@@ -2890,3 +3071,60 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
2890
3071
  border-radius: 0;
2891
3072
  }
2892
3073
  }
3074
+
3075
+ /* ═══ @gemboss/ui src/kit-plain.css ═══ */
3076
+ /* kit-plain.css — kit primitives that only the NON-React surfaces use (GemFactory, gemcare, gempress write the
3077
+ markup by hand). They ship inside gemboss-ui.css (scripts/build-css.mjs `own`), but the admin does NOT import
3078
+ this file: `ui.css` is loaded by the admin's root layout, so every rule there is paid on every admin screen,
3079
+ and adding these to it pushed the admin shell 0.2 KB over its first-load budget (CI of #3591, 01/10/2026).
3080
+ The admin has Polaris for menus, checkboxes and description lists. */
3081
+
3082
+ /* ── Icon button · Menu · Key-value list ──────────────────────────────────────
3083
+ Added 01/10/2026 for GemFactory's item dialog, which had seven actions in one header row; the
3084
+ secondary five now sit behind one "⋯" button. Plain CSS, same contract as the tabs and tables in ui.css.
3085
+
3086
+ Icon button: `<button class="ui-btn ui-btn--icon" aria-label="…"><svg…></button>` — square, the
3087
+ label is REQUIRED (there is no text for a screen reader to read otherwise).
3088
+
3089
+ Menu: a `<details>` so it opens with no script. `<details class="ui-menu ui-menu--end">
3090
+ <summary class="ui-btn ui-btn--icon" aria-label="More actions">…</summary><div class="ui-menu__list">
3091
+ <button class="ui-menu__item">Copy #12<span class="ui-menu__hint">for Claude Code</span></button>
3092
+ <hr class="ui-menu__sep"><button class="ui-menu__item ui-menu__item--danger">Delete…</button></div>
3093
+ </details>`. `--end` aligns the list to the right edge of its button. Closing on an outside click,
3094
+ on an item, and on Escape is the page's job (a few lines; GemFactory's ui-kit.mjs has them).
3095
+ Items are 44px under EVERY pointer, not only coarse: they touch each other (0px apart), and §9.1
3096
+ only allows a gap under 8px between targets that are already 44px.
3097
+
3098
+ Key-value list: `<dl class="ui-kv"><dt>Owner</dt><dd>Lily</dd>…</dl>` — label column sized to
3099
+ its longest label, value column takes the rest and wraps long keys and URLs. */
3100
+ .ui-btn--icon { flex: none; width: var(--ui-control-h); padding: 0; }
3101
+ .ui-btn--icon.ui-btn--sm { width: 28px; }
3102
+ .ui-btn--icon svg { width: 18px; height: 18px; }
3103
+ /* Menu: the SURFACE is the shell's `gemboss-popover-surface` and the rows are the account menu's row (shell.css
3104
+ declares `.gemboss-account-menu-item, .ui-menu__item` together), so a kit menu and the account menu on one page
3105
+ look the same and a fix to one reaches the other (§9.3). Markup: `<div class="ui-menu__list gemboss-popover-surface">`.
3106
+ Here: only placement, the hint, the separator and the disabled state. */
3107
+ .ui-menu { position: relative; display: inline-block; }
3108
+ .ui-menu > summary { list-style: none; cursor: pointer; }
3109
+ .ui-menu > summary::-webkit-details-marker { display: none; }
3110
+ .ui-menu__list { position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; display: grid; min-width: 220px; padding: var(--ui-space-1); box-sizing: border-box; }
3111
+ .ui-menu--end .ui-menu__list { left: auto; right: 0; }
3112
+ .ui-menu__item { display: flex; align-items: center; gap: var(--ui-space-2); white-space: nowrap; border-radius: var(--ui-radius-sm); }
3113
+ .ui-menu__item:is([disabled], [aria-disabled="true"]) { opacity: .5; cursor: not-allowed; pointer-events: none; }
3114
+ .ui-menu__hint { margin-left: auto; padding-left: var(--ui-space-4); color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }
3115
+ .ui-menu__sep { height: 1px; margin: var(--ui-space-1) 0; border: 0; background: var(--ui-border); }
3116
+ /* Checkbox: the LABEL is the target; disabled turns both the label and the box to not-allowed. Focus ring in the
3117
+ kit's own token (no --p-* here: this file is appended after the export's Polaris resolver runs). */
3118
+ .ui-check { display: inline-flex; align-items: center; gap: var(--ui-space-2); min-height: 44px; cursor: pointer; font-size: var(--ui-fs-body); color: var(--ui-text); }
3119
+ .ui-check > input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); cursor: inherit; }
3120
+ .ui-check > input:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
3121
+ .ui-check:has(> input:disabled) { opacity: .5; cursor: not-allowed; }
3122
+ .ui-label { display: block; margin: 0 0 var(--ui-space-1); font-size: var(--ui-fs-caption); font-weight: 600; color: var(--ui-text-muted); }
3123
+ .ui-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--ui-space-2) var(--ui-space-4); margin: 0; font-size: var(--ui-fs-body); }
3124
+ .ui-kv > dt { color: var(--ui-text-muted); }
3125
+ .ui-kv > dd { margin: 0; min-width: 0; overflow-wrap: anywhere; color: var(--ui-text); }
3126
+
3127
+ /* Under a thumb the icon-only button is 44 WIDE as well (the kit's coarse block already raises every .ui-btn to 44 tall). */
3128
+ @media (pointer: coarse) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; } }
3129
+ /* …and at phone WIDTH, as gemboss-field does: a desktop browser at 320px still reports pointer: fine. */
3130
+ @media (max-width: 47.9975em) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; min-height: 44px; } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/ui",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
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",
@@ -18,6 +18,7 @@
18
18
  "./app-shell.css": "./src/app-shell.css",
19
19
  "./shell.css": "./src/shell.css",
20
20
  "./auth.css": "./src/auth.css",
21
+ "./kit-plain.css": "./src/kit-plain.css",
21
22
  "./assistant": "./src/assistant/index.ts",
22
23
  "./assistant/dock": "./src/assistant/dock.ts",
23
24
  "./assistant.css": "./src/assistant.css"
@@ -35,7 +36,8 @@
35
36
  "build:css": "node scripts/build-css.mjs",
36
37
  "prepack": "node scripts/build-css.mjs && node scripts/build-catalog.mjs",
37
38
  "typecheck": "tsc -p tsconfig.json --noEmit",
38
- "catalog": "node scripts/build-css.mjs && node scripts/build-catalog.mjs"
39
+ "catalog": "node scripts/build-css.mjs && node scripts/build-catalog.mjs",
40
+ "fleet": "node scripts/fleet.mjs"
39
41
  },
40
42
  "peerDependencies": {
41
43
  "react": ">=18"
@@ -30,6 +30,10 @@
30
30
  border: 1px solid var(--p-color-input-border, var(--gemboss-field-line));
31
31
  border-radius: var(--p-border-radius-200, 8px);
32
32
  }
33
+ /* WebKit draws `input[type=search]` as `-webkit-appearance: searchfield`: its own padding and rounding, so in Safari
34
+ (macOS and iOS, the platform the 16px rule exists for) a search box did not match the fields beside it. Same box for
35
+ every text type; the clear button stays. (GemFactory #98 review, 01/10/2026.) */
36
+ input[type="search"]:is(.gemboss-field, .gemboss-field__bare) { -webkit-appearance: none; appearance: none; }
33
37
  .gemboss-field::placeholder { color: var(--p-color-text-secondary, var(--gemboss-field-placeholder)); }
34
38
  /* A NARROW field — the inspector's T/R/B/L boxes and its number spinners are 40-68px wide, and the
35
39
  shared 12px side padding left 38px of content box for a three-digit value (measured in Chromium:
package/src/icons.tsx CHANGED
@@ -54,6 +54,7 @@ const P = {
54
54
  check: <path d="M4 10.5 8 14.5 16 5.5" />,
55
55
  plus: <path d="M10 4v12M4 10h12" />,
56
56
  close: <path d="M5 5l10 10M15 5 5 15" />,
57
+ more: <><circle cx="4.5" cy="10" r="1" /><circle cx="10" cy="10" r="1" /><circle cx="15.5" cy="10" r="1" /></>,
57
58
  utensils: <><path d="M6 3v5a2 2 0 0 0 4 0V3M8 3v14" /><path d="M15 3c-1.6 1.4-2.5 3.6-2.5 6.5H15V17" /></>,
58
59
  signOut: <path d="M8 3.5H4v13h4M12.5 6.5 16 10l-3.5 3.5M16 10H8" />,
59
60
  } satisfies Record<string, React.ReactNode>;
package/src/index.ts CHANGED
@@ -6,4 +6,5 @@ export { GemMark } from "./GemMark";
6
6
  export { AuthError, AuthField, AuthFoot, AuthForm, AuthHelp, AuthLayout, PasswordField, AuthSubmit } from "./auth";
7
7
  export { BrandLockup } from "./brand";
8
8
  export { AccountMenu, AccountMenuItem, initialsOf } from "./account";
9
+ export { Menu, MenuItem, MenuSeparator } from "./menu";
9
10
  export { Icon, ICON_NAMES, type IconName } from "./icons";
@@ -0,0 +1,55 @@
1
+ /* kit-plain.css — kit primitives that only the NON-React surfaces use (GemFactory, gemcare, gempress write the
2
+ markup by hand). They ship inside gemboss-ui.css (scripts/build-css.mjs `own`), but the admin does NOT import
3
+ this file: `ui.css` is loaded by the admin's root layout, so every rule there is paid on every admin screen,
4
+ and adding these to it pushed the admin shell 0.2 KB over its first-load budget (CI of #3591, 01/10/2026).
5
+ The admin has Polaris for menus, checkboxes and description lists. */
6
+
7
+ /* ── Icon button · Menu · Key-value list ──────────────────────────────────────
8
+ Added 01/10/2026 for GemFactory's item dialog, which had seven actions in one header row; the
9
+ secondary five now sit behind one "⋯" button. Plain CSS, same contract as the tabs and tables in ui.css.
10
+
11
+ Icon button: `<button class="ui-btn ui-btn--icon" aria-label="…"><svg…></button>` — square, the
12
+ label is REQUIRED (there is no text for a screen reader to read otherwise).
13
+
14
+ Menu: a `<details>` so it opens with no script. `<details class="ui-menu ui-menu--end">
15
+ <summary class="ui-btn ui-btn--icon" aria-label="More actions">…</summary><div class="ui-menu__list">
16
+ <button class="ui-menu__item">Copy #12<span class="ui-menu__hint">for Claude Code</span></button>
17
+ <hr class="ui-menu__sep"><button class="ui-menu__item ui-menu__item--danger">Delete…</button></div>
18
+ </details>`. `--end` aligns the list to the right edge of its button. Closing on an outside click,
19
+ on an item, and on Escape is the page's job (a few lines; GemFactory's ui-kit.mjs has them).
20
+ Items are 44px under EVERY pointer, not only coarse: they touch each other (0px apart), and §9.1
21
+ only allows a gap under 8px between targets that are already 44px.
22
+
23
+ Key-value list: `<dl class="ui-kv"><dt>Owner</dt><dd>Lily</dd>…</dl>` — label column sized to
24
+ its longest label, value column takes the rest and wraps long keys and URLs. */
25
+ .ui-btn--icon { flex: none; width: var(--ui-control-h); padding: 0; }
26
+ .ui-btn--icon.ui-btn--sm { width: 28px; }
27
+ .ui-btn--icon svg { width: 18px; height: 18px; }
28
+ /* Menu: the SURFACE is the shell's `gemboss-popover-surface` and the rows are the account menu's row (shell.css
29
+ declares `.gemboss-account-menu-item, .ui-menu__item` together), so a kit menu and the account menu on one page
30
+ look the same and a fix to one reaches the other (§9.3). Markup: `<div class="ui-menu__list gemboss-popover-surface">`.
31
+ Here: only placement, the hint, the separator and the disabled state. */
32
+ .ui-menu { position: relative; display: inline-block; }
33
+ .ui-menu > summary { list-style: none; cursor: pointer; }
34
+ .ui-menu > summary::-webkit-details-marker { display: none; }
35
+ .ui-menu__list { position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; display: grid; min-width: 220px; padding: var(--ui-space-1); box-sizing: border-box; }
36
+ .ui-menu--end .ui-menu__list { left: auto; right: 0; }
37
+ .ui-menu__item { display: flex; align-items: center; gap: var(--ui-space-2); white-space: nowrap; border-radius: var(--ui-radius-sm); }
38
+ .ui-menu__item:is([disabled], [aria-disabled="true"]) { opacity: .5; cursor: not-allowed; pointer-events: none; }
39
+ .ui-menu__hint { margin-left: auto; padding-left: var(--ui-space-4); color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }
40
+ .ui-menu__sep { height: 1px; margin: var(--ui-space-1) 0; border: 0; background: var(--ui-border); }
41
+ /* Checkbox: the LABEL is the target; disabled turns both the label and the box to not-allowed. Focus ring in the
42
+ kit's own token (no --p-* here: this file is appended after the export's Polaris resolver runs). */
43
+ .ui-check { display: inline-flex; align-items: center; gap: var(--ui-space-2); min-height: 44px; cursor: pointer; font-size: var(--ui-fs-body); color: var(--ui-text); }
44
+ .ui-check > input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); cursor: inherit; }
45
+ .ui-check > input:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
46
+ .ui-check:has(> input:disabled) { opacity: .5; cursor: not-allowed; }
47
+ .ui-label { display: block; margin: 0 0 var(--ui-space-1); font-size: var(--ui-fs-caption); font-weight: 600; color: var(--ui-text-muted); }
48
+ .ui-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--ui-space-2) var(--ui-space-4); margin: 0; font-size: var(--ui-fs-body); }
49
+ .ui-kv > dt { color: var(--ui-text-muted); }
50
+ .ui-kv > dd { margin: 0; min-width: 0; overflow-wrap: anywhere; color: var(--ui-text); }
51
+
52
+ /* Under a thumb the icon-only button is 44 WIDE as well (the kit's coarse block already raises every .ui-btn to 44 tall). */
53
+ @media (pointer: coarse) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; } }
54
+ /* …and at phone WIDTH, as gemboss-field does: a desktop browser at 320px still reports pointer: fine. */
55
+ @media (max-width: 47.9975em) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; min-height: 44px; } }
package/src/menu.tsx ADDED
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ // menu.tsx: the kit's action menu ("⋯") — a `<details>` disclosure, so it opens with no JavaScript and a
4
+ // server-rendered surface (GemFactory, gemcare) can print it as plain markup. The surface is the shell's
5
+ // `gemboss-popover-surface` and the rows share the account menu's row (shell.css), so every menu in the
6
+ // fleet looks the same (§9.3). Where React hydrates, the menu closes itself like AccountMenu does: outside
7
+ // click, Escape (focus back on the trigger), and a button row after its handler (review #3591: a React page
8
+ // had no clean way to close it). The catalog renders its example from these components.
9
+ import React, { useEffect, useRef } from "react";
10
+ import { Icon } from "./icons";
11
+
12
+ /** The trigger is an icon button (`label` is its accessible name). `end` aligns the list to the right edge. */
13
+ export function Menu({ label, icon, end, children, className }: {
14
+ label: string;
15
+ icon?: React.ReactNode;
16
+ end?: boolean;
17
+ children?: React.ReactNode;
18
+ className?: string;
19
+ }) {
20
+ const ref = useRef<HTMLDetailsElement>(null);
21
+ useEffect(() => {
22
+ const el = ref.current;
23
+ if (!el) return undefined;
24
+ const onDoc = (e: MouseEvent) => { if (el.open && !el.contains(e.target as Node)) el.open = false; };
25
+ const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && el.open) { el.open = false; el.querySelector("summary")?.focus(); } };
26
+ document.addEventListener("click", onDoc);
27
+ document.addEventListener("keydown", onKey);
28
+ return () => { document.removeEventListener("click", onDoc); document.removeEventListener("keydown", onKey); };
29
+ }, []);
30
+ return (
31
+ <details ref={ref} className={["ui-menu", end ? "ui-menu--end" : "", className ?? ""].filter(Boolean).join(" ")}>
32
+ <summary className="ui-btn ui-btn--icon" aria-label={label} title={label}>{icon ?? <Icon name="more" />}</summary>
33
+ <div className="ui-menu__list gemboss-popover-surface">{children}</div>
34
+ </details>
35
+ );
36
+ }
37
+
38
+ /** One row: a link (`href`) or a button. `hint` is the quiet text on the right; `danger` for destructive rows.
39
+ * A disabled link is not a link: no href, aria-disabled, out of the tab order (an `<a disabled>` still follows
40
+ * on Enter). A caller's `className` is added to the row's class, not put in its place. */
41
+ export function MenuItem({ href, danger, hint, children, className, disabled, onClick, ...rest }: {
42
+ href?: string;
43
+ danger?: boolean;
44
+ hint?: React.ReactNode;
45
+ children?: React.ReactNode;
46
+ disabled?: boolean;
47
+ onClick?: React.MouseEventHandler<HTMLElement>;
48
+ } & Omit<React.HTMLAttributes<HTMLElement>, "onClick"> & Pick<React.AnchorHTMLAttributes<HTMLAnchorElement>, "target" | "rel">) {
49
+ const cls = ["ui-menu__item", danger ? "ui-menu__item--danger" : "", className ?? ""].filter(Boolean).join(" ");
50
+ const body = <>{children}{hint ? <span className="ui-menu__hint">{hint}</span> : null}</>;
51
+ if (href) {
52
+ if (disabled) return <a className={cls} aria-disabled="true" tabIndex={-1} {...rest}>{body}</a>;
53
+ return <a className={cls} href={href} onClick={onClick} {...rest}>{body}</a>;
54
+ }
55
+ const close: React.MouseEventHandler<HTMLButtonElement> = (e) => {
56
+ onClick?.(e);
57
+ e.currentTarget.closest("details")?.removeAttribute("open");
58
+ };
59
+ return <button type="button" className={cls} disabled={disabled} onClick={close} {...rest}>{body}</button>;
60
+ }
61
+
62
+ export function MenuSeparator() {
63
+ return <hr className="ui-menu__sep" />;
64
+ }
package/src/shell.css CHANGED
@@ -100,20 +100,24 @@
100
100
  /* Account dropdown menu items (avatar chip) — CSS-driven hover/focus, replaces inline JS handlers. */
101
101
  /* min-height 44 (28/09/2026): 12px padding around a 13px line measured 42.84px — under the 44px control
102
102
  floor (CLAUDE.md §9.1), in the admin's own menu and in every surface wearing it through @gemboss/ui. */
103
- .gemboss-account-menu-item {
103
+ .gemboss-account-menu-item,
104
+ .ui-menu__item {
104
105
  display: block; width: 100%; min-height: 44px; padding: 12px 16px; border: 0; background: transparent;
105
106
  font: inherit; font-size: 13px; color: var(--gemboss-text); text-align: left; text-decoration: none;
106
107
  cursor: pointer; transition: background var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
107
108
  }
108
109
 
109
- .gemboss-account-menu-item:hover { background: var(--gemboss-hover); }
110
+ .gemboss-account-menu-item:hover,
111
+ .ui-menu__item:hover { background: var(--gemboss-hover); }
110
112
 
111
- .gemboss-account-menu-item:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; background: var(--gemboss-hover); }
113
+ .gemboss-account-menu-item:focus-visible,
114
+ .ui-menu__item:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; background: var(--gemboss-hover); }
112
115
 
113
116
  /* `--gemboss-danger` var(--gemboss-danger) is 4.25:1 on `--gemboss-hover` #eceef2, and this row takes that hover
114
117
  background two lines above — so the one moment the item is under the cursor is the moment its label
115
118
  drops below AA. `--gemboss-danger-strong` #b42318 is 5.66:1 there and 7.30:1 on white. */
116
- .gemboss-account-menu-item.danger { color: var(--gemboss-danger-strong); }
119
+ .gemboss-account-menu-item.danger,
120
+ .ui-menu__item--danger { color: var(--gemboss-danger-strong); }
117
121
 
118
122
  .gemboss-account-chip {
119
123
  width: 30px;