@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 +6 -0
- package/catalog/index.html +51 -6
- package/gemboss-ui.css +98 -10
- package/package.json +2 -1
- package/src/gemboss-field.css +4 -0
- package/src/icons.tsx +1 -0
- package/src/index.ts +1 -0
- package/src/kit-plain.css +55 -0
- package/src/menu.tsx +64 -0
- package/src/shell.css +8 -4
- package/src/ui.css +27 -4
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
|
package/catalog/index.html
CHANGED
|
@@ -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.
|
|
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><button></code> lẫn <code><a></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><div class="cat-row">
|
|
33
33
|
<button type="button" class="ui-btn">Lưu nháp</button>
|
|
@@ -37,8 +37,9 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
37
37
|
<button type="button" class="ui-btn ui-btn--primary" disabled>Đang lưu…</button>
|
|
38
38
|
<a class="ui-btn" href="#buttons">Link dạng nút</a>
|
|
39
39
|
</div></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><div class="cat-stack">
|
|
41
|
-
<
|
|
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><div class="cat-stack">
|
|
41
|
+
<label class="ui-label" for="cat-f1">Tên chiến dịch</label>
|
|
42
|
+
<input id="cat-f1" class="gemboss-field" placeholder="Tên chiến dịch">
|
|
42
43
|
<label class="gemboss-field gemboss-field--group">
|
|
43
44
|
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false">
|
|
44
45
|
<g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -56,6 +57,12 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
56
57
|
</select>
|
|
57
58
|
<textarea class="gemboss-field" rows="4" placeholder="Ghi chú">
|
|
58
59
|
</textarea>
|
|
60
|
+
<textarea class="ui-textarea" placeholder="ui-textarea: cùng họ, cao sẵn 96px">
|
|
61
|
+
</textarea>
|
|
62
|
+
<label class="ui-check">
|
|
63
|
+
<input type="checkbox" checked> Chỉ thứ đã duyệt</label>
|
|
64
|
+
<label class="ui-check">
|
|
65
|
+
<input type="checkbox"> Khoá preset</label>
|
|
59
66
|
</div></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><div class="ui-tabs" role="tablist">
|
|
61
68
|
<button type="button" role="tab" aria-selected="true">Tổng quan</button>
|
|
@@ -65,6 +72,44 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
65
72
|
<button type="button" aria-pressed="true" class="is-text active">Kanban</button>
|
|
66
73
|
<button type="button" aria-pressed="false" class="is-text">Nhóm</button>
|
|
67
74
|
<button type="button" aria-pressed="false" class="is-text">Danh sách</button>
|
|
75
|
+
</div></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><div class="cat-row" style="align-items:flex-start;min-height:260px">
|
|
76
|
+
<button type="button" class="ui-btn ui-btn--icon" aria-label="Đóng">
|
|
77
|
+
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false">
|
|
78
|
+
<g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
79
|
+
<path d="M5 5l10 10M15 5 5 15">
|
|
80
|
+
</path>
|
|
81
|
+
</g>
|
|
82
|
+
</svg>
|
|
83
|
+
</button>
|
|
84
|
+
<details class="ui-menu" open>
|
|
85
|
+
<summary class="ui-btn ui-btn--icon" aria-label="Thêm thao tác" title="Thêm thao tác">
|
|
86
|
+
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false">
|
|
87
|
+
<g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
88
|
+
<circle cx="4.5" cy="10" r="1">
|
|
89
|
+
</circle>
|
|
90
|
+
<circle cx="10" cy="10" r="1">
|
|
91
|
+
</circle>
|
|
92
|
+
<circle cx="15.5" cy="10" r="1">
|
|
93
|
+
</circle>
|
|
94
|
+
</g>
|
|
95
|
+
</svg>
|
|
96
|
+
</summary>
|
|
97
|
+
<div class="ui-menu__list gemboss-popover-surface">
|
|
98
|
+
<button type="button" class="ui-menu__item">Chép #12<span class="ui-menu__hint">dán vào Claude Code</span>
|
|
99
|
+
</button>
|
|
100
|
+
<a class="ui-menu__item" href="#choice">Mở trang riêng</a>
|
|
101
|
+
<hr class="ui-menu__sep"/>
|
|
102
|
+
<button type="button" class="ui-menu__item ui-menu__item--danger">Xoá…</button>
|
|
103
|
+
</div>
|
|
104
|
+
</details>
|
|
105
|
+
<dl class="ui-kv" style="flex-basis:100%">
|
|
106
|
+
<dt>Người dựng</dt>
|
|
107
|
+
<dd>Lily</dd>
|
|
108
|
+
<dt>Người duyệt</dt>
|
|
109
|
+
<dd>chưa</dd>
|
|
110
|
+
<dt>Báo cáo</dt>
|
|
111
|
+
<dd>research/kho-section-can-build-2026-09-28.md</dd>
|
|
112
|
+
</dl>
|
|
68
113
|
</div></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><div class="cat-row">
|
|
69
114
|
<span class="ui-tag">Nháp</span>
|
|
70
115
|
<span class="ui-tag ui-tag--accent">Đã duyệt</span>
|
|
@@ -376,7 +421,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
376
421
|
</aside>
|
|
377
422
|
</div>
|
|
378
423
|
</main></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">
|
|
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><div style="height:360px;display:flex">
|
|
380
425
|
<aside class="gemboss-assistant-rail">
|
|
381
426
|
<div class="gemboss-assistant-rail-top">
|
|
382
427
|
<button type="button" class="gemboss-assistant-newchat" title="Phiên mới">
|
|
@@ -425,7 +470,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
425
470
|
</svg>
|
|
426
471
|
<span class="gemboss-assistant-thread-title">So sánh advertorial với listicle</span>
|
|
427
472
|
</span>
|
|
428
|
-
<span class="gemboss-assistant-thread-meta">
|
|
473
|
+
<span class="gemboss-assistant-thread-meta">23/8/2026</span>
|
|
429
474
|
</button>
|
|
430
475
|
<div class="gemboss-assistant-thread-actions">
|
|
431
476
|
<button type="button" class="gemboss-assistant-thread-act" aria-label="Options">
|
|
@@ -496,7 +541,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
496
541
|
</div>
|
|
497
542
|
</aside>
|
|
498
543
|
</div></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><g></code>, không trên <code><svg></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><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false">
|
|
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><g></code>, không trên <code><svg></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><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false">
|
|
500
545
|
<g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
501
546
|
<rect x="3" y="3.5" width="4" height="13" rx="1.2">
|
|
502
547
|
</rect>
|
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:
|
|
5
|
-
* sources sha256:
|
|
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(--
|
|
489
|
-
|
|
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.
|
|
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"
|
package/src/gemboss-field.css
CHANGED
|
@@ -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
|
|
110
|
+
.gemboss-account-menu-item:hover,
|
|
111
|
+
.ui-menu__item:hover { background: var(--gemboss-hover); }
|
|
110
112
|
|
|
111
|
-
.gemboss-account-menu-item:focus-visible
|
|
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
|
|
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(--
|
|
306
|
-
|
|
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
|