@gemboss/ui 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -11
- package/catalog/index.html +5 -5
- package/gemboss-ui.css +129 -31
- package/package.json +1 -1
- package/src/app-shell.css +12 -2
- package/src/assistant.css +44 -21
- package/src/auth.tsx +3 -0
- package/src/gemboss-field.css +1 -1
- package/src/kit-plain.css +60 -0
- package/src/kit-plain.js +13 -6
- package/src/launcher.tsx +16 -7
- package/src/shell.css +1 -0
- package/src/shell.tsx +3 -1
- package/src/ui.css +5 -4
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,33 @@ 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
|
|
262
|
-
bottom-right
|
|
263
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
- 0.6.0, not a patch: an existing `<AssistantLauncher>` call changes meaning (no longer fixed; the default copy is the
|
|
305
|
+
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
306
|
|
|
278
307
|
## The topbar kit: lockup, account, icons
|
|
279
308
|
|
package/catalog/index.html
CHANGED
|
@@ -27,7 +27,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
27
27
|
@media (max-width:760px){.cat{grid-template-columns:1fr;padding:16px}.cat-nav{position:static;flex-direction:row;flex-wrap:wrap}}
|
|
28
28
|
</style></head><body>
|
|
29
29
|
<div class="cat"><nav class="cat-nav" aria-label="Mục"><a href="#tokens">Token</a><a href="#buttons">Nút</a><a href="#fields">Ô nhập</a><a href="#choice">Tab, nhóm nút, thẻ</a><a href="#surfaces">Thẻ, số liệu, bảng, trạng thái trống</a><a href="#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.
|
|
30
|
+
<main><header class="cat-head"><h1>@gemboss/ui 0.6.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>47291357064c</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><button></code> lẫn <code><a></code> (căn giữa sẵn).</p><figure class="cat-demo"><figcaption>Mặc định · chính · ghost · nhỏ · tắt</figcaption><div class="cat-live"><div class="cat-row"><button type="button" class="ui-btn">Lưu nháp</button><button type="button" class="ui-btn ui-btn--primary">Xuất bản</button><button type="button" class="ui-btn ui-btn--ghost">Huỷ</button><button type="button" class="ui-btn ui-btn--sm">Nhỏ</button><button type="button" class="ui-btn ui-btn--primary" disabled>Đang lưu…</button><a class="ui-btn" href="#buttons">Link dạng nút</a></div></div><details class="cat-code"><summary>Markup</summary><pre><code><div class="cat-row">
|
|
33
33
|
<button type="button" class="ui-btn">Lưu nháp</button>
|
|
@@ -583,10 +583,10 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
583
583
|
<button type="button" class="ui-btn ui-btn--primary">Xoá</button>
|
|
584
584
|
</div>
|
|
585
585
|
</div></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="<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="../gemboss-ui.css"></head><body><div class="gemboss-platform-shell"><header class="gemboss-platform-topbar"><div class="gemboss-topbar-start"><button type="button" class="gemboss-mobile-menu-btn" aria-label="Mở menu" aria-expanded="false" aria-controls=":R0:"><svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M3 5.5h14M3 10h14M3 14.5h14" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"></path></svg></button><a class="gb-brand" href="#" aria-label="GemFactory home"><svg class="gb-brand-gem" viewBox="0 0 32 40" width="20.8" height="26" aria-hidden="true" focusable="false"><g transform="translate(0,0.8) scale(0.31)"><polygon points="26,14 74,14 66,50 34,50" fill="#5661F5" fill-opacity="0.92"></polygon><path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="currentColor" stroke-width="8.75" stroke-linejoin="round"></path><path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="currentColor" stroke-width="5.45" stroke-linejoin="round"></path></g></svg><span class="gb-brand-name">GemFactory</span></a></div><div class="gemboss-topbar-center"></div><div class="gemboss-topbar-end"><details class="gb-account"><summary class="gemboss-account-chip" aria-label="Account · Minh Anh" title="Minh Anh">MA</summary><div role="menu" class="gemboss-popover-surface gemboss-account-menu"><div class="gb-account-head"><div class="gb-account-name">Minh Anh</div><div class="gb-account-email">ma@gemboss.ai</div><div class="gb-account-role">Designer</div></div><a role="menuitem" class="gemboss-account-menu-item" href="#">Cài đặt</a><form method="post" action="#" class="gb-account-form"><button role="menuitem" type="submit" class="gemboss-account-menu-item danger">Đăng xuất</button></form></div></details></div></header><nav class="icon-nav" aria-label="Main navigation" id=":R0:"><div class="gnav-scroll"><div class="gnav-heading">Kho</div><div class="gnav-group active"><a class="gnav-item active" aria-current="page" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3.5" width="4" height="13" rx="1.2"></rect><rect x="8" y="3.5" width="4" height="9" rx="1.2"></rect><rect x="13" y="3.5" width="4" height="6" rx="1.2"></rect></g></svg></span><span class="gnav-label">Board</span></a></div><div class="gnav-group"><a class="gnav-item" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="8.25" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="13.5" width="13" height="3.5" rx="1"></rect></g></svg></span><span class="gnav-label">Ghép trang</span></a></div><div class="gnav-group"><a class="gnav-item" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3.2 17.3 16H2.7z"></path><path d="M10 8.2v3.6M10 13.9v.1"></path></g></svg></span><span class="gnav-label">Framework thiếu</span><span class="gnav-count" aria-label="3 mục đang mở">3</span></a></div></div><div class="gnav-bottom"><div class="gnav-sep"></div><a class="gnav-item gnav-assistant" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4.5h14v9H8.5L5 16.5v-3H3z"></path></g></svg></span><span class="gnav-label">Trợ lý</span></a></div></nav><button type="button" class="gemboss-mobile-nav-backdrop" aria-label="Đóng menu" tabindex="-1"></button><main class="gemboss-platform-main"><div style="padding:24px"><h1 style="margin:0">Nội dung màn</h1></div></main></div></body></html>"></iframe><details class="cat-code"><summary>Markup</summary><pre><code><div class="gemboss-platform-shell">
|
|
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="<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="../gemboss-ui.css"></head><body><div class="gemboss-platform-shell"><header class="gemboss-platform-topbar"><div class="gemboss-topbar-start"><button type="button" class="gemboss-mobile-menu-btn" aria-label="Mở menu" aria-expanded="false" data-label-open="Mở menu" data-label-close="Đóng menu" aria-controls=":R0:"><svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M3 5.5h14M3 10h14M3 14.5h14" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"></path></svg></button><a class="gb-brand" href="#" aria-label="GemFactory home"><svg class="gb-brand-gem" viewBox="0 0 32 40" width="20.8" height="26" aria-hidden="true" focusable="false"><g transform="translate(0,0.8) scale(0.31)"><polygon points="26,14 74,14 66,50 34,50" fill="#5661F5" fill-opacity="0.92"></polygon><path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="currentColor" stroke-width="8.75" stroke-linejoin="round"></path><path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="currentColor" stroke-width="5.45" stroke-linejoin="round"></path></g></svg><span class="gb-brand-name">GemFactory</span></a></div><div class="gemboss-topbar-center"></div><div class="gemboss-topbar-end"><details class="gb-account"><summary class="gemboss-account-chip" aria-label="Account · Minh Anh" title="Minh Anh">MA</summary><div role="menu" class="gemboss-popover-surface gemboss-account-menu"><div class="gb-account-head"><div class="gb-account-name">Minh Anh</div><div class="gb-account-email">ma@gemboss.ai</div><div class="gb-account-role">Designer</div></div><a role="menuitem" class="gemboss-account-menu-item" href="#">Cài đặt</a><form method="post" action="#" class="gb-account-form"><button role="menuitem" type="submit" class="gemboss-account-menu-item danger">Đăng xuất</button></form></div></details></div></header><nav class="icon-nav" aria-label="Main navigation" id=":R0:"><div class="gnav-scroll"><div class="gnav-heading">Kho</div><div class="gnav-group active"><a class="gnav-item active" aria-current="page" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3.5" width="4" height="13" rx="1.2"></rect><rect x="8" y="3.5" width="4" height="9" rx="1.2"></rect><rect x="13" y="3.5" width="4" height="6" rx="1.2"></rect></g></svg></span><span class="gnav-label">Board</span></a></div><div class="gnav-group"><a class="gnav-item" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="8.25" width="13" height="3.5" rx="1"></rect><rect x="3.5" y="13.5" width="13" height="3.5" rx="1"></rect></g></svg></span><span class="gnav-label">Ghép trang</span></a></div><div class="gnav-group"><a class="gnav-item" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3.2 17.3 16H2.7z"></path><path d="M10 8.2v3.6M10 13.9v.1"></path></g></svg></span><span class="gnav-label">Framework thiếu</span><span class="gnav-count" aria-label="3 mục đang mở">3</span></a></div></div><div class="gnav-bottom"><div class="gnav-sep"></div><a class="gnav-item gnav-assistant" href="#"><span class="gnav-ico"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4.5h14v9H8.5L5 16.5v-3H3z"></path></g></svg></span><span class="gnav-label">Trợ lý</span></a></div></nav><button type="button" class="gemboss-mobile-nav-backdrop" aria-label="Đóng menu" tabindex="-1"></button><main class="gemboss-platform-main"><div style="padding:24px"><h1 style="margin:0">Nội dung màn</h1></div></main></div></body></html>"></iframe><details class="cat-code"><summary>Markup</summary><pre><code><div class="gemboss-platform-shell">
|
|
587
587
|
<header class="gemboss-platform-topbar">
|
|
588
588
|
<div class="gemboss-topbar-start">
|
|
589
|
-
<button type="button" class="gemboss-mobile-menu-btn" aria-label="Mở menu" aria-expanded="false" aria-controls=":R0:">
|
|
589
|
+
<button type="button" class="gemboss-mobile-menu-btn" aria-label="Mở menu" aria-expanded="false" data-label-open="Mở menu" data-label-close="Đóng menu" aria-controls=":R0:">
|
|
590
590
|
<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true">
|
|
591
591
|
<path d="M3 5.5h14M3 10h14M3 14.5h14" stroke="currentColor" stroke-width="1.7" stroke-linecap="round">
|
|
592
592
|
</path>
|
|
@@ -703,7 +703,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
703
703
|
</div>
|
|
704
704
|
</main>
|
|
705
705
|
</div></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="<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="../gemboss-ui.css"></head><body><main class="gb-auth"><div class="gb-auth-split"><div class="gb-auth-stage"><div class="gb-auth-card"><header class="gb-auth-header"><span class="gb-auth-mark" aria-hidden="true"><svg viewBox="0 0 32 32" width="56" height="56" aria-hidden="true" focusable="false"><rect width="32" height="32" rx="7" style="fill:var(--gb-mark-bg)"></rect><g transform="translate(16,16) scale(0.24) translate(-50,-62)"><polygon points="26,14 74,14 66,50 34,50" fill="#8a93ff"></polygon><path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="#f4f4f2" stroke-width="5" stroke-linejoin="round"></path><path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="#f4f4f2" stroke-opacity="0.72" stroke-width="3" stroke-linejoin="round"></path></g></svg></span><h1>GemFactory</h1><p>Kho section của GemBoss</p></header><form class="gb-auth-form" method="post" action="#"><div class="gb-auth-field"><label for="pw">Mật khẩu</label><div class="gb-auth-pw"><input class="gb-auth-input" id="pw" type="password"/><button type="button" aria-controls="pw" aria-label="Hiện" title="Hiện" class="gb-auth-pw-toggle"><svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="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"></path><circle cx="10" cy="10" r="2.6"></circle></svg></button></div></div><button type="submit" class="gb-auth-submit">Đăng nhập</button></form></div></div><aside class="gb-auth-panel"><div class="gb-auth-panel-in"><p class="gb-auth-eyebrow">GemBoss</p><h2 class="gb-auth-panel-h">Một ngôn ngữ thiết kế cho mọi công cụ</h2><p class="gb-auth-panel-p">Đăng nhập bằng mật khẩu đội đã gửi riêng.</p></div></aside></div></main></body></html>"></iframe><details class="cat-code"><summary>Markup</summary><pre><code><main class="gb-auth">
|
|
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="<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="../gemboss-ui.css"></head><body><main class="gb-auth"><div class="gb-auth-split"><div class="gb-auth-stage"><div class="gb-auth-card"><header class="gb-auth-header"><span class="gb-auth-mark" aria-hidden="true"><svg viewBox="0 0 32 32" width="56" height="56" aria-hidden="true" focusable="false"><rect width="32" height="32" rx="7" style="fill:var(--gb-mark-bg)"></rect><g transform="translate(16,16) scale(0.24) translate(-50,-62)"><polygon points="26,14 74,14 66,50 34,50" fill="#8a93ff"></polygon><path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="#f4f4f2" stroke-width="5" stroke-linejoin="round"></path><path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="#f4f4f2" stroke-opacity="0.72" stroke-width="3" stroke-linejoin="round"></path></g></svg></span><h1>GemFactory</h1><p>Kho section của GemBoss</p></header><form class="gb-auth-form" method="post" action="#"><div class="gb-auth-field"><label for="pw">Mật khẩu</label><div class="gb-auth-pw"><input class="gb-auth-input" id="pw" type="password"/><button type="button" aria-controls="pw" aria-label="Hiện" title="Hiện" data-label-show="Hiện" data-label-hide="Ẩn" class="gb-auth-pw-toggle"><svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="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"></path><circle cx="10" cy="10" r="2.6"></circle></svg></button></div></div><button type="submit" class="gb-auth-submit">Đăng nhập</button></form></div></div><aside class="gb-auth-panel"><div class="gb-auth-panel-in"><p class="gb-auth-eyebrow">GemBoss</p><h2 class="gb-auth-panel-h">Một ngôn ngữ thiết kế cho mọi công cụ</h2><p class="gb-auth-panel-p">Đăng nhập bằng mật khẩu đội đã gửi riêng.</p></div></aside></div></main></body></html>"></iframe><details class="cat-code"><summary>Markup</summary><pre><code><main class="gb-auth">
|
|
707
707
|
<div class="gb-auth-split">
|
|
708
708
|
<div class="gb-auth-stage">
|
|
709
709
|
<div class="gb-auth-card">
|
|
@@ -730,7 +730,7 @@ body{margin:0;background:var(--gemboss-app-bg,#f1f1f1);color:var(--gemboss-text)
|
|
|
730
730
|
<label for="pw">Mật khẩu</label>
|
|
731
731
|
<div class="gb-auth-pw">
|
|
732
732
|
<input class="gb-auth-input" id="pw" type="password"/>
|
|
733
|
-
<button type="button" aria-controls="pw" aria-label="Hiện" title="Hiện" class="gb-auth-pw-toggle">
|
|
733
|
+
<button type="button" aria-controls="pw" aria-label="Hiện" title="Hiện" data-label-show="Hiện" data-label-hide="Ẩn" class="gb-auth-pw-toggle">
|
|
734
734
|
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
735
735
|
<path d="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">
|
|
736
736
|
</path>
|
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:
|
|
5
|
-
* sources sha256:
|
|
4
|
+
* gemboss commit: 47291357064c6877d0ebe7d118b6a1b52b1d9d22
|
|
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
|
-
* +
|
|
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-
|
|
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-
|
|
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,7 +2024,9 @@ 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
|
-
|
|
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; }
|
|
@@ -2029,6 +2036,10 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
2029
2036
|
.gemboss-topbar-center { flex: 1 1 auto; justify-content: flex-start; }
|
|
2030
2037
|
/* No actions: the empty slot would still take one flex `gap` (8px) off the search at 320px. */
|
|
2031
2038
|
.gemboss-topbar-end:empty { display: none; }
|
|
2039
|
+
/* Only the search gives. With `min-width: 0` on all three slots, flex shrank them all: the actions slot gave way and its
|
|
2040
|
+
* buttons (justify flex-end) spilled LEFT over the search (two 44px targets, assistant + account, overlapped
|
|
2041
|
+
* partner-radar's search by 15px at 320 and 360), and the start slot squeezed the gem mark to 16px. */
|
|
2042
|
+
.gemboss-topbar-start, .gemboss-topbar-end { flex: 0 0 auto; }
|
|
2032
2043
|
}
|
|
2033
2044
|
|
|
2034
2045
|
.gnav-heading {
|
|
@@ -2069,7 +2080,11 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
2069
2080
|
overflow: hidden; text-overflow: ellipsis; min-width: 0;
|
|
2070
2081
|
}
|
|
2071
2082
|
/* The admin shows only its mark below 460px; so does this. */
|
|
2072
|
-
@media (max-width: 460px) {
|
|
2083
|
+
@media (max-width: 460px) {
|
|
2084
|
+
.gb-brand-name { display: none; }
|
|
2085
|
+
/* The mark alone is ~21px wide: a 44px target, centred (visual-gate tap_target_link on content-radar at 320, 0.5.1). */
|
|
2086
|
+
.gb-brand { min-width: 44px; height: 44px; justify-content: center; }
|
|
2087
|
+
}
|
|
2073
2088
|
|
|
2074
2089
|
/* ── AccountMenu (./account.tsx): its rules live in shell.css since 29/09/2026, because the admin
|
|
2075
2090
|
* renders AccountMenu too now and loads shell.css, not this file. */
|
|
@@ -3180,40 +3195,63 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
3180
3195
|
.gb-dock-side .gb-dock-sidegrip, .gb-dock-pinside { display: none; }
|
|
3181
3196
|
}
|
|
3182
3197
|
|
|
3183
|
-
/* ── AssistantLauncher
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3198
|
+
/* ── AssistantLauncher: the button that opens the assistant. NOT floating (03/10/2026) ────────────────────────────────
|
|
3199
|
+
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
|
|
3200
|
+
on every page measured (/, /apps, /sources): a fixed button always sits on whatever text scrolls under it, and at
|
|
3201
|
+
scroll 0 that is a table column. GemCommunity had dropped its floating buttons for the same reason (#239). Chris chose
|
|
3202
|
+
"bỏ nút nổi". It now lives where nothing scrolls under it:
|
|
3203
|
+
· `--rail` wide screens: the last row of the rail (`railBottom`), a full-width primary button. Hidden ≤760px,
|
|
3204
|
+
where the rail is an off-canvas drawer nobody sees until they open it.
|
|
3205
|
+
· `--topbar` phones (≤760px): a 44px icon button in the topbar's `actions`, left of the account chip. Hidden above.
|
|
3206
|
+
The top-right corner stays the ACCOUNT. A surface renders both; exactly one is displayed at any width. */
|
|
3187
3207
|
.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
3208
|
display: inline-flex;
|
|
3193
3209
|
align-items: center;
|
|
3194
3210
|
gap: 8px;
|
|
3195
|
-
min-height: 44px;
|
|
3196
|
-
padding: 0 16px;
|
|
3197
3211
|
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
3212
|
cursor: pointer;
|
|
3213
|
+
font-family: var(--gemboss-font-sans, system-ui, sans-serif);
|
|
3204
3214
|
}
|
|
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
3215
|
.gb-assistant-launcher svg { flex: 0 0 auto; }
|
|
3216
|
+
.gb-assistant-launcher:focus-visible { outline: 2px solid var(--gemboss-accent, #5661f5); outline-offset: 2px; }
|
|
3217
|
+
.gb-assistant-launcher--rail {
|
|
3218
|
+
width: 100%;
|
|
3219
|
+
min-height: 44px; /* a control, not a list link: §9.1 asks 44 (visual-gate red at 768 with 36) */
|
|
3220
|
+
margin: 8px 0 4px;
|
|
3221
|
+
padding: 0 12px;
|
|
3222
|
+
border-radius: var(--gemboss-radius-md, 8px);
|
|
3223
|
+
background: var(--gemboss-primary, #1b1c1e);
|
|
3224
|
+
color: var(--gemboss-on-primary, #fff);
|
|
3225
|
+
font-size: 13px;
|
|
3226
|
+
font-weight: 600;
|
|
3227
|
+
line-height: 1;
|
|
3228
|
+
text-align: left;
|
|
3229
|
+
}
|
|
3230
|
+
.gb-assistant-launcher--rail:hover { background: var(--gemboss-primary-dark, #000); }
|
|
3231
|
+
.gb-assistant-launcher--rail .gb-assistant-launcher-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3208
3232
|
.gb-assistant-launcher-kbd {
|
|
3209
3233
|
font: inherit; font-size: 11px; opacity: 0.7;
|
|
3210
3234
|
border: 1px solid currentColor; border-radius: 5px; padding: 1px 5px;
|
|
3211
3235
|
}
|
|
3236
|
+
.gb-assistant-launcher--topbar {
|
|
3237
|
+
display: none;
|
|
3238
|
+
position: relative; /* holds the clipped label: an absolute child of a static button escapes to the nearest positioned box */
|
|
3239
|
+
justify-content: center;
|
|
3240
|
+
flex: 0 0 auto;
|
|
3241
|
+
width: 44px;
|
|
3242
|
+
height: 44px;
|
|
3243
|
+
padding: 0;
|
|
3244
|
+
border-radius: var(--gemboss-radius-md, 8px);
|
|
3245
|
+
background: transparent;
|
|
3246
|
+
color: var(--gemboss-text, #1b1c1e);
|
|
3247
|
+
}
|
|
3248
|
+
.gb-assistant-launcher--topbar:hover, .gb-assistant-launcher--topbar:active { background: var(--gemboss-hover, #eceef2); }
|
|
3249
|
+
.gb-assistant-launcher--topbar .gb-assistant-launcher-label, .gb-assistant-launcher--topbar .gb-assistant-launcher-kbd {
|
|
3250
|
+
position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
|
|
3251
|
+
}
|
|
3212
3252
|
@media (max-width: 760px) {
|
|
3213
|
-
.gb-assistant-launcher {
|
|
3214
|
-
.gb-assistant-launcher
|
|
3215
|
-
position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
|
|
3216
|
-
}
|
|
3253
|
+
.gb-assistant-launcher--rail { display: none; }
|
|
3254
|
+
.gb-assistant-launcher--topbar { display: inline-flex; }
|
|
3217
3255
|
}
|
|
3218
3256
|
|
|
3219
3257
|
/* ═══ @gemboss/ui src/kit-plain.css ═══ */
|
|
@@ -3593,6 +3631,66 @@ dialog.ui-modal { padding: 0; max-width: calc(100% - 40px); max-height: calc(100
|
|
|
3593
3631
|
dialog.ui-modal[open] { display: flex; flex-direction: column; }
|
|
3594
3632
|
dialog.ui-modal::backdrop { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px); }
|
|
3595
3633
|
|
|
3634
|
+
/* ── 0.5.1: what the first surfaces on 0.5.0 still drew themselves (GemFactory, content-radar, 03/10/2026) ─────────── */
|
|
3635
|
+
|
|
3636
|
+
/* At phone width a short compact button ("Bỏ") was 44 tall but 41 wide: a target is an area. */
|
|
3637
|
+
@media (pointer: coarse), (max-width: 47.9975em) { .ui-btn.ui-btn--sm { min-width: 44px; } }
|
|
3638
|
+
|
|
3639
|
+
/* 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. */
|
|
3640
|
+
.ui-badge--wrap { white-space: normal; overflow-wrap: anywhere; line-height: 16px; }
|
|
3641
|
+
|
|
3642
|
+
/* Page head with actions on the right, wrapping under the title when narrow:
|
|
3643
|
+
`<div class="ui-page-head ui-page-head--actions"><div><h1>…</h1><p>…</p></div><div class="ui-page-head__actions">
|
|
3644
|
+
<button class="ui-btn ui-btn--primary">…</button></div></div>`. */
|
|
3645
|
+
.ui-page-head--actions { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 12px 16px; }
|
|
3646
|
+
.ui-page-head--actions > :first-child { min-width: 0; }
|
|
3647
|
+
.ui-page-head__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2); }
|
|
3648
|
+
|
|
3649
|
+
/* Chips that TOGGLE (a filter applied in place, no URL): `<div class="ui-chips"><button type="button" class="ui-tag
|
|
3650
|
+
ui-tag--chip" aria-pressed="true">CRO</button>…`. Same box as the link chips (ui.css); pressed = the link's current. */
|
|
3651
|
+
.ui-chips > button.ui-tag--chip {
|
|
3652
|
+
min-height: 28px; padding: 4px 12px; border-radius: var(--ui-radius-md, 8px); cursor: pointer;
|
|
3653
|
+
font-family: inherit; font-size: var(--gemboss-fs-caption, 12px); color: var(--ui-text);
|
|
3654
|
+
}
|
|
3655
|
+
.ui-chips > button.ui-tag--chip:hover { background: var(--ui-surface-2); border-color: var(--ui-text-muted); }
|
|
3656
|
+
.ui-chips > button.ui-tag--chip:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 1px; }
|
|
3657
|
+
.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; }
|
|
3658
|
+
@media (pointer: coarse), (max-width: 47.9975em) { .ui-chips > button.ui-tag--chip { min-height: 44px; } }
|
|
3659
|
+
|
|
3660
|
+
/* Chat at phone WIDTH (gemwatcher's assistant, measured 03/10/2026): the composer read 14px (iOS zooms the page on tap
|
|
3661
|
+
below 16) and "new chat" was 35px wide under a fine pointer. shell.css raises the latter under `pointer: coarse`; a
|
|
3662
|
+
desktop window at 390px reports fine. Here and not in chat.css / shell.css: the admin's first-load budget. */
|
|
3663
|
+
@media (max-width: 47.9975em) {
|
|
3664
|
+
/* Class doubled: chat.css is appended AFTER this file in gemboss-ui.css, so a same-weight rule there would win. */
|
|
3665
|
+
.gemboss-ec-input.gemboss-ec-input textarea { font-size: 16px; }
|
|
3666
|
+
.gemboss-assistant-collapse, .gemboss-assistant-newchat { min-height: 44px; min-width: 44px; }
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
/* Segmented buttons that say their state the accessible way: `aria-pressed="true"` looks like `.active` (ui-tabs reads
|
|
3670
|
+
aria-selected already; gemcare had to set both on every group). */
|
|
3671
|
+
.ui-seg button[aria-pressed="true"] { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
|
|
3672
|
+
|
|
3673
|
+
/* Read-only field: visibly not editable (the admin's field has no such state; gempress kept its own `.field[readonly]`).
|
|
3674
|
+
The attribute doubled = (0,3,0), so it also holds over the field's own hover rule, which comes earlier. */
|
|
3675
|
+
:is(.gemboss-field, .ui-textarea)[readonly][readonly] { background: var(--ui-surface-2); border-color: var(--ui-border); cursor: default; }
|
|
3676
|
+
|
|
3677
|
+
/* A popup whose body and foot sit inside one <form>: the form takes no box, so `.ui-modal > .ui-modal__body` (the
|
|
3678
|
+
scrolling body) and the pinned foot still lay out as the popup's own children (gemcare's escalate form, 0.5.1). */
|
|
3679
|
+
.ui-modal > form { display: contents; }
|
|
3680
|
+
.ui-modal > form > .ui-modal__head, .ui-modal > form > .ui-modal__foot { flex: none; }
|
|
3681
|
+
.ui-modal > form > .ui-modal__body { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
3682
|
+
|
|
3683
|
+
/* A metric that FILTERS the view below it, as a toggle: `<button type="button" class="ui-metric" aria-pressed="true">`
|
|
3684
|
+
(gemwatcher's Radar cards: as links to the same hash, Cmd-click opened an unfiltered tab, Space did nothing and the
|
|
3685
|
+
chosen card was told by colour alone). A metric link that is the current view: `a.ui-metric[aria-current="page"]`.
|
|
3686
|
+
Selected = iris edge, the shell's "this one" colour. */
|
|
3687
|
+
button.ui-metric { font: inherit; text-align: left; color: inherit; cursor: pointer; }
|
|
3688
|
+
button.ui-metric:hover { border-color: var(--ui-text-muted); }
|
|
3689
|
+
button.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
|
|
3690
|
+
/* Class doubled: the chosen card is usually the one under the pointer, and `button.ui-metric:hover` (0,2,1) would
|
|
3691
|
+
repaint its edge grey the moment it is clicked (measured on gemwatcher Radar). */
|
|
3692
|
+
.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); }
|
|
3693
|
+
|
|
3596
3694
|
/* ═══ @gemboss/ui src/chat.css ═══ */
|
|
3597
3695
|
/*
|
|
3598
3696
|
* 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.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "GemBoss design system for every surface in the fleet: the admin shell (topbar + rail + drawer) as React components, and the admin stylesheet exported as one CSS file.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/src/app-shell.css
CHANGED
|
@@ -23,7 +23,9 @@
|
|
|
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
|
-
|
|
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; }
|
|
@@ -33,6 +35,10 @@
|
|
|
33
35
|
.gemboss-topbar-center { flex: 1 1 auto; justify-content: flex-start; }
|
|
34
36
|
/* No actions: the empty slot would still take one flex `gap` (8px) off the search at 320px. */
|
|
35
37
|
.gemboss-topbar-end:empty { display: none; }
|
|
38
|
+
/* Only the search gives. With `min-width: 0` on all three slots, flex shrank them all: the actions slot gave way and its
|
|
39
|
+
* buttons (justify flex-end) spilled LEFT over the search (two 44px targets, assistant + account, overlapped
|
|
40
|
+
* partner-radar's search by 15px at 320 and 360), and the start slot squeezed the gem mark to 16px. */
|
|
41
|
+
.gemboss-topbar-start, .gemboss-topbar-end { flex: 0 0 auto; }
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
.gnav-heading {
|
|
@@ -73,7 +79,11 @@
|
|
|
73
79
|
overflow: hidden; text-overflow: ellipsis; min-width: 0;
|
|
74
80
|
}
|
|
75
81
|
/* The admin shows only its mark below 460px; so does this. */
|
|
76
|
-
@media (max-width: 460px) {
|
|
82
|
+
@media (max-width: 460px) {
|
|
83
|
+
.gb-brand-name { display: none; }
|
|
84
|
+
/* The mark alone is ~21px wide: a 44px target, centred (visual-gate tap_target_link on content-radar at 320, 0.5.1). */
|
|
85
|
+
.gb-brand { min-width: 44px; height: 44px; justify-content: center; }
|
|
86
|
+
}
|
|
77
87
|
|
|
78
88
|
/* ── AccountMenu (./account.tsx): its rules live in shell.css since 29/09/2026, because the admin
|
|
79
89
|
* 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
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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 {
|
|
529
|
-
.gb-assistant-launcher
|
|
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} />
|
package/src/gemboss-field.css
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
172
|
+
var d = e.target.closest && e.target.closest(MENU);
|
|
166
173
|
closeMenus(d);
|
|
167
|
-
if (d && e.target.closest(
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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={
|
|
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-
|
|
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-
|
|
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"],
|