@gemboss/ui 0.6.1 → 0.6.3

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
@@ -331,6 +331,7 @@ import { AppShell, BrandLockup, AccountMenu, AccountMenuItem, Icon, RailItem } f
331
331
  - `AccountMenu`: the admin's 32px initials chip and popover (same CSS). A `<details>`, so it opens
332
332
  without JavaScript on a page that never hydrates; with React it also closes on an outside click and
333
333
  on Escape. `AccountMenuItem` is a link (`href`), a POST form (`formAction`), or a button.
334
+ - `TopbarSearch` (0.6.3): see "Topbar search" below.
334
335
  - `Icon`: the GemBoss set, 39 icons on a 20px grid with 1.5px strokes, drawn to sit next to Polaris.
335
336
  `ICON_NAMES` lists them. Internal tools cannot use `@shopify/polaris-icons`: its licence only covers
336
337
  applications that integrate with Shopify. Draw a missing one HERE, not in the surface: every rail
@@ -342,6 +343,19 @@ import { AppShell, BrandLockup, AccountMenu, AccountMenuItem, Icon, RailItem } f
342
343
  (`href`) or a button; `disabled` on a link drops the href and sets `aria-disabled`; `className` is added
343
344
  to the row's class.
344
345
 
346
+ ## Topbar search (0.6.3)
347
+
348
+ The admin's "Search ⌘K" box for every surface, as a real form (works with no JavaScript):
349
+
350
+ ```tsx
351
+ <AppShell brand={…} center={<TopbarSearch action="/search" placeholder="Search by #ID, name or key" />} … />
352
+ ```
353
+
354
+ - Submits `GET action?q=…` (`name` changes the parameter). `defaultValue` keeps the current query on the results page.
355
+ - Same metrics as the admin's box: 36px under a mouse, 44px under a finger, ⌘K hint hidden on focus and below 460px.
356
+ - Shortcut on a page without React: `gembossUI.initSearch()` from `kit-plain.js` (⌘K / Ctrl+K and "/" focus it,
357
+ Escape leaves it). "/" is ignored while another field has focus.
358
+
345
359
  ## Report pages (0.4.0)
346
360
 
347
361
  Plain CSS in `kit-plain.css` for a numbers page written by hand (first user: gemnews `/admin`, 03/10/2026; that page moved to GemCommunity the same day, so gemnews no longer takes the package). The
@@ -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.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>
30
+ <main><header class="cat-head"><h1>@gemboss/ui 0.6.3</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>b1b688827c14</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,7 +583,7 @@ 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; 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;
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;form role=&quot;search&quot; method=&quot;get&quot; action=&quot;#&quot; class=&quot;gemboss-topbar-search&quot;&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;18&quot; height=&quot;18&quot; class=&quot;gemboss-topbar-search-icon&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;circle cx=&quot;8.8&quot; cy=&quot;8.8&quot; r=&quot;5.3&quot;&gt;&lt;/circle&gt;&lt;path d=&quot;M12.8 12.8 17 17&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/svg&gt;&lt;input class=&quot;gemboss-topbar-search-input&quot; type=&quot;search&quot; name=&quot;q&quot; placeholder=&quot;Search by #ID, name or key&quot; aria-label=&quot;Search by #ID, name or key&quot; autoComplete=&quot;off&quot; enterKeyHint=&quot;search&quot;/&gt;&lt;kbd class=&quot;gemboss-topbar-search-kbd&quot; aria-hidden=&quot;true&quot;&gt;⌘K&lt;/kbd&gt;&lt;/form&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
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;
@@ -607,6 +607,18 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
607
607
  &lt;/a&gt;
608
608
  &lt;/div&gt;
609
609
  &lt;div class=&quot;gemboss-topbar-center&quot;&gt;
610
+ &lt;form role=&quot;search&quot; method=&quot;get&quot; action=&quot;#&quot; class=&quot;gemboss-topbar-search&quot;&gt;
611
+ &lt;svg viewBox=&quot;0 0 20 20&quot; width=&quot;18&quot; height=&quot;18&quot; class=&quot;gemboss-topbar-search-icon&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;
612
+ &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;
613
+ &lt;circle cx=&quot;8.8&quot; cy=&quot;8.8&quot; r=&quot;5.3&quot;&gt;
614
+ &lt;/circle&gt;
615
+ &lt;path d=&quot;M12.8 12.8 17 17&quot;&gt;
616
+ &lt;/path&gt;
617
+ &lt;/g&gt;
618
+ &lt;/svg&gt;
619
+ &lt;input class=&quot;gemboss-topbar-search-input&quot; type=&quot;search&quot; name=&quot;q&quot; placeholder=&quot;Search by #ID, name or key&quot; aria-label=&quot;Search by #ID, name or key&quot; autoComplete=&quot;off&quot; enterKeyHint=&quot;search&quot;/&gt;
620
+ &lt;kbd class=&quot;gemboss-topbar-search-kbd&quot; aria-hidden=&quot;true&quot;&gt;⌘K&lt;/kbd&gt;
621
+ &lt;/form&gt;
610
622
  &lt;/div&gt;
611
623
  &lt;div class=&quot;gemboss-topbar-end&quot;&gt;
612
624
  &lt;details class=&quot;gb-account&quot;&gt;
package/gemboss-ui.css CHANGED
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  * GemBoss admin design system — GENERATED — do not edit; regenerate from gemboss:
3
3
  * node apps/admin/scripts/export-ui-css.mjs --out <path> (npm run export:ui-css)
4
- * gemboss commit: 30b3920a2a6c25393d1c02e086146b6f8590c1e0
5
- * sources sha256: 22617cdf59a7330f1285221022e5341d94e5b5e74cf917ce6f863c85733505e1
4
+ * gemboss commit: b1b688827c1444a994a2e477ead056c4816f5dd2
5
+ * sources sha256: 6914d1ac2ece84f2132dce7c9cb1dc69b24fbd81dce5c961749a2c12ae56a9c9
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
  * + 181 rules from packages/ui/src/shell.css (the shared shell)
@@ -432,7 +432,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
432
432
  /* ── Thông báo: `<div class="ui-notice ui-notice--warning">…</div>`. Lỗi tải, cảnh báo cấu hình, việc cần người
433
433
  làm. Mỗi tông mang màu chữ riêng (nền tông luôn sáng, kể cả trong admin tối). Không có tông = ghi chú trung tính.
434
434
  role="status"/"alert" CHỈ khi thông báo xuất hiện sau khi trang đã tải (vùng live); có sẵn lúc tải thì không role. */
435
- .ui-notice { padding: 12px 16px; border-radius: var(--ui-radius-md); border: 1px solid var(--ui-border); background: var(--ui-surface-2); color: var(--ui-text); font-size: var(--ui-fs-body); }
435
+ .ui-notice { padding: 12px 16px; border-radius: var(--ui-radius-md); border: 1px solid var(--ui-border); background: var(--ui-surface-2); color: var(--ui-text); font-size: var(--ui-fs-body); overflow-wrap: anywhere; } /* a URL or key in a notice wraps instead of being cut by its card (gemwatcher Pulse, 0.6.2) */
436
436
  .ui-notice--warning { background: var(--gemboss-warning-soft); border-color: var(--gemboss-warning-border); color: var(--gemboss-warning); }
437
437
  .ui-notice--danger { background: var(--gemboss-danger-soft); border-color: var(--gemboss-danger-border); color: var(--gemboss-danger-strong); }
438
438
  .ui-notice--success { background: var(--gemboss-success-soft); border-color: var(--gemboss-success-border); color: var(--gemboss-success-strong); }
@@ -2097,6 +2097,44 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
2097
2097
  /* ── AccountMenu (./account.tsx): its rules live in shell.css since 29/09/2026, because the admin
2098
2098
  * renders AccountMenu too now and loads shell.css, not this file. */
2099
2099
 
2100
+ /* ── Topbar search (TopbarSearch, search.tsx; 0.6.3) ────────────────────────────────────────────────────────
2101
+ The admin's "Search ⌘K" box as a real form for AppShell's `center` slot. Same metrics as the admin's
2102
+ .gemboss-command-search (gemboss-shell.css): 36px under a mouse, 44px under a finger (pinned in the admin's
2103
+ kit-controls-reach-44.test.ts), 18px icon, 13px text, the shortcut hint at the right. Field tokens, so it
2104
+ wears the same box as every other text field in the kit. */
2105
+ .gemboss-topbar-search {
2106
+ width: 100%;
2107
+ max-width: 560px;
2108
+ height: 36px;
2109
+ min-width: 0;
2110
+ margin: 0 auto;
2111
+ border: 1px solid var(--gemboss-field-line);
2112
+ border-radius: var(--gemboss-radius-md);
2113
+ background: var(--gemboss-field-bg);
2114
+ padding: 0 8px 0 12px;
2115
+ display: grid;
2116
+ grid-template-columns: 18px minmax(0, 1fr) auto;
2117
+ gap: 8px;
2118
+ align-items: center;
2119
+ color: var(--gemboss-text-muted);
2120
+ }
2121
+ .gemboss-topbar-search:hover { border-color: var(--gemboss-field-line-hover); background: var(--gemboss-field-bg-hover); }
2122
+ .gemboss-topbar-search:focus-within { border-color: var(--gemboss-field-line-active); background: var(--gemboss-surface); }
2123
+ .gemboss-topbar-search-icon { width: 18px; height: 18px; color: currentColor; }
2124
+ .gemboss-topbar-search-input {
2125
+ min-width: 0; height: 100%; border: 0; background: transparent; padding: 0; margin: 0;
2126
+ font: inherit; font-size: var(--gemboss-fs-body); line-height: 18px; color: var(--gemboss-field-ink); outline: none;
2127
+ }
2128
+ .gemboss-topbar-search-input::placeholder { color: var(--gemboss-field-placeholder); opacity: 1; }
2129
+ .gemboss-topbar-search-kbd {
2130
+ border: 1px solid var(--gemboss-field-line); border-radius: var(--gemboss-radius-sm); background: var(--gemboss-surface);
2131
+ padding: 1px 6px; color: var(--gemboss-text-muted); font-family: inherit; font-size: var(--gemboss-fs-micro); line-height: 16px;
2132
+ }
2133
+ .gemboss-topbar-search:focus-within .gemboss-topbar-search-kbd { display: none; }
2134
+ @media (pointer: coarse) { .gemboss-topbar-search { height: 44px; } }
2135
+ /* A phone has no ⌘ key, and the box needs every pixel the bar has left. */
2136
+ @media (max-width: 460px) { .gemboss-topbar-search-kbd { display: none; } }
2137
+
2100
2138
  /* ═══ @gemboss/ui src/auth.css ═══ */
2101
2139
  /* The GemBoss sign-in screen — @gemboss/ui (28/09/2026).
2102
2140
  *
@@ -3392,7 +3430,9 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3392
3430
  /* Delta: `<span class="ui-delta ui-delta--good"><span aria-hidden="true">↑</span> 12%</span>`. Tốt / xấu là của BỀ MẶT
3393
3431
  quyết (lượt đọc tăng là tốt, tỉ lệ thoát tăng là xấu), mũi tên là hướng. Không tông = trung tính (không đổi, hoặc
3394
3432
  "chưa có kỳ trước"). Luôn kèm chữ cho trình đọc màn hình nếu mũi tên là thứ duy nhất nói hướng. */
3395
- .ui-delta { display: inline-flex; align-items: baseline; gap: 2px; font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
3433
+ /* Wraps (0.6.2): a delta that carries its comparison ("▼ 50% vs previous 30 days (20m)") ran 73px out of a 145px metric at
3434
+ 390 on gemcare. Short ones ("↑ 18%") still sit on one line; long ones break between words, never inside a number. */
3435
+ .ui-delta { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 2px; font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-text-muted); font-variant-numeric: tabular-nums; }
3396
3436
  .ui-delta--good { color: var(--ui-success-fg); }
3397
3437
  .ui-delta--bad { color: var(--ui-danger-fg); }
3398
3438
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/ui",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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
@@ -95,3 +95,41 @@
95
95
 
96
96
  /* ── AccountMenu (./account.tsx): its rules live in shell.css since 29/09/2026, because the admin
97
97
  * renders AccountMenu too now and loads shell.css, not this file. */
98
+
99
+ /* ── Topbar search (TopbarSearch, search.tsx; 0.6.3) ────────────────────────────────────────────────────────
100
+ The admin's "Search ⌘K" box as a real form for AppShell's `center` slot. Same metrics as the admin's
101
+ .gemboss-command-search (gemboss-shell.css): 36px under a mouse, 44px under a finger (pinned in the admin's
102
+ kit-controls-reach-44.test.ts), 18px icon, 13px text, the shortcut hint at the right. Field tokens, so it
103
+ wears the same box as every other text field in the kit. */
104
+ .gemboss-topbar-search {
105
+ width: 100%;
106
+ max-width: 560px;
107
+ height: 36px;
108
+ min-width: 0;
109
+ margin: 0 auto;
110
+ border: 1px solid var(--gemboss-field-line);
111
+ border-radius: var(--gemboss-radius-md);
112
+ background: var(--gemboss-field-bg);
113
+ padding: 0 8px 0 12px;
114
+ display: grid;
115
+ grid-template-columns: 18px minmax(0, 1fr) auto;
116
+ gap: 8px;
117
+ align-items: center;
118
+ color: var(--gemboss-text-muted);
119
+ }
120
+ .gemboss-topbar-search:hover { border-color: var(--gemboss-field-line-hover); background: var(--gemboss-field-bg-hover); }
121
+ .gemboss-topbar-search:focus-within { border-color: var(--gemboss-field-line-active); background: var(--gemboss-surface); }
122
+ .gemboss-topbar-search-icon { width: 18px; height: 18px; color: currentColor; }
123
+ .gemboss-topbar-search-input {
124
+ min-width: 0; height: 100%; border: 0; background: transparent; padding: 0; margin: 0;
125
+ font: inherit; font-size: var(--gemboss-fs-body); line-height: 18px; color: var(--gemboss-field-ink); outline: none;
126
+ }
127
+ .gemboss-topbar-search-input::placeholder { color: var(--gemboss-field-placeholder); opacity: 1; }
128
+ .gemboss-topbar-search-kbd {
129
+ border: 1px solid var(--gemboss-field-line); border-radius: var(--gemboss-radius-sm); background: var(--gemboss-surface);
130
+ padding: 1px 6px; color: var(--gemboss-text-muted); font-family: inherit; font-size: var(--gemboss-fs-micro); line-height: 16px;
131
+ }
132
+ .gemboss-topbar-search:focus-within .gemboss-topbar-search-kbd { display: none; }
133
+ @media (pointer: coarse) { .gemboss-topbar-search { height: 44px; } }
134
+ /* A phone has no ⌘ key, and the box needs every pixel the bar has left. */
135
+ @media (max-width: 460px) { .gemboss-topbar-search-kbd { display: none; } }
@@ -131,6 +131,28 @@ export function clampDockHeight(height: number, viewport: number): number {
131
131
  return Math.min(Math.max(Math.round(height), DOCK_MIN_HEIGHT), max);
132
132
  }
133
133
 
134
+ /**
135
+ * How far below the topbar a popup that has just OPENED starts (03/10/2026).
136
+ *
137
+ * The 12px ceiling above is where a popup the seller DRAGS can reach. It is not where one should open.
138
+ * Chris, 03/10: "default height cao hơn sát vào topbar". Measured on his browser (window 620px tall):
139
+ * gemwatcher had a dragged 599 saved, every reopen clamped it to 552, and the popup opened 12px under
140
+ * the topbar; a flat 480 default also leaves only 84px on a 620 window and ~0 on a 560 one.
141
+ */
142
+ export const DOCK_OPEN_GAP = 120;
143
+
144
+ /** The tallest a popup OPENS at in this window: a quarter-ish band of page stays visible above it. */
145
+ export function dockOpenCeiling(viewport: number): number {
146
+ return Math.max(DOCK_MIN_HEIGHT, Math.round(viewport) - DOCK_TOPBAR_HEIGHT - DOCK_OPEN_GAP);
147
+ }
148
+
149
+ /** The height a popup opens at: what the seller asked for (or the default), never closer to the topbar
150
+ * than DOCK_OPEN_GAP. Dragging in-session can still go up to the 12px ceiling of `clampDockHeight`. */
151
+ export function openingDockHeight(requested: number | null, viewport: number): number {
152
+ const want = requested !== null && Number.isFinite(requested) && requested > 0 ? requested : DOCK_DEFAULT_HEIGHT;
153
+ return clampDockHeight(Math.min(want, dockOpenCeiling(viewport)), viewport);
154
+ }
155
+
134
156
  export const initialAssistantDockState: AssistantDockState = {
135
157
  presentation: "hidden",
136
158
  mounted: false,
@@ -18,6 +18,7 @@ import {
18
18
  initialAssistantDockState,
19
19
  DOCK_DEFAULT_HEIGHT,
20
20
  DOCK_DEFAULT_OFFSET_RIGHT,
21
+ openingDockHeight,
21
22
  SIDE_DEFAULT_WIDTH,
22
23
  type AssistantDockState,
23
24
  } from "./dock";
@@ -115,8 +116,10 @@ export function AssistantProvider({ options, children }: { options?: AssistantOp
115
116
  // Restore the seller's popup size + pin after mount (localStorage is unavailable during SSR).
116
117
  useEffect(() => {
117
118
  try {
118
- const raw = Number(window.localStorage.getItem(storageKey(opts.current.storagePrefix, "dockHeight")));
119
- const height = Number.isFinite(raw) && raw > 0 ? raw : DOCK_DEFAULT_HEIGHT;
119
+ const stored = window.localStorage.getItem(storageKey(opts.current.storagePrefix, "dockHeight"));
120
+ // Opens no closer to the topbar than DOCK_OPEN_GAP, even when a past drag saved the ceiling
121
+ // (dock.ts, 03/10/2026). The stored number is left as it was: a drag rewrites it.
122
+ const height = openingDockHeight(stored === null || stored === "" ? null : Number(stored), window.innerHeight);
120
123
  requestedHeight.current = height;
121
124
  dispatch({ type: "resize", height, viewport: window.innerHeight });
122
125
  // `Number(null)` is 0, not NaN — so reading a key that was never written used to slide the
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export {
5
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
+ export { TopbarSearch } from "./search";
8
9
  export { AccountMenu, AccountMenuItem, initialsOf } from "./account";
9
10
  export { Menu, MenuItem, MenuSeparator } from "./menu";
10
11
  export { Icon, ICON_NAMES, type IconName } from "./icons";
package/src/kit-plain.css CHANGED
@@ -127,7 +127,9 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
127
127
  /* Delta: `<span class="ui-delta ui-delta--good"><span aria-hidden="true">↑</span> 12%</span>`. Tốt / xấu là của BỀ MẶT
128
128
  quyết (lượt đọc tăng là tốt, tỉ lệ thoát tăng là xấu), mũi tên là hướng. Không tông = trung tính (không đổi, hoặc
129
129
  "chưa có kỳ trước"). Luôn kèm chữ cho trình đọc màn hình nếu mũi tên là thứ duy nhất nói hướng. */
130
- .ui-delta { display: inline-flex; align-items: baseline; gap: 2px; font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
130
+ /* Wraps (0.6.2): a delta that carries its comparison ("▼ 50% vs previous 30 days (20m)") ran 73px out of a 145px metric at
131
+ 390 on gemcare. Short ones ("↑ 18%") still sit on one line; long ones break between words, never inside a number. */
132
+ .ui-delta { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 2px; font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-text-muted); font-variant-numeric: tabular-nums; }
131
133
  .ui-delta--good { color: var(--ui-success-fg); }
132
134
  .ui-delta--bad { color: var(--ui-danger-fg); }
133
135
 
package/src/kit-plain.js CHANGED
@@ -15,6 +15,7 @@
15
15
  if (await confirm({ title: "Delete #12?", body: "It leaves the board.", confirmLabel: "Delete", tone: "critical" })) …
16
16
  initShell(); // a hand-written AppShell: ☰ drawer, backdrop, Escape, followed link
17
17
  initPasswordFields(); // .gb-auth-pw-toggle reveal; resetPasswordFields() on sign-out
18
+ initSearch(); // TopbarSearch: ⌘K / Ctrl+K and "/" focus the box, Escape leaves it
18
19
  */
19
20
  (function (g) {
20
21
  "use strict";
@@ -149,7 +150,13 @@
149
150
  if (fresh) vp.dataset.readyAt = String(Date.now() + 50);
150
151
  var wait = Math.max(0, Number(vp.dataset.readyAt || 0) - Date.now());
151
152
  var put = function () { vp.append(t); };
152
- if (wait) setTimeout(put, wait); else put();
153
+ // ONE timer drains the waiting toasts in the order they were sent. A setTimeout per toast did not: the second one
154
+ // computed a slightly shorter wait and could land first (CI, 03/10/2026: "down" before "<b>hi</b>"). Anything sent
155
+ // after the region is ready still goes straight in, behind the queue.
156
+ if (wait || vp._gbQueue) {
157
+ (vp._gbQueue = vp._gbQueue || []).push(put);
158
+ if (!vp._gbFlush) vp._gbFlush = setTimeout(function () { var q = vp._gbQueue; vp._gbQueue = null; vp._gbFlush = null; q.forEach(function (f) { f(); }); }, wait);
159
+ } else put();
153
160
  if (!sticky) setTimeout(function () { t.remove(); }, wait + (opts.duration || (opts.error ? 6000 : 3500)));
154
161
  return t;
155
162
  }
@@ -281,7 +288,23 @@
281
288
  document.querySelectorAll(".gb-auth-pw-toggle").forEach(function (b) { pwSet(b, false); });
282
289
  }
283
290
 
291
+ // TopbarSearch (search.tsx, 0.6.3): the admin's ⌘K, for a page without React. "/" only when no field has focus, so it
292
+ // never eats a slash typed into a form; Escape blurs the box and gives the page its keys back.
293
+ var searchBound = false;
294
+ function initSearch() {
295
+ if (searchBound) return;
296
+ searchBound = true;
297
+ document.addEventListener("keydown", function (e) {
298
+ var box = document.querySelector(".gemboss-topbar-search-input");
299
+ if (!box) return;
300
+ var t = e.target, typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
301
+ if ((e.key === "k" || e.key === "K") && (e.metaKey || e.ctrlKey) && !e.altKey) { e.preventDefault(); box.focus(); box.select(); }
302
+ else if (e.key === "/" && !typing && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); box.focus(); }
303
+ else if (e.key === "Escape" && t === box) box.blur();
304
+ });
305
+ }
306
+
284
307
  g.gembossUI = { openModal: openModal, toast: toast, confirm: confirm, initMenus: initMenus, closeMenus: closeMenus,
285
- initShell: initShell, initPasswordFields: initPasswordFields, resetPasswordFields: resetPasswordFields,
308
+ initSearch: initSearch, initShell: initShell, initPasswordFields: initPasswordFields, resetPasswordFields: resetPasswordFields,
286
309
  openModals: function () { return MODALS.length; } };
287
310
  })(globalThis);
package/src/search.tsx ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The topbar search: a REAL search box in the topbar's centre column, for every surface — @gemboss/ui (03/10/2026).
3
+ *
4
+ * The admin's topbar has had "Search ⌘K" since the shell was drawn, as admin-only CSS (`.gemboss-command-search`
5
+ * in apps/admin gemboss-shell.css) on a button that opens its command palette. Other surfaces had no search in the
6
+ * frame at all: GemFactory's board had a filter box inside the page and nothing in the topbar (Chris, 03/10/2026:
7
+ * "bổ sung ô search theo gemboss ui chuẩn … ở topbar"). This is the same box, as a form: it works with no
8
+ * JavaScript (GET `action?q=…`), on a server-rendered page that never hydrates.
9
+ *
10
+ * Same look as the admin's (36px box, 18px icon, 13px text, the ⌘K hint), and the same two-tier target rule:
11
+ * 36px under a mouse, 44px under a finger (`pointer: coarse`), the size its own test pins in the admin.
12
+ * `kit-plain.js` `initSearch()` gives it the shortcut (⌘K / Ctrl+K and "/") on a page without React.
13
+ */
14
+ import React from "react";
15
+ import { Icon } from "./icons";
16
+
17
+ type FormProps = React.FormHTMLAttributes<HTMLFormElement>;
18
+
19
+ export function TopbarSearch({ action, name = "q", placeholder = "Search", label, defaultValue, shortcut = "⌘K", className, ...rest }: {
20
+ /** Where the query goes: `GET action?{name}=…`. */
21
+ action: string;
22
+ name?: string;
23
+ placeholder?: string;
24
+ /** Accessible name of the box; defaults to the placeholder. */
25
+ label?: string;
26
+ defaultValue?: string;
27
+ /** The hint shown at the right edge; null hides it. */
28
+ shortcut?: string | null;
29
+ } & Omit<FormProps, "action">) {
30
+ return (
31
+ <form role="search" method="get" action={action} className={className ? `gemboss-topbar-search ${className}` : "gemboss-topbar-search"} {...rest}>
32
+ <Icon name="search" size={18} className="gemboss-topbar-search-icon" />
33
+ <input className="gemboss-topbar-search-input" type="search" name={name} placeholder={placeholder}
34
+ aria-label={label ?? placeholder} defaultValue={defaultValue} autoComplete="off" enterKeyHint="search" />
35
+ {shortcut ? <kbd className="gemboss-topbar-search-kbd" aria-hidden="true">{shortcut}</kbd> : null}
36
+ </form>
37
+ );
38
+ }
package/src/ui.css CHANGED
@@ -249,7 +249,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
249
249
  /* ── Thông báo: `<div class="ui-notice ui-notice--warning">…</div>`. Lỗi tải, cảnh báo cấu hình, việc cần người
250
250
  làm. Mỗi tông mang màu chữ riêng (nền tông luôn sáng, kể cả trong admin tối). Không có tông = ghi chú trung tính.
251
251
  role="status"/"alert" CHỈ khi thông báo xuất hiện sau khi trang đã tải (vùng live); có sẵn lúc tải thì không role. */
252
- .ui-notice { padding: 12px 16px; border-radius: var(--ui-radius-md); border: 1px solid var(--ui-border); background: var(--ui-surface-2); color: var(--ui-text); font-size: var(--ui-fs-body); }
252
+ .ui-notice { padding: 12px 16px; border-radius: var(--ui-radius-md); border: 1px solid var(--ui-border); background: var(--ui-surface-2); color: var(--ui-text); font-size: var(--ui-fs-body); overflow-wrap: anywhere; } /* a URL or key in a notice wraps instead of being cut by its card (gemwatcher Pulse, 0.6.2) */
253
253
  .ui-notice--warning { background: var(--gemboss-warning-soft); border-color: var(--gemboss-warning-border); color: var(--gemboss-warning); }
254
254
  .ui-notice--danger { background: var(--gemboss-danger-soft); border-color: var(--gemboss-danger-border); color: var(--gemboss-danger-strong); }
255
255
  .ui-notice--success { background: var(--gemboss-success-soft); border-color: var(--gemboss-success-border); color: var(--gemboss-success-strong); }