@gemboss/ui 0.12.0 → 0.13.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 +33 -0
- package/README.md +74 -0
- package/catalog/index.html +2138 -10
- package/gemboss-ui.css +19 -3
- package/package.json +2 -1
- package/src/collections.css +350 -0
- package/src/collections.tsx +414 -0
- package/src/index-table.tsx +216 -0
- package/src/index.ts +11 -0
- package/src/kit-plain.css +18 -2
- package/src/kit-plain.js +136 -1
package/gemboss-ui.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* GemBoss admin design system — GENERATED — do not edit; regenerate from gemboss:
|
|
3
3
|
* node apps/admin/scripts/export-ui-css.mjs --out <path> (npm run export:ui-css)
|
|
4
|
-
* gemboss commit:
|
|
4
|
+
* gemboss commit: 4fa51b50e62110c020ab1052fada16e025665f4a
|
|
5
5
|
* sources sha256: 9b0060f874885bf4ba4ab9dc6448bc06ae386328bb9bfebcf23bfc6c2671561d
|
|
6
6
|
* contents: 1 allow-listed rule from apps/admin/src/app/globals.css + packages/ui/src/gemboss-tokens.css
|
|
7
7
|
* (verbatim) + packages/ui/src/ui.css (verbatim) + packages/ui/src/gemboss-field.css (verbatim)
|
|
@@ -3541,7 +3541,23 @@ html[data-gb-navigating] .gb-route-cover {
|
|
|
3541
3541
|
.ui-link:focus-visible,
|
|
3542
3542
|
.ui-prose :where(a:not([class])):focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 2px; border-radius: 2px; }
|
|
3543
3543
|
|
|
3544
|
-
.ui-card--media { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
3544
|
+
.ui-card--media { position: relative; padding: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
3545
|
+
/* 0.13.0 (README "Collections", card grid): the title is the card's link and its overlay covers the card
|
|
3546
|
+
(`a.ui-card__link`; the picture link is aria-hidden, out of the tab order); `.ui-card__menu` (a ⋯ ui-menu) sits in the
|
|
3547
|
+
picture's corner above it, and the card stops clipping so the open menu is not cut off; `data-ratio="4/3"` for a theme
|
|
3548
|
+
or a page. */
|
|
3549
|
+
.ui-card__link { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; color: var(--ui-text); font-size: var(--ui-fs-body); line-height: 20px; font-weight: 650; text-decoration: none; overflow-wrap: anywhere; }
|
|
3550
|
+
.ui-card__link::after { content: ""; position: absolute; inset: 0; z-index: 0; }
|
|
3551
|
+
.ui-card__link:focus-visible { outline: none; }
|
|
3552
|
+
.ui-card--media:has(.ui-card__link) { cursor: pointer; }
|
|
3553
|
+
.ui-card--media:has(.ui-card__link:hover) { border-color: var(--ui-text-muted); }
|
|
3554
|
+
.ui-card--media:has(.ui-card__link:focus-visible) { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
|
|
3555
|
+
.ui-card--media :is(.ui-card__foot button, .ui-card__foot a, .ui-card__body a:not(.ui-card__link), .ui-card__body button, select, input, summary) { position: relative; z-index: 1; }
|
|
3556
|
+
.ui-card__menu { position: absolute; top: 8px; right: 8px; z-index: 2; }
|
|
3557
|
+
.ui-card--media:has(> .ui-card__menu) { overflow: visible; }
|
|
3558
|
+
.ui-card--media:has(> .ui-card__menu) > .ui-card__media { border-radius: inherit; border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
|
3559
|
+
.ui-card__meta { color: var(--ui-text-muted); font-size: var(--ui-fs-caption); line-height: 16px; }
|
|
3560
|
+
.ui-card--media[data-ratio="4/3"] > .ui-card__media { aspect-ratio: 4 / 3; }
|
|
3545
3561
|
.ui-card__media {
|
|
3546
3562
|
display: block; width: 100%; aspect-ratio: 16 / 10; margin: 0; padding: 0; border: 0;
|
|
3547
3563
|
border-bottom: 1px solid var(--ui-border); background: var(--ui-surface-2); overflow: hidden; color: inherit;
|
|
@@ -3928,7 +3944,7 @@ button.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-off
|
|
|
3928
3944
|
}
|
|
3929
3945
|
.ui-chip:hover { border-style: solid; background: var(--ui-surface-2); }
|
|
3930
3946
|
.ui-chip:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
|
|
3931
|
-
.ui-chip:is([aria-pressed="true"], [aria-current="page"]) { border-style: solid; border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
|
|
3947
|
+
.ui-chip:is([aria-pressed="true"], [aria-current="page"], [aria-current="true"]) { border-style: solid; border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
|
|
3932
3948
|
@media (pointer: coarse), (max-width: 47.9975em) { .ui-chip { height: auto; min-height: 44px; min-width: 44px; } }
|
|
3933
3949
|
/* Card stack: `<div class="ui-stack"><div class="ui-card">…</div><div class="ui-card">…</div></div>`. 16px between blocks,
|
|
3934
3950
|
Polaris BlockStack gap 400, the admin's spacing between cards on a page. `--tight` is 8 (inside a card). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemboss/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "GemBoss design system for every surface in the fleet: the admin shell (topbar + rail + drawer) as React components, and the admin stylesheet exported as one CSS file.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"./chat.css": "./src/chat.css",
|
|
22
22
|
"./chat-composer.css": "./src/chat-composer.css",
|
|
23
23
|
"./editor.css": "./src/editor.css",
|
|
24
|
+
"./collections.css": "./src/collections.css",
|
|
24
25
|
"./auth.css": "./src/auth.css",
|
|
25
26
|
"./kit-plain.css": "./src/kit-plain.css",
|
|
26
27
|
"./kit-plain.js": "./src/kit-plain.js",
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/* collections.css (0.13.0): tables, lists, grids and boards, the six patterns Chris settled on 05/10/2026.
|
|
2
|
+
README "Collections" has the decision tree (which pattern when) and the markup of each.
|
|
3
|
+
|
|
4
|
+
Its OWN entry (`@gemboss/ui/collections.css`), loaded beside gemboss-ui.css, for two measured reasons:
|
|
5
|
+
- the admin loads ui.css from its root layout, so every rule there is paid on every admin screen, under a first-load
|
|
6
|
+
budget with no room (kit-plain.css says the same, 01/10/2026). The admin draws its tables with Polaris; nothing
|
|
7
|
+
here is for it.
|
|
8
|
+
- gemboss-ui.css had ~1.4 KB left under the 90 KB the export is held to (05/10/2026); these rules are several times
|
|
9
|
+
that, and a page with no table, list or board should not download them.
|
|
10
|
+
|
|
11
|
+
One owner per component (Gems/CLAUDE.md 9.3): `.ui-table` keeps its base in ui.css (it IS the Data table, Polaris
|
|
12
|
+
DataTable's numbers since 0.12.0); this file owns every modifier and part named here, and nothing else.
|
|
13
|
+
|
|
14
|
+
Numbers are Polaris 13.9.5 rendered in Chromium (Gems/_local/fleet/collections-2026-10/polaris-measured.json) and
|
|
15
|
+
pinned in polaris-reference.json, which src/density.test.ts measures on the catalog.
|
|
16
|
+
|
|
17
|
+
Not here, on purpose:
|
|
18
|
+
- zebra rows. Polaris paints even rows #f7f7f7, the exact colour of its hover, so hover vanishes on half the rows
|
|
19
|
+
(measured); the fleet used it 0 times. collections-gate.mjs reports a striped table.
|
|
20
|
+
- row actions that appear on hover. A finger and a keyboard never see them. One button may stay visible, the rest go
|
|
21
|
+
in the ⋯ menu (`ui-menu`).
|
|
22
|
+
- an uppercase header (content-radar `.gtable`, 11/700): the header is 12/550 (index) or 13/450 (data). */
|
|
23
|
+
|
|
24
|
+
/* ── Shared tones, derived from the surface's own ink and paper so the dark theme follows. ──
|
|
25
|
+
Mixed with --ui-text (#1b1c1e): 3.5% gives rgb(247,247,247) on white = Polaris bg-surface-secondary (header, hover);
|
|
26
|
+
6% gives rgb(241,241,241) = Polaris bg-surface-secondary-selected (a selected row). */
|
|
27
|
+
:root {
|
|
28
|
+
--ui-coll-head-bg: color-mix(in srgb, var(--ui-text, #1b1c1e) 3.5%, var(--ui-surface, #fff));
|
|
29
|
+
--ui-coll-hover: color-mix(in srgb, var(--ui-text, #1b1c1e) 3.5%, var(--ui-surface, #fff));
|
|
30
|
+
--ui-coll-selected: color-mix(in srgb, var(--ui-text, #1b1c1e) 6%, var(--ui-surface, #fff));
|
|
31
|
+
--ui-coll-check-w: 40px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ══ 1 · Data table: `ui-table` (ui.css) + modifiers ═════════════════════════════════════════════════════════════════
|
|
35
|
+
`<div class="ui-table-wrap"><table class="ui-table ui-table--sticky-first">…</table></div>`.
|
|
36
|
+
- `--sticky-first`: the row's label column stays put while the wrap scrolls sideways (below 768 a data table SCROLLS,
|
|
37
|
+
it never folds into a list; Chris 05/10/2026). An edge shadow appears once it has scrolled.
|
|
38
|
+
- `--compact`: Polaris `increasedTableDensity` (rows 32, header 33), only for a table of more than 20 rows.
|
|
39
|
+
- `tfoot.ui-table__totals` (650 on the secondary fill) and `.ui-table__footer` (source, "x of n") under the table. */
|
|
40
|
+
.ui-table--sticky-first tr { background: var(--ui-surface); }
|
|
41
|
+
.ui-table--sticky-first tbody tr:hover { background: var(--ui-surface-2); }
|
|
42
|
+
.ui-table--sticky-first :is(th, td):first-child { position: sticky; left: 0; z-index: 1; background: inherit; }
|
|
43
|
+
@supports (animation-timeline: scroll()) {
|
|
44
|
+
.ui-table-wrap:has(> .ui-table--sticky-first) { scroll-timeline: --ui-coll-x inline; }
|
|
45
|
+
.ui-table--sticky-first :is(th, td):first-child { animation: ui-coll-edge linear both; animation-timeline: --ui-coll-x; animation-range: 0 24px; }
|
|
46
|
+
}
|
|
47
|
+
@keyframes ui-coll-edge { from { box-shadow: none; } to { box-shadow: 1px 0 0 var(--ui-border), 6px 0 8px -6px rgba(0, 0, 0, .18); } }
|
|
48
|
+
.ui-table--compact :is(td, tbody th) { padding-block: 6px; }
|
|
49
|
+
.ui-table--compact thead th { padding-block: 6px; }
|
|
50
|
+
.ui-table__totals > tr { background: var(--ui-coll-head-bg); }
|
|
51
|
+
.ui-table__totals > tr > :is(td, th) { background: var(--ui-coll-head-bg); font-weight: 650; color: var(--ui-text); border-top: 1px solid var(--ui-border); border-bottom: 0; }
|
|
52
|
+
.ui-table__footer { padding: 8px 12px; background: var(--ui-coll-head-bg); border-top: 1px solid var(--ui-border); color: var(--ui-text-muted); font-size: var(--ui-fs-body); text-align: center; }
|
|
53
|
+
/* A row header (`th scope=row class=ui-table__rowhead`) reads as a cell, as in Polaris DataTable's first column. A part of
|
|
54
|
+
its own: ui.css keeps every other `th` muted. */
|
|
55
|
+
.ui-table th.ui-table__rowhead { font-weight: 450; color: var(--ui-text); text-align: left; }
|
|
56
|
+
|
|
57
|
+
/* ── Sort, both table kinds: `<th aria-sort="ascending"><a class="ui-table__sort" href="?sort=name&dir=desc">Name</a></th>`
|
|
58
|
+
(the order is in the URL: Back and a shared link keep it), or `<button type="button" class="ui-table__sort">` for a
|
|
59
|
+
client sort (kit-plain.js `data-gb-sort="client"`, or React `onSort`). `aria-sort` sits on the th: none on a sortable
|
|
60
|
+
column that is not the current one. One column at a time. The arrow is 16px; the target is 24px tall inside the 36px
|
|
61
|
+
header (WCAG 2.5.8), 44 under a finger. */
|
|
62
|
+
.ui-table__sort {
|
|
63
|
+
/* A block flex box, not inline: a 24px inline box with negative margins still grew the header's line box (45 → 46, 36 → 40). */
|
|
64
|
+
display: flex; width: max-content; max-width: 100%; align-items: center; gap: 4px; min-height: 24px; margin-block: -2px; padding: 0; border: 0;
|
|
65
|
+
background: none; color: inherit; font: inherit; text-align: inherit; text-decoration: none; cursor: pointer; white-space: nowrap;
|
|
66
|
+
}
|
|
67
|
+
.ui-table__sort:hover { color: var(--ui-text); }
|
|
68
|
+
.ui-table__sort:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; border-radius: 4px; }
|
|
69
|
+
.ui-table__sort::after {
|
|
70
|
+
content: ""; flex: none; width: 16px; height: 16px; background: currentColor; opacity: 0;
|
|
71
|
+
-webkit-mask: var(--ui-coll-arrow) center / 16px no-repeat; mask: var(--ui-coll-arrow) center / 16px no-repeat;
|
|
72
|
+
--ui-coll-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 3.5v9M4.5 9 8 12.5 11.5 9' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
73
|
+
}
|
|
74
|
+
.ui-table__sort:is(:hover, :focus-visible)::after { opacity: .45; }
|
|
75
|
+
th[aria-sort="ascending"] > .ui-table__sort::after, th[aria-sort="descending"] > .ui-table__sort::after { opacity: 1; }
|
|
76
|
+
th[aria-sort="ascending"] > .ui-table__sort::after { transform: rotate(180deg); }
|
|
77
|
+
th[aria-sort="ascending"] > .ui-table__sort, th[aria-sort="descending"] > .ui-table__sort { color: var(--ui-text); }
|
|
78
|
+
.ui-table .is-num > .ui-table__sort { flex-direction: row-reverse; margin-left: auto; }
|
|
79
|
+
|
|
80
|
+
/* ── Long content (both table kinds and the lists): one line with an ellipsis, the whole text in `title` or on the
|
|
81
|
+
detail page. `ui-trunc` caps a cell's text at --ui-trunc (default 32ch); `ui-clamp-2` allows two lines. */
|
|
82
|
+
.ui-trunc { display: block; max-width: var(--ui-trunc, 32ch); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
83
|
+
.ui-clamp-2 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; white-space: normal; overflow-wrap: anywhere; }
|
|
84
|
+
|
|
85
|
+
/* ══ 2 · Index table: `ui-index > ui-table-wrap > table.ui-table.ui-table--index` ═══════════════════════════════════
|
|
86
|
+
Polaris IndexTable: header 12/550 on the secondary fill, 36 tall; cells 6px 6px, a one-line row 34 with a 20px badge.
|
|
87
|
+
Row: `tr` with `data-href` (kit-plain.js) or `.is-linked` (React) = the WHOLE row opens the detail; the title stays a
|
|
88
|
+
real link, `<a class="ui-table__title" href>`, for Cmd-click and screen readers. A row with two or more in-place
|
|
89
|
+
editing controls is not linked (only its title is; collections-gate.mjs reports the other case).
|
|
90
|
+
Cells stay on one line (ellipsis); the primary cell may add one `ui-sub` line, so a row is 34 to 56, never 119. */
|
|
91
|
+
.ui-index { position: relative; container: ui-index / inline-size; min-width: 0; }
|
|
92
|
+
.ui-table--index { font-size: var(--ui-fs-body); }
|
|
93
|
+
.ui-table--index tr { background: var(--ui-surface); }
|
|
94
|
+
.ui-table.ui-table--index thead th {
|
|
95
|
+
padding: 8px 6px; background: var(--ui-coll-head-bg); color: var(--ui-text-muted);
|
|
96
|
+
font-size: var(--ui-fs-caption); line-height: 20px; font-weight: 550; text-transform: none; letter-spacing: 0;
|
|
97
|
+
}
|
|
98
|
+
.ui-table.ui-table--index :is(td, tbody th) { padding: 6px; line-height: 20px; white-space: nowrap; vertical-align: middle; border-bottom: 0; border-top: 1px solid var(--ui-border); }
|
|
99
|
+
.ui-table.ui-table--index :is(th, td):where(:first-child) { padding-left: var(--ui-space-3); }
|
|
100
|
+
.ui-table.ui-table--index :is(th, td):where(:last-child) { padding-right: var(--ui-space-3); }
|
|
101
|
+
.ui-table--index tbody tr:hover { background: var(--ui-coll-hover); }
|
|
102
|
+
.ui-table--index tbody tr:is(.is-selected, [aria-selected="true"]) { background: var(--ui-coll-selected); }
|
|
103
|
+
.ui-table--index tbody tr:is([data-href], .is-linked) { cursor: pointer; }
|
|
104
|
+
/* The row is the target: its focus ring is the row's, drawn when the title link has keyboard focus. */
|
|
105
|
+
.ui-table--index tbody tr:has(.ui-table__title:focus-visible) { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
|
|
106
|
+
.ui-table__title:focus-visible { outline: none; }
|
|
107
|
+
.ui-table__primary { min-width: 0; }
|
|
108
|
+
.ui-table__primary-inner { display: flex; align-items: center; gap: var(--ui-space-3); min-width: 0; }
|
|
109
|
+
.ui-table__primary-text { min-width: 0; }
|
|
110
|
+
.ui-table__title {
|
|
111
|
+
display: block; max-width: var(--ui-trunc, 40ch); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
112
|
+
color: var(--ui-text); font-weight: 650; text-decoration: none;
|
|
113
|
+
}
|
|
114
|
+
a.ui-table__title:hover { text-decoration: underline; }
|
|
115
|
+
.ui-table--index .ui-table__title + .ui-sub, .ui-table--index .ui-sub { display: block; max-width: var(--ui-trunc, 40ch); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 0; line-height: 16px; }
|
|
116
|
+
/* `ui-table td > a` (ui.css) makes a cell link inline-flex 24px; the title is a block with an ellipsis instead. */
|
|
117
|
+
.ui-table--index td > a.ui-table__title { display: block; min-height: 0; }
|
|
118
|
+
.ui-table--index td > .ui-table__primary-inner > a.ui-table__title, .ui-table--index .ui-table__primary-text > a.ui-table__title { line-height: 20px; }
|
|
119
|
+
|
|
120
|
+
/* Checkbox column: 40px, sticky at the left edge, like the label column after it while the wrap scrolls. */
|
|
121
|
+
/* Polaris: 8 8 8 12 in the header, 6 8 6 12 in a row. */
|
|
122
|
+
.ui-table.ui-table--index .ui-table__check { width: var(--ui-coll-check-w); padding: 6px 8px 6px 12px; }
|
|
123
|
+
.ui-table.ui-table--index thead .ui-table__check { padding: 8px 8px 8px 12px; }
|
|
124
|
+
.ui-table__checkbox { display: flex; width: max-content; align-items: center; justify-content: center; min-width: 24px; min-height: 24px; margin: -2px 0; cursor: pointer; }
|
|
125
|
+
.ui-table__checkbox > input { width: 16px; height: 16px; margin: 0; accent-color: var(--ui-accent); cursor: pointer; }
|
|
126
|
+
.ui-table__checkbox > input:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
|
|
127
|
+
.ui-table--index :is(.ui-table__check, .ui-table__primary) { position: sticky; z-index: 1; background: inherit; }
|
|
128
|
+
.ui-table--index .ui-table__check { left: 0; }
|
|
129
|
+
.ui-table--index .ui-table__primary { left: 0; }
|
|
130
|
+
.ui-table--index:has(.ui-table__check) .ui-table__primary { left: var(--ui-coll-check-w); }
|
|
131
|
+
.ui-table__actions { width: 1%; text-align: right; }
|
|
132
|
+
.ui-table__actions > * { vertical-align: middle; }
|
|
133
|
+
.ui-row-actions { display: inline-flex; align-items: center; gap: var(--ui-space-2); }
|
|
134
|
+
|
|
135
|
+
/* Bulk bar: replaces the header row while rows are selected (Polaris: 36px, in the header's place), so selecting the
|
|
136
|
+
first row does not move the table. 38 here = the wrap's top line + the 37px header; padding 4 (Polaris 6) because the
|
|
137
|
+
kit's small button is 28 tall. `<div class="ui-bulkbar" hidden>` right before the wrap; kit-plain.js / React show
|
|
138
|
+
it. It carries its own select-all box, because the folded list below 768 has no header. */
|
|
139
|
+
.ui-bulkbar {
|
|
140
|
+
position: relative; z-index: 3; display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2);
|
|
141
|
+
box-sizing: border-box; min-height: 38px; margin-bottom: -38px; padding: 4px 8px 4px 12px;
|
|
142
|
+
background: var(--ui-surface); border: 1px solid var(--ui-border); border-bottom-color: transparent; border-radius: var(--ui-radius-lg) var(--ui-radius-lg) 0 0;
|
|
143
|
+
font-size: var(--ui-fs-body);
|
|
144
|
+
}
|
|
145
|
+
.ui-bulkbar[hidden] { display: none; }
|
|
146
|
+
.ui-bulkbar__count { font-weight: 550; font-variant-numeric: tabular-nums; }
|
|
147
|
+
.ui-bulkbar__actions { display: inline-flex; flex-wrap: wrap; gap: var(--ui-space-2); margin-left: auto; }
|
|
148
|
+
.ui-bulkbar .ui-table__checkbox { margin-right: 4px; }
|
|
149
|
+
|
|
150
|
+
/* Sort below 768: the header is gone, so a row of chips (`ui-chip`, aria-current on the active one) takes its place. */
|
|
151
|
+
.ui-index__sortbar { display: none; }
|
|
152
|
+
|
|
153
|
+
/* ── Folded: the index table becomes a list when ITS CONTAINER is 768 or narrower (Chris 05/10/2026). A container,
|
|
154
|
+
not the viewport: right of a 240px rail a 1024px window leaves ~750px. The primary cell is the title line; the other
|
|
155
|
+
cells follow on one wrapped line in caption size (`data-label` prefixes a value with its column name); a cell marked
|
|
156
|
+
`data-condensed="hide"` is left out; checkbox and actions keep their places. Polaris `condensed`: rows 44 and up. */
|
|
157
|
+
@container ui-index (max-width: 768px) {
|
|
158
|
+
.ui-table--index, .ui-table--index tbody { display: block; width: 100%; }
|
|
159
|
+
.ui-table--index thead { display: none; }
|
|
160
|
+
.ui-table--index tbody tr {
|
|
161
|
+
position: relative; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px;
|
|
162
|
+
min-height: 44px; box-sizing: border-box; padding: 10px 12px; border-top: 1px solid var(--ui-border);
|
|
163
|
+
}
|
|
164
|
+
.ui-table--index tbody tr:first-child { border-top: 0; }
|
|
165
|
+
.ui-table--index tbody tr:has(> .ui-table__check) { padding-left: 48px; }
|
|
166
|
+
.ui-table.ui-table--index tbody :is(td, th) { display: block; padding: 0; border: 0; position: static; background: none; white-space: nowrap; }
|
|
167
|
+
.ui-table--index tbody .ui-table__primary { flex: 1 0 100%; white-space: normal; }
|
|
168
|
+
.ui-table--index tbody td:not(.ui-table__primary, .ui-table__check, .ui-table__actions) { flex: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis; color: var(--ui-text-muted); font-size: var(--ui-fs-caption); line-height: 18px; }
|
|
169
|
+
.ui-table--index tbody td[data-label]:not(.ui-table__primary)::before { content: attr(data-label) " "; color: var(--ui-text-muted); }
|
|
170
|
+
/* A capped value or a second line in a folded cell stays on the caption line (block, they broke "In" from its value). */
|
|
171
|
+
.ui-table--index tbody td:not(.ui-table__primary, .ui-table__check, .ui-table__actions) .ui-trunc { display: inline-block; vertical-align: bottom; }
|
|
172
|
+
.ui-table--index tbody td:not(.ui-table__primary, .ui-table__check, .ui-table__actions) .ui-sub { display: inline; margin-left: 6px; }
|
|
173
|
+
.ui-table--index tbody td.is-num { text-align: left; }
|
|
174
|
+
.ui-table--index tbody td[data-condensed="hide"] { display: none; }
|
|
175
|
+
.ui-table.ui-table--index tbody .ui-table__check { position: absolute; left: 4px; top: 4px; width: auto; padding: 0; }
|
|
176
|
+
/* Actions follow the second line, pushed right (absolute over the title, they covered it at 390). */
|
|
177
|
+
.ui-table.ui-table--index tbody .ui-table__actions { order: 99; margin-left: auto; width: auto; padding: 0; align-self: center; }
|
|
178
|
+
.ui-table--index .ui-table__checkbox { min-width: 44px; min-height: 44px; margin: 0; }
|
|
179
|
+
.ui-table--index .ui-table__title, .ui-table--index .ui-sub { max-width: none; }
|
|
180
|
+
.ui-index__sortbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2); margin: 0 0 var(--ui-space-3); font-size: var(--ui-fs-caption); color: var(--ui-text-muted); }
|
|
181
|
+
.ui-index .ui-bulkbar { margin-bottom: var(--ui-space-2); border-bottom-color: var(--ui-border); border-radius: var(--ui-radius-lg); }
|
|
182
|
+
.ui-index .ui-table-wrap { overflow-x: visible; }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ══ 3 · Resource list: `ul.ui-list > li.ui-list__item` ════════════════════════════════════════════════════════════
|
|
186
|
+
Polaris ResourceItem: 12 on every side, 12 between media and text, two 20px lines = a 64px row. The whole row is the
|
|
187
|
+
title's link (an overlay on `a.ui-list__title`, as ResourceItem's own); every other control in the row sits above it.
|
|
188
|
+
Slots: `__media` (Avatar 24/32/40 or `ui-thumb`, one size per list) · `__body` > `__title` (one line) + `__sub`
|
|
189
|
+
(two lines at most) + `__meta` (12px) + `__badges` (three, then "+N") · `__aside` (a score, a count) · `__actions`
|
|
190
|
+
(one button + ⋯). Never a `ui-card` per row: the list is the card. */
|
|
191
|
+
.ui-list {
|
|
192
|
+
container: ui-list / inline-size; margin: 0; padding: 0; list-style: none;
|
|
193
|
+
background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg); box-shadow: var(--ui-card-shadow);
|
|
194
|
+
}
|
|
195
|
+
.ui-list--flush { border: 0; border-radius: 0; box-shadow: none; background: transparent; }
|
|
196
|
+
.ui-list__item {
|
|
197
|
+
position: relative; display: flex; align-items: flex-start; gap: var(--ui-space-3);
|
|
198
|
+
box-sizing: border-box; padding: var(--ui-space-3); min-height: 44px; border-top: 1px solid var(--ui-border);
|
|
199
|
+
}
|
|
200
|
+
.ui-list > .ui-list__item:first-child { border-top: 0; }
|
|
201
|
+
.ui-list > .ui-list__item:first-child { border-top-left-radius: inherit; border-top-right-radius: inherit; }
|
|
202
|
+
.ui-list > .ui-list__item:last-child { border-bottom-left-radius: inherit; border-bottom-right-radius: inherit; }
|
|
203
|
+
.ui-list__item:has(.ui-list__title[href]):hover { background: var(--ui-coll-hover); }
|
|
204
|
+
.ui-list__item:is(.is-selected, [aria-selected="true"]) { background: var(--ui-coll-selected); }
|
|
205
|
+
.ui-list__item:has(.ui-list__title:focus-visible) { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
|
|
206
|
+
.ui-list__media { flex: none; display: flex; align-items: center; min-height: 40px; }
|
|
207
|
+
.ui-list__body { flex: 1 1 auto; min-width: 0; }
|
|
208
|
+
.ui-list__title {
|
|
209
|
+
display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
210
|
+
color: var(--ui-text); font-size: var(--ui-fs-body); line-height: 20px; font-weight: 650; text-decoration: none;
|
|
211
|
+
}
|
|
212
|
+
.ui-list__title[href]::after { content: ""; position: absolute; inset: 0; z-index: 0; }
|
|
213
|
+
.ui-list__title:focus-visible { outline: none; }
|
|
214
|
+
.ui-list__sub { margin: 0; color: var(--ui-text-muted); font-size: var(--ui-fs-body); line-height: 20px; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; overflow-wrap: anywhere; }
|
|
215
|
+
.ui-list__meta { margin: 2px 0 0; color: var(--ui-text-muted); font-size: var(--ui-fs-caption); line-height: 16px; }
|
|
216
|
+
.ui-list__badges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
|
|
217
|
+
.ui-list__aside { flex: none; align-self: center; color: var(--ui-text); font-variant-numeric: tabular-nums; text-align: right; }
|
|
218
|
+
.ui-list__actions { flex: none; align-self: center; display: inline-flex; align-items: center; gap: var(--ui-space-2); }
|
|
219
|
+
/* Every control in a row is above the row's link: a checkbox, a button, the ⋯ menu, a second link. */
|
|
220
|
+
.ui-list__item :is(.ui-list__actions, .ui-list__aside a, .ui-list__body a:not(.ui-list__title), button, input, select, textarea, label, summary, details) { position: relative; z-index: 1; }
|
|
221
|
+
@container ui-list (max-width: 400px) {
|
|
222
|
+
.ui-list__item { flex-wrap: wrap; }
|
|
223
|
+
.ui-list__aside { order: 3; flex: 1 0 100%; text-align: left; }
|
|
224
|
+
}
|
|
225
|
+
/* Thumbnail (Polaris Thumbnail): 24 · 40 (default) · 60 · 80, radius 8 (6 at 24), a 1px line, cropped to fill. */
|
|
226
|
+
.ui-thumb { display: block; flex: none; width: 40px; height: 40px; box-sizing: border-box; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); background: var(--ui-surface-2); overflow: hidden; object-fit: cover; }
|
|
227
|
+
.ui-thumb--xs { width: 24px; height: 24px; border-radius: var(--ui-radius-sm); }
|
|
228
|
+
.ui-thumb--md { width: 60px; height: 60px; }
|
|
229
|
+
.ui-thumb--lg { width: 80px; height: 80px; }
|
|
230
|
+
.ui-thumb > img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
231
|
+
|
|
232
|
+
/* ══ 4 · Compact list: `ul.ui-list.ui-list--compact > li.ui-list__item > a|button|div.ui-list__row` ══════════════════
|
|
233
|
+
One line per item: an icon or dot (16-20) · the label (ellipsis) · meta on the right (a time, a count) · a chevron or
|
|
234
|
+
one button. 36 under a mouse, 44 under a finger or at phone width (a control; Gems/CLAUDE.md 9.1). */
|
|
235
|
+
.ui-list--compact > .ui-list__item { display: block; padding: 0; min-height: 0; }
|
|
236
|
+
.ui-list__row {
|
|
237
|
+
display: flex; align-items: center; gap: var(--ui-space-2); box-sizing: border-box; width: 100%; min-height: 36px; padding: 8px 12px;
|
|
238
|
+
border: 0; background: none; color: var(--ui-text); font: inherit; font-size: var(--ui-fs-body); line-height: 20px; text-align: left; text-decoration: none;
|
|
239
|
+
border-radius: inherit;
|
|
240
|
+
}
|
|
241
|
+
:is(a, button).ui-list__row { cursor: pointer; }
|
|
242
|
+
:is(a, button).ui-list__row:hover { background: var(--ui-coll-hover); }
|
|
243
|
+
:is(a, button).ui-list__row:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
|
|
244
|
+
.ui-list__row[aria-current="page"], .ui-list__row.is-selected { background: var(--ui-coll-selected); }
|
|
245
|
+
.ui-list__icon { flex: none; display: inline-flex; width: 20px; height: 20px; align-items: center; justify-content: center; color: var(--ui-text-muted); }
|
|
246
|
+
.ui-list__icon > svg { width: 16px; height: 16px; }
|
|
247
|
+
.ui-list__label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
248
|
+
.ui-list__row > .ui-list__meta { flex: none; margin: 0; font-variant-numeric: tabular-nums; }
|
|
249
|
+
@media (pointer: coarse), (max-width: 47.9975em) { .ui-list__row { min-height: 44px; } }
|
|
250
|
+
|
|
251
|
+
/* `--activity`: a timeline. A 20px rail on the left (an 8px dot on a 1px line), the event, one line of detail, the time.
|
|
252
|
+
One shape for the six timelines the admin drew, partner-radar `.tl`, talent-radar `.tr-rows`, GemFactory `.gf-hist`.
|
|
253
|
+
Tone the dot: `ui-list__dot--success | --warning | --critical | --info`. */
|
|
254
|
+
.ui-list--activity > .ui-list__item { display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; column-gap: var(--ui-space-2); align-items: start; padding: 8px 12px; min-height: 36px; border-top: 0; }
|
|
255
|
+
.ui-list--activity > .ui-list__item::before { content: ""; position: absolute; left: 21.5px; top: 0; bottom: 0; width: 1px; background: var(--ui-border); }
|
|
256
|
+
.ui-list--activity > .ui-list__item:first-child::before { top: 14px; }
|
|
257
|
+
.ui-list--activity > .ui-list__item:last-child::before { bottom: calc(100% - 14px); }
|
|
258
|
+
.ui-list--activity > .ui-list__item:only-child::before { display: none; }
|
|
259
|
+
.ui-list__dot { position: relative; z-index: 1; width: 8px; height: 8px; margin: 6px 0 0 6px; border-radius: 50%; background: var(--ui-text-muted); box-shadow: 0 0 0 3px var(--ui-surface); }
|
|
260
|
+
.ui-list__dot--success { background: var(--gemboss-success, #0a8f6a); }
|
|
261
|
+
.ui-list__dot--warning { background: var(--gemboss-warning, #b25e09); }
|
|
262
|
+
.ui-list__dot--critical { background: var(--gemboss-danger-strong, #c70a24); }
|
|
263
|
+
.ui-list__dot--info { background: var(--gemboss-accent, #5661f5); }
|
|
264
|
+
.ui-list--activity .ui-list__label { display: block; white-space: nowrap; }
|
|
265
|
+
.ui-list--activity .ui-list__detail { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ui-text-muted); font-size: var(--ui-fs-caption); line-height: 16px; }
|
|
266
|
+
.ui-list--activity time, .ui-list--activity .ui-list__time { color: var(--ui-text-muted); font-size: var(--ui-fs-caption); line-height: 20px; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
267
|
+
|
|
268
|
+
/* ══ 5 · Card grid: `div.ui-cardgrid[data-min="200|240|320"] > article.ui-card.ui-card--media` ═════════════════════
|
|
269
|
+
The picture is what people choose by. auto-fill with ONE of three minimum widths (240 by default), 16 apart (Polaris
|
|
270
|
+
InlineGrid gap 400): the column count follows the room, never a media query on the viewport (admin's
|
|
271
|
+
`.gemboss-theme-grid` repeat(3) left ~201px cards at 768). The card and its parts (`__link`, `__menu`, `__meta`,
|
|
272
|
+
`data-ratio`) are the media card's, so they live with it in kit-plain.css (one owner, Gems/CLAUDE.md 9.3). */
|
|
273
|
+
.ui-cardgrid { --ui-cardgrid-min: 240px; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(var(--ui-cardgrid-min), 100%), 1fr)); gap: var(--ui-space-4); margin: 0; padding: 0; list-style: none; }
|
|
274
|
+
.ui-cardgrid[data-min="200"] { --ui-cardgrid-min: 200px; }
|
|
275
|
+
.ui-cardgrid[data-min="320"] { --ui-cardgrid-min: 320px; }
|
|
276
|
+
.ui-cardgrid > * { min-width: 0; }
|
|
277
|
+
|
|
278
|
+
/* ══ 6 · Board: `div.ui-board > section.ui-board__col` ═════════════════════════════════════════════════════════════
|
|
279
|
+
Items that move through fixed states (six columns at most) and people move them. Column: `header.ui-board__head`
|
|
280
|
+
(a tone dot, the name, the count) · `ol.ui-board__list` of cards · after 12 cards `details.ui-board__more` ("Show 24
|
|
281
|
+
more", nested for each further 24; no script). An empty column says so and stays: it is where a card goes.
|
|
282
|
+
Moving a card needs a way that is not dragging (a select or the card's ⋯ menu): a keyboard and a finger cannot drag.
|
|
283
|
+
Columns are at least 240 and share the room; at phone width each is 78% of the board and they snap while it scrolls
|
|
284
|
+
sideways; they never stack. */
|
|
285
|
+
.ui-board {
|
|
286
|
+
display: grid; grid-auto-flow: column; grid-auto-columns: minmax(240px, 1fr); gap: var(--ui-space-4);
|
|
287
|
+
align-items: start; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; padding-bottom: 4px;
|
|
288
|
+
}
|
|
289
|
+
/* Phone width: a column is 78% of the board, so the next one peeks in; columns are states, their number never changes. */
|
|
290
|
+
@media (max-width: 47.9975em) { .ui-board { grid-auto-columns: 78%; } }
|
|
291
|
+
.ui-board__col { display: flex; flex-direction: column; gap: var(--ui-space-2); min-width: 0; padding: var(--ui-space-2); scroll-snap-align: start; background: var(--ui-coll-head-bg); border-radius: var(--ui-radius-lg); }
|
|
292
|
+
.ui-board__head { display: flex; align-items: center; gap: var(--ui-space-2); min-height: 28px; padding: 0 4px; font-size: var(--ui-fs-body); font-weight: 650; }
|
|
293
|
+
.ui-board__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
294
|
+
.ui-board__count { margin-left: auto; color: var(--ui-text-muted); font-weight: 450; font-variant-numeric: tabular-nums; }
|
|
295
|
+
.ui-board__dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--ui-text-muted); }
|
|
296
|
+
.ui-board__dot--success { background: var(--gemboss-success, #0a8f6a); }
|
|
297
|
+
.ui-board__dot--warning { background: var(--gemboss-warning, #b25e09); }
|
|
298
|
+
.ui-board__dot--critical { background: var(--gemboss-danger-strong, #c70a24); }
|
|
299
|
+
.ui-board__dot--info { background: var(--gemboss-accent, #5661f5); }
|
|
300
|
+
.ui-board__list { display: grid; gap: var(--ui-space-2); margin: 0; padding: 0; list-style: none; }
|
|
301
|
+
.ui-board__list > li { min-width: 0; }
|
|
302
|
+
.ui-board__empty { margin: 0; padding: var(--ui-space-4) var(--ui-space-2); color: var(--ui-text-muted); font-size: var(--ui-fs-body); text-align: center; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius-md); }
|
|
303
|
+
.ui-board__more > summary { list-style: none; justify-self: center; margin: 4px auto 0; width: fit-content; }
|
|
304
|
+
.ui-board__more > summary::-webkit-details-marker { display: none; }
|
|
305
|
+
.ui-board__more[open] > summary { display: none; }
|
|
306
|
+
.ui-board__more { display: grid; gap: var(--ui-space-2); }
|
|
307
|
+
|
|
308
|
+
/* ══ States (Gems/CLAUDE.md 9.2), every pattern ════════════════════════════════════════════════════════════════════
|
|
309
|
+
empty (`ui-empty`, ui.css) · no match (`ui-empty--no-match`: the header and the filters stay, "Clear filters") ·
|
|
310
|
+
loading (`ui-skeleton--table|--list|--compact|--grid|--board`, the shape of the pattern) · reloading (`aria-busy` on
|
|
311
|
+
the collection: the old rows stay, dimmed) · error (`ui-coll-error`: a banner in the frame + Retry; with old data,
|
|
312
|
+
"as of HH:MM") · selected · sort · filter (`ui-toolbar` + `ui-chip` + `ui-coll-count`) · pagination (`ui-pager`,
|
|
313
|
+
`ui-more`) · long content (`ui-trunc`, `ui-clamp-2`) · hover · focus-visible · reduced motion. */
|
|
314
|
+
.ui-empty--no-match { padding-block: 32px; }
|
|
315
|
+
.ui-coll-error { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2) var(--ui-space-3); margin: var(--ui-space-3); }
|
|
316
|
+
.ui-coll-error > :first-child { flex: 1 1 240px; min-width: 0; }
|
|
317
|
+
.ui-coll-count { margin-left: auto; color: var(--ui-text-muted); font-size: var(--ui-fs-body); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
318
|
+
.ui-chip__x { margin-right: -4px; font-size: 14px; line-height: 1; opacity: .7; }
|
|
319
|
+
:is(.ui-table-wrap, .ui-index, .ui-list, .ui-cardgrid, .ui-board)[aria-busy="true"] { position: relative; }
|
|
320
|
+
:is(.ui-table-wrap, .ui-index, .ui-list, .ui-cardgrid, .ui-board)[aria-busy="true"] > :not(.ui-bulkbar, .ui-coll-busy) { opacity: .55; pointer-events: none; transition: opacity .15s; }
|
|
321
|
+
.ui-coll-busy { position: absolute; top: 8px; right: 8px; z-index: 4; }
|
|
322
|
+
|
|
323
|
+
/* Skeletons in the shape of the pattern (Polaris: 8px bars, radius 4, 12 apart; a 40px thumbnail). A table skeleton is
|
|
324
|
+
a real `ui-table` with its real header and bars in the cells, so nothing moves when the rows arrive. */
|
|
325
|
+
:is(.ui-skeleton--table, .ui-skeleton--list, .ui-skeleton--compact, .ui-skeleton--grid, .ui-skeleton--board) .ui-skeleton__line { height: 8px; border-radius: 4px; }
|
|
326
|
+
/* A bar is 8 on a 20px line (margin 6 above and below), so a placeholder row is exactly as tall as the row it stands for. */
|
|
327
|
+
.ui-skeleton--table td .ui-skeleton__line { width: 70%; margin-block: 6px; }
|
|
328
|
+
.ui-skeleton--table td:first-child .ui-skeleton__line { width: 85%; }
|
|
329
|
+
.ui-skeleton--table tbody tr:hover { background: none; }
|
|
330
|
+
.ui-skeleton__block { display: block; flex: none; width: 40px; height: 40px; border-radius: var(--ui-radius-md); background: var(--ui-surface-2); }
|
|
331
|
+
.ui-skeleton__block--round { border-radius: 50%; width: 32px; height: 32px; }
|
|
332
|
+
.ui-skeleton--list > .ui-list__item { align-items: center; }
|
|
333
|
+
.ui-skeleton--list .ui-list__body { display: grid; gap: 12px; }
|
|
334
|
+
.ui-skeleton--list .ui-list__body > .ui-skeleton__line:last-child { width: 60%; }
|
|
335
|
+
.ui-skeleton--compact .ui-list__row { gap: 12px; }
|
|
336
|
+
.ui-skeleton--compact .ui-list__row > .ui-skeleton__line { flex: 1 1 auto; max-width: 60%; }
|
|
337
|
+
.ui-skeleton--grid .ui-card__body { display: grid; gap: 12px; justify-items: stretch; }
|
|
338
|
+
.ui-skeleton--grid .ui-card__body > .ui-skeleton__line:last-child { width: 50%; }
|
|
339
|
+
.ui-skeleton--board .ui-board__list > li { height: 72px; border-radius: var(--ui-radius-md); background: var(--ui-surface); }
|
|
340
|
+
|
|
341
|
+
/* Under a finger: the sort header and the row checkbox are controls, 44 (the header grows; Polaris does the same). */
|
|
342
|
+
@media (pointer: coarse) {
|
|
343
|
+
.ui-table__sort { min-height: 44px; margin-block: -12px; }
|
|
344
|
+
.ui-table__checkbox { min-width: 44px; min-height: 44px; margin: -12px 0; }
|
|
345
|
+
}
|
|
346
|
+
@media (prefers-reduced-motion: reduce) {
|
|
347
|
+
.ui-table--sticky-first :is(th, td):first-child { animation: none; }
|
|
348
|
+
:is(.ui-table-wrap, .ui-index, .ui-list, .ui-cardgrid, .ui-board)[aria-busy="true"] > * { transition: none; }
|
|
349
|
+
.ui-board { scroll-behavior: auto; }
|
|
350
|
+
}
|