@gemboss/ui 0.1.0 → 0.2.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 CHANGED
@@ -109,6 +109,36 @@ Its styles are the `gemboss-assistant-*` rules in `packages/ui/src/shell.css` (t
109
109
  @imports too), and they reach you through `gemboss-ui.css` together with `gemboss-field.css`. The admin passes a second tab ("Talk to a person", gemcare) through `secondTab` and its button
110
110
  through `fullActions`; a surface without those leaves them out.
111
111
 
112
+ ## Resizable panels
113
+
114
+ `ui-resizer` is the seam a person drags between two panels; `ui-resizing` on `<body>` is the drag in
115
+ progress (`dragging` on the seam itself is optional and keeps its grip lit). The admin's four editors
116
+ (chat | preview) use them; they are made for gemcare's inbox columns too, which adopt them at their
117
+ next `ui:sync`. The CSS owns the LOOK: a grip that appears on hover, on
118
+ keyboard focus and while dragging; a 24px finger band on coarse pointers that keeps the visible seam
119
+ thin; and a full-page shield while dragging, so an iframe never swallows the pointer.
120
+
121
+ ```html
122
+ <div class="ui-resizer" role="separator" aria-orientation="vertical" tabindex="0"
123
+ aria-label="Conversation list width" aria-valuenow="340" aria-valuemin="260" aria-valuemax="520"></div>
124
+ ```
125
+
126
+ The surface owns where the seam sits and what it does. Five rules, each paid for once in the admin
127
+ (`apps/admin/src/components/shell/useResizableChatPanel.ts` holds the measurements):
128
+
129
+ 1. **Pointer events, not mouse events.** `mousedown` never fires for a finger.
130
+ 2. **Keyboard.** A focusable separator that ignores keys is a control that does nothing: arrows move
131
+ it, Home / End go to the floor and the ceiling. Leave Alt / Cmd / Ctrl + arrow to the browser
132
+ (Back, Forward), and say the REAL ceiling in `aria-valuemax`, not the constant.
133
+ 3. **Clamp to the box, not to a constant.** A width saved on a 1920px screen follows the person to a
134
+ 1280px one. The ceiling is what the box can spare after the other panel's floor.
135
+ 4. **Release on `pointerup`, `pointercancel` and window `blur`.** A missed release leaves the shield
136
+ over the page and nothing can be clicked.
137
+ 5. **Remember per browser** (localStorage), and recompute the clamp when the box changes size.
138
+
139
+ React surfaces: `useResizableChatPanel` in the admin is the reference. Others write the same markup and
140
+ the five rules by hand; the catalog's "Thanh kéo giữa hai cột" shows the markup.
141
+
112
142
  ## The topbar kit: lockup, account, icons
113
143
 
114
144
  What goes INTO the frame, so a surface does not draw its own (28/09/2026: GemFactory's rail icons came
@@ -205,4 +235,22 @@ Publish from a worktree at the MERGED commit on `main`, not from a branch, so th
205
235
  to publish (403) from an account without two-factor auth; the account needs 2FA set to
206
236
  "Authorization and publishing".
207
237
 
238
+ The account's second factor is a passkey, so run the publish in a real terminal as
239
+ `npm publish --auth-type=web` and approve the page it opens (each approval page lives 5 minutes).
240
+ Without a terminal npm answers `EOTP` instead of offering the page. After `+ @gemboss/ui@x.y.z`
241
+ npm says the package "is being processed": `npm view` returns 404 for a few more minutes (0.1.1,
242
+ 30/09/2026). Publishing from CI with npm trusted publishing needs a GitHub-hosted runner, and this
243
+ repo's are blocked by the Actions budget since 12/09/2026 (see `admin-freshness.yml`).
244
+
208
245
  Consumers pin a version; a bump is a PR in their repo, not a surprise.
246
+
247
+ ## Who is on which version
248
+
249
+ ```bash
250
+ npm run fleet --prefix packages/ui # every surface vs the npm release (reads their repos with `gh`)
251
+ node packages/ui/scripts/fleet.mjs --release-only # main vs npm only; CI runs this as a warning
252
+ ```
253
+
254
+ Compared by content: sha256 of `gemboss-ui.css` minus its `gemboss commit:` line, so a gemboss commit
255
+ that leaves the design system alone does not make everyone look stale. Add a row to `CONSUMERS` in
256
+ `packages/ui/scripts/fleet.mjs` when a surface starts using the package.
@@ -26,8 +26,8 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
26
26
 
27
27
  @media (max-width:760px){.cat{grid-template-columns:1fr;padding:16px}.cat-nav{position:static;flex-direction:row;flex-wrap:wrap}}
28
28
  </style></head><body>
29
- <div class="cat"><nav class="cat-nav" aria-label="Mục"><a href="#tokens">Token</a><a href="#buttons">Nút</a><a href="#fields">Ô nhập</a><a href="#choice">Tab, nhóm nút, thẻ</a><a href="#surfaces">Thẻ, số liệu, bảng, trạng thái trống</a><a href="#modal">Hộp thoại</a><a href="#shell">Khung app</a><a href="#auth">Đăng nhập</a><a href="#assistant">Trợ lý: cột phiên</a><a href="#icons">Icon</a></nav>
30
- <main><header class="cat-head"><h1>@gemboss/ui 0.1.0</h1><p>Ngôn ngữ thiết kế chung của các công cụ GemBoss. Markup dưới mỗi ví dụ là đúng thứ package render ra; bề mặt không dùng React (HTML tĩnh, Python) chép markup này và nạp <code>gemboss-ui.css</code>. Dựng từ gemboss <code>cf59218eb5d0</code>.</p></header>
29
+ <div class="cat"><nav class="cat-nav" aria-label="Mục"><a href="#tokens">Token</a><a href="#buttons">Nút</a><a href="#fields">Ô nhập</a><a href="#choice">Tab, nhóm nút, thẻ</a><a href="#surfaces">Thẻ, số liệu, bảng, trạng thái trống</a><a href="#resizer">Thanh kéo giữa hai cột</a><a href="#modal">Hộp thoại</a><a href="#shell">Khung app</a><a href="#auth">Đăng nhập</a><a href="#assistant">Trợ lý: cột phiên</a><a href="#icons">Icon</a></nav>
30
+ <main><header class="cat-head"><h1>@gemboss/ui 0.2.0</h1><p>Ngôn ngữ thiết kế chung của các công cụ GemBoss. Markup dưới mỗi ví dụ là đúng thứ package render ra; bề mặt không dùng React (HTML tĩnh, Python) chép markup này và nạp <code>gemboss-ui.css</code>. Dựng từ gemboss <code>62b85f8a0c75</code>.</p></header>
31
31
  <section id="tokens"><h2>Token</h2><p>Đổi giao diện một bề mặt bằng cách đặt lại token, không viết đè class. Tên và giá trị đọc thẳng từ gemboss-ui.css.</p><div class="cat-swatches"><div class="cat-swatch"><i style="background:var(--gemboss-primary)"></i><code>--gemboss-primary</code><span>#1b1c1e</span></div><div class="cat-swatch"><i style="background:var(--gemboss-primary-dark)"></i><code>--gemboss-primary-dark</code><span>#000000</span></div><div class="cat-swatch"><i style="background:var(--gemboss-primary-soft)"></i><code>--gemboss-primary-soft</code><span>#f2f3f5</span></div><div class="cat-swatch"><i style="background:var(--gemboss-accent)"></i><code>--gemboss-accent</code><span>#5661f5</span></div><div class="cat-swatch"><i style="background:var(--gemboss-accent-strong)"></i><code>--gemboss-accent-strong</code><span>#3a44de</span></div><div class="cat-swatch"><i style="background:var(--gemboss-accent-soft)"></i><code>--gemboss-accent-soft</code><span>#eef0fe</span></div><div class="cat-swatch"><i style="background:var(--gemboss-text-strong)"></i><code>--gemboss-text-strong</code><span>#1b1c1e</span></div><div class="cat-swatch"><i style="background:var(--gemboss-text)"></i><code>--gemboss-text</code><span>#1b1c1e</span></div><div class="cat-swatch"><i style="background:var(--gemboss-text-muted)"></i><code>--gemboss-text-muted</code><span>#686b71</span></div><div class="cat-swatch"><i style="background:var(--gemboss-surface)"></i><code>--gemboss-surface</code><span>#ffffff</span></div><div class="cat-swatch"><i style="background:var(--gemboss-canvas)"></i><code>--gemboss-canvas</code><span>#ffffff</span></div><div class="cat-swatch"><i style="background:var(--gemboss-preview-canvas)"></i><code>--gemboss-preview-canvas</code><span>#eceef1</span></div><div class="cat-swatch"><i style="background:var(--gemboss-rail-bg)"></i><code>--gemboss-rail-bg</code><span>#ffffff</span></div><div class="cat-swatch"><i style="background:var(--gemboss-panel-bg)"></i><code>--gemboss-panel-bg</code><span>#ffffff</span></div><div class="cat-swatch"><i style="background:var(--gemboss-divider)"></i><code>--gemboss-divider</code><span>#e5e7ea</span></div><div class="cat-swatch"><i style="background:var(--gemboss-divider-soft)"></i><code>--gemboss-divider-soft</code><span>#edeff2</span></div><div class="cat-swatch"><i style="background:var(--gemboss-hover)"></i><code>--gemboss-hover</code><span>#eceef2</span></div><div class="cat-swatch"><i style="background:var(--gemboss-active-bg)"></i><code>--gemboss-active-bg</code><span>#f0f1f4</span></div><div class="cat-swatch"><i style="background:var(--gemboss-active-fg)"></i><code>--gemboss-active-fg</code><span>#1b1c1e</span></div><div class="cat-swatch"><i style="background:var(--gemboss-danger)"></i><code>--gemboss-danger</code><span>#d72c0d</span></div><div class="cat-swatch"><i style="background:var(--gemboss-danger-strong)"></i><code>--gemboss-danger-strong</code><span>#b42318</span></div><div class="cat-swatch"><i style="background:var(--gemboss-danger-soft)"></i><code>--gemboss-danger-soft</code><span>#fff0e9</span></div><div class="cat-swatch"><i style="background:var(--gemboss-danger-border)"></i><code>--gemboss-danger-border</code><span>#f5c9bc</span></div><div class="cat-swatch"><i style="background:var(--gemboss-success)"></i><code>--gemboss-success</code><span>#007f5f</span></div><div class="cat-swatch"><i style="background:var(--gemboss-success-strong)"></i><code>--gemboss-success-strong</code><span>#0a6a4c</span></div><div class="cat-swatch"><i style="background:var(--gemboss-success-soft)"></i><code>--gemboss-success-soft</code><span>#e3f1df</span></div><div class="cat-swatch"><i style="background:var(--gemboss-success-border)"></i><code>--gemboss-success-border</code><span>#b7dfcd</span></div><div class="cat-swatch"><i style="background:var(--gemboss-warning)"></i><code>--gemboss-warning</code><span>#9a6700</span></div><div class="cat-swatch"><i style="background:var(--gemboss-warning-soft)"></i><code>--gemboss-warning-soft</code><span>#fff8e0</span></div><div class="cat-swatch"><i style="background:var(--gemboss-warning-border)"></i><code>--gemboss-warning-border</code><span>#f5d569</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-bg)"></i><code>--gemboss-dark-bg</code><span>#0a0a0c</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-rail)"></i><code>--gemboss-dark-rail</code><span>#0e0e11</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-surface)"></i><code>--gemboss-dark-surface</code><span>#16161a</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-surface-2)"></i><code>--gemboss-dark-surface-2</code><span>#1e1e24</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-divider)"></i><code>--gemboss-dark-divider</code><span>#2a2a31</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-text)"></i><code>--gemboss-dark-text</code><span>#ededf0</span></div><div class="cat-swatch"><i style="background:var(--gemboss-dark-muted)"></i><code>--gemboss-dark-muted</code><span>#9a9aa4</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-bg)"></i><code>--gemboss-field-bg</code><span>#fdfdfd</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-bg-hover)"></i><code>--gemboss-field-bg-hover</code><span>#fafafa</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-ink)"></i><code>--gemboss-field-ink</code><span>#303030</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-placeholder)"></i><code>--gemboss-field-placeholder</code><span>#616161</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-line)"></i><code>--gemboss-field-line</code><span>#8a8a8a</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-line-hover)"></i><code>--gemboss-field-line-hover</code><span>#616161</span></div><div class="cat-swatch"><i style="background:var(--gemboss-field-line-active)"></i><code>--gemboss-field-line-active</code><span>#1a1a1a</span></div><div class="cat-swatch"><i style="background:var(--gemboss-chip-ink)"></i><code>--gemboss-chip-ink</code><span>#303633</span></div><div class="cat-swatch"><i style="background:var(--gemboss-chip-line)"></i><code>--gemboss-chip-line</code><span>#cfd6ce</span></div><div class="cat-swatch"><i style="background:var(--gemboss-chip-line-hover)"></i><code>--gemboss-chip-line-hover</code><span>#aab4ad</span></div><div class="cat-swatch"><i style="background:var(--gemboss-chip-bg-hover)"></i><code>--gemboss-chip-bg-hover</code><span>#f6f7f5</span></div><div class="cat-swatch"><i style="background:var(--gemboss-on-primary)"></i><code>--gemboss-on-primary</code><span>#ffffff</span></div><div class="cat-swatch"><i style="background:var(--gemboss-app-bg)"></i><code>--gemboss-app-bg</code><span>#ffffff</span></div></div><table class="ui-table cat-scale"><thead><tr><th>Token</th><th>Giá trị</th><th>Mẫu</th></tr></thead><tbody><tr><td><code>--gemboss-fs-micro</code></td><td>11px</td><td style="font-size:var(--gemboss-fs-micro)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-caption</code></td><td>12px</td><td style="font-size:var(--gemboss-fs-caption)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-body</code></td><td>14px</td><td style="font-size:var(--gemboss-fs-body)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-body-lg</code></td><td>16px</td><td style="font-size:var(--gemboss-fs-body-lg)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-h3</code></td><td>16px</td><td style="font-size:var(--gemboss-fs-h3)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-h2</code></td><td>20px</td><td style="font-size:var(--gemboss-fs-h2)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-h1</code></td><td>24px</td><td style="font-size:var(--gemboss-fs-h1)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-fs-display</code></td><td>30px</td><td style="font-size:var(--gemboss-fs-display)">Ngôn ngữ thiết kế chung</td></tr><tr><td><code>--gemboss-radius-xs</code></td><td>4px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-xs)"></i></td></tr><tr><td><code>--gemboss-radius-sm</code></td><td>6px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-sm)"></i></td></tr><tr><td><code>--gemboss-radius-md</code></td><td>8px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-md)"></i></td></tr><tr><td><code>--gemboss-radius-lg</code></td><td>12px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-lg)"></i></td></tr><tr><td><code>--gemboss-radius-xl</code></td><td>16px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-xl)"></i></td></tr><tr><td><code>--gemboss-radius-2xl</code></td><td>20px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-2xl)"></i></td></tr><tr><td><code>--gemboss-radius-full</code></td><td>999px</td><td><i class="cat-radius" style="border-radius:var(--gemboss-radius-full)"></i></td></tr><tr><td><code>--gemboss-shadow-resting</code></td><td class="cat-small">0 1px 2px rgba(31, 30, 29, 0.04), 0 2px 6px rgba(31, 30, 29, 0.06)</td><td><i class="cat-radius" style="box-shadow:var(--gemboss-shadow-resting);border:0"></i></td></tr><tr><td><code>--gemboss-shadow-raised</code></td><td class="cat-small">0 4px 16px rgba(31, 30, 29, 0.10)</td><td><i class="cat-radius" style="box-shadow:var(--gemboss-shadow-raised);border:0"></i></td></tr><tr><td><code>--gemboss-shadow-overlay</code></td><td class="cat-small">0 16px 48px rgba(31, 30, 29, 0.18)</td><td><i class="cat-radius" style="box-shadow:var(--gemboss-shadow-overlay);border:0"></i></td></tr></tbody></table></section>
32
32
  <section id="buttons"><h2>Nút</h2><p>Một lớp <code>ui-btn</code>, ba biến thể, một cỡ nhỏ. Dùng được trên <code>&lt;button&gt;</code> lẫn <code>&lt;a&gt;</code> (căn giữa sẵn).</p><figure class="cat-demo"><figcaption>Mặc định · chính · ghost · nhỏ · tắt</figcaption><div class="cat-live"><div class="cat-row"><button type="button" class="ui-btn">Lưu nháp</button><button type="button" class="ui-btn ui-btn--primary">Xuất bản</button><button type="button" class="ui-btn ui-btn--ghost">Huỷ</button><button type="button" class="ui-btn ui-btn--sm">Nhỏ</button><button type="button" class="ui-btn ui-btn--primary" disabled>Đang lưu…</button><a class="ui-btn" href="#buttons">Link dạng nút</a></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;cat-row&quot;&gt;
33
33
  &lt;button type=&quot;button&quot; class=&quot;ui-btn&quot;&gt;Lưu nháp&lt;/button&gt;
@@ -70,15 +70,93 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
70
70
  &lt;span class=&quot;ui-tag ui-tag--accent&quot;&gt;Đã duyệt&lt;/span&gt;
71
71
  &lt;span class=&quot;ui-tag ui-tag--chip&quot;&gt;SECTION&lt;/span&gt;
72
72
  &lt;span class=&quot;ui-badge&quot;&gt;12&lt;/span&gt;
73
+ &lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Tab bằng link (<code>&lt;nav class=ui-tabs&gt;</code>, <code>aria-current=page</code> là tab đang chọn)</figcaption><div class="cat-live"><nav class="ui-tabs" aria-label="Chế độ xem"><a href="#" aria-current="page">Tăng trưởng</a><a href="#">Mới lên</a><a href="#">Quảng cáo</a></nav></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;nav class=&quot;ui-tabs&quot; aria-label=&quot;Chế độ xem&quot;&gt;
74
+ &lt;a href=&quot;#&quot; aria-current=&quot;page&quot;&gt;Tăng trưởng&lt;/a&gt;
75
+ &lt;a href=&quot;#&quot;&gt;Mới lên&lt;/a&gt;
76
+ &lt;a href=&quot;#&quot;&gt;Quảng cáo&lt;/a&gt;
77
+ &lt;/nav&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Lọc bằng link (<code>ui-seg</code> với <code>&lt;a&gt;</code>; danh sách dài dùng <code>ui-chips</code>)</figcaption><div class="cat-live"><div class="ui-seg" role="group" aria-label="Hạng"><a class="is-text" href="#" aria-current="page">Tất cả</a><a class="is-text" href="#">Plus</a><a class="is-text" href="#">Premier</a></div><div class="ui-chips" style="margin-top:12px"><a class="ui-tag ui-tag--chip" href="#" aria-current="page">CRO</a><a class="ui-tag ui-tag--chip" href="#">Klaviyo</a><a class="ui-tag ui-tag--chip" href="#">Meta Ads</a></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-seg&quot; role=&quot;group&quot; aria-label=&quot;Hạng&quot;&gt;
78
+ &lt;a class=&quot;is-text&quot; href=&quot;#&quot; aria-current=&quot;page&quot;&gt;Tất cả&lt;/a&gt;
79
+ &lt;a class=&quot;is-text&quot; href=&quot;#&quot;&gt;Plus&lt;/a&gt;
80
+ &lt;a class=&quot;is-text&quot; href=&quot;#&quot;&gt;Premier&lt;/a&gt;
81
+ &lt;/div&gt;
82
+ &lt;div class=&quot;ui-chips&quot; style=&quot;margin-top:12px&quot;&gt;
83
+ &lt;a class=&quot;ui-tag ui-tag--chip&quot; href=&quot;#&quot; aria-current=&quot;page&quot;&gt;CRO&lt;/a&gt;
84
+ &lt;a class=&quot;ui-tag ui-tag--chip&quot; href=&quot;#&quot;&gt;Klaviyo&lt;/a&gt;
85
+ &lt;a class=&quot;ui-tag ui-tag--chip&quot; href=&quot;#&quot;&gt;Meta Ads&lt;/a&gt;
86
+ &lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Tông màu cho tag (<code>--success</code> / <code>--warning</code> / <code>--danger</code>)</figcaption><div class="cat-live"><div class="cat-row"><span class="ui-tag ui-tag--success">Đã tích hợp</span><span class="ui-tag ui-tag--warning">Chờ duyệt</span><span class="ui-tag ui-tag--danger">Lỗi tải</span></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;cat-row&quot;&gt;
87
+ &lt;span class=&quot;ui-tag ui-tag--success&quot;&gt;Đã tích hợp&lt;/span&gt;
88
+ &lt;span class=&quot;ui-tag ui-tag--warning&quot;&gt;Chờ duyệt&lt;/span&gt;
89
+ &lt;span class=&quot;ui-tag ui-tag--danger&quot;&gt;Lỗi tải&lt;/span&gt;
90
+ &lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Danh sách link dạng chip (<code>ui-chips</code>: cao ≥ 28px, 44px dưới ngón tay, khe 8px)</figcaption><div class="cat-live"><div class="ui-chips"><a class="ui-tag ui-tag--chip" href="#">Judge.me Product Reviews</a><a class="ui-tag ui-tag--chip" href="#">Klaviyo</a><a class="ui-tag ui-tag--chip" href="#">Instafeed</a></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-chips&quot;&gt;
91
+ &lt;a class=&quot;ui-tag ui-tag--chip&quot; href=&quot;#&quot;&gt;Judge.me Product Reviews&lt;/a&gt;
92
+ &lt;a class=&quot;ui-tag ui-tag--chip&quot; href=&quot;#&quot;&gt;Klaviyo&lt;/a&gt;
93
+ &lt;a class=&quot;ui-tag ui-tag--chip&quot; href=&quot;#&quot;&gt;Instafeed&lt;/a&gt;
73
94
  &lt;/div&gt;</code></pre></details></figure></section>
74
- <section id="surfaces"><h2>Thẻ, số liệu, bảng, trạng thái trống</h2><figure class="cat-demo"><figcaption>Card</figcaption><div class="cat-live"><div class="ui-card" style="padding:16px">Một khối nội dung. Nền, viền, bo góc và bóng lấy từ token.</div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-card&quot; style=&quot;padding:16px&quot;&gt;Một khối nội dung. Nền, viền, bo góc và bóng lấy từ token.&lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Số liệu (<code>ui-stats</code> / <code>ui-stat</code>)</figcaption><div class="cat-live"><div class="ui-stats"><div class="ui-stat"><b>17</b> Nháp</div><div class="ui-stat is-clickable active"><b>4</b> Đang duyệt</div><div class="ui-stat is-clickable"><b>0</b> Loại</div></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-stats&quot;&gt;
95
+ <section id="surfaces"><h2>Thẻ, số liệu, bảng, trạng thái trống</h2><figure class="cat-demo"><figcaption>Tiêu đề trang (<code>ui-page-head</code>) · chữ phụ (<code>ui-muted</code>)</figcaption><div class="cat-live"><div class="ui-page-head"><h1>Đối thủ</h1><p>Page builder nào đang tích hợp với ai.</p></div><p class="ui-muted">Cập nhật 01/10, 300 trang app đã đọc.</p></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-page-head&quot;&gt;
96
+ &lt;h1&gt;Đối thủ&lt;/h1&gt;
97
+ &lt;p&gt;Page builder nào đang tích hợp với ai.&lt;/p&gt;
98
+ &lt;/div&gt;
99
+ &lt;p class=&quot;ui-muted&quot;&gt;Cập nhật 01/10, 300 trang app đã đọc.&lt;/p&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Thanh công cụ (<code>ui-toolbar</code>)</figcaption><div class="cat-live"><div class="ui-toolbar"><div class="gemboss-field gemboss-field--group" style="flex:1 1 260px"><input class="gemboss-field__bare" placeholder="Tìm app…" aria-label="Tìm app"></div><select class="ui-select" aria-label="Xếp theo"><option>Store cài</option></select><button type="button" class="ui-btn">Xuất CSV</button></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-toolbar&quot;&gt;
100
+ &lt;div class=&quot;gemboss-field gemboss-field--group&quot; style=&quot;flex:1 1 260px&quot;&gt;
101
+ &lt;input class=&quot;gemboss-field__bare&quot; placeholder=&quot;Tìm app…&quot; aria-label=&quot;Tìm app&quot;&gt;
102
+ &lt;/div&gt;
103
+ &lt;select class=&quot;ui-select&quot; aria-label=&quot;Xếp theo&quot;&gt;
104
+ &lt;option&gt;Store cài&lt;/option&gt;
105
+ &lt;/select&gt;
106
+ &lt;button type=&quot;button&quot; class=&quot;ui-btn&quot;&gt;Xuất CSV&lt;/button&gt;
107
+ &lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Ô số liệu (<code>ui-metrics</code> / <code>ui-metric</code>, tông <code>--success|--warning|--danger</code>)</figcaption><div class="cat-live"><div class="ui-metrics"><div class="ui-metric"><span class="ui-metric__val">31.503</span><span class="ui-metric__lbl">App</span></div><div class="ui-metric ui-metric--success"><span class="ui-metric__val">157</span><span class="ui-metric__lbl">Khớp</span></div><div class="ui-metric ui-metric--danger"><span class="ui-metric__val">3</span><span class="ui-metric__lbl">Lỗi</span></div><a class="ui-metric" href="#"><span class="ui-metric__val">$41</span><span class="ui-metric__lbl">Chi phí tháng này</span></a></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-metrics&quot;&gt;
108
+ &lt;div class=&quot;ui-metric&quot;&gt;
109
+ &lt;span class=&quot;ui-metric__val&quot;&gt;31.503&lt;/span&gt;
110
+ &lt;span class=&quot;ui-metric__lbl&quot;&gt;App&lt;/span&gt;
111
+ &lt;/div&gt;
112
+ &lt;div class=&quot;ui-metric ui-metric--success&quot;&gt;
113
+ &lt;span class=&quot;ui-metric__val&quot;&gt;157&lt;/span&gt;
114
+ &lt;span class=&quot;ui-metric__lbl&quot;&gt;Khớp&lt;/span&gt;
115
+ &lt;/div&gt;
116
+ &lt;div class=&quot;ui-metric ui-metric--danger&quot;&gt;
117
+ &lt;span class=&quot;ui-metric__val&quot;&gt;3&lt;/span&gt;
118
+ &lt;span class=&quot;ui-metric__lbl&quot;&gt;Lỗi&lt;/span&gt;
119
+ &lt;/div&gt;
120
+ &lt;a class=&quot;ui-metric&quot; href=&quot;#&quot;&gt;
121
+ &lt;span class=&quot;ui-metric__val&quot;&gt;$41&lt;/span&gt;
122
+ &lt;span class=&quot;ui-metric__lbl&quot;&gt;Chi phí tháng này&lt;/span&gt;
123
+ &lt;/a&gt;
124
+ &lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Cặp nhãn – giá trị (<code>ui-dl</code>)</figcaption><div class="cat-live"><dl class="ui-dl"><div><dt>Quốc gia</dt><dd>IL</dd></div><div><dt>Store cài</dt><dd>133.340</dd></div><div><dt>Điểm tiềm năng</dt><dd>71</dd></div></dl></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;dl class=&quot;ui-dl&quot;&gt;
125
+ &lt;div&gt;
126
+ &lt;dt&gt;Quốc gia&lt;/dt&gt;
127
+ &lt;dd&gt;IL&lt;/dd&gt;
128
+ &lt;/div&gt;
129
+ &lt;div&gt;
130
+ &lt;dt&gt;Store cài&lt;/dt&gt;
131
+ &lt;dd&gt;133.340&lt;/dd&gt;
132
+ &lt;/div&gt;
133
+ &lt;div&gt;
134
+ &lt;dt&gt;Điểm tiềm năng&lt;/dt&gt;
135
+ &lt;dd&gt;71&lt;/dd&gt;
136
+ &lt;/div&gt;
137
+ &lt;/dl&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Thông báo (<code>ui-notice</code>, tông <code>--warning|--danger|--success</code>)</figcaption><div class="cat-live"><div class="ui-notice ui-notice--warning">Token Slack đã hết hạn: bản tin thứ Hai sẽ không gửi được.</div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-notice ui-notice--warning&quot;&gt;Token Slack đã hết hạn: bản tin thứ Hai sẽ không gửi được.&lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Phân trang (<code>ui-pager</code>)</figcaption><div class="cat-live"><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><a href="#" rel="next">Sau <span aria-hidden="true">›</span></a></nav></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;nav class=&quot;ui-pager&quot; aria-label=&quot;Trang&quot;&gt;
138
+ &lt;a href=&quot;#&quot; rel=&quot;prev&quot;&gt;
139
+ &lt;span aria-hidden=&quot;true&quot;&gt;‹&lt;/span&gt; Trước&lt;/a&gt;
140
+ &lt;span aria-current=&quot;page&quot;&gt;2 / 9&lt;/span&gt;
141
+ &lt;a href=&quot;#&quot; rel=&quot;next&quot;&gt;Sau &lt;span aria-hidden=&quot;true&quot;&gt;›&lt;/span&gt;
142
+ &lt;/a&gt;
143
+ &lt;/nav&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Card</figcaption><div class="cat-live"><div class="ui-card" style="padding:16px">Một khối nội dung. Nền, viền, bo góc và bóng lấy từ token.</div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-card&quot; style=&quot;padding:16px&quot;&gt;Một khối nội dung. Nền, viền, bo góc và bóng lấy từ token.&lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Đầu thẻ có nút (<code>ui-card__head</code>)</figcaption><div class="cat-live"><div class="ui-card"><div class="ui-card__head"><h3 class="ui-card__title">Hạng đối tác</h3><div><button type="button" class="ui-btn ui-btn--primary">Đổi hạng</button><button type="button" class="ui-btn">Kết thúc</button></div></div><p class="ui-muted" style="margin:0">Partner từ 01/08/2026.</p></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-card&quot;&gt;
144
+ &lt;div class=&quot;ui-card__head&quot;&gt;
145
+ &lt;h3 class=&quot;ui-card__title&quot;&gt;Hạng đối tác&lt;/h3&gt;
146
+ &lt;div&gt;
147
+ &lt;button type=&quot;button&quot; class=&quot;ui-btn ui-btn--primary&quot;&gt;Đổi hạng&lt;/button&gt;
148
+ &lt;button type=&quot;button&quot; class=&quot;ui-btn&quot;&gt;Kết thúc&lt;/button&gt;
149
+ &lt;/div&gt;
150
+ &lt;/div&gt;
151
+ &lt;p class=&quot;ui-muted&quot; style=&quot;margin:0&quot;&gt;Partner từ 01/08/2026.&lt;/p&gt;
152
+ &lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Số liệu (<code>ui-stats</code> / <code>ui-stat</code>)</figcaption><div class="cat-live"><div class="ui-stats"><div class="ui-stat"><b>17</b> Nháp</div><div class="ui-stat is-clickable active"><b>4</b> Đang duyệt</div><div class="ui-stat is-clickable"><b>0</b> Loại</div></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-stats&quot;&gt;
75
153
  &lt;div class=&quot;ui-stat&quot;&gt;
76
154
  &lt;b&gt;17&lt;/b&gt; Nháp&lt;/div&gt;
77
155
  &lt;div class=&quot;ui-stat is-clickable active&quot;&gt;
78
156
  &lt;b&gt;4&lt;/b&gt; Đang duyệt&lt;/div&gt;
79
157
  &lt;div class=&quot;ui-stat is-clickable&quot;&gt;
80
158
  &lt;b&gt;0&lt;/b&gt; Loại&lt;/div&gt;
81
- &lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Bảng (<code>is-num</code> căn phải, số đều cột)</figcaption><div class="cat-live"><div class="ui-table-wrap"><table class="ui-table"><thead><tr><th>Store</th><th>Ngành</th><th class="is-num">Trang</th></tr></thead><tbody><tr><td>fluke.com</td><td>Audio</td><td class="is-num">1.284</td></tr><tr><td>glowrecipe.com</td><td>Mỹ phẩm</td><td class="is-num">312</td></tr></tbody></table></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-table-wrap&quot;&gt;
159
+ &lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Bảng (<code>is-num</code> căn phải, số đều cột; <code>ui-sub</code> dòng phụ trong ô)</figcaption><div class="cat-live"><div class="ui-table-wrap"><table class="ui-table"><thead><tr><th>Store</th><th>Ngành</th><th class="is-num">Trang</th></tr></thead><tbody><tr><td>fluke.com<span class="ui-sub">Shopify Plus</span></td><td>Audio</td><td class="is-num">1.284</td></tr><tr><td>glowrecipe.com<span class="ui-sub">Shopify</span></td><td>Mỹ phẩm</td><td class="is-num">312</td></tr></tbody></table></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-table-wrap&quot;&gt;
82
160
  &lt;table class=&quot;ui-table&quot;&gt;
83
161
  &lt;thead&gt;
84
162
  &lt;tr&gt;
@@ -89,12 +167,14 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
89
167
  &lt;/thead&gt;
90
168
  &lt;tbody&gt;
91
169
  &lt;tr&gt;
92
- &lt;td&gt;fluke.com&lt;/td&gt;
170
+ &lt;td&gt;fluke.com&lt;span class=&quot;ui-sub&quot;&gt;Shopify Plus&lt;/span&gt;
171
+ &lt;/td&gt;
93
172
  &lt;td&gt;Audio&lt;/td&gt;
94
173
  &lt;td class=&quot;is-num&quot;&gt;1.284&lt;/td&gt;
95
174
  &lt;/tr&gt;
96
175
  &lt;tr&gt;
97
- &lt;td&gt;glowrecipe.com&lt;/td&gt;
176
+ &lt;td&gt;glowrecipe.com&lt;span class=&quot;ui-sub&quot;&gt;Shopify&lt;/span&gt;
177
+ &lt;/td&gt;
98
178
  &lt;td&gt;Mỹ phẩm&lt;/td&gt;
99
179
  &lt;td class=&quot;is-num&quot;&gt;312&lt;/td&gt;
100
180
  &lt;/tr&gt;
@@ -102,6 +182,12 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
102
182
  &lt;/table&gt;
103
183
  &lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Trống</figcaption><div class="cat-live"><div class="ui-empty">Chưa có gì ở đây.<button type="button" class="ui-btn ui-btn--primary">Tạo mục đầu tiên</button></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-empty&quot;&gt;Chưa có gì ở đây.&lt;button type=&quot;button&quot; class=&quot;ui-btn ui-btn--primary&quot;&gt;Tạo mục đầu tiên&lt;/button&gt;
104
184
  &lt;/div&gt;</code></pre></details></figure></section>
185
+ <section id="resizer"><h2>Thanh kéo giữa hai cột</h2><p>Thanh <code>ui-resizer</code> đứng giữa hai cột, người dùng kéo để đổi độ rộng. Phần NHÌN (vạch nắm hiện khi rê chuột hoặc focus bàn phím, vùng chạm 24px cho ngón tay, lớp che khi đang kéo) là của gemboss-ui.css; vị trí và hành vi là của bề mặt: dùng <b>pointer event</b> (chạm là một pointer), gắn <code>ui-resizing</code> lên <code>&lt;body&gt;</code> trong lúc kéo, phím mũi tên / Home / End di chuyển nó, kẹp theo bề rộng THẬT của khung (không theo một con số), và nhớ độ rộng theo trình duyệt. Admin: <code>useResizableChatPanel</code>; gemcare: viết tay theo đúng markup này.</p><figure class="cat-demo"><figcaption>Rê chuột hoặc Tab vào khe giữa để thấy vạch nắm</figcaption><div class="cat-live"><div style="display:grid;grid-template-columns:200px 5px minmax(0,1fr);height:140px;border:1px solid var(--gemboss-divider);border-radius:var(--gemboss-radius-md);overflow:hidden"><div style="padding:12px;background:var(--gemboss-surface)">Danh sách</div><div class="ui-resizer" role="separator" aria-orientation="vertical" aria-label="Độ rộng danh sách" tabindex="0" aria-valuenow="200" aria-valuemin="160" aria-valuemax="420"></div><div style="padding:12px;background:var(--gemboss-surface)">Nội dung</div></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div style=&quot;display:grid;grid-template-columns:200px 5px minmax(0,1fr);height:140px;border:1px solid var(--gemboss-divider);border-radius:var(--gemboss-radius-md);overflow:hidden&quot;&gt;
186
+ &lt;div style=&quot;padding:12px;background:var(--gemboss-surface)&quot;&gt;Danh sách&lt;/div&gt;
187
+ &lt;div class=&quot;ui-resizer&quot; role=&quot;separator&quot; aria-orientation=&quot;vertical&quot; aria-label=&quot;Độ rộng danh sách&quot; tabindex=&quot;0&quot; aria-valuenow=&quot;200&quot; aria-valuemin=&quot;160&quot; aria-valuemax=&quot;420&quot;&gt;
188
+ &lt;/div&gt;
189
+ &lt;div style=&quot;padding:12px;background:var(--gemboss-surface)&quot;&gt;Nội dung&lt;/div&gt;
190
+ &lt;/div&gt;</code></pre></details></figure></section>
105
191
  <section id="modal"><h2>Hộp thoại</h2><p>Lớp phủ <code>ui-overlay</code> + hộp <code>ui-modal</code> (ở đây vẽ không có lớp phủ để xem được trong trang). Thân hộp thoại tự đệm: <code>ui-modal__body</code> / <code>__foot</code> chưa có CSS nào, kể cả trong admin.</p><figure class="cat-demo"><figcaption>Hộp thoại (<code>ui-modal__head</code> có nút đóng 44px)</figcaption><div class="cat-live"><div class="ui-modal" role="dialog" aria-modal="true" aria-labelledby="cat-m"><div class="ui-modal__head"><div class="ui-modal__title" id="cat-m">Xoá phiên này?</div><button type="button" class="ui-modal__close" aria-label="Đóng"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10M15 5 5 15"></path></g></svg></button></div><div style="padding:16px 20px">Lịch sử vẫn còn trong sổ; phiên chỉ ẩn khỏi danh sách.<div class="cat-row" style="justify-content:flex-end;margin-top:16px"><button type="button" class="ui-btn ui-btn--ghost">Huỷ</button><button type="button" class="ui-btn ui-btn--primary">Xoá</button></div></div></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-modal&quot; role=&quot;dialog&quot; aria-modal=&quot;true&quot; aria-labelledby=&quot;cat-m&quot;&gt;
106
192
  &lt;div class=&quot;ui-modal__head&quot;&gt;
107
193
  &lt;div class=&quot;ui-modal__title&quot; id=&quot;cat-m&quot;&gt;Xoá phiên này?&lt;/div&gt;
@@ -120,10 +206,10 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
120
206
  &lt;/div&gt;
121
207
  &lt;/div&gt;
122
208
  &lt;/div&gt;</code></pre></details></figure></section>
123
- <section id="shell"><h2>Khung app</h2><p>Topbar 56px, rail 240px, ≤760px thành ngăn kéo. Logo <code>BrandLockup</code>, tài khoản <code>AccountMenu</code> (bấm chữ viết tắt), mục Trợ lý ở chân rail có class <code>gnav-assistant</code>.</p><figure class="cat-demo cat-screen"><figcaption>AppShell + BrandLockup + AccountMenu + Icon</figcaption><iframe title="AppShell + BrandLockup + AccountMenu + Icon" style="height:460px" srcdoc="&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;../gemboss-ui.css&quot;&gt;&lt;/head&gt;&lt;body&gt;&lt;div class=&quot;gemboss-platform-shell&quot;&gt;&lt;header class=&quot;gemboss-platform-topbar&quot;&gt;&lt;div class=&quot;gemboss-topbar-start&quot;&gt;&lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-menu-btn&quot; aria-label=&quot;Mở menu&quot; aria-expanded=&quot;false&quot;&gt;&lt;svg width=&quot;20&quot; height=&quot;20&quot; viewBox=&quot;0 0 20 20&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M3 5.5h14M3 10h14M3 14.5h14&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.7&quot; stroke-linecap=&quot;round&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/button&gt;&lt;a class=&quot;gb-brand&quot; href=&quot;#&quot; aria-label=&quot;GemFactory home&quot;&gt;&lt;svg class=&quot;gb-brand-gem&quot; viewBox=&quot;0 0 32 40&quot; width=&quot;20.8&quot; height=&quot;26&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g transform=&quot;translate(0,0.8) scale(0.31)&quot;&gt;&lt;polygon points=&quot;26,14 74,14 66,50 34,50&quot; fill=&quot;#5661F5&quot; fill-opacity=&quot;0.92&quot;&gt;&lt;/polygon&gt;&lt;path d=&quot;M26,14 74,14 96,50 50,110 4,50Z&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;8.75&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;5.45&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;span class=&quot;gb-brand-name&quot;&gt;GemFactory&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gemboss-topbar-center&quot;&gt;&lt;/div&gt;&lt;div class=&quot;gemboss-topbar-end&quot;&gt;&lt;details class=&quot;gb-account&quot;&gt;&lt;summary class=&quot;gemboss-account-chip&quot; aria-label=&quot;Account · Minh Anh&quot; title=&quot;Minh Anh&quot;&gt;MA&lt;/summary&gt;&lt;div role=&quot;menu&quot; class=&quot;gemboss-popover-surface gemboss-account-menu&quot;&gt;&lt;div class=&quot;gb-account-head&quot;&gt;&lt;div class=&quot;gb-account-name&quot;&gt;Minh Anh&lt;/div&gt;&lt;div class=&quot;gb-account-email&quot;&gt;ma@gemboss.ai&lt;/div&gt;&lt;div class=&quot;gb-account-role&quot;&gt;Designer&lt;/div&gt;&lt;/div&gt;&lt;a role=&quot;menuitem&quot; class=&quot;gemboss-account-menu-item&quot; href=&quot;#&quot;&gt;Cài đặt&lt;/a&gt;&lt;form method=&quot;post&quot; action=&quot;#&quot; class=&quot;gb-account-form&quot;&gt;&lt;button role=&quot;menuitem&quot; type=&quot;submit&quot; class=&quot;gemboss-account-menu-item danger&quot;&gt;Đăng xuất&lt;/button&gt;&lt;/form&gt;&lt;/div&gt;&lt;/details&gt;&lt;/div&gt;&lt;/header&gt;&lt;nav class=&quot;icon-nav&quot; aria-label=&quot;Main navigation&quot;&gt;&lt;div class=&quot;gnav-scroll&quot;&gt;&lt;div class=&quot;gnav-heading&quot;&gt;Kho&lt;/div&gt;&lt;div class=&quot;gnav-group active&quot;&gt;&lt;a class=&quot;gnav-item active&quot; aria-current=&quot;page&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;rect x=&quot;3&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;13&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;8&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;9&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;13&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;6&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Board&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gnav-group&quot;&gt;&lt;a class=&quot;gnav-item&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;rect x=&quot;3.5&quot; y=&quot;3&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;3.5&quot; y=&quot;8.25&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;3.5&quot; y=&quot;13.5&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Ghép trang&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gnav-group&quot;&gt;&lt;a class=&quot;gnav-item&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M10 3.2 17.3 16H2.7z&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M10 8.2v3.6M10 13.9v.1&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Framework thiếu&lt;/span&gt;&lt;span class=&quot;gnav-count&quot; aria-label=&quot;3 mục đang mở&quot;&gt;3&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;gnav-bottom&quot;&gt;&lt;div class=&quot;gnav-sep&quot;&gt;&lt;/div&gt;&lt;a class=&quot;gnav-item gnav-assistant&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M3 4.5h14v9H8.5L5 16.5v-3H3z&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Trợ lý&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;/nav&gt;&lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-nav-backdrop&quot; aria-label=&quot;Đóng menu&quot; tabindex=&quot;-1&quot;&gt;&lt;/button&gt;&lt;main class=&quot;gemboss-platform-main&quot;&gt;&lt;div style=&quot;padding:24px&quot;&gt;&lt;h1 style=&quot;margin:0&quot;&gt;Nội dung màn&lt;/h1&gt;&lt;/div&gt;&lt;/main&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;"></iframe><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;gemboss-platform-shell&quot;&gt;
209
+ <section id="shell"><h2>Khung app</h2><p>Topbar 56px, rail 240px, ≤760px thành ngăn kéo. Logo <code>BrandLockup</code>, tài khoản <code>AccountMenu</code> (bấm chữ viết tắt), mục Trợ lý ở chân rail có class <code>gnav-assistant</code>.</p><figure class="cat-demo cat-screen"><figcaption>AppShell + BrandLockup + AccountMenu + Icon</figcaption><iframe title="AppShell + BrandLockup + AccountMenu + Icon" style="height:460px" srcdoc="&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;../gemboss-ui.css&quot;&gt;&lt;/head&gt;&lt;body&gt;&lt;div class=&quot;gemboss-platform-shell&quot;&gt;&lt;header class=&quot;gemboss-platform-topbar&quot;&gt;&lt;div class=&quot;gemboss-topbar-start&quot;&gt;&lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-menu-btn&quot; aria-label=&quot;Mở menu&quot; aria-expanded=&quot;false&quot; aria-controls=&quot;:R0:&quot;&gt;&lt;svg width=&quot;20&quot; height=&quot;20&quot; viewBox=&quot;0 0 20 20&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M3 5.5h14M3 10h14M3 14.5h14&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.7&quot; stroke-linecap=&quot;round&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/button&gt;&lt;a class=&quot;gb-brand&quot; href=&quot;#&quot; aria-label=&quot;GemFactory home&quot;&gt;&lt;svg class=&quot;gb-brand-gem&quot; viewBox=&quot;0 0 32 40&quot; width=&quot;20.8&quot; height=&quot;26&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g transform=&quot;translate(0,0.8) scale(0.31)&quot;&gt;&lt;polygon points=&quot;26,14 74,14 66,50 34,50&quot; fill=&quot;#5661F5&quot; fill-opacity=&quot;0.92&quot;&gt;&lt;/polygon&gt;&lt;path d=&quot;M26,14 74,14 96,50 50,110 4,50Z&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;8.75&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;5.45&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;span class=&quot;gb-brand-name&quot;&gt;GemFactory&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gemboss-topbar-center&quot;&gt;&lt;/div&gt;&lt;div class=&quot;gemboss-topbar-end&quot;&gt;&lt;details class=&quot;gb-account&quot;&gt;&lt;summary class=&quot;gemboss-account-chip&quot; aria-label=&quot;Account · Minh Anh&quot; title=&quot;Minh Anh&quot;&gt;MA&lt;/summary&gt;&lt;div role=&quot;menu&quot; class=&quot;gemboss-popover-surface gemboss-account-menu&quot;&gt;&lt;div class=&quot;gb-account-head&quot;&gt;&lt;div class=&quot;gb-account-name&quot;&gt;Minh Anh&lt;/div&gt;&lt;div class=&quot;gb-account-email&quot;&gt;ma@gemboss.ai&lt;/div&gt;&lt;div class=&quot;gb-account-role&quot;&gt;Designer&lt;/div&gt;&lt;/div&gt;&lt;a role=&quot;menuitem&quot; class=&quot;gemboss-account-menu-item&quot; href=&quot;#&quot;&gt;Cài đặt&lt;/a&gt;&lt;form method=&quot;post&quot; action=&quot;#&quot; class=&quot;gb-account-form&quot;&gt;&lt;button role=&quot;menuitem&quot; type=&quot;submit&quot; class=&quot;gemboss-account-menu-item danger&quot;&gt;Đăng xuất&lt;/button&gt;&lt;/form&gt;&lt;/div&gt;&lt;/details&gt;&lt;/div&gt;&lt;/header&gt;&lt;nav class=&quot;icon-nav&quot; aria-label=&quot;Main navigation&quot; id=&quot;:R0:&quot;&gt;&lt;div class=&quot;gnav-scroll&quot;&gt;&lt;div class=&quot;gnav-heading&quot;&gt;Kho&lt;/div&gt;&lt;div class=&quot;gnav-group active&quot;&gt;&lt;a class=&quot;gnav-item active&quot; aria-current=&quot;page&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;rect x=&quot;3&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;13&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;8&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;9&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;13&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;6&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Board&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gnav-group&quot;&gt;&lt;a class=&quot;gnav-item&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;rect x=&quot;3.5&quot; y=&quot;3&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;3.5&quot; y=&quot;8.25&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;3.5&quot; y=&quot;13.5&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Ghép trang&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gnav-group&quot;&gt;&lt;a class=&quot;gnav-item&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M10 3.2 17.3 16H2.7z&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M10 8.2v3.6M10 13.9v.1&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Framework thiếu&lt;/span&gt;&lt;span class=&quot;gnav-count&quot; aria-label=&quot;3 mục đang mở&quot;&gt;3&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;gnav-bottom&quot;&gt;&lt;div class=&quot;gnav-sep&quot;&gt;&lt;/div&gt;&lt;a class=&quot;gnav-item gnav-assistant&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M3 4.5h14v9H8.5L5 16.5v-3H3z&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Trợ lý&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;/nav&gt;&lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-nav-backdrop&quot; aria-label=&quot;Đóng menu&quot; tabindex=&quot;-1&quot;&gt;&lt;/button&gt;&lt;main class=&quot;gemboss-platform-main&quot;&gt;&lt;div style=&quot;padding:24px&quot;&gt;&lt;h1 style=&quot;margin:0&quot;&gt;Nội dung màn&lt;/h1&gt;&lt;/div&gt;&lt;/main&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;"></iframe><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;gemboss-platform-shell&quot;&gt;
124
210
  &lt;header class=&quot;gemboss-platform-topbar&quot;&gt;
125
211
  &lt;div class=&quot;gemboss-topbar-start&quot;&gt;
126
- &lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-menu-btn&quot; aria-label=&quot;Mở menu&quot; aria-expanded=&quot;false&quot;&gt;
212
+ &lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-menu-btn&quot; aria-label=&quot;Mở menu&quot; aria-expanded=&quot;false&quot; aria-controls=&quot;:R0:&quot;&gt;
127
213
  &lt;svg width=&quot;20&quot; height=&quot;20&quot; viewBox=&quot;0 0 20 20&quot; aria-hidden=&quot;true&quot;&gt;
128
214
  &lt;path d=&quot;M3 5.5h14M3 10h14M3 14.5h14&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.7&quot; stroke-linecap=&quot;round&quot;&gt;
129
215
  &lt;/path&gt;
@@ -162,7 +248,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
162
248
  &lt;/details&gt;
163
249
  &lt;/div&gt;
164
250
  &lt;/header&gt;
165
- &lt;nav class=&quot;icon-nav&quot; aria-label=&quot;Main navigation&quot;&gt;
251
+ &lt;nav class=&quot;icon-nav&quot; aria-label=&quot;Main navigation&quot; id=&quot;:R0:&quot;&gt;
166
252
  &lt;div class=&quot;gnav-scroll&quot;&gt;
167
253
  &lt;div class=&quot;gnav-heading&quot;&gt;Kho&lt;/div&gt;
168
254
  &lt;div class=&quot;gnav-group active&quot;&gt;
@@ -290,7 +376,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
290
376
  &lt;/aside&gt;
291
377
  &lt;/div&gt;
292
378
  &lt;/main&gt;</code></pre></details></figure></section>
293
- <section id="assistant"><h2>Trợ lý: cột phiên</h2><p><code>SessionRail</code> (React). Khung toàn màn hình / cửa sổ nổi / thanh là <code>AssistantHost</code>, cần React vì có trạng thái.</p><figure class="cat-demo"><figcaption>SessionRail</figcaption><div class="cat-live"><div style="height:360px;display:flex"><aside class="gemboss-assistant-rail"><div class="gemboss-assistant-rail-top"><button type="button" class="gemboss-assistant-newchat" title="Phiên mới"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H5.5A1.5 1.5 0 0 0 4 5.5v9A1.5 1.5 0 0 0 5.5 16h9a1.5 1.5 0 0 0 1.5-1.5V9"></path><path d="M14.5 3.5l2 2L10 12H8v-2z"></path></g></svg><span>Phiên mới</span></button><button type="button" class="gemboss-assistant-collapse" aria-label="Collapse sidebar" title="Collapse"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5l-5 5 5 5"></path></g></svg></button></div><div class="gemboss-assistant-search gemboss-field gemboss-field--group"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="5"></circle><path d="M13 13l3.5 3.5"></path></g></svg><input class="gemboss-field__bare" placeholder="Tìm phiên…" aria-label="Search history" value=""/><kbd class="gemboss-assistant-kbd">/</kbd></div><div class="gemboss-assistant-threads"><div class="gemboss-assistant-group"><div class="gemboss-assistant-group-label">Đã ghim</div><div class="gemboss-assistant-thread"><button type="button" class="gemboss-assistant-thread-main" title="So sánh advertorial với listicle"><span class="gemboss-assistant-thread-titlerow"><svg viewBox="0 0 20 20" class="gemboss-assistant-pin-dot" aria-hidden="true" focusable="false"><path d="M8 3.5h4l-.5 5 2.5 2.5H6l2.5-2.5z" fill="currentColor"></path><path d="M10 11v5.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path></svg><span class="gemboss-assistant-thread-title">So sánh advertorial với listicle</span></span><span class="gemboss-assistant-thread-meta">20/8/2026</span></button><div class="gemboss-assistant-thread-actions"><button type="button" class="gemboss-assistant-thread-act" aria-label="Options"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="10" r=".8"></circle><circle cx="10" cy="10" r=".8"></circle><circle cx="15" cy="10" r=".8"></circle></g></svg></button></div></div></div><div class="gemboss-assistant-group"><div class="gemboss-assistant-group-label">Hôm nay</div><div class="gemboss-assistant-thread active"><button type="button" class="gemboss-assistant-thread-main" title="Store nào đang dựng loạt landing?"><span class="gemboss-assistant-thread-titlerow"><span class="gemboss-assistant-thread-title">Store nào đang dựng loạt landing?</span></span><span class="gemboss-assistant-thread-meta">3 giờ trước</span></button><div class="gemboss-assistant-thread-actions"><button type="button" class="gemboss-assistant-thread-act" aria-label="Options"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="10" r=".8"></circle><circle cx="10" cy="10" r=".8"></circle><circle cx="15" cy="10" r=".8"></circle></g></svg></button></div></div></div><div class="gemboss-assistant-group"><div class="gemboss-assistant-group-label">Hôm qua</div><div class="gemboss-assistant-thread"><button type="button" class="gemboss-assistant-thread-main" title="Landing điểm cao nhất của corpus"><span class="gemboss-assistant-thread-titlerow"><span class="gemboss-assistant-thread-title">Landing điểm cao nhất của corpus</span></span><span class="gemboss-assistant-thread-meta">1 ngày trước</span></button><div class="gemboss-assistant-thread-actions"><button type="button" class="gemboss-assistant-thread-act" aria-label="Options"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="10" r=".8"></circle><circle cx="10" cy="10" r=".8"></circle><circle cx="15" cy="10" r=".8"></circle></g></svg></button></div></div></div></div></aside></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div style=&quot;height:360px;display:flex&quot;&gt;
379
+ <section id="assistant"><h2>Trợ lý: cột phiên</h2><p><code>SessionRail</code> (React). Khung toàn màn hình / cửa sổ nổi / thanh là <code>AssistantHost</code>, cần React vì có trạng thái.</p><figure class="cat-demo"><figcaption>SessionRail</figcaption><div class="cat-live"><div style="height:360px;display:flex"><aside class="gemboss-assistant-rail"><div class="gemboss-assistant-rail-top"><button type="button" class="gemboss-assistant-newchat" title="Phiên mới"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H5.5A1.5 1.5 0 0 0 4 5.5v9A1.5 1.5 0 0 0 5.5 16h9a1.5 1.5 0 0 0 1.5-1.5V9"></path><path d="M14.5 3.5l2 2L10 12H8v-2z"></path></g></svg><span>Phiên mới</span></button><button type="button" class="gemboss-assistant-collapse" aria-label="Collapse sidebar" title="Collapse"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5l-5 5 5 5"></path></g></svg></button></div><div class="gemboss-assistant-search gemboss-field gemboss-field--group"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="5"></circle><path d="M13 13l3.5 3.5"></path></g></svg><input class="gemboss-field__bare" placeholder="Tìm phiên…" aria-label="Search history" value=""/><kbd class="gemboss-assistant-kbd">/</kbd></div><div class="gemboss-assistant-threads"><div class="gemboss-assistant-group"><div class="gemboss-assistant-group-label">Đã ghim</div><div class="gemboss-assistant-thread"><button type="button" class="gemboss-assistant-thread-main" title="So sánh advertorial với listicle"><span class="gemboss-assistant-thread-titlerow"><svg viewBox="0 0 20 20" class="gemboss-assistant-pin-dot" aria-hidden="true" focusable="false"><path d="M8 3.5h4l-.5 5 2.5 2.5H6l2.5-2.5z" fill="currentColor"></path><path d="M10 11v5.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path></svg><span class="gemboss-assistant-thread-title">So sánh advertorial với listicle</span></span><span class="gemboss-assistant-thread-meta">22/8/2026</span></button><div class="gemboss-assistant-thread-actions"><button type="button" class="gemboss-assistant-thread-act" aria-label="Options"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="10" r=".8"></circle><circle cx="10" cy="10" r=".8"></circle><circle cx="15" cy="10" r=".8"></circle></g></svg></button></div></div></div><div class="gemboss-assistant-group"><div class="gemboss-assistant-group-label">Hôm nay</div><div class="gemboss-assistant-thread active"><button type="button" class="gemboss-assistant-thread-main" title="Store nào đang dựng loạt landing?"><span class="gemboss-assistant-thread-titlerow"><span class="gemboss-assistant-thread-title">Store nào đang dựng loạt landing?</span></span><span class="gemboss-assistant-thread-meta">3 giờ trước</span></button><div class="gemboss-assistant-thread-actions"><button type="button" class="gemboss-assistant-thread-act" aria-label="Options"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="10" r=".8"></circle><circle cx="10" cy="10" r=".8"></circle><circle cx="15" cy="10" r=".8"></circle></g></svg></button></div></div></div><div class="gemboss-assistant-group"><div class="gemboss-assistant-group-label">Hôm qua</div><div class="gemboss-assistant-thread"><button type="button" class="gemboss-assistant-thread-main" title="Landing điểm cao nhất của corpus"><span class="gemboss-assistant-thread-titlerow"><span class="gemboss-assistant-thread-title">Landing điểm cao nhất của corpus</span></span><span class="gemboss-assistant-thread-meta">1 ngày trước</span></button><div class="gemboss-assistant-thread-actions"><button type="button" class="gemboss-assistant-thread-act" aria-label="Options"><svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="10" r=".8"></circle><circle cx="10" cy="10" r=".8"></circle><circle cx="15" cy="10" r=".8"></circle></g></svg></button></div></div></div></div></aside></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div style=&quot;height:360px;display:flex&quot;&gt;
294
380
  &lt;aside class=&quot;gemboss-assistant-rail&quot;&gt;
295
381
  &lt;div class=&quot;gemboss-assistant-rail-top&quot;&gt;
296
382
  &lt;button type=&quot;button&quot; class=&quot;gemboss-assistant-newchat&quot; title=&quot;Phiên mới&quot;&gt;
@@ -339,7 +425,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
339
425
  &lt;/svg&gt;
340
426
  &lt;span class=&quot;gemboss-assistant-thread-title&quot;&gt;So sánh advertorial với listicle&lt;/span&gt;
341
427
  &lt;/span&gt;
342
- &lt;span class=&quot;gemboss-assistant-thread-meta&quot;&gt;20/8/2026&lt;/span&gt;
428
+ &lt;span class=&quot;gemboss-assistant-thread-meta&quot;&gt;22/8/2026&lt;/span&gt;
343
429
  &lt;/button&gt;
344
430
  &lt;div class=&quot;gemboss-assistant-thread-actions&quot;&gt;
345
431
  &lt;button type=&quot;button&quot; class=&quot;gemboss-assistant-thread-act&quot; aria-label=&quot;Options&quot;&gt;
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: cf59218eb5d076a1585551bfb3eca691a812c31b
5
- * sources sha256: 89ec556bd16fd74dc4733abb73c0550383b1efb0cb7b6c5bb33d86f98d35bcab
4
+ * gemboss commit: 62b85f8a0c756bf63d370bff60e6debaaf4ea4df
5
+ * sources sha256: fd64ea38ae59bb5ecf1c38da341be89f963c9fef11bd3189e83433588f4d88b5
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
@@ -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 {
@@ -402,13 +494,16 @@
402
494
  Grouped toggle: icon squares (.ui-seg button) or text tabs (.is-text). The
403
495
  selected segment uses the accent-soft state. */
404
496
  .ui-seg { display: inline-flex; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); overflow: hidden; }
405
- .ui-seg button {
497
+ .ui-seg :is(button, a) {
406
498
  display: inline-grid; place-items: center; width: 36px; height: 36px;
407
499
  border: 0; background: transparent; color: var(--ui-text-muted); cursor: pointer; font-family: inherit;
408
500
  }
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); }
501
+ .ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
502
+ .ui-seg :is(button, a) + :is(button, a) { border-left: 1px solid var(--ui-border); }
503
+ .ui-seg :is(button, a):hover { background: var(--ui-surface-2); color: var(--ui-text); }
504
+ /* Lọc / chế độ xem bằng LINK (trang server-render, chọn = đổi URL): `<a aria-current="page">` là ô đang chọn. */
505
+ .ui-seg a { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; color: var(--ui-text-muted); }
506
+ .ui-seg a[aria-current="page"],
412
507
  .ui-seg button.active { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
413
508
  .ui-seg svg { width: 16px; height: 16px; fill: currentColor; }
414
509
 
@@ -490,7 +585,9 @@
490
585
  decision, not an oversight; it gets its own measurement pass or none. */
491
586
  .ui-btn:not(.ui-btn--sm),
492
587
  .ui-stat.is-clickable,
493
- .ui-seg button { min-height: 44px; }
588
+ .ui-chips > a.ui-tag--chip,
589
+ .ui-pager > a,
590
+ .ui-seg :is(button, a) { min-height: 44px; }
494
591
  /* A clickable stat whose label is one short token ("7d" on /admin/gem-intelligence) measured 40x44
495
592
  in the 13/09/2026 sweep: the height floor held, the width did not. */
496
593
  .ui-stat.is-clickable { min-width: 44px; justify-content: center; }
@@ -499,7 +596,7 @@
499
596
  .ui-select.ui-select,
500
597
  .ui-select--slim.ui-select--slim { min-height: 44px; }
501
598
  /* The icon-only segment is 36 WIDE as well — a target is an area. */
502
- .ui-seg button { min-width: 44px; }
599
+ .ui-seg :is(button, a) { min-width: 44px; }
503
600
  }
504
601
 
505
602
  /* ── Skeleton ─────────────────────────────────────────────────────────────────
@@ -523,6 +620,46 @@
523
620
 
524
621
  }
525
622
 
623
+ /* ── Resizer: the seam you drag between two panels ────────────────────────────
624
+ One look for every split in the fleet: the admin's editors (chat | preview) today; made for gemcare's
625
+ 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
626
+ placed strip) belongs to the surface. While a drag runs the surface puts `ui-resizing` on <body>
627
+ (and may put `dragging` on the seam itself to keep its grip lit).
628
+ Markup: <div class="ui-resizer" role="separator" aria-orientation="vertical" tabindex="0"
629
+ aria-label="…" aria-valuenow aria-valuemin aria-valuemax>. Pointer events, not mouse events (touch
630
+ is a pointer); arrow keys move it — a focusable separator that ignores the keyboard is INERT. */
631
+ /* `touch-action: none` is what makes a FINGER drag reach this element at all. `preventDefault()` on
632
+ `pointerdown` does not stop panning: the browser hands the gesture to the nearest scroller and sends
633
+ `pointercancel`, so the seam jumps once and stops. */
634
+ .ui-resizer { cursor: col-resize; position: relative; touch-action: none; outline: none; }
635
+ /* The grip pill is invisible at rest (a clean seam) and fades in over the zone, on keyboard focus,
636
+ and while a drag runs. Focus shows the pill rather than a ring: on a 5px seam a ring is a smear. */
637
+ .ui-resizer::before {
638
+ content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
639
+ width: 4px; height: 44px; border-radius: var(--gemboss-radius-full);
640
+ background: var(--gemboss-primary);
641
+ opacity: 0;
642
+ transition: opacity 0.14s ease, height 0.14s ease;
643
+ }
644
+ .ui-resizer:hover::before,
645
+ .ui-resizer:focus-visible::before,
646
+ .ui-resizer.dragging::before,
647
+ body.ui-resizing .ui-resizer::before { opacity: 1; height: 52px; }
648
+ /* A FINGER NEEDS MORE THAN 5px, and the line must still look like 5px. WCAG 2.5.8 puts the floor for
649
+ this kind of target at 24px, so the HIT area grows and the visible seam does not — widening the
650
+ element itself would also move the columns. Only where the pointer is coarse. */
651
+ @media (pointer: coarse) {
652
+ .ui-resizer::after {
653
+ content: ""; position: absolute; top: 0; bottom: 0; left: -14px; right: -14px;
654
+ touch-action: none; /* the band must claim the gesture too, or the scroller still wins */
655
+ z-index: 2; /* above the panels either side, transparent */
656
+ }
657
+ }
658
+ body.ui-resizing, body.ui-resizing * { cursor: col-resize !important; user-select: none !important; }
659
+ /* While dragging, a full-viewport transparent shield sits above everything — iframes included — so the
660
+ pointer stream is never captured by a cross-document frame (the classic "stuck cursor"). */
661
+ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999; cursor: col-resize; }
662
+
526
663
  /* ── Tabs · Table · Empty state ───────────────────────────────────────────────
527
664
  Plain-CSS primitives for surfaces that are NOT React (gemcare's vanilla dashboard reads this
528
665
  file through `npm run export:ui-css`). The React app has Polaris <Tabs>/<DataTable>/<EmptyState>
@@ -537,7 +674,8 @@
537
674
  display: flex; gap: var(--ui-space-1); border-bottom: 1px solid var(--ui-border);
538
675
  overflow-x: auto; scrollbar-width: none;
539
676
  }
540
- .ui-tabs > button {
677
+ .ui-tabs > button,
678
+ .ui-tabs > a {
541
679
  position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
542
680
  min-height: 44px; padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
543
681
  border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;
@@ -545,9 +683,15 @@
545
683
  color: var(--ui-text-muted);
546
684
  transition: color var(--gemboss-transition-fast) ease, background-color var(--gemboss-transition-fast) ease;
547
685
  }
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; }
686
+ .ui-tabs > button:hover,
687
+ .ui-tabs > a:hover { color: var(--ui-text); background: var(--ui-surface-2); }
688
+ .ui-tabs > button:focus-visible,
689
+ .ui-tabs > a:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
690
+ .ui-tabs > a { text-decoration: none; }
691
+ /* 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. */
692
+ .ui-tabs > a[aria-current="page"],
550
693
  .ui-tabs > button[aria-selected="true"] { color: var(--ui-text-strong); font-weight: 600; }
694
+ .ui-tabs > a[aria-current="page"]::after,
551
695
  .ui-tabs > button[aria-selected="true"]::after {
552
696
  content: ""; position: absolute; left: var(--ui-space-3); right: var(--ui-space-3); bottom: 0;
553
697
  height: 2px; border-radius: var(--gemboss-radius-full); background: var(--ui-accent);
@@ -573,6 +717,12 @@
573
717
  .ui-table tbody tr:last-child > td { border-bottom: 0; }
574
718
  .ui-table tbody tr:hover { background: var(--ui-surface-2); }
575
719
  .ui-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
720
+ /* 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>`.
721
+ Trước đây mỗi surface tự viết `.muted` riêng trong ô bảng (partner-radar, gemwatcher). */
722
+ /* 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).
723
+ Chữ 13px cho hộp 17px; visual-gate đo đỏ ở partner-radar /competitors 01/10/2026. inline-flex giữ nó trên dòng. */
724
+ .ui-table td > a { display: inline-flex; align-items: center; min-height: 24px; }
725
+ .ui-table .ui-sub { display: block; margin-top: 2px; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); font-weight: 400; }
576
726
 
577
727
  /* Empty state: `<div class="ui-empty"><p class="ui-empty__title">…</p><p class="ui-empty__body">…</p>
578
728
  <button class="ui-btn">…</button></div>`. Says what is missing and what to do next. */
@@ -1827,6 +1977,8 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
1827
1977
  @media (max-width: 760px) {
1828
1978
  /* The topbar is one flex row here: the middle slot takes what is left. */
1829
1979
  .gemboss-topbar-center { flex: 1 1 auto; justify-content: flex-start; }
1980
+ /* No actions: the empty slot would still take one flex `gap` (8px) off the search at 320px. */
1981
+ .gemboss-topbar-end:empty { display: none; }
1830
1982
  }
1831
1983
 
1832
1984
  .gnav-heading {
@@ -1835,7 +1987,8 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
1835
1987
  letter-spacing: 0.08em; text-transform: uppercase;
1836
1988
  color: var(--gemboss-text-muted);
1837
1989
  }
1838
- .gnav-scroll > .gnav-heading:first-child { padding-top: 2px; }
1990
+ .gnav-scroll > .gnav-heading:first-child,
1991
+ .gnav-scroll > .gnav-section:first-child > .gnav-heading:first-child { padding-top: 2px; }
1839
1992
 
1840
1993
  .gnav-count {
1841
1994
  margin-left: auto; flex: 0 0 auto; min-width: 20px; padding: 0 6px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "GemBoss design system for every surface in the fleet: the admin shell (topbar + rail + drawer) as React components, and the admin stylesheet exported as one CSS file.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -35,7 +35,8 @@
35
35
  "build:css": "node scripts/build-css.mjs",
36
36
  "prepack": "node scripts/build-css.mjs && node scripts/build-catalog.mjs",
37
37
  "typecheck": "tsc -p tsconfig.json --noEmit",
38
- "catalog": "node scripts/build-css.mjs && node scripts/build-catalog.mjs"
38
+ "catalog": "node scripts/build-css.mjs && node scripts/build-catalog.mjs",
39
+ "fleet": "node scripts/fleet.mjs"
39
40
  },
40
41
  "peerDependencies": {
41
42
  "react": ">=18"
package/src/app-shell.css CHANGED
@@ -20,6 +20,8 @@
20
20
  @media (max-width: 760px) {
21
21
  /* The topbar is one flex row here: the middle slot takes what is left. */
22
22
  .gemboss-topbar-center { flex: 1 1 auto; justify-content: flex-start; }
23
+ /* No actions: the empty slot would still take one flex `gap` (8px) off the search at 320px. */
24
+ .gemboss-topbar-end:empty { display: none; }
23
25
  }
24
26
 
25
27
  .gnav-heading {
@@ -28,7 +30,8 @@
28
30
  letter-spacing: 0.08em; text-transform: uppercase;
29
31
  color: var(--gemboss-text-muted);
30
32
  }
31
- .gnav-scroll > .gnav-heading:first-child { padding-top: 2px; }
33
+ .gnav-scroll > .gnav-heading:first-child,
34
+ .gnav-scroll > .gnav-section:first-child > .gnav-heading:first-child { padding-top: 2px; }
32
35
 
33
36
  .gnav-count {
34
37
  margin-left: auto; flex: 0 0 auto; min-width: 20px; padding: 0 6px;
@@ -143,6 +143,7 @@ export function groupSessions(sessions: AssistantSession[], L: SessionRailLabels
143
143
  export function SessionRail({
144
144
  sessions, unlistedIds = [], activeId, pendingId, busyIds = [], collapsed, onToggleCollapsed,
145
145
  onNewChat, newChatTitle, onOpen, onRename, onTogglePin, onDelete, onExport, footer, searchRef, labels, icons,
146
+ titleMaxLength = 200,
146
147
  }: {
147
148
  /** Pinned-first, newest-first — the order the rail shows them in. */
148
149
  sessions: AssistantSession[];
@@ -160,6 +161,9 @@ export function SessionRail({
160
161
  newChatTitle?: string;
161
162
  onOpen: (id: string) => void;
162
163
  onRename: (id: string, title: string) => void;
164
+ /** Longest title the rename box takes. Match the server's limit, or a long name is refused there
165
+ * and the row snaps back with no reason given (gemwatcher's store is VARCHAR(200)). */
166
+ titleMaxLength?: number;
163
167
  onTogglePin?: (session: AssistantSession) => void;
164
168
  onDelete: (id: string) => void;
165
169
  onExport?: (id: string) => void;
@@ -254,6 +258,7 @@ export function SessionRail({
254
258
  <div className="gemboss-assistant-thread-edit">
255
259
  <input className="gemboss-field"
256
260
  autoFocus
261
+ maxLength={titleMaxLength}
257
262
  value={editValue}
258
263
  onChange={(e) => setEditValue(e.target.value)}
259
264
  onKeyDown={(e) => {
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export {
2
2
  AppShell, MobileMenuButton, MobileNavBackdrop, Rail, RailBottom, RailCount, RailGroup, RailHeading,
3
- RailItem, RailScroll, RailSep, RailSub, RailSubItem, Shell, ShellMain, Topbar,
3
+ RailItem, RailScroll, RailSection, RailSep, RailSub, RailSubItem, Shell, ShellMain, Topbar,
4
4
  } from "./shell";
5
5
  export { GemMark } from "./GemMark";
6
6
  export { AuthError, AuthField, AuthFoot, AuthForm, AuthHelp, AuthLayout, PasswordField, AuthSubmit } from "./auth";
package/src/shell.tsx CHANGED
@@ -17,7 +17,7 @@
17
17
  * No router. `RailItem`/`RailSubItem` render an `<a>` unless given `as` — the admin passes
18
18
  * `next/link`, a hash-routed SPA passes nothing. Every other prop is forwarded untouched.
19
19
  */
20
- import React, { useEffect, useState } from "react";
20
+ import React, { useEffect, useId, useState } from "react";
21
21
 
22
22
  const cx = (...parts: Array<string | false | null | undefined>) => parts.filter(Boolean).join(" ");
23
23
 
@@ -162,6 +162,8 @@ export function AppShell({ brand, center, actions, rail, railBottom, railLabel,
162
162
  children?: React.ReactNode;
163
163
  }) {
164
164
  const [open, setOpen] = useState(false);
165
+ // The hamburger names what it opens (`aria-controls`), so a screen reader can jump to the rail.
166
+ const railId = useId();
165
167
  useEffect(() => {
166
168
  if (!open || !drawerCloseEvent) return;
167
169
  const close = () => setOpen(false);
@@ -183,13 +185,13 @@ export function AppShell({ brand, center, actions, rail, railBottom, railLabel,
183
185
  <Shell mobileNavOpen={open}>
184
186
  <Topbar>
185
187
  <div className="gemboss-topbar-start">
186
- <MobileMenuButton open={open} onToggle={() => setOpen((o) => !o)} labels={menuLabels} />
188
+ <MobileMenuButton open={open} onToggle={() => setOpen((o) => !o)} labels={menuLabels} aria-controls={railId} />
187
189
  {brand}
188
190
  </div>
189
191
  <div className="gemboss-topbar-center">{center}</div>
190
192
  <div className="gemboss-topbar-end">{actions}</div>
191
193
  </Topbar>
192
- <Rail label={railLabel} onClick={closeOnFollow}>
194
+ <Rail id={railId} label={railLabel} onClick={closeOnFollow}>
193
195
  <RailScroll>{rail}</RailScroll>
194
196
  {railBottom ? <RailBottom>{railBottom}</RailBottom> : null}
195
197
  </Rail>
@@ -205,6 +207,20 @@ export function RailHeading({ id, children }: { id?: string; children?: React.Re
205
207
  return <div className="gnav-heading" id={id}>{children}</div>;
206
208
  }
207
209
 
210
+ /** A family of rail items under its heading, announced as a group: `role="group"` labelled by the
211
+ * heading, so a screen reader reads "Market, group" before its items. A bare `RailHeading` followed by
212
+ * items is only text to it (gemwatcher lost its groups that way moving onto this package, 29/09/2026).
213
+ * Adds no box of its own; app-shell.css keeps the first-heading spacing for it. */
214
+ export function RailSection({ heading, children }: { heading: React.ReactNode; children?: React.ReactNode }) {
215
+ const id = useId();
216
+ return (
217
+ <div className="gnav-section" role="group" aria-labelledby={id}>
218
+ <RailHeading id={id}>{heading}</RailHeading>
219
+ {children}
220
+ </div>
221
+ );
222
+ }
223
+
208
224
  /** A count pill at the end of a `RailItem` (unread, drafts). Caps at 99+. */
209
225
  export function RailCount({ value, label }: { value: number; label?: string }) {
210
226
  if (!value) return null;
package/src/ui.css CHANGED
@@ -31,6 +31,10 @@
31
31
  --ui-accent-soft: var(--gemboss-active-bg, #f0f1f4); /* tinted bg for a selected/active state */
32
32
  --ui-accent-strong: var(--gemboss-active-fg, #1b1c1e); /* readable accent text ON --ui-accent-soft */
33
33
  --ui-text-strong: var(--gemboss-text-strong, #1b1c1e);
34
+ /* 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. */
35
+ --ui-success-fg: var(--gemboss-success);
36
+ --ui-warning-fg: var(--gemboss-primary-dark);
37
+ --ui-danger-fg: var(--gemboss-danger-strong);
34
38
  --ui-focus: var(--gemboss-primary, #1b1c1e);
35
39
  --ui-radius-sm: 6px;
36
40
  --ui-radius-md: 8px;
@@ -90,6 +94,10 @@
90
94
  --ui-accent-soft: var(--adm-accent-soft);
91
95
  --ui-accent-strong: var(--adm-accent-fg);
92
96
  --ui-text-strong: var(--adm-text-strong);
97
+ /* 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). */
98
+ --ui-success-fg: color-mix(in srgb, var(--gemboss-success) 70%, #ffffff);
99
+ --ui-warning-fg: color-mix(in srgb, #c99a2e 70%, #ffffff);
100
+ --ui-danger-fg: color-mix(in srgb, var(--gemboss-danger-strong) 70%, #ffffff);
93
101
  --ui-focus: var(--adm-accent);
94
102
  }
95
103
 
@@ -120,6 +128,11 @@
120
128
  padding: var(--ui-space-4); color: var(--ui-text);
121
129
  }
122
130
  .ui-card__title { font-size: 14px; font-weight: 600; margin: 0 0 var(--ui-space-2); }
131
+ /* Đầu thẻ có nút: `<div class="ui-card__head"><h2 class="ui-card__title">…</h2><button class="ui-btn">…</button></div>`.
132
+ Tiêu đề trái, hành động phải, xuống dòng khi hẹp (01/10/2026: partner-radar rel-head / agr). */
133
+ .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); }
134
+ .ui-card__head > .ui-card__title { margin: 0; }
135
+ .ui-card__head > :last-child:not(.ui-card__title) { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); }
123
136
 
124
137
  /* ── Select ──────────────────────────────────────────────────────────────────
125
138
  Native <select> reskinned; custom caret as an inline SVG data-URI (caret color
@@ -164,6 +177,85 @@
164
177
  }
165
178
  .ui-tag--chip.ui-tag--accent { color: var(--adm-accent-fg, var(--ui-accent)); border-color: var(--ui-accent); background: transparent; }
166
179
 
180
+ /* Danh sách link dạng chip: `<div class="ui-chips"><a class="ui-tag ui-tag--chip" href>…</a>…</div>` (01/10/2026,
181
+ 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
182
+ 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
183
+ ≥ 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).
184
+ Khe 8px: hai target nhỏ hơn 44px phải cách nhau ≥ 8px. */
185
+ .ui-chips { display: flex; flex-wrap: wrap; gap: var(--ui-space-2, 8px); margin: 0; padding: 0; list-style: none; }
186
+ .ui-chips > a.ui-tag--chip {
187
+ min-height: 28px; padding: 4px 12px; border-radius: var(--ui-radius-md, 8px);
188
+ font-family: inherit; font-size: var(--gemboss-fs-caption, 12px); color: var(--ui-text); text-decoration: none;
189
+ }
190
+ .ui-chips > a.ui-tag--chip:hover { background: var(--ui-surface-2); border-color: var(--ui-text-muted); }
191
+ .ui-chips > a.ui-tag--chip:focus-visible { outline: 2px solid var(--p-color-border-focus, #005bd3); outline-offset: 1px; }
192
+ /* 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.
193
+ Ô đang chọn = `aria-current="page"` (lọc là một URL), cùng tông với ui-seg đang chọn. */
194
+ .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; }
195
+
196
+ /* ── Tông màu cho tag: `ui-tag--success | --warning | --danger` (token ngữ nghĩa có sẵn). Trạng thái, nguồn,
197
+ kết quả kiểm — thay cho màu viết cứng ở từng surface. */
198
+ .ui-tag--success { background: var(--gemboss-success-soft); color: var(--gemboss-success-strong, var(--gemboss-success)); }
199
+ .ui-tag--warning { background: var(--gemboss-warning-soft); color: var(--gemboss-warning); }
200
+ .ui-tag--danger { background: var(--gemboss-danger-soft); color: var(--gemboss-danger-strong, var(--gemboss-danger)); }
201
+
202
+ /* ── Chữ phụ: `<p class="ui-muted">`, `<span class="ui-muted">` — mô tả, chú thích, dòng meta. Cỡ caption, màu muted.
203
+ Trước đây mỗi surface tự viết `.muted` (partner-radar: 180 chỗ). */
204
+ .ui-muted { color: var(--ui-text-muted); font-size: var(--gemboss-fs-caption); }
205
+
206
+ /* ── Tiêu đề trang: `<div class="ui-page-head"><h1>…</h1><p>…</p></div>`. Đưa lên NGUYÊN VĂN từ admin
207
+ `.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
208
+ 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. */
209
+ .ui-page-head { margin-bottom: 20px; }
210
+ .ui-page-head h1 { color: var(--ui-text-strong); font-size: 22px; font-weight: 650; letter-spacing: -0.01em; }
211
+ .ui-page-head p { color: var(--ui-text-muted); font-size: 14px; margin-top: 4px; }
212
+
213
+ /* ── 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.
214
+ Đưa lên từ admin `.gemboss-admin-toolbar`. Chỉ BỐ CỤC: ô tìm là `gemboss-field gemboss-field--group`. */
215
+ .ui-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-bottom: 20px; }
216
+
217
+ /* ── Ô số liệu: `<div class="ui-metrics"><div class="ui-metric"><span class="ui-metric__val">12</span>
218
+ <span class="ui-metric__lbl">Nhãn</span></div></div>`; tông `ui-metric--success | --warning | --danger`.
219
+ Đư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). */
220
+ .ui-metrics { display: flex; flex-wrap: wrap; gap: 8px; margin: 2px 0; }
221
+ .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; }
222
+ /* 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
223
+ (visual-gate đỏ ở partner-radar /ops 01/10/2026 khi link nằm trong __val). */
224
+ a.ui-metric { text-decoration: none; color: inherit; }
225
+ a.ui-metric:hover { border-color: var(--ui-text-muted); }
226
+ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
227
+ .ui-metric__val { font-size: 17px; font-weight: 700; color: var(--ui-text); font-variant-numeric: tabular-nums; line-height: 1.15; }
228
+ .ui-metric__lbl { font-size: var(--gemboss-fs-caption); font-weight: 500; color: var(--ui-text-muted); }
229
+ .ui-metric--success { border-color: color-mix(in srgb, var(--gemboss-success) 40%, transparent); }
230
+ .ui-metric--success .ui-metric__val { color: var(--ui-success-fg); }
231
+ .ui-metric--warning { border-color: color-mix(in srgb, #c99a2e 40%, transparent); }
232
+ .ui-metric--warning .ui-metric__val { color: var(--ui-warning-fg); }
233
+ .ui-metric--danger { border-color: color-mix(in srgb, var(--gemboss-danger-strong) 40%, transparent); }
234
+ .ui-metric--danger .ui-metric__val { color: var(--ui-danger-fg); }
235
+
236
+ /* ── 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ơ.
237
+ Lưới tự giãn: mỗi ô ≥ 160px, xuống dòng khi hẹp. */
238
+ .ui-dl { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px 16px; margin: 0; }
239
+ .ui-dl > div { min-width: 0; }
240
+ .ui-dl dt { font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
241
+ .ui-dl dd { margin: 4px 0 0; font-weight: 600; color: var(--ui-text); overflow-wrap: anywhere; }
242
+
243
+ /* ── 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
244
+ 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.
245
+ 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. */
246
+ .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); }
247
+ .ui-notice--warning { background: var(--gemboss-warning-soft); border-color: var(--gemboss-warning-border); color: var(--gemboss-warning); }
248
+ .ui-notice--danger { background: var(--gemboss-danger-soft); border-color: var(--gemboss-danger-border); color: var(--gemboss-danger-strong); }
249
+ .ui-notice--success { background: var(--gemboss-success-soft); border-color: var(--gemboss-success-border); color: var(--gemboss-success-strong); }
250
+
251
+ /* ── 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>
252
+ <a href rel="next">Sau ›</a></nav>`. Link ≥ 32px (≥ 24px WCAG 2.5.8), 44px dưới ngón tay (khối coarse). */
253
+ .ui-pager { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 16px 0; }
254
+ .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); }
255
+ .ui-pager > a:hover { background: var(--ui-surface-2); }
256
+ .ui-pager > a:focus-visible { outline: 2px solid var(--p-color-border-focus, #005bd3); outline-offset: 1px; }
257
+ .ui-pager > [aria-current="page"] { color: var(--ui-text-muted); font-size: var(--gemboss-fs-caption); }
258
+
167
259
  /* ── Badge ────────────────────────────────────────────────────────────────────
168
260
  Uppercase mono pill for a status/label (was gemboss-admin-storage-badge). */
169
261
  .ui-badge {
@@ -219,13 +311,16 @@
219
311
  Grouped toggle: icon squares (.ui-seg button) or text tabs (.is-text). The
220
312
  selected segment uses the accent-soft state. */
221
313
  .ui-seg { display: inline-flex; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); overflow: hidden; }
222
- .ui-seg button {
314
+ .ui-seg :is(button, a) {
223
315
  display: inline-grid; place-items: center; width: 36px; height: 36px;
224
316
  border: 0; background: transparent; color: var(--ui-text-muted); cursor: pointer; font-family: inherit;
225
317
  }
226
- .ui-seg button.is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
227
- .ui-seg button + button { border-left: 1px solid var(--ui-border); }
228
- .ui-seg button:hover { background: var(--ui-surface-2); color: var(--ui-text); }
318
+ .ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
319
+ .ui-seg :is(button, a) + :is(button, a) { border-left: 1px solid var(--ui-border); }
320
+ .ui-seg :is(button, a):hover { background: var(--ui-surface-2); color: var(--ui-text); }
321
+ /* Lọc / chế độ xem bằng LINK (trang server-render, chọn = đổi URL): `<a aria-current="page">` là ô đang chọn. */
322
+ .ui-seg a { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; color: var(--ui-text-muted); }
323
+ .ui-seg a[aria-current="page"],
229
324
  .ui-seg button.active { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
230
325
  .ui-seg svg { width: 16px; height: 16px; fill: currentColor; }
231
326
 
@@ -307,7 +402,9 @@
307
402
  decision, not an oversight; it gets its own measurement pass or none. */
308
403
  .ui-btn:not(.ui-btn--sm),
309
404
  .ui-stat.is-clickable,
310
- .ui-seg button { min-height: 44px; }
405
+ .ui-chips > a.ui-tag--chip,
406
+ .ui-pager > a,
407
+ .ui-seg :is(button, a) { min-height: 44px; }
311
408
  /* A clickable stat whose label is one short token ("7d" on /admin/gem-intelligence) measured 40x44
312
409
  in the 13/09/2026 sweep: the height floor held, the width did not. */
313
410
  .ui-stat.is-clickable { min-width: 44px; justify-content: center; }
@@ -316,7 +413,7 @@
316
413
  .ui-select.ui-select,
317
414
  .ui-select--slim.ui-select--slim { min-height: 44px; }
318
415
  /* The icon-only segment is 36 WIDE as well — a target is an area. */
319
- .ui-seg button { min-width: 44px; }
416
+ .ui-seg :is(button, a) { min-width: 44px; }
320
417
  }
321
418
 
322
419
  /* ── Skeleton ─────────────────────────────────────────────────────────────────
@@ -340,6 +437,46 @@
340
437
 
341
438
  }
342
439
 
440
+ /* ── Resizer: the seam you drag between two panels ────────────────────────────
441
+ One look for every split in the fleet: the admin's editors (chat | preview) today; made for gemcare's
442
+ 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
443
+ placed strip) belongs to the surface. While a drag runs the surface puts `ui-resizing` on <body>
444
+ (and may put `dragging` on the seam itself to keep its grip lit).
445
+ Markup: <div class="ui-resizer" role="separator" aria-orientation="vertical" tabindex="0"
446
+ aria-label="…" aria-valuenow aria-valuemin aria-valuemax>. Pointer events, not mouse events (touch
447
+ is a pointer); arrow keys move it — a focusable separator that ignores the keyboard is INERT. */
448
+ /* `touch-action: none` is what makes a FINGER drag reach this element at all. `preventDefault()` on
449
+ `pointerdown` does not stop panning: the browser hands the gesture to the nearest scroller and sends
450
+ `pointercancel`, so the seam jumps once and stops. */
451
+ .ui-resizer { cursor: col-resize; position: relative; touch-action: none; outline: none; }
452
+ /* The grip pill is invisible at rest (a clean seam) and fades in over the zone, on keyboard focus,
453
+ and while a drag runs. Focus shows the pill rather than a ring: on a 5px seam a ring is a smear. */
454
+ .ui-resizer::before {
455
+ content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
456
+ width: 4px; height: 44px; border-radius: var(--gemboss-radius-full);
457
+ background: var(--gemboss-primary);
458
+ opacity: 0;
459
+ transition: opacity 0.14s ease, height 0.14s ease;
460
+ }
461
+ .ui-resizer:hover::before,
462
+ .ui-resizer:focus-visible::before,
463
+ .ui-resizer.dragging::before,
464
+ body.ui-resizing .ui-resizer::before { opacity: 1; height: 52px; }
465
+ /* A FINGER NEEDS MORE THAN 5px, and the line must still look like 5px. WCAG 2.5.8 puts the floor for
466
+ this kind of target at 24px, so the HIT area grows and the visible seam does not — widening the
467
+ element itself would also move the columns. Only where the pointer is coarse. */
468
+ @media (pointer: coarse) {
469
+ .ui-resizer::after {
470
+ content: ""; position: absolute; top: 0; bottom: 0; left: -14px; right: -14px;
471
+ touch-action: none; /* the band must claim the gesture too, or the scroller still wins */
472
+ z-index: 2; /* above the panels either side, transparent */
473
+ }
474
+ }
475
+ body.ui-resizing, body.ui-resizing * { cursor: col-resize !important; user-select: none !important; }
476
+ /* While dragging, a full-viewport transparent shield sits above everything — iframes included — so the
477
+ pointer stream is never captured by a cross-document frame (the classic "stuck cursor"). */
478
+ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999; cursor: col-resize; }
479
+
343
480
  /* ── Tabs · Table · Empty state ───────────────────────────────────────────────
344
481
  Plain-CSS primitives for surfaces that are NOT React (gemcare's vanilla dashboard reads this
345
482
  file through `npm run export:ui-css`). The React app has Polaris <Tabs>/<DataTable>/<EmptyState>
@@ -354,7 +491,8 @@
354
491
  display: flex; gap: var(--ui-space-1); border-bottom: 1px solid var(--ui-border);
355
492
  overflow-x: auto; scrollbar-width: none;
356
493
  }
357
- .ui-tabs > button {
494
+ .ui-tabs > button,
495
+ .ui-tabs > a {
358
496
  position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
359
497
  min-height: 44px; padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
360
498
  border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;
@@ -362,9 +500,15 @@
362
500
  color: var(--ui-text-muted);
363
501
  transition: color var(--gemboss-transition-fast) ease, background-color var(--gemboss-transition-fast) ease;
364
502
  }
365
- .ui-tabs > button:hover { color: var(--ui-text); background: var(--ui-surface-2); }
366
- .ui-tabs > button:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
503
+ .ui-tabs > button:hover,
504
+ .ui-tabs > a:hover { color: var(--ui-text); background: var(--ui-surface-2); }
505
+ .ui-tabs > button:focus-visible,
506
+ .ui-tabs > a:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
507
+ .ui-tabs > a { text-decoration: none; }
508
+ /* 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. */
509
+ .ui-tabs > a[aria-current="page"],
367
510
  .ui-tabs > button[aria-selected="true"] { color: var(--ui-text-strong); font-weight: 600; }
511
+ .ui-tabs > a[aria-current="page"]::after,
368
512
  .ui-tabs > button[aria-selected="true"]::after {
369
513
  content: ""; position: absolute; left: var(--ui-space-3); right: var(--ui-space-3); bottom: 0;
370
514
  height: 2px; border-radius: var(--gemboss-radius-full); background: var(--ui-accent);
@@ -390,6 +534,12 @@
390
534
  .ui-table tbody tr:last-child > td { border-bottom: 0; }
391
535
  .ui-table tbody tr:hover { background: var(--ui-surface-2); }
392
536
  .ui-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
537
+ /* 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>`.
538
+ Trước đây mỗi surface tự viết `.muted` riêng trong ô bảng (partner-radar, gemwatcher). */
539
+ /* 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).
540
+ Chữ 13px cho hộp 17px; visual-gate đo đỏ ở partner-radar /competitors 01/10/2026. inline-flex giữ nó trên dòng. */
541
+ .ui-table td > a { display: inline-flex; align-items: center; min-height: 24px; }
542
+ .ui-table .ui-sub { display: block; margin-top: 2px; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); font-weight: 400; }
393
543
 
394
544
  /* Empty state: `<div class="ui-empty"><p class="ui-empty__title">…</p><p class="ui-empty__body">…</p>
395
545
  <button class="ui-btn">…</button></div>`. Says what is missing and what to do next. */