@gemboss/ui 0.4.2 → 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 +102 -17
- package/catalog/index.html +74 -9
- package/gemboss-ui.css +335 -60
- package/package.json +6 -3
- package/src/app-shell.css +22 -1
- package/src/assistant.css +44 -21
- package/src/auth.tsx +3 -0
- package/src/chat-composer.css +1 -1
- package/src/chat.css +1 -1
- package/src/gemboss-field.css +3 -3
- package/src/gemboss-tokens.css +1 -1
- package/src/icon.svg +1 -0
- package/src/kit-plain.css +223 -4
- package/src/kit-plain.js +115 -7
- package/src/launcher.tsx +16 -7
- package/src/shell.css +1 -0
- package/src/shell.tsx +3 -1
- package/src/ui.css +33 -25
package/src/kit-plain.js
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
const m = openModal({ title: "Delete #12?", body: '<div class="ui-modal__body">…</div>', foot: "…" });
|
|
13
13
|
m.close(value); // onClose(value) runs once; Escape / backdrop / × close with null
|
|
14
14
|
toast("Saved"); toast("Could not save", { error: true });
|
|
15
|
+
if (await confirm({ title: "Delete #12?", body: "It leaves the board.", confirmLabel: "Delete", tone: "critical" })) …
|
|
16
|
+
initShell(); // a hand-written AppShell: ☰ drawer, backdrop, Escape, followed link
|
|
17
|
+
initPasswordFields(); // .gb-auth-pw-toggle reveal; resetPasswordFields() on sign-out
|
|
15
18
|
*/
|
|
16
19
|
(function (g) {
|
|
17
20
|
"use strict";
|
|
@@ -99,7 +102,10 @@
|
|
|
99
102
|
overlay.addEventListener("click", function (e) { if (e.target === overlay && downOnBackdrop) m.close(null); downOnBackdrop = false; });
|
|
100
103
|
var x = panel.querySelector(".ui-modal__head .ui-modal__close");
|
|
101
104
|
if (x) x.onclick = function () { m.close(null); };
|
|
102
|
-
|
|
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);
|
|
103
109
|
if (!MODALS.length) savedOverflow = document.body.style.overflow;
|
|
104
110
|
MODALS.push(m);
|
|
105
111
|
document.body.append(overlay);
|
|
@@ -150,22 +156,26 @@
|
|
|
150
156
|
|
|
151
157
|
/* Menus (details.ui-menu, kit-plain.css) open with no script; closing is the page's job: an outside click, a click on
|
|
152
158
|
an item, the window losing focus (a click inside an iframe never reaches this document), and Escape. Escape closes
|
|
153
|
-
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. */
|
|
154
162
|
var menusOn = false;
|
|
163
|
+
var MENU = "details.ui-menu, details.gb-account";
|
|
164
|
+
var MENU_ITEM = ".ui-menu__item, .gemboss-account-menu-item";
|
|
155
165
|
function closeMenus(except) {
|
|
156
|
-
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; });
|
|
157
167
|
}
|
|
158
168
|
function initMenus() {
|
|
159
169
|
if (menusOn) return;
|
|
160
170
|
menusOn = true;
|
|
161
171
|
document.addEventListener("click", function (e) {
|
|
162
|
-
var d = e.target.closest && e.target.closest(
|
|
172
|
+
var d = e.target.closest && e.target.closest(MENU);
|
|
163
173
|
closeMenus(d);
|
|
164
|
-
if (d && e.target.closest(
|
|
174
|
+
if (d && e.target.closest(MENU_ITEM)) d.open = false;
|
|
165
175
|
});
|
|
166
176
|
addEventListener("blur", function () { closeMenus(); });
|
|
167
177
|
addEventListener("keydown", function (e) {
|
|
168
|
-
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]");
|
|
169
179
|
if (!d) return;
|
|
170
180
|
e.preventDefault();
|
|
171
181
|
e.stopImmediatePropagation();
|
|
@@ -175,5 +185,103 @@
|
|
|
175
185
|
}, true);
|
|
176
186
|
}
|
|
177
187
|
|
|
178
|
-
|
|
188
|
+
/* Confirm: the kit's answer to `window.confirm()` (four surfaces used the browser's box for deletes, 02/10/2026).
|
|
189
|
+
Resolves true on the confirm button, false on cancel / Escape / backdrop / ×. Strings are TEXT. `tone: "critical"`
|
|
190
|
+
paints the confirm button red (a delete); focus starts on Cancel then, so Enter never destroys by accident. */
|
|
191
|
+
function confirm(o) {
|
|
192
|
+
o = o || {};
|
|
193
|
+
var critical = o.tone === "critical";
|
|
194
|
+
return new Promise(function (resolve) {
|
|
195
|
+
var m = openModal({
|
|
196
|
+
title: esc(o.title || ""),
|
|
197
|
+
label: o.title ? "" : (o.label || "Confirm"),
|
|
198
|
+
// The × is a second Cancel: named in the caller's language unless told otherwise (review #3665).
|
|
199
|
+
closeLabel: o.closeLabel || o.cancelLabel,
|
|
200
|
+
body: o.body ? '<div class="ui-modal__body">' + esc(o.body) + "</div>" : "",
|
|
201
|
+
foot: '<button type="button" class="ui-btn" data-gb-confirm="0"' + (critical ? " autofocus" : "") + ">" + esc(o.cancelLabel || "Cancel") + "</button>" +
|
|
202
|
+
'<button type="button" class="ui-btn ui-btn--primary' + (critical ? " ui-btn--critical" : "") + '" data-gb-confirm="1"' + (critical ? "" : " autofocus") + ">" +
|
|
203
|
+
esc(o.confirmLabel || "OK") + "</button>",
|
|
204
|
+
onClose: function (v) { resolve(v === true); },
|
|
205
|
+
});
|
|
206
|
+
m.panel.querySelectorAll("[data-gb-confirm]").forEach(function (b) {
|
|
207
|
+
b.onclick = function () { m.close(b.getAttribute("data-gb-confirm") === "1"); };
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* The shell's drawer, for a page that wrote AppShell's markup by hand (renderToStaticMarkup, or the README's
|
|
213
|
+
tree). The same behaviour as the React AppShell: the ☰ button toggles it, the backdrop, Escape and a followed
|
|
214
|
+
rail link close it, and so does `gemboss:assistant-docked`; the backdrop leaves the Tab order while closed (it is
|
|
215
|
+
a full-screen invisible button at phone width otherwise), and the ☰ names the rail it controls when the rail has
|
|
216
|
+
an id. Labels: `data-label-open` / `data-label-close` on the button, else its current aria-label is kept.
|
|
217
|
+
Idempotent. */
|
|
218
|
+
var shellOn = false;
|
|
219
|
+
function initShell() {
|
|
220
|
+
if (shellOn) return;
|
|
221
|
+
shellOn = true;
|
|
222
|
+
var shell = function () { return document.querySelector(".gemboss-platform-shell"); };
|
|
223
|
+
var set = function (open) {
|
|
224
|
+
var s = shell();
|
|
225
|
+
if (!s) return;
|
|
226
|
+
s.classList.toggle("is-mobile-nav-open", open);
|
|
227
|
+
var bd = s.querySelector(".gemboss-mobile-nav-backdrop");
|
|
228
|
+
if (bd) bd.tabIndex = open ? 0 : -1;
|
|
229
|
+
var b = s.querySelector(".gemboss-mobile-menu-btn");
|
|
230
|
+
if (!b) return;
|
|
231
|
+
var rail = s.querySelector(".icon-nav[id]");
|
|
232
|
+
if (rail && !b.hasAttribute("aria-controls")) b.setAttribute("aria-controls", rail.id);
|
|
233
|
+
b.setAttribute("aria-expanded", String(open));
|
|
234
|
+
var l = b.getAttribute(open ? "data-label-close" : "data-label-open");
|
|
235
|
+
if (l) b.setAttribute("aria-label", l);
|
|
236
|
+
};
|
|
237
|
+
var isOpen = function () { var s = shell(); return !!s && s.classList.contains("is-mobile-nav-open"); };
|
|
238
|
+
document.addEventListener("click", function (e) {
|
|
239
|
+
var t = e.target;
|
|
240
|
+
if (!t.closest) return;
|
|
241
|
+
if (t.closest(".gemboss-mobile-menu-btn")) set(!isOpen());
|
|
242
|
+
else if (t.closest(".gemboss-mobile-nav-backdrop") || t.closest(".icon-nav .gnav-item, .icon-nav .gnav-subitem")) set(false);
|
|
243
|
+
});
|
|
244
|
+
// Escape while an IME is composing (vi telex, zh, ja) cancels the composition, not the drawer.
|
|
245
|
+
addEventListener("keydown", function (e) { if (e.key === "Escape" && !e.isComposing && e.keyCode !== 229 && isOpen() && !MODALS.length) set(false); });
|
|
246
|
+
addEventListener("gemboss:assistant-docked", function () { set(false); });
|
|
247
|
+
set(isOpen());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* The sign-in screen's reveal button (PasswordField's `.gb-auth-pw-toggle`) on a page with no React: flips the
|
|
251
|
+
input between password and text, its name between `data-label-show` / `data-label-hide` (English defaults), and
|
|
252
|
+
the eye's slash, as the component does. `reset()` puts every field back to hidden (call it on sign-out, or the
|
|
253
|
+
next person sees the password in plain text). Idempotent. */
|
|
254
|
+
var pwOn = false;
|
|
255
|
+
var SLASH = "M3 17 17 3";
|
|
256
|
+
// ONE lookup for the button's field: its aria-controls, else the input beside it in `.gb-auth-pw`.
|
|
257
|
+
function pwInput(b) {
|
|
258
|
+
return document.getElementById(b.getAttribute("aria-controls") || "") || (b.parentNode && b.parentNode.querySelector("input"));
|
|
259
|
+
}
|
|
260
|
+
function pwSet(b, shown) {
|
|
261
|
+
var input = pwInput(b);
|
|
262
|
+
if (!input) return;
|
|
263
|
+
input.type = shown ? "text" : "password";
|
|
264
|
+
var name = shown ? (b.getAttribute("data-label-hide") || "Hide password") : (b.getAttribute("data-label-show") || "Show password");
|
|
265
|
+
b.setAttribute("aria-label", name);
|
|
266
|
+
b.setAttribute("title", name);
|
|
267
|
+
var svg = b.querySelector("svg"), slash = svg && svg.querySelector('path[d="' + SLASH + '"]');
|
|
268
|
+
if (svg && shown && !slash) { var p = document.createElementNS("http://www.w3.org/2000/svg", "path"); p.setAttribute("d", SLASH); svg.append(p); }
|
|
269
|
+
if (slash && !shown) slash.remove();
|
|
270
|
+
}
|
|
271
|
+
function initPasswordFields() {
|
|
272
|
+
if (pwOn) return;
|
|
273
|
+
pwOn = true;
|
|
274
|
+
document.addEventListener("click", function (e) {
|
|
275
|
+
var b = e.target.closest && e.target.closest(".gb-auth-pw-toggle");
|
|
276
|
+
var input = b && pwInput(b);
|
|
277
|
+
if (input) pwSet(b, input.type === "password");
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function resetPasswordFields() {
|
|
281
|
+
document.querySelectorAll(".gb-auth-pw-toggle").forEach(function (b) { pwSet(b, false); });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
g.gembossUI = { openModal: openModal, toast: toast, confirm: confirm, initMenus: initMenus, closeMenus: closeMenus,
|
|
285
|
+
initShell: initShell, initPasswordFields: initPasswordFields, resetPasswordFields: resetPasswordFields,
|
|
286
|
+
openModals: function () { return MODALS.length; } };
|
|
179
287
|
})(globalThis);
|
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
|
@@ -35,7 +35,10 @@
|
|
|
35
35
|
--ui-success-fg: var(--gemboss-success);
|
|
36
36
|
--ui-warning-fg: var(--gemboss-primary-dark);
|
|
37
37
|
--ui-danger-fg: var(--gemboss-danger-strong);
|
|
38
|
-
|
|
38
|
+
/* ONE focus ring for every control (02/10/2026): the admin's, iris — gemboss-shell.css points Polaris'
|
|
39
|
+
--p-color-border-focus at --gemboss-accent. Before, kit buttons, tabs and checks drew black, fields
|
|
40
|
+
and selects #005bd3 (Polaris' default, which the export shipped), and the admin drew iris. */
|
|
41
|
+
--ui-focus: var(--gemboss-accent, #5661f5);
|
|
39
42
|
--ui-radius-sm: 6px;
|
|
40
43
|
--ui-radius-md: 8px;
|
|
41
44
|
--ui-radius-lg: 12px;
|
|
@@ -43,7 +46,10 @@
|
|
|
43
46
|
--ui-space-2: 8px;
|
|
44
47
|
--ui-space-3: 12px;
|
|
45
48
|
--ui-space-4: 16px;
|
|
46
|
-
|
|
49
|
+
/* ONE control height under a mouse: Polaris' TextField/Select (32px). Field, select, button and
|
|
50
|
+
segment share it, so a toolbar row is one height (was 32 / 36 / 34 / 36). Under a thumb every
|
|
51
|
+
control is 44px (the coarse block below; kit-plain.css adds phone WIDTH for non-admin surfaces). */
|
|
52
|
+
--ui-control-h: 32px;
|
|
47
53
|
--ui-fs-body: 13px;
|
|
48
54
|
--ui-fs-caption: 12px;
|
|
49
55
|
/* Content column width for the seller app shell — ONE source of truth for every wide surface
|
|
@@ -140,10 +146,10 @@
|
|
|
140
146
|
Its BOX is the text field's (29/09/2026, Chris): same --gemboss-field-* tokens for ground, line and
|
|
141
147
|
ink, same hover, same focus ring as `.gemboss-field`. Before, a grey --ui-surface-2 select stood next
|
|
142
148
|
to a bordered white field in the same form (gempress editor, admin forms), and `:focus` removed the
|
|
143
|
-
outline and only recoloured the border, so a keyboard user saw almost nothing. Height
|
|
144
|
-
|
|
149
|
+
outline and only recoloured the border, so a keyboard user saw almost nothing. Height is
|
|
150
|
+
--ui-control-h, the button's and the field's, because a select sits in toolbars beside both. */
|
|
145
151
|
.ui-select {
|
|
146
|
-
height:
|
|
152
|
+
height: var(--ui-control-h); padding: 0 32px 0 12px; max-width: 100%;
|
|
147
153
|
background: var(--p-color-input-bg-surface, var(--gemboss-field-bg)) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 20 20'><path fill='%239a9aa4' d='M5 7l5 5 5-5z'/></svg>") no-repeat right 10px center;
|
|
148
154
|
border: 1px solid var(--p-color-input-border, var(--gemboss-field-line)); border-radius: var(--ui-radius-md);
|
|
149
155
|
color: var(--p-color-text, var(--gemboss-field-ink)); font-family: inherit; font-size: var(--ui-fs-body); font-weight: 500;
|
|
@@ -155,7 +161,7 @@
|
|
|
155
161
|
}
|
|
156
162
|
.ui-select:focus { outline: none; }
|
|
157
163
|
.ui-select:focus-visible:not(:disabled) {
|
|
158
|
-
outline: 2px solid var(--p-color-border-focus,
|
|
164
|
+
outline: 2px solid var(--p-color-border-focus, var(--ui-focus)); outline-offset: 1px;
|
|
159
165
|
border-color: var(--p-color-input-border-active, var(--gemboss-field-line-active));
|
|
160
166
|
}
|
|
161
167
|
.ui-select:disabled { opacity: .5; cursor: not-allowed; }
|
|
@@ -166,7 +172,7 @@
|
|
|
166
172
|
bordered mono token (was gemboss-vault-chip). `accent` tone for the highlighted state. */
|
|
167
173
|
.ui-tag {
|
|
168
174
|
display: inline-flex; align-items: center; gap: 6px;
|
|
169
|
-
font-size:
|
|
175
|
+
font-size: var(--gemboss-fs-micro, 11px); font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase;
|
|
170
176
|
padding: 1px 6px; border-radius: 6px; background: var(--ui-surface-2); color: var(--ui-text-muted);
|
|
171
177
|
}
|
|
172
178
|
.ui-tag--accent { background: var(--adm-accent-soft, rgba(20,22,26,.05)); color: var(--adm-accent-fg, var(--ui-accent)); }
|
|
@@ -188,7 +194,7 @@
|
|
|
188
194
|
font-family: inherit; font-size: var(--gemboss-fs-caption, 12px); color: var(--ui-text); text-decoration: none;
|
|
189
195
|
}
|
|
190
196
|
.ui-chips > a.ui-tag--chip:hover { background: var(--ui-surface-2); border-color: var(--ui-text-muted); }
|
|
191
|
-
.ui-chips > a.ui-tag--chip:focus-visible { outline: 2px solid var(--p-color-border-focus,
|
|
197
|
+
.ui-chips > a.ui-tag--chip:focus-visible { outline: 2px solid var(--p-color-border-focus, var(--ui-focus)); outline-offset: 1px; }
|
|
192
198
|
/* Danh sách LỌC dài (chuyên môn, hạng…) dùng ui-chips thay ui-seg: ui-seg không xuống dòng nên tràn ngang ở 320px.
|
|
193
199
|
Ô đang chọn = `aria-current="page"` (lọc là một URL), cùng tông với ui-seg đang chọn. */
|
|
194
200
|
.ui-chips > a.ui-tag--chip[aria-current="page"] { background: var(--ui-accent-soft); border-color: var(--ui-accent); color: var(--ui-accent-strong); font-weight: 600; }
|
|
@@ -207,8 +213,8 @@
|
|
|
207
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
|
|
208
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. */
|
|
209
215
|
.ui-page-head { margin-bottom: 20px; }
|
|
210
|
-
.ui-page-head h1 { color: var(--ui-text-strong); font-size: 22px; font-weight: 650; letter-spacing: -0.01em; }
|
|
211
|
-
.ui-page-head p { color: var(--ui-text-muted); font-size:
|
|
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
|
+
.ui-page-head p { color: var(--ui-text-muted); font-size: var(--ui-fs-body); margin-top: 4px; }
|
|
212
218
|
|
|
213
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.
|
|
214
220
|
Đưa lên từ admin `.gemboss-admin-toolbar`. Chỉ BỐ CỤC: ô tìm là `gemboss-field gemboss-field--group`. */
|
|
@@ -243,7 +249,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
243
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
|
|
244
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.
|
|
245
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. */
|
|
246
|
-
.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(--
|
|
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); }
|
|
247
253
|
.ui-notice--warning { background: var(--gemboss-warning-soft); border-color: var(--gemboss-warning-border); color: var(--gemboss-warning); }
|
|
248
254
|
.ui-notice--danger { background: var(--gemboss-danger-soft); border-color: var(--gemboss-danger-border); color: var(--gemboss-danger-strong); }
|
|
249
255
|
.ui-notice--success { background: var(--gemboss-success-soft); border-color: var(--gemboss-success-border); color: var(--gemboss-success-strong); }
|
|
@@ -251,19 +257,20 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
251
257
|
/* ── Phân trang: `<nav class="ui-pager" aria-label="Trang"><a href rel="prev"><span aria-hidden="true">‹</span> Trước</a><span aria-current="page">2 / 9</span>
|
|
252
258
|
<a href rel="next">Sau ›</a></nav>`. Link ≥ 32px (≥ 24px WCAG 2.5.8), 44px dưới ngón tay (khối coarse). */
|
|
253
259
|
.ui-pager { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 16px 0; }
|
|
254
|
-
.ui-pager > a { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); color: var(--ui-text); text-decoration: none; font-size: var(--
|
|
260
|
+
.ui-pager > a { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); color: var(--ui-text); text-decoration: none; font-size: var(--ui-fs-body); }
|
|
255
261
|
.ui-pager > a:hover { background: var(--ui-surface-2); }
|
|
256
|
-
.ui-pager > a:focus-visible { outline: 2px solid var(--p-color-border-focus,
|
|
262
|
+
.ui-pager > a:focus-visible { outline: 2px solid var(--p-color-border-focus, var(--ui-focus)); outline-offset: 1px; }
|
|
257
263
|
.ui-pager > [aria-current="page"] { color: var(--ui-text-muted); font-size: var(--gemboss-fs-caption); }
|
|
258
264
|
|
|
259
265
|
/* ── Badge ────────────────────────────────────────────────────────────────────
|
|
260
|
-
|
|
266
|
+
Polaris' Badge (02/10/2026; was an uppercase mono pill): 12px/550, padding 2/8, radius 8, the secondary
|
|
267
|
+
fill. The admin's /admin console draws it too (components/ui/Badge.tsx). Tones (`--success|--warning|--critical|
|
|
268
|
+
--info`, `--dot`) are in kit-plain.css, for the non-React surfaces; the seller app uses Polaris <Badge tone>. */
|
|
261
269
|
.ui-badge {
|
|
262
|
-
display: inline-flex; align-items: center;
|
|
263
|
-
font-size:
|
|
264
|
-
padding:
|
|
265
|
-
|
|
266
|
-
}
|
|
270
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
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: 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) */
|
|
267
274
|
|
|
268
275
|
/* ── Modal ────────────────────────────────────────────────────────────────────
|
|
269
276
|
Rendered INLINE (not portaled): a portal escapes the .gemboss-admin-shell token
|
|
@@ -293,7 +300,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
293
300
|
}
|
|
294
301
|
.ui-modal__close svg { width: 20px; height: 20px; }
|
|
295
302
|
.ui-modal__close:hover { background: var(--ui-surface-2); color: var(--ui-text); }
|
|
296
|
-
.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; }
|
|
297
304
|
|
|
298
305
|
/* `size="lg"` — the screen-shaped dialog. Only the HOOK lives here: this sheet is imported by the
|
|
299
306
|
root layout, so every rule in it is paid on every screen, and the geometry of a dialog with one
|
|
@@ -317,7 +324,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
317
324
|
}
|
|
318
325
|
.ui-textarea:focus { outline: none; }
|
|
319
326
|
.ui-textarea:focus-visible:not(:disabled) {
|
|
320
|
-
outline: 2px solid var(--p-color-border-focus,
|
|
327
|
+
outline: 2px solid var(--p-color-border-focus, var(--ui-focus)); outline-offset: 1px;
|
|
321
328
|
border-color: var(--p-color-input-border-active, var(--gemboss-field-line-active));
|
|
322
329
|
}
|
|
323
330
|
.ui-textarea:disabled { opacity: .5; cursor: not-allowed; }
|
|
@@ -335,12 +342,13 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
335
342
|
selected segment uses the accent-soft state. */
|
|
336
343
|
.ui-seg { display: inline-flex; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); overflow: hidden; }
|
|
337
344
|
.ui-seg :is(button, a) {
|
|
338
|
-
display: inline-grid; place-items: center; width:
|
|
345
|
+
display: inline-grid; place-items: center; width: calc(var(--ui-control-h) - 2px); height: calc(var(--ui-control-h) - 2px);
|
|
339
346
|
border: 0; background: transparent; color: var(--ui-text-muted); cursor: pointer; font-family: inherit;
|
|
340
347
|
}
|
|
341
348
|
.ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
|
|
342
349
|
.ui-seg :is(button, a) + :is(button, a) { border-left: 1px solid var(--ui-border); }
|
|
343
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; }
|
|
344
352
|
/* Lọc / chế độ xem bằng LINK (trang server-render, chọn = đổi URL): `<a aria-current="page">` là ô đang chọn. */
|
|
345
353
|
.ui-seg a { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; color: var(--ui-text-muted); }
|
|
346
354
|
.ui-seg a[aria-current="page"],
|
|
@@ -519,7 +527,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
519
527
|
position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
|
|
520
528
|
min-height: 44px; padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
|
|
521
529
|
border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;
|
|
522
|
-
font-family: inherit; font-size: var(--
|
|
530
|
+
font-family: inherit; font-size: var(--ui-fs-body); font-weight: 500; white-space: nowrap;
|
|
523
531
|
color: var(--ui-text-muted);
|
|
524
532
|
transition: color var(--gemboss-transition-fast) ease, background-color var(--gemboss-transition-fast) ease;
|
|
525
533
|
}
|
|
@@ -545,7 +553,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
545
553
|
overflow-x: auto; max-width: 100%;
|
|
546
554
|
background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
|
|
547
555
|
}
|
|
548
|
-
.ui-table { width: 100%; border-collapse: collapse; font-size: var(--
|
|
556
|
+
.ui-table { width: 100%; border-collapse: collapse; font-size: var(--ui-fs-body); color: var(--ui-text); }
|
|
549
557
|
.ui-table th,
|
|
550
558
|
.ui-table td {
|
|
551
559
|
padding: var(--ui-space-2) var(--ui-space-3); text-align: left; vertical-align: middle;
|
|
@@ -569,7 +577,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
569
577
|
.ui-empty {
|
|
570
578
|
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--ui-space-2);
|
|
571
579
|
padding: var(--ui-space-4); padding-block: 40px; text-align: center;
|
|
572
|
-
color: var(--ui-text-muted); font-size: var(--
|
|
580
|
+
color: var(--ui-text-muted); font-size: var(--ui-fs-body);
|
|
573
581
|
}
|
|
574
582
|
.ui-empty__title { margin: 0; font-size: var(--gemboss-fs-h3); font-weight: 600; color: var(--ui-text-strong); }
|
|
575
583
|
.ui-empty__body { margin: 0; max-width: 48ch; line-height: 1.5; }
|