@uiresponse/renderer-react 0.1.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/ATTRIBUTION.md +59 -0
- package/README.md +191 -0
- package/dist/uir-renderer-web.css +1 -0
- package/dist/uir-renderer-web.js +2213 -0
- package/package.json +63 -0
- package/src/UIResponsePage.jsx +576 -0
- package/src/blocks/containers.jsx +150 -0
- package/src/blocks/content.jsx +292 -0
- package/src/blocks/context.js +27 -0
- package/src/blocks/data.jsx +551 -0
- package/src/blocks/primitives.jsx +290 -0
- package/src/blocks/viz.jsx +616 -0
- package/src/core/actions.js +201 -0
- package/src/core/condition.js +90 -0
- package/src/core/empty.js +44 -0
- package/src/core/format.js +225 -0
- package/src/core/presentation.js +58 -0
- package/src/core/record.js +160 -0
- package/src/core/store.js +357 -0
- package/src/core/value.js +211 -0
- package/src/index.js +22 -0
- package/src/theme/tokens.js +364 -0
- package/src/theme/uir.css +1070 -0
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* OBL renderer — structural stylesheet.
|
|
3
|
+
*
|
|
4
|
+
* Every value here reads a token. There are no literal colours and no literal typefaces below this
|
|
5
|
+
* comment: a tenant swaps `themeToCssVars()` and the whole page follows. That is the contract.
|
|
6
|
+
*
|
|
7
|
+
* Loaded AFTER `@openuidev/react-ui/index.css`. We do NOT restyle their components: in the light
|
|
8
|
+
* theme this stylesheet emits no `--openui-*` overrides, and nothing here reaches inside a chart, a
|
|
9
|
+
* control or a table primitive of theirs. Our layer is layout, spacing, and the semantic states OBL
|
|
10
|
+
* has and they do not — absence, staleness, per-block loading, per-block failure.
|
|
11
|
+
*
|
|
12
|
+
* ── DESIGN SOURCE OF TRUTH ────────────────────────────────────────────────────────────────────
|
|
13
|
+
* Every treatment below is converged on `design/UIR-element-system.dc.html`, the element system Dan
|
|
14
|
+
* built and approved. That file is the authority; when it and this stylesheet disagree, this one is
|
|
15
|
+
* wrong. Values live in `theme/tokens.js`, extracted from the sheets rather than eyeballed.
|
|
16
|
+
*
|
|
17
|
+
* ── THE COMPOSITION MODEL: ELEMENTS ON A SURFACE ──────────────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* A generated page is not a document. It is a handful of elements placed ON the host's surface —
|
|
20
|
+
* a workroom canvas, a chat thread, a home screen. So there is no inner paper, no bounded sheet,
|
|
21
|
+
* no masthead, and no horizontal rules. The renderer draws TILES and puts them in a grid.
|
|
22
|
+
*
|
|
23
|
+
* What that buys, concretely:
|
|
24
|
+
* - Hierarchy comes from TYPE SCALE and TILE SIZE, never from rule weight or a display headline.
|
|
25
|
+
* - A one-block page looks like one element in space, not a poster with a lonely chart on it.
|
|
26
|
+
* - Chrome scales to content: a dashboard earns a header line; a one-block answer earns a label.
|
|
27
|
+
* - The host owns the background. `.uir-page` paints none, so the same blocks drop onto a dark
|
|
28
|
+
* canvas, into a chat card, or into a full window without carrying a page-shaped hole with them.
|
|
29
|
+
*
|
|
30
|
+
* Earlier passes drew a grey paper sheet with a masthead and full-width hairlines, then an editorial
|
|
31
|
+
* identity of condensed display type and tracked mono eyebrows. Both are gone. A page a model
|
|
32
|
+
* generated inside someone else's product must not arrive wearing our personality.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
.uir-page {
|
|
36
|
+
--uir-row-pad: var(--uir-density-comfortable);
|
|
37
|
+
/* No background. The surface belongs to the host. */
|
|
38
|
+
color: var(--uir-color-ink);
|
|
39
|
+
font-family: var(--uir-font-body);
|
|
40
|
+
font-size: var(--uir-size-base);
|
|
41
|
+
line-height: 1.5;
|
|
42
|
+
padding: var(--uir-space-lg);
|
|
43
|
+
-webkit-font-smoothing: antialiased;
|
|
44
|
+
}
|
|
45
|
+
.uir-page *, .uir-page *::before, .uir-page *::after { box-sizing: border-box; }
|
|
46
|
+
.uir-shell { display: flex; flex-direction: column; gap: var(--uir-space-xl); margin: 0 auto; max-width: var(--uir-measure); }
|
|
47
|
+
|
|
48
|
+
/* Chat is a slot, not a smaller page. The artifact keeps exactly the same bytes and semantics; the
|
|
49
|
+
inline rendering context removes shell padding, width ceremony and the title/status masthead so
|
|
50
|
+
its blocks land directly in the transcript. Individual data blocks remain tiles when their type
|
|
51
|
+
needs a frame (table, chart, metric); there is no tile around the group of tiles. */
|
|
52
|
+
.uir-page--inline { padding: 0; }
|
|
53
|
+
.uir-page--inline .uir-shell { gap: var(--uir-space-md); margin: 0; max-width: none; }
|
|
54
|
+
|
|
55
|
+
/* Numbers, dates and identifiers are set with tabular figures. A column of numbers that does not
|
|
56
|
+
align on the decimal cannot be compared, and comparison is what the block is for. */
|
|
57
|
+
.uir-num, .uir-mono {
|
|
58
|
+
font-family: var(--uir-font-mono);
|
|
59
|
+
font-feature-settings: "tnum" 1, "zero" 1;
|
|
60
|
+
font-variant-numeric: tabular-nums;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* A label. Not an eyebrow: no tracking, no uppercase, no mono. */
|
|
64
|
+
.uir-eyebrow {
|
|
65
|
+
color: var(--uir-color-graphite);
|
|
66
|
+
font-family: var(--uir-font-body);
|
|
67
|
+
font-size: var(--uir-size-small);
|
|
68
|
+
font-weight: 400;
|
|
69
|
+
letter-spacing: 0;
|
|
70
|
+
text-transform: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ── Header. A line, not a masthead. ──────────────────────────────────────── */
|
|
74
|
+
.uir-header { display: flex; flex-direction: column; gap: 3px; }
|
|
75
|
+
.uir-header__line { align-items: baseline; display: flex; flex-wrap: wrap; gap: var(--uir-space-md); }
|
|
76
|
+
.uir-header__title {
|
|
77
|
+
font-size: var(--uir-size-large);
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
letter-spacing: -0.008em;
|
|
80
|
+
line-height: 1.2;
|
|
81
|
+
margin: 0;
|
|
82
|
+
}
|
|
83
|
+
.uir-header__subtitle { color: var(--uir-color-graphite); font-size: var(--uir-size-small); margin: 0; max-width: 68ch; }
|
|
84
|
+
|
|
85
|
+
/* One or two elements is an answer, not a document: the title drops to a label and gets out of
|
|
86
|
+
the way. A masthead over a single chart is a poster. */
|
|
87
|
+
.uir-header[data-minimal] .uir-header__title {
|
|
88
|
+
color: var(--uir-color-graphite);
|
|
89
|
+
font-size: var(--uir-size-small);
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
letter-spacing: 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.uir-header__status {
|
|
95
|
+
align-items: center; color: var(--uir-color-faint); display: inline-flex;
|
|
96
|
+
font-size: var(--uir-size-micro); gap: 0.5em; margin-left: auto;
|
|
97
|
+
}
|
|
98
|
+
.uir-header__status::before {
|
|
99
|
+
animation: uir-pulse 1.4s ease-in-out infinite; background: var(--uir-color-graphite);
|
|
100
|
+
border-radius: 50%; content: ""; height: 5px; width: 5px;
|
|
101
|
+
}
|
|
102
|
+
/* The animation is information. It ends when the information does. */
|
|
103
|
+
.uir-header__status[data-settled]::before { animation: none; background: var(--uir-color-positive); }
|
|
104
|
+
@keyframes uir-pulse { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }
|
|
105
|
+
|
|
106
|
+
/* ── Sections: a labelled GROUPING of tiles. No frame, no rule, no rail. ────
|
|
107
|
+
Grouping is legible from the gap around it — which is how grouping works on a canvas. */
|
|
108
|
+
.uir-section { display: flex; flex-direction: column; gap: var(--uir-space-md); }
|
|
109
|
+
/* 12px / 500 / graphite. Not an eyebrow: no tracking, no uppercase (design, "Containers"). */
|
|
110
|
+
.uir-section__label {
|
|
111
|
+
color: var(--uir-color-graphite);
|
|
112
|
+
font-size: var(--uir-size-small);
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
letter-spacing: 0;
|
|
115
|
+
margin: 0;
|
|
116
|
+
}
|
|
117
|
+
.uir-section[data-density="compact"] { --uir-row-pad: var(--uir-density-compact); }
|
|
118
|
+
|
|
119
|
+
/* ── The editorial section: headline, standfirst, then the things. ───────────
|
|
120
|
+
A section that opens with a `text` block is a magazine section, and its title stops being a filing
|
|
121
|
+
label and becomes a headline. Nothing about the document changed; this is the renderer reading a
|
|
122
|
+
composition the document could already express (see containers.jsx `isEditorial`).
|
|
123
|
+
|
|
124
|
+
The headline borrows the metric ladder's `primaryValue` rather than introducing a display scale of
|
|
125
|
+
its own: a section headline and a hero number are the same rank of thing, and the ladder is where
|
|
126
|
+
this renderer keeps rank. */
|
|
127
|
+
.uir-section[data-editorial] { gap: var(--uir-space-lg); }
|
|
128
|
+
.uir-section[data-editorial] > .uir-section__label {
|
|
129
|
+
color: var(--uir-color-ink);
|
|
130
|
+
font-size: var(--uir-size-primary-value);
|
|
131
|
+
font-weight: 600;
|
|
132
|
+
letter-spacing: -0.02em;
|
|
133
|
+
line-height: 1.1;
|
|
134
|
+
margin-bottom: calc(var(--uir-space-md) * -1); /* pull the standfirst up under its headline */
|
|
135
|
+
max-width: 24ch;
|
|
136
|
+
text-wrap: balance;
|
|
137
|
+
}
|
|
138
|
+
/* The standfirst: one sentence, set at reading measure, in the quiet voice. */
|
|
139
|
+
.uir-section[data-editorial] > .uir-arrange > .uir-block[data-type="text"]:first-child .uir-text {
|
|
140
|
+
color: var(--uir-color-graphite);
|
|
141
|
+
font-size: var(--uir-size-large);
|
|
142
|
+
font-weight: 400;
|
|
143
|
+
letter-spacing: 0;
|
|
144
|
+
line-height: 1.5;
|
|
145
|
+
max-width: 64ch;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* The rail bleeds through the page's right margin. Scoped to an editorial section so a rail inside a
|
|
149
|
+
card or a drawer keeps its box. `--uir-space-lg` is `.uir-page`'s own padding. */
|
|
150
|
+
.uir-section[data-editorial] .uir-posters[data-mode="rail"] {
|
|
151
|
+
margin-right: calc(var(--uir-space-lg) * -1);
|
|
152
|
+
padding-right: var(--uir-space-lg);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ── Arrangement. Consistent gaps; the renderer derives the column count. ─── */
|
|
156
|
+
.uir-arrange { align-items: start; display: grid; gap: var(--uir-space-md); }
|
|
157
|
+
.uir-arrange[data-arrangement="stack"] { grid-template-columns: minmax(0, 1fr); }
|
|
158
|
+
.uir-arrange[data-arrangement="row"] { align-items: end; grid-auto-columns: minmax(0, 1fr); grid-auto-flow: column; }
|
|
159
|
+
/* There is no `columns` key in OBL (R5) — a voice renderer cannot honour `3`. `auto-fit` IS the
|
|
160
|
+
renderer deriving the count from the item count and the medium. */
|
|
161
|
+
.uir-arrange[data-arrangement="grid"] { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
|
162
|
+
.uir-arrange[data-arrangement="flow"] { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
|
|
163
|
+
|
|
164
|
+
/* A chart, a table, a timeline and a detail need width the way a metric needs none. They claim the
|
|
165
|
+
row; the compact tiles wrap above them. Their axes and bands are unusable at 200px. */
|
|
166
|
+
.uir-arrange[data-arrangement="row"]:has(> .uir-block[data-type="chart"]),
|
|
167
|
+
.uir-arrange[data-arrangement="row"]:has(> .uir-block[data-type="table"]),
|
|
168
|
+
.uir-arrange[data-arrangement="row"]:has(> .uir-block[data-type="timeline"]) {
|
|
169
|
+
grid-auto-flow: row;
|
|
170
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
171
|
+
}
|
|
172
|
+
.uir-arrange:not([data-arrangement="stack"]) > .uir-block[data-type="chart"],
|
|
173
|
+
.uir-arrange:not([data-arrangement="stack"]) > .uir-block[data-type="timeline"],
|
|
174
|
+
/* A calendar squeezed into a column is seven columns inside one. It claims the row. */
|
|
175
|
+
.uir-arrange:not([data-arrangement="stack"]) > .uir-block[data-type="calendar"],
|
|
176
|
+
.uir-arrange:not([data-arrangement="stack"]) > .uir-block[data-type="table"],
|
|
177
|
+
.uir-arrange:not([data-arrangement="stack"]) > .uir-block[data-type="detail"] { grid-column: 1 / -1; }
|
|
178
|
+
|
|
179
|
+
@media (max-width: 640px) {
|
|
180
|
+
.uir-arrange[data-arrangement="row"] { grid-auto-columns: auto; grid-auto-flow: row; }
|
|
181
|
+
.uir-page { padding: var(--uir-space-md); }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ── THE TILE. A discrete, self-contained object placed on the surface. ───── */
|
|
185
|
+
.uir-block { min-width: 0; position: relative; }
|
|
186
|
+
|
|
187
|
+
.uir-block[data-tile] {
|
|
188
|
+
background: var(--uir-color-sheet);
|
|
189
|
+
border: 1px solid var(--uir-color-rule);
|
|
190
|
+
border-radius: var(--uir-radius-lg);
|
|
191
|
+
box-shadow: var(--uir-shadow);
|
|
192
|
+
padding: var(--uir-tile-pad-y) var(--uir-tile-pad-x);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* STREAMING ARRIVAL. A tile folds in when its data lands — `obFold`, lifted from the design's own
|
|
196
|
+
<style> block along with its 0.45s cubic-bezier. The frame is already there; what arrives is the
|
|
197
|
+
content, which is exactly what progressive loading means. */
|
|
198
|
+
@keyframes uir-fold {
|
|
199
|
+
from { opacity: 0; transform: translateY(7px) scale(0.985); }
|
|
200
|
+
to { opacity: 1; transform: none; }
|
|
201
|
+
}
|
|
202
|
+
.uir-block[data-tile][data-state="ready"][data-arrived] { animation: uir-fold var(--uir-motion-fold); }
|
|
203
|
+
|
|
204
|
+
/* Emphasis is TYPE SCALE and TILE SIZE. A `hero` tile is bigger and says its number louder; it does
|
|
205
|
+
not get a heavier border, because a heavy border is a rule wearing a costume. */
|
|
206
|
+
.uir-block[data-tile][data-emphasis="hero"] { grid-column: span 2; }
|
|
207
|
+
.uir-block[data-tile][data-type="metric"][data-emphasis="subtle"] { }
|
|
208
|
+
@media (max-width: 520px) { .uir-block[data-tile][data-emphasis="hero"] { grid-column: 1 / -1; } }
|
|
209
|
+
.uir-block[data-tile][data-emphasis="subtle"] { background: transparent; border-color: transparent; box-shadow: none; }
|
|
210
|
+
|
|
211
|
+
/* A metric and a progress bar are COMPACT tiles: they say one number. Stretched across a stack they
|
|
212
|
+
become a 900px card with a word in the corner. In a grid they size themselves; in a stack they
|
|
213
|
+
need a ceiling. (A `hero` metric keeps its full span — that is what `hero` asked for.) */
|
|
214
|
+
.uir-arrange[data-arrangement="stack"] > .uir-block[data-tile][data-type="metric"]:not([data-emphasis="hero"]),
|
|
215
|
+
.uir-arrange[data-arrangement="stack"] > .uir-block[data-tile][data-type="progress"]:not([data-emphasis="hero"]) {
|
|
216
|
+
max-width: 300px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.uir-card {
|
|
220
|
+
background: var(--uir-color-sheet);
|
|
221
|
+
border: 1px solid var(--uir-color-rule);
|
|
222
|
+
border-radius: var(--uir-radius-lg);
|
|
223
|
+
box-shadow: var(--uir-shadow);
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-direction: column;
|
|
226
|
+
gap: var(--uir-space-lg);
|
|
227
|
+
padding: var(--uir-space-lg);
|
|
228
|
+
}
|
|
229
|
+
.uir-card__title { font-size: var(--uir-size-base); font-weight: 600; letter-spacing: -0.005em; margin: 0; }
|
|
230
|
+
.uir-card--clickable { cursor: pointer; transition: border-color 120ms ease; }
|
|
231
|
+
.uir-card--clickable:hover { border-color: var(--uir-color-brand); }
|
|
232
|
+
.uir-block[data-type="card"][data-emphasis="hero"] { grid-column: 1 / -1; }
|
|
233
|
+
|
|
234
|
+
/* A card IS a tile; its children are contents, not objects. A tile inside a tile is a box in a box,
|
|
235
|
+
which is the exact thing this composition model exists to stop. */
|
|
236
|
+
.uir-card .uir-block[data-tile],
|
|
237
|
+
.uir-card .uir-block[data-tile][data-emphasis="hero"] {
|
|
238
|
+
background: none; border: 0; border-radius: 0; grid-column: auto; padding: 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ── Tabs: a segmented control, not a ruled tab strip. ────────────────────── */
|
|
242
|
+
.uir-tabs { display: flex; flex-direction: column; gap: var(--uir-space-md); }
|
|
243
|
+
.uir-tabs__list {
|
|
244
|
+
align-self: start; background: var(--uir-color-sunk); border-radius: 9px; display: inline-flex;
|
|
245
|
+
gap: 2px; max-width: 100%; overflow-x: auto; padding: 3px; scrollbar-width: none;
|
|
246
|
+
}
|
|
247
|
+
.uir-tab {
|
|
248
|
+
background: none; border: 0; border-radius: 7px; color: var(--uir-color-graphite); cursor: pointer;
|
|
249
|
+
font-family: var(--uir-font-body); font-size: var(--uir-size-small); font-weight: 500;
|
|
250
|
+
padding: 5px 14px; transition: background 120ms ease, color 120ms ease; white-space: nowrap;
|
|
251
|
+
}
|
|
252
|
+
.uir-tab[aria-selected="true"] { background: var(--uir-color-sheet); box-shadow: var(--uir-shadow); color: var(--uir-color-ink); }
|
|
253
|
+
.uir-tab:hover:not([aria-selected="true"]) { color: var(--uir-color-ink); }
|
|
254
|
+
|
|
255
|
+
/* ── metric — the compact tile. ───────────────────────────────────────────── */
|
|
256
|
+
.uir-metric { display: flex; flex-direction: column; }
|
|
257
|
+
.uir-metric__label { color: var(--uir-color-graphite); font-size: var(--uir-size-label); font-weight: 400; letter-spacing: 0; text-transform: none; }
|
|
258
|
+
.uir-metric__value {
|
|
259
|
+
font-feature-settings: "tnum" 1;
|
|
260
|
+
font-variant-numeric: tabular-nums;
|
|
261
|
+
font-weight: 600;
|
|
262
|
+
margin-top: 8px;
|
|
263
|
+
}
|
|
264
|
+
/* 14px / 400 / faint, 3px off the number — never the same weight as the value it qualifies. */
|
|
265
|
+
.uir-metric__unit { color: var(--uir-color-faint); font-size: var(--uir-size-unit); font-weight: 400; letter-spacing: 0; margin-left: 3px; }
|
|
266
|
+
|
|
267
|
+
/* The ladder, straight off `01 · Metric`. */
|
|
268
|
+
.uir-metric[data-emphasis="hero"] .uir-metric__value { font-size: var(--uir-size-hero-value); letter-spacing: -0.02em; line-height: 1.05; }
|
|
269
|
+
.uir-metric[data-emphasis="primary"] .uir-metric__value { font-size: var(--uir-size-primary-value); letter-spacing: -0.015em; line-height: 1.1; }
|
|
270
|
+
.uir-metric[data-emphasis="normal"] .uir-metric__value { font-size: var(--uir-size-normal-value); line-height: 1.2; }
|
|
271
|
+
.uir-metric[data-emphasis="subtle"] .uir-metric__value { color: var(--uir-color-muted); font-size: var(--uir-size-subtle-value); font-weight: 500; line-height: 1.3; margin-top: 4px; }
|
|
272
|
+
|
|
273
|
+
/* DELTA — a renderer capability, ahead of the schema. See viz.jsx: OBL has no `metric.delta`, so a
|
|
274
|
+
metric without one simply renders none. The treatment is NOT in the design file (it shows no
|
|
275
|
+
deltas at all); it reuses the design's status-tone chips, and is flagged for Dan in NOTES. */
|
|
276
|
+
.uir-metric__delta {
|
|
277
|
+
align-items: center; align-self: flex-start; border-radius: 999px; display: inline-flex; gap: 4px;
|
|
278
|
+
font-size: var(--uir-size-small); font-weight: 500; margin-top: 8px; padding: 2px 8px;
|
|
279
|
+
}
|
|
280
|
+
.uir-metric__delta[data-tone="positive"] { background: var(--uir-color-positive-soft); color: var(--uir-color-positive); }
|
|
281
|
+
.uir-metric__delta[data-tone="negative"] { background: var(--uir-color-negative-soft); color: var(--uir-color-negative); }
|
|
282
|
+
.uir-metric__delta[data-tone="neutral"] { background: var(--uir-color-neutral-soft); color: var(--uir-color-neutral); }
|
|
283
|
+
.uir-metric__delta em { font-style: normal; font-variant-numeric: tabular-nums; }
|
|
284
|
+
.uir-metric__delta span { color: var(--uir-color-graphite); font-weight: 400; }
|
|
285
|
+
.uir-metric--clickable { background: none; border: 0; cursor: pointer; padding: 0; text-align: left; width: 100%; }
|
|
286
|
+
.uir-metric--clickable:hover .uir-metric__value { color: var(--uir-color-brand); }
|
|
287
|
+
|
|
288
|
+
/* ── Absence. `empty.show: "dash"` is OBL's default because "rendering an unfiled royalty as £0.00
|
|
289
|
+
is a lie the page tells confidently." The SEMANTIC survives — a missing value is visibly missing,
|
|
290
|
+
never a zero and never a blank cell. The costume does not: the dotted fill-in rule and the tracked
|
|
291
|
+
uppercase mono stamp were personality, and a page generated for someone else's product must not
|
|
292
|
+
arrive wearing ours. Muted text carries the same claim, quietly. */
|
|
293
|
+
.uir-absent { color: var(--uir-color-faint); }
|
|
294
|
+
.uir-metric__value .uir-absent--dash { color: var(--uir-color-rule-strong); }
|
|
295
|
+
.uir-absent--dash { font-variant-numeric: tabular-nums; }
|
|
296
|
+
.uir-absent--text { font-size: var(--uir-size-micro); }
|
|
297
|
+
.uir-absent--zero { color: var(--uir-color-ink); }
|
|
298
|
+
|
|
299
|
+
/* ── table (`03 · Table`) — a tile whose contents are rows. The tile carries a title row, then the
|
|
300
|
+
column heads, then rows separated by `ruleSoft`: a separator INSIDE a tile must be lighter than
|
|
301
|
+
the tile's own border, or the tile reads as a stack of boxes. ── */
|
|
302
|
+
.uir-block[data-type="table"][data-tile] { padding: var(--uir-tile-pad-y) 0 0; }
|
|
303
|
+
.uir-table__head { align-items: center; display: flex; justify-content: space-between; padding: 0 var(--uir-tile-pad-x) var(--uir-space-md); }
|
|
304
|
+
.uir-table__title {
|
|
305
|
+
color: var(--uir-color-ink); font-size: var(--uir-size-large); font-weight: 600;
|
|
306
|
+
margin: 0;
|
|
307
|
+
}
|
|
308
|
+
.uir-density { background: var(--uir-color-sunk); border-radius: var(--uir-radius-md); display: inline-flex; gap: 2px; padding: 2px; }
|
|
309
|
+
.uir-density button {
|
|
310
|
+
background: none; border: 0; border-radius: var(--uir-radius-sm); color: var(--uir-color-graphite);
|
|
311
|
+
cursor: pointer; font-family: inherit; font-size: var(--uir-size-small); font-weight: 500; padding: 4px 10px;
|
|
312
|
+
}
|
|
313
|
+
.uir-density button[aria-pressed="true"] { background: var(--uir-color-sheet); box-shadow: var(--uir-shadow); color: var(--uir-color-ink); }
|
|
314
|
+
.uir-table-wrap { margin: 0; }
|
|
315
|
+
.uir-table__scroll { overflow-x: auto; scrollbar-width: thin; }
|
|
316
|
+
.uir-table { border-collapse: collapse; font-size: var(--uir-size-base); width: 100%; }
|
|
317
|
+
.uir-table[data-density="comfortable"] { --uir-row-pad: var(--uir-density-comfortable); }
|
|
318
|
+
.uir-table[data-density="compact"] { --uir-row-pad: var(--uir-density-compact); }
|
|
319
|
+
.uir-table thead th {
|
|
320
|
+
border-top: 1px solid var(--uir-color-rule-soft);
|
|
321
|
+
color: var(--uir-color-faint);
|
|
322
|
+
font-size: var(--uir-size-small);
|
|
323
|
+
font-weight: 500;
|
|
324
|
+
letter-spacing: 0;
|
|
325
|
+
padding: 7px var(--uir-tile-pad-x);
|
|
326
|
+
text-align: left;
|
|
327
|
+
text-transform: none;
|
|
328
|
+
white-space: nowrap;
|
|
329
|
+
}
|
|
330
|
+
.uir-table tbody td { border-top: 1px solid var(--uir-color-rule-soft); padding: var(--uir-row-pad) var(--uir-tile-pad-x); vertical-align: middle; }
|
|
331
|
+
.uir-table tbody tr { --row-bg: var(--uir-color-sheet); position: relative; }
|
|
332
|
+
.uir-table tbody tr:hover { --row-bg: var(--uir-color-sunk); background: var(--uir-color-sunk); }
|
|
333
|
+
.uir-table tbody tr[data-clickable="true"] { cursor: pointer; }
|
|
334
|
+
.uir-table tbody tr[data-clickable="true"]:hover { --row-bg: var(--uir-color-brand-soft); background: var(--uir-color-brand-soft); }
|
|
335
|
+
.uir-table tbody td:first-child { color: var(--uir-color-ink); font-weight: 500; }
|
|
336
|
+
.uir-table td[data-numeric="true"], .uir-table th[data-numeric="true"] { text-align: right; }
|
|
337
|
+
.uir-table td[data-numeric="true"] { font-variant-numeric: tabular-nums; }
|
|
338
|
+
.uir-name-cell { align-items: center; display: inline-flex; gap: 10px; }
|
|
339
|
+
.uir-initials {
|
|
340
|
+
align-items: center; background: var(--uir-color-sunk); color: var(--uir-color-muted); display: inline-flex;
|
|
341
|
+
flex: none; font-size: 9.5px; font-weight: 600; height: 24px; justify-content: center; letter-spacing: 0.02em; width: 24px;
|
|
342
|
+
}
|
|
343
|
+
.uir-initials[data-shape="round"] { border-radius: 50%; }
|
|
344
|
+
.uir-initials[data-shape="square"] { border-radius: var(--uir-radius-md); color: var(--uir-color-graphite); font-size: 10px; height: 32px; width: 32px; }
|
|
345
|
+
|
|
346
|
+
/* A column whose declared role is `image` is a picture, not a URL. `role` is the dataset saying what
|
|
347
|
+
a column MEANS as opposed to what it stores (DECISIONS D7); this is the renderer honouring it. */
|
|
348
|
+
.uir-thumb {
|
|
349
|
+
background: var(--uir-color-sunk); border: 1px solid var(--uir-color-rule); border-radius: var(--uir-radius-sm);
|
|
350
|
+
display: block; height: 26px; object-fit: cover; width: 26px;
|
|
351
|
+
}
|
|
352
|
+
.uir-table td:has(> .uir-thumb) { padding-bottom: 5px; padding-top: 5px; width: 1%; }
|
|
353
|
+
|
|
354
|
+
/* Row verbs float over the row's trailing edge on hover rather than occupying a column: reserving
|
|
355
|
+
one pushes a six-column table past its tile. Keyboard focus anywhere in the row brings them back,
|
|
356
|
+
so they are hidden, never unreachable. */
|
|
357
|
+
.uir-table__actions-cell { padding: 0 !important; width: 0; }
|
|
358
|
+
.uir-table__actions {
|
|
359
|
+
align-items: center; background: var(--row-bg); display: flex; gap: var(--uir-space-sm); opacity: 0;
|
|
360
|
+
padding: 0 var(--uir-tile-pad-x) 0 var(--uir-space-sm); pointer-events: none; position: absolute;
|
|
361
|
+
right: 0; top: 50%; transform: translateY(-50%); transition: opacity 110ms ease;
|
|
362
|
+
}
|
|
363
|
+
.uir-table__actions::before {
|
|
364
|
+
background: linear-gradient(to right, transparent, var(--row-bg)); bottom: 0; content: "";
|
|
365
|
+
left: -44px; position: absolute; top: 0; width: 44px;
|
|
366
|
+
}
|
|
367
|
+
.uir-table tbody tr:hover .uir-table__actions,
|
|
368
|
+
.uir-table tbody tr:focus-within .uir-table__actions { opacity: 1; pointer-events: auto; }
|
|
369
|
+
.uir-table__actions .uir-button { position: relative; }
|
|
370
|
+
@media (hover: none) {
|
|
371
|
+
.uir-table__actions { opacity: 1; padding-left: 0; pointer-events: auto; position: static; transform: none; }
|
|
372
|
+
.uir-table__actions::before { display: none; }
|
|
373
|
+
.uir-table__actions-cell { padding: var(--uir-row-pad) var(--uir-tile-pad-x) !important; width: auto; }
|
|
374
|
+
}
|
|
375
|
+
.uir-pagination {
|
|
376
|
+
align-items: center; border-top: 1px solid var(--uir-color-rule-soft); color: var(--uir-color-graphite);
|
|
377
|
+
display: flex; font-size: var(--uir-size-small); font-variant-numeric: tabular-nums;
|
|
378
|
+
justify-content: space-between; padding: 10px var(--uir-tile-pad-x);
|
|
379
|
+
}
|
|
380
|
+
.uir-pagination > div { display: flex; gap: 6px; }
|
|
381
|
+
.uir-pagination button {
|
|
382
|
+
align-items: center; background: var(--uir-color-sheet); border: 1px solid var(--uir-color-rule);
|
|
383
|
+
border-radius: var(--uir-radius-sm); color: var(--uir-color-muted); cursor: pointer; display: inline-flex;
|
|
384
|
+
font-family: inherit; font-size: 18px; height: 26px; justify-content: center; line-height: 1; width: 26px;
|
|
385
|
+
}
|
|
386
|
+
.uir-pagination button:hover:not(:disabled) { background: var(--uir-color-sunk-strong); color: var(--uir-color-ink); }
|
|
387
|
+
.uir-pagination button:disabled { cursor: default; opacity: 0.35; }
|
|
388
|
+
|
|
389
|
+
/* ── list ─────────────────────────────────────────────────────────────────── */
|
|
390
|
+
.uir-list { display: flex; flex-direction: column; margin: 0 calc(-1 * var(--uir-space-sm)); }
|
|
391
|
+
.uir-list__item {
|
|
392
|
+
align-items: center; background: none; border: 0; border-radius: var(--uir-radius-md);
|
|
393
|
+
display: flex; gap: var(--uir-space-md); justify-content: space-between; padding: 8px var(--uir-space-sm);
|
|
394
|
+
text-align: left; width: 100%;
|
|
395
|
+
}
|
|
396
|
+
.uir-list__copy { display: flex; flex: 1; flex-direction: column; min-width: 0; }
|
|
397
|
+
/* The sheet's row hover is a quiet ground, not a tinted one: a row that turns blue on hover has
|
|
398
|
+
spent the accent on the act of pointing at it (design sheet, "04 · List"). */
|
|
399
|
+
.uir-list__item[data-clickable="true"] { cursor: pointer; }
|
|
400
|
+
.uir-list__item[data-clickable="true"]:hover { background: var(--uir-color-sunk-strong); }
|
|
401
|
+
.uir-list__title { font-family: var(--uir-font-body); font-size: var(--uir-size-small); font-weight: 500; letter-spacing: -0.003em; }
|
|
402
|
+
.uir-list__subtitle { color: var(--uir-color-graphite); font-family: var(--uir-font-body); font-size: var(--uir-size-small); letter-spacing: 0; white-space: nowrap; }
|
|
403
|
+
|
|
404
|
+
/* The one mark a row action gets. It leads the row's trailing edge and moves a hair on hover, which
|
|
405
|
+
is the whole animation budget for "this opens something". */
|
|
406
|
+
.uir-chevron { align-self: center; flex: none; stroke: var(--uir-color-faint); transition: transform 160ms ease; }
|
|
407
|
+
.uir-list__item[data-clickable="true"]:hover .uir-chevron { transform: translateX(2px); }
|
|
408
|
+
@media (prefers-reduced-motion: reduce) {
|
|
409
|
+
.uir-chevron { transition: none; }
|
|
410
|
+
.uir-list__item[data-clickable="true"]:hover .uir-chevron { transform: none; }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* ── BLUR-UP: an image that arrives, instead of one painted a band at a time. ─
|
|
414
|
+
The element's BACKGROUND is the low-resolution proxy (`primitives.jsx` sets it inline, from the
|
|
415
|
+
dataset's `*_thumb_url` column); the element's CONTENT is the full image. `filter` blurs the whole
|
|
416
|
+
element, so the full image appears already blurred and in register, then sharpens. One element:
|
|
417
|
+
nothing is positioned over anything, and this drops unchanged into a sleeve, a hero card and a
|
|
418
|
+
26px table cell, which size their images three different ways.
|
|
419
|
+
|
|
420
|
+
With no proxy the image is simply transparent until it has loaded, then fades in whole. That is
|
|
421
|
+
the universal fallback, and it is the one that kills the top-down JPEG wipe everywhere else. */
|
|
422
|
+
.uir-img {
|
|
423
|
+
background-position: center; background-repeat: no-repeat; background-size: cover;
|
|
424
|
+
/* `transform` rides here too, so that the sleeve's hover lift does not have to declare a
|
|
425
|
+
`transition` of its own and silently drop the filter's. */
|
|
426
|
+
transition: filter 400ms ease, opacity 400ms ease, transform 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
|
427
|
+
}
|
|
428
|
+
.uir-img[data-blurup="thumb"]:not([data-loaded]) { filter: blur(12px); }
|
|
429
|
+
.uir-img[data-blurup="none"]:not([data-loaded]) { opacity: 0; }
|
|
430
|
+
.uir-img[data-loaded] { filter: none; opacity: 1; }
|
|
431
|
+
/* The sleeve clips, so its proxy may bleed past the edge the blur softens. Nothing else may. */
|
|
432
|
+
.uir-poster__art > .uir-img[data-blurup="thumb"]:not([data-loaded]) { transform: scale(1.06); }
|
|
433
|
+
@media (prefers-reduced-motion: reduce) {
|
|
434
|
+
.uir-img { transition: none; }
|
|
435
|
+
.uir-poster__art > .uir-img[data-blurup="thumb"]:not([data-loaded]) { transform: none; }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* ── posters: a list of things you can SEE ───────────────────────────────────
|
|
439
|
+
The artwork is the ink. Everything around it is monochrome and quiet, so that the only colour on
|
|
440
|
+
the page is the colour the label actually made. There is no card: no border, no fill, no shadow.
|
|
441
|
+
Framing a picture is what makes a screen look like a dashboard.
|
|
442
|
+
|
|
443
|
+
`grid` for three or fewer, `rail` for four or more — the renderer's call, from the content
|
|
444
|
+
(see PosterList). The rail runs past the right edge on purpose: a carousel that stops flush with
|
|
445
|
+
its container reads as a grid that failed, and one that bleeds says there is more to push through. */
|
|
446
|
+
.uir-posters { display: grid; gap: var(--uir-space-lg); }
|
|
447
|
+
.uir-posters[data-mode="grid"] { grid-template-columns: repeat(auto-fit, minmax(150px, 200px)); }
|
|
448
|
+
.uir-posters[data-mode="rail"] {
|
|
449
|
+
grid-auto-columns: minmax(150px, 184px);
|
|
450
|
+
grid-auto-flow: column;
|
|
451
|
+
overflow-x: auto;
|
|
452
|
+
overscroll-behavior-x: contain;
|
|
453
|
+
padding-bottom: var(--uir-space-sm);
|
|
454
|
+
scroll-snap-type: x proximity;
|
|
455
|
+
scrollbar-width: thin;
|
|
456
|
+
}
|
|
457
|
+
.uir-posters[data-mode="rail"] > .uir-poster { scroll-snap-align: start; }
|
|
458
|
+
|
|
459
|
+
.uir-poster {
|
|
460
|
+
background: none; border: 0; display: flex; flex-direction: column; gap: 2px;
|
|
461
|
+
min-width: 0; padding: 0; text-align: left; width: 100%;
|
|
462
|
+
}
|
|
463
|
+
.uir-poster__art {
|
|
464
|
+
aspect-ratio: 1 / 1; /* a record is square */
|
|
465
|
+
background: var(--uir-color-sunk);
|
|
466
|
+
border-radius: var(--uir-radius-md);
|
|
467
|
+
display: block;
|
|
468
|
+
margin-bottom: var(--uir-space-sm);
|
|
469
|
+
overflow: hidden;
|
|
470
|
+
position: relative;
|
|
471
|
+
width: 100%;
|
|
472
|
+
}
|
|
473
|
+
/* The ring is INSIDE the art, so a white-ground cover keeps an edge without the page growing a
|
|
474
|
+
border. It is the sleeve's own edge, not a frame around it. */
|
|
475
|
+
.uir-poster__art::after {
|
|
476
|
+
border-radius: inherit; box-shadow: inset 0 0 0 1px var(--uir-color-rule);
|
|
477
|
+
content: ""; inset: 0; pointer-events: none; position: absolute;
|
|
478
|
+
}
|
|
479
|
+
.uir-poster__art > img { display: block; height: 100%; object-fit: cover; width: 100%; }
|
|
480
|
+
/* No cover yet. A plate, not a broken-image glyph, and not a missing card. */
|
|
481
|
+
.uir-poster__art[data-empty] { background: var(--uir-color-sunk); }
|
|
482
|
+
|
|
483
|
+
.uir-poster__title {
|
|
484
|
+
color: var(--uir-color-ink); font-size: var(--uir-size-large); font-weight: 600;
|
|
485
|
+
letter-spacing: -0.01em; line-height: 1.25;
|
|
486
|
+
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
|
|
487
|
+
}
|
|
488
|
+
.uir-poster__caption {
|
|
489
|
+
color: var(--uir-color-graphite); font-size: var(--uir-size-small); line-height: 1.45;
|
|
490
|
+
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* One micro-interaction, and only on things you can actually open. */
|
|
494
|
+
.uir-poster[data-clickable="true"] { cursor: pointer; }
|
|
495
|
+
/* The transition lives on `.uir-img`, which every sleeve's art now is. Re-declaring it here would
|
|
496
|
+
replace the blur-up's `filter` transition rather than add to it. */
|
|
497
|
+
.uir-poster[data-clickable="true"]:hover .uir-poster__art > img { transform: scale(1.03); }
|
|
498
|
+
.uir-poster[data-clickable="true"]:hover .uir-poster__title { color: var(--uir-color-brand); }
|
|
499
|
+
.uir-poster:focus-visible { border-radius: var(--uir-radius-md); outline: 2px solid var(--uir-color-focus); outline-offset: 3px; }
|
|
500
|
+
@media (prefers-reduced-motion: reduce) {
|
|
501
|
+
.uir-poster[data-clickable="true"]:hover .uir-poster__art > img { transform: none; }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/* A shelf needs the width of the page, not a column of a grid — and no tile around it. A `list` is a
|
|
505
|
+
tile, and a tile has a border, a fill and a shadow; drawn around a row of sleeves that is a frame
|
|
506
|
+
around a frame around a picture. The pictures ARE the objects. Nothing else needs an edge. */
|
|
507
|
+
.uir-arrange:not([data-arrangement="stack"]) > .uir-block[data-type="list"]:has(.uir-posters) { grid-column: 1 / -1; }
|
|
508
|
+
.uir-block[data-tile][data-type="list"]:has(.uir-posters) {
|
|
509
|
+
background: none;
|
|
510
|
+
border: 0;
|
|
511
|
+
box-shadow: none;
|
|
512
|
+
padding: 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/* ── detail ───────────────────────────────────────────────────────────────────
|
|
516
|
+
A record has a name, a state, and then some facts, and it must not say all three in one voice.
|
|
517
|
+
The name is set on the METRIC ladder (tokens.js `size`), not on a new scale — a record's name and
|
|
518
|
+
a metric's value are the same rank of thing, and `emphasis` already governs that rank. Everything
|
|
519
|
+
below the name is deliberately quiet: the facts are a caption, not a competing column of text. */
|
|
520
|
+
.uir-detail { display: flex; flex-direction: column; gap: var(--uir-space-lg); margin: 0; }
|
|
521
|
+
.uir-detail__head { display: flex; flex-direction: column; gap: var(--uir-space-sm); align-items: flex-start; }
|
|
522
|
+
.uir-detail__name {
|
|
523
|
+
color: var(--uir-color-ink); font-size: var(--uir-size-normal-value); font-weight: 600;
|
|
524
|
+
letter-spacing: -0.01em; line-height: 1.15; margin: 0; text-wrap: balance;
|
|
525
|
+
}
|
|
526
|
+
/* Emphasis is type scale, never colour or rule weight (tokens.js). Same ladder as `metric`. */
|
|
527
|
+
.uir-detail[data-emphasis="hero"] .uir-detail__name { font-size: var(--uir-size-hero-value); letter-spacing: -0.02em; line-height: 1.05; }
|
|
528
|
+
.uir-detail[data-emphasis="primary"] .uir-detail__name { font-size: var(--uir-size-primary-value); letter-spacing: -0.015em; line-height: 1.1; }
|
|
529
|
+
.uir-detail[data-emphasis="subtle"] .uir-detail__name { color: var(--uir-color-muted); font-size: var(--uir-size-subtle-value); font-weight: 500; }
|
|
530
|
+
|
|
531
|
+
.uir-detail__badges { display: flex; flex-wrap: wrap; gap: var(--uir-space-sm); }
|
|
532
|
+
|
|
533
|
+
.uir-detail__facts { display: grid; gap: 0; margin: 0; }
|
|
534
|
+
.uir-detail__row { align-items: baseline; display: grid; gap: var(--uir-space-md); grid-template-columns: minmax(110px, 26%) 1fr; padding: 7px 0; }
|
|
535
|
+
.uir-detail__row + .uir-detail__row { border-top: 1px solid var(--uir-color-rule); }
|
|
536
|
+
/* A headed detail's facts are metadata. The rules between them go from `rule` to `ruleSoft`: they
|
|
537
|
+
separate a caption, and a caption does not need a table's scaffolding to be read. */
|
|
538
|
+
.uir-detail[data-headed] .uir-detail__row + .uir-detail__row { border-top-color: var(--uir-color-rule-soft); }
|
|
539
|
+
.uir-detail dt { color: var(--uir-color-graphite); font-size: var(--uir-size-small); }
|
|
540
|
+
.uir-detail__value { font-size: var(--uir-size-small); font-variant-numeric: tabular-nums; margin: 0; }
|
|
541
|
+
.uir-detail__value[data-numeric="true"] { font-variant-numeric: tabular-nums; }
|
|
542
|
+
.uir-detail__link { color: var(--uir-color-brand); text-decoration: none; overflow-wrap: anywhere; }
|
|
543
|
+
.uir-detail__link:hover { text-decoration: underline; }
|
|
544
|
+
|
|
545
|
+
/* `layout.density` is a container's word about its contents, and a detail's rows are contents. */
|
|
546
|
+
.uir-section[data-density="compact"] .uir-detail__row { padding: 3px 0; }
|
|
547
|
+
.uir-section[data-density="compact"] .uir-detail { gap: var(--uir-space-md); }
|
|
548
|
+
|
|
549
|
+
/* ── the hero record card: a sleeve, not two stacked boxes ────────────────────
|
|
550
|
+
When a card composes MEDIA and DETAIL, the two are not siblings in a stack — they are one object
|
|
551
|
+
seen and one object named. The cover bleeds to three edges of the card and squares off, which is
|
|
552
|
+
what a record sleeve does, and the name sits against it rather than under it.
|
|
553
|
+
|
|
554
|
+
Inferred from the blocks the author composed, never from a new prop: `:has()` reads the same
|
|
555
|
+
document the voice renderer reads, and a renderer that cannot do `:has()` simply draws the stack. */
|
|
556
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) {
|
|
557
|
+
column-gap: var(--uir-space-xl);
|
|
558
|
+
display: grid;
|
|
559
|
+
grid-template-columns: minmax(150px, 32%) 1fr;
|
|
560
|
+
/* THE THIRD ROW EXISTS TO ABSORB SLACK, and it is the whole reason this composition works.
|
|
561
|
+
A grid item spanning several tracks has its height distributed across the INTRINSIC ones
|
|
562
|
+
(`auto`, `min-content`, `max-content`). With `grid-auto-rows: min-content`, the cover — taller
|
|
563
|
+
than the two text rows put together — inflated both of them, and the record's name floated into
|
|
564
|
+
the middle of the card with a void above it. `1fr` is flexible, not intrinsic, so the spanning
|
|
565
|
+
cover's extra height lands there and the badge and the name stay where they were put. */
|
|
566
|
+
grid-template-rows: min-content min-content 1fr;
|
|
567
|
+
/* The cover's height is the card's; text alone would letterbox it into a strip. */
|
|
568
|
+
min-height: 232px;
|
|
569
|
+
row-gap: var(--uir-space-md);
|
|
570
|
+
}
|
|
571
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) > .uir-block {
|
|
572
|
+
grid-column: 2;
|
|
573
|
+
min-width: 0;
|
|
574
|
+
}
|
|
575
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) > .uir-block[data-type="media"] {
|
|
576
|
+
/* `.uir-arrange` aligns its items to `start`, which is right for tiles of unequal height and wrong
|
|
577
|
+
for a plate that must fill the card. Its image is out of flow, so without this the block is
|
|
578
|
+
exactly zero pixels tall and the cover does not exist. */
|
|
579
|
+
align-self: stretch;
|
|
580
|
+
grid-column: 1;
|
|
581
|
+
grid-row: 1 / -1;
|
|
582
|
+
/* Cancel the card's own padding on the three edges the cover reaches. */
|
|
583
|
+
margin: calc(var(--uir-space-lg) * -1) 0 calc(var(--uir-space-lg) * -1) calc(var(--uir-space-lg) * -1);
|
|
584
|
+
}
|
|
585
|
+
/* `order` is honoured by grid auto-placement, so the status chip lands beside the name rather than
|
|
586
|
+
beneath the facts — without touching the document's block order, which is the author's claim. */
|
|
587
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) > .uir-block[data-type="badge"] { order: -1; }
|
|
588
|
+
|
|
589
|
+
/* The image is taken OUT OF FLOW so it contributes no height of its own to the tracks it spans —
|
|
590
|
+
see the note on the third row. It fills the plate it is given, and `cover` crops rather than
|
|
591
|
+
distorts: a sleeve with the wrong aspect ratio is a different artwork. */
|
|
592
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) .uir-media {
|
|
593
|
+
height: 100%;
|
|
594
|
+
position: relative;
|
|
595
|
+
}
|
|
596
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) .uir-media__image {
|
|
597
|
+
border-radius: var(--uir-radius-lg) 0 0 var(--uir-radius-lg);
|
|
598
|
+
height: 100%;
|
|
599
|
+
inset: 0;
|
|
600
|
+
object-fit: cover;
|
|
601
|
+
position: absolute;
|
|
602
|
+
width: 100%;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/* Narrow: the sleeve goes back on top, square, and the name reads beneath it. Everything the wide
|
|
606
|
+
composition needed — the spanned column, the slack row, the out-of-flow image — is undone here,
|
|
607
|
+
because a single column has no span to absorb and no plate to fill. */
|
|
608
|
+
@media (max-width: 640px) {
|
|
609
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) {
|
|
610
|
+
grid-template-columns: 1fr;
|
|
611
|
+
grid-template-rows: none;
|
|
612
|
+
min-height: 0;
|
|
613
|
+
}
|
|
614
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) > .uir-block,
|
|
615
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) > .uir-block[data-type="media"] {
|
|
616
|
+
grid-column: 1;
|
|
617
|
+
grid-row: auto;
|
|
618
|
+
}
|
|
619
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) > .uir-block[data-type="media"] {
|
|
620
|
+
margin: calc(var(--uir-space-lg) * -1) calc(var(--uir-space-lg) * -1) 0;
|
|
621
|
+
}
|
|
622
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) .uir-media {
|
|
623
|
+
height: auto;
|
|
624
|
+
position: static;
|
|
625
|
+
}
|
|
626
|
+
.uir-card > .uir-arrange:has(> .uir-block[data-type="media"]):has(> .uir-block[data-type="detail"]) .uir-media__image {
|
|
627
|
+
aspect-ratio: 1 / 1;
|
|
628
|
+
border-radius: var(--uir-radius-lg) var(--uir-radius-lg) 0 0;
|
|
629
|
+
height: auto;
|
|
630
|
+
position: static;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/* ── timeline: the distance between two items reflects the distance between their dates. ── */
|
|
635
|
+
.uir-timeline { position: relative; }
|
|
636
|
+
.uir-timeline__track { height: 224px; position: relative; }
|
|
637
|
+
.uir-timeline__axis { background: var(--uir-color-rule); height: 1px; left: 0; position: absolute; right: 0; top: 50%; }
|
|
638
|
+
.uir-timeline__today-line { border-left: 1px dashed var(--uir-color-rule-strong); bottom: 50%; position: absolute; top: 2px; width: 0; }
|
|
639
|
+
.uir-timeline__today {
|
|
640
|
+
color: var(--uir-color-faint); font-family: var(--uir-font-mono); font-size: 9.5px;
|
|
641
|
+
position: absolute; top: -6px; transform: translateX(-50%);
|
|
642
|
+
}
|
|
643
|
+
.uir-timeline__ticks { bottom: 16px; left: 0; position: absolute; right: 0; }
|
|
644
|
+
.uir-timeline__ticks span { color: var(--uir-color-faint); font-size: var(--uir-size-micro); position: absolute; transform: translateX(-50%); }
|
|
645
|
+
.uir-timeline__item { bottom: 0; position: absolute; top: 0; width: 0; }
|
|
646
|
+
.uir-timeline__dot {
|
|
647
|
+
background: var(--uir-color-brand); border: 2px solid var(--uir-color-sheet); border-radius: 50%;
|
|
648
|
+
height: 10px; left: 0; position: absolute; top: 50%; transform: translate(-50%, -50%); width: 10px; z-index: 2;
|
|
649
|
+
}
|
|
650
|
+
.uir-timeline__span { background: var(--uir-color-brand); height: 3px; opacity: 0.22; position: absolute; top: 50%; transform: translateY(-50%); }
|
|
651
|
+
.uir-timeline__stem { background: var(--uir-color-rule-strong); left: 0; position: absolute; width: 1px; }
|
|
652
|
+
/* Four lanes. The stem grows to reach whichever lane its caption sits in, so every label is still
|
|
653
|
+
tied to its own dot — a caption that floats free of its point is worse than a collision. */
|
|
654
|
+
.uir-timeline__item[data-side="above"] .uir-timeline__stem { bottom: 50%; height: 16px; }
|
|
655
|
+
.uir-timeline__item[data-side="below"] .uir-timeline__stem { height: 16px; top: 50%; }
|
|
656
|
+
.uir-timeline__item[data-lane="2"] .uir-timeline__stem { height: 58px; }
|
|
657
|
+
.uir-timeline__item[data-lane="3"] .uir-timeline__stem { height: 58px; }
|
|
658
|
+
|
|
659
|
+
.uir-timeline__caption { position: absolute; text-align: center; transform: translateX(-50%); width: 104px; }
|
|
660
|
+
.uir-timeline__item[data-side="above"] .uir-timeline__caption { bottom: calc(50% + 18px); }
|
|
661
|
+
.uir-timeline__item[data-side="below"] .uir-timeline__caption { display: flex; flex-direction: column-reverse; top: calc(50% + 18px); }
|
|
662
|
+
.uir-timeline__item[data-lane="2"] .uir-timeline__caption { bottom: calc(50% + 60px); }
|
|
663
|
+
.uir-timeline__item[data-lane="3"] .uir-timeline__caption { top: calc(50% + 60px); }
|
|
664
|
+
.uir-timeline__label {
|
|
665
|
+
-webkit-box-orient: vertical; -webkit-line-clamp: 2; display: -webkit-box; overflow: hidden;
|
|
666
|
+
font-family: var(--uir-font-body); font-size: var(--uir-size-small); font-weight: 500; letter-spacing: -0.003em; line-height: 1.2;
|
|
667
|
+
}
|
|
668
|
+
.uir-timeline__date { color: var(--uir-color-faint); font-size: var(--uir-size-micro); margin-top: 2px; white-space: nowrap; }
|
|
669
|
+
.uir-timeline__item[data-side="below"] .uir-timeline__date { margin: 0 0 2px; }
|
|
670
|
+
|
|
671
|
+
/* ── progress ─────────────────────────────────────────────────────────────── */
|
|
672
|
+
.uir-progress__head { align-items: baseline; display: flex; gap: var(--uir-space-md); justify-content: space-between; margin-bottom: 8px; }
|
|
673
|
+
.uir-progress__track { background: var(--uir-color-sunk); border-radius: 999px; height: 6px; overflow: hidden; }
|
|
674
|
+
/* `obBarIn` from design sheet `2a`: the bar draws itself in from zero on arrival, and TRANSITIONS
|
|
675
|
+
thereafter — a re-resolved value slides, it does not redraw from nothing. Two different events,
|
|
676
|
+
two different motions, which is why the sheet specifies both. */
|
|
677
|
+
.uir-progress__fill {
|
|
678
|
+
animation: uir-bar-in var(--uir-motion-bar-in);
|
|
679
|
+
background: var(--uir-color-brand);
|
|
680
|
+
border-radius: 999px;
|
|
681
|
+
height: 100%;
|
|
682
|
+
transition: width 420ms cubic-bezier(0.2, 0.7, 0.2, 1);
|
|
683
|
+
}
|
|
684
|
+
@keyframes uir-bar-in { from { width: 0; } }
|
|
685
|
+
.uir-progress__value { font-size: var(--uir-size-small); font-variant-numeric: tabular-nums; font-weight: 500; }
|
|
686
|
+
.uir-progress__ratio { color: var(--uir-color-graphite); font-weight: 400; }
|
|
687
|
+
|
|
688
|
+
/* ── badge: a status, stated. An element, not a tile. ─────────────────────── */
|
|
689
|
+
.uir-badge {
|
|
690
|
+
align-items: center; border-radius: 999px; display: inline-flex; gap: 0.4em;
|
|
691
|
+
font-family: var(--uir-font-body); font-size: var(--uir-size-micro); font-weight: 500;
|
|
692
|
+
letter-spacing: 0; padding: 3px 10px; text-transform: none; white-space: nowrap;
|
|
693
|
+
}
|
|
694
|
+
/* The same badge, inside a row. It sits on the cell's baseline rather than claiming its own line,
|
|
695
|
+
because a table's rows must stay the same height or the column stops being scannable. */
|
|
696
|
+
.uir-badge--cell { font-size: var(--uir-size-micro); vertical-align: baseline; }
|
|
697
|
+
.uir-badge[data-tone="positive"] { background: var(--uir-color-positive-soft); color: var(--uir-color-positive); }
|
|
698
|
+
.uir-badge[data-tone="warning"] { background: var(--uir-color-warning-soft); color: var(--uir-color-warning); }
|
|
699
|
+
.uir-badge[data-tone="negative"] { background: var(--uir-color-negative-soft); color: var(--uir-color-negative); }
|
|
700
|
+
.uir-badge[data-tone="info"] { background: var(--uir-color-info-soft); color: var(--uir-color-info); }
|
|
701
|
+
.uir-badge[data-tone="neutral"] { background: var(--uir-color-neutral-soft); color: var(--uir-color-neutral); }
|
|
702
|
+
.uir-badge__label { color: var(--uir-color-graphite); font-weight: 400; }
|
|
703
|
+
|
|
704
|
+
/* ── text: prose on the surface. No tile, no quote bar. ───────────────────── */
|
|
705
|
+
.uir-text { font-size: var(--uir-size-small); margin: 0; max-width: 70ch; }
|
|
706
|
+
.uir-text[data-emphasis="hero"] { font-size: var(--uir-size-large); font-weight: 600; letter-spacing: -0.01em; }
|
|
707
|
+
.uir-text[data-emphasis="primary"] { font-size: var(--uir-size-base); font-weight: 500; }
|
|
708
|
+
.uir-text[data-emphasis="subtle"] { color: var(--uir-color-graphite); }
|
|
709
|
+
|
|
710
|
+
/* ── media ────────────────────────────────────────────────────────────────── */
|
|
711
|
+
/* ── Calendar. A bounded period, drawn whole. ──────────────────────────────────
|
|
712
|
+
Every day of the period is present, including the empty ones: a month with its empty days removed
|
|
713
|
+
is not a month, it is a list — and the block for lists is `list`. */
|
|
714
|
+
.uir-calendar__head { align-items: baseline; display: flex; justify-content: space-between; margin-bottom: var(--uir-space-md); }
|
|
715
|
+
.uir-calendar__count { color: var(--uir-color-faint); font-size: var(--uir-size-micro); }
|
|
716
|
+
.uir-calendar__grid { display: grid; gap: 1px; grid-template-columns: repeat(7, minmax(0, 1fr)); }
|
|
717
|
+
.uir-calendar__weekday {
|
|
718
|
+
color: var(--uir-color-faint);
|
|
719
|
+
font-size: var(--uir-size-micro);
|
|
720
|
+
font-weight: 500;
|
|
721
|
+
padding-bottom: var(--uir-space-sm);
|
|
722
|
+
text-align: center;
|
|
723
|
+
}
|
|
724
|
+
.uir-calendar__cell {
|
|
725
|
+
background: var(--uir-color-sheet);
|
|
726
|
+
border-radius: var(--uir-radius-sm);
|
|
727
|
+
display: flex;
|
|
728
|
+
flex-direction: column;
|
|
729
|
+
gap: 2px;
|
|
730
|
+
min-height: 62px;
|
|
731
|
+
outline: 1px solid var(--uir-color-rule-soft);
|
|
732
|
+
padding: var(--uir-space-sm);
|
|
733
|
+
}
|
|
734
|
+
/* The days that make the weeks line up. Present, because the shape of the month depends on them;
|
|
735
|
+
quiet, because they are not in the period the reader asked about. */
|
|
736
|
+
.uir-calendar__cell[data-outside] { opacity: 0.38; }
|
|
737
|
+
.uir-calendar__cell[data-occupied] { background: var(--uir-color-brand-soft); }
|
|
738
|
+
.uir-calendar__date { color: var(--uir-color-graphite); font-size: var(--uir-size-micro); font-variant-numeric: tabular-nums; }
|
|
739
|
+
.uir-calendar__item {
|
|
740
|
+
color: var(--uir-color-ink);
|
|
741
|
+
font-size: var(--uir-size-micro);
|
|
742
|
+
overflow: hidden;
|
|
743
|
+
text-overflow: ellipsis;
|
|
744
|
+
white-space: nowrap;
|
|
745
|
+
}
|
|
746
|
+
.uir-calendar[data-span="week"] .uir-calendar__cell { min-height: 96px; }
|
|
747
|
+
|
|
748
|
+
.uir-media { margin: 0; }
|
|
749
|
+
.uir-media__image { border-radius: var(--uir-radius-md); display: block; max-width: 100%; }
|
|
750
|
+
.uir-media__caption { color: var(--uir-color-graphite); font-size: var(--uir-size-small); margin-top: var(--uir-space-sm); }
|
|
751
|
+
.uir-audio { align-items: center; display: flex; gap: var(--uir-space-md); }
|
|
752
|
+
.uir-audio > audio { display: none; }
|
|
753
|
+
.uir-audio > button {
|
|
754
|
+
align-items: center; background: var(--uir-color-ink); border: 0; border-radius: 50%; color: var(--uir-color-sheet);
|
|
755
|
+
cursor: pointer; display: inline-flex; flex: none; height: 32px; justify-content: center; padding: 0; width: 32px;
|
|
756
|
+
}
|
|
757
|
+
.uir-audio > button:hover { background: var(--uir-color-muted); }
|
|
758
|
+
.uir-audio > button span { font-size: 10px; transform: translateX(1px); }
|
|
759
|
+
.uir-audio__body { flex: 1; min-width: 0; }
|
|
760
|
+
.uir-audio__line { align-items: baseline; display: flex; gap: 10px; justify-content: space-between; }
|
|
761
|
+
.uir-audio__line strong { font-size: 13.5px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
762
|
+
.uir-audio__line span { color: var(--uir-color-graphite); flex: none; font-size: var(--uir-size-small); font-variant-numeric: tabular-nums; }
|
|
763
|
+
.uir-audio__track { background: var(--uir-color-rule-soft); border-radius: 2px; height: 3px; margin-top: 8px; overflow: hidden; }
|
|
764
|
+
.uir-audio__track span { background: var(--uir-color-ink); display: block; height: 100%; }
|
|
765
|
+
|
|
766
|
+
/* The frame reserves its box from `aspect` BEFORE the provider loads. Nothing else in the renderer
|
|
767
|
+
knows a third-party frame's shape, and a page that reflows around one is a page that jumps. */
|
|
768
|
+
.uir-media__frame {
|
|
769
|
+
background: var(--uir-color-sunk);
|
|
770
|
+
border-radius: var(--uir-radius-md);
|
|
771
|
+
overflow: hidden;
|
|
772
|
+
width: 100%;
|
|
773
|
+
}
|
|
774
|
+
/* A portrait frame given the tile's full width would be taller than the viewport. `aspect` is the
|
|
775
|
+
semantic claim the page made; the cap is this renderer's answer to it, not a second claim. */
|
|
776
|
+
.uir-media__frame[data-aspect="9:16"] { max-width: 320px; }
|
|
777
|
+
.uir-media__embed { border: 0; display: block; height: 100%; width: 100%; }
|
|
778
|
+
.uir-embed-meta { align-items: center; display: flex; gap: 9px; padding: 11px 6px 2px; }
|
|
779
|
+
.uir-embed-meta__icon {
|
|
780
|
+
align-items: center; background: var(--uir-color-sunk); border: 1px solid var(--uir-color-rule);
|
|
781
|
+
border-radius: 4px; color: var(--uir-color-graphite); display: inline-flex; flex: none; font-size: 9px;
|
|
782
|
+
font-weight: 600; height: 16px; justify-content: center; width: 16px;
|
|
783
|
+
}
|
|
784
|
+
.uir-embed-meta__copy { display: flex; flex: 1; flex-direction: column; min-width: 0; }
|
|
785
|
+
.uir-embed-meta__copy strong { font-size: 12.5px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
786
|
+
.uir-embed-meta__copy span { color: var(--uir-color-faint); font-family: var(--uir-font-mono); font-size: 10.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
787
|
+
.uir-embed-meta a { color: var(--uir-color-graphite); flex: none; font-size: var(--uir-size-small); font-weight: 500; text-decoration: none; }
|
|
788
|
+
.uir-embed-meta a:hover { color: var(--uir-color-ink); }
|
|
789
|
+
|
|
790
|
+
/* ── input / button ───────────────────────────────────────────────────────── */
|
|
791
|
+
.uir-field { display: flex; flex-direction: column; gap: 5px; max-width: 280px; }
|
|
792
|
+
.uir-input, .uir-select {
|
|
793
|
+
appearance: none; background: var(--uir-color-sheet); border: 1px solid var(--uir-color-rule-strong);
|
|
794
|
+
border-radius: var(--uir-radius-md); color: var(--uir-color-ink); font-family: var(--uir-font-body);
|
|
795
|
+
font-size: var(--uir-size-small); padding: 7px 10px; width: 100%;
|
|
796
|
+
}
|
|
797
|
+
.uir-select {
|
|
798
|
+
background-image: linear-gradient(45deg, transparent 50%, var(--uir-color-graphite) 50%), linear-gradient(135deg, var(--uir-color-graphite) 50%, transparent 50%);
|
|
799
|
+
background-position: calc(100% - 16px) 50%, calc(100% - 11px) 50%;
|
|
800
|
+
background-repeat: no-repeat; background-size: 5px 5px, 5px 5px; padding-right: 30px;
|
|
801
|
+
}
|
|
802
|
+
.uir-select-row { align-items: center; display: flex; gap: 10px; }
|
|
803
|
+
.uir-select-row .uir-select { flex: 1; min-width: 0; }
|
|
804
|
+
.uir-select-clear { background: none; border: 0; color: var(--uir-color-graphite); cursor: pointer; font-family: inherit; font-size: 12.5px; font-weight: 500; padding: 4px 2px; }
|
|
805
|
+
.uir-select-clear:hover { color: var(--uir-color-ink); }
|
|
806
|
+
.uir-input:focus-visible, .uir-select:focus-visible, .uir-button:focus-visible, .uir-tab:focus-visible,
|
|
807
|
+
.uir-list__item:focus-visible, .uir-table tbody tr:focus-visible, .uir-metric--clickable:focus-visible {
|
|
808
|
+
outline: 2px solid var(--uir-color-focus); outline-offset: 2px;
|
|
809
|
+
}
|
|
810
|
+
.uir-toggle { align-items: center; display: flex; font-size: 13.5px; justify-content: space-between; max-width: 280px; padding: 7px 0; }
|
|
811
|
+
.uir-toggle > button {
|
|
812
|
+
background: var(--uir-color-rule); border: 0; border-radius: 11px; cursor: pointer; display: flex;
|
|
813
|
+
height: 22px; padding: 2px; transition: background 150ms ease; width: 36px;
|
|
814
|
+
}
|
|
815
|
+
.uir-toggle > button > span {
|
|
816
|
+
background: var(--uir-color-sheet); border-radius: 50%; box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
|
817
|
+
display: block; height: 18px; transform: translateX(0); transition: transform 150ms ease; width: 18px;
|
|
818
|
+
}
|
|
819
|
+
.uir-toggle > button[aria-checked="true"] { background: var(--uir-color-brand); }
|
|
820
|
+
.uir-toggle > button[aria-checked="true"] > span { transform: translateX(14px); }
|
|
821
|
+
|
|
822
|
+
.uir-button {
|
|
823
|
+
background: var(--uir-color-ink); border: 1px solid var(--uir-color-ink); border-radius: var(--uir-radius-md);
|
|
824
|
+
color: var(--uir-color-sheet); cursor: pointer; font-family: var(--uir-font-body); font-size: var(--uir-size-small);
|
|
825
|
+
font-weight: 500; letter-spacing: 0; padding: 7px 14px; text-transform: none;
|
|
826
|
+
transition: background 120ms ease, border-color 120ms ease;
|
|
827
|
+
}
|
|
828
|
+
.uir-button:hover:not(:disabled) { background: var(--uir-color-brand); border-color: var(--uir-color-brand); }
|
|
829
|
+
.uir-button--ghost { background: var(--uir-color-sheet); border-color: var(--uir-color-rule-strong); color: var(--uir-color-ink); }
|
|
830
|
+
.uir-button--ghost:hover:not(:disabled) { background: var(--uir-color-brand-soft); border-color: var(--uir-color-brand); color: var(--uir-color-brand); }
|
|
831
|
+
.uir-button--danger { background: var(--uir-color-sheet); border-color: var(--uir-color-rule); color: var(--uir-color-negative); }
|
|
832
|
+
.uir-button--danger:hover:not(:disabled) { background: var(--uir-color-negative-soft); border-color: var(--uir-color-negative); color: var(--uir-color-negative); }
|
|
833
|
+
.uir-button--confirm { background: #D92D20; border-color: #D92D20; color: #FFFFFF; }
|
|
834
|
+
.uir-button--confirm:hover:not(:disabled) { background: var(--uir-color-negative); border-color: var(--uir-color-negative); }
|
|
835
|
+
.uir-button--small { font-size: var(--uir-size-micro); padding: 4px 9px; }
|
|
836
|
+
.uir-button:disabled { cursor: not-allowed; opacity: 0.45; }
|
|
837
|
+
|
|
838
|
+
/* A disabled verb must say WHY. Anchored from the right so a 260px tooltip cannot widen a table. */
|
|
839
|
+
.uir-disabled-wrap { display: inline-flex; position: relative; }
|
|
840
|
+
.uir-disabled-wrap .uir-reason {
|
|
841
|
+
background: var(--uir-color-ink); border-radius: var(--uir-radius-md); bottom: calc(100% + 6px);
|
|
842
|
+
color: var(--uir-color-sheet); font-family: var(--uir-font-body); font-size: var(--uir-size-micro);
|
|
843
|
+
letter-spacing: 0; line-height: 1.35; opacity: 0; padding: 7px 9px; pointer-events: none;
|
|
844
|
+
position: absolute; right: 0; text-transform: none;
|
|
845
|
+
transition: opacity 110ms ease, visibility 110ms; visibility: hidden;
|
|
846
|
+
max-width: 260px; width: max-content; z-index: 5;
|
|
847
|
+
}
|
|
848
|
+
.uir-disabled-wrap:hover .uir-reason, .uir-disabled-wrap:focus-within .uir-reason { opacity: 1; visibility: visible; }
|
|
849
|
+
|
|
850
|
+
/* Default confirmation surface (`12 · Button`). */
|
|
851
|
+
.uir-confirm { align-items: center; display: flex; inset: 0; justify-content: center; position: fixed; z-index: 60; }
|
|
852
|
+
.uir-confirm__scrim { background: rgba(9,9,11,0.28); border: 0; inset: 0; padding: 0; position: absolute; }
|
|
853
|
+
.uir-confirm__panel {
|
|
854
|
+
background: var(--uir-color-sheet); border-radius: var(--uir-radius-lg); box-shadow: 0 12px 32px rgba(0,0,0,0.18);
|
|
855
|
+
box-sizing: border-box; padding: 18px 18px 14px; position: relative; width: min(280px, calc(100vw - 32px));
|
|
856
|
+
}
|
|
857
|
+
.uir-confirm__panel:focus { outline: none; }
|
|
858
|
+
.uir-confirm__panel h2 { font-size: 14.5px; font-weight: 600; margin: 0; }
|
|
859
|
+
.uir-confirm__panel p { color: var(--uir-color-muted); font-size: 12.5px; line-height: 1.5; margin: 6px 0 0; }
|
|
860
|
+
.uir-confirm__actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
|
|
861
|
+
.uir-confirm__actions .uir-button { border-radius: 7px; font-size: 12.5px; padding: 5px 12px; }
|
|
862
|
+
|
|
863
|
+
/* ── Progressive loading (`States — skeleton`). The tile shape holds while content streams. The
|
|
864
|
+
shimmer is the design's `obShimmer`: a 90deg sweep across a 480px-wide gradient, 1.4s linear. ── */
|
|
865
|
+
/* ── Arrival, row by row. ────────────────────────────────────────────────────
|
|
866
|
+
`data-arrived` is already set on the exact frame a block's data lands (UIResponsePage). The block folds
|
|
867
|
+
in; its rows rise in behind it, one short beat apart. Nothing new is invented — this is `uir-fold`
|
|
868
|
+
read one level deeper, so a view assembles like it is being poured rather than stamped.
|
|
869
|
+
|
|
870
|
+
`--uir-i` is the row's capped index (data.jsx). The delay is bounded by that cap, never by the
|
|
871
|
+
row count: a 3,000-row table finishes arriving in under half a second, like a 12-row one. */
|
|
872
|
+
.uir-block[data-arrived] .uir-table tbody tr,
|
|
873
|
+
.uir-block[data-arrived] .uir-list__item,
|
|
874
|
+
.uir-block[data-arrived] .uir-poster {
|
|
875
|
+
animation: uir-rise 0.34s cubic-bezier(0.2, 0.7, 0.3, 1) backwards;
|
|
876
|
+
animation-delay: calc(var(--uir-i, 0) * 34ms);
|
|
877
|
+
}
|
|
878
|
+
@keyframes uir-rise { from { opacity: 0; transform: translateY(6px); } }
|
|
879
|
+
|
|
880
|
+
@media (prefers-reduced-motion: reduce) {
|
|
881
|
+
.uir-block[data-arrived] .uir-table tbody tr,
|
|
882
|
+
.uir-block[data-arrived] .uir-list__item,
|
|
883
|
+
.uir-block[data-arrived] .uir-poster { animation: none; }
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
@keyframes uir-shimmer { from { background-position: -240px 0; } to { background-position: 240px 0; } }
|
|
887
|
+
.uir-skeleton { display: flex; flex-direction: column; padding-top: 0; }
|
|
888
|
+
.uir-skeleton__bar, .uir-skeleton__value {
|
|
889
|
+
animation: uir-shimmer var(--uir-motion-shimmer);
|
|
890
|
+
background: linear-gradient(90deg, var(--uir-shimmer-from) 25%, var(--uir-shimmer-to) 40%, var(--uir-shimmer-from) 60%);
|
|
891
|
+
background-size: 480px 100%;
|
|
892
|
+
}
|
|
893
|
+
.uir-skeleton__bar { border-radius: 5px; height: 10px; margin-top: 14px; }
|
|
894
|
+
.uir-skeleton__bar:first-child { margin-top: 0; width: 88px; }
|
|
895
|
+
.uir-skeleton__bar--wide { width: 100%; }
|
|
896
|
+
.uir-skeleton__bar--mid { width: 62%; }
|
|
897
|
+
.uir-skeleton__bar--narrow { width: 34%; }
|
|
898
|
+
.uir-skeleton__value { border-radius: 6px; height: 28px; margin-top: 12px; width: 150px; }
|
|
899
|
+
|
|
900
|
+
/* What the document already said, set in real type. It does not shimmer, and it does not move when
|
|
901
|
+
the rows arrive: these are the same styles the finished block uses. */
|
|
902
|
+
.uir-skeleton__heading { font-size: var(--uir-size-base); font-weight: 600; letter-spacing: -0.005em; margin: 0; }
|
|
903
|
+
.uir-skeleton__label { color: var(--uir-color-graphite); font-size: var(--uir-size-label); }
|
|
904
|
+
.uir-skeleton__cols {
|
|
905
|
+
border-bottom: 1px solid var(--uir-color-rule); color: var(--uir-color-faint); display: flex;
|
|
906
|
+
font-size: var(--uir-size-small); gap: var(--uir-space-lg); margin-top: var(--uir-space-md);
|
|
907
|
+
padding-bottom: var(--uir-space-sm);
|
|
908
|
+
}
|
|
909
|
+
.uir-loading-rule { display: none; }
|
|
910
|
+
.uir-block[data-stale="true"] { opacity: 0.45; transition: opacity 140ms ease; }
|
|
911
|
+
|
|
912
|
+
/* VALIDATION TICK (`States — validation`). Off by default: a tick means "this block was validated",
|
|
913
|
+
and the renderer does not validate — Phase 2 runs before it. The host says so with `validated`,
|
|
914
|
+
or nothing is drawn. A mark that appears whether or not the check ran is a decoration. */
|
|
915
|
+
@keyframes uir-tick {
|
|
916
|
+
0% { opacity: 0; transform: scale(0.6); }
|
|
917
|
+
15%, 75% { opacity: 1; transform: scale(1); }
|
|
918
|
+
100% { opacity: 0; transform: scale(1); }
|
|
919
|
+
}
|
|
920
|
+
.uir-tick {
|
|
921
|
+
align-items: center; animation: uir-tick var(--uir-motion-tick); background: var(--uir-color-sheet);
|
|
922
|
+
border-radius: 9px; box-shadow: 0 1px 4px rgba(0,0,0,0.14); display: inline-flex; height: 18px;
|
|
923
|
+
justify-content: center; pointer-events: none; position: absolute; right: -7px; top: -7px; width: 18px; z-index: 3;
|
|
924
|
+
}
|
|
925
|
+
.uir-tick svg { display: block; }
|
|
926
|
+
.uir-tick polyline { fill: none; stroke: var(--uir-color-tick); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
|
|
927
|
+
|
|
928
|
+
/* NARRATION — the commentary track. Prose, not chrome: the same voice as the chat thread's
|
|
929
|
+
sentences, sitting between tiles like a person talking you through what just landed. */
|
|
930
|
+
.uir-narration {
|
|
931
|
+
animation: uir-fold 0.45s cubic-bezier(0.2, 0.7, 0.3, 1);
|
|
932
|
+
color: var(--uir-color-ink); font-size: var(--uir-size-base); line-height: 1.6;
|
|
933
|
+
margin: 2px 2px 4px; text-wrap: pretty;
|
|
934
|
+
}
|
|
935
|
+
@media (prefers-reduced-motion: reduce) { .uir-narration { animation: none; } }
|
|
936
|
+
|
|
937
|
+
/* The tick is a transient flourish — it announces, holds, and fades. With motion off there is no
|
|
938
|
+
moment to announce, and a mark frozen at its end state is invisible anyway; drop it whole. */
|
|
939
|
+
@media (prefers-reduced-motion: reduce) {
|
|
940
|
+
.uir-tick { display: none; }
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/* ── An error is one TILE that failed, not an outage (`States — error`). It says so at tile scale,
|
|
944
|
+
names the thing it could not resolve in mono, and offers the one action that can help. ── */
|
|
945
|
+
.uir-error { align-items: flex-start; display: flex; gap: 11px; min-width: 0; }
|
|
946
|
+
.uir-error__mark {
|
|
947
|
+
align-items: center; background: var(--uir-color-negative-soft); border-radius: 9px;
|
|
948
|
+
color: var(--uir-color-negative); display: inline-flex; flex: none; font-size: var(--uir-size-micro);
|
|
949
|
+
font-weight: 600; height: 18px; justify-content: center; line-height: 1; margin-top: 1px; width: 18px;
|
|
950
|
+
}
|
|
951
|
+
.uir-error__body { display: flex; flex-direction: column; min-width: 0; }
|
|
952
|
+
.uir-error__label { color: var(--uir-color-ink); font-size: var(--uir-size-label); font-weight: 500; }
|
|
953
|
+
.uir-error__msg {
|
|
954
|
+
-webkit-box-orient: vertical; -webkit-line-clamp: 3; color: var(--uir-color-graphite); display: -webkit-box;
|
|
955
|
+
font-size: var(--uir-size-small); line-height: 1.45; margin-top: 3px; overflow: hidden;
|
|
956
|
+
}
|
|
957
|
+
.uir-error__msg code { background: none; font-family: var(--uir-font-mono); font-size: var(--uir-size-micro); padding: 0; }
|
|
958
|
+
.uir-error__retry {
|
|
959
|
+
align-self: flex-start; background: var(--uir-color-sheet); border: 1px solid var(--uir-color-rule);
|
|
960
|
+
border-radius: var(--uir-radius-sm); color: var(--uir-color-muted); cursor: pointer;
|
|
961
|
+
font-family: inherit; font-size: var(--uir-size-small); font-weight: 500; margin-top: 9px; padding: 3px 10px;
|
|
962
|
+
}
|
|
963
|
+
.uir-error__retry:hover { background: var(--uir-color-sunk-strong); color: var(--uir-color-ink); }
|
|
964
|
+
|
|
965
|
+
/* ── chart: the plot fills its tile; nothing frames it twice. ─────────────── */
|
|
966
|
+
.uir-chart-frame { margin: 0; }
|
|
967
|
+
|
|
968
|
+
/* Multi-series legend (`02 · Chart`). Value beside its label, tabular, not hidden in a tooltip. */
|
|
969
|
+
.uir-chart__legend { display: flex; flex-wrap: wrap; gap: 18px; margin: 8px 0 10px; }
|
|
970
|
+
.uir-chart__legend-item { align-items: center; display: inline-flex; gap: 6px; }
|
|
971
|
+
.uir-chart__swatch { border-radius: 4px; height: 8px; width: 8px; }
|
|
972
|
+
.uir-chart__legend-label { color: var(--uir-color-muted); font-size: var(--uir-size-small); }
|
|
973
|
+
.uir-chart__legend-value { color: var(--uir-color-ink); font-size: var(--uir-size-small); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
974
|
+
.uir-chart-frame__inner { width: 100%; }
|
|
975
|
+
|
|
976
|
+
/* pie/donut: the ring is sized within the tile and the legend reads beside it, WITH values.
|
|
977
|
+
Their built-in legend puts labels under a ring that grows to fill whatever box it is given —
|
|
978
|
+
which is how a four-slice donut became a bloated hoop with three slivers bunched at the top. */
|
|
979
|
+
.uir-pie { align-items: center; display: grid; gap: var(--uir-space-xl); grid-template-columns: auto minmax(0, auto); justify-content: start; }
|
|
980
|
+
@media (max-width: 460px) { .uir-pie { grid-template-columns: minmax(0, 1fr); justify-items: center; } }
|
|
981
|
+
/* The legend reads BESIDE the ring, and its value sits next to its label — not flung to the far
|
|
982
|
+
edge of the tile. A percentage a reader has to track across 600px is a percentage they will not read. */
|
|
983
|
+
.uir-pie__legend { display: flex; flex-direction: column; gap: 8px; min-width: 0; width: min(340px, 100%); }
|
|
984
|
+
.uir-pie__row { align-items: center; display: grid; gap: var(--uir-space-sm); grid-template-columns: 9px minmax(0, 1fr) auto auto; }
|
|
985
|
+
.uir-pie__swatch { border-radius: 2px; height: 9px; width: 9px; }
|
|
986
|
+
.uir-pie__label { color: var(--uir-color-ink); font-size: var(--uir-size-small); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
987
|
+
.uir-pie__value { font-size: var(--uir-size-small); font-variant-numeric: tabular-nums; font-weight: 500; }
|
|
988
|
+
.uir-pie__pct { color: var(--uir-color-faint); font-size: var(--uir-size-small); font-variant-numeric: tabular-nums; min-width: 3.4em; text-align: right; }
|
|
989
|
+
|
|
990
|
+
@media (prefers-reduced-motion: reduce) {
|
|
991
|
+
.uir-page *, .uir-page *::before, .uir-page *::after {
|
|
992
|
+
animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important;
|
|
993
|
+
}
|
|
994
|
+
.uir-loading-rule::after { transform: scaleX(0.35); }
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/* ── Stage checklist. An ordered run, and the order is the content. ────────────
|
|
998
|
+
Not a rail with a travelling marker: the marker is a drawing of "the first stage not yet done",
|
|
999
|
+
which the words already say. A renderer that drew only the words would lose nothing. */
|
|
1000
|
+
.uir-stages { display: flex; flex-wrap: wrap; gap: var(--uir-space-sm); list-style: none; margin: 0; padding: 0; }
|
|
1001
|
+
.uir-stages__stage {
|
|
1002
|
+
align-items: center;
|
|
1003
|
+
background: var(--uir-color-sunk);
|
|
1004
|
+
border-radius: 999px;
|
|
1005
|
+
color: var(--uir-color-graphite);
|
|
1006
|
+
display: inline-flex;
|
|
1007
|
+
font-size: var(--uir-size-micro);
|
|
1008
|
+
gap: 5px;
|
|
1009
|
+
padding: 4px 10px;
|
|
1010
|
+
}
|
|
1011
|
+
.uir-stages__stage[data-done] { background: var(--uir-color-positive-soft); color: var(--uir-color-positive); }
|
|
1012
|
+
/* Where you are. A ring, not a fill: the stage is not done, and a filled chip would say it was. */
|
|
1013
|
+
.uir-stages__stage[data-current] { box-shadow: inset 0 0 0 1px var(--uir-color-brand); color: var(--uir-color-ink); }
|
|
1014
|
+
.uir-stages__mark { font-size: 10px; line-height: 1; }
|
|
1015
|
+
.uir-stages__label { font-weight: 500; }
|
|
1016
|
+
/* The state is said, not implied by colour — a page read aloud, or read by someone who cannot
|
|
1017
|
+
distinguish the two greens, must still know which stages are behind. */
|
|
1018
|
+
.uir-stages__state { opacity: 0.72; }
|
|
1019
|
+
|
|
1020
|
+
/* ── Overlay. A page PRESENTED, not a page navigated to (DECISIONS A13). ──────
|
|
1021
|
+
Geometry and motion are extracted from design sheet `2a · Chat to page prototype`, not chosen:
|
|
1022
|
+
372px, right, 1px left border, -16px 0 40px shadow, obSlideIn 0.32s, obFade 0.25s on an
|
|
1023
|
+
rgba(9,9,11,0.18) scrim. Nothing about a drawer is in the schema — `mode: "overlay"` says only
|
|
1024
|
+
that the user has not left, and this is one medium's answer to that claim. */
|
|
1025
|
+
.uir-overlay { inset: 0; position: fixed; z-index: 40; }
|
|
1026
|
+
.uir-overlay__scrim { animation: uir-fade var(--uir-motion-fade); background: var(--uir-overlay-scrim); inset: 0; position: absolute; }
|
|
1027
|
+
.uir-overlay__panel {
|
|
1028
|
+
animation: uir-slide-in var(--uir-motion-slide-in);
|
|
1029
|
+
background: var(--uir-color-sheet);
|
|
1030
|
+
border-left: 1px solid var(--uir-color-rule);
|
|
1031
|
+
bottom: 0;
|
|
1032
|
+
box-shadow: var(--uir-overlay-shadow);
|
|
1033
|
+
box-sizing: border-box;
|
|
1034
|
+
display: flex;
|
|
1035
|
+
flex-direction: column;
|
|
1036
|
+
max-width: 100vw;
|
|
1037
|
+
position: absolute;
|
|
1038
|
+
right: 0;
|
|
1039
|
+
top: 0;
|
|
1040
|
+
width: var(--uir-overlay-width);
|
|
1041
|
+
}
|
|
1042
|
+
.uir-overlay__panel:focus { outline: none; }
|
|
1043
|
+
.uir-overlay__head { align-items: center; display: flex; flex: none; justify-content: space-between; padding: 18px 22px 14px; }
|
|
1044
|
+
.uir-overlay__title { color: var(--uir-color-ink); font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
|
|
1045
|
+
.uir-overlay__close {
|
|
1046
|
+
align-items: center; background: var(--uir-color-sunk); border: none; border-radius: 14px;
|
|
1047
|
+
cursor: pointer; display: inline-flex; height: 28px; justify-content: center; width: 28px;
|
|
1048
|
+
}
|
|
1049
|
+
.uir-overlay__close:hover { background: var(--uir-color-sunk-strong); }
|
|
1050
|
+
.uir-overlay__close line { stroke: var(--uir-color-muted); stroke-linecap: round; stroke-width: 1.5; }
|
|
1051
|
+
.uir-overlay__body { flex: 1; overflow-y: auto; padding: 2px 22px 20px; }
|
|
1052
|
+
/* The nested page is content in a panel, not a page in a window: it brings no padding of its own. */
|
|
1053
|
+
.uir-page--nested { padding: 0; }
|
|
1054
|
+
.uir-page--nested .uir-shell { gap: var(--uir-space-md); }
|
|
1055
|
+
|
|
1056
|
+
@keyframes uir-slide-in { from { transform: translateX(100%); } }
|
|
1057
|
+
@keyframes uir-fade { from { opacity: 0; } }
|
|
1058
|
+
|
|
1059
|
+
/* Motion is an enhancement, never information. Anyone who asked their system to stop moving things
|
|
1060
|
+
gets the same page, arrived at instantly. */
|
|
1061
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1062
|
+
.uir-overlay__panel, .uir-overlay__scrim { animation: none; }
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/* Motion is an enhancement, never information: the bar's WIDTH is the fact, and it is right on the
|
|
1066
|
+
first painted frame either way. */
|
|
1067
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1068
|
+
.uir-progress__fill { animation: none; transition: none; }
|
|
1069
|
+
.uir-block[data-tile] { animation: none; }
|
|
1070
|
+
}
|