@gemboss/ui 0.4.1 → 0.5.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 +83 -8
- package/catalog/index.html +73 -8
- package/gemboss-ui.css +254 -36
- package/package.json +6 -3
- package/src/app-shell.css +11 -0
- package/src/assistant.css +36 -0
- package/src/chat-composer.css +1 -1
- package/src/chat.css +9 -4
- package/src/gemboss-field.css +2 -2
- package/src/gemboss-tokens.css +1 -1
- package/src/icon.svg +1 -0
- package/src/icons.tsx +2 -0
- package/src/index.ts +1 -0
- package/src/kit-plain.css +163 -4
- package/src/kit-plain.js +102 -1
- package/src/launcher.tsx +33 -0
- package/src/ui.css +29 -22
package/src/gemboss-tokens.css
CHANGED
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
and reading as "the font keeps shrinking". New rule: pick a step here, never a
|
|
80
80
|
raw px. Floor is 12px (--fs-caption); 11px (--fs-micro) is reserved for bold
|
|
81
81
|
micro-badges/count-pills ONLY — nothing below that. Body default is 14px, one
|
|
82
|
-
step up from Polaris bodySm
|
|
82
|
+
step up from Polaris bodyMd (13px; bodySm is 12) which the app had been leaning on for prose. The kit (ui.css) reads --ui-fs-body, 13px, because it sits beside Polaris components. */
|
|
83
83
|
--gemboss-fs-micro: 11px; /* bold count-pills / status chips only */
|
|
84
84
|
--gemboss-fs-caption: 12px; /* metadata, timestamps, secondary labels */
|
|
85
85
|
--gemboss-fs-body: 14px; /* default body / reading text */
|
package/src/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100"><rect width="100" height="100" rx="24" fill="#0B0B0C"/><g transform="translate(18,10) scale(0.66)"><polygon points="26,14 74,14 66,50 34,50" fill="#5661F5"/><path d="M26,14 74,14 96,50 50,110 4,50Z" fill="none" stroke="#fff" stroke-width="9" stroke-linejoin="round"/><path d="M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110" fill="none" stroke="#fff" stroke-width="5.6" stroke-linejoin="round"/></g></svg>
|
package/src/icons.tsx
CHANGED
|
@@ -57,6 +57,8 @@ const P = {
|
|
|
57
57
|
more: <><circle cx="4.5" cy="10" r="1" /><circle cx="10" cy="10" r="1" /><circle cx="15.5" cy="10" r="1" /></>,
|
|
58
58
|
utensils: <><path d="M6 3v5a2 2 0 0 0 4 0V3M8 3v14" /><path d="M15 3c-1.6 1.4-2.5 3.6-2.5 6.5H15V17" /></>,
|
|
59
59
|
signOut: <path d="M8 3.5H4v13h4M12.5 6.5 16 10l-3.5 3.5M16 10H8" />,
|
|
60
|
+
// The assistant's mark (✦ in the surfaces' old hand-drawn buttons): a four-point star + a small one.
|
|
61
|
+
sparkle: <><path d="M9 3.5c.4 2.9 1.6 4.1 4.5 4.5-2.9.4-4.1 1.6-4.5 4.5-.4-2.9-1.6-4.1-4.5-4.5 2.9-.4 4.1-1.6 4.5-4.5z" /><path d="M14.5 12.5c.2 1.3.7 1.8 2 2-1.3.2-1.8.7-2 2-.2-1.3-.7-1.8-2-2 1.3-.2 1.8-.7 2-2z" /></>,
|
|
60
62
|
} satisfies Record<string, React.ReactNode>;
|
|
61
63
|
// `satisfies`, not a `Record<string, …>` annotation: the annotation widened `keyof` to `string`, so a
|
|
62
64
|
// typo (`name="setings"`) type-checked and drew an empty box (review #3474).
|
package/src/index.ts
CHANGED
|
@@ -8,3 +8,4 @@ export { BrandLockup } from "./brand";
|
|
|
8
8
|
export { AccountMenu, AccountMenuItem, initialsOf } from "./account";
|
|
9
9
|
export { Menu, MenuItem, MenuSeparator } from "./menu";
|
|
10
10
|
export { Icon, ICON_NAMES, type IconName } from "./icons";
|
|
11
|
+
export { AssistantLauncher } from "./launcher";
|
package/src/kit-plain.css
CHANGED
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
.ui-link:hover,
|
|
78
78
|
.ui-prose a:not([class]):hover { color: var(--gemboss-accent, #5661f5); text-decoration-thickness: 2px; }
|
|
79
79
|
.ui-link:focus-visible,
|
|
80
|
-
.ui-prose a:not([class]):focus-visible { outline: 2px solid var(--ui-focus, #
|
|
80
|
+
.ui-prose a:not([class]):focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 2px; border-radius: 2px; }
|
|
81
81
|
|
|
82
82
|
.ui-card--media { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
83
83
|
.ui-card__media {
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
}
|
|
87
87
|
.ui-card__media > img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
88
88
|
button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
89
|
-
.ui-card__media:focus-visible { outline: 2px solid var(--ui-focus, #
|
|
89
|
+
.ui-card__media:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: -2px; }
|
|
90
90
|
/* justify-items: start: a tag or a button in the body keeps its own width instead of stretching across the card. */
|
|
91
91
|
.ui-card__body { display: grid; justify-items: start; gap: var(--ui-space-2, 8px); padding: var(--ui-space-3, 12px) var(--ui-space-4, 16px); min-width: 0; }
|
|
92
92
|
.ui-card__foot {
|
|
@@ -96,8 +96,8 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/* Popup body and foot: `<div class="ui-modal__body">…</div><div class="ui-modal__foot"><button class="ui-btn">…</button></div>`.
|
|
99
|
-
|
|
100
|
-
hand (GemFactory: 16/20 and a hairline above the actions). kit-plain.js's openModal writes exactly this shape. */
|
|
99
|
+
ui.css never styled these, and the admin does not load this file: its Modal.tsx children carry their own padding
|
|
100
|
+
(or none). Every non-React surface padded them by hand (GemFactory: 16/20 and a hairline above the actions). kit-plain.js's openModal writes exactly this shape. */
|
|
101
101
|
.ui-modal__body { padding: 16px 20px; }
|
|
102
102
|
.ui-modal__foot {
|
|
103
103
|
display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
|
|
@@ -214,3 +214,162 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
|
214
214
|
/* Tab / nhóm nút là ĐIỀU KHIỂN: ≥ 44px dưới ngón tay (§9.1). Khối coarse của ui.css chỉ bắt `pointer: coarse`; ở khổ điện
|
|
215
215
|
thoại trình duyệt máy tính vẫn báo `fine`, nên nâng theo khổ như ui-btn--icon ở trên. */
|
|
216
216
|
@media (pointer: coarse), (max-width: 47.9975em) { .ui-seg :is(button, a) { height: 44px; min-height: 44px; } }
|
|
217
|
+
|
|
218
|
+
/* ── Polaris parity: the primitives every surface was drawing for itself (02/10/2026) ──────────────────────
|
|
219
|
+
An audit of the six surfaces on the package (gemwatcher, GemFactory, gempress, gemcare, partner-radar,
|
|
220
|
+
content-radar) found the same gaps re-filled six ways: no destructive button (four surfaces fell back to the
|
|
221
|
+
browser's `confirm()`), one-tone badges, banners as bare colour blocks, hand-made radio / switch / tooltip /
|
|
222
|
+
spinner, and help and error text under a field in five sizes. The admin has all of them from Polaris; this is
|
|
223
|
+
the same shape in plain CSS, with Polaris' measurements and GemBoss' tokens (the danger / success / warning
|
|
224
|
+
set the kit's tags and notices already use). The admin does not load this file. */
|
|
225
|
+
|
|
226
|
+
/* Destructive button: `ui-btn ui-btn--critical` (red text on the default button) and
|
|
227
|
+
`ui-btn ui-btn--primary ui-btn--critical` (red fill), Polaris' `tone="critical"` in both variants. Red text is
|
|
228
|
+
--gemboss-danger-strong: 7.3:1 on white, 5.7:1 on the hover ground (shell.css says why not --gemboss-danger). */
|
|
229
|
+
.ui-btn--critical { color: var(--gemboss-danger-strong); }
|
|
230
|
+
.ui-btn--critical:hover { background: var(--gemboss-danger-soft); color: var(--gemboss-danger-strong); }
|
|
231
|
+
.ui-btn--primary.ui-btn--critical { background: var(--gemboss-danger); border-color: var(--gemboss-danger); color: var(--gemboss-on-primary); }
|
|
232
|
+
.ui-btn--primary.ui-btn--critical:hover { background: var(--gemboss-danger-strong); border-color: var(--gemboss-danger-strong); color: var(--gemboss-on-primary); }
|
|
233
|
+
.ui-btn--critical:focus-visible { outline-color: var(--gemboss-danger-strong); }
|
|
234
|
+
|
|
235
|
+
/* Under a thumb, and at phone WIDTH (a desktop browser at 320px reports pointer: fine), EVERY kit button is a 44px
|
|
236
|
+
control, the compact one included: §9.1 has no density exception, and GemFactory, gempress and gemcare each
|
|
237
|
+
restated this floor for `--sm` themselves. ui.css's coarse block leaves `--sm` alone for the admin's own
|
|
238
|
+
settings page; this file is not loaded there. */
|
|
239
|
+
@media (pointer: coarse), (max-width: 47.9975em) {
|
|
240
|
+
.ui-btn, .ui-btn.ui-btn--sm { min-height: 44px; }
|
|
241
|
+
.ui-select.ui-select { min-height: 44px; }
|
|
242
|
+
/* A target is an AREA: the compact icon button (28 wide) and an icon segment (30) grow sideways as well. */
|
|
243
|
+
.ui-btn--icon.ui-btn--sm, .ui-seg :is(button, a):not(.is-text) { min-width: 44px; }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* Badge tones: `<span class="ui-badge ui-badge--success">Active</span>`. Polaris' tones on GemBoss' soft grounds (Polaris' `attention` is `--warning` here: the fleet has one amber).
|
|
247
|
+
`ui-badge--dot` puts a status dot before the label (filled = done, the tone's colour). */
|
|
248
|
+
.ui-badge--success { background: var(--gemboss-success-soft); color: var(--gemboss-success-strong); }
|
|
249
|
+
.ui-badge--warning { background: var(--gemboss-warning-soft); color: var(--gemboss-warning); }
|
|
250
|
+
.ui-badge--critical { background: var(--gemboss-danger-soft); color: var(--gemboss-danger-strong); }
|
|
251
|
+
.ui-badge--info { background: var(--gemboss-accent-soft); color: var(--gemboss-accent-strong); }
|
|
252
|
+
.ui-badge--dot::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
253
|
+
|
|
254
|
+
/* Banner: Polaris' in-page Banner. A tone strip with an icon and a title, a body, optional actions, optional
|
|
255
|
+
dismiss. `ui-notice` stays for a one-line note; a banner is for something the person must read or act on.
|
|
256
|
+
<div class="ui-banner ui-banner--warning" role="status">
|
|
257
|
+
<div class="ui-banner__head"><svg class="ui-banner__icon" …/><h3 class="ui-banner__title">Slack token expired</h3>
|
|
258
|
+
<button type="button" class="ui-banner__close" aria-label="Dismiss">…</button></div>
|
|
259
|
+
<div class="ui-banner__body"><p>Monday's digest will not send.</p>
|
|
260
|
+
<div class="ui-banner__actions"><a class="ui-btn" href>Reconnect</a></div></div></div>
|
|
261
|
+
No tone = neutral. role="status" / "alert" only when it appears after the page loaded. */
|
|
262
|
+
.ui-banner {
|
|
263
|
+
--ui-banner-bg: var(--ui-surface-2); --ui-banner-fg: var(--ui-text);
|
|
264
|
+
border-radius: var(--ui-radius-lg); overflow: hidden; background: var(--ui-surface); color: var(--ui-text);
|
|
265
|
+
box-shadow: 0 0 0 1px var(--ui-border), 0 3px 1px -1px rgba(26, 26, 26, 0.07);
|
|
266
|
+
font-size: var(--ui-fs-body); line-height: 20px;
|
|
267
|
+
}
|
|
268
|
+
.ui-banner--success { --ui-banner-bg: var(--gemboss-success-soft); --ui-banner-fg: var(--gemboss-success-strong); }
|
|
269
|
+
.ui-banner--warning { --ui-banner-bg: var(--gemboss-warning-soft); --ui-banner-fg: var(--gemboss-warning); }
|
|
270
|
+
.ui-banner--critical { --ui-banner-bg: var(--gemboss-danger-soft); --ui-banner-fg: var(--gemboss-danger-strong); }
|
|
271
|
+
.ui-banner--info { --ui-banner-bg: var(--gemboss-accent-soft); --ui-banner-fg: var(--gemboss-accent-strong); }
|
|
272
|
+
.ui-banner__head { display: flex; align-items: center; gap: var(--ui-space-2); min-height: 32px; padding: 6px var(--ui-space-3); background: var(--ui-banner-bg); color: var(--ui-banner-fg); }
|
|
273
|
+
.ui-banner__icon { flex: none; width: 20px; height: 20px; fill: currentColor; }
|
|
274
|
+
.ui-banner__title { flex: 1 1 auto; min-width: 0; margin: 0; font-size: var(--ui-fs-body); font-weight: 650; line-height: 20px; }
|
|
275
|
+
.ui-banner__close {
|
|
276
|
+
flex: none; display: inline-grid; place-items: center; width: 32px; height: 32px; margin: -6px -8px -6px 0;
|
|
277
|
+
border: 0; border-radius: var(--ui-radius-md); background: transparent; color: inherit; cursor: pointer;
|
|
278
|
+
}
|
|
279
|
+
.ui-banner__close:hover { background: color-mix(in srgb, currentColor 10%, transparent); }
|
|
280
|
+
.ui-banner__close:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
|
|
281
|
+
.ui-banner__close svg { width: 16px; height: 16px; }
|
|
282
|
+
.ui-banner__body { padding: var(--ui-space-3) var(--ui-space-4); }
|
|
283
|
+
.ui-banner__body > :first-child { margin-top: 0; }
|
|
284
|
+
.ui-banner__body > :last-child { margin-bottom: 0; }
|
|
285
|
+
.ui-banner__actions { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); margin-top: var(--ui-space-3); }
|
|
286
|
+
@media (pointer: coarse), (max-width: 47.9975em) { .ui-banner__close { width: 44px; height: 44px; margin: -12px -12px -12px 0; } }
|
|
287
|
+
|
|
288
|
+
/* Help and error text under a field, Polaris' Labelled help text and InlineError:
|
|
289
|
+
<label class="ui-label" for="x">Domain</label><input id="x" class="gemboss-field gemboss-field--tall"
|
|
290
|
+
aria-describedby="x-help x-err" aria-invalid="true"><p class="ui-help" id="x-help">Without https://</p>
|
|
291
|
+
<p class="ui-error" id="x-err">This store is already tracked.</p>
|
|
292
|
+
aria-invalid paints the field in the critical tone; the message says what to do, not only what is wrong. */
|
|
293
|
+
.ui-help { margin: var(--ui-space-1) 0 0; font-size: var(--ui-fs-body); line-height: 20px; color: var(--ui-text-muted); }
|
|
294
|
+
.ui-error:empty { display: none; } /* a placeholder kept for aria-describedby draws no icon */
|
|
295
|
+
.ui-error { display: flex; gap: 6px; margin: var(--ui-space-1) 0 0; font-size: var(--ui-fs-body); line-height: 20px; color: var(--gemboss-danger-strong); }
|
|
296
|
+
.ui-error::before {
|
|
297
|
+
content: ""; flex: none; width: 16px; height: 16px; margin-top: 2px; background: currentColor;
|
|
298
|
+
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/></svg>") center / contain no-repeat;
|
|
299
|
+
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/></svg>") center / contain no-repeat;
|
|
300
|
+
}
|
|
301
|
+
.gemboss-field[aria-invalid="true"], .gemboss-field--group:has([aria-invalid="true"]),
|
|
302
|
+
.ui-select[aria-invalid="true"], .ui-textarea[aria-invalid="true"] {
|
|
303
|
+
border-color: var(--gemboss-danger-strong); background-color: var(--gemboss-danger-soft);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* Radio: the checkbox's row with `type="radio"` — `<fieldset class="ui-choices"><legend class="ui-label">Plan</legend>
|
|
307
|
+
<label class="ui-check"><input type="radio" name="plan" checked> Basic</label>…</fieldset>`. */
|
|
308
|
+
.ui-choices { display: grid; gap: 0; margin: 0; padding: 0; border: 0; min-width: 0; }
|
|
309
|
+
.ui-choices > legend { padding: 0; }
|
|
310
|
+
|
|
311
|
+
/* Switch: a checkbox that reads as on / off, for a setting that applies at once (no Save button).
|
|
312
|
+
`<label class="ui-check ui-switch"><input type="checkbox" role="switch" checked> Send Monday digest</label>`.
|
|
313
|
+
Track 32×20, thumb 16; the row stays the checkbox's 44px target. */
|
|
314
|
+
.ui-switch > input[type="checkbox"] {
|
|
315
|
+
appearance: none; -webkit-appearance: none; position: relative; width: 32px; height: 20px; margin: 0;
|
|
316
|
+
border-radius: var(--gemboss-radius-full); background: var(--gemboss-field-line); /* 3.5:1 on white, WCAG 1.4.11 */ transition: background-color var(--gemboss-transition-fast) ease;
|
|
317
|
+
}
|
|
318
|
+
.ui-switch > input[type="checkbox"]::before {
|
|
319
|
+
content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
|
|
320
|
+
background: var(--gemboss-surface); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); transition: transform var(--gemboss-transition-fast) ease;
|
|
321
|
+
}
|
|
322
|
+
.ui-switch > input[type="checkbox"]:checked { background: var(--ui-accent); }
|
|
323
|
+
.ui-switch > input[type="checkbox"]:checked::before { transform: translateX(12px); }
|
|
324
|
+
@media (prefers-reduced-motion: reduce) { .ui-switch > input[type="checkbox"], .ui-switch > input[type="checkbox"]::before { transition: none; } }
|
|
325
|
+
|
|
326
|
+
/* Tooltip: a SHORT label for an icon-only control, shown on hover and keyboard focus. Not for anything the person
|
|
327
|
+
needs to act on (a touch screen never hovers).
|
|
328
|
+
<span class="ui-tooltip"><button class="ui-btn ui-btn--icon" aria-describedby="t1" aria-label="Archive">…</button>
|
|
329
|
+
<span class="ui-tooltip__bubble" role="tooltip" id="t1">Archive</span></span> `--end` aligns it right. */
|
|
330
|
+
.ui-tooltip { position: relative; display: inline-flex; }
|
|
331
|
+
.ui-tooltip__bubble {
|
|
332
|
+
position: absolute; z-index: 30; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
|
|
333
|
+
width: max-content; max-width: 240px; padding: 4px 8px; border-radius: var(--ui-radius-md);
|
|
334
|
+
background: var(--ui-surface); color: var(--ui-text); font-size: var(--ui-fs-body); line-height: 20px; font-weight: 450;
|
|
335
|
+
box-shadow: 0 8px 16px -4px rgba(26, 26, 26, 0.22), 0 0 0 1px var(--ui-border);
|
|
336
|
+
opacity: 0; visibility: hidden; pointer-events: none; transition: opacity var(--gemboss-transition-fast) ease;
|
|
337
|
+
}
|
|
338
|
+
.ui-tooltip--end .ui-tooltip__bubble { left: auto; right: 0; transform: none; }
|
|
339
|
+
.ui-tooltip:hover > .ui-tooltip__bubble, .ui-tooltip:focus-within > .ui-tooltip__bubble { opacity: 1; visibility: visible; }
|
|
340
|
+
|
|
341
|
+
/* Spinner: `<span class="ui-spinner" role="status"><span class="ui-sr-only">Loading…</span></span>`, 20px
|
|
342
|
+
(Polaris small); `--lg` is 44px. Reduced motion turns it slower, never off: it is the only sign of waiting. */
|
|
343
|
+
.ui-spinner {
|
|
344
|
+
display: inline-block; flex: none; width: 20px; height: 20px; box-sizing: border-box; border-radius: 50%;
|
|
345
|
+
border: 2px solid color-mix(in srgb, var(--ui-text) 18%, transparent); border-top-color: var(--ui-text);
|
|
346
|
+
animation: ui-spin 0.7s linear infinite; vertical-align: middle;
|
|
347
|
+
}
|
|
348
|
+
.ui-spinner--lg { width: 44px; height: 44px; border-width: 4px; }
|
|
349
|
+
@keyframes ui-spin { to { transform: rotate(360deg); } }
|
|
350
|
+
@media (prefers-reduced-motion: reduce) { .ui-spinner { animation-duration: 2s; } }
|
|
351
|
+
|
|
352
|
+
/* Progress: `<progress class="ui-progress" value="37" max="100" aria-label="Import">37%</progress>`, 8px, the
|
|
353
|
+
primary fill (Polaris' small ProgressBar). Tones: `--success`, `--critical`. */
|
|
354
|
+
.ui-progress { appearance: none; -webkit-appearance: none; display: block; width: 100%; height: 8px; border: 0; border-radius: var(--gemboss-radius-full); overflow: hidden; background: var(--gemboss-divider); color: var(--ui-accent); }
|
|
355
|
+
.ui-progress::-webkit-progress-bar { background: var(--gemboss-divider); border-radius: inherit; }
|
|
356
|
+
.ui-progress::-webkit-progress-value { background: currentColor; border-radius: inherit; }
|
|
357
|
+
.ui-progress::-moz-progress-bar { background: currentColor; border-radius: inherit; }
|
|
358
|
+
.ui-progress--success { color: var(--gemboss-success); }
|
|
359
|
+
.ui-progress--critical { color: var(--gemboss-danger); }
|
|
360
|
+
|
|
361
|
+
/* Wider popup: `openModal({ className: "ui-modal--wide" })`, Polaris' default Modal (620px). The kit's own 460 is
|
|
362
|
+
Polaris' small step plus one; a form with two columns or a table needs this one. */
|
|
363
|
+
.ui-modal--wide { width: min(620px, 100%); }
|
|
364
|
+
|
|
365
|
+
/* Native popup: `<dialog class="ui-modal">` opened with `showModal()`, for React (and other) surfaces that do not load
|
|
366
|
+
kit-plain.js. The browser does what openModal does by hand: Escape closes it, the page behind is inert, it sits in
|
|
367
|
+
the top layer above the shell's topbar. Same head / body / foot markup; `::backdrop` is the kit's overlay. gemcare
|
|
368
|
+
and content-radar already used `<dialog>`, each with its own width, padding and backdrop (02/10/2026: four shapes,
|
|
369
|
+
420 to 760 wide). Close on a backdrop click yourself: a click whose target IS the dialog landed outside the panel.
|
|
370
|
+
`<dialog class="ui-modal" aria-labelledby="t"><div class="ui-modal__head"><div class="ui-modal__title" id="t">…</div>
|
|
371
|
+
<button type="button" class="ui-modal__close" aria-label="Close">…</button></div><div class="ui-modal__body">…</div>
|
|
372
|
+
<div class="ui-modal__foot">…</div></dialog>` */
|
|
373
|
+
dialog.ui-modal { padding: 0; max-width: calc(100% - 40px); max-height: calc(100dvh - 40px); color: var(--ui-text); }
|
|
374
|
+
dialog.ui-modal[open] { display: flex; flex-direction: column; }
|
|
375
|
+
dialog.ui-modal::backdrop { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px); }
|
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";
|
|
@@ -175,5 +178,103 @@
|
|
|
175
178
|
}, true);
|
|
176
179
|
}
|
|
177
180
|
|
|
178
|
-
|
|
181
|
+
/* Confirm: the kit's answer to `window.confirm()` (four surfaces used the browser's box for deletes, 02/10/2026).
|
|
182
|
+
Resolves true on the confirm button, false on cancel / Escape / backdrop / ×. Strings are TEXT. `tone: "critical"`
|
|
183
|
+
paints the confirm button red (a delete); focus starts on Cancel then, so Enter never destroys by accident. */
|
|
184
|
+
function confirm(o) {
|
|
185
|
+
o = o || {};
|
|
186
|
+
var critical = o.tone === "critical";
|
|
187
|
+
return new Promise(function (resolve) {
|
|
188
|
+
var m = openModal({
|
|
189
|
+
title: esc(o.title || ""),
|
|
190
|
+
label: o.title ? "" : (o.label || "Confirm"),
|
|
191
|
+
// The × is a second Cancel: named in the caller's language unless told otherwise (review #3665).
|
|
192
|
+
closeLabel: o.closeLabel || o.cancelLabel,
|
|
193
|
+
body: o.body ? '<div class="ui-modal__body">' + esc(o.body) + "</div>" : "",
|
|
194
|
+
foot: '<button type="button" class="ui-btn" data-gb-confirm="0"' + (critical ? " autofocus" : "") + ">" + esc(o.cancelLabel || "Cancel") + "</button>" +
|
|
195
|
+
'<button type="button" class="ui-btn ui-btn--primary' + (critical ? " ui-btn--critical" : "") + '" data-gb-confirm="1"' + (critical ? "" : " autofocus") + ">" +
|
|
196
|
+
esc(o.confirmLabel || "OK") + "</button>",
|
|
197
|
+
onClose: function (v) { resolve(v === true); },
|
|
198
|
+
});
|
|
199
|
+
m.panel.querySelectorAll("[data-gb-confirm]").forEach(function (b) {
|
|
200
|
+
b.onclick = function () { m.close(b.getAttribute("data-gb-confirm") === "1"); };
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* The shell's drawer, for a page that wrote AppShell's markup by hand (renderToStaticMarkup, or the README's
|
|
206
|
+
tree). The same behaviour as the React AppShell: the ☰ button toggles it, the backdrop, Escape and a followed
|
|
207
|
+
rail link close it, and so does `gemboss:assistant-docked`; the backdrop leaves the Tab order while closed (it is
|
|
208
|
+
a full-screen invisible button at phone width otherwise), and the ☰ names the rail it controls when the rail has
|
|
209
|
+
an id. Labels: `data-label-open` / `data-label-close` on the button, else its current aria-label is kept.
|
|
210
|
+
Idempotent. */
|
|
211
|
+
var shellOn = false;
|
|
212
|
+
function initShell() {
|
|
213
|
+
if (shellOn) return;
|
|
214
|
+
shellOn = true;
|
|
215
|
+
var shell = function () { return document.querySelector(".gemboss-platform-shell"); };
|
|
216
|
+
var set = function (open) {
|
|
217
|
+
var s = shell();
|
|
218
|
+
if (!s) return;
|
|
219
|
+
s.classList.toggle("is-mobile-nav-open", open);
|
|
220
|
+
var bd = s.querySelector(".gemboss-mobile-nav-backdrop");
|
|
221
|
+
if (bd) bd.tabIndex = open ? 0 : -1;
|
|
222
|
+
var b = s.querySelector(".gemboss-mobile-menu-btn");
|
|
223
|
+
if (!b) return;
|
|
224
|
+
var rail = s.querySelector(".icon-nav[id]");
|
|
225
|
+
if (rail && !b.hasAttribute("aria-controls")) b.setAttribute("aria-controls", rail.id);
|
|
226
|
+
b.setAttribute("aria-expanded", String(open));
|
|
227
|
+
var l = b.getAttribute(open ? "data-label-close" : "data-label-open");
|
|
228
|
+
if (l) b.setAttribute("aria-label", l);
|
|
229
|
+
};
|
|
230
|
+
var isOpen = function () { var s = shell(); return !!s && s.classList.contains("is-mobile-nav-open"); };
|
|
231
|
+
document.addEventListener("click", function (e) {
|
|
232
|
+
var t = e.target;
|
|
233
|
+
if (!t.closest) return;
|
|
234
|
+
if (t.closest(".gemboss-mobile-menu-btn")) set(!isOpen());
|
|
235
|
+
else if (t.closest(".gemboss-mobile-nav-backdrop") || t.closest(".icon-nav .gnav-item, .icon-nav .gnav-subitem")) set(false);
|
|
236
|
+
});
|
|
237
|
+
// Escape while an IME is composing (vi telex, zh, ja) cancels the composition, not the drawer.
|
|
238
|
+
addEventListener("keydown", function (e) { if (e.key === "Escape" && !e.isComposing && e.keyCode !== 229 && isOpen() && !MODALS.length) set(false); });
|
|
239
|
+
addEventListener("gemboss:assistant-docked", function () { set(false); });
|
|
240
|
+
set(isOpen());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* The sign-in screen's reveal button (PasswordField's `.gb-auth-pw-toggle`) on a page with no React: flips the
|
|
244
|
+
input between password and text, its name between `data-label-show` / `data-label-hide` (English defaults), and
|
|
245
|
+
the eye's slash, as the component does. `reset()` puts every field back to hidden (call it on sign-out, or the
|
|
246
|
+
next person sees the password in plain text). Idempotent. */
|
|
247
|
+
var pwOn = false;
|
|
248
|
+
var SLASH = "M3 17 17 3";
|
|
249
|
+
// ONE lookup for the button's field: its aria-controls, else the input beside it in `.gb-auth-pw`.
|
|
250
|
+
function pwInput(b) {
|
|
251
|
+
return document.getElementById(b.getAttribute("aria-controls") || "") || (b.parentNode && b.parentNode.querySelector("input"));
|
|
252
|
+
}
|
|
253
|
+
function pwSet(b, shown) {
|
|
254
|
+
var input = pwInput(b);
|
|
255
|
+
if (!input) return;
|
|
256
|
+
input.type = shown ? "text" : "password";
|
|
257
|
+
var name = shown ? (b.getAttribute("data-label-hide") || "Hide password") : (b.getAttribute("data-label-show") || "Show password");
|
|
258
|
+
b.setAttribute("aria-label", name);
|
|
259
|
+
b.setAttribute("title", name);
|
|
260
|
+
var svg = b.querySelector("svg"), slash = svg && svg.querySelector('path[d="' + SLASH + '"]');
|
|
261
|
+
if (svg && shown && !slash) { var p = document.createElementNS("http://www.w3.org/2000/svg", "path"); p.setAttribute("d", SLASH); svg.append(p); }
|
|
262
|
+
if (slash && !shown) slash.remove();
|
|
263
|
+
}
|
|
264
|
+
function initPasswordFields() {
|
|
265
|
+
if (pwOn) return;
|
|
266
|
+
pwOn = true;
|
|
267
|
+
document.addEventListener("click", function (e) {
|
|
268
|
+
var b = e.target.closest && e.target.closest(".gb-auth-pw-toggle");
|
|
269
|
+
var input = b && pwInput(b);
|
|
270
|
+
if (input) pwSet(b, input.type === "password");
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function resetPasswordFields() {
|
|
274
|
+
document.querySelectorAll(".gb-auth-pw-toggle").forEach(function (b) { pwSet(b, false); });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
g.gembossUI = { openModal: openModal, toast: toast, confirm: confirm, initMenus: initMenus, closeMenus: closeMenus,
|
|
278
|
+
initShell: initShell, initPasswordFields: initPasswordFields, resetPasswordFields: resetPasswordFields,
|
|
279
|
+
openModals: function () { return MODALS.length; } };
|
|
179
280
|
})(globalThis);
|
package/src/launcher.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* The button that opens a surface's AI assistant: bottom-right, floating (02/10/2026, Chris: "chắc vẫn phải để button ở góc
|
|
4
|
+
* dưới bên phải còn góc trên bên phải là account … đồng bộ"). partner-radar had it in the topbar's top-right corner, which on
|
|
5
|
+
* a phone became a black ✦ circle exactly where content-radar shows the account chip — two surfaces on one frame, the same
|
|
6
|
+
* corner doing two different jobs. The top-right corner is the ACCOUNT (AccountMenu); the assistant lives here.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately dumb: no provider, no state. A surface on `@gemboss/ui/assistant` passes `hidden` while its dock is open
|
|
9
|
+
* (`presentation !== "hidden"`); a surface with its own panel does the same with its own flag.
|
|
10
|
+
*/
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { Icon } from "./icons";
|
|
13
|
+
|
|
14
|
+
export function AssistantLauncher({ label, shortcut, hidden, onClick, className, ...rest }: {
|
|
15
|
+
/** Visible name, e.g. "Hỏi AI" / "Partner Assistant". Kept as the accessible name when the text hides on phones. */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Keyboard hint shown on wide screens, e.g. "⌘J". Decorative: the surface owns the shortcut itself. */
|
|
18
|
+
shortcut?: string;
|
|
19
|
+
/** The assistant is already open — the launcher steps out of the way (it would sit under the dock). */
|
|
20
|
+
hidden?: boolean;
|
|
21
|
+
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
22
|
+
className?: string;
|
|
23
|
+
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "hidden">) {
|
|
24
|
+
if (hidden) return null;
|
|
25
|
+
return (
|
|
26
|
+
<button type="button" className={className ? `gb-assistant-launcher ${className}` : "gb-assistant-launcher"}
|
|
27
|
+
aria-label={shortcut ? `${label} (${shortcut})` : label} onClick={onClick} {...rest}>
|
|
28
|
+
<Icon name="sparkle" />
|
|
29
|
+
<span className="gb-assistant-launcher-label">{label}</span>
|
|
30
|
+
{shortcut ? <kbd className="gb-assistant-launcher-kbd">{shortcut}</kbd> : null}
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
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; }
|
|
@@ -208,7 +214,7 @@
|
|
|
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
216
|
.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:
|
|
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,18 +257,19 @@ 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
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
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: var(--ui-surface-2); color: var(--ui-text-muted);
|
|
266
273
|
}
|
|
267
274
|
|
|
268
275
|
/* ── Modal ────────────────────────────────────────────────────────────────────
|
|
@@ -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,7 +342,7 @@ 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; }
|
|
@@ -519,7 +526,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
519
526
|
position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
|
|
520
527
|
min-height: 44px; padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
|
|
521
528
|
border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;
|
|
522
|
-
font-family: inherit; font-size: var(--
|
|
529
|
+
font-family: inherit; font-size: var(--ui-fs-body); font-weight: 500; white-space: nowrap;
|
|
523
530
|
color: var(--ui-text-muted);
|
|
524
531
|
transition: color var(--gemboss-transition-fast) ease, background-color var(--gemboss-transition-fast) ease;
|
|
525
532
|
}
|
|
@@ -545,7 +552,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
545
552
|
overflow-x: auto; max-width: 100%;
|
|
546
553
|
background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
|
|
547
554
|
}
|
|
548
|
-
.ui-table { width: 100%; border-collapse: collapse; font-size: var(--
|
|
555
|
+
.ui-table { width: 100%; border-collapse: collapse; font-size: var(--ui-fs-body); color: var(--ui-text); }
|
|
549
556
|
.ui-table th,
|
|
550
557
|
.ui-table td {
|
|
551
558
|
padding: var(--ui-space-2) var(--ui-space-3); text-align: left; vertical-align: middle;
|
|
@@ -569,7 +576,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
569
576
|
.ui-empty {
|
|
570
577
|
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--ui-space-2);
|
|
571
578
|
padding: var(--ui-space-4); padding-block: 40px; text-align: center;
|
|
572
|
-
color: var(--ui-text-muted); font-size: var(--
|
|
579
|
+
color: var(--ui-text-muted); font-size: var(--ui-fs-body);
|
|
573
580
|
}
|
|
574
581
|
.ui-empty__title { margin: 0; font-size: var(--gemboss-fs-h3); font-weight: 600; color: var(--ui-text-strong); }
|
|
575
582
|
.ui-empty__body { margin: 0; max-width: 48ch; line-height: 1.5; }
|