@gemboss/ui 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -169,6 +169,11 @@ import { AppShell, BrandLockup, AccountMenu, AccountMenuItem, Icon, RailItem } f
169
169
  applications that integrate with Shopify. Draw a missing one HERE, not in the surface: every rail
170
170
  icon must keep `fill="none"` on its inner `<g>`, because `.gnav-ico svg { fill: currentColor }`
171
171
  outranks an attribute on the `<svg>` itself and paints an outline icon solid.
172
+ - `Menu` / `MenuItem` / `MenuSeparator` (0.2.1): the "⋯" action menu. Same `<details>` rule as the account
173
+ menu (works as plain markup; with React it closes on an outside click, on Escape with focus back on the
174
+ trigger, and after a button row's handler). Rows share the account menu's row style. `MenuItem` is a link
175
+ (`href`) or a button; `disabled` on a link drops the href and sets `aria-disabled`; `className` is added
176
+ to the row's class.
172
177
 
173
178
  ## The catalog: every token, class and component, with its markup
174
179
 
@@ -215,6 +220,7 @@ something the base cannot express, add a token or a prop here, and every surface
215
220
  |---|---|
216
221
  | `gemboss-ui.css` | built at pack time: `apps/admin/scripts/export-ui-css.mjs` (tokens + kit + field + `packages/ui/src/shell.css`, with Polaris `--p-*` resolved to their fallbacks) + `packages/ui/src/app-shell.css` + `packages/ui/src/auth.css` + `packages/ui/src/assistant.css` |
217
222
  | `packages/ui/src/shell.css` | the SHARED SHELL (rail, topbar, main, mobile drawer, the assistant's thread rail, popover surface, account chip). The admin's `gemboss-shell.css` @imports it on line 2; the export refuses a shared-shell rule in the admin's sheet or an admin-only rule here (one owner). |
223
+ | `packages/ui/src/kit-plain.css` | primitives for surfaces that render plain HTML, not React (0.2.1): menu, key/value list, icon button. Shipped inside `gemboss-ui.css`; the admin does not import it (its first-load CSS budget has ~0.1 KB headroom). |
218
224
  | `packages/ui/src/**/*.tsx` | the components, as TypeScript source. Next: add `@gemboss/ui` to `transpilePackages`. Vite compiles it as is. |
219
225
 
220
226
  `gemboss-ui.css` is never committed and never edited. To change a colour or a radius, change
@@ -27,7 +27,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
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
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>
30
+ <main><header class="cat-head"><h1>@gemboss/ui 0.2.1</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>be1da9b1254e</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;
@@ -37,8 +37,9 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
37
37
  &lt;button type=&quot;button&quot; class=&quot;ui-btn ui-btn--primary&quot; disabled&gt;Đang lưu…&lt;/button&gt;
38
38
  &lt;a class=&quot;ui-btn&quot; href=&quot;#buttons&quot;&gt;Link dạng nút&lt;/a&gt;
39
39
  &lt;/div&gt;</code></pre></details></figure></section>
40
- <section id="fields"><h2>Ô nhập</h2><p>Mọi ô chữ dùng <code>gemboss-field</code>: cùng viền, cùng focus, 44px trên màn chạm.</p><figure class="cat-demo"><figcaption>Ô chữ · ô có biểu tượng · chọn · vùng chữ</figcaption><div class="cat-live"><div class="cat-stack"><input class="gemboss-field" placeholder="Tên chiến dịch"><label class="gemboss-field gemboss-field--group"><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"><circle cx="8.8" cy="8.8" r="5.3"></circle><path d="M12.8 12.8 17 17"></path></g></svg><input class="gemboss-field__bare" placeholder="Tìm store theo domain…"></label><select class="ui-select"><option>Mọi ngành</option><option>Mỹ phẩm</option></select><textarea class="gemboss-field" rows="4" placeholder="Ghi chú"></textarea></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;cat-stack&quot;&gt;
41
- &lt;input class=&quot;gemboss-field&quot; placeholder=&quot;Tên chiến dịch&quot;&gt;
40
+ <section id="fields"><h2>Ô nhập</h2><p>Mọi ô chữ dùng <code>gemboss-field</code>: cùng viền, cùng focus, 44px trên màn chạm.</p><figure class="cat-demo"><figcaption>Ô chữ · ô có biểu tượng · chọn · vùng chữ</figcaption><div class="cat-live"><div class="cat-stack"><label class="ui-label" for="cat-f1">Tên chiến dịch</label><input id="cat-f1" class="gemboss-field" placeholder="Tên chiến dịch"><label class="gemboss-field gemboss-field--group"><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"><circle cx="8.8" cy="8.8" r="5.3"></circle><path d="M12.8 12.8 17 17"></path></g></svg><input class="gemboss-field__bare" placeholder="Tìm store theo domain…"></label><select class="ui-select"><option>Mọi ngành</option><option>Mỹ phẩm</option></select><textarea class="gemboss-field" rows="4" placeholder="Ghi chú"></textarea><textarea class="ui-textarea" placeholder="ui-textarea: cùng họ, cao sẵn 96px"></textarea><label class="ui-check"><input type="checkbox" checked> Chỉ thứ đã duyệt</label><label class="ui-check"><input type="checkbox"> Khoá preset</label></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;cat-stack&quot;&gt;
41
+ &lt;label class=&quot;ui-label&quot; for=&quot;cat-f1&quot;&gt;Tên chiến dịch&lt;/label&gt;
42
+ &lt;input id=&quot;cat-f1&quot; class=&quot;gemboss-field&quot; placeholder=&quot;Tên chiến dịch&quot;&gt;
42
43
  &lt;label class=&quot;gemboss-field gemboss-field--group&quot;&gt;
43
44
  &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;
44
45
  &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;
@@ -56,6 +57,12 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
56
57
  &lt;/select&gt;
57
58
  &lt;textarea class=&quot;gemboss-field&quot; rows=&quot;4&quot; placeholder=&quot;Ghi chú&quot;&gt;
58
59
  &lt;/textarea&gt;
60
+ &lt;textarea class=&quot;ui-textarea&quot; placeholder=&quot;ui-textarea: cùng họ, cao sẵn 96px&quot;&gt;
61
+ &lt;/textarea&gt;
62
+ &lt;label class=&quot;ui-check&quot;&gt;
63
+ &lt;input type=&quot;checkbox&quot; checked&gt; Chỉ thứ đã duyệt&lt;/label&gt;
64
+ &lt;label class=&quot;ui-check&quot;&gt;
65
+ &lt;input type=&quot;checkbox&quot;&gt; Khoá preset&lt;/label&gt;
59
66
  &lt;/div&gt;</code></pre></details></figure></section>
60
67
  <section id="choice"><h2>Tab, nhóm nút, thẻ</h2><figure class="cat-demo"><figcaption>Tab (<code>aria-selected</code> quyết định tab đang chọn)</figcaption><div class="cat-live"><div class="ui-tabs" role="tablist"><button type="button" role="tab" aria-selected="true">Tổng quan</button><button type="button" role="tab" aria-selected="false">Chi tiết</button><button type="button" role="tab" aria-selected="false" disabled>Sắp có</button></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;ui-tabs&quot; role=&quot;tablist&quot;&gt;
61
68
  &lt;button type=&quot;button&quot; role=&quot;tab&quot; aria-selected=&quot;true&quot;&gt;Tổng quan&lt;/button&gt;
@@ -65,6 +72,44 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
65
72
  &lt;button type=&quot;button&quot; aria-pressed=&quot;true&quot; class=&quot;is-text active&quot;&gt;Kanban&lt;/button&gt;
66
73
  &lt;button type=&quot;button&quot; aria-pressed=&quot;false&quot; class=&quot;is-text&quot;&gt;Nhóm&lt;/button&gt;
67
74
  &lt;button type=&quot;button&quot; aria-pressed=&quot;false&quot; class=&quot;is-text&quot;&gt;Danh sách&lt;/button&gt;
75
+ &lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Nút icon · menu (<code>details.ui-menu</code>, mở không cần script) · danh sách nhãn-giá trị</figcaption><div class="cat-live"><div class="cat-row" style="align-items:flex-start;min-height:260px"><button type="button" class="ui-btn ui-btn--icon" 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><details class="ui-menu" open><summary class="ui-btn ui-btn--icon" aria-label="Thêm thao tác" title="Thêm thao tác"><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"><circle cx="4.5" cy="10" r="1"></circle><circle cx="10" cy="10" r="1"></circle><circle cx="15.5" cy="10" r="1"></circle></g></svg></summary><div class="ui-menu__list gemboss-popover-surface"><button type="button" class="ui-menu__item">Chép #12<span class="ui-menu__hint">dán vào Claude Code</span></button><a class="ui-menu__item" href="#choice">Mở trang riêng</a><hr class="ui-menu__sep"/><button type="button" class="ui-menu__item ui-menu__item--danger">Xoá…</button></div></details><dl class="ui-kv" style="flex-basis:100%"><dt>Người dựng</dt><dd>Lily</dd><dt>Người duyệt</dt><dd>chưa</dd><dt>Báo cáo</dt><dd>research/kho-section-can-build-2026-09-28.md</dd></dl></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;cat-row&quot; style=&quot;align-items:flex-start;min-height:260px&quot;&gt;
76
+ &lt;button type=&quot;button&quot; class=&quot;ui-btn ui-btn--icon&quot; aria-label=&quot;Đóng&quot;&gt;
77
+ &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;
78
+ &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;
79
+ &lt;path d=&quot;M5 5l10 10M15 5 5 15&quot;&gt;
80
+ &lt;/path&gt;
81
+ &lt;/g&gt;
82
+ &lt;/svg&gt;
83
+ &lt;/button&gt;
84
+ &lt;details class=&quot;ui-menu&quot; open&gt;
85
+ &lt;summary class=&quot;ui-btn ui-btn--icon&quot; aria-label=&quot;Thêm thao tác&quot; title=&quot;Thêm thao tác&quot;&gt;
86
+ &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;
87
+ &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;
88
+ &lt;circle cx=&quot;4.5&quot; cy=&quot;10&quot; r=&quot;1&quot;&gt;
89
+ &lt;/circle&gt;
90
+ &lt;circle cx=&quot;10&quot; cy=&quot;10&quot; r=&quot;1&quot;&gt;
91
+ &lt;/circle&gt;
92
+ &lt;circle cx=&quot;15.5&quot; cy=&quot;10&quot; r=&quot;1&quot;&gt;
93
+ &lt;/circle&gt;
94
+ &lt;/g&gt;
95
+ &lt;/svg&gt;
96
+ &lt;/summary&gt;
97
+ &lt;div class=&quot;ui-menu__list gemboss-popover-surface&quot;&gt;
98
+ &lt;button type=&quot;button&quot; class=&quot;ui-menu__item&quot;&gt;Chép #12&lt;span class=&quot;ui-menu__hint&quot;&gt;dán vào Claude Code&lt;/span&gt;
99
+ &lt;/button&gt;
100
+ &lt;a class=&quot;ui-menu__item&quot; href=&quot;#choice&quot;&gt;Mở trang riêng&lt;/a&gt;
101
+ &lt;hr class=&quot;ui-menu__sep&quot;/&gt;
102
+ &lt;button type=&quot;button&quot; class=&quot;ui-menu__item ui-menu__item--danger&quot;&gt;Xoá…&lt;/button&gt;
103
+ &lt;/div&gt;
104
+ &lt;/details&gt;
105
+ &lt;dl class=&quot;ui-kv&quot; style=&quot;flex-basis:100%&quot;&gt;
106
+ &lt;dt&gt;Người dựng&lt;/dt&gt;
107
+ &lt;dd&gt;Lily&lt;/dd&gt;
108
+ &lt;dt&gt;Người duyệt&lt;/dt&gt;
109
+ &lt;dd&gt;chưa&lt;/dd&gt;
110
+ &lt;dt&gt;Báo cáo&lt;/dt&gt;
111
+ &lt;dd&gt;research/kho-section-can-build-2026-09-28.md&lt;/dd&gt;
112
+ &lt;/dl&gt;
68
113
  &lt;/div&gt;</code></pre></details></figure><figure class="cat-demo"><figcaption>Thẻ · nhãn đếm</figcaption><div class="cat-live"><div class="cat-row"><span class="ui-tag">Nháp</span><span class="ui-tag ui-tag--accent">Đã duyệt</span><span class="ui-tag ui-tag--chip">SECTION</span><span class="ui-badge">12</span></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;cat-row&quot;&gt;
69
114
  &lt;span class=&quot;ui-tag&quot;&gt;Nháp&lt;/span&gt;
70
115
  &lt;span class=&quot;ui-tag ui-tag--accent&quot;&gt;Đã duyệt&lt;/span&gt;
@@ -376,7 +421,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
376
421
  &lt;/aside&gt;
377
422
  &lt;/div&gt;
378
423
  &lt;/main&gt;</code></pre></details></figure></section>
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;
424
+ <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">23/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;
380
425
  &lt;aside class=&quot;gemboss-assistant-rail&quot;&gt;
381
426
  &lt;div class=&quot;gemboss-assistant-rail-top&quot;&gt;
382
427
  &lt;button type=&quot;button&quot; class=&quot;gemboss-assistant-newchat&quot; title=&quot;Phiên mới&quot;&gt;
@@ -425,7 +470,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
425
470
  &lt;/svg&gt;
426
471
  &lt;span class=&quot;gemboss-assistant-thread-title&quot;&gt;So sánh advertorial với listicle&lt;/span&gt;
427
472
  &lt;/span&gt;
428
- &lt;span class=&quot;gemboss-assistant-thread-meta&quot;&gt;22/8/2026&lt;/span&gt;
473
+ &lt;span class=&quot;gemboss-assistant-thread-meta&quot;&gt;23/8/2026&lt;/span&gt;
429
474
  &lt;/button&gt;
430
475
  &lt;div class=&quot;gemboss-assistant-thread-actions&quot;&gt;
431
476
  &lt;button type=&quot;button&quot; class=&quot;gemboss-assistant-thread-act&quot; aria-label=&quot;Options&quot;&gt;
@@ -496,7 +541,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
496
541
  &lt;/div&gt;
497
542
  &lt;/aside&gt;
498
543
  &lt;/div&gt;</code></pre></details></figure></section>
499
- <section id="icons"><h2>Icon</h2><p>Bộ icon GemBoss: 39 icon, lưới 20px, nét 1.5. <code>fill="none"</code> nằm trên <code>&lt;g&gt;</code>, không trên <code>&lt;svg&gt;</code>.</p><div class="cat-icons"><div class="gnav-ico cat-icon" title="home"><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="M3.5 9 10 3.5 16.5 9"></path><path d="M5 7.8v8.7h10V7.8"></path><path d="M8.5 16.5v-4h3v4"></path></g></svg><code>home</code></div><div class="gnav-ico cat-icon" title="dashboard"><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"><rect x="3" y="3" width="6" height="6" rx="1.3"></rect><rect x="11" y="3" width="6" height="6" rx="1.3"></rect><rect x="3" y="11" width="6" height="6" rx="1.3"></rect><rect x="11" y="11" width="6" height="6" rx="1.3"></rect></g></svg><code>dashboard</code></div><div class="gnav-ico cat-icon" title="board"><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"><rect x="3" y="3.5" width="4" height="13" rx="1.2"></rect><rect x="8" y="3.5" width="4" height="9" rx="1.2"></rect><rect x="13" y="3.5" width="4" height="6" rx="1.2"></rect></g></svg><code>board</code></div><div class="gnav-ico cat-icon" title="layout"><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"><rect x="3.5" y="3" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="8.25" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="13.5" width="13" height="3.5" rx="1"></rect></g></svg><code>layout</code></div><div class="gnav-ico cat-icon" title="layers"><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="M10 3 17 6.8 10 10.6 3 6.8z"></path><path d="M3 10.1l7 3.8 7-3.8"></path><path d="M3 13.4l7 3.8 7-3.8"></path></g></svg><code>layers</code></div><div class="gnav-ico cat-icon" title="document"><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 2.8h6.8l3.2 3.2v11.2H5z"></path><path d="M11.8 2.8V6H15M7.5 10h5M7.5 13h5"></path></g></svg><code>document</code></div><div class="gnav-ico cat-icon" title="article"><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"><rect x="3" y="3.5" width="14" height="13" rx="1.5"></rect><path d="M6 7h8M6 10h8M6 13h5"></path></g></svg><code>article</code></div><div class="gnav-ico cat-icon" title="page"><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"><rect x="2.5" y="3.5" width="15" height="13" rx="2"></rect><path d="M2.5 7.5h15M6 11h8M6 13.5h5"></path></g></svg><code>page</code></div><div class="gnav-ico cat-icon" title="image"><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"><rect x="3" y="3.5" width="14" height="13" rx="1.8"></rect><circle cx="7.3" cy="7.8" r="1.3"></circle><path d="M3.5 15l4.2-4.2 3 3 2.2-2.2 3.6 3.6"></path></g></svg><code>image</code></div><div class="gnav-ico cat-icon" title="folder"><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="M2.8 5.3c0-.8.6-1.3 1.3-1.3h3.4l1.8 2h6.6c.8 0 1.3.6 1.3 1.3v7.7c0 .8-.6 1.3-1.3 1.3H4.1c-.8 0-1.3-.6-1.3-1.3z"></path></g></svg><code>folder</code></div><div class="gnav-ico cat-icon" title="list"><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="M8 5h9M8 10h9M8 15h9"></path><circle cx="4" cy="5" r=".9"></circle><circle cx="4" cy="10" r=".9"></circle><circle cx="4" cy="15" r=".9"></circle></g></svg><code>list</code></div><div class="gnav-ico cat-icon" title="activity"><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="M2.5 10h3l2-5 3 10 2-5h5"></path></g></svg><code>activity</code></div><div class="gnav-ico cat-icon" title="chart"><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="M3 16.5h14"></path><rect x="4.5" y="9" width="2.8" height="5.5" rx=".7"></rect><rect x="8.6" y="5" width="2.8" height="9.5" rx=".7"></rect><rect x="12.7" y="11" width="2.8" height="3.5" rx=".7"></rect></g></svg><code>chart</code></div><div class="gnav-ico cat-icon" title="radar"><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"><circle cx="10" cy="10" r="7"></circle><circle cx="10" cy="10" r="3.5"></circle><path d="M10 10l5-5"></path></g></svg><code>radar</code></div><div class="gnav-ico cat-icon" title="map"><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"><rect x="3" y="3" width="14" height="14" rx="2"></rect><path d="M3 7.7h14M3 12.3h14M7.7 3v14M12.3 3v14"></path></g></svg><code>map</code></div><div class="gnav-ico cat-icon" title="globe"><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"><circle cx="10" cy="10" r="7"></circle><path d="M3 10h14M10 3c2 2 2.8 4.3 2.8 7s-.8 5-2.8 7c-2-2-2.8-4.3-2.8-7s.8-5 2.8-7z"></path></g></svg><code>globe</code></div><div class="gnav-ico cat-icon" title="megaphone"><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="M3 8v4h3l6 4V4L6 8H3z"></path><path d="M15 7.5a3.5 3.5 0 0 1 0 5"></path></g></svg><code>megaphone</code></div><div class="gnav-ico cat-icon" title="star"><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="M10 2.8l2.2 4.5 5 .7-3.6 3.5.9 5-4.5-2.4-4.5 2.4.9-5L2.8 8l5-.7z"></path></g></svg><code>star</code></div><div class="gnav-ico cat-icon" title="eye"><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="M1.8 10S4.7 4.8 10 4.8 18.2 10 18.2 10 15.3 15.2 10 15.2 1.8 10 1.8 10z"></path><circle cx="10" cy="10" r="2.5"></circle></g></svg><code>eye</code></div><div class="gnav-ico cat-icon" title="inbox"><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="M3 11l2-7h10l2 7v5H3z"></path><path d="M3 11h4l1 2h4l1-2h4"></path></g></svg><code>inbox</code></div><div class="gnav-ico cat-icon" title="bell"><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 13.5V9a5 5 0 0 1 10 0v4.5l1.3 1.5H3.7z"></path><path d="M8.3 17.2a1.9 1.9 0 0 0 3.4 0"></path></g></svg><code>bell</code></div><div class="gnav-ico cat-icon" title="chat"><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="M3 4.5h14v9H8.5L5 16.5v-3H3z"></path></g></svg><code>chat</code></div><div class="gnav-ico cat-icon" title="money"><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"><circle cx="10" cy="10" r="7"></circle><path d="M12 7.6c-.5-.7-1.2-1-2-1-1.1 0-2 .7-2 1.6 0 2.1 4 1.2 4 3.4 0 .9-.9 1.6-2 1.6-.8 0-1.6-.3-2.1-1M10 5v1.6M10 13.2V15"></path></g></svg><code>money</code></div><div class="gnav-ico cat-icon" title="store"><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="M3.2 7.5 4.5 3.5h11l1.3 4"></path><path d="M3.2 7.5c0 1.3 1 2.2 2.3 2.2s2.2-1 2.2-2.2c0 1.3 1 2.2 2.3 2.2s2.3-1 2.3-2.2c0 1.3.9 2.2 2.2 2.2s2.3-.9 2.3-2.2"></path><path d="M4.5 9.5v7h11v-7M8.5 16.5v-3.5h3v3.5"></path></g></svg><code>store</code></div><div class="gnav-ico cat-icon" title="user"><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"><circle cx="10" cy="7" r="3.2"></circle><path d="M3.8 16.5c.8-2.8 3.3-4.5 6.2-4.5s5.4 1.7 6.2 4.5"></path></g></svg><code>user</code></div><div class="gnav-ico cat-icon" title="users"><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"><circle cx="7.5" cy="7.3" r="2.8"></circle><path d="M2.5 16.3c.6-2.5 2.5-4 5-4s4.4 1.5 5 4"></path><path d="M12.3 4.8a2.8 2.8 0 0 1 0 5.2M14.3 12.5c1.6.4 2.7 1.7 3.2 3.8"></path></g></svg><code>users</code></div><div class="gnav-ico cat-icon" title="calendar"><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"><rect x="3" y="4.5" width="14" height="12.5" rx="1.8"></rect><path d="M3 8.5h14M7 3v3M13 3v3"></path></g></svg><code>calendar</code></div><div class="gnav-ico cat-icon" title="clock"><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"><circle cx="10" cy="10" r="7"></circle><path d="M10 6v4l2.8 1.8"></path></g></svg><code>clock</code></div><div class="gnav-ico cat-icon" title="search"><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"><circle cx="8.8" cy="8.8" r="5.3"></circle><path d="M12.8 12.8 17 17"></path></g></svg><code>search</code></div><div class="gnav-ico cat-icon" title="link"><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="M8.6 11.4a3 3 0 0 0 4.2 0l2.6-2.6a3 3 0 0 0-4.2-4.2l-1 1"></path><path d="M11.4 8.6a3 3 0 0 0-4.2 0l-2.6 2.6a3 3 0 0 0 4.2 4.2l1-1"></path></g></svg><code>link</code></div><div class="gnav-ico cat-icon" title="sliders"><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="M3 5h14M3 10h14M3 15h14"></path><circle cx="7" cy="5" r="1.6"></circle><circle cx="13" cy="10" r="1.6"></circle><circle cx="9" cy="15" r="1.6"></circle></g></svg><code>sliders</code></div><div class="gnav-ico cat-icon" title="settings"><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="M15.52 9.08 17.15 9.15 17.15 10.85 15.52 10.92 14.56 13.26 15.66 14.45 14.45 15.66 13.26 14.56 10.92 15.52 10.85 17.15 9.15 17.15 9.08 15.52 6.74 14.56 5.55 15.66 4.34 14.45 5.44 13.26 4.48 10.92 2.85 10.85 2.85 9.15 4.48 9.08 5.44 6.74 4.34 5.55 5.55 4.34 6.74 5.44 9.08 4.48 9.15 2.85 10.85 2.85 10.92 4.48 13.26 5.44 14.45 4.34 15.66 5.55 14.56 6.74Z"></path><circle cx="10" cy="10" r="2.4"></circle></g></svg><code>settings</code></div><div class="gnav-ico cat-icon" title="alert"><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="M10 3.2 17.3 16H2.7z"></path><path d="M10 8.2v3.6M10 13.9v.1"></path></g></svg><code>alert</code></div><div class="gnav-ico cat-icon" title="info"><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"><circle cx="10" cy="10" r="7"></circle><path d="M10 9v4.5M10 6.4v.1"></path></g></svg><code>info</code></div><div class="gnav-ico cat-icon" title="check"><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="M4 10.5 8 14.5 16 5.5"></path></g></svg><code>check</code></div><div class="gnav-ico cat-icon" title="plus"><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="M10 4v12M4 10h12"></path></g></svg><code>plus</code></div><div class="gnav-ico cat-icon" title="close"><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><code>close</code></div><div class="gnav-ico cat-icon" title="utensils"><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="M6 3v5a2 2 0 0 0 4 0V3M8 3v14"></path><path d="M15 3c-1.6 1.4-2.5 3.6-2.5 6.5H15V17"></path></g></svg><code>utensils</code></div><div class="gnav-ico cat-icon" title="signOut"><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="M8 3.5H4v13h4M12.5 6.5 16 10l-3.5 3.5M16 10H8"></path></g></svg><code>signOut</code></div></div><figure class="cat-demo"><figcaption>Một icon (markup)</figcaption><div class="cat-live"><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"><rect x="3" y="3.5" width="4" height="13" rx="1.2"></rect><rect x="8" y="3.5" width="4" height="9" rx="1.2"></rect><rect x="13" y="3.5" width="4" height="6" rx="1.2"></rect></g></svg></div><details class="cat-code"><summary>Markup</summary><pre><code>&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;
544
+ <section id="icons"><h2>Icon</h2><p>Bộ icon GemBoss: 40 icon, lưới 20px, nét 1.5. <code>fill="none"</code> nằm trên <code>&lt;g&gt;</code>, không trên <code>&lt;svg&gt;</code>.</p><div class="cat-icons"><div class="gnav-ico cat-icon" title="home"><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="M3.5 9 10 3.5 16.5 9"></path><path d="M5 7.8v8.7h10V7.8"></path><path d="M8.5 16.5v-4h3v4"></path></g></svg><code>home</code></div><div class="gnav-ico cat-icon" title="dashboard"><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"><rect x="3" y="3" width="6" height="6" rx="1.3"></rect><rect x="11" y="3" width="6" height="6" rx="1.3"></rect><rect x="3" y="11" width="6" height="6" rx="1.3"></rect><rect x="11" y="11" width="6" height="6" rx="1.3"></rect></g></svg><code>dashboard</code></div><div class="gnav-ico cat-icon" title="board"><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"><rect x="3" y="3.5" width="4" height="13" rx="1.2"></rect><rect x="8" y="3.5" width="4" height="9" rx="1.2"></rect><rect x="13" y="3.5" width="4" height="6" rx="1.2"></rect></g></svg><code>board</code></div><div class="gnav-ico cat-icon" title="layout"><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"><rect x="3.5" y="3" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="8.25" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="13.5" width="13" height="3.5" rx="1"></rect></g></svg><code>layout</code></div><div class="gnav-ico cat-icon" title="layers"><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="M10 3 17 6.8 10 10.6 3 6.8z"></path><path d="M3 10.1l7 3.8 7-3.8"></path><path d="M3 13.4l7 3.8 7-3.8"></path></g></svg><code>layers</code></div><div class="gnav-ico cat-icon" title="document"><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 2.8h6.8l3.2 3.2v11.2H5z"></path><path d="M11.8 2.8V6H15M7.5 10h5M7.5 13h5"></path></g></svg><code>document</code></div><div class="gnav-ico cat-icon" title="article"><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"><rect x="3" y="3.5" width="14" height="13" rx="1.5"></rect><path d="M6 7h8M6 10h8M6 13h5"></path></g></svg><code>article</code></div><div class="gnav-ico cat-icon" title="page"><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"><rect x="2.5" y="3.5" width="15" height="13" rx="2"></rect><path d="M2.5 7.5h15M6 11h8M6 13.5h5"></path></g></svg><code>page</code></div><div class="gnav-ico cat-icon" title="image"><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"><rect x="3" y="3.5" width="14" height="13" rx="1.8"></rect><circle cx="7.3" cy="7.8" r="1.3"></circle><path d="M3.5 15l4.2-4.2 3 3 2.2-2.2 3.6 3.6"></path></g></svg><code>image</code></div><div class="gnav-ico cat-icon" title="folder"><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="M2.8 5.3c0-.8.6-1.3 1.3-1.3h3.4l1.8 2h6.6c.8 0 1.3.6 1.3 1.3v7.7c0 .8-.6 1.3-1.3 1.3H4.1c-.8 0-1.3-.6-1.3-1.3z"></path></g></svg><code>folder</code></div><div class="gnav-ico cat-icon" title="list"><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="M8 5h9M8 10h9M8 15h9"></path><circle cx="4" cy="5" r=".9"></circle><circle cx="4" cy="10" r=".9"></circle><circle cx="4" cy="15" r=".9"></circle></g></svg><code>list</code></div><div class="gnav-ico cat-icon" title="activity"><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="M2.5 10h3l2-5 3 10 2-5h5"></path></g></svg><code>activity</code></div><div class="gnav-ico cat-icon" title="chart"><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="M3 16.5h14"></path><rect x="4.5" y="9" width="2.8" height="5.5" rx=".7"></rect><rect x="8.6" y="5" width="2.8" height="9.5" rx=".7"></rect><rect x="12.7" y="11" width="2.8" height="3.5" rx=".7"></rect></g></svg><code>chart</code></div><div class="gnav-ico cat-icon" title="radar"><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"><circle cx="10" cy="10" r="7"></circle><circle cx="10" cy="10" r="3.5"></circle><path d="M10 10l5-5"></path></g></svg><code>radar</code></div><div class="gnav-ico cat-icon" title="map"><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"><rect x="3" y="3" width="14" height="14" rx="2"></rect><path d="M3 7.7h14M3 12.3h14M7.7 3v14M12.3 3v14"></path></g></svg><code>map</code></div><div class="gnav-ico cat-icon" title="globe"><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"><circle cx="10" cy="10" r="7"></circle><path d="M3 10h14M10 3c2 2 2.8 4.3 2.8 7s-.8 5-2.8 7c-2-2-2.8-4.3-2.8-7s.8-5 2.8-7z"></path></g></svg><code>globe</code></div><div class="gnav-ico cat-icon" title="megaphone"><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="M3 8v4h3l6 4V4L6 8H3z"></path><path d="M15 7.5a3.5 3.5 0 0 1 0 5"></path></g></svg><code>megaphone</code></div><div class="gnav-ico cat-icon" title="star"><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="M10 2.8l2.2 4.5 5 .7-3.6 3.5.9 5-4.5-2.4-4.5 2.4.9-5L2.8 8l5-.7z"></path></g></svg><code>star</code></div><div class="gnav-ico cat-icon" title="eye"><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="M1.8 10S4.7 4.8 10 4.8 18.2 10 18.2 10 15.3 15.2 10 15.2 1.8 10 1.8 10z"></path><circle cx="10" cy="10" r="2.5"></circle></g></svg><code>eye</code></div><div class="gnav-ico cat-icon" title="inbox"><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="M3 11l2-7h10l2 7v5H3z"></path><path d="M3 11h4l1 2h4l1-2h4"></path></g></svg><code>inbox</code></div><div class="gnav-ico cat-icon" title="bell"><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 13.5V9a5 5 0 0 1 10 0v4.5l1.3 1.5H3.7z"></path><path d="M8.3 17.2a1.9 1.9 0 0 0 3.4 0"></path></g></svg><code>bell</code></div><div class="gnav-ico cat-icon" title="chat"><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="M3 4.5h14v9H8.5L5 16.5v-3H3z"></path></g></svg><code>chat</code></div><div class="gnav-ico cat-icon" title="money"><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"><circle cx="10" cy="10" r="7"></circle><path d="M12 7.6c-.5-.7-1.2-1-2-1-1.1 0-2 .7-2 1.6 0 2.1 4 1.2 4 3.4 0 .9-.9 1.6-2 1.6-.8 0-1.6-.3-2.1-1M10 5v1.6M10 13.2V15"></path></g></svg><code>money</code></div><div class="gnav-ico cat-icon" title="store"><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="M3.2 7.5 4.5 3.5h11l1.3 4"></path><path d="M3.2 7.5c0 1.3 1 2.2 2.3 2.2s2.2-1 2.2-2.2c0 1.3 1 2.2 2.3 2.2s2.3-1 2.3-2.2c0 1.3.9 2.2 2.2 2.2s2.3-.9 2.3-2.2"></path><path d="M4.5 9.5v7h11v-7M8.5 16.5v-3.5h3v3.5"></path></g></svg><code>store</code></div><div class="gnav-ico cat-icon" title="user"><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"><circle cx="10" cy="7" r="3.2"></circle><path d="M3.8 16.5c.8-2.8 3.3-4.5 6.2-4.5s5.4 1.7 6.2 4.5"></path></g></svg><code>user</code></div><div class="gnav-ico cat-icon" title="users"><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"><circle cx="7.5" cy="7.3" r="2.8"></circle><path d="M2.5 16.3c.6-2.5 2.5-4 5-4s4.4 1.5 5 4"></path><path d="M12.3 4.8a2.8 2.8 0 0 1 0 5.2M14.3 12.5c1.6.4 2.7 1.7 3.2 3.8"></path></g></svg><code>users</code></div><div class="gnav-ico cat-icon" title="calendar"><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"><rect x="3" y="4.5" width="14" height="12.5" rx="1.8"></rect><path d="M3 8.5h14M7 3v3M13 3v3"></path></g></svg><code>calendar</code></div><div class="gnav-ico cat-icon" title="clock"><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"><circle cx="10" cy="10" r="7"></circle><path d="M10 6v4l2.8 1.8"></path></g></svg><code>clock</code></div><div class="gnav-ico cat-icon" title="search"><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"><circle cx="8.8" cy="8.8" r="5.3"></circle><path d="M12.8 12.8 17 17"></path></g></svg><code>search</code></div><div class="gnav-ico cat-icon" title="link"><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="M8.6 11.4a3 3 0 0 0 4.2 0l2.6-2.6a3 3 0 0 0-4.2-4.2l-1 1"></path><path d="M11.4 8.6a3 3 0 0 0-4.2 0l-2.6 2.6a3 3 0 0 0 4.2 4.2l1-1"></path></g></svg><code>link</code></div><div class="gnav-ico cat-icon" title="sliders"><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="M3 5h14M3 10h14M3 15h14"></path><circle cx="7" cy="5" r="1.6"></circle><circle cx="13" cy="10" r="1.6"></circle><circle cx="9" cy="15" r="1.6"></circle></g></svg><code>sliders</code></div><div class="gnav-ico cat-icon" title="settings"><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="M15.52 9.08 17.15 9.15 17.15 10.85 15.52 10.92 14.56 13.26 15.66 14.45 14.45 15.66 13.26 14.56 10.92 15.52 10.85 17.15 9.15 17.15 9.08 15.52 6.74 14.56 5.55 15.66 4.34 14.45 5.44 13.26 4.48 10.92 2.85 10.85 2.85 9.15 4.48 9.08 5.44 6.74 4.34 5.55 5.55 4.34 6.74 5.44 9.08 4.48 9.15 2.85 10.85 2.85 10.92 4.48 13.26 5.44 14.45 4.34 15.66 5.55 14.56 6.74Z"></path><circle cx="10" cy="10" r="2.4"></circle></g></svg><code>settings</code></div><div class="gnav-ico cat-icon" title="alert"><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="M10 3.2 17.3 16H2.7z"></path><path d="M10 8.2v3.6M10 13.9v.1"></path></g></svg><code>alert</code></div><div class="gnav-ico cat-icon" title="info"><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"><circle cx="10" cy="10" r="7"></circle><path d="M10 9v4.5M10 6.4v.1"></path></g></svg><code>info</code></div><div class="gnav-ico cat-icon" title="check"><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="M4 10.5 8 14.5 16 5.5"></path></g></svg><code>check</code></div><div class="gnav-ico cat-icon" title="plus"><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="M10 4v12M4 10h12"></path></g></svg><code>plus</code></div><div class="gnav-ico cat-icon" title="close"><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><code>close</code></div><div class="gnav-ico cat-icon" title="more"><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"><circle cx="4.5" cy="10" r="1"></circle><circle cx="10" cy="10" r="1"></circle><circle cx="15.5" cy="10" r="1"></circle></g></svg><code>more</code></div><div class="gnav-ico cat-icon" title="utensils"><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="M6 3v5a2 2 0 0 0 4 0V3M8 3v14"></path><path d="M15 3c-1.6 1.4-2.5 3.6-2.5 6.5H15V17"></path></g></svg><code>utensils</code></div><div class="gnav-ico cat-icon" title="signOut"><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="M8 3.5H4v13h4M12.5 6.5 16 10l-3.5 3.5M16 10H8"></path></g></svg><code>signOut</code></div></div><figure class="cat-demo"><figcaption>Một icon (markup)</figcaption><div class="cat-live"><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"><rect x="3" y="3.5" width="4" height="13" rx="1.2"></rect><rect x="8" y="3.5" width="4" height="9" rx="1.2"></rect><rect x="13" y="3.5" width="4" height="6" rx="1.2"></rect></g></svg></div><details class="cat-code"><summary>Markup</summary><pre><code>&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;
500
545
  &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;
501
546
  &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;
502
547
  &lt;/rect&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: 62b85f8a0c756bf63d370bff60e6debaaf4ea4df
5
- * sources sha256: fd64ea38ae59bb5ecf1c38da341be89f963c9fef11bd3189e83433588f4d88b5
4
+ * gemboss commit: be1da9b1254eb62cd68a2e65f026c94b4d7258cf
5
+ * sources sha256: d4f46aabf4df141636ddacc91c119762a521312a30154ca0e6d82412cb99491c
6
6
  * contents: 1 allow-listed rule from apps/admin/src/app/globals.css + packages/ui/src/gemboss-tokens.css
7
7
  * (verbatim) + packages/ui/src/ui.css (verbatim) + packages/ui/src/gemboss-field.css (verbatim)
8
8
  * + 180 rules from packages/ui/src/shell.css (the shared shell)
@@ -337,7 +337,7 @@
337
337
  background-color: var(--gemboss-field-bg-hover);
338
338
  }
339
339
  .ui-select:focus { outline: none; }
340
- .ui-select:focus-visible {
340
+ .ui-select:focus-visible:not(:disabled) {
341
341
  outline: 2px solid #005bd3; outline-offset: 1px;
342
342
  border-color: var(--gemboss-field-line-active);
343
343
  }
@@ -483,12 +483,35 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
483
483
  consumer belongs in that consumer's chunk (see SettingsDialog.module.css). */
484
484
  .ui-modal--lg { position: relative; }
485
485
 
486
+ /* The kit's multi-line field joins the ONE field family (01/10/2026, Chris: "form control đồng bộ"):
487
+ same input-border token, hover and focus ring as `gemboss-field` and `.ui-select`. Before, it drew a
488
+ lighter --ui-border box at 14px with no focus ring, so on GemFactory's compose form a textarea sat
489
+ beside selects of a darker border and text fields of a third look (measured on prod). */
486
490
  .ui-textarea {
487
491
  width: 100%; min-height: 96px; resize: vertical; box-sizing: border-box;
488
- background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md);
489
- padding: 12px; color: var(--ui-text); font-size: 14px; font-family: inherit; outline: none;
492
+ background: var(--gemboss-field-bg);
493
+ border: 1px solid var(--gemboss-field-line); border-radius: var(--ui-radius-md);
494
+ padding: 12px; color: var(--gemboss-field-ink); font-size: var(--ui-fs-body); line-height: 1.5; font-family: inherit;
495
+ }
496
+ .ui-textarea::placeholder { color: var(--gemboss-field-placeholder); }
497
+ .ui-textarea:hover:not(:disabled) {
498
+ border-color: var(--gemboss-field-line-hover);
499
+ background-color: var(--gemboss-field-bg-hover);
500
+ }
501
+ .ui-textarea:focus { outline: none; }
502
+ .ui-textarea:focus-visible:not(:disabled) {
503
+ outline: 2px solid #005bd3; outline-offset: 1px;
504
+ border-color: var(--gemboss-field-line-active);
505
+ }
506
+ .ui-textarea:disabled { opacity: .5; cursor: not-allowed; }
507
+ /* On a PHONE-WIDTH screen every field of the family reads 16px (iOS zooms the page on tap below that)
508
+ and a select is a 44px target — by WIDTH, as `gemboss-field` does, because a desktop browser at 320px
509
+ still reports pointer: fine. Measured on GemFactory at 390px before this: select 13px, textarea 14px. */
510
+ @media (max-width: 47.9975em) {
511
+ .ui-select.ui-select, .ui-textarea { font-size: 16px; }
512
+ /* Height only for the full select: `--slim` is a density decision (table rows), as the coarse block keeps it. */
513
+ .ui-select:not(.ui-select--slim) { min-height: 44px; }
490
514
  }
491
- .ui-textarea:focus { border-color: var(--ui-accent); }
492
515
 
493
516
  /* ── Segmented control ────────────────────────────────────────────────────────
494
517
  Grouped toggle: icon squares (.ui-seg button) or text tabs (.is-text). The
@@ -768,6 +791,10 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
768
791
  border: 1px solid var(--gemboss-field-line);
769
792
  border-radius: 8px;
770
793
  }
794
+ /* WebKit draws `input[type=search]` as `-webkit-appearance: searchfield`: its own padding and rounding, so in Safari
795
+ (macOS and iOS, the platform the 16px rule exists for) a search box did not match the fields beside it. Same box for
796
+ every text type; the clear button stays. (GemFactory #98 review, 01/10/2026.) */
797
+ input[type="search"]:is(.gemboss-field, .gemboss-field__bare) { -webkit-appearance: none; appearance: none; }
771
798
  .gemboss-field::placeholder { color: var(--gemboss-field-placeholder); }
772
799
  /* A NARROW field — the inspector's T/R/B/L boxes and its number spinners are 40-68px wide, and the
773
800
  shared 12px side padding left 38px of content box for a three-digit value (measured in Chromium:
@@ -882,7 +909,8 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
882
909
  z-index: 320;
883
910
  overflow: hidden;
884
911
  }
885
- .gemboss-account-menu-item {
912
+ .gemboss-account-menu-item,
913
+ .ui-menu__item {
886
914
  display: block;
887
915
  width: 100%;
888
916
  min-height: 44px;
@@ -897,15 +925,18 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
897
925
  cursor: pointer;
898
926
  transition: background var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
899
927
  }
900
- .gemboss-account-menu-item:hover {
928
+ .gemboss-account-menu-item:hover,
929
+ .ui-menu__item:hover {
901
930
  background: var(--gemboss-hover);
902
931
  }
903
- .gemboss-account-menu-item:focus-visible {
932
+ .gemboss-account-menu-item:focus-visible,
933
+ .ui-menu__item:focus-visible {
904
934
  outline: 2px solid var(--gemboss-primary);
905
935
  outline-offset: -2px;
906
936
  background: var(--gemboss-hover);
907
937
  }
908
- .gemboss-account-menu-item.danger {
938
+ .gemboss-account-menu-item.danger,
939
+ .ui-menu__item--danger {
909
940
  color: var(--gemboss-danger-strong);
910
941
  }
911
942
  .gemboss-account-chip {
@@ -3040,3 +3071,60 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
3040
3071
  border-radius: 0;
3041
3072
  }
3042
3073
  }
3074
+
3075
+ /* ═══ @gemboss/ui src/kit-plain.css ═══ */
3076
+ /* kit-plain.css — kit primitives that only the NON-React surfaces use (GemFactory, gemcare, gempress write the
3077
+ markup by hand). They ship inside gemboss-ui.css (scripts/build-css.mjs `own`), but the admin does NOT import
3078
+ this file: `ui.css` is loaded by the admin's root layout, so every rule there is paid on every admin screen,
3079
+ and adding these to it pushed the admin shell 0.2 KB over its first-load budget (CI of #3591, 01/10/2026).
3080
+ The admin has Polaris for menus, checkboxes and description lists. */
3081
+
3082
+ /* ── Icon button · Menu · Key-value list ──────────────────────────────────────
3083
+ Added 01/10/2026 for GemFactory's item dialog, which had seven actions in one header row; the
3084
+ secondary five now sit behind one "⋯" button. Plain CSS, same contract as the tabs and tables in ui.css.
3085
+
3086
+ Icon button: `<button class="ui-btn ui-btn--icon" aria-label="…"><svg…></button>` — square, the
3087
+ label is REQUIRED (there is no text for a screen reader to read otherwise).
3088
+
3089
+ Menu: a `<details>` so it opens with no script. `<details class="ui-menu ui-menu--end">
3090
+ <summary class="ui-btn ui-btn--icon" aria-label="More actions">…</summary><div class="ui-menu__list">
3091
+ <button class="ui-menu__item">Copy #12<span class="ui-menu__hint">for Claude Code</span></button>
3092
+ <hr class="ui-menu__sep"><button class="ui-menu__item ui-menu__item--danger">Delete…</button></div>
3093
+ </details>`. `--end` aligns the list to the right edge of its button. Closing on an outside click,
3094
+ on an item, and on Escape is the page's job (a few lines; GemFactory's ui-kit.mjs has them).
3095
+ Items are 44px under EVERY pointer, not only coarse: they touch each other (0px apart), and §9.1
3096
+ only allows a gap under 8px between targets that are already 44px.
3097
+
3098
+ Key-value list: `<dl class="ui-kv"><dt>Owner</dt><dd>Lily</dd>…</dl>` — label column sized to
3099
+ its longest label, value column takes the rest and wraps long keys and URLs. */
3100
+ .ui-btn--icon { flex: none; width: var(--ui-control-h); padding: 0; }
3101
+ .ui-btn--icon.ui-btn--sm { width: 28px; }
3102
+ .ui-btn--icon svg { width: 18px; height: 18px; }
3103
+ /* Menu: the SURFACE is the shell's `gemboss-popover-surface` and the rows are the account menu's row (shell.css
3104
+ declares `.gemboss-account-menu-item, .ui-menu__item` together), so a kit menu and the account menu on one page
3105
+ look the same and a fix to one reaches the other (§9.3). Markup: `<div class="ui-menu__list gemboss-popover-surface">`.
3106
+ Here: only placement, the hint, the separator and the disabled state. */
3107
+ .ui-menu { position: relative; display: inline-block; }
3108
+ .ui-menu > summary { list-style: none; cursor: pointer; }
3109
+ .ui-menu > summary::-webkit-details-marker { display: none; }
3110
+ .ui-menu__list { position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; display: grid; min-width: 220px; padding: var(--ui-space-1); box-sizing: border-box; }
3111
+ .ui-menu--end .ui-menu__list { left: auto; right: 0; }
3112
+ .ui-menu__item { display: flex; align-items: center; gap: var(--ui-space-2); white-space: nowrap; border-radius: var(--ui-radius-sm); }
3113
+ .ui-menu__item:is([disabled], [aria-disabled="true"]) { opacity: .5; cursor: not-allowed; pointer-events: none; }
3114
+ .ui-menu__hint { margin-left: auto; padding-left: var(--ui-space-4); color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }
3115
+ .ui-menu__sep { height: 1px; margin: var(--ui-space-1) 0; border: 0; background: var(--ui-border); }
3116
+ /* Checkbox: the LABEL is the target; disabled turns both the label and the box to not-allowed. Focus ring in the
3117
+ kit's own token (no --p-* here: this file is appended after the export's Polaris resolver runs). */
3118
+ .ui-check { display: inline-flex; align-items: center; gap: var(--ui-space-2); min-height: 44px; cursor: pointer; font-size: var(--ui-fs-body); color: var(--ui-text); }
3119
+ .ui-check > input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); cursor: inherit; }
3120
+ .ui-check > input:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
3121
+ .ui-check:has(> input:disabled) { opacity: .5; cursor: not-allowed; }
3122
+ .ui-label { display: block; margin: 0 0 var(--ui-space-1); font-size: var(--ui-fs-caption); font-weight: 600; color: var(--ui-text-muted); }
3123
+ .ui-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--ui-space-2) var(--ui-space-4); margin: 0; font-size: var(--ui-fs-body); }
3124
+ .ui-kv > dt { color: var(--ui-text-muted); }
3125
+ .ui-kv > dd { margin: 0; min-width: 0; overflow-wrap: anywhere; color: var(--ui-text); }
3126
+
3127
+ /* Under a thumb the icon-only button is 44 WIDE as well (the kit's coarse block already raises every .ui-btn to 44 tall). */
3128
+ @media (pointer: coarse) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; } }
3129
+ /* …and at phone WIDTH, as gemboss-field does: a desktop browser at 320px still reports pointer: fine. */
3130
+ @media (max-width: 47.9975em) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; min-height: 44px; } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/ui",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "GemBoss design system for every surface in the fleet: the admin shell (topbar + rail + drawer) as React components, and the admin stylesheet exported as one CSS file.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -18,6 +18,7 @@
18
18
  "./app-shell.css": "./src/app-shell.css",
19
19
  "./shell.css": "./src/shell.css",
20
20
  "./auth.css": "./src/auth.css",
21
+ "./kit-plain.css": "./src/kit-plain.css",
21
22
  "./assistant": "./src/assistant/index.ts",
22
23
  "./assistant/dock": "./src/assistant/dock.ts",
23
24
  "./assistant.css": "./src/assistant.css"
@@ -30,6 +30,10 @@
30
30
  border: 1px solid var(--p-color-input-border, var(--gemboss-field-line));
31
31
  border-radius: var(--p-border-radius-200, 8px);
32
32
  }
33
+ /* WebKit draws `input[type=search]` as `-webkit-appearance: searchfield`: its own padding and rounding, so in Safari
34
+ (macOS and iOS, the platform the 16px rule exists for) a search box did not match the fields beside it. Same box for
35
+ every text type; the clear button stays. (GemFactory #98 review, 01/10/2026.) */
36
+ input[type="search"]:is(.gemboss-field, .gemboss-field__bare) { -webkit-appearance: none; appearance: none; }
33
37
  .gemboss-field::placeholder { color: var(--p-color-text-secondary, var(--gemboss-field-placeholder)); }
34
38
  /* A NARROW field — the inspector's T/R/B/L boxes and its number spinners are 40-68px wide, and the
35
39
  shared 12px side padding left 38px of content box for a three-digit value (measured in Chromium:
package/src/icons.tsx CHANGED
@@ -54,6 +54,7 @@ const P = {
54
54
  check: <path d="M4 10.5 8 14.5 16 5.5" />,
55
55
  plus: <path d="M10 4v12M4 10h12" />,
56
56
  close: <path d="M5 5l10 10M15 5 5 15" />,
57
+ more: <><circle cx="4.5" cy="10" r="1" /><circle cx="10" cy="10" r="1" /><circle cx="15.5" cy="10" r="1" /></>,
57
58
  utensils: <><path d="M6 3v5a2 2 0 0 0 4 0V3M8 3v14" /><path d="M15 3c-1.6 1.4-2.5 3.6-2.5 6.5H15V17" /></>,
58
59
  signOut: <path d="M8 3.5H4v13h4M12.5 6.5 16 10l-3.5 3.5M16 10H8" />,
59
60
  } satisfies Record<string, React.ReactNode>;
package/src/index.ts CHANGED
@@ -6,4 +6,5 @@ export { GemMark } from "./GemMark";
6
6
  export { AuthError, AuthField, AuthFoot, AuthForm, AuthHelp, AuthLayout, PasswordField, AuthSubmit } from "./auth";
7
7
  export { BrandLockup } from "./brand";
8
8
  export { AccountMenu, AccountMenuItem, initialsOf } from "./account";
9
+ export { Menu, MenuItem, MenuSeparator } from "./menu";
9
10
  export { Icon, ICON_NAMES, type IconName } from "./icons";
@@ -0,0 +1,55 @@
1
+ /* kit-plain.css — kit primitives that only the NON-React surfaces use (GemFactory, gemcare, gempress write the
2
+ markup by hand). They ship inside gemboss-ui.css (scripts/build-css.mjs `own`), but the admin does NOT import
3
+ this file: `ui.css` is loaded by the admin's root layout, so every rule there is paid on every admin screen,
4
+ and adding these to it pushed the admin shell 0.2 KB over its first-load budget (CI of #3591, 01/10/2026).
5
+ The admin has Polaris for menus, checkboxes and description lists. */
6
+
7
+ /* ── Icon button · Menu · Key-value list ──────────────────────────────────────
8
+ Added 01/10/2026 for GemFactory's item dialog, which had seven actions in one header row; the
9
+ secondary five now sit behind one "⋯" button. Plain CSS, same contract as the tabs and tables in ui.css.
10
+
11
+ Icon button: `<button class="ui-btn ui-btn--icon" aria-label="…"><svg…></button>` — square, the
12
+ label is REQUIRED (there is no text for a screen reader to read otherwise).
13
+
14
+ Menu: a `<details>` so it opens with no script. `<details class="ui-menu ui-menu--end">
15
+ <summary class="ui-btn ui-btn--icon" aria-label="More actions">…</summary><div class="ui-menu__list">
16
+ <button class="ui-menu__item">Copy #12<span class="ui-menu__hint">for Claude Code</span></button>
17
+ <hr class="ui-menu__sep"><button class="ui-menu__item ui-menu__item--danger">Delete…</button></div>
18
+ </details>`. `--end` aligns the list to the right edge of its button. Closing on an outside click,
19
+ on an item, and on Escape is the page's job (a few lines; GemFactory's ui-kit.mjs has them).
20
+ Items are 44px under EVERY pointer, not only coarse: they touch each other (0px apart), and §9.1
21
+ only allows a gap under 8px between targets that are already 44px.
22
+
23
+ Key-value list: `<dl class="ui-kv"><dt>Owner</dt><dd>Lily</dd>…</dl>` — label column sized to
24
+ its longest label, value column takes the rest and wraps long keys and URLs. */
25
+ .ui-btn--icon { flex: none; width: var(--ui-control-h); padding: 0; }
26
+ .ui-btn--icon.ui-btn--sm { width: 28px; }
27
+ .ui-btn--icon svg { width: 18px; height: 18px; }
28
+ /* Menu: the SURFACE is the shell's `gemboss-popover-surface` and the rows are the account menu's row (shell.css
29
+ declares `.gemboss-account-menu-item, .ui-menu__item` together), so a kit menu and the account menu on one page
30
+ look the same and a fix to one reaches the other (§9.3). Markup: `<div class="ui-menu__list gemboss-popover-surface">`.
31
+ Here: only placement, the hint, the separator and the disabled state. */
32
+ .ui-menu { position: relative; display: inline-block; }
33
+ .ui-menu > summary { list-style: none; cursor: pointer; }
34
+ .ui-menu > summary::-webkit-details-marker { display: none; }
35
+ .ui-menu__list { position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; display: grid; min-width: 220px; padding: var(--ui-space-1); box-sizing: border-box; }
36
+ .ui-menu--end .ui-menu__list { left: auto; right: 0; }
37
+ .ui-menu__item { display: flex; align-items: center; gap: var(--ui-space-2); white-space: nowrap; border-radius: var(--ui-radius-sm); }
38
+ .ui-menu__item:is([disabled], [aria-disabled="true"]) { opacity: .5; cursor: not-allowed; pointer-events: none; }
39
+ .ui-menu__hint { margin-left: auto; padding-left: var(--ui-space-4); color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }
40
+ .ui-menu__sep { height: 1px; margin: var(--ui-space-1) 0; border: 0; background: var(--ui-border); }
41
+ /* Checkbox: the LABEL is the target; disabled turns both the label and the box to not-allowed. Focus ring in the
42
+ kit's own token (no --p-* here: this file is appended after the export's Polaris resolver runs). */
43
+ .ui-check { display: inline-flex; align-items: center; gap: var(--ui-space-2); min-height: 44px; cursor: pointer; font-size: var(--ui-fs-body); color: var(--ui-text); }
44
+ .ui-check > input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); cursor: inherit; }
45
+ .ui-check > input:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
46
+ .ui-check:has(> input:disabled) { opacity: .5; cursor: not-allowed; }
47
+ .ui-label { display: block; margin: 0 0 var(--ui-space-1); font-size: var(--ui-fs-caption); font-weight: 600; color: var(--ui-text-muted); }
48
+ .ui-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--ui-space-2) var(--ui-space-4); margin: 0; font-size: var(--ui-fs-body); }
49
+ .ui-kv > dt { color: var(--ui-text-muted); }
50
+ .ui-kv > dd { margin: 0; min-width: 0; overflow-wrap: anywhere; color: var(--ui-text); }
51
+
52
+ /* Under a thumb the icon-only button is 44 WIDE as well (the kit's coarse block already raises every .ui-btn to 44 tall). */
53
+ @media (pointer: coarse) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; } }
54
+ /* …and at phone WIDTH, as gemboss-field does: a desktop browser at 320px still reports pointer: fine. */
55
+ @media (max-width: 47.9975em) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; min-height: 44px; } }
package/src/menu.tsx ADDED
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ // menu.tsx: the kit's action menu ("⋯") — a `<details>` disclosure, so it opens with no JavaScript and a
4
+ // server-rendered surface (GemFactory, gemcare) can print it as plain markup. The surface is the shell's
5
+ // `gemboss-popover-surface` and the rows share the account menu's row (shell.css), so every menu in the
6
+ // fleet looks the same (§9.3). Where React hydrates, the menu closes itself like AccountMenu does: outside
7
+ // click, Escape (focus back on the trigger), and a button row after its handler (review #3591: a React page
8
+ // had no clean way to close it). The catalog renders its example from these components.
9
+ import React, { useEffect, useRef } from "react";
10
+ import { Icon } from "./icons";
11
+
12
+ /** The trigger is an icon button (`label` is its accessible name). `end` aligns the list to the right edge. */
13
+ export function Menu({ label, icon, end, children, className }: {
14
+ label: string;
15
+ icon?: React.ReactNode;
16
+ end?: boolean;
17
+ children?: React.ReactNode;
18
+ className?: string;
19
+ }) {
20
+ const ref = useRef<HTMLDetailsElement>(null);
21
+ useEffect(() => {
22
+ const el = ref.current;
23
+ if (!el) return undefined;
24
+ const onDoc = (e: MouseEvent) => { if (el.open && !el.contains(e.target as Node)) el.open = false; };
25
+ const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && el.open) { el.open = false; el.querySelector("summary")?.focus(); } };
26
+ document.addEventListener("click", onDoc);
27
+ document.addEventListener("keydown", onKey);
28
+ return () => { document.removeEventListener("click", onDoc); document.removeEventListener("keydown", onKey); };
29
+ }, []);
30
+ return (
31
+ <details ref={ref} className={["ui-menu", end ? "ui-menu--end" : "", className ?? ""].filter(Boolean).join(" ")}>
32
+ <summary className="ui-btn ui-btn--icon" aria-label={label} title={label}>{icon ?? <Icon name="more" />}</summary>
33
+ <div className="ui-menu__list gemboss-popover-surface">{children}</div>
34
+ </details>
35
+ );
36
+ }
37
+
38
+ /** One row: a link (`href`) or a button. `hint` is the quiet text on the right; `danger` for destructive rows.
39
+ * A disabled link is not a link: no href, aria-disabled, out of the tab order (an `<a disabled>` still follows
40
+ * on Enter). A caller's `className` is added to the row's class, not put in its place. */
41
+ export function MenuItem({ href, danger, hint, children, className, disabled, onClick, ...rest }: {
42
+ href?: string;
43
+ danger?: boolean;
44
+ hint?: React.ReactNode;
45
+ children?: React.ReactNode;
46
+ disabled?: boolean;
47
+ onClick?: React.MouseEventHandler<HTMLElement>;
48
+ } & Omit<React.HTMLAttributes<HTMLElement>, "onClick"> & Pick<React.AnchorHTMLAttributes<HTMLAnchorElement>, "target" | "rel">) {
49
+ const cls = ["ui-menu__item", danger ? "ui-menu__item--danger" : "", className ?? ""].filter(Boolean).join(" ");
50
+ const body = <>{children}{hint ? <span className="ui-menu__hint">{hint}</span> : null}</>;
51
+ if (href) {
52
+ if (disabled) return <a className={cls} aria-disabled="true" tabIndex={-1} {...rest}>{body}</a>;
53
+ return <a className={cls} href={href} onClick={onClick} {...rest}>{body}</a>;
54
+ }
55
+ const close: React.MouseEventHandler<HTMLButtonElement> = (e) => {
56
+ onClick?.(e);
57
+ e.currentTarget.closest("details")?.removeAttribute("open");
58
+ };
59
+ return <button type="button" className={cls} disabled={disabled} onClick={close} {...rest}>{body}</button>;
60
+ }
61
+
62
+ export function MenuSeparator() {
63
+ return <hr className="ui-menu__sep" />;
64
+ }
package/src/shell.css CHANGED
@@ -100,20 +100,24 @@
100
100
  /* Account dropdown menu items (avatar chip) — CSS-driven hover/focus, replaces inline JS handlers. */
101
101
  /* min-height 44 (28/09/2026): 12px padding around a 13px line measured 42.84px — under the 44px control
102
102
  floor (CLAUDE.md §9.1), in the admin's own menu and in every surface wearing it through @gemboss/ui. */
103
- .gemboss-account-menu-item {
103
+ .gemboss-account-menu-item,
104
+ .ui-menu__item {
104
105
  display: block; width: 100%; min-height: 44px; padding: 12px 16px; border: 0; background: transparent;
105
106
  font: inherit; font-size: 13px; color: var(--gemboss-text); text-align: left; text-decoration: none;
106
107
  cursor: pointer; transition: background var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
107
108
  }
108
109
 
109
- .gemboss-account-menu-item:hover { background: var(--gemboss-hover); }
110
+ .gemboss-account-menu-item:hover,
111
+ .ui-menu__item:hover { background: var(--gemboss-hover); }
110
112
 
111
- .gemboss-account-menu-item:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; background: var(--gemboss-hover); }
113
+ .gemboss-account-menu-item:focus-visible,
114
+ .ui-menu__item:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; background: var(--gemboss-hover); }
112
115
 
113
116
  /* `--gemboss-danger` var(--gemboss-danger) is 4.25:1 on `--gemboss-hover` #eceef2, and this row takes that hover
114
117
  background two lines above — so the one moment the item is under the cursor is the moment its label
115
118
  drops below AA. `--gemboss-danger-strong` #b42318 is 5.66:1 there and 7.30:1 on white. */
116
- .gemboss-account-menu-item.danger { color: var(--gemboss-danger-strong); }
119
+ .gemboss-account-menu-item.danger,
120
+ .ui-menu__item--danger { color: var(--gemboss-danger-strong); }
117
121
 
118
122
  .gemboss-account-chip {
119
123
  width: 30px;
package/src/ui.css CHANGED
@@ -154,7 +154,7 @@
154
154
  background-color: var(--p-color-input-bg-surface-hover, var(--gemboss-field-bg-hover));
155
155
  }
156
156
  .ui-select:focus { outline: none; }
157
- .ui-select:focus-visible {
157
+ .ui-select:focus-visible:not(:disabled) {
158
158
  outline: 2px solid var(--p-color-border-focus, #005bd3); outline-offset: 1px;
159
159
  border-color: var(--p-color-input-border-active, var(--gemboss-field-line-active));
160
160
  }
@@ -300,12 +300,35 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
300
300
  consumer belongs in that consumer's chunk (see SettingsDialog.module.css). */
301
301
  .ui-modal--lg { position: relative; }
302
302
 
303
+ /* The kit's multi-line field joins the ONE field family (01/10/2026, Chris: "form control đồng bộ"):
304
+ same input-border token, hover and focus ring as `gemboss-field` and `.ui-select`. Before, it drew a
305
+ lighter --ui-border box at 14px with no focus ring, so on GemFactory's compose form a textarea sat
306
+ beside selects of a darker border and text fields of a third look (measured on prod). */
303
307
  .ui-textarea {
304
308
  width: 100%; min-height: 96px; resize: vertical; box-sizing: border-box;
305
- background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md);
306
- padding: 12px; color: var(--ui-text); font-size: 14px; font-family: inherit; outline: none;
309
+ background: var(--p-color-input-bg-surface, var(--gemboss-field-bg));
310
+ border: 1px solid var(--p-color-input-border, var(--gemboss-field-line)); border-radius: var(--ui-radius-md);
311
+ padding: 12px; color: var(--p-color-text, var(--gemboss-field-ink)); font-size: var(--ui-fs-body); line-height: 1.5; font-family: inherit;
312
+ }
313
+ .ui-textarea::placeholder { color: var(--p-color-text-secondary, var(--gemboss-field-placeholder)); }
314
+ .ui-textarea:hover:not(:disabled) {
315
+ border-color: var(--p-color-input-border-hover, var(--gemboss-field-line-hover));
316
+ background-color: var(--p-color-input-bg-surface-hover, var(--gemboss-field-bg-hover));
317
+ }
318
+ .ui-textarea:focus { outline: none; }
319
+ .ui-textarea:focus-visible:not(:disabled) {
320
+ outline: 2px solid var(--p-color-border-focus, #005bd3); outline-offset: 1px;
321
+ border-color: var(--p-color-input-border-active, var(--gemboss-field-line-active));
322
+ }
323
+ .ui-textarea:disabled { opacity: .5; cursor: not-allowed; }
324
+ /* On a PHONE-WIDTH screen every field of the family reads 16px (iOS zooms the page on tap below that)
325
+ and a select is a 44px target — by WIDTH, as `gemboss-field` does, because a desktop browser at 320px
326
+ still reports pointer: fine. Measured on GemFactory at 390px before this: select 13px, textarea 14px. */
327
+ @media (max-width: 47.9975em) {
328
+ .ui-select.ui-select, .ui-textarea { font-size: 16px; }
329
+ /* Height only for the full select: `--slim` is a density decision (table rows), as the coarse block keeps it. */
330
+ .ui-select:not(.ui-select--slim) { min-height: 44px; }
307
331
  }
308
- .ui-textarea:focus { border-color: var(--ui-accent); }
309
332
 
310
333
  /* ── Segmented control ────────────────────────────────────────────────────────
311
334
  Grouped toggle: icon squares (.ui-seg button) or text tabs (.is-text). The