@gemboss/ui 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -221,6 +221,26 @@ New primitives, Polaris' shapes on GemBoss tokens (`kit-plain.css`; the admin us
221
221
  | A popup in React (no kit-plain.js) | `<dialog class="ui-modal">` + `showModal()`: same head / body / foot, the kit's backdrop; the browser gives Escape and the inert page | Modal |
222
222
  | Favicon | `@gemboss/ui/icon.svg` (the admin's `app/icon.svg`, byte for byte) | |
223
223
 
224
+ 0.5.1 adds what the first surfaces on 0.5.0 still drew themselves (GemFactory, content-radar):
225
+
226
+ | Need | Class / API |
227
+ |---|---|
228
+ | Title with buttons on the right | `ui-page-head ui-page-head--actions` > `div` (h1 + p) + `.ui-page-head__actions` |
229
+ | Chips that toggle in place (no URL) | `.ui-chips > button.ui-tag--chip[aria-pressed]` |
230
+ | A long badge label | `ui-badge ui-badge--wrap` |
231
+ | Close the account menu on a static page | `gembossUI.initMenus()` now closes `details.gb-account` as well |
232
+ | ☰ and eye names on a static render | `MobileMenuButton` / `PasswordField` emit `data-label-open/close`, `data-label-show/hide` |
233
+
234
+ And four targets that measured under 44px at phone width are now 44: a compact text button's width, the bare input
235
+ inside `gemboss-field--group`, the brand mark alone (≤460px), a toggle chip. A popup opened by an Escape handler no
236
+ longer catches that same Escape (`openModal` listens from the next task). The rail item and segmented buttons draw the iris focus ring (they had
237
+ none: the browser drew its own), the popup × too (it drew near-black); at phone width the chat composer is 16px (iOS
238
+ zoomed it at 14) and the thread rail's "new chat" / collapse buttons are 44px under a fine pointer as well. Headings carry
239
+ their own line-height (a wrapped 22px title overlapped itself on the 20px body line), a badge is a tint of the ink (it
240
+ vanished on a hovered table row), `ui-seg` reads `aria-pressed`, a `[readonly]` field looks read-only, and a popup may
241
+ wrap body + foot in one `<form>` (`.ui-modal > form` takes no box). A metric that filters the view is a toggle: `button.ui-metric[aria-pressed]` (or
242
+ `a.ui-metric[aria-current="page"]` for a link), drawn with the iris edge when chosen.
243
+
224
244
  If your surface still sets any of these itself (a `body { font-size }`, a `min-height: 44px` on `.ui-btn--sm`, a
225
245
  `.danger` button, a `confirm()`), delete it on the upgrade: the package draws it now, and a surface rule wins the
226
246
  cascade over the package's zero-specificity base. `packages/ui/src/kit-look.test.ts` measures the table above in
@@ -256,24 +276,35 @@ The surface owns where the seam sits and what it does. Five rules, each paid for
256
276
  React surfaces: `useResizableChatPanel` in the admin is the reference. Others write the same markup and
257
277
  the five rules by hand; the catalog's "Thanh kéo giữa hai cột" shows the markup.
258
278
 
259
- ## The assistant launcher (0.4.2)
279
+ ## The assistant launcher (0.4.2; not floating since 0.6.0)
260
280
 
261
- The top-right corner of the topbar is the ACCOUNT (`AccountMenu`). The button that opens a surface's AI assistant floats
262
- bottom-right instead (Chris, 02/10/2026): partner-radar had it top-right, and on a phone it became a ✦ circle exactly where
263
- content-radar shows the account chip.
281
+ The top-right corner of the topbar is the ACCOUNT (`AccountMenu`). The assistant button is NOT a floating button: 0.4.2
282
+ floated it bottom-right, and a fixed button sits on whatever text scrolls under it (visual-gate `content_covered` red at
283
+ 5 of 6 widths on every partner-radar page measured, 03/10/2026; GemCommunity had already dropped its floating buttons for
284
+ the same reason, #239). Chris chose to drop it. Render TWO copies; CSS displays exactly one at any width:
264
285
 
265
286
  ```tsx
266
- import { AssistantLauncher } from "@gemboss/ui";
287
+ import { AppShell, AssistantLauncher } from "@gemboss/ui";
267
288
 
268
- <AssistantLauncher label="Partner Assistant" shortcut="⌘J"
269
- hidden={dock.presentation !== "hidden"} onClick={() => dock.reopen()} />
289
+ const open = dock.presentation !== "hidden";
290
+ <AppShell
291
+ actions={<>
292
+ <AssistantLauncher placement="topbar" label="Partner Assistant" shortcut="⌘J" hidden={open} onClick={() => dock.reopen()} />
293
+ <AccountMenu … />
294
+ </>}
295
+ railBottom={<AssistantLauncher label="Partner Assistant" shortcut="⌘J" hidden={open} onClick={() => dock.reopen()} />}
296
+ />
270
297
  ```
271
298
 
272
- - No provider and no state: pass `hidden` while your assistant is open (it would sit under the dock).
273
- - 44px floor everywhere; label + shortcut on wide screens, the `sparkle` icon alone at ≤760px (the label stays the
274
- accessible name). Safe-area insets keep it off the iPhone home indicator. z-index 280: under the dock and the topbar,
275
- over page content.
299
+ - `placement="rail"` (default): the last row of the rail, full width, primary (near-black), label + shortcut. Hidden at
300
+ ≤760px, where the rail is a drawer nobody sees until they open it.
301
+ - `placement="topbar"`: a 44×44 icon button, left of the account chip, ≤760px only. The label stays the accessible name.
302
+ - No provider and no state: pass `hidden` while your assistant is open.
276
303
  - The shortcut is a hint only: the surface owns the key binding.
304
+ - The phone topbar (≤760px) gives in this order when it is short: the search (to a 96px floor), then the brand name
305
+ (ellipsis). The actions never shrink, so the account chip stays on screen (0.6.1; `topbar.test.tsx`).
306
+ - 0.6.0, not a patch: an existing `<AssistantLauncher>` call changes meaning (no longer fixed; the default copy is the
307
+ rail one, hidden on phones). A surface on `^0.5.x` must move to `^0.6.0` in the same PR that places the two copies.
277
308
 
278
309
  ## The topbar kit: lockup, account, icons
279
310
 
@@ -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="#report">Báo cáo</a><a href="#resizer">Thanh kéo giữa hai cột</a><a href="#modal">Hộp thoại, toast, menu (JS thuần)</a><a href="#shell">Khung app</a><a href="#auth">Đăng nhập</a><a href="#chat">Thân chat</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.5.0</h1><p>Ngôn ngữ thiết kế chung của các công cụ GemBoss. Markup dưới mỗi ví dụ là đúng thứ package render ra; bề mặt không dùng React (HTML tĩnh, Python) chép markup này và nạp <code>gemboss-ui.css</code>. Dựng từ gemboss <code>5a8d401f4c0a</code>.</p></header>
30
+ <main><header class="cat-head"><h1>@gemboss/ui 0.6.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>30b3920a2a6c</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ể, tông đỏ, một cỡ nhỏ. Cao 32px dưới chuột (bằng ô nhập và ô chọn), 44px dưới ngón tay và ở khổ điện thoại, cỡ nhỏ cũng vậy. Dùng được trên <code>&lt;button&gt;</code> lẫn <code>&lt;a&gt;</code> (căn giữa sẵn).</p><figure class="cat-demo"><figcaption>Mặc định · chính · ghost · nhỏ · tắt</figcaption><div class="cat-live"><div class="cat-row"><button type="button" class="ui-btn">Lưu nháp</button><button type="button" class="ui-btn ui-btn--primary">Xuất bản</button><button type="button" class="ui-btn ui-btn--ghost">Huỷ</button><button type="button" class="ui-btn ui-btn--sm">Nhỏ</button><button type="button" class="ui-btn ui-btn--primary" disabled>Đang lưu…</button><a class="ui-btn" href="#buttons">Link dạng nút</a></div></div><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;cat-row&quot;&gt;
33
33
  &lt;button type=&quot;button&quot; class=&quot;ui-btn&quot;&gt;Lưu nháp&lt;/button&gt;
@@ -583,10 +583,10 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
583
583
  &lt;button type=&quot;button&quot; class=&quot;ui-btn ui-btn--primary&quot;&gt;Xoá&lt;/button&gt;
584
584
  &lt;/div&gt;
585
585
  &lt;/div&gt;</code></pre></details></figure></section>
586
- <section id="shell"><h2>Khung app</h2><p>Topbar 56px, rail 240px, ≤760px thành ngăn kéo. Logo <code>BrandLockup</code>, tài khoản <code>AccountMenu</code> (bấm chữ viết tắt), mục Trợ lý ở chân rail có class <code>gnav-assistant</code>.</p><figure class="cat-demo cat-screen"><figcaption>AppShell + BrandLockup + AccountMenu + Icon</figcaption><iframe title="AppShell + BrandLockup + AccountMenu + Icon" style="height:460px" srcdoc="&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;../gemboss-ui.css&quot;&gt;&lt;/head&gt;&lt;body&gt;&lt;div class=&quot;gemboss-platform-shell&quot;&gt;&lt;header class=&quot;gemboss-platform-topbar&quot;&gt;&lt;div class=&quot;gemboss-topbar-start&quot;&gt;&lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-menu-btn&quot; aria-label=&quot;Mở menu&quot; aria-expanded=&quot;false&quot; aria-controls=&quot;:R0:&quot;&gt;&lt;svg width=&quot;20&quot; height=&quot;20&quot; viewBox=&quot;0 0 20 20&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M3 5.5h14M3 10h14M3 14.5h14&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.7&quot; stroke-linecap=&quot;round&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/button&gt;&lt;a class=&quot;gb-brand&quot; href=&quot;#&quot; aria-label=&quot;GemFactory home&quot;&gt;&lt;svg class=&quot;gb-brand-gem&quot; viewBox=&quot;0 0 32 40&quot; width=&quot;20.8&quot; height=&quot;26&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g transform=&quot;translate(0,0.8) scale(0.31)&quot;&gt;&lt;polygon points=&quot;26,14 74,14 66,50 34,50&quot; fill=&quot;#5661F5&quot; fill-opacity=&quot;0.92&quot;&gt;&lt;/polygon&gt;&lt;path d=&quot;M26,14 74,14 96,50 50,110 4,50Z&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;8.75&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;5.45&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;span class=&quot;gb-brand-name&quot;&gt;GemFactory&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gemboss-topbar-center&quot;&gt;&lt;/div&gt;&lt;div class=&quot;gemboss-topbar-end&quot;&gt;&lt;details class=&quot;gb-account&quot;&gt;&lt;summary class=&quot;gemboss-account-chip&quot; aria-label=&quot;Account · Minh Anh&quot; title=&quot;Minh Anh&quot;&gt;MA&lt;/summary&gt;&lt;div role=&quot;menu&quot; class=&quot;gemboss-popover-surface gemboss-account-menu&quot;&gt;&lt;div class=&quot;gb-account-head&quot;&gt;&lt;div class=&quot;gb-account-name&quot;&gt;Minh Anh&lt;/div&gt;&lt;div class=&quot;gb-account-email&quot;&gt;ma@gemboss.ai&lt;/div&gt;&lt;div class=&quot;gb-account-role&quot;&gt;Designer&lt;/div&gt;&lt;/div&gt;&lt;a role=&quot;menuitem&quot; class=&quot;gemboss-account-menu-item&quot; href=&quot;#&quot;&gt;Cài đặt&lt;/a&gt;&lt;form method=&quot;post&quot; action=&quot;#&quot; class=&quot;gb-account-form&quot;&gt;&lt;button role=&quot;menuitem&quot; type=&quot;submit&quot; class=&quot;gemboss-account-menu-item danger&quot;&gt;Đăng xuất&lt;/button&gt;&lt;/form&gt;&lt;/div&gt;&lt;/details&gt;&lt;/div&gt;&lt;/header&gt;&lt;nav class=&quot;icon-nav&quot; aria-label=&quot;Main navigation&quot; id=&quot;:R0:&quot;&gt;&lt;div class=&quot;gnav-scroll&quot;&gt;&lt;div class=&quot;gnav-heading&quot;&gt;Kho&lt;/div&gt;&lt;div class=&quot;gnav-group active&quot;&gt;&lt;a class=&quot;gnav-item active&quot; aria-current=&quot;page&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;rect x=&quot;3&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;13&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;8&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;9&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;13&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;6&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Board&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gnav-group&quot;&gt;&lt;a class=&quot;gnav-item&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;rect x=&quot;3.5&quot; y=&quot;3&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;3.5&quot; y=&quot;8.25&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;3.5&quot; y=&quot;13.5&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Ghép trang&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gnav-group&quot;&gt;&lt;a class=&quot;gnav-item&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M10 3.2 17.3 16H2.7z&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M10 8.2v3.6M10 13.9v.1&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Framework thiếu&lt;/span&gt;&lt;span class=&quot;gnav-count&quot; aria-label=&quot;3 mục đang mở&quot;&gt;3&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;gnav-bottom&quot;&gt;&lt;div class=&quot;gnav-sep&quot;&gt;&lt;/div&gt;&lt;a class=&quot;gnav-item gnav-assistant&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M3 4.5h14v9H8.5L5 16.5v-3H3z&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Trợ lý&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;/nav&gt;&lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-nav-backdrop&quot; aria-label=&quot;Đóng menu&quot; tabindex=&quot;-1&quot;&gt;&lt;/button&gt;&lt;main class=&quot;gemboss-platform-main&quot;&gt;&lt;div style=&quot;padding:24px&quot;&gt;&lt;h1 style=&quot;margin:0&quot;&gt;Nội dung màn&lt;/h1&gt;&lt;/div&gt;&lt;/main&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;"></iframe><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;gemboss-platform-shell&quot;&gt;
586
+ <section id="shell"><h2>Khung app</h2><p>Topbar 56px, rail 240px, ≤760px thành ngăn kéo. Logo <code>BrandLockup</code>, tài khoản <code>AccountMenu</code> (bấm chữ viết tắt), mục Trợ lý ở chân rail có class <code>gnav-assistant</code>.</p><figure class="cat-demo cat-screen"><figcaption>AppShell + BrandLockup + AccountMenu + Icon</figcaption><iframe title="AppShell + BrandLockup + AccountMenu + Icon" style="height:460px" srcdoc="&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;../gemboss-ui.css&quot;&gt;&lt;/head&gt;&lt;body&gt;&lt;div class=&quot;gemboss-platform-shell&quot;&gt;&lt;header class=&quot;gemboss-platform-topbar&quot;&gt;&lt;div class=&quot;gemboss-topbar-start&quot;&gt;&lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-menu-btn&quot; aria-label=&quot;Mở menu&quot; aria-expanded=&quot;false&quot; data-label-open=&quot;Mở menu&quot; data-label-close=&quot;Đóng menu&quot; aria-controls=&quot;:R0:&quot;&gt;&lt;svg width=&quot;20&quot; height=&quot;20&quot; viewBox=&quot;0 0 20 20&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M3 5.5h14M3 10h14M3 14.5h14&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.7&quot; stroke-linecap=&quot;round&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/button&gt;&lt;a class=&quot;gb-brand&quot; href=&quot;#&quot; aria-label=&quot;GemFactory home&quot;&gt;&lt;svg class=&quot;gb-brand-gem&quot; viewBox=&quot;0 0 32 40&quot; width=&quot;20.8&quot; height=&quot;26&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g transform=&quot;translate(0,0.8) scale(0.31)&quot;&gt;&lt;polygon points=&quot;26,14 74,14 66,50 34,50&quot; fill=&quot;#5661F5&quot; fill-opacity=&quot;0.92&quot;&gt;&lt;/polygon&gt;&lt;path d=&quot;M26,14 74,14 96,50 50,110 4,50Z&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;8.75&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;5.45&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;span class=&quot;gb-brand-name&quot;&gt;GemFactory&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gemboss-topbar-center&quot;&gt;&lt;/div&gt;&lt;div class=&quot;gemboss-topbar-end&quot;&gt;&lt;details class=&quot;gb-account&quot;&gt;&lt;summary class=&quot;gemboss-account-chip&quot; aria-label=&quot;Account · Minh Anh&quot; title=&quot;Minh Anh&quot;&gt;MA&lt;/summary&gt;&lt;div role=&quot;menu&quot; class=&quot;gemboss-popover-surface gemboss-account-menu&quot;&gt;&lt;div class=&quot;gb-account-head&quot;&gt;&lt;div class=&quot;gb-account-name&quot;&gt;Minh Anh&lt;/div&gt;&lt;div class=&quot;gb-account-email&quot;&gt;ma@gemboss.ai&lt;/div&gt;&lt;div class=&quot;gb-account-role&quot;&gt;Designer&lt;/div&gt;&lt;/div&gt;&lt;a role=&quot;menuitem&quot; class=&quot;gemboss-account-menu-item&quot; href=&quot;#&quot;&gt;Cài đặt&lt;/a&gt;&lt;form method=&quot;post&quot; action=&quot;#&quot; class=&quot;gb-account-form&quot;&gt;&lt;button role=&quot;menuitem&quot; type=&quot;submit&quot; class=&quot;gemboss-account-menu-item danger&quot;&gt;Đăng xuất&lt;/button&gt;&lt;/form&gt;&lt;/div&gt;&lt;/details&gt;&lt;/div&gt;&lt;/header&gt;&lt;nav class=&quot;icon-nav&quot; aria-label=&quot;Main navigation&quot; id=&quot;:R0:&quot;&gt;&lt;div class=&quot;gnav-scroll&quot;&gt;&lt;div class=&quot;gnav-heading&quot;&gt;Kho&lt;/div&gt;&lt;div class=&quot;gnav-group active&quot;&gt;&lt;a class=&quot;gnav-item active&quot; aria-current=&quot;page&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;rect x=&quot;3&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;13&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;8&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;9&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;13&quot; y=&quot;3.5&quot; width=&quot;4&quot; height=&quot;6&quot; rx=&quot;1.2&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Board&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gnav-group&quot;&gt;&lt;a class=&quot;gnav-item&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;rect x=&quot;3.5&quot; y=&quot;3&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;3.5&quot; y=&quot;8.25&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;rect x=&quot;3.5&quot; y=&quot;13.5&quot; width=&quot;13&quot; height=&quot;3.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Ghép trang&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;gnav-group&quot;&gt;&lt;a class=&quot;gnav-item&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M10 3.2 17.3 16H2.7z&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M10 8.2v3.6M10 13.9v.1&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Framework thiếu&lt;/span&gt;&lt;span class=&quot;gnav-count&quot; aria-label=&quot;3 mục đang mở&quot;&gt;3&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;gnav-bottom&quot;&gt;&lt;div class=&quot;gnav-sep&quot;&gt;&lt;/div&gt;&lt;a class=&quot;gnav-item gnav-assistant&quot; href=&quot;#&quot;&gt;&lt;span class=&quot;gnav-ico&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;g fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;path d=&quot;M3 4.5h14v9H8.5L5 16.5v-3H3z&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;gnav-label&quot;&gt;Trợ lý&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;/nav&gt;&lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-nav-backdrop&quot; aria-label=&quot;Đóng menu&quot; tabindex=&quot;-1&quot;&gt;&lt;/button&gt;&lt;main class=&quot;gemboss-platform-main&quot;&gt;&lt;div style=&quot;padding:24px&quot;&gt;&lt;h1 style=&quot;margin:0&quot;&gt;Nội dung màn&lt;/h1&gt;&lt;/div&gt;&lt;/main&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;"></iframe><details class="cat-code"><summary>Markup</summary><pre><code>&lt;div class=&quot;gemboss-platform-shell&quot;&gt;
587
587
  &lt;header class=&quot;gemboss-platform-topbar&quot;&gt;
588
588
  &lt;div class=&quot;gemboss-topbar-start&quot;&gt;
589
- &lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-menu-btn&quot; aria-label=&quot;Mở menu&quot; aria-expanded=&quot;false&quot; aria-controls=&quot;:R0:&quot;&gt;
589
+ &lt;button type=&quot;button&quot; class=&quot;gemboss-mobile-menu-btn&quot; aria-label=&quot;Mở menu&quot; aria-expanded=&quot;false&quot; data-label-open=&quot;Mở menu&quot; data-label-close=&quot;Đóng menu&quot; aria-controls=&quot;:R0:&quot;&gt;
590
590
  &lt;svg width=&quot;20&quot; height=&quot;20&quot; viewBox=&quot;0 0 20 20&quot; aria-hidden=&quot;true&quot;&gt;
591
591
  &lt;path d=&quot;M3 5.5h14M3 10h14M3 14.5h14&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.7&quot; stroke-linecap=&quot;round&quot;&gt;
592
592
  &lt;/path&gt;
@@ -703,7 +703,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
703
703
  &lt;/div&gt;
704
704
  &lt;/main&gt;
705
705
  &lt;/div&gt;</code></pre></details></figure></section>
706
- <section id="auth"><h2>Đăng nhập</h2><figure class="cat-demo cat-screen"><figcaption>AuthLayout + PasswordField</figcaption><iframe title="AuthLayout + PasswordField" style="height:620px" srcdoc="&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;../gemboss-ui.css&quot;&gt;&lt;/head&gt;&lt;body&gt;&lt;main class=&quot;gb-auth&quot;&gt;&lt;div class=&quot;gb-auth-split&quot;&gt;&lt;div class=&quot;gb-auth-stage&quot;&gt;&lt;div class=&quot;gb-auth-card&quot;&gt;&lt;header class=&quot;gb-auth-header&quot;&gt;&lt;span class=&quot;gb-auth-mark&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg viewBox=&quot;0 0 32 32&quot; width=&quot;56&quot; height=&quot;56&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;rect width=&quot;32&quot; height=&quot;32&quot; rx=&quot;7&quot; style=&quot;fill:var(--gb-mark-bg)&quot;&gt;&lt;/rect&gt;&lt;g transform=&quot;translate(16,16) scale(0.24) translate(-50,-62)&quot;&gt;&lt;polygon points=&quot;26,14 74,14 66,50 34,50&quot; fill=&quot;#8a93ff&quot;&gt;&lt;/polygon&gt;&lt;path d=&quot;M26,14 74,14 96,50 50,110 4,50Z&quot; fill=&quot;none&quot; stroke=&quot;#f4f4f2&quot; stroke-width=&quot;5&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110&quot; fill=&quot;none&quot; stroke=&quot;#f4f4f2&quot; stroke-opacity=&quot;0.72&quot; stroke-width=&quot;3&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;h1&gt;GemFactory&lt;/h1&gt;&lt;p&gt;Kho section của GemBoss&lt;/p&gt;&lt;/header&gt;&lt;form class=&quot;gb-auth-form&quot; method=&quot;post&quot; action=&quot;#&quot;&gt;&lt;div class=&quot;gb-auth-field&quot;&gt;&lt;label for=&quot;pw&quot;&gt;Mật khẩu&lt;/label&gt;&lt;div class=&quot;gb-auth-pw&quot;&gt;&lt;input class=&quot;gb-auth-input&quot; id=&quot;pw&quot; type=&quot;password&quot;/&gt;&lt;button type=&quot;button&quot; aria-controls=&quot;pw&quot; aria-label=&quot;Hiện&quot; title=&quot;Hiện&quot; class=&quot;gb-auth-pw-toggle&quot;&gt;&lt;svg width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 20 20&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.6&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;path d=&quot;M1.5 10S4.6 4.5 10 4.5 18.5 10 18.5 10 15.4 15.5 10 15.5 1.5 10 1.5 10Z&quot;&gt;&lt;/path&gt;&lt;circle cx=&quot;10&quot; cy=&quot;10&quot; r=&quot;2.6&quot;&gt;&lt;/circle&gt;&lt;/svg&gt;&lt;/button&gt;&lt;/div&gt;&lt;/div&gt;&lt;button type=&quot;submit&quot; class=&quot;gb-auth-submit&quot;&gt;Đăng nhập&lt;/button&gt;&lt;/form&gt;&lt;/div&gt;&lt;/div&gt;&lt;aside class=&quot;gb-auth-panel&quot;&gt;&lt;div class=&quot;gb-auth-panel-in&quot;&gt;&lt;p class=&quot;gb-auth-eyebrow&quot;&gt;GemBoss&lt;/p&gt;&lt;h2 class=&quot;gb-auth-panel-h&quot;&gt;Một ngôn ngữ thiết kế cho mọi công cụ&lt;/h2&gt;&lt;p class=&quot;gb-auth-panel-p&quot;&gt;Đăng nhập bằng mật khẩu đội đã gửi riêng.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;&lt;/div&gt;&lt;/main&gt;&lt;/body&gt;&lt;/html&gt;"></iframe><details class="cat-code"><summary>Markup</summary><pre><code>&lt;main class=&quot;gb-auth&quot;&gt;
706
+ <section id="auth"><h2>Đăng nhập</h2><figure class="cat-demo cat-screen"><figcaption>AuthLayout + PasswordField</figcaption><iframe title="AuthLayout + PasswordField" style="height:620px" srcdoc="&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;../gemboss-ui.css&quot;&gt;&lt;/head&gt;&lt;body&gt;&lt;main class=&quot;gb-auth&quot;&gt;&lt;div class=&quot;gb-auth-split&quot;&gt;&lt;div class=&quot;gb-auth-stage&quot;&gt;&lt;div class=&quot;gb-auth-card&quot;&gt;&lt;header class=&quot;gb-auth-header&quot;&gt;&lt;span class=&quot;gb-auth-mark&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg viewBox=&quot;0 0 32 32&quot; width=&quot;56&quot; height=&quot;56&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;rect width=&quot;32&quot; height=&quot;32&quot; rx=&quot;7&quot; style=&quot;fill:var(--gb-mark-bg)&quot;&gt;&lt;/rect&gt;&lt;g transform=&quot;translate(16,16) scale(0.24) translate(-50,-62)&quot;&gt;&lt;polygon points=&quot;26,14 74,14 66,50 34,50&quot; fill=&quot;#8a93ff&quot;&gt;&lt;/polygon&gt;&lt;path d=&quot;M26,14 74,14 96,50 50,110 4,50Z&quot; fill=&quot;none&quot; stroke=&quot;#f4f4f2&quot; stroke-width=&quot;5&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110&quot; fill=&quot;none&quot; stroke=&quot;#f4f4f2&quot; stroke-opacity=&quot;0.72&quot; stroke-width=&quot;3&quot; stroke-linejoin=&quot;round&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/span&gt;&lt;h1&gt;GemFactory&lt;/h1&gt;&lt;p&gt;Kho section của GemBoss&lt;/p&gt;&lt;/header&gt;&lt;form class=&quot;gb-auth-form&quot; method=&quot;post&quot; action=&quot;#&quot;&gt;&lt;div class=&quot;gb-auth-field&quot;&gt;&lt;label for=&quot;pw&quot;&gt;Mật khẩu&lt;/label&gt;&lt;div class=&quot;gb-auth-pw&quot;&gt;&lt;input class=&quot;gb-auth-input&quot; id=&quot;pw&quot; type=&quot;password&quot;/&gt;&lt;button type=&quot;button&quot; aria-controls=&quot;pw&quot; aria-label=&quot;Hiện&quot; title=&quot;Hiện&quot; data-label-show=&quot;Hiện&quot; data-label-hide=&quot;Ẩn&quot; class=&quot;gb-auth-pw-toggle&quot;&gt;&lt;svg width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 20 20&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.6&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;&lt;path d=&quot;M1.5 10S4.6 4.5 10 4.5 18.5 10 18.5 10 15.4 15.5 10 15.5 1.5 10 1.5 10Z&quot;&gt;&lt;/path&gt;&lt;circle cx=&quot;10&quot; cy=&quot;10&quot; r=&quot;2.6&quot;&gt;&lt;/circle&gt;&lt;/svg&gt;&lt;/button&gt;&lt;/div&gt;&lt;/div&gt;&lt;button type=&quot;submit&quot; class=&quot;gb-auth-submit&quot;&gt;Đăng nhập&lt;/button&gt;&lt;/form&gt;&lt;/div&gt;&lt;/div&gt;&lt;aside class=&quot;gb-auth-panel&quot;&gt;&lt;div class=&quot;gb-auth-panel-in&quot;&gt;&lt;p class=&quot;gb-auth-eyebrow&quot;&gt;GemBoss&lt;/p&gt;&lt;h2 class=&quot;gb-auth-panel-h&quot;&gt;Một ngôn ngữ thiết kế cho mọi công cụ&lt;/h2&gt;&lt;p class=&quot;gb-auth-panel-p&quot;&gt;Đăng nhập bằng mật khẩu đội đã gửi riêng.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;&lt;/div&gt;&lt;/main&gt;&lt;/body&gt;&lt;/html&gt;"></iframe><details class="cat-code"><summary>Markup</summary><pre><code>&lt;main class=&quot;gb-auth&quot;&gt;
707
707
  &lt;div class=&quot;gb-auth-split&quot;&gt;
708
708
  &lt;div class=&quot;gb-auth-stage&quot;&gt;
709
709
  &lt;div class=&quot;gb-auth-card&quot;&gt;
@@ -730,7 +730,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
730
730
  &lt;label for=&quot;pw&quot;&gt;Mật khẩu&lt;/label&gt;
731
731
  &lt;div class=&quot;gb-auth-pw&quot;&gt;
732
732
  &lt;input class=&quot;gb-auth-input&quot; id=&quot;pw&quot; type=&quot;password&quot;/&gt;
733
- &lt;button type=&quot;button&quot; aria-controls=&quot;pw&quot; aria-label=&quot;Hiện&quot; title=&quot;Hiện&quot; class=&quot;gb-auth-pw-toggle&quot;&gt;
733
+ &lt;button type=&quot;button&quot; aria-controls=&quot;pw&quot; aria-label=&quot;Hiện&quot; title=&quot;Hiện&quot; data-label-show=&quot;Hiện&quot; data-label-hide=&quot;Ẩn&quot; class=&quot;gb-auth-pw-toggle&quot;&gt;
734
734
  &lt;svg width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 20 20&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.6&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;
735
735
  &lt;path d=&quot;M1.5 10S4.6 4.5 10 4.5 18.5 10 18.5 10 15.4 15.5 10 15.5 1.5 10 1.5 10Z&quot;&gt;
736
736
  &lt;/path&gt;
package/gemboss-ui.css CHANGED
@@ -1,11 +1,11 @@
1
1
  /*
2
2
  * GemBoss admin design system — GENERATED — do not edit; regenerate from gemboss:
3
3
  * node apps/admin/scripts/export-ui-css.mjs --out <path> (npm run export:ui-css)
4
- * gemboss commit: 5a8d401f4c0a723a8ed69bbbe8871a0323fd66cd
5
- * sources sha256: e74fceb32d9769c9079a6fc1d3cda8d911188ce0b0825965f85ebf185d9200fb
4
+ * gemboss commit: 30b3920a2a6c25393d1c02e086146b6f8590c1e0
5
+ * sources sha256: 22617cdf59a7330f1285221022e5341d94e5b5e74cf917ce6f863c85733505e1
6
6
  * contents: 1 allow-listed rule from apps/admin/src/app/globals.css + packages/ui/src/gemboss-tokens.css
7
7
  * (verbatim) + packages/ui/src/ui.css (verbatim) + packages/ui/src/gemboss-field.css (verbatim)
8
- * + 180 rules from packages/ui/src/shell.css (the shared shell)
8
+ * + 181 rules from packages/ui/src/shell.css (the shared shell)
9
9
  * Polaris --p-* references are resolved to their fallbacks; declarations with no fallback are dropped
10
10
  * (0 dropped).
11
11
  */
@@ -396,7 +396,7 @@
396
396
  `.gemboss-admin-page-head` (01/10/2026, Chris: một chủ sở hữu; admin dùng bản này). Cố ý không đặt margin cho h1
397
397
  và không dùng flex: admin không reset margin tiêu đề, nên đổi ở đây là đổi khoảng cách của 14 trang admin. */
398
398
  .ui-page-head { margin-bottom: 20px; }
399
- .ui-page-head h1 { color: var(--ui-text-strong); font-size: 22px; font-weight: 650; letter-spacing: -0.01em; }
399
+ .ui-page-head h1 { color: var(--ui-text-strong); font-size: 22px; line-height: 28px; font-weight: 650; letter-spacing: -0.01em; } /* own line-height: the 20px body line overlapped a wrapped title (0.5.1) */
400
400
  .ui-page-head p { color: var(--ui-text-muted); font-size: var(--ui-fs-body); margin-top: 4px; }
401
401
 
402
402
  /* ── Thanh công cụ: `<div class="ui-toolbar">` — ô tìm giãn, điều khiển bám phải, một hàng gọn, xuống dòng khi hẹp.
@@ -452,8 +452,8 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
452
452
  .ui-badge {
453
453
  display: inline-flex; align-items: center; gap: 4px;
454
454
  font-size: var(--ui-fs-caption); line-height: 16px; font-weight: 550; white-space: nowrap;
455
- padding: 2px 8px; border-radius: var(--ui-radius-md); background: var(--ui-surface-2); color: var(--ui-text-muted);
456
- }
455
+ padding: 2px 8px; border-radius: var(--ui-radius-md); background: color-mix(in srgb, var(--ui-text) 8%, transparent); color: var(--ui-text-muted);
456
+ } /* a tint of the ink, not --ui-surface-2: that is the table row hover, and a badge vanished on hover (0.5.1) */
457
457
 
458
458
  /* ── Modal ────────────────────────────────────────────────────────────────────
459
459
  Rendered INLINE (not portaled): a portal escapes the .gemboss-admin-shell token
@@ -483,7 +483,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
483
483
  }
484
484
  .ui-modal__close svg { width: 20px; height: 20px; }
485
485
  .ui-modal__close:hover { background: var(--ui-surface-2); color: var(--ui-text); }
486
- .ui-modal__close:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: -2px; }
486
+ .ui-modal__close:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
487
487
 
488
488
  /* `size="lg"` — the screen-shaped dialog. Only the HOOK lives here: this sheet is imported by the
489
489
  root layout, so every rule in it is paid on every screen, and the geometry of a dialog with one
@@ -531,6 +531,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
531
531
  .ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
532
532
  .ui-seg :is(button, a) + :is(button, a) { border-left: 1px solid var(--ui-border); }
533
533
  .ui-seg :is(button, a):hover { background: var(--ui-surface-2); color: var(--ui-text); }
534
+ .ui-seg :is(button, a):focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
534
535
  /* Lọc / chế độ xem bằng LINK (trang server-render, chọn = đổi URL): `<a aria-current="page">` là ô đang chọn. */
535
536
  .ui-seg a { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; color: var(--ui-text-muted); }
536
537
  .ui-seg a[aria-current="page"],
@@ -856,7 +857,7 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
856
857
  /* ↑ the right edge is what keeps a WHITE swatch visible: without it the block melted into the field (measured). */
857
858
  .gemboss-field--group input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
858
859
  .gemboss-field--group input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 0; }
859
- .gemboss-field__bare { flex: 1; min-width: 0; align-self: stretch; padding: 0 12px 0 0; border: 0; border-radius: 0; outline: none; background: transparent; color: inherit; font: inherit; }
860
+ .gemboss-field__bare { flex: 1; min-width: 0; align-self: stretch; margin-block: -1px; /* over the group's 1px border: the input is the 44px target, not 42 (0.5.1) */ padding: 0 12px 0 0; border: 0; border-radius: 0; outline: none; background: transparent; color: inherit; font: inherit; }
860
861
  /* …and the placeholder, which "draws nothing" does not cover: the surfaces that used to declare
861
862
  their own (the /admin search rows, the settings and palette rows) stopped when they stopped
862
863
  owning the box, and without this every bare field would fall back to the browser's grey. */
@@ -1195,6 +1196,10 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
1195
1196
  .gnav-item:hover {
1196
1197
  background: var(--gemboss-nav-hover);
1197
1198
  }
1199
+ .gnav-item:focus-visible {
1200
+ outline: 2px solid var(--gemboss-accent);
1201
+ outline-offset: -2px;
1202
+ }
1198
1203
  .gnav-item.active {
1199
1204
  color: var(--gemboss-active-fg);
1200
1205
  font-weight: 600;
@@ -2019,16 +2024,30 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
2019
2024
  font-size: 13px; line-height: 20px; font-weight: 450; color: var(--gemboss-text);
2020
2025
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
2021
2026
  }
2022
- :where(h1, h2, h3) { letter-spacing: -0.01em; }
2027
+ /* Headings get their own line (Polaris heading line-heights are ~1.25-1.4): inheriting the body's 20px made a wrapped 22px
2028
+ * title overlap itself (gempress, 0.5.1). */
2029
+ :where(h1, h2, h3) { letter-spacing: -0.01em; line-height: 1.3; }
2023
2030
 
2024
2031
  .gemboss-topbar-start { display: flex; align-items: center; gap: 8px; min-width: 0; }
2025
2032
  .gemboss-topbar-center { display: flex; justify-content: center; min-width: 0; }
2026
2033
  .gemboss-topbar-end { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; }
2027
2034
  @media (max-width: 760px) {
2028
- /* The topbar is one flex row here: the middle slot takes what is left. */
2029
- .gemboss-topbar-center { flex: 1 1 auto; justify-content: flex-start; }
2035
+ /* The topbar is one flex row here: the middle slot takes what is left. Basis 0, not auto: with `auto` the search's own
2036
+ * width (up to 520px) was its claim, so the other slots had to give way to it (0.6.0 pinned them instead, see below). With
2037
+ * 0 it only takes what the others leave, down to a 96px floor: an input with its icon still reads as search. */
2038
+ .gemboss-topbar-center { flex: 1 1 0; min-width: 96px; justify-content: flex-start; }
2039
+ .gemboss-topbar-center:empty { min-width: 0; }
2030
2040
  /* No actions: the empty slot would still take one flex `gap` (8px) off the search at 320px. */
2031
2041
  .gemboss-topbar-end:empty { display: none; }
2042
+ /* The actions never give: with `min-width: 0` the slot shrank and its buttons (justify flex-end) spilled LEFT over the
2043
+ * search (assistant + account, 15px over partner-radar's search at 320 and 360, 0.6.0).
2044
+ * The start slot gives LAST, after the search reaches its floor: the brand name ellipsizes (.gb-brand-name) instead of
2045
+ * the account chip leaving the screen. 0.6.0 pinned it (`flex: 0 0 auto`) and a 30-character brand at 461px pushed the
2046
+ * chip 44px past the right edge (review #3690). The menu button and the mark keep their 44px. */
2047
+ .gemboss-topbar-end { flex: 0 0 auto; }
2048
+ .gemboss-topbar-start { flex: 0 1 auto; }
2049
+ .gemboss-topbar-start > .gemboss-mobile-menu-btn { flex: 0 0 auto; }
2050
+ .gemboss-topbar-start > .gb-brand { min-width: 44px; }
2032
2051
  }
2033
2052
 
2034
2053
  .gnav-heading {
@@ -2069,7 +2088,11 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
2069
2088
  overflow: hidden; text-overflow: ellipsis; min-width: 0;
2070
2089
  }
2071
2090
  /* The admin shows only its mark below 460px; so does this. */
2072
- @media (max-width: 460px) { .gb-brand-name { display: none; } }
2091
+ @media (max-width: 460px) {
2092
+ .gb-brand-name { display: none; }
2093
+ /* The mark alone is ~21px wide: a 44px target, centred (visual-gate tap_target_link on content-radar at 320, 0.5.1). */
2094
+ .gb-brand { min-width: 44px; height: 44px; justify-content: center; }
2095
+ }
2073
2096
 
2074
2097
  /* ── AccountMenu (./account.tsx): its rules live in shell.css since 29/09/2026, because the admin
2075
2098
  * renders AccountMenu too now and loads shell.css, not this file. */
@@ -3180,40 +3203,63 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
3180
3203
  .gb-dock-side .gb-dock-sidegrip, .gb-dock-pinside { display: none; }
3181
3204
  }
3182
3205
 
3183
- /* ── AssistantLauncher (02/10/2026): the floating button that opens the assistant, bottom-right ──────────────────────
3184
- Top-right is the account corner (AccountMenu). Below the dock (290/320) and the topbar (300), above page content; the
3185
- phone drawer (1000) covers it. 44px floor at every width; text + shortcut on wide screens, the icon alone on phones
3186
- (the label stays the accessible name). Clears the iPhone home indicator with safe-area insets. */
3206
+ /* ── AssistantLauncher: the button that opens the assistant. NOT floating (03/10/2026) ────────────────────────────────
3207
+ 0.4.2 floated it bottom-right (z 280). visual-gate on partner-radar prod, 03/10: `content_covered` red at 5 of 6 widths
3208
+ on every page measured (/, /apps, /sources): a fixed button always sits on whatever text scrolls under it, and at
3209
+ scroll 0 that is a table column. GemCommunity had dropped its floating buttons for the same reason (#239). Chris chose
3210
+ "bỏ nút nổi". It now lives where nothing scrolls under it:
3211
+ · `--rail` wide screens: the last row of the rail (`railBottom`), a full-width primary button. Hidden ≤760px,
3212
+ where the rail is an off-canvas drawer nobody sees until they open it.
3213
+ · `--topbar` phones (≤760px): a 44px icon button in the topbar's `actions`, left of the account chip. Hidden above.
3214
+ The top-right corner stays the ACCOUNT. A surface renders both; exactly one is displayed at any width. */
3187
3215
  .gb-assistant-launcher {
3188
- position: fixed;
3189
- right: calc(20px + env(safe-area-inset-right, 0px));
3190
- bottom: calc(20px + env(safe-area-inset-bottom, 0px));
3191
- z-index: 280;
3192
3216
  display: inline-flex;
3193
3217
  align-items: center;
3194
3218
  gap: 8px;
3195
- min-height: 44px;
3196
- padding: 0 16px;
3197
3219
  border: 0;
3198
- border-radius: 999px;
3199
- background: var(--gemboss-primary, #1b1c1e);
3200
- color: var(--gemboss-on-primary, #fff);
3201
- font: 600 14px/1 var(--gemboss-font-sans, system-ui, sans-serif);
3202
- box-shadow: var(--gemboss-shadow-raised, 0 6px 18px rgba(0, 0, 0, 0.18));
3203
3220
  cursor: pointer;
3221
+ font-family: var(--gemboss-font-sans, system-ui, sans-serif);
3204
3222
  }
3205
- .gb-assistant-launcher:hover { background: var(--gemboss-primary-dark, #000); }
3206
- .gb-assistant-launcher:focus-visible { outline: 2px solid var(--gemboss-accent, #5661f5); outline-offset: 2px; }
3207
3223
  .gb-assistant-launcher svg { flex: 0 0 auto; }
3224
+ .gb-assistant-launcher:focus-visible { outline: 2px solid var(--gemboss-accent, #5661f5); outline-offset: 2px; }
3225
+ .gb-assistant-launcher--rail {
3226
+ width: 100%;
3227
+ min-height: 44px; /* a control, not a list link: §9.1 asks 44 (visual-gate red at 768 with 36) */
3228
+ margin: 8px 0 4px;
3229
+ padding: 0 12px;
3230
+ border-radius: var(--gemboss-radius-md, 8px);
3231
+ background: var(--gemboss-primary, #1b1c1e);
3232
+ color: var(--gemboss-on-primary, #fff);
3233
+ font-size: 13px;
3234
+ font-weight: 600;
3235
+ line-height: 1;
3236
+ text-align: left;
3237
+ }
3238
+ .gb-assistant-launcher--rail:hover { background: var(--gemboss-primary-dark, #000); }
3239
+ .gb-assistant-launcher--rail .gb-assistant-launcher-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3208
3240
  .gb-assistant-launcher-kbd {
3209
3241
  font: inherit; font-size: 11px; opacity: 0.7;
3210
3242
  border: 1px solid currentColor; border-radius: 5px; padding: 1px 5px;
3211
3243
  }
3244
+ .gb-assistant-launcher--topbar {
3245
+ display: none;
3246
+ position: relative; /* holds the clipped label: an absolute child of a static button escapes to the nearest positioned box */
3247
+ justify-content: center;
3248
+ flex: 0 0 auto;
3249
+ width: 44px;
3250
+ height: 44px;
3251
+ padding: 0;
3252
+ border-radius: var(--gemboss-radius-md, 8px);
3253
+ background: transparent;
3254
+ color: var(--gemboss-text, #1b1c1e);
3255
+ }
3256
+ .gb-assistant-launcher--topbar:hover, .gb-assistant-launcher--topbar:active { background: var(--gemboss-hover, #eceef2); }
3257
+ .gb-assistant-launcher--topbar .gb-assistant-launcher-label, .gb-assistant-launcher--topbar .gb-assistant-launcher-kbd {
3258
+ position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
3259
+ }
3212
3260
  @media (max-width: 760px) {
3213
- .gb-assistant-launcher { min-width: 52px; min-height: 52px; padding: 0; justify-content: center; }
3214
- .gb-assistant-launcher-label, .gb-assistant-launcher-kbd {
3215
- position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
3216
- }
3261
+ .gb-assistant-launcher--rail { display: none; }
3262
+ .gb-assistant-launcher--topbar { display: inline-flex; }
3217
3263
  }
3218
3264
 
3219
3265
  /* ═══ @gemboss/ui src/kit-plain.css ═══ */
@@ -3593,6 +3639,66 @@ dialog.ui-modal { padding: 0; max-width: calc(100% - 40px); max-height: calc(100
3593
3639
  dialog.ui-modal[open] { display: flex; flex-direction: column; }
3594
3640
  dialog.ui-modal::backdrop { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px); }
3595
3641
 
3642
+ /* ── 0.5.1: what the first surfaces on 0.5.0 still drew themselves (GemFactory, content-radar, 03/10/2026) ─────────── */
3643
+
3644
+ /* At phone width a short compact button ("Bỏ") was 44 tall but 41 wide: a target is an area. */
3645
+ @media (pointer: coarse), (max-width: 47.9975em) { .ui-btn.ui-btn--sm { min-width: 44px; } }
3646
+
3647
+ /* A long label in a badge ("Đã dùng 1 lần + 4 bài cùng nguồn · 2 Hot News") wraps instead of running out of its card. */
3648
+ .ui-badge--wrap { white-space: normal; overflow-wrap: anywhere; line-height: 16px; }
3649
+
3650
+ /* Page head with actions on the right, wrapping under the title when narrow:
3651
+ `<div class="ui-page-head ui-page-head--actions"><div><h1>…</h1><p>…</p></div><div class="ui-page-head__actions">
3652
+ <button class="ui-btn ui-btn--primary">…</button></div></div>`. */
3653
+ .ui-page-head--actions { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 12px 16px; }
3654
+ .ui-page-head--actions > :first-child { min-width: 0; }
3655
+ .ui-page-head__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2); }
3656
+
3657
+ /* Chips that TOGGLE (a filter applied in place, no URL): `<div class="ui-chips"><button type="button" class="ui-tag
3658
+ ui-tag--chip" aria-pressed="true">CRO</button>…`. Same box as the link chips (ui.css); pressed = the link's current. */
3659
+ .ui-chips > button.ui-tag--chip {
3660
+ min-height: 28px; padding: 4px 12px; border-radius: var(--ui-radius-md, 8px); cursor: pointer;
3661
+ font-family: inherit; font-size: var(--gemboss-fs-caption, 12px); color: var(--ui-text);
3662
+ }
3663
+ .ui-chips > button.ui-tag--chip:hover { background: var(--ui-surface-2); border-color: var(--ui-text-muted); }
3664
+ .ui-chips > button.ui-tag--chip:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 1px; }
3665
+ .ui-chips > button.ui-tag--chip[aria-pressed="true"] { background: var(--ui-accent-soft); border-color: var(--ui-accent); color: var(--ui-accent-strong); font-weight: 600; }
3666
+ @media (pointer: coarse), (max-width: 47.9975em) { .ui-chips > button.ui-tag--chip { min-height: 44px; } }
3667
+
3668
+ /* Chat at phone WIDTH (gemwatcher's assistant, measured 03/10/2026): the composer read 14px (iOS zooms the page on tap
3669
+ below 16) and "new chat" was 35px wide under a fine pointer. shell.css raises the latter under `pointer: coarse`; a
3670
+ desktop window at 390px reports fine. Here and not in chat.css / shell.css: the admin's first-load budget. */
3671
+ @media (max-width: 47.9975em) {
3672
+ /* Class doubled: chat.css is appended AFTER this file in gemboss-ui.css, so a same-weight rule there would win. */
3673
+ .gemboss-ec-input.gemboss-ec-input textarea { font-size: 16px; }
3674
+ .gemboss-assistant-collapse, .gemboss-assistant-newchat { min-height: 44px; min-width: 44px; }
3675
+ }
3676
+
3677
+ /* Segmented buttons that say their state the accessible way: `aria-pressed="true"` looks like `.active` (ui-tabs reads
3678
+ aria-selected already; gemcare had to set both on every group). */
3679
+ .ui-seg button[aria-pressed="true"] { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
3680
+
3681
+ /* Read-only field: visibly not editable (the admin's field has no such state; gempress kept its own `.field[readonly]`).
3682
+ The attribute doubled = (0,3,0), so it also holds over the field's own hover rule, which comes earlier. */
3683
+ :is(.gemboss-field, .ui-textarea)[readonly][readonly] { background: var(--ui-surface-2); border-color: var(--ui-border); cursor: default; }
3684
+
3685
+ /* A popup whose body and foot sit inside one <form>: the form takes no box, so `.ui-modal > .ui-modal__body` (the
3686
+ scrolling body) and the pinned foot still lay out as the popup's own children (gemcare's escalate form, 0.5.1). */
3687
+ .ui-modal > form { display: contents; }
3688
+ .ui-modal > form > .ui-modal__head, .ui-modal > form > .ui-modal__foot { flex: none; }
3689
+ .ui-modal > form > .ui-modal__body { flex: 1 1 auto; min-height: 0; overflow: auto; }
3690
+
3691
+ /* A metric that FILTERS the view below it, as a toggle: `<button type="button" class="ui-metric" aria-pressed="true">`
3692
+ (gemwatcher's Radar cards: as links to the same hash, Cmd-click opened an unfiltered tab, Space did nothing and the
3693
+ chosen card was told by colour alone). A metric link that is the current view: `a.ui-metric[aria-current="page"]`.
3694
+ Selected = iris edge, the shell's "this one" colour. */
3695
+ button.ui-metric { font: inherit; text-align: left; color: inherit; cursor: pointer; }
3696
+ button.ui-metric:hover { border-color: var(--ui-text-muted); }
3697
+ button.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
3698
+ /* Class doubled: the chosen card is usually the one under the pointer, and `button.ui-metric:hover` (0,2,1) would
3699
+ repaint its edge grey the moment it is clicked (measured on gemwatcher Radar). */
3700
+ .ui-metric.ui-metric:is([aria-pressed="true"], [aria-current="page"]) { border-color: var(--gemboss-accent); box-shadow: inset 0 0 0 1px var(--gemboss-accent); }
3701
+
3596
3702
  /* ═══ @gemboss/ui src/chat.css ═══ */
3597
3703
  /*
3598
3704
  * The chat BODY — what an answer looks like, wherever a GemBoss surface puts a chat (01/10/2026).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/ui",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",
package/src/app-shell.css CHANGED
@@ -23,16 +23,30 @@
23
23
  font-size: 13px; line-height: 20px; font-weight: 450; color: var(--gemboss-text);
24
24
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
25
25
  }
26
- :where(h1, h2, h3) { letter-spacing: -0.01em; }
26
+ /* Headings get their own line (Polaris heading line-heights are ~1.25-1.4): inheriting the body's 20px made a wrapped 22px
27
+ * title overlap itself (gempress, 0.5.1). */
28
+ :where(h1, h2, h3) { letter-spacing: -0.01em; line-height: 1.3; }
27
29
 
28
30
  .gemboss-topbar-start { display: flex; align-items: center; gap: 8px; min-width: 0; }
29
31
  .gemboss-topbar-center { display: flex; justify-content: center; min-width: 0; }
30
32
  .gemboss-topbar-end { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; }
31
33
  @media (max-width: 760px) {
32
- /* The topbar is one flex row here: the middle slot takes what is left. */
33
- .gemboss-topbar-center { flex: 1 1 auto; justify-content: flex-start; }
34
+ /* The topbar is one flex row here: the middle slot takes what is left. Basis 0, not auto: with `auto` the search's own
35
+ * width (up to 520px) was its claim, so the other slots had to give way to it (0.6.0 pinned them instead, see below). With
36
+ * 0 it only takes what the others leave, down to a 96px floor: an input with its icon still reads as search. */
37
+ .gemboss-topbar-center { flex: 1 1 0; min-width: 96px; justify-content: flex-start; }
38
+ .gemboss-topbar-center:empty { min-width: 0; }
34
39
  /* No actions: the empty slot would still take one flex `gap` (8px) off the search at 320px. */
35
40
  .gemboss-topbar-end:empty { display: none; }
41
+ /* The actions never give: with `min-width: 0` the slot shrank and its buttons (justify flex-end) spilled LEFT over the
42
+ * search (assistant + account, 15px over partner-radar's search at 320 and 360, 0.6.0).
43
+ * The start slot gives LAST, after the search reaches its floor: the brand name ellipsizes (.gb-brand-name) instead of
44
+ * the account chip leaving the screen. 0.6.0 pinned it (`flex: 0 0 auto`) and a 30-character brand at 461px pushed the
45
+ * chip 44px past the right edge (review #3690). The menu button and the mark keep their 44px. */
46
+ .gemboss-topbar-end { flex: 0 0 auto; }
47
+ .gemboss-topbar-start { flex: 0 1 auto; }
48
+ .gemboss-topbar-start > .gemboss-mobile-menu-btn { flex: 0 0 auto; }
49
+ .gemboss-topbar-start > .gb-brand { min-width: 44px; }
36
50
  }
37
51
 
38
52
  .gnav-heading {
@@ -73,7 +87,11 @@
73
87
  overflow: hidden; text-overflow: ellipsis; min-width: 0;
74
88
  }
75
89
  /* The admin shows only its mark below 460px; so does this. */
76
- @media (max-width: 460px) { .gb-brand-name { display: none; } }
90
+ @media (max-width: 460px) {
91
+ .gb-brand-name { display: none; }
92
+ /* The mark alone is ~21px wide: a 44px target, centred (visual-gate tap_target_link on content-radar at 320, 0.5.1). */
93
+ .gb-brand { min-width: 44px; height: 44px; justify-content: center; }
94
+ }
77
95
 
78
96
  /* ── AccountMenu (./account.tsx): its rules live in shell.css since 29/09/2026, because the admin
79
97
  * renders AccountMenu too now and loads shell.css, not this file. */
package/src/assistant.css CHANGED
@@ -495,38 +495,61 @@
495
495
  .gb-dock-side .gb-dock-sidegrip, .gb-dock-pinside { display: none; }
496
496
  }
497
497
 
498
- /* ── AssistantLauncher (02/10/2026): the floating button that opens the assistant, bottom-right ──────────────────────
499
- Top-right is the account corner (AccountMenu). Below the dock (290/320) and the topbar (300), above page content; the
500
- phone drawer (1000) covers it. 44px floor at every width; text + shortcut on wide screens, the icon alone on phones
501
- (the label stays the accessible name). Clears the iPhone home indicator with safe-area insets. */
498
+ /* ── AssistantLauncher: the button that opens the assistant. NOT floating (03/10/2026) ────────────────────────────────
499
+ 0.4.2 floated it bottom-right (z 280). visual-gate on partner-radar prod, 03/10: `content_covered` red at 5 of 6 widths
500
+ on every page measured (/, /apps, /sources): a fixed button always sits on whatever text scrolls under it, and at
501
+ scroll 0 that is a table column. GemCommunity had dropped its floating buttons for the same reason (#239). Chris chose
502
+ "bỏ nút nổi". It now lives where nothing scrolls under it:
503
+ · `--rail` wide screens: the last row of the rail (`railBottom`), a full-width primary button. Hidden ≤760px,
504
+ where the rail is an off-canvas drawer nobody sees until they open it.
505
+ · `--topbar` phones (≤760px): a 44px icon button in the topbar's `actions`, left of the account chip. Hidden above.
506
+ The top-right corner stays the ACCOUNT. A surface renders both; exactly one is displayed at any width. */
502
507
  .gb-assistant-launcher {
503
- position: fixed;
504
- right: calc(20px + env(safe-area-inset-right, 0px));
505
- bottom: calc(20px + env(safe-area-inset-bottom, 0px));
506
- z-index: 280;
507
508
  display: inline-flex;
508
509
  align-items: center;
509
510
  gap: 8px;
510
- min-height: 44px;
511
- padding: 0 16px;
512
511
  border: 0;
513
- border-radius: 999px;
514
- background: var(--gemboss-primary, #1b1c1e);
515
- color: var(--gemboss-on-primary, #fff);
516
- font: 600 14px/1 var(--gemboss-font-sans, system-ui, sans-serif);
517
- box-shadow: var(--gemboss-shadow-raised, 0 6px 18px rgba(0, 0, 0, 0.18));
518
512
  cursor: pointer;
513
+ font-family: var(--gemboss-font-sans, system-ui, sans-serif);
519
514
  }
520
- .gb-assistant-launcher:hover { background: var(--gemboss-primary-dark, #000); }
521
- .gb-assistant-launcher:focus-visible { outline: 2px solid var(--gemboss-accent, #5661f5); outline-offset: 2px; }
522
515
  .gb-assistant-launcher svg { flex: 0 0 auto; }
516
+ .gb-assistant-launcher:focus-visible { outline: 2px solid var(--gemboss-accent, #5661f5); outline-offset: 2px; }
517
+ .gb-assistant-launcher--rail {
518
+ width: 100%;
519
+ min-height: 44px; /* a control, not a list link: §9.1 asks 44 (visual-gate red at 768 with 36) */
520
+ margin: 8px 0 4px;
521
+ padding: 0 12px;
522
+ border-radius: var(--gemboss-radius-md, 8px);
523
+ background: var(--gemboss-primary, #1b1c1e);
524
+ color: var(--gemboss-on-primary, #fff);
525
+ font-size: 13px;
526
+ font-weight: 600;
527
+ line-height: 1;
528
+ text-align: left;
529
+ }
530
+ .gb-assistant-launcher--rail:hover { background: var(--gemboss-primary-dark, #000); }
531
+ .gb-assistant-launcher--rail .gb-assistant-launcher-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
523
532
  .gb-assistant-launcher-kbd {
524
533
  font: inherit; font-size: 11px; opacity: 0.7;
525
534
  border: 1px solid currentColor; border-radius: 5px; padding: 1px 5px;
526
535
  }
536
+ .gb-assistant-launcher--topbar {
537
+ display: none;
538
+ position: relative; /* holds the clipped label: an absolute child of a static button escapes to the nearest positioned box */
539
+ justify-content: center;
540
+ flex: 0 0 auto;
541
+ width: 44px;
542
+ height: 44px;
543
+ padding: 0;
544
+ border-radius: var(--gemboss-radius-md, 8px);
545
+ background: transparent;
546
+ color: var(--gemboss-text, #1b1c1e);
547
+ }
548
+ .gb-assistant-launcher--topbar:hover, .gb-assistant-launcher--topbar:active { background: var(--gemboss-hover, #eceef2); }
549
+ .gb-assistant-launcher--topbar .gb-assistant-launcher-label, .gb-assistant-launcher--topbar .gb-assistant-launcher-kbd {
550
+ position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
551
+ }
527
552
  @media (max-width: 760px) {
528
- .gb-assistant-launcher { min-width: 52px; min-height: 52px; padding: 0; justify-content: center; }
529
- .gb-assistant-launcher-label, .gb-assistant-launcher-kbd {
530
- position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
531
- }
553
+ .gb-assistant-launcher--rail { display: none; }
554
+ .gb-assistant-launcher--topbar { display: inline-flex; }
532
555
  }
package/src/auth.tsx CHANGED
@@ -173,6 +173,9 @@ export function PasswordField({ id, label = "Password", toggleLabels = { show: "
173
173
  // ("Hide password, pressed" while the password sits in plain text).
174
174
  aria-label={name}
175
175
  title={name}
176
+ // Both names for kit-plain.js `initPasswordFields()` on a page with no React (static render).
177
+ data-label-show={toggleLabels.show}
178
+ data-label-hide={toggleLabels.hide}
176
179
  className="gb-auth-pw-toggle"
177
180
  >
178
181
  <EyeIcon off={shown} />
@@ -88,7 +88,7 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
88
88
  /* ↑ the right edge is what keeps a WHITE swatch visible: without it the block melted into the field (measured). */
89
89
  .gemboss-field--group input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
90
90
  .gemboss-field--group input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 0; }
91
- .gemboss-field__bare { flex: 1; min-width: 0; align-self: stretch; padding: 0 12px 0 0; border: 0; border-radius: 0; outline: none; background: transparent; color: inherit; font: inherit; }
91
+ .gemboss-field__bare { flex: 1; min-width: 0; align-self: stretch; margin-block: -1px; /* over the group's 1px border: the input is the 44px target, not 42 (0.5.1) */ padding: 0 12px 0 0; border: 0; border-radius: 0; outline: none; background: transparent; color: inherit; font: inherit; }
92
92
  /* …and the placeholder, which "draws nothing" does not cover: the surfaces that used to declare
93
93
  their own (the /admin search rows, the settings and palette rows) stopped when they stopped
94
94
  owning the box, and without this every bare field would fall back to the browser's grey. */
package/src/kit-plain.css CHANGED
@@ -373,3 +373,63 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
373
373
  dialog.ui-modal { padding: 0; max-width: calc(100% - 40px); max-height: calc(100dvh - 40px); color: var(--ui-text); }
374
374
  dialog.ui-modal[open] { display: flex; flex-direction: column; }
375
375
  dialog.ui-modal::backdrop { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px); }
376
+
377
+ /* ── 0.5.1: what the first surfaces on 0.5.0 still drew themselves (GemFactory, content-radar, 03/10/2026) ─────────── */
378
+
379
+ /* At phone width a short compact button ("Bỏ") was 44 tall but 41 wide: a target is an area. */
380
+ @media (pointer: coarse), (max-width: 47.9975em) { .ui-btn.ui-btn--sm { min-width: 44px; } }
381
+
382
+ /* A long label in a badge ("Đã dùng 1 lần + 4 bài cùng nguồn · 2 Hot News") wraps instead of running out of its card. */
383
+ .ui-badge--wrap { white-space: normal; overflow-wrap: anywhere; line-height: 16px; }
384
+
385
+ /* Page head with actions on the right, wrapping under the title when narrow:
386
+ `<div class="ui-page-head ui-page-head--actions"><div><h1>…</h1><p>…</p></div><div class="ui-page-head__actions">
387
+ <button class="ui-btn ui-btn--primary">…</button></div></div>`. */
388
+ .ui-page-head--actions { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 12px 16px; }
389
+ .ui-page-head--actions > :first-child { min-width: 0; }
390
+ .ui-page-head__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2); }
391
+
392
+ /* Chips that TOGGLE (a filter applied in place, no URL): `<div class="ui-chips"><button type="button" class="ui-tag
393
+ ui-tag--chip" aria-pressed="true">CRO</button>…`. Same box as the link chips (ui.css); pressed = the link's current. */
394
+ .ui-chips > button.ui-tag--chip {
395
+ min-height: 28px; padding: 4px 12px; border-radius: var(--ui-radius-md, 8px); cursor: pointer;
396
+ font-family: inherit; font-size: var(--gemboss-fs-caption, 12px); color: var(--ui-text);
397
+ }
398
+ .ui-chips > button.ui-tag--chip:hover { background: var(--ui-surface-2); border-color: var(--ui-text-muted); }
399
+ .ui-chips > button.ui-tag--chip:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 1px; }
400
+ .ui-chips > button.ui-tag--chip[aria-pressed="true"] { background: var(--ui-accent-soft); border-color: var(--ui-accent); color: var(--ui-accent-strong); font-weight: 600; }
401
+ @media (pointer: coarse), (max-width: 47.9975em) { .ui-chips > button.ui-tag--chip { min-height: 44px; } }
402
+
403
+ /* Chat at phone WIDTH (gemwatcher's assistant, measured 03/10/2026): the composer read 14px (iOS zooms the page on tap
404
+ below 16) and "new chat" was 35px wide under a fine pointer. shell.css raises the latter under `pointer: coarse`; a
405
+ desktop window at 390px reports fine. Here and not in chat.css / shell.css: the admin's first-load budget. */
406
+ @media (max-width: 47.9975em) {
407
+ /* Class doubled: chat.css is appended AFTER this file in gemboss-ui.css, so a same-weight rule there would win. */
408
+ .gemboss-ec-input.gemboss-ec-input textarea { font-size: 16px; }
409
+ .gemboss-assistant-collapse, .gemboss-assistant-newchat { min-height: 44px; min-width: 44px; }
410
+ }
411
+
412
+ /* Segmented buttons that say their state the accessible way: `aria-pressed="true"` looks like `.active` (ui-tabs reads
413
+ aria-selected already; gemcare had to set both on every group). */
414
+ .ui-seg button[aria-pressed="true"] { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
415
+
416
+ /* Read-only field: visibly not editable (the admin's field has no such state; gempress kept its own `.field[readonly]`).
417
+ The attribute doubled = (0,3,0), so it also holds over the field's own hover rule, which comes earlier. */
418
+ :is(.gemboss-field, .ui-textarea)[readonly][readonly] { background: var(--ui-surface-2); border-color: var(--ui-border); cursor: default; }
419
+
420
+ /* A popup whose body and foot sit inside one <form>: the form takes no box, so `.ui-modal > .ui-modal__body` (the
421
+ scrolling body) and the pinned foot still lay out as the popup's own children (gemcare's escalate form, 0.5.1). */
422
+ .ui-modal > form { display: contents; }
423
+ .ui-modal > form > .ui-modal__head, .ui-modal > form > .ui-modal__foot { flex: none; }
424
+ .ui-modal > form > .ui-modal__body { flex: 1 1 auto; min-height: 0; overflow: auto; }
425
+
426
+ /* A metric that FILTERS the view below it, as a toggle: `<button type="button" class="ui-metric" aria-pressed="true">`
427
+ (gemwatcher's Radar cards: as links to the same hash, Cmd-click opened an unfiltered tab, Space did nothing and the
428
+ chosen card was told by colour alone). A metric link that is the current view: `a.ui-metric[aria-current="page"]`.
429
+ Selected = iris edge, the shell's "this one" colour. */
430
+ button.ui-metric { font: inherit; text-align: left; color: inherit; cursor: pointer; }
431
+ button.ui-metric:hover { border-color: var(--ui-text-muted); }
432
+ button.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
433
+ /* Class doubled: the chosen card is usually the one under the pointer, and `button.ui-metric:hover` (0,2,1) would
434
+ repaint its edge grey the moment it is clicked (measured on gemwatcher Radar). */
435
+ .ui-metric.ui-metric:is([aria-pressed="true"], [aria-current="page"]) { border-color: var(--gemboss-accent); box-shadow: inset 0 0 0 1px var(--gemboss-accent); }
package/src/kit-plain.js CHANGED
@@ -102,7 +102,10 @@
102
102
  overlay.addEventListener("click", function (e) { if (e.target === overlay && downOnBackdrop) m.close(null); downOnBackdrop = false; });
103
103
  var x = panel.querySelector(".ui-modal__head .ui-modal__close");
104
104
  if (x) x.onclick = function () { m.close(null); };
105
- addEventListener("keydown", onKey);
105
+ // On the NEXT task, not now: a popup opened from a keydown handler (content-radar's composer asks "close without
106
+ // saving?" on Escape) would otherwise receive that same Escape when it bubbles up to window, and close itself at
107
+ // once (0.5.1). A listener added during a dispatch still runs if its target has not been reached yet.
108
+ setTimeout(function () { if (MODALS.indexOf(m) >= 0) addEventListener("keydown", onKey); }, 0);
106
109
  if (!MODALS.length) savedOverflow = document.body.style.overflow;
107
110
  MODALS.push(m);
108
111
  document.body.append(overlay);
@@ -153,22 +156,26 @@
153
156
 
154
157
  /* Menus (details.ui-menu, kit-plain.css) open with no script; closing is the page's job: an outside click, a click on
155
158
  an item, the window losing focus (a click inside an iframe never reaches this document), and Escape. Escape closes
156
- the MENU first and stops there, so a menu inside a popup does not close the popup with it. Idempotent. */
159
+ the MENU first and stops there, so a menu inside a popup does not close the popup with it. Idempotent.
160
+ The account menu (AccountMenu, `details.gb-account`) is a menu too since 0.5.1: GemFactory kept four lines of its
161
+ own for it. */
157
162
  var menusOn = false;
163
+ var MENU = "details.ui-menu, details.gb-account";
164
+ var MENU_ITEM = ".ui-menu__item, .gemboss-account-menu-item";
158
165
  function closeMenus(except) {
159
- document.querySelectorAll("details.ui-menu[open]").forEach(function (d) { if (d !== except) d.open = false; });
166
+ document.querySelectorAll("details.ui-menu[open], details.gb-account[open]").forEach(function (d) { if (d !== except) d.open = false; });
160
167
  }
161
168
  function initMenus() {
162
169
  if (menusOn) return;
163
170
  menusOn = true;
164
171
  document.addEventListener("click", function (e) {
165
- var d = e.target.closest && e.target.closest("details.ui-menu");
172
+ var d = e.target.closest && e.target.closest(MENU);
166
173
  closeMenus(d);
167
- if (d && e.target.closest(".ui-menu__item")) d.open = false;
174
+ if (d && e.target.closest(MENU_ITEM)) d.open = false;
168
175
  });
169
176
  addEventListener("blur", function () { closeMenus(); });
170
177
  addEventListener("keydown", function (e) {
171
- var d = e.key === "Escape" && !e.isComposing && document.querySelector("details.ui-menu[open]");
178
+ var d = e.key === "Escape" && !e.isComposing && document.querySelector("details.ui-menu[open], details.gb-account[open]");
172
179
  if (!d) return;
173
180
  e.preventDefault();
174
181
  e.stopImmediatePropagation();
package/src/launcher.tsx CHANGED
@@ -1,9 +1,12 @@
1
1
  "use client";
2
2
  /**
3
- * The button that opens a surface's AI assistant: bottom-right, floating (02/10/2026, Chris: "chắc vẫn phải để button ở góc
4
- * dưới bên phải còn góc trên bên phải là account … đồng bộ"). partner-radar had it in the topbar's top-right corner, which on
5
- * a phone became a black ✦ circle exactly where content-radar shows the account chip — two surfaces on one frame, the same
6
- * corner doing two different jobs. The top-right corner is the ACCOUNT (AccountMenu); the assistant lives here.
3
+ * The button that opens a surface's AI assistant. The top-right corner of the topbar is the ACCOUNT (AccountMenu, 02/10/2026:
4
+ * partner-radar's ✦ there read as content-radar's account chip on phones).
5
+ *
6
+ * Not floating (03/10/2026): 0.4.2 floated it bottom-right, and a fixed button covers whatever text scrolls under it
7
+ * (visual-gate `content_covered` red at 5 of 6 widths on partner-radar). Chris chose to drop the floating button. A surface
8
+ * renders it TWICE and CSS shows one: `placement="rail"` in AppShell's `railBottom` (wide screens), `placement="topbar"` in
9
+ * `actions` before the account chip (≤760px, where the rail is a drawer).
7
10
  *
8
11
  * Deliberately dumb: no provider, no state. A surface on `@gemboss/ui/assistant` passes `hidden` while its dock is open
9
12
  * (`presentation !== "hidden"`); a surface with its own panel does the same with its own flag.
@@ -11,19 +14,25 @@
11
14
  import React from "react";
12
15
  import { Icon } from "./icons";
13
16
 
14
- export function AssistantLauncher({ label, shortcut, hidden, onClick, className, ...rest }: {
17
+ // Whole class names, not a `--${placement}` template: an interpolated stem would exempt every class under it from the
18
+ // admin's orphan-rule gate (a-tap-target-rule-outlived-its-markup.test.ts).
19
+ const PLACEMENT = { rail: "gb-assistant-launcher--rail", topbar: "gb-assistant-launcher--topbar" } as const;
20
+
21
+ export function AssistantLauncher({ label, shortcut, placement = "rail", hidden, onClick, className, ...rest }: {
15
22
  /** Visible name, e.g. "Hỏi AI" / "Partner Assistant". Kept as the accessible name when the text hides on phones. */
16
23
  label: string;
17
24
  /** Keyboard hint shown on wide screens, e.g. "⌘J". Decorative: the surface owns the shortcut itself. */
18
25
  shortcut?: string;
19
- /** The assistant is already open — the launcher steps out of the way (it would sit under the dock). */
26
+ /** Where this copy sits: the rail's last row (wide screens) or the topbar (phones). Render one of each. */
27
+ placement?: "rail" | "topbar";
28
+ /** The assistant is already open: the launcher steps out of the way. */
20
29
  hidden?: boolean;
21
30
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
22
31
  className?: string;
23
32
  } & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "hidden">) {
24
33
  if (hidden) return null;
25
34
  return (
26
- <button type="button" className={className ? `gb-assistant-launcher ${className}` : "gb-assistant-launcher"}
35
+ <button type="button" className={`gb-assistant-launcher ${PLACEMENT[placement]}${className ? ` ${className}` : ""}`}
27
36
  aria-label={shortcut ? `${label} (${shortcut})` : label} onClick={onClick} {...rest}>
28
37
  <Icon name="sparkle" />
29
38
  <span className="gb-assistant-launcher-label">{label}</span>
package/src/shell.css CHANGED
@@ -403,6 +403,7 @@
403
403
  }
404
404
 
405
405
  .gnav-item:hover { background: var(--gemboss-nav-hover); }
406
+ .gnav-item:focus-visible { outline: 2px solid var(--gemboss-accent); outline-offset: -2px; } /* the one iris ring (0.5.1): had none, the browser drew its own */
406
407
 
407
408
  /* neutral gray on white (Polaris-like) */
408
409
  /* Active hub: a full coral pill when you're ON the hub's own page. But when a deeper
package/src/shell.tsx CHANGED
@@ -42,8 +42,10 @@ export function Topbar({ className, children, ...rest }: React.HTMLAttributes<HT
42
42
  export function MobileMenuButton({ open, onToggle, labels = { open: "Open menu", close: "Close menu" }, ...rest }:
43
43
  { open: boolean; onToggle: () => void; labels?: { open: string; close: string } } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
44
44
  return (
45
+ // data-label-*: a page rendered with renderToStaticMarkup has no React state; kit-plain.js `initShell()` reads
46
+ // both names from here (GemFactory patched them into the markup by hand before 0.5.1).
45
47
  <button type="button" className="gemboss-mobile-menu-btn" aria-label={open ? labels.close : labels.open}
46
- aria-expanded={open} onClick={onToggle} {...rest}>
48
+ aria-expanded={open} data-label-open={labels.open} data-label-close={labels.close} onClick={onToggle} {...rest}>
47
49
  <svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true">
48
50
  <path d="M3 5.5h14M3 10h14M3 14.5h14" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
49
51
  </svg>
package/src/ui.css CHANGED
@@ -213,7 +213,7 @@
213
213
  `.gemboss-admin-page-head` (01/10/2026, Chris: một chủ sở hữu; admin dùng bản này). Cố ý không đặt margin cho h1
214
214
  và không dùng flex: admin không reset margin tiêu đề, nên đổi ở đây là đổi khoảng cách của 14 trang admin. */
215
215
  .ui-page-head { margin-bottom: 20px; }
216
- .ui-page-head h1 { color: var(--ui-text-strong); font-size: 22px; font-weight: 650; letter-spacing: -0.01em; }
216
+ .ui-page-head h1 { color: var(--ui-text-strong); font-size: 22px; line-height: 28px; font-weight: 650; letter-spacing: -0.01em; } /* own line-height: the 20px body line overlapped a wrapped title (0.5.1) */
217
217
  .ui-page-head p { color: var(--ui-text-muted); font-size: var(--ui-fs-body); margin-top: 4px; }
218
218
 
219
219
  /* ── Thanh công cụ: `<div class="ui-toolbar">` — ô tìm giãn, điều khiển bám phải, một hàng gọn, xuống dòng khi hẹp.
@@ -269,8 +269,8 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
269
269
  .ui-badge {
270
270
  display: inline-flex; align-items: center; gap: 4px;
271
271
  font-size: var(--ui-fs-caption); line-height: 16px; font-weight: 550; white-space: nowrap;
272
- padding: 2px 8px; border-radius: var(--ui-radius-md); background: var(--ui-surface-2); color: var(--ui-text-muted);
273
- }
272
+ padding: 2px 8px; border-radius: var(--ui-radius-md); background: color-mix(in srgb, var(--ui-text) 8%, transparent); color: var(--ui-text-muted);
273
+ } /* a tint of the ink, not --ui-surface-2: that is the table row hover, and a badge vanished on hover (0.5.1) */
274
274
 
275
275
  /* ── Modal ────────────────────────────────────────────────────────────────────
276
276
  Rendered INLINE (not portaled): a portal escapes the .gemboss-admin-shell token
@@ -300,7 +300,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
300
300
  }
301
301
  .ui-modal__close svg { width: 20px; height: 20px; }
302
302
  .ui-modal__close:hover { background: var(--ui-surface-2); color: var(--ui-text); }
303
- .ui-modal__close:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: -2px; }
303
+ .ui-modal__close:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
304
304
 
305
305
  /* `size="lg"` — the screen-shaped dialog. Only the HOOK lives here: this sheet is imported by the
306
306
  root layout, so every rule in it is paid on every screen, and the geometry of a dialog with one
@@ -348,6 +348,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
348
348
  .ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
349
349
  .ui-seg :is(button, a) + :is(button, a) { border-left: 1px solid var(--ui-border); }
350
350
  .ui-seg :is(button, a):hover { background: var(--ui-surface-2); color: var(--ui-text); }
351
+ .ui-seg :is(button, a):focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
351
352
  /* Lọc / chế độ xem bằng LINK (trang server-render, chọn = đổi URL): `<a aria-current="page">` là ô đang chọn. */
352
353
  .ui-seg a { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; color: var(--ui-text-muted); }
353
354
  .ui-seg a[aria-current="page"],