@gemboss/ui 0.8.1 → 0.12.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/CHANGELOG.md +122 -0
- package/README.md +212 -3
- package/catalog/index.html +140 -7
- package/gemboss-ui.css +199 -69
- package/package.json +12 -3
- package/src/app-shell.css +89 -0
- package/src/editor.css +1099 -0
- package/src/gemboss-tokens.css +1 -1
- package/src/index.ts +2 -0
- package/src/kit-plain.css +39 -12
- package/src/kit-plain.js +118 -1
- package/src/nav-dwell.ts +6 -0
- package/src/next/index.ts +4 -0
- package/src/next/nav.tsx +142 -0
- package/src/route-holding.tsx +65 -0
- package/src/shell.tsx +12 -7
- package/src/theme-dark.css +1 -0
- package/src/ui.css +67 -54
package/src/ui.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* ══
|
|
2
2
|
Tool/App UI kit — the shared primitive layer for the internal operator surfaces
|
|
3
3
|
(admin console, seller app chrome, theme-editor). ONE semantic token spine
|
|
4
4
|
(`--ui-*`) that every primitive reads, plus the primitive CSS itself.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
The `--ui-*` layer is SEMANTIC and resolved per surface: components only read
|
|
12
12
|
`--ui-*`; each surface maps those to its own tokens (admin → `--adm-*` below),
|
|
13
13
|
so one <Button> looks native everywhere without hard-coding a palette.
|
|
14
|
-
|
|
14
|
+
══ */
|
|
15
15
|
|
|
16
16
|
:root {
|
|
17
17
|
--ui-surface: #ffffff;
|
|
@@ -46,10 +46,15 @@
|
|
|
46
46
|
--ui-space-2: 8px;
|
|
47
47
|
--ui-space-3: 12px;
|
|
48
48
|
--ui-space-4: 16px;
|
|
49
|
-
/*
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
/* Polaris' two heights under a mouse (0.12.0): a FIELD (gemboss-field, ui-select) is TextField/Select, 32px; a BUTTON
|
|
50
|
+
(ui-btn, ui-seg, ui-tabs) is Button, 28px. A toolbar row is 32 + 28, as in the admin. Under a thumb every control is
|
|
51
|
+
44px (the coarse block below; kit-plain.css adds phone WIDTH for non-admin surfaces). */
|
|
52
52
|
--ui-control-h: 32px;
|
|
53
|
+
--ui-btn-h: 28px;
|
|
54
|
+
/* Polaris Card (13.9.5): no grey frame of its own, --p-shadow-100 under it. The edge is ONE 1px line in the surface's
|
|
55
|
+
--ui-border (Polaris paints a bevel inset on a ::before; a border cannot be covered by a full-bleed child, and it
|
|
56
|
+
follows a surface's dark --ui-border, which a light bevel would not). */
|
|
57
|
+
--ui-card-shadow: 0 1px 0 rgba(26,26,26,.07);
|
|
53
58
|
--ui-fs-body: 13px;
|
|
54
59
|
--ui-fs-caption: 12px;
|
|
55
60
|
/* Content column width for the seller app shell — ONE source of truth for every wide surface
|
|
@@ -105,16 +110,17 @@
|
|
|
105
110
|
--ui-warning-fg: color-mix(in srgb, #c99a2e 70%, #ffffff);
|
|
106
111
|
--ui-danger-fg: color-mix(in srgb, var(--gemboss-danger-strong) 70%, #ffffff);
|
|
107
112
|
--ui-focus: var(--adm-accent);
|
|
113
|
+
--ui-card-shadow: none;
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
/* ── Button
|
|
116
|
+
/* ── Button ──
|
|
111
117
|
inline-flex + center so a <button> and an <a> render identically (anchors ignore
|
|
112
118
|
`height` as inline elements — the root cause of the misaligned link-buttons). */
|
|
113
119
|
.ui-btn {
|
|
114
120
|
display: inline-flex; align-items: center; justify-content: center; gap: var(--ui-space-2);
|
|
115
|
-
height: var(--ui-
|
|
121
|
+
height: var(--ui-btn-h); padding: 0 var(--ui-space-3); box-sizing: border-box;
|
|
116
122
|
line-height: 1; white-space: nowrap; vertical-align: middle;
|
|
117
|
-
border-radius: var(--ui-radius-md); font-family: inherit; font-size: var(--ui-fs-
|
|
123
|
+
border-radius: var(--ui-radius-md); font-family: inherit; font-size: var(--ui-fs-caption); font-weight: 550;
|
|
118
124
|
cursor: pointer; text-decoration: none; user-select: none;
|
|
119
125
|
border: 1px solid var(--ui-border); background: transparent; color: var(--ui-text);
|
|
120
126
|
transition: background .12s ease, border-color .12s ease;
|
|
@@ -126,33 +132,33 @@
|
|
|
126
132
|
.ui-btn--primary:hover { background: var(--ui-accent-hover); border-color: var(--ui-accent-hover); }
|
|
127
133
|
.ui-btn--ghost { border-color: transparent; background: transparent; color: var(--ui-text-muted); }
|
|
128
134
|
.ui-btn--ghost:hover { background: var(--ui-surface-2); color: var(--ui-text); }
|
|
129
|
-
|
|
135
|
+
/* `ui-btn--sm`: the default size under a mouse since 0.12.0; it only marks a button the admin keeps compact under a finger. */
|
|
130
136
|
|
|
131
|
-
/* ── Card
|
|
137
|
+
/* ── Card ── */
|
|
132
138
|
.ui-card {
|
|
133
139
|
background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
|
|
134
|
-
padding: var(--ui-space-4); color: var(--ui-text);
|
|
140
|
+
box-shadow: var(--ui-card-shadow); padding: var(--ui-space-4); color: var(--ui-text);
|
|
135
141
|
}
|
|
136
|
-
.ui-card__title { font-size: 14px; font-weight:
|
|
142
|
+
.ui-card__title { font-size: 14px; line-height: 20px; font-weight: 650; margin: 0 0 var(--ui-space-2); }
|
|
137
143
|
/* Đầu thẻ có nút: `<div class="ui-card__head"><h2 class="ui-card__title">…</h2><button class="ui-btn">…</button></div>`.
|
|
138
144
|
Tiêu đề trái, hành động phải, xuống dòng khi hẹp (01/10/2026: partner-radar rel-head / agr). */
|
|
139
145
|
.ui-card__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--ui-space-2); margin-bottom: var(--ui-space-2); }
|
|
140
146
|
.ui-card__head > .ui-card__title { margin: 0; }
|
|
141
147
|
.ui-card__head > :last-child:not(.ui-card__title) { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); }
|
|
142
148
|
|
|
143
|
-
/* ── Select
|
|
149
|
+
/* ── Select ──
|
|
144
150
|
Native <select> reskinned; custom caret as an inline SVG data-URI (caret color
|
|
145
151
|
uses the muted token via %-encoding fallback).
|
|
146
152
|
Its BOX is the text field's (29/09/2026, Chris): same --gemboss-field-* tokens for ground, line and
|
|
147
153
|
ink, same hover, same focus ring as `.gemboss-field`. Before, a grey --ui-surface-2 select stood next
|
|
148
154
|
to a bordered white field in the same form (gempress editor, admin forms), and `:focus` removed the
|
|
149
155
|
outline and only recoloured the border, so a keyboard user saw almost nothing. Height is
|
|
150
|
-
--ui-control-h, the
|
|
156
|
+
--ui-control-h, the field's (Polaris Select, 32): a field, not a button (28, --ui-btn-h). */
|
|
151
157
|
.ui-select {
|
|
152
158
|
height: var(--ui-control-h); padding: 0 32px 0 12px; max-width: 100%;
|
|
153
159
|
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;
|
|
154
160
|
border: 1px solid var(--p-color-input-border, var(--gemboss-field-line)); border-radius: var(--ui-radius-md);
|
|
155
|
-
color: var(--p-color-text, var(--gemboss-field-ink)); font-family: inherit; font-size: var(--ui-fs-body); font-weight:
|
|
161
|
+
color: var(--p-color-text, var(--gemboss-field-ink)); font-family: inherit; font-size: var(--ui-fs-body); font-weight: 450;
|
|
156
162
|
appearance: none; -webkit-appearance: none; cursor: pointer;
|
|
157
163
|
}
|
|
158
164
|
.ui-select:hover:not(:disabled) {
|
|
@@ -167,13 +173,13 @@
|
|
|
167
173
|
.ui-select:disabled { opacity: .5; cursor: not-allowed; }
|
|
168
174
|
.ui-select--slim { height: 30px; font-size: var(--ui-fs-caption); }
|
|
169
175
|
|
|
170
|
-
/* ── Tag / Chip
|
|
176
|
+
/* ── Tag / Chip ──
|
|
171
177
|
Small inline labels. `solid` = filled muted pill (was gemboss-vault-tag); `chip` =
|
|
172
178
|
bordered mono token (was gemboss-vault-chip). `accent` tone for the highlighted state. */
|
|
173
179
|
.ui-tag {
|
|
174
180
|
display: inline-flex; align-items: center; gap: 6px;
|
|
175
|
-
font-size: var(--
|
|
176
|
-
padding:
|
|
181
|
+
font-size: var(--ui-fs-caption); line-height: 20px; font-weight: 450;
|
|
182
|
+
padding: 0 6px; border-radius: var(--ui-radius-md); background: var(--ui-surface-2); color: var(--ui-text-muted);
|
|
177
183
|
}
|
|
178
184
|
.ui-tag--accent { background: var(--adm-accent-soft, rgba(20,22,26,.05)); color: var(--adm-accent-fg, var(--ui-accent)); }
|
|
179
185
|
.ui-tag--chip {
|
|
@@ -207,13 +213,13 @@
|
|
|
207
213
|
|
|
208
214
|
/* ── Chữ phụ: `<p class="ui-muted">`, `<span class="ui-muted">` — mô tả, chú thích, dòng meta. Cỡ caption, màu muted.
|
|
209
215
|
Trước đây mỗi surface tự viết `.muted` (partner-radar: 180 chỗ). */
|
|
210
|
-
.ui-muted { color: var(--ui-text-muted); font-size: var(--
|
|
216
|
+
.ui-muted { color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }
|
|
211
217
|
|
|
212
218
|
/* ── Tiêu đề trang: `<div class="ui-page-head"><h1>…</h1><p>…</p></div>`. Đưa lên NGUYÊN VĂN từ admin
|
|
213
|
-
`.gemboss-admin-page-head` (01/10/2026, Chris: một chủ sở hữu; admin dùng bản này).
|
|
214
|
-
và không dùng flex:
|
|
219
|
+
`.gemboss-admin-page-head` (01/10/2026, Chris: một chủ sở hữu; admin dùng bản này). Không đặt margin cho h1
|
|
220
|
+
và không dùng flex: margin 0 đến từ reset (Polaris ở admin, app-shell.css trong shell của bề mặt từ 0.12.0). */
|
|
215
221
|
.ui-page-head { margin-bottom: 20px; }
|
|
216
|
-
.ui-page-head h1 { color: var(--ui-text-strong); font-size:
|
|
222
|
+
.ui-page-head h1 { color: var(--ui-text-strong); font-size: 20px; line-height: 24px; font-weight: 700; letter-spacing: -0.01em; } /* own line-height: the 20px body line overlapped a wrapped title (0.5.1) */
|
|
217
223
|
.ui-page-head p { color: var(--ui-text-muted); font-size: var(--ui-fs-body); margin-top: 4px; }
|
|
218
224
|
|
|
219
225
|
/* ── 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.
|
|
@@ -231,7 +237,7 @@ a.ui-metric { text-decoration: none; color: inherit; }
|
|
|
231
237
|
a.ui-metric:hover { border-color: var(--ui-text-muted); }
|
|
232
238
|
a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
|
|
233
239
|
.ui-metric__val { font-size: 17px; font-weight: 700; color: var(--ui-text); font-variant-numeric: tabular-nums; line-height: 1.15; }
|
|
234
|
-
.ui-metric__lbl { font-size: var(--
|
|
240
|
+
.ui-metric__lbl { font-size: var(--ui-fs-caption); font-weight: 500; color: var(--ui-text-muted); }
|
|
235
241
|
.ui-metric--success { border-color: color-mix(in srgb, var(--gemboss-success) 40%, transparent); }
|
|
236
242
|
.ui-metric--success .ui-metric__val { color: var(--ui-success-fg); }
|
|
237
243
|
.ui-metric--warning { border-color: color-mix(in srgb, #c99a2e 40%, transparent); }
|
|
@@ -243,7 +249,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
243
249
|
Lưới tự giãn: mỗi ô ≥ 160px, xuống dòng khi hẹp. */
|
|
244
250
|
.ui-dl { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px 16px; margin: 0; }
|
|
245
251
|
.ui-dl > div { min-width: 0; }
|
|
246
|
-
.ui-dl dt { font-size: var(--
|
|
252
|
+
.ui-dl dt { font-size: var(--ui-fs-caption); color: var(--ui-text-muted); }
|
|
247
253
|
.ui-dl dd { margin: 4px 0 0; font-weight: 600; color: var(--ui-text); overflow-wrap: anywhere; }
|
|
248
254
|
|
|
249
255
|
/* ── 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
|
|
@@ -260,22 +266,22 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
260
266
|
.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); }
|
|
261
267
|
.ui-pager > a:hover { background: var(--ui-surface-2); }
|
|
262
268
|
.ui-pager > a:focus-visible { outline: 2px solid var(--p-color-border-focus, var(--ui-focus)); outline-offset: 1px; }
|
|
263
|
-
.ui-pager > [aria-current="page"] { color: var(--ui-text-muted); font-size: var(--
|
|
269
|
+
.ui-pager > [aria-current="page"] { color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }
|
|
264
270
|
|
|
265
|
-
/* ── Badge
|
|
266
|
-
Polaris' Badge (02/10/2026; was an uppercase mono pill): 12px/550, padding 2/8, radius 8, the secondary
|
|
271
|
+
/* ── Badge ──
|
|
272
|
+
Polaris' Badge (02/10/2026; was an uppercase mono pill): 12px/450 (its text span; 550 was the box), padding 2/8, radius 8, the secondary
|
|
267
273
|
fill. The admin's /admin console draws it too (components/ui/Badge.tsx). Tones (`--success|--warning|--critical|
|
|
268
274
|
--info`, `--dot`) are in kit-plain.css, for the non-React surfaces; the seller app uses Polaris <Badge tone>. */
|
|
269
275
|
.ui-badge {
|
|
270
276
|
display: inline-flex; align-items: center; gap: 4px;
|
|
271
|
-
font-size: var(--ui-fs-caption); line-height: 16px; font-weight:
|
|
277
|
+
font-size: var(--ui-fs-caption); line-height: 16px; font-weight: 450; white-space: nowrap;
|
|
272
278
|
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
279
|
} /* 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
280
|
|
|
275
|
-
/* ── Modal
|
|
281
|
+
/* ── Modal ──
|
|
276
282
|
Rendered INLINE (not portaled): a portal escapes the .gemboss-admin-shell token
|
|
277
283
|
scope and would render light. Kept in-tree so --ui-* resolves to the surface.
|
|
278
|
-
0.7.3: Polaris Modal's density (#1099, README "Popup density").
|
|
284
|
+
0.7.3: Polaris Modal's density (#1099, README "Popup density"). Buttons are 28 everywhere since 0.12.0 (--ui-btn-h). */
|
|
279
285
|
.ui-overlay {
|
|
280
286
|
position: fixed; inset: 0; z-index: 500;
|
|
281
287
|
background: rgba(0, 0, 0, 0.5);
|
|
@@ -308,7 +314,6 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
308
314
|
display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
|
|
309
315
|
padding: 12px 16px; border-top: 1px solid var(--ui-border);
|
|
310
316
|
}
|
|
311
|
-
:is(.ui-modal__head, .ui-modal__foot) .ui-btn { height: 28px; padding: 0 12px; font-size: var(--ui-fs-caption); }
|
|
312
317
|
/* 44px under a thumb or at phone width; -8px keeps the head 53. */
|
|
313
318
|
@media (pointer: coarse), (max-width: 47.9975em) {
|
|
314
319
|
.ui-modal__close { width: 44px; height: 44px; margin: -8px -8px -8px 0; }
|
|
@@ -347,15 +352,15 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
347
352
|
.ui-select:not(.ui-select--slim) { min-height: 44px; }
|
|
348
353
|
}
|
|
349
354
|
|
|
350
|
-
/* ── Segmented control
|
|
355
|
+
/* ── Segmented control ──
|
|
351
356
|
Grouped toggle: icon squares (.ui-seg button) or text tabs (.is-text). The
|
|
352
357
|
selected segment uses the accent-soft state. */
|
|
353
358
|
.ui-seg { display: inline-flex; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); overflow: hidden; }
|
|
354
359
|
.ui-seg :is(button, a) {
|
|
355
|
-
display: inline-grid; place-items: center; width: calc(var(--ui-
|
|
360
|
+
display: inline-grid; place-items: center; width: calc(var(--ui-btn-h) - 2px); height: calc(var(--ui-btn-h) - 2px);
|
|
356
361
|
border: 0; background: transparent; color: var(--ui-text-muted); cursor: pointer; font-family: inherit;
|
|
357
362
|
}
|
|
358
|
-
.ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-
|
|
363
|
+
.ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-caption); font-weight: 550; }
|
|
359
364
|
.ui-seg :is(button, a) + :is(button, a) { border-left: 1px solid var(--ui-border); }
|
|
360
365
|
.ui-seg :is(button, a):hover { background: var(--ui-surface-2); color: var(--ui-text); }
|
|
361
366
|
.ui-seg :is(button, a):focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
|
|
@@ -365,7 +370,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
365
370
|
.ui-seg button.active { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
|
|
366
371
|
.ui-seg svg { width: 16px; height: 16px; fill: currentColor; }
|
|
367
372
|
|
|
368
|
-
/* ── Stat chip
|
|
373
|
+
/* ── Stat chip ──
|
|
369
374
|
Count-led pill; clickable ones double as filters (active = accent-soft). */
|
|
370
375
|
.ui-stats { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); margin-bottom: 24px; }
|
|
371
376
|
.ui-stat {
|
|
@@ -380,7 +385,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
380
385
|
.ui-stat.is-clickable.active { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
|
|
381
386
|
.ui-stat.is-clickable.active b { color: var(--ui-accent-strong); }
|
|
382
387
|
|
|
383
|
-
/* ── Toast / notification bar
|
|
388
|
+
/* ── Toast / notification bar ──
|
|
384
389
|
App-wide "done / failed" pill for async actions (delete/duplicate/publish…).
|
|
385
390
|
Polaris-style: dark pill, bottom-center, error variant in critical red. Lives
|
|
386
391
|
in <ToastProvider> (components/ui/Toast.tsx), portaled to <body>. */
|
|
@@ -423,7 +428,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
423
428
|
}
|
|
424
429
|
@media (prefers-reduced-motion: reduce) { .gemboss-toast { animation: none; } }
|
|
425
430
|
|
|
426
|
-
/* ── The kit's controls under a thumb
|
|
431
|
+
/* ── The kit's controls under a thumb ──
|
|
427
432
|
* Measured by hit test on /admin/{theme-kernel,media,style-packs} at 390 and 1366 with a coarse
|
|
428
433
|
* pointer: segmented buttons 36x36, selects 30-36 tall, clickable stat tiles 29, the asset row's
|
|
429
434
|
* note button 30x30. §9.1 puts every one of them in the CONTROL tier (44px).
|
|
@@ -431,9 +436,11 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
431
436
|
* HERE rather than in each surface's stylesheet, because this is the shared primitive layer — the
|
|
432
437
|
* same reason the kit exists at all. The admin console and the seller app both read it.
|
|
433
438
|
*
|
|
434
|
-
* `pointer: coarse` only: a mouse keeps the density these surfaces were
|
|
435
|
-
* for every pointer is what #2147 did to a working width.
|
|
439
|
+
* The block below is `pointer: coarse` only: a mouse at desktop width keeps the density these surfaces were
|
|
440
|
+
* designed at. Growing a box for every pointer is what #2147 did to a working width.
|
|
436
441
|
*/
|
|
442
|
+
/* Phone WIDTH (< 768) with a mouse too (a narrow window): buttons and segments are 44 (0.12.0; at desktop width 28). */
|
|
443
|
+
@media (max-width: 47.9975em) { .ui-btn:not(.ui-btn--sm), .ui-seg :is(button, a) { min-height: 44px; } .ui-seg :is(button, a):not(.is-text) { min-width: 44px; } }
|
|
437
444
|
@media (pointer: coarse) {
|
|
438
445
|
/* NOT `.ui-textarea`: it already declares `min-height: 96px` earlier in this file, so adding it to
|
|
439
446
|
a 44px group LOWERS it — same specificity, later line. The note dialog's box would have gone from
|
|
@@ -460,7 +467,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
460
467
|
.ui-seg :is(button, a) { min-width: 44px; }
|
|
461
468
|
}
|
|
462
469
|
|
|
463
|
-
/* ── Skeleton
|
|
470
|
+
/* ── Skeleton ──
|
|
464
471
|
Vạch xám giữ chỗ cho nội dung chưa về. Ánh sáng chạy qua là gợi ý "đang chờ", không phải trang
|
|
465
472
|
trí — nên nó tắt hẳn với `prefers-reduced-motion`, giữ nguyên vạch tĩnh. */
|
|
466
473
|
.ui-skeleton { display: flex; flex-direction: column; gap: var(--ui-space-2); }
|
|
@@ -481,7 +488,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
481
488
|
|
|
482
489
|
}
|
|
483
490
|
|
|
484
|
-
/* ── Resizer: the seam you drag between two panels
|
|
491
|
+
/* ── Resizer: the seam you drag between two panels ──
|
|
485
492
|
One look for every split in the fleet: the admin's editors (chat | preview) today; made for gemcare's
|
|
486
493
|
inbox columns too (they adopt it at their next ui:sync). The element is the drag target; its GEOMETRY (a flex item, a grid edge, an absolutely
|
|
487
494
|
placed strip) belongs to the surface. While a drag runs the surface puts `ui-resizing` on <body>
|
|
@@ -521,7 +528,7 @@ body.ui-resizing, body.ui-resizing * { cursor: col-resize !important; user-selec
|
|
|
521
528
|
pointer stream is never captured by a cross-document frame (the classic "stuck cursor"). */
|
|
522
529
|
body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999; cursor: col-resize; }
|
|
523
530
|
|
|
524
|
-
/* ── Tabs · Table · Empty state
|
|
531
|
+
/* ── Tabs · Table · Empty state ──
|
|
525
532
|
Plain-CSS primitives for surfaces that are NOT React (gemcare's vanilla dashboard reads this
|
|
526
533
|
file through `npm run export:ui-css`). The React app has Polaris <Tabs>/<DataTable>/<EmptyState>
|
|
527
534
|
for these; nothing in apps/admin uses the classes below, and they restyle no existing component.
|
|
@@ -529,18 +536,18 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
529
536
|
|
|
530
537
|
Tabs: `<div class="ui-tabs" role="tablist"><button role="tab" aria-selected="true">…` — the
|
|
531
538
|
selected state IS `aria-selected`, so what a screen reader announces and what is painted cannot
|
|
532
|
-
disagree.
|
|
533
|
-
|
|
539
|
+
disagree. Polaris Tabs under a mouse (0.12.0): 28px, 12px/550; 8px apart, the §9.1 gap for targets under 44.
|
|
540
|
+
44px under a finger or at phone width (the rule after these). */
|
|
534
541
|
.ui-tabs {
|
|
535
|
-
display: flex; gap: var(--ui-space-
|
|
542
|
+
display: flex; gap: var(--ui-space-2); border-bottom: 1px solid var(--ui-border);
|
|
536
543
|
overflow-x: auto; scrollbar-width: none;
|
|
537
544
|
}
|
|
538
545
|
.ui-tabs > button,
|
|
539
546
|
.ui-tabs > a {
|
|
540
547
|
position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
|
|
541
|
-
min-height:
|
|
548
|
+
min-height: var(--ui-btn-h); padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
|
|
542
549
|
border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;
|
|
543
|
-
font-family: inherit; font-size: var(--ui-fs-
|
|
550
|
+
font-family: inherit; font-size: var(--ui-fs-caption); font-weight: 550; white-space: nowrap;
|
|
544
551
|
color: var(--ui-text-muted);
|
|
545
552
|
transition: color var(--gemboss-transition-fast) ease, background-color var(--gemboss-transition-fast) ease;
|
|
546
553
|
}
|
|
@@ -551,13 +558,14 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
551
558
|
.ui-tabs > a { text-decoration: none; }
|
|
552
559
|
/* Tab bằng link (mỗi tab là một URL): `<nav class="ui-tabs"><a aria-current="page">` — cùng hình với tab nút. */
|
|
553
560
|
.ui-tabs > a[aria-current="page"],
|
|
554
|
-
.ui-tabs > button[aria-selected="true"] { color: var(--ui-text-strong);
|
|
561
|
+
.ui-tabs > button[aria-selected="true"] { color: var(--ui-text-strong); }
|
|
555
562
|
.ui-tabs > a[aria-current="page"]::after,
|
|
556
563
|
.ui-tabs > button[aria-selected="true"]::after {
|
|
557
564
|
content: ""; position: absolute; left: var(--ui-space-3); right: var(--ui-space-3); bottom: 0;
|
|
558
565
|
height: 2px; border-radius: var(--gemboss-radius-full); background: var(--ui-accent);
|
|
559
566
|
}
|
|
560
567
|
.ui-tabs > button:disabled { opacity: .5; cursor: not-allowed; }
|
|
568
|
+
@media (pointer: coarse), (max-width: 47.9975em) { .ui-tabs > :is(button, a) { min-height: 44px; } }
|
|
561
569
|
|
|
562
570
|
/* Table: wrap it — `<div class="ui-table-wrap"><table class="ui-table">` — so a wide table scrolls
|
|
563
571
|
inside its card instead of pushing the page sideways at 320px. Numeric cells take `.is-num`:
|
|
@@ -565,16 +573,21 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
565
573
|
.ui-table-wrap {
|
|
566
574
|
overflow-x: auto; max-width: 100%;
|
|
567
575
|
background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
|
|
576
|
+
box-shadow: var(--ui-card-shadow);
|
|
568
577
|
}
|
|
569
578
|
.ui-table { width: 100%; border-collapse: collapse; font-size: var(--ui-fs-body); color: var(--ui-text); }
|
|
570
579
|
.ui-table th,
|
|
571
580
|
.ui-table td {
|
|
572
|
-
padding: var(--ui-space-2)
|
|
581
|
+
padding: var(--ui-space-2) 6px; text-align: left; vertical-align: middle;
|
|
573
582
|
border-bottom: 1px solid var(--ui-border);
|
|
574
583
|
}
|
|
575
|
-
.
|
|
576
|
-
|
|
577
|
-
}
|
|
584
|
+
/* Polaris DataTable (13.9.5, measured on /app/analytics 05/10/2026): cells 8px 6px (a row 36), header 12px 6px (45),
|
|
585
|
+
the first column 12px in from the edge. */
|
|
586
|
+
/* :where() keeps the edge columns at the base rule's weight, so a surface's own `.x-table td { padding }` still wins. */
|
|
587
|
+
.ui-table :is(th, td):where(:first-child) { padding-left: var(--ui-space-3); }
|
|
588
|
+
.ui-table :is(th, td):where(:last-child) { padding-right: var(--ui-space-3); }
|
|
589
|
+
.ui-table th { font-weight: 450; color: var(--ui-text-muted); white-space: nowrap; }
|
|
590
|
+
.ui-table thead th { padding-block: var(--ui-space-3); } /* the column header; a row header (tbody th) is a cell */
|
|
578
591
|
.ui-table tbody tr:last-child > td { border-bottom: 0; }
|
|
579
592
|
.ui-table tbody tr:hover { background: var(--ui-surface-2); }
|
|
580
593
|
.ui-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
@@ -583,7 +596,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
583
596
|
/* Link nằm thẳng trong ô (tên bản ghi dẫn sang trang chi tiết) là link trong DANH SÁCH: ≥ 24px (WCAG 2.5.8, §9.1).
|
|
584
597
|
Chữ 13px cho hộp 17px; visual-gate đo đỏ ở partner-radar /competitors 01/10/2026. inline-flex giữ nó trên dòng. */
|
|
585
598
|
.ui-table td > a { display: inline-flex; align-items: center; min-height: 24px; }
|
|
586
|
-
.ui-table .ui-sub { display: block; margin-top: 2px; font-size: var(--
|
|
599
|
+
.ui-table .ui-sub { display: block; margin-top: 2px; font-size: var(--ui-fs-caption); color: var(--ui-text-muted); font-weight: 400; }
|
|
587
600
|
|
|
588
601
|
/* Empty state: `<div class="ui-empty"><p class="ui-empty__title">…</p><p class="ui-empty__body">…</p>
|
|
589
602
|
<button class="ui-btn">…</button></div>`. Says what is missing and what to do next. */
|
|
@@ -592,6 +605,6 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
592
605
|
padding: var(--ui-space-4); padding-block: 40px; text-align: center;
|
|
593
606
|
color: var(--ui-text-muted); font-size: var(--ui-fs-body);
|
|
594
607
|
}
|
|
595
|
-
.ui-empty__title { margin: 0; font-size:
|
|
608
|
+
.ui-empty__title { margin: 0; font-size: 14px; line-height: 20px; font-weight: 650; color: var(--ui-text-strong); }
|
|
596
609
|
.ui-empty__body { margin: 0; max-width: 48ch; line-height: 1.5; }
|
|
597
610
|
.ui-empty > .ui-btn { margin-top: var(--ui-space-2); }
|