@terpjs/react-core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -21
- package/package.json +6 -5
- package/src/AppShell.test.tsx +323 -4
- package/src/AppShell.tsx +401 -66
- package/src/EmptyState.test.tsx +30 -0
- package/src/EmptyState.tsx +23 -3
- package/src/Field.test.tsx +30 -0
- package/src/Field.tsx +36 -8
- package/src/FormPage.tsx +54 -0
- package/src/LoginView.test.tsx +34 -2
- package/src/LoginView.tsx +43 -18
- package/src/ModuleNav.test.tsx +17 -10
- package/src/ModuleNav.tsx +35 -3
- package/src/Page.tsx +23 -1
- package/src/ProfileView.test.tsx +1 -1
- package/src/ProfileView.tsx +2 -4
- package/src/SettingsPage.tsx +50 -0
- package/src/SplitPage.tsx +150 -0
- package/src/UserMenu.test.tsx +28 -5
- package/src/UserMenu.tsx +15 -9
- package/src/admin/AuditLogAdmin.tsx +21 -7
- package/src/admin/GroupCreate.tsx +17 -3
- package/src/admin/GroupDetail.tsx +48 -13
- package/src/admin/GroupsAdmin.tsx +13 -5
- package/src/admin/UserCreate.tsx +40 -11
- package/src/admin/UserDetail.tsx +4 -1
- package/src/admin/UsersAdmin.tsx +14 -6
- package/src/admin/admin.test.tsx +212 -8
- package/src/admin/fieldErrors.ts +45 -0
- package/src/bootstrap.test.tsx +208 -0
- package/src/bootstrap.tsx +121 -5
- package/src/breakpoints.ts +41 -0
- package/src/dataview/DataView.tsx +12 -5
- package/src/dataview/DataViewCardList.tsx +8 -7
- package/src/dataview/DataViewPagination.tsx +15 -8
- package/src/dataview/DataViewTable.tsx +32 -21
- package/src/dataview/README.md +13 -2
- package/src/dataview/index.ts +1 -0
- package/src/dataview/internal.tsx +31 -1
- package/src/dataview/types.ts +26 -3
- package/src/format.test.tsx +213 -0
- package/src/format.ts +150 -0
- package/src/icons.tsx +67 -5
- package/src/index.ts +56 -6
- package/src/layout.manifest.json +118 -0
- package/src/layout.manifest.test.ts +205 -0
- package/src/layout.test.tsx +198 -1
- package/src/layout.tsx +208 -11
- package/src/layoutContract.test.tsx +311 -2
- package/src/layoutContract.ts +44 -3
- package/src/layoutDeclaration.test.ts +435 -0
- package/src/layoutDeclaration.ts +531 -0
- package/src/locale.tsx +12 -0
- package/src/markers.test.ts +27 -5
- package/src/nav.test.ts +234 -4
- package/src/nav.ts +180 -6
- package/src/navActive.test.ts +115 -0
- package/src/navActive.ts +119 -0
- package/src/navLink.tsx +20 -2
- package/src/previewBridge.test.ts +327 -0
- package/src/previewBridge.ts +278 -0
- package/src/raw.d.ts +14 -2
- package/src/review.test.tsx +272 -0
- package/src/router.test.tsx +575 -2
- package/src/router.tsx +212 -19
- package/src/styles.test.ts +535 -58
- package/src/styles.ts +1130 -111
- package/src/theme.test.tsx +29 -0
- package/src/theme.themes.test.ts +13 -7
- package/src/theme.tsx +30 -33
- package/src/themes.ts +54 -0
- package/src/toast.tsx +2 -1
- package/src/tokens.guard.test.ts +239 -0
- package/src/typography.test.tsx +213 -0
- package/src/typography.tsx +255 -0
- package/src/ui/Avatar.test.tsx +63 -0
- package/src/ui/Avatar.tsx +65 -0
- package/src/ui/Button.test.tsx +69 -3
- package/src/ui/Button.tsx +57 -4
- package/src/ui/Card.test.tsx +13 -0
- package/src/ui/Card.tsx +28 -1
- package/src/ui/Checkbox.tsx +10 -2
- package/src/ui/Combobox.test.tsx +139 -0
- package/src/ui/Combobox.tsx +255 -43
- package/src/ui/DatePicker.tsx +44 -12
- package/src/ui/Input.test.tsx +123 -0
- package/src/ui/Input.tsx +65 -2
- package/src/ui/Menu.tsx +16 -5
- package/src/ui/Popover.tsx +13 -0
- package/src/ui/Radio.tsx +10 -5
- package/src/ui/Select.test.tsx +232 -0
- package/src/ui/Select.tsx +177 -8
- package/src/ui/Switch.tsx +10 -2
- package/src/ui/Tabs.test.tsx +28 -0
- package/src/ui/Tabs.tsx +30 -6
- package/src/ui/Tooltip.test.tsx +56 -1
- package/src/ui/Tooltip.tsx +69 -6
- package/src/uiText.literals.test.ts +199 -0
- package/src/uiText.tsx +36 -0
- package/src/unwrap.test.ts +132 -0
- package/src/unwrap.ts +118 -32
package/src/styles.ts
CHANGED
|
@@ -74,11 +74,37 @@
|
|
|
74
74
|
* is what lets an app override any framework rule without `!important` — the
|
|
75
75
|
* restyling this phase exists to enable.
|
|
76
76
|
*
|
|
77
|
+
* ## Motion
|
|
78
|
+
*
|
|
79
|
+
* Every `transition` here names the published motion scale rather than a literal.
|
|
80
|
+
* It wrote `150ms ease` 28 times and `100ms ease` once while reading a motion token
|
|
81
|
+
* zero times — four duration tokens and three easings published in 2a with no reader,
|
|
82
|
+
* which is the shape `--color-fg-on-brand` was deleted for. Wiring them was provably
|
|
83
|
+
* inert: `--motion-duration-fast` IS `150ms`, `--motion-duration-instant` IS `100ms`
|
|
84
|
+
* and `--motion-easing-standard` IS `ease`, so all 29 literals mapped onto a token
|
|
85
|
+
* pair and no computed value changed.
|
|
86
|
+
*
|
|
87
|
+
* Four tokens stay unread, and that is a recorded position rather than an oversight:
|
|
88
|
+
* `--motion-duration-base`, `--motion-duration-slow`, `--motion-easing-entrance` and
|
|
89
|
+
* `--motion-easing-exit` map onto no literal this sheet contains. Deleting them is a
|
|
90
|
+
* contract change (the manifest publishes them); giving them readers means inventing
|
|
91
|
+
* overlay entrance/exit animations, which is a behaviour change dressed as a token
|
|
92
|
+
* wiring — and the screenshot lane runs with `animations: "disabled"`, so it could not
|
|
93
|
+
* see either the animation or a wrong duration in it. They are named in
|
|
94
|
+
* `tokens.guard.test.ts` as an exact list, so wiring one shrinks that list and
|
|
95
|
+
* publishing an eighth forces the decision instead of drifting.
|
|
96
|
+
*
|
|
97
|
+
* The spinner's `0.8s` is the one deliberate literal left. It is a rotation period
|
|
98
|
+
* rather than an interaction step, and the scale tops out at 400ms, so there is no
|
|
99
|
+
* token to name — the gate is scoped to `transition` for exactly that reason.
|
|
100
|
+
*
|
|
77
101
|
* The injector is idempotent, SSR-safe (guarded on `document`), and appends
|
|
78
102
|
* the rules through `textContent` — never `innerHTML` — so no HTML sink is
|
|
79
103
|
* touched.
|
|
80
104
|
*/
|
|
81
105
|
|
|
106
|
+
import { WIDE_VIEWPORT_QUERY } from "./breakpoints";
|
|
107
|
+
|
|
82
108
|
/** The `<style>` element id used to detect a prior injection. */
|
|
83
109
|
export const TERP_STYLES_ID = "terp-core-styles";
|
|
84
110
|
|
|
@@ -119,6 +145,37 @@ export const TERP_STYLES_CSS = `
|
|
|
119
145
|
depend on that at all: a custom property declared on an ancestor is inherited
|
|
120
146
|
rather than cascaded against, so a DataView stamping the attribute on itself
|
|
121
147
|
wins over :root whatever the source order. */
|
|
148
|
+
/* The comfortable island, which ADR 0094 deferred "until something asks" — and the shell
|
|
149
|
+
taking a density of its own is what asked. DataView's own docstring names the gap: inside an
|
|
150
|
+
already-compact subtree, density="comfortable" did not make anything comfortable, because
|
|
151
|
+
comfortable was the ABSENCE of an attribute and absence cannot override an ancestor.
|
|
152
|
+
That was fine while nothing could put a DataView inside a compact subtree. With
|
|
153
|
+
AppShell density="compact" it becomes a legal prop combination that silently does nothing —
|
|
154
|
+
the shape this phase keeps refusing, most recently in Select's options union. (A running tally
|
|
155
|
+
lived here and in four other places, and two of them said three. A citation keeps.)
|
|
156
|
+
The mechanism is the compact rule mirrored, and it works through INHERITANCE rather than
|
|
157
|
+
specificity: the nearest ancestor carrying either attribute sets the live tokens for its
|
|
158
|
+
subtree, so an island simply re-sets them. The two selectors never match the same element,
|
|
159
|
+
so they never compete. Unlayered for the compact rule's reason — inside a layer it would
|
|
160
|
+
tie with the contract's own unlayered :root values whenever the attribute lands on <html>,
|
|
161
|
+
which is what the :root-qualified copies below settle.
|
|
162
|
+
The values are the :root values by construction, so stamping comfortable where nothing is
|
|
163
|
+
compact computes exactly what it computed before: provably zero-diff. */
|
|
164
|
+
/* Each selector is written twice, and the :root-qualified copy is the load-bearing one. The
|
|
165
|
+
contract declares these same custom properties on :root, unlayered, at (0,1,0) — and a
|
|
166
|
+
bare [data-density="..."] on <html> is ALSO (0,1,0), so the two tied and only the order
|
|
167
|
+
the two sheets happened to load decided the winner. A production build extracts tokens.css
|
|
168
|
+
to a <link> that precedes the injected sheet, so react-core won by construction and the
|
|
169
|
+
exposure was the dev server and any host loading the tokens late. :root[data-density] is
|
|
170
|
+
(0,2,0) and wins outright; the unqualified copy stays for a density island on a subtree,
|
|
171
|
+
where there is no :root to qualify and nothing to compete with. */
|
|
172
|
+
:root[data-density="comfortable"],
|
|
173
|
+
[data-density="comfortable"] {
|
|
174
|
+
--density-control-min-height: var(--density-comfortable-control-min-height);
|
|
175
|
+
--density-cell-pad-y: var(--density-comfortable-cell-pad-y);
|
|
176
|
+
--density-cell-pad-x: var(--density-comfortable-cell-pad-x);
|
|
177
|
+
}
|
|
178
|
+
:root[data-density="compact"],
|
|
122
179
|
[data-density="compact"] {
|
|
123
180
|
--density-control-min-height: var(--density-compact-control-min-height);
|
|
124
181
|
--density-cell-pad-y: var(--density-compact-cell-pad-y);
|
|
@@ -158,9 +215,26 @@ body {
|
|
|
158
215
|
they match every scrollable element globally). Paired with the color-scheme
|
|
159
216
|
declaration on the token roots so any native chrome we do not restyle here
|
|
160
217
|
(notably the native <select> option popup) also follows the theme. */
|
|
218
|
+
/* And the gutter is reserved whether or not the page is currently long enough to need
|
|
219
|
+
it. Without this, every navigation between a page that fits and a page that does not
|
|
220
|
+
changes the width of the content box by the scrollbar's width, so the whole layout —
|
|
221
|
+
the header, the table, the centred login card — jumps sideways on the way in and back
|
|
222
|
+
on the way out. The document is the scroll container here (the sidebar is sticky in
|
|
223
|
+
normal flow rather than a scroller of its own), so the root is the right and only
|
|
224
|
+
place for it.
|
|
225
|
+
|
|
226
|
+
stable, not "stable both-edges": the gutter belongs where the scrollbar goes. The
|
|
227
|
+
cost is that a page which never scrolls is off true viewport centre by the gutter,
|
|
228
|
+
most visibly on the sign-in screen, and that is the trade every app that reserves the
|
|
229
|
+
gutter takes. It is layered, so an app that would rather have the jump can turn it off
|
|
230
|
+
from its own unlayered theme.css.
|
|
231
|
+
|
|
232
|
+
This one moves pixels, which is why it is a deliberate line here rather than something
|
|
233
|
+
to slip in beside a refactor: it narrows the content box of every scroll-free page. */
|
|
161
234
|
html {
|
|
162
235
|
scrollbar-width: thin;
|
|
163
236
|
scrollbar-color: var(--color-neutral-300) transparent;
|
|
237
|
+
scrollbar-gutter: stable;
|
|
164
238
|
}
|
|
165
239
|
::-webkit-scrollbar {
|
|
166
240
|
width: 10px;
|
|
@@ -204,8 +278,12 @@ html {
|
|
|
204
278
|
font-size: var(--font-size-sm);
|
|
205
279
|
font-weight: var(--font-weight-medium);
|
|
206
280
|
line-height: 1.2;
|
|
207
|
-
transition:
|
|
208
|
-
|
|
281
|
+
transition:
|
|
282
|
+
background-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
283
|
+
color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
284
|
+
border-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
285
|
+
box-shadow var(--motion-duration-fast) var(--motion-easing-standard),
|
|
286
|
+
transform var(--motion-duration-instant) var(--motion-easing-standard);
|
|
209
287
|
}
|
|
210
288
|
[data-terp="button"][data-variant="primary"] {
|
|
211
289
|
background: var(--color-brand-primary);
|
|
@@ -225,6 +303,33 @@ html {
|
|
|
225
303
|
background: transparent;
|
|
226
304
|
color: var(--color-neutral-700);
|
|
227
305
|
}
|
|
306
|
+
/* Size. Two rules, not three: the standard control's geometry is the base rule above, so
|
|
307
|
+
md is the absence of an attribute — the same shape density takes, where "comfortable" is
|
|
308
|
+
the token sheet's own :root value and the attribute for it matches no rule.
|
|
309
|
+
|
|
310
|
+
The heights are a calc() off the density token rather than a second family of tokens,
|
|
311
|
+
and that is what makes size and density compose without either knowing about the other:
|
|
312
|
+
a small button in a compact subtree resolves 2rem - 0.5rem, because the compact
|
|
313
|
+
re-scoping has already moved the token this reads. A --control-height-sm of its own
|
|
314
|
+
would have needed a compact counterpart, a re-scoping line, and a rule to keep the two
|
|
315
|
+
in step, to express something the space scale already says. */
|
|
316
|
+
[data-terp="button"][data-size="sm"] {
|
|
317
|
+
min-height: calc(var(--density-control-min-height) - var(--space-2));
|
|
318
|
+
padding: 0 var(--space-3);
|
|
319
|
+
font-size: var(--font-size-xs);
|
|
320
|
+
}
|
|
321
|
+
[data-terp="button"][data-size="lg"] {
|
|
322
|
+
min-height: calc(var(--density-control-min-height) + var(--space-2));
|
|
323
|
+
padding: 0 var(--space-6);
|
|
324
|
+
font-size: var(--font-size-base);
|
|
325
|
+
}
|
|
326
|
+
/* Full width beats the base rule's width: fit-content on specificity — (0,2,0) against
|
|
327
|
+
(0,1,0) in the same layer — so it needs neither a layer of its own nor an escalation.
|
|
328
|
+
It exists as a prop because the only other way to reach it was the caller writing
|
|
329
|
+
style={{ width: "100%" }}, which app modules may not do. */
|
|
330
|
+
[data-terp="button"][data-full-width="true"] {
|
|
331
|
+
width: 100%;
|
|
332
|
+
}
|
|
228
333
|
[data-terp="button-icon"] {
|
|
229
334
|
display: inline-flex;
|
|
230
335
|
align-items: center;
|
|
@@ -337,7 +442,9 @@ html {
|
|
|
337
442
|
font-size: var(--font-size-sm);
|
|
338
443
|
font-weight: var(--font-weight-normal);
|
|
339
444
|
line-height: 1.25;
|
|
340
|
-
transition:
|
|
445
|
+
transition:
|
|
446
|
+
border-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
447
|
+
box-shadow var(--motion-duration-fast) var(--motion-easing-standard);
|
|
341
448
|
}
|
|
342
449
|
input[data-terp="input"] {
|
|
343
450
|
min-height: var(--density-control-min-height);
|
|
@@ -374,6 +481,14 @@ textarea[data-terp="input"] {
|
|
|
374
481
|
flex-direction: column;
|
|
375
482
|
gap: var(--space-2);
|
|
376
483
|
margin: 0;
|
|
484
|
+
/* The list reset both of these primitives document a use for and neither had. as="ul" is
|
|
485
|
+
offered by Stack and Grid alike, and a <ul> arrives with a 40px inline padding and a
|
|
486
|
+
marker per child from the UA sheet — so the documented use rendered bulleted and indented.
|
|
487
|
+
hubpage-grid and resource-list-items already carry exactly this, because both are always
|
|
488
|
+
lists; these two are lists only when asked, which is why it was missed. A no-op on a div,
|
|
489
|
+
and the data-padding rules outweigh it at (0,2,0) when an inset is asked for. */
|
|
490
|
+
padding: 0;
|
|
491
|
+
list-style: none;
|
|
377
492
|
}
|
|
378
493
|
[data-terp="stack"][data-direction="row"] { flex-direction: row; }
|
|
379
494
|
[data-terp="stack"][data-gap="0"] { gap: var(--space-0); }
|
|
@@ -384,6 +499,116 @@ textarea[data-terp="input"] {
|
|
|
384
499
|
[data-terp="stack"][data-gap="6"] { gap: var(--space-6); }
|
|
385
500
|
[data-terp="stack"][data-gap="8"] { gap: var(--space-8); }
|
|
386
501
|
[data-terp="stack"][data-wrap="true"] { flex-wrap: wrap; }
|
|
502
|
+
/* Padding, the dimension Stack did not have — which is why a padded region was reachable
|
|
503
|
+
only through a Card, whose border and background came along whether or not they were
|
|
504
|
+
wanted. A step on the same token scale as gap, so there are no arbitrary insets. */
|
|
505
|
+
[data-terp="stack"][data-padding="0"] { padding: var(--space-0); }
|
|
506
|
+
[data-terp="stack"][data-padding="1"] { padding: var(--space-1); }
|
|
507
|
+
[data-terp="stack"][data-padding="2"] { padding: var(--space-2); }
|
|
508
|
+
[data-terp="stack"][data-padding="3"] { padding: var(--space-3); }
|
|
509
|
+
[data-terp="stack"][data-padding="4"] { padding: var(--space-4); }
|
|
510
|
+
[data-terp="stack"][data-padding="6"] { padding: var(--space-6); }
|
|
511
|
+
[data-terp="stack"][data-padding="8"] { padding: var(--space-8); }
|
|
512
|
+
/* The wide half of a responsive Stack. This block must stay AFTER the rules above and not
|
|
513
|
+
merely above the Grid family: a stack with direction {narrow: "row", wide: "column"}
|
|
514
|
+
carries both data-direction="row" and data-direction-wide="column", and the two selectors
|
|
515
|
+
weigh the same (0,2,0), so nothing but source order decides which wins above the cutover.
|
|
516
|
+
Same for every gap pair. styles.test.ts pins the order, because getting it backwards
|
|
517
|
+
renders the narrow value at every width and looks like the prop not working.
|
|
518
|
+
|
|
519
|
+
The query is INTERPOLATED from ./breakpoints, which is the one \${…} in this sheet and is
|
|
520
|
+
deliberate rather than a slip: it is the complement of the exact string AppShell and
|
|
521
|
+
DataView hand to matchMedia, so the two halves of the cutover partition the viewport by
|
|
522
|
+
construction. Written out here they would be two literals that agree until someone edits
|
|
523
|
+
one. (The convention of grepping this literal for \${ still holds — there should be
|
|
524
|
+
exactly this one.) */
|
|
525
|
+
@media ${WIDE_VIEWPORT_QUERY} {
|
|
526
|
+
[data-terp="stack"][data-direction-wide="column"] { flex-direction: column; }
|
|
527
|
+
[data-terp="stack"][data-direction-wide="row"] { flex-direction: row; }
|
|
528
|
+
[data-terp="stack"][data-gap-wide="0"] { gap: var(--space-0); }
|
|
529
|
+
[data-terp="stack"][data-gap-wide="1"] { gap: var(--space-1); }
|
|
530
|
+
[data-terp="stack"][data-gap-wide="2"] { gap: var(--space-2); }
|
|
531
|
+
[data-terp="stack"][data-gap-wide="3"] { gap: var(--space-3); }
|
|
532
|
+
[data-terp="stack"][data-gap-wide="4"] { gap: var(--space-4); }
|
|
533
|
+
[data-terp="stack"][data-gap-wide="6"] { gap: var(--space-6); }
|
|
534
|
+
[data-terp="stack"][data-gap-wide="8"] { gap: var(--space-8); }
|
|
535
|
+
/* The split's two columns, at the one cutover the chrome around it already uses.
|
|
536
|
+
Three list tracks, three rules, no length ever handed in as a style — the listWidth
|
|
537
|
+
prop is a step for the reason Grid's minColumn is (ADR 0097 §4). The detail track is
|
|
538
|
+
minmax(0, 1fr) so it takes the remainder and still lets a wide table scroll inside
|
|
539
|
+
itself rather than widening the row. */
|
|
540
|
+
[data-terp="splitpage-panes"][data-list-width="sm"] {
|
|
541
|
+
grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
|
|
542
|
+
}
|
|
543
|
+
[data-terp="splitpage-panes"][data-list-width="md"] {
|
|
544
|
+
grid-template-columns: minmax(0, 24rem) minmax(0, 1fr);
|
|
545
|
+
}
|
|
546
|
+
[data-terp="splitpage-panes"][data-list-width="lg"] {
|
|
547
|
+
grid-template-columns: minmax(0, 32rem) minmax(0, 1fr);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* Grids -------------------------------------------------------------------- */
|
|
552
|
+
/* The two-dimensional primitive. The base rule is the DEFAULT shape — auto-fit at the
|
|
553
|
+
sm track floor, stretched cells — so the three defaults match no attribute, exactly
|
|
554
|
+
as density's "comfortable" and Button's md do.
|
|
555
|
+
|
|
556
|
+
min(16rem, 100%) rather than a bare 16rem, and the min() is load-bearing rather than
|
|
557
|
+
defensive: a bare floor wider than the container makes the single track overflow it,
|
|
558
|
+
so a grid in a narrow panel would scroll sideways instead of going one-column. Same
|
|
559
|
+
mechanism the hub grid uses, and the same 16rem, so the two agree by construction
|
|
560
|
+
rather than by coincidence.
|
|
561
|
+
|
|
562
|
+
The four floors are rem literals rather than tokens on purpose. They are a design
|
|
563
|
+
scale with exactly one consumer today, and vocabulary published before something
|
|
564
|
+
reads it gets retired here — the four density cell tokens were deleted for precisely
|
|
565
|
+
that and came back with their readers. They become tokens the day an app asks to move
|
|
566
|
+
them, which is also when a compact counterpart would have to be decided. */
|
|
567
|
+
[data-terp="grid"] {
|
|
568
|
+
display: grid;
|
|
569
|
+
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
|
|
570
|
+
gap: var(--space-4);
|
|
571
|
+
align-items: stretch;
|
|
572
|
+
min-width: 0;
|
|
573
|
+
margin: 0;
|
|
574
|
+
/* See the note on stack above: the same reset, for the same documented as="ul". */
|
|
575
|
+
padding: 0;
|
|
576
|
+
list-style: none;
|
|
577
|
+
}
|
|
578
|
+
[data-terp="grid"][data-min-column="xs"] {
|
|
579
|
+
grid-template-columns: repeat(auto-fit, minmax(min(10rem, 100%), 1fr));
|
|
580
|
+
}
|
|
581
|
+
[data-terp="grid"][data-min-column="md"] {
|
|
582
|
+
grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
|
|
583
|
+
}
|
|
584
|
+
[data-terp="grid"][data-min-column="lg"] {
|
|
585
|
+
grid-template-columns: repeat(auto-fit, minmax(min(26rem, 100%), 1fr));
|
|
586
|
+
}
|
|
587
|
+
/* A fixed count. minmax(0, 1fr) rather than a bare 1fr, because 1fr floors at the
|
|
588
|
+
track's min-content size — so one long unbroken word in a cell widens its column and
|
|
589
|
+
the grid overflows its container, which is the failure a two-column form of long field
|
|
590
|
+
labels walks straight into. */
|
|
591
|
+
[data-terp="grid"][data-columns="1"] { grid-template-columns: minmax(0, 1fr); }
|
|
592
|
+
[data-terp="grid"][data-columns="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
593
|
+
[data-terp="grid"][data-columns="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
594
|
+
[data-terp="grid"][data-columns="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
595
|
+
[data-terp="grid"][data-gap="0"] { gap: var(--space-0); }
|
|
596
|
+
[data-terp="grid"][data-gap="1"] { gap: var(--space-1); }
|
|
597
|
+
[data-terp="grid"][data-gap="2"] { gap: var(--space-2); }
|
|
598
|
+
[data-terp="grid"][data-gap="3"] { gap: var(--space-3); }
|
|
599
|
+
[data-terp="grid"][data-gap="4"] { gap: var(--space-4); }
|
|
600
|
+
[data-terp="grid"][data-gap="6"] { gap: var(--space-6); }
|
|
601
|
+
[data-terp="grid"][data-gap="8"] { gap: var(--space-8); }
|
|
602
|
+
[data-terp="grid"][data-align="start"] { align-items: start; }
|
|
603
|
+
[data-terp="grid"][data-align="center"] { align-items: center; }
|
|
604
|
+
[data-terp="grid"][data-align="end"] { align-items: end; }
|
|
605
|
+
[data-terp="grid"][data-padding="0"] { padding: var(--space-0); }
|
|
606
|
+
[data-terp="grid"][data-padding="1"] { padding: var(--space-1); }
|
|
607
|
+
[data-terp="grid"][data-padding="2"] { padding: var(--space-2); }
|
|
608
|
+
[data-terp="grid"][data-padding="3"] { padding: var(--space-3); }
|
|
609
|
+
[data-terp="grid"][data-padding="4"] { padding: var(--space-4); }
|
|
610
|
+
[data-terp="grid"][data-padding="6"] { padding: var(--space-6); }
|
|
611
|
+
[data-terp="grid"][data-padding="8"] { padding: var(--space-8); }
|
|
387
612
|
|
|
388
613
|
/* Detail lists ------------------------------------------------------------- */
|
|
389
614
|
/* The term and value are inline boxes inside a block row, which is what makes
|
|
@@ -392,14 +617,66 @@ textarea[data-terp="input"] {
|
|
|
392
617
|
margin: 0;
|
|
393
618
|
display: grid;
|
|
394
619
|
gap: var(--space-1);
|
|
620
|
+
grid-template-columns: minmax(0, 1fr);
|
|
621
|
+
}
|
|
622
|
+
[data-terp="detail-list-row"] {
|
|
623
|
+
min-width: 0;
|
|
395
624
|
}
|
|
396
625
|
[data-terp="detail-list-term"] {
|
|
397
626
|
display: inline;
|
|
398
627
|
font-weight: var(--font-weight-medium);
|
|
399
628
|
}
|
|
629
|
+
/* The colon belongs to the inline layout alone, so it is a rule rather than a text node —
|
|
630
|
+
aligned and stacked must not have one, and no rule can withdraw a text node. Decorative
|
|
631
|
+
either way: the dt/dd pairing is what carries the relationship to assistive tech. */
|
|
632
|
+
[data-terp="detail-list"]:not([data-layout]) [data-terp="detail-list-term"]::after {
|
|
633
|
+
content: ": ";
|
|
634
|
+
white-space: pre;
|
|
635
|
+
}
|
|
400
636
|
[data-terp="detail-list-value"] {
|
|
401
637
|
display: inline;
|
|
402
638
|
margin: 0;
|
|
639
|
+
min-width: 0;
|
|
640
|
+
/* Flooring the track at 0 is not enough on its own, and the specimen is what showed it: a
|
|
641
|
+
64-character digest has nothing to break at, so it overflows the column whatever the
|
|
642
|
+
column's floor. This is the declaration that makes it wrap. Same answer profile-email
|
|
643
|
+
already uses for a long address — where it is noted as unobservable, because that
|
|
644
|
+
screen's session is a fixed short one; detail-list-long-value is the first picture of
|
|
645
|
+
the mechanism anywhere in the suite. */
|
|
646
|
+
overflow-wrap: anywhere;
|
|
647
|
+
}
|
|
648
|
+
/* Two pairs per row. minmax(0, 1fr) rather than 1fr for the reason Grid's fixed counts need
|
|
649
|
+
it: a bare 1fr floors at min-content, and a 64-character digest with nothing to break on
|
|
650
|
+
then widens its column and pushes the list past its container — which is the defect the
|
|
651
|
+
diagnosis was describing, rather than the missing alignment it named. */
|
|
652
|
+
[data-terp="detail-list"][data-columns="2"] {
|
|
653
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
654
|
+
column-gap: var(--space-4);
|
|
655
|
+
}
|
|
656
|
+
/* Aligned: every label in a shared column. The row wrapper becomes display: contents so the
|
|
657
|
+
dt and dd are grid items of the dl itself — the only way to align across rows without
|
|
658
|
+
changing the DOM, and the reason the wrapper needed a marker at all. */
|
|
659
|
+
[data-terp="detail-list"][data-layout="aligned"] {
|
|
660
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
661
|
+
column-gap: var(--space-3);
|
|
662
|
+
}
|
|
663
|
+
[data-terp="detail-list"][data-layout="aligned"][data-columns="2"] {
|
|
664
|
+
grid-template-columns: repeat(2, auto minmax(0, 1fr));
|
|
665
|
+
}
|
|
666
|
+
[data-terp="detail-list"][data-layout="aligned"] [data-terp="detail-list-row"] {
|
|
667
|
+
display: contents;
|
|
668
|
+
}
|
|
669
|
+
[data-terp="detail-list"][data-layout="aligned"] [data-terp="detail-list-term"],
|
|
670
|
+
[data-terp="detail-list"][data-layout="aligned"] [data-terp="detail-list-value"],
|
|
671
|
+
[data-terp="detail-list"][data-layout="stacked"] [data-terp="detail-list-term"],
|
|
672
|
+
[data-terp="detail-list"][data-layout="stacked"] [data-terp="detail-list-value"] {
|
|
673
|
+
display: block;
|
|
674
|
+
}
|
|
675
|
+
/* Stacked: label above value. The label takes the muted step so the pair reads as one unit
|
|
676
|
+
rather than two lines of equal weight. */
|
|
677
|
+
[data-terp="detail-list"][data-layout="stacked"] [data-terp="detail-list-term"] {
|
|
678
|
+
font-size: var(--font-size-xs);
|
|
679
|
+
color: var(--color-fg-muted);
|
|
403
680
|
}
|
|
404
681
|
|
|
405
682
|
/* Checkboxes / radios / switches ------------------------------------------- */
|
|
@@ -428,7 +705,7 @@ textarea[data-terp="input"] {
|
|
|
428
705
|
block-size: 1.25rem;
|
|
429
706
|
accent-color: var(--color-fg-accent);
|
|
430
707
|
cursor: pointer;
|
|
431
|
-
transition: background-color
|
|
708
|
+
transition: background-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
432
709
|
}
|
|
433
710
|
[data-terp="radio-group"] {
|
|
434
711
|
display: grid;
|
|
@@ -477,6 +754,141 @@ textarea[data-terp="input"] {
|
|
|
477
754
|
[data-terp="card-heading"] {
|
|
478
755
|
min-width: 0;
|
|
479
756
|
}
|
|
757
|
+
/* Chrome off, heading kept. Three declarations removed rather than a second component
|
|
758
|
+
with six markers of its own describing the same DOM: a titled region inside something
|
|
759
|
+
that is already a surface wants no second border, and the commonest instance is a
|
|
760
|
+
section whose body is a DataView — boxed, the table gets a border inside a border and
|
|
761
|
+
loses the full width its own scroll container gives it.
|
|
762
|
+
|
|
763
|
+
padding: 0 rather than dropping the declaration, because the base rule sets it and an
|
|
764
|
+
absent value inherits nothing useful. */
|
|
765
|
+
[data-terp="card"][data-variant="plain"] {
|
|
766
|
+
background: none;
|
|
767
|
+
border-color: transparent;
|
|
768
|
+
padding: 0;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/* Prose --------------------------------------------------------------------- */
|
|
772
|
+
/* The first readers of the published type scale. --font-line-height-* and
|
|
773
|
+
--font-letter-spacing-* shipped in 0.7.0 with nothing reading them, and unlike the
|
|
774
|
+
motion family they could not simply be wired in: this sheet writes line heights of
|
|
775
|
+
1.2, 1.25, 1.3, 1.4 and 1.5, and the scale offers 1.2, 1.35, 1.5 and 1.7 — so only 8
|
|
776
|
+
of 32 literals map, and converting the rest would change rendered line heights across
|
|
777
|
+
a dozen components. That is a typography pass with its own baselines. New components
|
|
778
|
+
have nothing depending on their metrics, so they take the published scale and the
|
|
779
|
+
family gets honest consumers; tokens.guard.test.ts tracks what is still unread.
|
|
780
|
+
|
|
781
|
+
Headings carry no colour: they inherit the page's ink, so a heading inside a tinted
|
|
782
|
+
surface stays legible without a rule per surface. margin: 0 because the browser
|
|
783
|
+
default fights the parent's gap, which is the same reason page-title and card-title
|
|
784
|
+
both declare it. */
|
|
785
|
+
[data-terp="heading"] {
|
|
786
|
+
margin: 0;
|
|
787
|
+
font-family: var(--font-family-sans);
|
|
788
|
+
font-weight: var(--font-weight-semibold);
|
|
789
|
+
line-height: var(--font-line-height-tight);
|
|
790
|
+
letter-spacing: var(--font-letter-spacing-tight);
|
|
791
|
+
}
|
|
792
|
+
[data-terp="heading"][data-size="sm"] {
|
|
793
|
+
font-size: var(--font-size-sm);
|
|
794
|
+
line-height: var(--font-line-height-snug);
|
|
795
|
+
letter-spacing: var(--font-letter-spacing-base);
|
|
796
|
+
}
|
|
797
|
+
[data-terp="heading"][data-size="base"] {
|
|
798
|
+
font-size: var(--font-size-base);
|
|
799
|
+
line-height: var(--font-line-height-snug);
|
|
800
|
+
letter-spacing: var(--font-letter-spacing-base);
|
|
801
|
+
}
|
|
802
|
+
[data-terp="heading"][data-size="lg"] { font-size: var(--font-size-lg); }
|
|
803
|
+
[data-terp="heading"][data-size="xl"] { font-size: var(--font-size-xl); }
|
|
804
|
+
|
|
805
|
+
/* Body copy. The base rule is the default tone and step, so neither stamps an
|
|
806
|
+
attribute. A measure is capped in ch rather than rem, because the readable line
|
|
807
|
+
length is a count of characters and follows the font size — in rem it would stop
|
|
808
|
+
being a measure the moment an app changed the type scale. */
|
|
809
|
+
[data-terp="text"] {
|
|
810
|
+
margin: 0;
|
|
811
|
+
font-family: var(--font-family-sans);
|
|
812
|
+
font-size: var(--font-size-base);
|
|
813
|
+
line-height: var(--font-line-height-base);
|
|
814
|
+
color: var(--color-fg-default);
|
|
815
|
+
}
|
|
816
|
+
[data-terp="text"][data-size="xs"] { font-size: var(--font-size-xs); }
|
|
817
|
+
[data-terp="text"][data-size="sm"] { font-size: var(--font-size-sm); }
|
|
818
|
+
[data-terp="text"][data-size="lg"] {
|
|
819
|
+
font-size: var(--font-size-lg);
|
|
820
|
+
line-height: var(--font-line-height-relaxed);
|
|
821
|
+
}
|
|
822
|
+
[data-terp="text"][data-tone="muted"] { color: var(--color-fg-muted); }
|
|
823
|
+
[data-terp="text"][data-tone="subtle"] { color: var(--color-fg-subtle); }
|
|
824
|
+
[data-terp="text"][data-measure="narrow"] { max-width: 48ch; }
|
|
825
|
+
[data-terp="text"][data-measure="base"] { max-width: 72ch; }
|
|
826
|
+
|
|
827
|
+
/* Code. The inline form takes a tinted chip so an identifier reads as one inside a
|
|
828
|
+
sentence; the block form drops the chip — a bordered box around a bordered box again —
|
|
829
|
+
and keeps the border on the <pre>. overflow-x on the block is what makes a long line
|
|
830
|
+
scroll rather than widen the page, and it is the reason the <pre> is focusable: a
|
|
831
|
+
scroll container a keyboard cannot reach cannot be scrolled at all (SC 2.1.1). */
|
|
832
|
+
[data-terp="code"] {
|
|
833
|
+
font-family: var(--font-family-mono);
|
|
834
|
+
font-size: 0.875em;
|
|
835
|
+
padding: 0.1em 0.32em;
|
|
836
|
+
border-radius: var(--radius-sm);
|
|
837
|
+
background: var(--color-neutral-100);
|
|
838
|
+
color: var(--color-fg-default);
|
|
839
|
+
}
|
|
840
|
+
[data-terp="code-block"] {
|
|
841
|
+
margin: 0;
|
|
842
|
+
padding: var(--space-3);
|
|
843
|
+
border: 1px solid var(--color-neutral-200);
|
|
844
|
+
border-radius: var(--radius-md);
|
|
845
|
+
background: var(--color-neutral-50);
|
|
846
|
+
overflow-x: auto;
|
|
847
|
+
font-size: var(--font-size-sm);
|
|
848
|
+
line-height: var(--font-line-height-base);
|
|
849
|
+
}
|
|
850
|
+
[data-terp="code-block"] [data-terp="code"] {
|
|
851
|
+
padding: 0;
|
|
852
|
+
background: none;
|
|
853
|
+
border-radius: 0;
|
|
854
|
+
font-size: inherit;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/* Links. Two selector shapes for one marker, because an in-app link's marker lands on a
|
|
858
|
+
wrapper: navLink accepts { to, children } and nothing else, so the router's own Link
|
|
859
|
+
cannot be handed an attribute. The external case marks the anchor itself. Same shape
|
|
860
|
+
HubCard already uses, for the same reason. */
|
|
861
|
+
[data-terp="link"],
|
|
862
|
+
[data-terp="link"] a {
|
|
863
|
+
color: var(--color-fg-accent);
|
|
864
|
+
text-decoration: underline;
|
|
865
|
+
text-underline-offset: 2px;
|
|
866
|
+
border-radius: var(--radius-sm);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/* Dividers ----------------------------------------------------------------- */
|
|
870
|
+
/* An <hr>, so the separation reaches the accessibility tree and not only the pixels.
|
|
871
|
+
Its own border reset first: a bare <hr> comes with a browser border and margin that
|
|
872
|
+
differ between engines, which is most of why a module reaches for a bordered div.
|
|
873
|
+
|
|
874
|
+
The vertical form takes its height from its flex or grid line rather than inventing
|
|
875
|
+
one, so it works between the items of a row Stack and is zero-height in a block
|
|
876
|
+
parent. That is worth knowing before reaching for it, and it is why its specimen
|
|
877
|
+
renders inside a fixed-height row — with equal-height siblings there would be nothing
|
|
878
|
+
to see either way. */
|
|
879
|
+
[data-terp="divider"] {
|
|
880
|
+
border: 0;
|
|
881
|
+
margin: 0;
|
|
882
|
+
align-self: stretch;
|
|
883
|
+
background: var(--color-neutral-200);
|
|
884
|
+
block-size: 1px;
|
|
885
|
+
inline-size: auto;
|
|
886
|
+
}
|
|
887
|
+
[data-terp="divider"][data-orientation="vertical"] {
|
|
888
|
+
block-size: auto;
|
|
889
|
+
inline-size: 1px;
|
|
890
|
+
}
|
|
891
|
+
|
|
480
892
|
[data-terp="card-actions"] {
|
|
481
893
|
flex-shrink: 0;
|
|
482
894
|
}
|
|
@@ -519,7 +931,10 @@ textarea[data-terp="input"] {
|
|
|
519
931
|
margin-block-end: -1px;
|
|
520
932
|
border-top-left-radius: var(--radius-sm);
|
|
521
933
|
border-top-right-radius: var(--radius-sm);
|
|
522
|
-
transition:
|
|
934
|
+
transition:
|
|
935
|
+
background-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
936
|
+
color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
937
|
+
border-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
523
938
|
}
|
|
524
939
|
[data-terp="tab-panel"] {
|
|
525
940
|
color: var(--color-neutral-900);
|
|
@@ -584,7 +999,7 @@ textarea[data-terp="input"] {
|
|
|
584
999
|
[data-terp="breadcrumbs"] a {
|
|
585
1000
|
color: var(--color-neutral-600);
|
|
586
1001
|
text-decoration: none;
|
|
587
|
-
transition: color
|
|
1002
|
+
transition: color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
588
1003
|
}
|
|
589
1004
|
[data-terp="breadcrumbs-separator"] {
|
|
590
1005
|
display: inline-flex;
|
|
@@ -607,7 +1022,29 @@ textarea[data-terp="input"] {
|
|
|
607
1022
|
shell's hover rule has keyed on it since before this migration.
|
|
608
1023
|
|
|
609
1024
|
That is why nothing here needs a style object handed across a public boundary, which
|
|
610
|
-
is what AppShellLinkContext.style and RenderBrandLink's style param used to be.
|
|
1025
|
+
is what AppShellLinkContext.style and RenderBrandLink's style param used to be.
|
|
1026
|
+
|
|
1027
|
+
The sidebar paints from its OWN colour family now — --color-sidebar-bg / -fg / -muted /
|
|
1028
|
+
-accent / -border — rather than from the neutral ramp. Those five were declared in all five
|
|
1029
|
+
themes and read by NOTHING, which is exactly the offence --color-fg-on-brand was deleted
|
|
1030
|
+
for; the difference is that here the vocabulary is right and the readers were missing, so
|
|
1031
|
+
wiring is the fix and deleting would have been the mistake. It went unnoticed for four
|
|
1032
|
+
releases because tokens.guard.test.ts tracked three families and --color- was not one.
|
|
1033
|
+
|
|
1034
|
+
Mostly inert, and recounted rather than estimated: of the twenty-five declarations, FIFTEEN
|
|
1035
|
+
already equalled the neutral the sheet was reading — background, foreground and border agree
|
|
1036
|
+
in every theme except the light background. Ten move, in three groups.
|
|
1037
|
+
|
|
1038
|
+
The light sidebar goes #ffffff -> #f8fafc, a faint separation from the canvas that the dark
|
|
1039
|
+
themes always had and light never did. The nav link's resting ink dims in every theme (light
|
|
1040
|
+
#334155 -> #475569, dark #e2e8f0 -> #b4c0d0), which is the deliberate half: a sidebar's
|
|
1041
|
+
resting links are secondary to the page and the active one should carry the weight. And the
|
|
1042
|
+
hover wash changes in four themes — every one but light, where the two values agree — which
|
|
1043
|
+
an earlier version of this comment did not mention at all, because it counted the resting
|
|
1044
|
+
declarations and forgot that accent is one of the five.
|
|
1045
|
+
|
|
1046
|
+
Every text pairing was checked before being wired, not after: 7.24:1 light, 7.94 dark, 7.50
|
|
1047
|
+
midnight, 7.60 twilight, and 18.42 for contrast against its AAA floor. */
|
|
611
1048
|
[data-terp="appshell"] {
|
|
612
1049
|
display: flex;
|
|
613
1050
|
align-items: stretch;
|
|
@@ -632,13 +1069,13 @@ textarea[data-terp="input"] {
|
|
|
632
1069
|
top: 0;
|
|
633
1070
|
height: 100vh;
|
|
634
1071
|
overflow-x: hidden;
|
|
635
|
-
width:
|
|
636
|
-
background: var(--color-
|
|
637
|
-
border-inline-end: 1px solid var(--color-
|
|
638
|
-
transition: width
|
|
1072
|
+
width: var(--shell-sidebar-width-expanded);
|
|
1073
|
+
background: var(--color-sidebar-bg);
|
|
1074
|
+
border-inline-end: 1px solid var(--color-sidebar-border);
|
|
1075
|
+
transition: width var(--motion-duration-fast) var(--motion-easing-standard);
|
|
639
1076
|
}
|
|
640
1077
|
[data-terp="appshell-sidebar"][data-collapsed="true"] {
|
|
641
|
-
width:
|
|
1078
|
+
width: var(--shell-sidebar-width-collapsed);
|
|
642
1079
|
}
|
|
643
1080
|
/* The mobile drawer, reached from the shell root's variant rather than from an attribute
|
|
644
1081
|
of its own — the viewport is one fact and the root owns it. 100dvh rather than 100vh so
|
|
@@ -671,16 +1108,51 @@ textarea[data-terp="input"] {
|
|
|
671
1108
|
gap: var(--space-2);
|
|
672
1109
|
padding: var(--space-1) var(--space-2);
|
|
673
1110
|
min-height: 2.25rem;
|
|
674
|
-
color: var(--color-
|
|
1111
|
+
color: var(--color-sidebar-fg);
|
|
675
1112
|
text-decoration: none;
|
|
676
1113
|
border-radius: var(--radius-md);
|
|
677
1114
|
box-sizing: border-box;
|
|
678
|
-
transition: background-color
|
|
1115
|
+
transition: background-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
679
1116
|
}
|
|
680
1117
|
[data-terp="appshell-sidebar"][data-collapsed="true"] [data-terp="appshell-brand"] {
|
|
681
1118
|
justify-content: center;
|
|
682
1119
|
padding-inline: 0;
|
|
683
1120
|
}
|
|
1121
|
+
/* The brand mark's box (ADR 0098 §9). The brand link used to hand whatever it was given
|
|
1122
|
+
straight into a flex row, so an app's asset sized itself and an oversized one was clipped by
|
|
1123
|
+
the aside's overflow-x: hidden with nothing to say so — in the 4rem rail, which is exactly
|
|
1124
|
+
where a brand most needs to survive. One declared size, published as --shell-brand-size, caps
|
|
1125
|
+
it in every placement, and the descendant rule catches the img or svg inside a wrapper rather
|
|
1126
|
+
than only a direct child.
|
|
1127
|
+
Zero-diff for every shell shipped so far: the default TerpMark is 28px and the token is
|
|
1128
|
+
1.75rem, so the box is exactly the size of the thing that used to be the flex item. */
|
|
1129
|
+
[data-terp="appshell-mark"] {
|
|
1130
|
+
display: inline-flex;
|
|
1131
|
+
flex: none;
|
|
1132
|
+
align-items: center;
|
|
1133
|
+
justify-content: center;
|
|
1134
|
+
width: var(--shell-brand-size);
|
|
1135
|
+
height: var(--shell-brand-size);
|
|
1136
|
+
}
|
|
1137
|
+
[data-terp="appshell-mark"] * {
|
|
1138
|
+
max-width: 100%;
|
|
1139
|
+
max-height: 100%;
|
|
1140
|
+
}
|
|
1141
|
+
/* The light/dark pair, and the switch is a token because the alternative rots. The framework
|
|
1142
|
+
ships five themes and three of them are dark; a company mark with dark ink is invisible on
|
|
1143
|
+
those three, and the bundled icons' currentColor answer is not available to a brand asset.
|
|
1144
|
+
Enumerating the dark themes HERE would be a list that goes stale the first time one is added,
|
|
1145
|
+
so the token build emits --appearance-show-light / --appearance-show-dark from each theme's
|
|
1146
|
+
declared appearance — the same field it already emits color-scheme from, and one
|
|
1147
|
+
themes.json requires. A sixth theme cannot forget to answer.
|
|
1148
|
+
The values are block / none rather than this rule's own display type, so a theme never has
|
|
1149
|
+
to know what layout the shell uses: the box above does the centring, not the mark. */
|
|
1150
|
+
[data-terp="appshell-mark"] > [data-appearance="light"] {
|
|
1151
|
+
display: var(--appearance-show-light);
|
|
1152
|
+
}
|
|
1153
|
+
[data-terp="appshell-mark"] > [data-appearance="dark"] {
|
|
1154
|
+
display: var(--appearance-show-dark);
|
|
1155
|
+
}
|
|
684
1156
|
[data-terp="appshell-brand-row"] {
|
|
685
1157
|
display: flex;
|
|
686
1158
|
align-items: center;
|
|
@@ -696,7 +1168,7 @@ textarea[data-terp="input"] {
|
|
|
696
1168
|
white-space: nowrap;
|
|
697
1169
|
font-size: var(--font-size-base);
|
|
698
1170
|
font-weight: var(--font-weight-semibold);
|
|
699
|
-
color: var(--color-
|
|
1171
|
+
color: var(--color-sidebar-fg);
|
|
700
1172
|
letter-spacing: 0;
|
|
701
1173
|
}
|
|
702
1174
|
[data-terp="appshell-nav"] {
|
|
@@ -711,6 +1183,47 @@ textarea[data-terp="input"] {
|
|
|
711
1183
|
display: grid;
|
|
712
1184
|
gap: var(--space-1);
|
|
713
1185
|
}
|
|
1186
|
+
/* Navigation groups. The wrapper itself declares nothing in the sidebar: it is a plain block
|
|
1187
|
+
containing the same list, which is what makes adding it to the ungrouped case free.
|
|
1188
|
+
|
|
1189
|
+
The separation is on the ADJACENT SIBLING, and these two rules are the first + combinators in
|
|
1190
|
+
this sheet. That is deliberate rather than careless: what is wanted is a separation BETWEEN
|
|
1191
|
+
siblings, which is exactly what + expresses, and the alternatives are worse here. A gap on the
|
|
1192
|
+
nav would make the nav a grid or flex container, and a stretched single row would then resize
|
|
1193
|
+
the one group every app has today. A margin on every group would need a :first-child to undo
|
|
1194
|
+
it, which is a positional selector where a sibling one says the thing directly. With one group
|
|
1195
|
+
neither rule matches at all, so the flat sidebar is untouched to the pixel.
|
|
1196
|
+
|
|
1197
|
+
SCOPED TO THE SIDEBAR, and that scope is load-bearing. In header placement the nav is a flex
|
|
1198
|
+
ROW (below), where margin-block-start is a CROSS-axis margin — never collapsed, applied to a
|
|
1199
|
+
flex item — so an unscoped rule would push every group after the first down by 1rem and grow
|
|
1200
|
+
the sticky header with it. The mobile drawer is the same aside carrying the same marker, so it
|
|
1201
|
+
keeps this rule, which is correct: the drawer stacks. */
|
|
1202
|
+
[data-terp="appshell-sidebar"] [data-terp="appshell-nav-group"] + [data-terp="appshell-nav-group"] {
|
|
1203
|
+
margin-block-start: var(--space-4);
|
|
1204
|
+
}
|
|
1205
|
+
/* The group label. Not a heading element — see the AppShell render for why the outline is the
|
|
1206
|
+
binding constraint and why axe cannot see it.
|
|
1207
|
+
|
|
1208
|
+
The horizontal padding matches the nav link's own (var(--space-3)), so the label sits on the
|
|
1209
|
+
same left edge as the icons under it rather than floating in the rail's gutter.
|
|
1210
|
+
|
|
1211
|
+
letter-spacing comes from the published scale rather than a bare literal, and this is the rule
|
|
1212
|
+
the scale was waiting for: tokens.guard.test.ts records --font-letter-spacing-wide as unread
|
|
1213
|
+
with the comment "for the uppercase-label treatment nothing in the package uses". This is that
|
|
1214
|
+
treatment, so the token gets its first reader and leaves the unread list. font-weight is our
|
|
1215
|
+
own choice and not inherited from the login separator, which declares none — a group label
|
|
1216
|
+
competing with the links under it needs the weight to read as a header rather than as a
|
|
1217
|
+
disabled item. */
|
|
1218
|
+
[data-terp="appshell-nav-group-label"] {
|
|
1219
|
+
display: block;
|
|
1220
|
+
padding: var(--space-1) var(--space-3);
|
|
1221
|
+
font-size: var(--font-size-xs);
|
|
1222
|
+
font-weight: var(--font-weight-semibold);
|
|
1223
|
+
letter-spacing: var(--font-letter-spacing-wide);
|
|
1224
|
+
text-transform: uppercase;
|
|
1225
|
+
color: var(--color-sidebar-muted);
|
|
1226
|
+
}
|
|
714
1227
|
/* Sidebar navigation links, and this is the rule the whole shell migration was for. The
|
|
715
1228
|
geometry used to be NAV_LINK_STYLE, a CSSProperties object exported from AppShell for
|
|
716
1229
|
every router's link renderer to spread — so an app could not restyle a nav link at all
|
|
@@ -727,7 +1240,7 @@ textarea[data-terp="input"] {
|
|
|
727
1240
|
gap: var(--space-2);
|
|
728
1241
|
padding: var(--space-2) var(--space-3);
|
|
729
1242
|
border-radius: var(--radius-md);
|
|
730
|
-
color: var(--color-
|
|
1243
|
+
color: var(--color-sidebar-muted);
|
|
731
1244
|
font-size: var(--font-size-sm);
|
|
732
1245
|
font-weight: var(--font-weight-medium);
|
|
733
1246
|
text-decoration: none;
|
|
@@ -735,7 +1248,9 @@ textarea[data-terp="input"] {
|
|
|
735
1248
|
overflow: hidden;
|
|
736
1249
|
box-sizing: border-box;
|
|
737
1250
|
min-height: 2.25rem;
|
|
738
|
-
transition:
|
|
1251
|
+
transition:
|
|
1252
|
+
background-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
1253
|
+
color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
739
1254
|
}
|
|
740
1255
|
/* The collapsed rail's link geometry: one centred fixed-size icon in the content track.
|
|
741
1256
|
(0,3,1) against the base's (0,1,1), so it wins on specificity with no source-order
|
|
@@ -751,16 +1266,20 @@ textarea[data-terp="input"] {
|
|
|
751
1266
|
text-overflow: ellipsis;
|
|
752
1267
|
white-space: nowrap;
|
|
753
1268
|
}
|
|
754
|
-
/* Visually hidden,
|
|
1269
|
+
/* Visually hidden, five elements, one rule. One is the skip link, which is the whole point of
|
|
1270
|
+
the block for it: hidden at rest and un-hidden by a rule in terp.state. Two are the drawer's
|
|
1271
|
+
focus sentinels, which
|
|
755
1272
|
must stay focusable and so cannot be display: none. The other two are the brand title
|
|
756
1273
|
and the nav labels in the icon rail, which were a style-object TERNARY before this —
|
|
757
1274
|
the component picked between two objects per render, and the collapsed branch was
|
|
758
1275
|
painted by nothing, because the rail state was internal and no specimen could reach it.
|
|
759
1276
|
That is what defaultCollapsed is for. */
|
|
1277
|
+
[data-terp="appshell-skip-link"],
|
|
760
1278
|
[data-terp="drawer-focus-start"],
|
|
761
1279
|
[data-terp="drawer-focus-end"],
|
|
762
1280
|
[data-terp="appshell-sidebar"][data-collapsed="true"] [data-terp="appshell-brand-title"],
|
|
763
|
-
[data-terp="appshell-sidebar"][data-collapsed="true"] [data-terp="appshell-nav-label"]
|
|
1281
|
+
[data-terp="appshell-sidebar"][data-collapsed="true"] [data-terp="appshell-nav-label"],
|
|
1282
|
+
[data-terp="appshell-sidebar"][data-collapsed="true"] [data-terp="appshell-nav-group-label"] {
|
|
764
1283
|
position: absolute;
|
|
765
1284
|
width: 1px;
|
|
766
1285
|
height: 1px;
|
|
@@ -787,7 +1306,7 @@ textarea[data-terp="input"] {
|
|
|
787
1306
|
justify-content: space-between;
|
|
788
1307
|
gap: var(--space-3);
|
|
789
1308
|
padding: var(--space-2) var(--space-4);
|
|
790
|
-
min-height:
|
|
1309
|
+
min-height: var(--shell-header-height);
|
|
791
1310
|
box-sizing: border-box;
|
|
792
1311
|
background: var(--color-neutral-0);
|
|
793
1312
|
border-block-end: 1px solid var(--color-neutral-200);
|
|
@@ -826,6 +1345,19 @@ textarea[data-terp="input"] {
|
|
|
826
1345
|
font-weight: var(--font-weight-normal);
|
|
827
1346
|
line-height: 1.25;
|
|
828
1347
|
}
|
|
1348
|
+
/* The drawer's close button sits INSIDE the sidebar, so its INK comes from the sidebar family
|
|
1349
|
+
and not from the header toggle's neutral. Everything else it shares with the toggle above —
|
|
1350
|
+
both are icon buttons in shell chrome, and only the colour depends on which chrome.
|
|
1351
|
+
Wiring the sidebar family without this left two adjacent controls in the same drawer on two
|
|
1352
|
+
different ramps: in dark a nav link hovering to #334155 beside a close button hovering to
|
|
1353
|
+
#263449, and in contrast a blue wash beside a grey one.
|
|
1354
|
+
An override, NOT a split of the rule above. Splitting it was the first attempt and it moved
|
|
1355
|
+
the shared declarations into this selector, so the header toggle lost its background, border,
|
|
1356
|
+
radius, cursor and type — visible immediately as ~1,160 repainted pixels on every shell
|
|
1357
|
+
specimen, which is how a one-line edit to a grouped selector announces itself. */
|
|
1358
|
+
[data-terp="appshell-brand-row"] > [data-terp="iconbutton"] {
|
|
1359
|
+
color: var(--color-sidebar-muted);
|
|
1360
|
+
}
|
|
829
1361
|
[data-terp="appshell-main"] {
|
|
830
1362
|
flex-grow: 1;
|
|
831
1363
|
padding: var(--space-6);
|
|
@@ -840,6 +1372,72 @@ textarea[data-terp="input"] {
|
|
|
840
1372
|
color: var(--color-fg-subtle);
|
|
841
1373
|
font-size: var(--font-size-xs);
|
|
842
1374
|
}
|
|
1375
|
+
/* The header placement (ADR 0098 §8): the nav moves into the header and the sidebar is not
|
|
1376
|
+
rendered at all. Three rules, and the attribute is stamped only on desktop and only when the
|
|
1377
|
+
prop asked for it, so none of them needs a [data-variant] guard and no existing shell is in
|
|
1378
|
+
scope of any of them.
|
|
1379
|
+
|
|
1380
|
+
The header BECOMES the sidebar surface, which is one declaration doing the work of six.
|
|
1381
|
+
Every part of the navigation — the brand, its title, the resting link, the hover wash, the
|
|
1382
|
+
active link — already reads --color-sidebar-*, so moving the surface carries the whole family
|
|
1383
|
+
with it and not one property is overridden here. It is also the only reading that survives
|
|
1384
|
+
theming: an app that paints its sidebar navy gets a navy header with the same legible ink,
|
|
1385
|
+
where per-property overrides would have given it navy ink on a white header. The declared
|
|
1386
|
+
sidebar pairings are therefore still exactly the pairings in play and the contrast gate needs
|
|
1387
|
+
nothing new — which is the check that the mechanism is right rather than merely short.
|
|
1388
|
+
|
|
1389
|
+
In the shipped themes this moves almost nothing: --color-sidebar-bg equals
|
|
1390
|
+
--color-neutral-0 in four of the five, and in light it is the same #f8fafc the sidebar
|
|
1391
|
+
already uses. The rule is load-bearing for an app's theme, not for ours. */
|
|
1392
|
+
[data-terp="appshell"][data-nav-placement="header"] [data-terp="appshell-header"] {
|
|
1393
|
+
background: var(--color-sidebar-bg);
|
|
1394
|
+
border-block-end-color: var(--color-sidebar-border);
|
|
1395
|
+
}
|
|
1396
|
+
/* The list turns horizontal. Flex rather than a row of grid columns, because the header is
|
|
1397
|
+
already a wrapping row: a nav with more items than fit takes a second line and the header
|
|
1398
|
+
grows, instead of overflowing to somewhere no pointer can reach. */
|
|
1399
|
+
[data-terp="appshell"][data-nav-placement="header"] [data-terp="appshell-nav-list"] {
|
|
1400
|
+
display: flex;
|
|
1401
|
+
flex-wrap: wrap;
|
|
1402
|
+
}
|
|
1403
|
+
/* The nav keeps its flex-grow from the sidebar rule, which here takes the slack between the
|
|
1404
|
+
brand and the header group and pins the controls right with no margin of its own.
|
|
1405
|
+
overflow is a FIX rather than a reset: the sidebar's nav is a vertical scroll container, and
|
|
1406
|
+
a computed overflow-y of auto forces overflow-x to auto as well, so in a header — where the
|
|
1407
|
+
box is exactly one link tall — a focused link's 2px outline and 3px ring would be clipped on
|
|
1408
|
+
both edges by a scroller that can never scroll. */
|
|
1409
|
+
[data-terp="appshell"][data-nav-placement="header"] [data-terp="appshell-nav"] {
|
|
1410
|
+
overflow: visible;
|
|
1411
|
+
/* The groups are a ROW here, not a column. Without this each group wrapper is a block and a
|
|
1412
|
+
two-group header renders one stacked list per group — a nav placed in the header to avoid
|
|
1413
|
+
permanent chrome, growing the header instead.
|
|
1414
|
+
|
|
1415
|
+
The row lives on the NAV rather than on the wrappers because the wrappers are what has to
|
|
1416
|
+
line up, and it is why the stacking margin above is scoped away from this placement: a
|
|
1417
|
+
block-start margin on a flex item is a cross-axis margin and is never collapsed.
|
|
1418
|
+
|
|
1419
|
+
With one group this changes nothing measurable. The single wrapper becomes a flex item
|
|
1420
|
+
sized to its content instead of a full-width block, and its list is laid out from the same
|
|
1421
|
+
left edge either way; the existing app-shell-header-nav baseline is what says so. */
|
|
1422
|
+
display: flex;
|
|
1423
|
+
flex-wrap: wrap;
|
|
1424
|
+
gap: var(--space-5);
|
|
1425
|
+
}
|
|
1426
|
+
/* A group in the header: its label sits beside its links rather than above them, since a header
|
|
1427
|
+
row has no second line to put it on. The gap is the label-to-list separation and nothing
|
|
1428
|
+
else — the list keeps its own var(--space-1) between links. */
|
|
1429
|
+
[data-terp="appshell"][data-nav-placement="header"] [data-terp="appshell-nav-group"] {
|
|
1430
|
+
display: flex;
|
|
1431
|
+
align-items: center;
|
|
1432
|
+
gap: var(--space-2);
|
|
1433
|
+
}
|
|
1434
|
+
/* The label's block padding is the sidebar's, where it separates the label from the links BELOW
|
|
1435
|
+
it. In a row that padding is on the wrong axis: it adds to the group gap and pushes the label
|
|
1436
|
+
off the links' centre line. Zeroed to the inline axis only, so the label keeps the horizontal
|
|
1437
|
+
rhythm and loses the vertical. */
|
|
1438
|
+
[data-terp="appshell"][data-nav-placement="header"] [data-terp="appshell-nav-group-label"] {
|
|
1439
|
+
padding-block: 0;
|
|
1440
|
+
}
|
|
843
1441
|
|
|
844
1442
|
/* The page frame ----------------------------------------------------------- */
|
|
845
1443
|
/* Every routed view is this shape: one header carrying the breadcrumb trail (when
|
|
@@ -867,6 +1465,20 @@ textarea[data-terp="input"] {
|
|
|
867
1465
|
align-content: start;
|
|
868
1466
|
min-width: 0;
|
|
869
1467
|
}
|
|
1468
|
+
/* The narrow frame: a form or a settings screen, capped header and all.
|
|
1469
|
+
32rem is not a new number. It is exactly what admin-form declares on the two packaged create
|
|
1470
|
+
screens and what ProfileView's card carries, and 4b already named that card as a page measure
|
|
1471
|
+
wearing a card's clothes. So this is the mechanism those three were each hand-rolling, and
|
|
1472
|
+
folding them into it is the follow-up rather than part of shipping it.
|
|
1473
|
+
max-width on the ARTICLE, not width on its children, which is the opposite of the shell's
|
|
1474
|
+
content measure one rule above. Two reasons. The header is meant to be capped here — a Save
|
|
1475
|
+
button a screen-width from its field is worse than one over it — so there is nothing to
|
|
1476
|
+
exempt and no :not() to write. And capping the article composes with the shell measure by
|
|
1477
|
+
construction instead of competing with it: the article is already at most the shell's measure,
|
|
1478
|
+
and this takes it narrower still. */
|
|
1479
|
+
[data-terp="page"][data-measure="narrow"] {
|
|
1480
|
+
max-width: 32rem;
|
|
1481
|
+
}
|
|
870
1482
|
[data-terp="page-header"] {
|
|
871
1483
|
display: grid;
|
|
872
1484
|
gap: var(--space-2);
|
|
@@ -899,6 +1511,94 @@ textarea[data-terp="input"] {
|
|
|
899
1511
|
color: var(--color-neutral-900);
|
|
900
1512
|
line-height: 1.3;
|
|
901
1513
|
}
|
|
1514
|
+
/* The content measure, and the subheader band, which are ONE declaration rather than two
|
|
1515
|
+
features (ADR 0097 §2). A full-width band only means anything once the column beside it is
|
|
1516
|
+
constrained, and constraining the column is what leaves the header spanning the full track.
|
|
1517
|
+
|
|
1518
|
+
No new element and no portal, and both were considered rather than assumed. A wrapper
|
|
1519
|
+
around the body — display: contents included — becomes the sole child of article.children
|
|
1520
|
+
and fails every governed page closed, because that slot check is a DOM traversal and sees
|
|
1521
|
+
the node whether or not it generates a box. A portal leaves no node and survives that, but
|
|
1522
|
+
createPortal needs a container that exists when the child renders and the shell can only
|
|
1523
|
+
publish one through state: first commit local, second commit in the band, a one-frame jump
|
|
1524
|
+
on every navigation traded for nothing.
|
|
1525
|
+
|
|
1526
|
+
Neither is needed, because [data-terp="page"] is ALREADY a single-column grid. The header
|
|
1527
|
+
keeps the track; every other child takes the measure. So the band is the header that was
|
|
1528
|
+
always there.
|
|
1529
|
+
|
|
1530
|
+
The exemption is keyed on the page-header MARKER rather than on the header TAG, and that
|
|
1531
|
+
distinction is a fix rather than a detail. A :not(header) exempts every <header> that happens
|
|
1532
|
+
to be a direct child, so a bespoke screen writing
|
|
1533
|
+
<Page><header>section head</header><DataView/></Page> — legal, since the plain Page is
|
|
1534
|
+
deliberately unconstrained by the layout contract — would get a second full-width band it
|
|
1535
|
+
never asked for, silently. The frame's own header is the only thing meant to span the track,
|
|
1536
|
+
and the marker says so. (The layout contract's runtime check still drops the header by TAG
|
|
1537
|
+
name, because that check runs where no marker is guaranteed; the two mechanisms answer
|
|
1538
|
+
different questions and only this one is a style.)
|
|
1539
|
+
|
|
1540
|
+
And no backticks in this block, which is not a style note: a backtick here TERMINATES
|
|
1541
|
+
TERP_STYLES_CSS and the parse fails somewhere else entirely with "try inserting a
|
|
1542
|
+
semicolon". This comment cost that mistake once while being written.
|
|
1543
|
+
|
|
1544
|
+
Gated on an attribute the SHELL stamps, so nothing moves for any app today: with
|
|
1545
|
+
data-content-width absent this rule matches nothing at all. And "full width" means the full
|
|
1546
|
+
width of the article's own track — appshell-main's padding is outside it, so this is a
|
|
1547
|
+
measure within the content column rather than a bleed to the window edge, which would need
|
|
1548
|
+
a negative margin and therefore an inline site.
|
|
1549
|
+
|
|
1550
|
+
WIDTH, not max-width, and that is the whole correctness of the rule rather than a
|
|
1551
|
+
preference. This selector weighs (0,4,0) — four attribute selectors, three of them here and
|
|
1552
|
+
one inside :not(), and the universal contributes nothing — so as a max-width it OUTRANKS
|
|
1553
|
+
every component that declares a narrower one, and
|
|
1554
|
+
five of them are legal children of a governed body: resource-list (40rem), admin-form
|
|
1555
|
+
(32rem), dialog (26rem) and text[data-measure] at 48ch and 72ch. Measured before it was
|
|
1556
|
+
fixed: an admin-form inside a measured shell computed max-width 1280px instead of 512px,
|
|
1557
|
+
so the packaged provisioning form rendered two and a half times too wide. The shell would
|
|
1558
|
+
have been WIDENING the very components that already carry their own measure — including the
|
|
1559
|
+
Text prop this mechanism was modelled on.
|
|
1560
|
+
|
|
1561
|
+
As a width it composes instead of competing, because CSS resolves max-width AFTER width:
|
|
1562
|
+
min(100%, measure) caps a child that has no measure of its own, and a child that has one
|
|
1563
|
+
still wins with it. min() rather than a bare token so a track narrower than the measure is
|
|
1564
|
+
untouched rather than overflowing. */
|
|
1565
|
+
[data-terp="appshell"][data-content-width="measured"]
|
|
1566
|
+
[data-terp="page"] > *:not([data-terp="page-header"]) {
|
|
1567
|
+
width: min(100%, var(--shell-content-max-width));
|
|
1568
|
+
}
|
|
1569
|
+
/* The reach-through, for the one body child that generates no box of its own. Markdown is
|
|
1570
|
+
display: contents (see its rule, which used to claim no child-star selector existed in this
|
|
1571
|
+
sheet — the one above is exactly that selector, and the claim is corrected there). The rule
|
|
1572
|
+
above therefore MATCHES the markdown wrapper and then has nothing to apply a width to, since
|
|
1573
|
+
a non-inherited property on a boxless element is dropped. The result was prose running the
|
|
1574
|
+
full width of a measured shell, which is the one thing that mechanism exists to prevent, on
|
|
1575
|
+
the one component whose whole purpose is long-form text. Capping its blocks instead reaches
|
|
1576
|
+
the boxes the wrapper stands in for. */
|
|
1577
|
+
[data-terp="appshell"][data-content-width="measured"]
|
|
1578
|
+
[data-terp="page"] > [data-terp="markdown"] > * {
|
|
1579
|
+
width: min(100%, var(--shell-content-max-width));
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/* The split archetype ------------------------------------------------------ */
|
|
1583
|
+
/* A list beside the record it selects. Mobile-first: one column, list first, so the tab
|
|
1584
|
+
sequence is the reading order in both layouts and the stacked case needs no rule at all.
|
|
1585
|
+
The two-column form lives in the sheet's ONE existing wide-viewport block further down,
|
|
1586
|
+
rather than opening a second @media — same reason Stack's responsive rules went there.
|
|
1587
|
+
|
|
1588
|
+
align-items: start so a short detail pane does not stretch to the list's height, which is
|
|
1589
|
+
what makes the two read as panes rather than as table cells. */
|
|
1590
|
+
[data-terp="splitpage-panes"] {
|
|
1591
|
+
display: grid;
|
|
1592
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1593
|
+
gap: var(--space-4);
|
|
1594
|
+
align-items: start;
|
|
1595
|
+
min-width: 0;
|
|
1596
|
+
}
|
|
1597
|
+
/* Each pane is a min-width: 0 grid item, or a wide DataView inside one refuses to shrink and
|
|
1598
|
+
pushes the row past its track — the same floor Grid's cells carry, for the same reason. */
|
|
1599
|
+
[data-terp="splitpane"] {
|
|
1600
|
+
min-width: 0;
|
|
1601
|
+
}
|
|
902
1602
|
|
|
903
1603
|
/* The sign-in screen ------------------------------------------------------- */
|
|
904
1604
|
/* The one screen an unauthenticated user sees, and the only full-viewport page in
|
|
@@ -966,10 +1666,6 @@ textarea[data-terp="input"] {
|
|
|
966
1666
|
display: grid;
|
|
967
1667
|
gap: var(--space-3);
|
|
968
1668
|
}
|
|
969
|
-
[data-terp="login-form"] > [data-terp="button"],
|
|
970
|
-
[data-terp="login-sso"] > [data-terp="button"] {
|
|
971
|
-
width: 100%;
|
|
972
|
-
}
|
|
973
1669
|
[data-terp="login-separator"] {
|
|
974
1670
|
display: flex;
|
|
975
1671
|
align-items: center;
|
|
@@ -1010,12 +1706,20 @@ textarea[data-terp="input"] {
|
|
|
1010
1706
|
border: 1px solid var(--color-neutral-200);
|
|
1011
1707
|
border-radius: var(--radius-lg);
|
|
1012
1708
|
}
|
|
1013
|
-
/* The initials tile.
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1709
|
+
/* The initials tile, once. This was two rules — profile-avatar and user-menu-avatar —
|
|
1710
|
+
of eleven declarations each, identical but for a width, a height and a font size,
|
|
1711
|
+
which is a component the framework happened to ship twice under two names.
|
|
1712
|
+
|
|
1713
|
+
It is aria-hidden, so axe skips it by design and the declared pairing is the only
|
|
1714
|
+
thing measuring its ink: brand-primary-contrast on brand-primary is
|
|
1715
|
+
primary-button-label, which the contrast gate holds at AA in all five themes.
|
|
1716
|
+
Exactly the shape of NavIcon's fallback tile, which failed at 1.60 for as long as
|
|
1717
|
+
nothing declared it.
|
|
1718
|
+
|
|
1719
|
+
md carries no attribute of its own, the way every other sized component here works:
|
|
1720
|
+
the base rule IS the default, and a data-size="md" rule would leave two places
|
|
1721
|
+
describing the same tile. */
|
|
1722
|
+
[data-terp="avatar"] {
|
|
1019
1723
|
display: inline-flex;
|
|
1020
1724
|
align-items: center;
|
|
1021
1725
|
justify-content: center;
|
|
@@ -1028,6 +1732,11 @@ textarea[data-terp="input"] {
|
|
|
1028
1732
|
font-size: var(--font-size-lg);
|
|
1029
1733
|
font-weight: var(--font-weight-medium);
|
|
1030
1734
|
}
|
|
1735
|
+
[data-terp="avatar"][data-size="sm"] {
|
|
1736
|
+
width: 2rem;
|
|
1737
|
+
height: 2rem;
|
|
1738
|
+
font-size: var(--font-size-sm);
|
|
1739
|
+
}
|
|
1031
1740
|
[data-terp="profile-email"] {
|
|
1032
1741
|
overflow-wrap: anywhere;
|
|
1033
1742
|
}
|
|
@@ -1059,21 +1768,6 @@ textarea[data-terp="input"] {
|
|
|
1059
1768
|
margin: 0;
|
|
1060
1769
|
font-size: var(--font-size-base);
|
|
1061
1770
|
}
|
|
1062
|
-
/* The audit event's JSON payload. No font-family: it is a <pre>, so the UA
|
|
1063
|
-
stylesheet's monospace already applies and the inline object set none either.
|
|
1064
|
-
|
|
1065
|
-
font-size-sm loses the inline fallback the object carried (0.875rem) and no other
|
|
1066
|
-
rule in this sheet has one. The fallback could never fire — tokens.guard.test.ts
|
|
1067
|
-
refuses any var() in react-core naming a property the contract does not publish,
|
|
1068
|
-
so the token is always there. It recorded an author's doubt, not an option. */
|
|
1069
|
-
[data-terp="admin-payload"] {
|
|
1070
|
-
margin: 0;
|
|
1071
|
-
padding: var(--space-3);
|
|
1072
|
-
background: var(--color-neutral-100);
|
|
1073
|
-
border-radius: var(--radius-md);
|
|
1074
|
-
font-size: var(--font-size-sm);
|
|
1075
|
-
overflow-x: auto;
|
|
1076
|
-
}
|
|
1077
1771
|
|
|
1078
1772
|
/* Hub cards --------------------------------------------------------------- */
|
|
1079
1773
|
/* This whole family was in terp.state, resting declarations and all, for the same
|
|
@@ -1141,7 +1835,9 @@ textarea[data-terp="input"] {
|
|
|
1141
1835
|
[data-terp="hubcard"] {
|
|
1142
1836
|
height: 100%;
|
|
1143
1837
|
min-height: 0;
|
|
1144
|
-
transition:
|
|
1838
|
+
transition:
|
|
1839
|
+
box-shadow var(--motion-duration-fast) var(--motion-easing-standard),
|
|
1840
|
+
transform var(--motion-duration-fast) var(--motion-easing-standard);
|
|
1145
1841
|
}
|
|
1146
1842
|
[data-terp="hubcard-body"] {
|
|
1147
1843
|
display: grid;
|
|
@@ -1155,7 +1851,7 @@ textarea[data-terp="input"] {
|
|
|
1155
1851
|
background: var(--color-neutral-0);
|
|
1156
1852
|
color: var(--color-neutral-900);
|
|
1157
1853
|
box-sizing: border-box;
|
|
1158
|
-
transition: border-color
|
|
1854
|
+
transition: border-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
1159
1855
|
}
|
|
1160
1856
|
/* -heading, not -title: in this sheet a heading is the BOX holding a title
|
|
1161
1857
|
(card-heading, dataview-card-heading) and a title is the text box itself
|
|
@@ -1185,7 +1881,7 @@ textarea[data-terp="input"] {
|
|
|
1185
1881
|
color: var(--color-neutral-900);
|
|
1186
1882
|
font-size: var(--font-size-base);
|
|
1187
1883
|
font-weight: var(--font-weight-semibold);
|
|
1188
|
-
transition: color
|
|
1884
|
+
transition: color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
1189
1885
|
}
|
|
1190
1886
|
/* neutral-600 rather than fg-muted, and it is not the tinted-surface case: this text
|
|
1191
1887
|
sits on the card's own neutral-0 and measures 7.58 / 7.94 / 7.50 / 7.60 / 18.42. */
|
|
@@ -1243,20 +1939,32 @@ textarea[data-terp="input"] {
|
|
|
1243
1939
|
[data-terp="dataview"] {
|
|
1244
1940
|
display: grid;
|
|
1245
1941
|
}
|
|
1246
|
-
/* The full variant's surface
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
the
|
|
1258
|
-
|
|
1259
|
-
|
|
1942
|
+
/* The full variant's surface belongs to the TABLE, not to the whole view.
|
|
1943
|
+
It used to wrap everything: one card holding the toolbar, the table and the
|
|
1944
|
+
pagination, divided internally by a border under the toolbar and over the
|
|
1945
|
+
pagination. That reads as three bands of one object, and it costs twice — the
|
|
1946
|
+
table's cells are then flush against the outer frame (nothing between the first
|
|
1947
|
+
column's text and the card edge but cell padding), and the toolbar's controls sit
|
|
1948
|
+
inside a surface they do not belong to.
|
|
1949
|
+
|
|
1950
|
+
So the surface moves down one level, onto whatever occupies the table's slot, and
|
|
1951
|
+
the toolbar and the pagination float on the page background instead. The table
|
|
1952
|
+
becomes the object; the controls above and below it become controls. That also
|
|
1953
|
+
dissolves the flush-to-the-edge problem rather than padding around it: the table's
|
|
1954
|
+
own frame is the edge now, and --density-cell-pad-x is already the inset from it.
|
|
1955
|
+
|
|
1956
|
+
The alternative considered was keeping the outer card and adding inline padding to
|
|
1957
|
+
the table inside it. Rejected: it fixes the symptom by making the card thicker,
|
|
1958
|
+
leaves the toolbar inside a surface, and leaves two nested frames whenever the view
|
|
1959
|
+
is empty (the empty state's dashed frame inside the card's solid one).
|
|
1960
|
+
|
|
1961
|
+
Keyed on [data-variant="full"] rather than the bare marker for the reason the
|
|
1962
|
+
previous rule gave and which still holds: [data-variant="embedded"] must declare
|
|
1963
|
+
nothing, and un-declaring a surface with background: transparent / border: 0 is the
|
|
1964
|
+
shape ADR 0094 exists to avoid. */
|
|
1965
|
+
[data-terp="dataview"][data-variant="full"] > [data-terp="dataview-scroll"],
|
|
1966
|
+
[data-terp="dataview"][data-variant="full"] > [data-terp="dataview-error"],
|
|
1967
|
+
[data-terp="dataview"][data-variant="full"] > [data-terp="dataview-skeleton"] {
|
|
1260
1968
|
background: var(--color-neutral-0);
|
|
1261
1969
|
border: 1px solid var(--color-neutral-200);
|
|
1262
1970
|
border-radius: var(--radius-lg);
|
|
@@ -1335,22 +2043,38 @@ textarea[data-terp="input"] {
|
|
|
1335
2043
|
No colour declaration here on purpose. Two of this element's direct children
|
|
1336
2044
|
are arbitrary caller slots, and inheriting a muted ink onto app-authored filter
|
|
1337
2045
|
controls would be a silent restyle of app DOM. */
|
|
2046
|
+
/* Floating: no background, no divider, and no inline padding. The divider was the
|
|
2047
|
+
seam between two bands of one card and there is no card now — a border under a strip
|
|
2048
|
+
that sits on the page background is a line drawn across nothing. Dropping the inline
|
|
2049
|
+
padding aligns the controls with the table's OUTER edge (its frame) rather than with
|
|
2050
|
+
its cell text, which is the alignment a floating control row wants: the eye follows
|
|
2051
|
+
the frame, and a control indented to meet the first column's text reads as belonging
|
|
2052
|
+
inside the table.
|
|
2053
|
+
|
|
2054
|
+
The background's previous justification is worth answering rather than deleting: it
|
|
2055
|
+
was there for the EMBEDDED variant, whose root declares nothing but a display, so that
|
|
2056
|
+
the band would not show the page canvas through it. That is now the intent in both
|
|
2057
|
+
variants. A floating strip shows whatever is behind it — the page in the full variant,
|
|
2058
|
+
the app's own card in the embedded one — and in neither case is a neutral-0 rectangle
|
|
2059
|
+
under the controls something the design asks for. dataview-toolbar-bare still renders
|
|
2060
|
+
on a neutral-50 host, so the difference is visible in a baseline either way. */
|
|
1338
2061
|
[data-terp="dataview-toolbar"] {
|
|
1339
2062
|
display: flex;
|
|
1340
2063
|
align-items: center;
|
|
1341
2064
|
gap: var(--space-2);
|
|
1342
2065
|
flex-wrap: wrap;
|
|
1343
|
-
padding: var(--space-2)
|
|
1344
|
-
border-block-end: 1px solid var(--color-neutral-200);
|
|
1345
|
-
background: var(--color-neutral-0);
|
|
1346
|
-
border-top-left-radius: var(--radius-lg);
|
|
1347
|
-
border-top-right-radius: var(--radius-lg);
|
|
2066
|
+
padding-block: var(--space-2);
|
|
1348
2067
|
min-height: 3rem;
|
|
1349
2068
|
}
|
|
1350
2069
|
/* Selection mode. A resting surface rather than an interaction state, so
|
|
1351
2070
|
terp.base — and (0,2,0) against the base's (0,1,0) means it wins on
|
|
1352
2071
|
specificity alone, needing no :not() and no source-order dependency. */
|
|
1353
2072
|
[data-terp="dataview-toolbar"][data-variant="selection"] {
|
|
2073
|
+
/* Still a filled surface, because it marks a MODE and losing that would make
|
|
2074
|
+
selection invisible — but now it is a surface of its own rather than a band of the
|
|
2075
|
+
card, so it takes the padding and radius that make it read as one. */
|
|
2076
|
+
padding-inline: var(--density-cell-pad-x);
|
|
2077
|
+
border-radius: var(--radius-md);
|
|
1354
2078
|
background: var(--color-neutral-50);
|
|
1355
2079
|
}
|
|
1356
2080
|
[data-terp="dataview-toolbar-count"] {
|
|
@@ -1423,6 +2147,46 @@ textarea[data-terp="input"] {
|
|
|
1423
2147
|
cursor: pointer;
|
|
1424
2148
|
color: var(--color-fg-subtle);
|
|
1425
2149
|
}
|
|
2150
|
+
/* The password field's reveal toggle, built on the search box above rather than beside
|
|
2151
|
+
it: same positioning context, same absolutely-placed control, same specificity
|
|
2152
|
+
argument. Only type="password" wraps, so every other input is still a bare element
|
|
2153
|
+
and the two child selectors in this sheet that reach for data-terp="input" — the
|
|
2154
|
+
toolbar search and the resource-list create field — can never meet a wrapper. */
|
|
2155
|
+
[data-terp="input-password"] {
|
|
2156
|
+
position: relative;
|
|
2157
|
+
display: inline-flex;
|
|
2158
|
+
align-items: center;
|
|
2159
|
+
}
|
|
2160
|
+
/* Room for the toggle. It must out-rank input[data-terp="input"] { padding: 0
|
|
2161
|
+
var(--space-3) } and does so on SPECIFICITY — two attributes (0,2,0) against an
|
|
2162
|
+
attribute plus a type (0,1,1) — the same trap and the same escape the search field
|
|
2163
|
+
documents above. Asymmetric on purpose: the glyph sits at the end, and reserving room
|
|
2164
|
+
at both ends would indent the value for nothing. */
|
|
2165
|
+
[data-terp="input-password"] > [data-terp="input"] {
|
|
2166
|
+
padding-inline-end: var(--space-6);
|
|
2167
|
+
width: 100%;
|
|
2168
|
+
}
|
|
2169
|
+
/* The toggle itself, the seventeenth element wearing the iconbutton marker. It takes
|
|
2170
|
+
that marker rather than one of its own because it is one: the shared rule already
|
|
2171
|
+
carries its transition, its hover wash and its disabled treatment, and duplicating
|
|
2172
|
+
those under a new name to avoid editing one enumeration would be the wrong trade. */
|
|
2173
|
+
/* Edge ships its own reveal control inside every password field, so without this the user gets
|
|
2174
|
+
two: the native eye sitting on top of ours, in a box sized for one glyph. Same class of fix as
|
|
2175
|
+
the number stepper this sheet already suppresses -- an unthemeable browser affordance the
|
|
2176
|
+
framework replaces rather than competes with. */
|
|
2177
|
+
input[data-terp="input"][type="password"]::-ms-reveal {
|
|
2178
|
+
display: none;
|
|
2179
|
+
}
|
|
2180
|
+
[data-terp="input-password"] > [data-terp="iconbutton"] {
|
|
2181
|
+
position: absolute;
|
|
2182
|
+
inset-inline-end: var(--space-1);
|
|
2183
|
+
display: inline-flex;
|
|
2184
|
+
padding: var(--space-1);
|
|
2185
|
+
background: transparent;
|
|
2186
|
+
border: none;
|
|
2187
|
+
cursor: pointer;
|
|
2188
|
+
color: var(--color-fg-subtle);
|
|
2189
|
+
}
|
|
1426
2190
|
/* "Refreshing…", and this is the one place the prefer-an-existing-DOM-attribute
|
|
1427
2191
|
rule is REFUSED with its own reasoning. [data-terp="dataview-toolbar"]
|
|
1428
2192
|
> [role="status"] looks textbook — the component does own this span's role — but
|
|
@@ -1491,6 +2255,28 @@ textarea[data-terp="input"] {
|
|
|
1491
2255
|
white-space: nowrap;
|
|
1492
2256
|
background: var(--color-neutral-0);
|
|
1493
2257
|
}
|
|
2258
|
+
/* A column's declared track. A MINIMUM rather than a width, because a specified width is only a
|
|
2259
|
+
preference under table-layout: auto and the algorithm shrinks it to fit — which is why the pixel
|
|
2260
|
+
hint this replaces did nothing at all, measured in the workbench at three columns asking for
|
|
2261
|
+
700px each and fitting the box exactly. What auto layout cannot take away is a minimum.
|
|
2262
|
+
|
|
2263
|
+
Three steps, and no more: these are the three bands the framework's own tables declare, and a
|
|
2264
|
+
step is additive to add and breaking to remove. In rem, so a declared track follows the root
|
|
2265
|
+
font size; the system columns below keep their pixels on purpose, being chrome rather than
|
|
2266
|
+
content, and converting them is a density pass with its own baselines.
|
|
2267
|
+
|
|
2268
|
+
Nothing here ever meets an inline width. A resized column stops emitting the attribute, so the
|
|
2269
|
+
user's own drag replaces the declared track instead of losing to it — the minimum would win the
|
|
2270
|
+
cascade, and a column springing back from a drag reads as a broken resizer. */
|
|
2271
|
+
[data-terp="dataview-table"] > thead > tr > th[data-width="xs"] {
|
|
2272
|
+
min-inline-size: 5rem;
|
|
2273
|
+
}
|
|
2274
|
+
[data-terp="dataview-table"] > thead > tr > th[data-width="sm"] {
|
|
2275
|
+
min-inline-size: 6.5rem;
|
|
2276
|
+
}
|
|
2277
|
+
[data-terp="dataview-table"] > thead > tr > th[data-width="md"] {
|
|
2278
|
+
min-inline-size: 9.5rem;
|
|
2279
|
+
}
|
|
1494
2280
|
[data-terp="dataview-row"] > td {
|
|
1495
2281
|
padding: var(--density-cell-pad-y) var(--density-cell-pad-x);
|
|
1496
2282
|
border-bottom: 1px solid var(--color-neutral-100);
|
|
@@ -1505,6 +2291,17 @@ textarea[data-terp="input"] {
|
|
|
1505
2291
|
data-tone on an element no selector could reach, and the row-tones baseline
|
|
1506
2292
|
would have lost its tints the moment the tone moved out of a style object.
|
|
1507
2293
|
It is unconditional now, and clickability is an attribute of its own. */
|
|
2294
|
+
/* The last row draws no bottom border, which fixes two things the full variant's own
|
|
2295
|
+
comment had already named and deferred. The container carries a 1px border and a
|
|
2296
|
+
radius, so the last row's border sat a pixel inside it as a DOUBLE line, and with no
|
|
2297
|
+
overflow: hidden it also ran straight across the rounded bottom corners. Dropping the
|
|
2298
|
+
border is the fix rather than clipping the container: overflow: hidden here would trap
|
|
2299
|
+
the horizontal scroll container and any overlay a cell renders, which is why that
|
|
2300
|
+
comment refused it. tbody's last row, not the table's — a footer row would want its
|
|
2301
|
+
own rule. */
|
|
2302
|
+
[data-terp="dataview-row"]:last-child > td {
|
|
2303
|
+
border-bottom: none;
|
|
2304
|
+
}
|
|
1508
2305
|
[data-terp="dataview-row"][data-clickable="true"] {
|
|
1509
2306
|
cursor: pointer;
|
|
1510
2307
|
}
|
|
@@ -1538,15 +2335,31 @@ textarea[data-terp="input"] {
|
|
|
1538
2335
|
sorted, so the unsorted glyph's dimming keys off its ABSENCE rather than off
|
|
1539
2336
|
an attribute minted for it — this component owns aria-sort, unlike the
|
|
1540
2337
|
breadcrumb's aria-current, which a router stamps on every ancestor link. */
|
|
2338
|
+
/* font: inherit is not enough, and the gap it leaves split the header row in two.
|
|
2339
|
+
The font shorthand carries no text-transform and no letter-spacing, and the UA
|
|
2340
|
+
stylesheet resets both on form controls — so in any table mixing sortable and
|
|
2341
|
+
non-sortable columns the plain th rendered uppercase with 0.04em tracking while
|
|
2342
|
+
the sortable one rendered sentence case with none, side by side in one row. Both
|
|
2343
|
+
are named explicitly because inheritance is what the UA overrode.
|
|
2344
|
+
|
|
2345
|
+
The block padding mirrors the th's so the button fills the cell it sits in,
|
|
2346
|
+
pulled back out by the negative margin. The control was a 17px-tall target inside
|
|
2347
|
+
a 34px cell — half the cell unused, and the most-used control in a data app
|
|
2348
|
+
clearing WCAG 2.5.8 only through the spacing exception. Filling the cell costs
|
|
2349
|
+
nothing and changes no layout: the button's box grows into padding the th
|
|
2350
|
+
already reserved. */
|
|
1541
2351
|
[data-terp="dataview-column-sort"] {
|
|
1542
2352
|
display: inline-flex;
|
|
1543
2353
|
align-items: center;
|
|
1544
2354
|
gap: var(--space-1);
|
|
1545
2355
|
font: inherit;
|
|
2356
|
+
text-transform: inherit;
|
|
2357
|
+
letter-spacing: inherit;
|
|
1546
2358
|
color: inherit;
|
|
1547
2359
|
background: transparent;
|
|
1548
2360
|
border: none;
|
|
1549
|
-
padding: 0;
|
|
2361
|
+
padding: var(--space-2) 0;
|
|
2362
|
+
margin-block: calc(-1 * var(--space-2));
|
|
1550
2363
|
cursor: pointer;
|
|
1551
2364
|
}
|
|
1552
2365
|
[data-terp="dataview-table"] > thead > tr > th:not([aria-sort]) > [data-terp="dataview-column-sort"] > svg {
|
|
@@ -1760,8 +2573,7 @@ th[data-terp="dataview-actions-cell"] > span {
|
|
|
1760
2573
|
justify-content: space-between;
|
|
1761
2574
|
gap: var(--space-3);
|
|
1762
2575
|
flex-wrap: wrap;
|
|
1763
|
-
padding: var(--space-2)
|
|
1764
|
-
border-block-start: 1px solid var(--color-neutral-200);
|
|
2576
|
+
padding-block: var(--space-2);
|
|
1765
2577
|
font-size: var(--font-size-sm);
|
|
1766
2578
|
color: var(--color-fg-subtle);
|
|
1767
2579
|
}
|
|
@@ -1949,6 +2761,24 @@ th[data-terp="dataview-actions-cell"] > span {
|
|
|
1949
2761
|
border-radius: var(--radius-lg);
|
|
1950
2762
|
background: var(--color-neutral-0);
|
|
1951
2763
|
}
|
|
2764
|
+
/* Compact: a section's emptiness rather than the page's. Same frame and same words,
|
|
2765
|
+
laid out as a row — the glyph beside the text instead of above it — so two of
|
|
2766
|
+
these stacked read as two quiet sections rather than 480px of repeated poster. */
|
|
2767
|
+
[data-terp="empty-state"][data-size="compact"] {
|
|
2768
|
+
grid-template-columns: auto 1fr;
|
|
2769
|
+
justify-items: start;
|
|
2770
|
+
align-items: center;
|
|
2771
|
+
gap: var(--space-2) var(--space-3);
|
|
2772
|
+
padding: var(--space-3) var(--space-4);
|
|
2773
|
+
text-align: start;
|
|
2774
|
+
}
|
|
2775
|
+
[data-terp="empty-state"][data-size="compact"] > [data-terp="empty-state-title"] {
|
|
2776
|
+
font-size: var(--font-size-sm);
|
|
2777
|
+
}
|
|
2778
|
+
[data-terp="empty-state"][data-size="compact"] > [data-terp="empty-state-description"],
|
|
2779
|
+
[data-terp="empty-state"][data-size="compact"] > :not([data-terp="empty-state-icon"]):not([data-terp="empty-state-title"]) {
|
|
2780
|
+
grid-column: 2;
|
|
2781
|
+
}
|
|
1952
2782
|
[data-terp="empty-state-icon"] {
|
|
1953
2783
|
color: var(--color-neutral-400);
|
|
1954
2784
|
display: inline-flex;
|
|
@@ -2064,6 +2894,71 @@ input[data-terp="input"][role="combobox"] {
|
|
|
2064
2894
|
position: relative;
|
|
2065
2895
|
display: grid;
|
|
2066
2896
|
}
|
|
2897
|
+
/* Multiple: the tokens share the field's box with the input, wrapping onto as many rows
|
|
2898
|
+
as the selection needs. A fixed-height field would either clip the third token or
|
|
2899
|
+
reserve room for tokens nobody has chosen — and a set-valued field whose height never
|
|
2900
|
+
changes is lying about how much is in it.
|
|
2901
|
+
|
|
2902
|
+
The input keeps a minimum inline size so a filter is still typeable when the tokens have
|
|
2903
|
+
taken most of a row, and flex-basis 0 so it yields to them rather than pushing the last
|
|
2904
|
+
token out of the box. */
|
|
2905
|
+
[data-terp="combobox-field"][data-multiple="true"] {
|
|
2906
|
+
display: flex;
|
|
2907
|
+
flex-wrap: wrap;
|
|
2908
|
+
align-items: center;
|
|
2909
|
+
gap: var(--space-1);
|
|
2910
|
+
padding: var(--space-1);
|
|
2911
|
+
border: 1px solid var(--color-neutral-300);
|
|
2912
|
+
border-radius: var(--radius-md);
|
|
2913
|
+
background: var(--color-neutral-0);
|
|
2914
|
+
}
|
|
2915
|
+
[data-terp="combobox-field"][data-multiple="true"] > [data-terp="input"] {
|
|
2916
|
+
flex: 1 1 0;
|
|
2917
|
+
min-inline-size: 6rem;
|
|
2918
|
+
border: none;
|
|
2919
|
+
background: transparent;
|
|
2920
|
+
padding-inline: var(--space-1);
|
|
2921
|
+
}
|
|
2922
|
+
[data-terp="combobox-token"] {
|
|
2923
|
+
display: inline-flex;
|
|
2924
|
+
align-items: center;
|
|
2925
|
+
gap: var(--space-1);
|
|
2926
|
+
padding-block: 0;
|
|
2927
|
+
padding-inline: var(--space-2);
|
|
2928
|
+
font-size: var(--font-size-sm);
|
|
2929
|
+
color: var(--color-neutral-900);
|
|
2930
|
+
background: var(--color-neutral-100);
|
|
2931
|
+
border: 1px solid var(--color-neutral-200);
|
|
2932
|
+
border-radius: var(--radius-sm);
|
|
2933
|
+
/* Matches the control height a token sits beside, so a row of tokens and the input
|
|
2934
|
+
share one baseline instead of the tokens riding high. */
|
|
2935
|
+
min-block-size: calc(var(--density-control-min-height) - var(--space-2));
|
|
2936
|
+
}
|
|
2937
|
+
/* The remove control is a real button and a real tab stop, which is the accessible half of
|
|
2938
|
+
the Backspace shortcut rather than a duplicate of it: the shortcut is discoverable only if
|
|
2939
|
+
you already know it, and a token nobody can reach by keyboard cannot be removed by one. */
|
|
2940
|
+
[data-terp="combobox-token-remove"] {
|
|
2941
|
+
display: inline-flex;
|
|
2942
|
+
align-items: center;
|
|
2943
|
+
justify-content: center;
|
|
2944
|
+
min-inline-size: var(--space-4);
|
|
2945
|
+
min-block-size: var(--space-4);
|
|
2946
|
+
padding: 0;
|
|
2947
|
+
color: var(--color-neutral-600);
|
|
2948
|
+
background: transparent;
|
|
2949
|
+
border: none;
|
|
2950
|
+
border-radius: var(--radius-sm);
|
|
2951
|
+
cursor: pointer;
|
|
2952
|
+
line-height: 1;
|
|
2953
|
+
}
|
|
2954
|
+
[data-terp="combobox-token-remove"]:hover:not(:disabled) {
|
|
2955
|
+
color: var(--color-neutral-900);
|
|
2956
|
+
background: var(--color-neutral-200);
|
|
2957
|
+
}
|
|
2958
|
+
[data-terp="combobox-token-remove"]:disabled {
|
|
2959
|
+
cursor: not-allowed;
|
|
2960
|
+
opacity: 0.5;
|
|
2961
|
+
}
|
|
2067
2962
|
/* Addressed structurally rather than by a marker of its own: it is an
|
|
2068
2963
|
iconbutton, and the only thing distinguishing it is where it sits. */
|
|
2069
2964
|
[data-terp="combobox-field"] > [data-terp="iconbutton"] {
|
|
@@ -2248,7 +3143,7 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2248
3143
|
}
|
|
2249
3144
|
[data-terp="tooltip"] {
|
|
2250
3145
|
position: absolute;
|
|
2251
|
-
z-index:
|
|
3146
|
+
z-index: var(--z-index-tooltip);
|
|
2252
3147
|
inset-block-end: calc(100% + var(--space-1));
|
|
2253
3148
|
inset-inline-start: 0;
|
|
2254
3149
|
max-inline-size: min(18rem, calc(100vw - 2 * var(--space-4)));
|
|
@@ -2260,7 +3155,10 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2260
3155
|
font-weight: var(--font-weight-medium);
|
|
2261
3156
|
line-height: 1.4;
|
|
2262
3157
|
box-shadow: var(--shadow-md);
|
|
2263
|
-
pointer-events: none
|
|
3158
|
+
/* No pointer-events: none. It was here, and it makes WCAG 1.4.13's Hoverable clause
|
|
3159
|
+
impossible by construction: a bubble the pointer cannot reach is a bubble nobody
|
|
3160
|
+
tracking with a pointer, or reading under magnification, can finish reading. The
|
|
3161
|
+
component keeps it open across the gap with a short close delay instead. */
|
|
2264
3162
|
white-space: normal;
|
|
2265
3163
|
}
|
|
2266
3164
|
|
|
@@ -2285,7 +3183,10 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2285
3183
|
font-size: var(--font-size-sm);
|
|
2286
3184
|
font-weight: var(--font-weight-normal);
|
|
2287
3185
|
line-height: 1.25;
|
|
2288
|
-
transition:
|
|
3186
|
+
transition:
|
|
3187
|
+
background-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
3188
|
+
color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
3189
|
+
box-shadow var(--motion-duration-fast) var(--motion-easing-standard);
|
|
2289
3190
|
}
|
|
2290
3191
|
/* The panel's contents. This sits INSIDE popover-panel, which supplies the
|
|
2291
3192
|
surface — so the menu owns only the stacking of its items. */
|
|
@@ -2310,7 +3211,9 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2310
3211
|
font-size: var(--font-size-sm);
|
|
2311
3212
|
font-weight: var(--font-weight-normal);
|
|
2312
3213
|
line-height: 1.25;
|
|
2313
|
-
transition:
|
|
3214
|
+
transition:
|
|
3215
|
+
background-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
3216
|
+
color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
2314
3217
|
}
|
|
2315
3218
|
/* Destructive is the one enumerable choice an item has, so it is an attribute.
|
|
2316
3219
|
The disabled treatment is a state rule keyed on :disabled, because the element
|
|
@@ -2363,7 +3266,12 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2363
3266
|
width: 100%;
|
|
2364
3267
|
padding: var(--space-2);
|
|
2365
3268
|
text-align: left;
|
|
2366
|
-
|
|
3269
|
+
/* The sidebar family, not the neutral one. This row renders inside the sidebar (and inside the
|
|
3270
|
+
header group under navPlacement="header", which takes the sidebar surface), so its ink and
|
|
3271
|
+
that background are a pairing in play — and the contrast gate can only measure a pairing it
|
|
3272
|
+
can name. Provably zero-diff: --color-neutral-900 and --color-sidebar-fg are byte-equal in
|
|
3273
|
+
all five themes. */
|
|
3274
|
+
color: var(--color-sidebar-fg);
|
|
2367
3275
|
border-color: transparent;
|
|
2368
3276
|
min-height: 0;
|
|
2369
3277
|
}
|
|
@@ -2373,19 +3281,6 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2373
3281
|
gap: 0;
|
|
2374
3282
|
padding: 0;
|
|
2375
3283
|
}
|
|
2376
|
-
[data-terp="user-menu-avatar"] {
|
|
2377
|
-
display: inline-flex;
|
|
2378
|
-
align-items: center;
|
|
2379
|
-
justify-content: center;
|
|
2380
|
-
width: 2rem;
|
|
2381
|
-
height: 2rem;
|
|
2382
|
-
flex-shrink: 0;
|
|
2383
|
-
border-radius: var(--radius-full);
|
|
2384
|
-
background: var(--color-brand-primary);
|
|
2385
|
-
color: var(--color-brand-primary-contrast);
|
|
2386
|
-
font-size: var(--font-size-sm);
|
|
2387
|
-
font-weight: var(--font-weight-medium);
|
|
2388
|
-
}
|
|
2389
3284
|
[data-terp="user-menu-identity"] {
|
|
2390
3285
|
display: grid;
|
|
2391
3286
|
min-width: 0;
|
|
@@ -2396,9 +3291,17 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2396
3291
|
text-overflow: ellipsis;
|
|
2397
3292
|
white-space: nowrap;
|
|
2398
3293
|
}
|
|
3294
|
+
/* Two surfaces, one marker. UserMenu renders this span in the trigger AND in the portalled
|
|
3295
|
+
panel, and only the first sits on the sidebar — the panel is in document.body, where the
|
|
3296
|
+
sidebar palette does not apply. So the sidebar copy is scoped (the portal puts the panel
|
|
3297
|
+
outside this selector by construction) and the panel keeps the neutral. The sheet already
|
|
3298
|
+
argues this exact split for the drawer close button. */
|
|
2399
3299
|
[data-terp="user-menu-role"] {
|
|
2400
3300
|
color: var(--color-neutral-600);
|
|
2401
3301
|
}
|
|
3302
|
+
[data-terp="user-menu"] [data-terp="user-menu-role"] {
|
|
3303
|
+
color: var(--color-sidebar-muted);
|
|
3304
|
+
}
|
|
2402
3305
|
/* The panel's identity block, and the panel's own geometry — both keyed on the
|
|
2403
3306
|
owner, because the portal put them outside every selector that could otherwise
|
|
2404
3307
|
reach them. */
|
|
@@ -2440,8 +3343,14 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2440
3343
|
rules ([data-terp="markdown"] p, ... ul), and a declaration added here would
|
|
2441
3344
|
simply do nothing with nothing to say so. And display: contents does not change
|
|
2442
3345
|
selector matching, only box generation: a parent's > * child selector now matches
|
|
2443
|
-
this wrapper rather than the blocks.
|
|
2444
|
-
|
|
3346
|
+
this wrapper rather than the blocks.
|
|
3347
|
+
|
|
3348
|
+
That last sentence used to end "Nothing in this sheet uses one, which is why the
|
|
3349
|
+
wrapper is free today", and it stopped being true when the measured content width
|
|
3350
|
+
shipped: that rule is a child-star selector on the page's body children. It matched it,
|
|
3351
|
+
found no box to give a width to, and let prose run full-bleed in a measured shell.
|
|
3352
|
+
The reach-through beside that rule is the fix; this wrapper is free of everything
|
|
3353
|
+
else.
|
|
2445
3354
|
|
|
2446
3355
|
Under SSR the sheet is not injected at all (the injector is document-guarded), so
|
|
2447
3356
|
a server-rendered page has this element as a block box until hydration. That is
|
|
@@ -2574,12 +3483,15 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2574
3483
|
margin-block-start: var(--space-2);
|
|
2575
3484
|
}
|
|
2576
3485
|
|
|
2577
|
-
/* Icon-only buttons.
|
|
3486
|
+
/* Icon-only buttons. Seventeen elements wear this marker and not one declares a
|
|
2578
3487
|
transition inline, so this belongs in terp.base — it sat in terp.state only
|
|
2579
3488
|
because that is where the hover rules needing it live. Same correction 817f572
|
|
2580
3489
|
made for Tabs and Breadcrumbs. */
|
|
2581
3490
|
[data-terp="iconbutton"] {
|
|
2582
|
-
transition:
|
|
3491
|
+
transition:
|
|
3492
|
+
background-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
3493
|
+
color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
3494
|
+
box-shadow var(--motion-duration-fast) var(--motion-easing-standard);
|
|
2583
3495
|
}
|
|
2584
3496
|
|
|
2585
3497
|
/* Popover ------------------------------------------------------------------ */
|
|
@@ -2613,12 +3525,26 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2613
3525
|
|
|
2614
3526
|
The stacking level is the token that was published for it. Every component in
|
|
2615
3527
|
the package hardcoded its own number while a full --z-index-* family sat
|
|
2616
|
-
unread.
|
|
2617
|
-
|
|
2618
|
-
--z-index-toast.
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
than a
|
|
3528
|
+
unread. That is finished: AppShell reads --z-index-drawer, --z-index-backdrop
|
|
3529
|
+
and --z-index-sticky, the skip link reads --z-index-skip-link, and the toast
|
|
3530
|
+
viewport reads --z-index-toast. (This paragraph said "AppShell still writes
|
|
3531
|
+
50/40/30 ... and comes right with its own migration" for a release after that
|
|
3532
|
+
migration landed, which is the shape of stale comment worth naming: it read as
|
|
3533
|
+
a known gap rather than as a finished one.) Tooltip's z-index WAS 1 on the
|
|
3534
|
+
reasoning that the tooltip is absolutely positioned inside its own anchor, so 1
|
|
3535
|
+
is a local lift within a stacking context rather than a place in the app-wide
|
|
3536
|
+
order. That is sound about the anchor and wrong about the page:
|
|
3537
|
+
[data-terp="tooltip-anchor"] is only position: relative, which does NOT create a
|
|
3538
|
+
stacking context, so the 1 competed in the ROOT context — against a sticky header
|
|
3539
|
+
at 30 and an open popover at 60 — and lost. Reported as a tooltip that renders
|
|
3540
|
+
below content "sometimes, not always", which is exactly what a level that depends
|
|
3541
|
+
on whatever ancestor happens to establish a context looks like. It reads
|
|
3542
|
+
--z-index-tooltip (70) now, the level published for it.
|
|
3543
|
+
|
|
3544
|
+
Still true, and NOT fixed by this: an ancestor with overflow: hidden clips an
|
|
3545
|
+
absolutely positioned tooltip whatever its level. The fix for that is the one
|
|
3546
|
+
[data-terp="popover-panel"] already uses — position: fixed with measured
|
|
3547
|
+
coordinates — and it is a change to the component rather than to this sheet. */
|
|
2622
3548
|
[data-terp="popover-panel"] {
|
|
2623
3549
|
position: fixed;
|
|
2624
3550
|
z-index: var(--z-index-popover);
|
|
@@ -2712,12 +3638,24 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2712
3638
|
opacity: 0.55;
|
|
2713
3639
|
cursor: not-allowed;
|
|
2714
3640
|
}
|
|
3641
|
+
/* A loading button IS disabled — the component sets both — so this has to sit here rather
|
|
3642
|
+
than in terp.base beside the other attribute-keyed button rules. In terp.base it would
|
|
3643
|
+
lose to the :disabled rule above on layer order and the cursor would silently stay
|
|
3644
|
+
not-allowed, which reads as "you may not" where the truth is "not yet". Declared after
|
|
3645
|
+
it so source order settles the tie the equal (0,2,0) specificity leaves.
|
|
3646
|
+
|
|
3647
|
+
Neither cursor is visible to any lane: Playwright's screenshots do not paint a pointer.
|
|
3648
|
+
The computed lane asserts both. */
|
|
3649
|
+
[data-terp="button"][data-loading="true"] {
|
|
3650
|
+
cursor: progress;
|
|
3651
|
+
}
|
|
2715
3652
|
|
|
2716
3653
|
/* Icon-only buttons: the shell's two header toggles, four pagination arrows, the
|
|
2717
3654
|
toast dismisser, the combobox's clear button, the calendar's two month arrows,
|
|
2718
|
-
the DataView's expand toggle, the view-options panel's two reorder arrows,
|
|
2719
|
-
|
|
2720
|
-
|
|
3655
|
+
the DataView's expand toggle, the view-options panel's two reorder arrows, the
|
|
3656
|
+
DataView toolbar's clear-search button and two layout toggles, and the password
|
|
3657
|
+
field's reveal toggle.
|
|
3658
|
+
SEVENTEEN SITES sharing a transition and nothing else — no shared SURFACE, because
|
|
2721
3659
|
each is styled by where it sits. Sites rather than elements: the reorder arrows
|
|
2722
3660
|
render twice per column row, so the element count is a function of how many
|
|
2723
3661
|
columns a view has, while the list of places to check is fixed.
|
|
@@ -2761,7 +3699,7 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2761
3699
|
|
|
2762
3700
|
The :disabled cursor no longer shouts, and re-deriving that is more useful than
|
|
2763
3701
|
trusting it. The question is never "has anything migrated" but "can any element
|
|
2764
|
-
this selector matches still beat it" — so: which of the
|
|
3702
|
+
this selector matches still beat it" — so: which of the seventeen can carry the
|
|
2765
3703
|
disabled attribute at all? The shell's toggles cannot (no disabled prop). The
|
|
2766
3704
|
toast dismisser cannot. The combobox's clear button renders only while the
|
|
2767
3705
|
field is enabled and takes no disabled of its own. The calendar's arrows page
|
|
@@ -2773,7 +3711,8 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2773
3711
|
background: var(--color-neutral-100);
|
|
2774
3712
|
color: var(--color-neutral-900);
|
|
2775
3713
|
}
|
|
2776
|
-
[data-terp="iconbutton"]:disabled
|
|
3714
|
+
[data-terp="iconbutton"]:disabled,
|
|
3715
|
+
[data-terp="iconbutton"][aria-disabled="true"] {
|
|
2777
3716
|
opacity: 0.4;
|
|
2778
3717
|
cursor: not-allowed;
|
|
2779
3718
|
}
|
|
@@ -2787,7 +3726,13 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2787
3726
|
only the text controls had migrated left a disabled Combobox painted exactly
|
|
2788
3727
|
like an enabled one and deleted the aria-invalid border outright. All six
|
|
2789
3728
|
now take their base from this sheet, so layer order is enough. */
|
|
2790
|
-
|
|
3729
|
+
/* The invalid exclusion is not tidying. This selector weighs (0,4,0) and the danger border below
|
|
3730
|
+
weighs (0,2,0), both unlayered against each other inside terp.state — so without the third
|
|
3731
|
+
:not() a pointer resting on a field that has just failed validation repaints its border from
|
|
3732
|
+
the danger token to a neutral grey, and the error state disappears for exactly as long as the
|
|
3733
|
+
user is pointing at the thing they need to fix. Narrowing the aggressor rather than adding a
|
|
3734
|
+
competing [aria-invalid="true"]:hover rule is this sheet's convention. */
|
|
3735
|
+
[data-terp="input"]:hover:not(:disabled):not(:focus):not([aria-invalid="true"]) {
|
|
2791
3736
|
border-color: var(--color-neutral-400);
|
|
2792
3737
|
}
|
|
2793
3738
|
[data-terp="input"]:focus,
|
|
@@ -2861,8 +3806,8 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2861
3806
|
The rail's scrollbar suppression moved from an attribute on the nav to the sidebar's,
|
|
2862
3807
|
because collapsed is one fact and it now has one owner. */
|
|
2863
3808
|
[data-terp="appshell-nav"] a:hover:not([aria-current="page"]) {
|
|
2864
|
-
background: var(--color-
|
|
2865
|
-
color: var(--color-
|
|
3809
|
+
background: var(--color-sidebar-accent);
|
|
3810
|
+
color: var(--color-sidebar-fg);
|
|
2866
3811
|
}
|
|
2867
3812
|
[data-terp="appshell-nav"] a[aria-current="page"] {
|
|
2868
3813
|
background: var(--color-brand-primary-soft);
|
|
@@ -2873,12 +3818,80 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2873
3818
|
overflow-x: hidden;
|
|
2874
3819
|
scrollbar-width: none;
|
|
2875
3820
|
}
|
|
3821
|
+
/* The rail's group separation. The label is visually hidden here (it joins the block above), so
|
|
3822
|
+
without this the groups are a single undifferentiated column of icons and the structure the
|
|
3823
|
+
expanded sidebar shows simply disappears at 4rem. A rule the LINE has to carry, because the
|
|
3824
|
+
label cannot: the divider is what is left of the label once the text is gone.
|
|
3825
|
+
|
|
3826
|
+
It replaces rather than adds to the expanded margin — same specificity family, one attribute
|
|
3827
|
+
more — so the rail does not pay 1rem per group in a column that is already scrolling. */
|
|
3828
|
+
[data-terp="appshell-sidebar"][data-collapsed="true"] [data-terp="appshell-nav-group"] + [data-terp="appshell-nav-group"] {
|
|
3829
|
+
margin-block-start: var(--space-2);
|
|
3830
|
+
padding-block-start: var(--space-2);
|
|
3831
|
+
border-block-start: 1px solid var(--color-sidebar-border);
|
|
3832
|
+
}
|
|
2876
3833
|
[data-terp="appshell-sidebar"][data-collapsed="true"] [data-terp="appshell-nav"]::-webkit-scrollbar {
|
|
2877
3834
|
width: 0;
|
|
2878
3835
|
height: 0;
|
|
2879
3836
|
}
|
|
2880
3837
|
[data-terp="appshell-brand"]:hover {
|
|
2881
|
-
background: var(--color-
|
|
3838
|
+
background: var(--color-sidebar-accent);
|
|
3839
|
+
}
|
|
3840
|
+
/* And its hover, for the same reason: the shared iconbutton hover wash is a neutral, which is
|
|
3841
|
+
the header toggle's context and not this one's. */
|
|
3842
|
+
[data-terp="appshell-brand-row"] > [data-terp="iconbutton"]:hover {
|
|
3843
|
+
background: var(--color-sidebar-accent);
|
|
3844
|
+
color: var(--color-sidebar-fg);
|
|
3845
|
+
}
|
|
3846
|
+
/* The skip link, visible only while focused.
|
|
3847
|
+
In terp.state, and that is not filing: the resting half is the shared visually-hidden block
|
|
3848
|
+
in terp.base, which sets position, a 1px box and clip, and un-hiding has to beat all of it.
|
|
3849
|
+
On specificity it would not — a selector list takes the specificity of the member that
|
|
3850
|
+
MATCHES, and for this element that member is [data-terp="appshell-skip-link"] at (0,1,0),
|
|
3851
|
+
the same weight as this rule. (An earlier version of this comment cited the list's (0,3,0)
|
|
3852
|
+
member, which is the collapsed-rail selector and never matches a skip link; that reading
|
|
3853
|
+
would have made the rules a source-order coin flip rather than a layer decision.) Layer
|
|
3854
|
+
order settles it with nothing to reason about.
|
|
3855
|
+
:focus-visible rather than :focus, matching the sheet's shared ring: a skip link reached by
|
|
3856
|
+
pointer is a link nobody asked to see.
|
|
3857
|
+
Above the sticky header (30) and its backdrop (40) so it is not painted under the chrome it
|
|
3858
|
+
sits over; below the drawer (50) because nothing should paint over an open modal. Stacking
|
|
3859
|
+
order does NOT keep it out of the drawer's focus trap and this comment used to say it did —
|
|
3860
|
+
z-index has no bearing on tab order. The link is simply not RENDERED while the drawer is
|
|
3861
|
+
open; see AppShell. */
|
|
3862
|
+
/* The skip link's target takes focus and must NOT paint the shared ring.
|
|
3863
|
+
The main element carries a data-terp marker and now a tabIndex of -1, which together put it
|
|
3864
|
+
in scope of the shared [data-terp]:focus-visible ring — so activating the skip link outlined
|
|
3865
|
+
the entire content column, header to footer, plus a 3px halo. Measured: it matches
|
|
3866
|
+
:focus-visible, with a 2px solid outline and rgba(37,99,235,0.35) 0 0 0 3px. The ring exists
|
|
3867
|
+
to say which CONTROL will take the next keystroke; a scroll target that was focused
|
|
3868
|
+
programmatically is not one, and the visible result of following a skip link should be the
|
|
3869
|
+
content rather than a box drawn around it.
|
|
3870
|
+
Scoped to this marker rather than to tabindex=-1 in general: other elements take -1 for other
|
|
3871
|
+
reasons and some of them are controls. */
|
|
3872
|
+
[data-terp="appshell-main"]:focus-visible {
|
|
3873
|
+
outline: none;
|
|
3874
|
+
box-shadow: none;
|
|
3875
|
+
}
|
|
3876
|
+
[data-terp="appshell-skip-link"]:focus-visible {
|
|
3877
|
+
position: fixed;
|
|
3878
|
+
top: var(--space-2);
|
|
3879
|
+
inset-inline-start: var(--space-2);
|
|
3880
|
+
z-index: var(--z-index-skip-link);
|
|
3881
|
+
width: auto;
|
|
3882
|
+
height: auto;
|
|
3883
|
+
margin: 0;
|
|
3884
|
+
padding: var(--space-2) var(--space-3);
|
|
3885
|
+
clip: auto;
|
|
3886
|
+
overflow: visible;
|
|
3887
|
+
background: var(--color-neutral-0);
|
|
3888
|
+
color: var(--color-fg-accent);
|
|
3889
|
+
border: var(--border-width-thin) solid var(--color-fg-accent);
|
|
3890
|
+
border-radius: var(--radius-md);
|
|
3891
|
+
font-family: var(--font-family-sans);
|
|
3892
|
+
font-size: var(--font-size-sm);
|
|
3893
|
+
font-weight: var(--font-weight-medium);
|
|
3894
|
+
text-decoration: none;
|
|
2882
3895
|
}
|
|
2883
3896
|
|
|
2884
3897
|
/* Tabs -------------------------------------------------------------------- */
|
|
@@ -2998,13 +4011,14 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2998
4011
|
}
|
|
2999
4012
|
|
|
3000
4013
|
/* The pager's disabled ink, scoped rather than added to the shared iconbutton
|
|
3001
|
-
rule above: of the
|
|
4014
|
+
rule above: of the seventeen sites wearing that marker only six can be disabled at
|
|
3002
4015
|
all — these four and the view-options panel's two reorder arrows, which carry
|
|
3003
4016
|
their own scoped ink below for the same reason — and giving the shared rule a
|
|
3004
4017
|
colour would change how a disabled calendar arrow looks the day one becomes
|
|
3005
4018
|
disableable. The shared rule supplies the opacity and the cursor; this supplies
|
|
3006
4019
|
the ink the pager had inline. */
|
|
3007
|
-
[data-terp="dataview-pager"] > [data-terp="iconbutton"]:disabled
|
|
4020
|
+
[data-terp="dataview-pager"] > [data-terp="iconbutton"]:disabled,
|
|
4021
|
+
[data-terp="dataview-pager"] > [data-terp="iconbutton"][aria-disabled="true"] {
|
|
3008
4022
|
color: var(--color-neutral-300);
|
|
3009
4023
|
}
|
|
3010
4024
|
|
|
@@ -3034,7 +4048,7 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
3034
4048
|
table-layout: fixed;
|
|
3035
4049
|
}
|
|
3036
4050
|
[data-terp="dataview-table"] tbody tr {
|
|
3037
|
-
transition: background-color
|
|
4051
|
+
transition: background-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
3038
4052
|
}
|
|
3039
4053
|
[data-terp="dataview-table"] tbody tr:hover td {
|
|
3040
4054
|
background: var(--color-neutral-50);
|
|
@@ -3061,8 +4075,13 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
3061
4075
|
is enough, and the card's tone rules lose to it on layer rather than on
|
|
3062
4076
|
specificity. Keeping the escalation past this point is the quiet failure: nothing
|
|
3063
4077
|
would render differently, the declaration would simply become unthemeable. */
|
|
4078
|
+
/* Both halves carry the data-clickable guard, and the card half did not. A card list stamps
|
|
4079
|
+
data-clickable only when onRowClick is set, but renders the selection checkbox on
|
|
4080
|
+
selectionEnabled alone — so in a selectable-but-not-clickable list, focusing a checkbox
|
|
4081
|
+
washed the whole card in brand-soft and buried its data-tone. Focus is not selection, and
|
|
4082
|
+
a card that does nothing when clicked has no "activate me" state to advertise. */
|
|
3064
4083
|
[data-terp="dataview-row"][data-clickable="true"]:focus-within td,
|
|
3065
|
-
[data-terp="dataview-card"]:focus-within {
|
|
4084
|
+
[data-terp="dataview-card"][data-clickable="true"]:focus-within {
|
|
3066
4085
|
background: var(--color-brand-primary-soft);
|
|
3067
4086
|
}
|
|
3068
4087
|
|