@terpjs/react-core 0.8.0 → 0.10.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 +62 -22
- package/package.json +6 -5
- package/src/AppShell.test.tsx +314 -0
- package/src/AppShell.tsx +384 -63
- package/src/Authorized.test.tsx +63 -1
- package/src/Authorized.tsx +35 -2
- package/src/Field.test.tsx +30 -0
- package/src/Field.tsx +36 -8
- package/src/FormPage.tsx +54 -0
- package/src/LoginView.tsx +35 -75
- package/src/ModuleNav.test.tsx +26 -0
- package/src/ModuleNav.tsx +45 -38
- package/src/Page.test.tsx +9 -6
- package/src/Page.tsx +37 -39
- package/src/ProfileView.test.tsx +15 -0
- package/src/ProfileView.tsx +9 -36
- package/src/ResourceList.tsx +13 -24
- 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 -16
- package/src/admin/GroupCreate.tsx +18 -4
- package/src/admin/GroupDetail.tsx +50 -15
- package/src/admin/GroupsAdmin.tsx +13 -5
- package/src/admin/UserCreate.tsx +41 -12
- package/src/admin/UserDetail.tsx +4 -1
- package/src/admin/UsersAdmin.tsx +14 -6
- package/src/admin/admin.test.tsx +238 -3
- 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/download.test.tsx +153 -0
- package/src/download.tsx +132 -0
- package/src/files.tsx +2 -11
- package/src/format.test.tsx +213 -0
- package/src/format.ts +150 -0
- package/src/icons.tsx +67 -5
- package/src/index.ts +63 -7
- 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 +3 -0
- package/src/markers.test.ts +141 -15
- 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/routeSearch.ts +73 -0
- package/src/routeTypes.ts +50 -6
- package/src/router.test.tsx +766 -3
- package/src/router.tsx +277 -28
- package/src/sso.test.tsx +6 -3
- package/src/styles.test.ts +518 -27
- package/src/styles.ts +1287 -66
- 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 +192 -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 +71 -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 +49 -0
- package/src/ui/Combobox.tsx +8 -2
- package/src/ui/DatePicker.tsx +28 -5
- 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.tsx +16 -6
- package/src/ui/Tooltip.test.tsx +56 -1
- package/src/ui/Tooltip.tsx +69 -6
- package/src/uiText.tsx +9 -0
- package/src/unwrap.test.ts +132 -0
- package/src/unwrap.ts +118 -32
package/src/styles.ts
CHANGED
|
@@ -13,12 +13,18 @@
|
|
|
13
13
|
* anything. A new escalation is therefore a claim that some element still styles
|
|
14
14
|
* itself inline on the same property — state it, with the file, or do not add it.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* THE LEDGER IS EMPTY. No module declares a module-scope base style object any more,
|
|
17
|
+
* and the unmarked-surface worklist is empty with it — both gated in markers.test.ts,
|
|
18
|
+
* both kept rather than deleted, for the reason the escalation ledger below is kept:
|
|
19
|
+
* an empty gate is where the next entry has to justify itself.
|
|
20
|
+
*
|
|
21
|
+
* And the measure is now the whole surface rather than the annotated part of it. The
|
|
22
|
+
* ledger counts module-scope `CSSProperties` declarations, which a call-site literal and
|
|
23
|
+
* an unannotated style object both slip past — that is how the built-in admin views kept
|
|
24
|
+
* five base styles through the entire migration with both ratchets reading clean. A third
|
|
25
|
+
* gate counts inline style SITES per file, so the only way out of it is to render none;
|
|
26
|
+
* the nine that remain are ADR 0094 §3's permanent inline side and nothing else, named
|
|
27
|
+
* one by one in markers.test.ts.
|
|
22
28
|
*
|
|
23
29
|
* A migrated component gets its base here and renders no `style={}` for it —
|
|
24
30
|
* though it may still pass an inline value the sheet has no business owning, which
|
|
@@ -68,11 +74,37 @@
|
|
|
68
74
|
* is what lets an app override any framework rule without `!important` — the
|
|
69
75
|
* restyling this phase exists to enable.
|
|
70
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
|
+
*
|
|
71
101
|
* The injector is idempotent, SSR-safe (guarded on `document`), and appends
|
|
72
102
|
* the rules through `textContent` — never `innerHTML` — so no HTML sink is
|
|
73
103
|
* touched.
|
|
74
104
|
*/
|
|
75
105
|
|
|
106
|
+
import { WIDE_VIEWPORT_QUERY } from "./breakpoints";
|
|
107
|
+
|
|
76
108
|
/** The `<style>` element id used to detect a prior injection. */
|
|
77
109
|
export const TERP_STYLES_ID = "terp-core-styles";
|
|
78
110
|
|
|
@@ -113,6 +145,37 @@ export const TERP_STYLES_CSS = `
|
|
|
113
145
|
depend on that at all: a custom property declared on an ancestor is inherited
|
|
114
146
|
rather than cascaded against, so a DataView stamping the attribute on itself
|
|
115
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"],
|
|
116
179
|
[data-density="compact"] {
|
|
117
180
|
--density-control-min-height: var(--density-compact-control-min-height);
|
|
118
181
|
--density-cell-pad-y: var(--density-compact-cell-pad-y);
|
|
@@ -152,9 +215,26 @@ body {
|
|
|
152
215
|
they match every scrollable element globally). Paired with the color-scheme
|
|
153
216
|
declaration on the token roots so any native chrome we do not restyle here
|
|
154
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. */
|
|
155
234
|
html {
|
|
156
235
|
scrollbar-width: thin;
|
|
157
236
|
scrollbar-color: var(--color-neutral-300) transparent;
|
|
237
|
+
scrollbar-gutter: stable;
|
|
158
238
|
}
|
|
159
239
|
::-webkit-scrollbar {
|
|
160
240
|
width: 10px;
|
|
@@ -198,8 +278,12 @@ html {
|
|
|
198
278
|
font-size: var(--font-size-sm);
|
|
199
279
|
font-weight: var(--font-weight-medium);
|
|
200
280
|
line-height: 1.2;
|
|
201
|
-
transition:
|
|
202
|
-
|
|
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);
|
|
203
287
|
}
|
|
204
288
|
[data-terp="button"][data-variant="primary"] {
|
|
205
289
|
background: var(--color-brand-primary);
|
|
@@ -219,6 +303,33 @@ html {
|
|
|
219
303
|
background: transparent;
|
|
220
304
|
color: var(--color-neutral-700);
|
|
221
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
|
+
}
|
|
222
333
|
[data-terp="button-icon"] {
|
|
223
334
|
display: inline-flex;
|
|
224
335
|
align-items: center;
|
|
@@ -331,7 +442,9 @@ html {
|
|
|
331
442
|
font-size: var(--font-size-sm);
|
|
332
443
|
font-weight: var(--font-weight-normal);
|
|
333
444
|
line-height: 1.25;
|
|
334
|
-
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);
|
|
335
448
|
}
|
|
336
449
|
input[data-terp="input"] {
|
|
337
450
|
min-height: var(--density-control-min-height);
|
|
@@ -368,6 +481,14 @@ textarea[data-terp="input"] {
|
|
|
368
481
|
flex-direction: column;
|
|
369
482
|
gap: var(--space-2);
|
|
370
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;
|
|
371
492
|
}
|
|
372
493
|
[data-terp="stack"][data-direction="row"] { flex-direction: row; }
|
|
373
494
|
[data-terp="stack"][data-gap="0"] { gap: var(--space-0); }
|
|
@@ -378,6 +499,116 @@ textarea[data-terp="input"] {
|
|
|
378
499
|
[data-terp="stack"][data-gap="6"] { gap: var(--space-6); }
|
|
379
500
|
[data-terp="stack"][data-gap="8"] { gap: var(--space-8); }
|
|
380
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); }
|
|
381
612
|
|
|
382
613
|
/* Detail lists ------------------------------------------------------------- */
|
|
383
614
|
/* The term and value are inline boxes inside a block row, which is what makes
|
|
@@ -386,14 +617,66 @@ textarea[data-terp="input"] {
|
|
|
386
617
|
margin: 0;
|
|
387
618
|
display: grid;
|
|
388
619
|
gap: var(--space-1);
|
|
620
|
+
grid-template-columns: minmax(0, 1fr);
|
|
621
|
+
}
|
|
622
|
+
[data-terp="detail-list-row"] {
|
|
623
|
+
min-width: 0;
|
|
389
624
|
}
|
|
390
625
|
[data-terp="detail-list-term"] {
|
|
391
626
|
display: inline;
|
|
392
627
|
font-weight: var(--font-weight-medium);
|
|
393
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
|
+
}
|
|
394
636
|
[data-terp="detail-list-value"] {
|
|
395
637
|
display: inline;
|
|
396
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);
|
|
397
680
|
}
|
|
398
681
|
|
|
399
682
|
/* Checkboxes / radios / switches ------------------------------------------- */
|
|
@@ -422,7 +705,7 @@ textarea[data-terp="input"] {
|
|
|
422
705
|
block-size: 1.25rem;
|
|
423
706
|
accent-color: var(--color-fg-accent);
|
|
424
707
|
cursor: pointer;
|
|
425
|
-
transition: background-color
|
|
708
|
+
transition: background-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
426
709
|
}
|
|
427
710
|
[data-terp="radio-group"] {
|
|
428
711
|
display: grid;
|
|
@@ -471,6 +754,141 @@ textarea[data-terp="input"] {
|
|
|
471
754
|
[data-terp="card-heading"] {
|
|
472
755
|
min-width: 0;
|
|
473
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
|
+
|
|
474
892
|
[data-terp="card-actions"] {
|
|
475
893
|
flex-shrink: 0;
|
|
476
894
|
}
|
|
@@ -513,12 +931,44 @@ textarea[data-terp="input"] {
|
|
|
513
931
|
margin-block-end: -1px;
|
|
514
932
|
border-top-left-radius: var(--radius-sm);
|
|
515
933
|
border-top-right-radius: var(--radius-sm);
|
|
516
|
-
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);
|
|
517
938
|
}
|
|
518
939
|
[data-terp="tab-panel"] {
|
|
519
940
|
color: var(--color-neutral-900);
|
|
520
941
|
}
|
|
521
942
|
|
|
943
|
+
/* Module navigation -------------------------------------------------------- */
|
|
944
|
+
/* Secondary tabs for a module's sub-pages, and very nearly the same object as tab
|
|
945
|
+
above: a transparent 2px edge the active item colours, muted ink the active item
|
|
946
|
+
darkens. The difference is that these are router links rather than buttons, and
|
|
947
|
+
that difference decides where the active state is keyed — see terp.state.
|
|
948
|
+
|
|
949
|
+
The edges are logical (border-block-end) to match tab and appshell-footer rather
|
|
950
|
+
than the physical borderBottom the component declared. Identical in every writing
|
|
951
|
+
mode this framework ships, and the sheet already had one convention. */
|
|
952
|
+
[data-terp="module-nav"] {
|
|
953
|
+
border-block-end: 1px solid var(--color-neutral-200);
|
|
954
|
+
}
|
|
955
|
+
[data-terp="module-nav-list"] {
|
|
956
|
+
list-style: none;
|
|
957
|
+
margin: 0;
|
|
958
|
+
padding: 0;
|
|
959
|
+
display: flex;
|
|
960
|
+
flex-wrap: wrap;
|
|
961
|
+
gap: var(--space-3);
|
|
962
|
+
}
|
|
963
|
+
[data-terp="module-nav-link"] {
|
|
964
|
+
display: inline-flex;
|
|
965
|
+
align-items: center;
|
|
966
|
+
padding: var(--space-2) 0;
|
|
967
|
+
color: var(--color-neutral-600);
|
|
968
|
+
text-decoration: none;
|
|
969
|
+
border-block-end: 2px solid transparent;
|
|
970
|
+
}
|
|
971
|
+
|
|
522
972
|
/* Breadcrumbs -------------------------------------------------------------- */
|
|
523
973
|
/* The trail owns its whole subtree, so the list and its items are addressed
|
|
524
974
|
structurally and the current crumb by the aria-current it already carries —
|
|
@@ -549,7 +999,7 @@ textarea[data-terp="input"] {
|
|
|
549
999
|
[data-terp="breadcrumbs"] a {
|
|
550
1000
|
color: var(--color-neutral-600);
|
|
551
1001
|
text-decoration: none;
|
|
552
|
-
transition: color
|
|
1002
|
+
transition: color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
553
1003
|
}
|
|
554
1004
|
[data-terp="breadcrumbs-separator"] {
|
|
555
1005
|
display: inline-flex;
|
|
@@ -572,7 +1022,29 @@ textarea[data-terp="input"] {
|
|
|
572
1022
|
shell's hover rule has keyed on it since before this migration.
|
|
573
1023
|
|
|
574
1024
|
That is why nothing here needs a style object handed across a public boundary, which
|
|
575
|
-
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. */
|
|
576
1048
|
[data-terp="appshell"] {
|
|
577
1049
|
display: flex;
|
|
578
1050
|
align-items: stretch;
|
|
@@ -597,13 +1069,13 @@ textarea[data-terp="input"] {
|
|
|
597
1069
|
top: 0;
|
|
598
1070
|
height: 100vh;
|
|
599
1071
|
overflow-x: hidden;
|
|
600
|
-
width:
|
|
601
|
-
background: var(--color-
|
|
602
|
-
border-inline-end: 1px solid var(--color-
|
|
603
|
-
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);
|
|
604
1076
|
}
|
|
605
1077
|
[data-terp="appshell-sidebar"][data-collapsed="true"] {
|
|
606
|
-
width:
|
|
1078
|
+
width: var(--shell-sidebar-width-collapsed);
|
|
607
1079
|
}
|
|
608
1080
|
/* The mobile drawer, reached from the shell root's variant rather than from an attribute
|
|
609
1081
|
of its own — the viewport is one fact and the root owns it. 100dvh rather than 100vh so
|
|
@@ -636,16 +1108,51 @@ textarea[data-terp="input"] {
|
|
|
636
1108
|
gap: var(--space-2);
|
|
637
1109
|
padding: var(--space-1) var(--space-2);
|
|
638
1110
|
min-height: 2.25rem;
|
|
639
|
-
color: var(--color-
|
|
1111
|
+
color: var(--color-sidebar-fg);
|
|
640
1112
|
text-decoration: none;
|
|
641
1113
|
border-radius: var(--radius-md);
|
|
642
1114
|
box-sizing: border-box;
|
|
643
|
-
transition: background-color
|
|
1115
|
+
transition: background-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
644
1116
|
}
|
|
645
1117
|
[data-terp="appshell-sidebar"][data-collapsed="true"] [data-terp="appshell-brand"] {
|
|
646
1118
|
justify-content: center;
|
|
647
1119
|
padding-inline: 0;
|
|
648
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
|
+
}
|
|
649
1156
|
[data-terp="appshell-brand-row"] {
|
|
650
1157
|
display: flex;
|
|
651
1158
|
align-items: center;
|
|
@@ -661,7 +1168,7 @@ textarea[data-terp="input"] {
|
|
|
661
1168
|
white-space: nowrap;
|
|
662
1169
|
font-size: var(--font-size-base);
|
|
663
1170
|
font-weight: var(--font-weight-semibold);
|
|
664
|
-
color: var(--color-
|
|
1171
|
+
color: var(--color-sidebar-fg);
|
|
665
1172
|
letter-spacing: 0;
|
|
666
1173
|
}
|
|
667
1174
|
[data-terp="appshell-nav"] {
|
|
@@ -676,6 +1183,47 @@ textarea[data-terp="input"] {
|
|
|
676
1183
|
display: grid;
|
|
677
1184
|
gap: var(--space-1);
|
|
678
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
|
+
}
|
|
679
1227
|
/* Sidebar navigation links, and this is the rule the whole shell migration was for. The
|
|
680
1228
|
geometry used to be NAV_LINK_STYLE, a CSSProperties object exported from AppShell for
|
|
681
1229
|
every router's link renderer to spread — so an app could not restyle a nav link at all
|
|
@@ -692,7 +1240,7 @@ textarea[data-terp="input"] {
|
|
|
692
1240
|
gap: var(--space-2);
|
|
693
1241
|
padding: var(--space-2) var(--space-3);
|
|
694
1242
|
border-radius: var(--radius-md);
|
|
695
|
-
color: var(--color-
|
|
1243
|
+
color: var(--color-sidebar-muted);
|
|
696
1244
|
font-size: var(--font-size-sm);
|
|
697
1245
|
font-weight: var(--font-weight-medium);
|
|
698
1246
|
text-decoration: none;
|
|
@@ -700,7 +1248,9 @@ textarea[data-terp="input"] {
|
|
|
700
1248
|
overflow: hidden;
|
|
701
1249
|
box-sizing: border-box;
|
|
702
1250
|
min-height: 2.25rem;
|
|
703
|
-
transition:
|
|
1251
|
+
transition:
|
|
1252
|
+
background-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
1253
|
+
color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
704
1254
|
}
|
|
705
1255
|
/* The collapsed rail's link geometry: one centred fixed-size icon in the content track.
|
|
706
1256
|
(0,3,1) against the base's (0,1,1), so it wins on specificity with no source-order
|
|
@@ -716,16 +1266,20 @@ textarea[data-terp="input"] {
|
|
|
716
1266
|
text-overflow: ellipsis;
|
|
717
1267
|
white-space: nowrap;
|
|
718
1268
|
}
|
|
719
|
-
/* 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
|
|
720
1272
|
must stay focusable and so cannot be display: none. The other two are the brand title
|
|
721
1273
|
and the nav labels in the icon rail, which were a style-object TERNARY before this —
|
|
722
1274
|
the component picked between two objects per render, and the collapsed branch was
|
|
723
1275
|
painted by nothing, because the rail state was internal and no specimen could reach it.
|
|
724
1276
|
That is what defaultCollapsed is for. */
|
|
1277
|
+
[data-terp="appshell-skip-link"],
|
|
725
1278
|
[data-terp="drawer-focus-start"],
|
|
726
1279
|
[data-terp="drawer-focus-end"],
|
|
727
1280
|
[data-terp="appshell-sidebar"][data-collapsed="true"] [data-terp="appshell-brand-title"],
|
|
728
|
-
[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"] {
|
|
729
1283
|
position: absolute;
|
|
730
1284
|
width: 1px;
|
|
731
1285
|
height: 1px;
|
|
@@ -752,7 +1306,7 @@ textarea[data-terp="input"] {
|
|
|
752
1306
|
justify-content: space-between;
|
|
753
1307
|
gap: var(--space-3);
|
|
754
1308
|
padding: var(--space-2) var(--space-4);
|
|
755
|
-
min-height:
|
|
1309
|
+
min-height: var(--shell-header-height);
|
|
756
1310
|
box-sizing: border-box;
|
|
757
1311
|
background: var(--color-neutral-0);
|
|
758
1312
|
border-block-end: 1px solid var(--color-neutral-200);
|
|
@@ -791,6 +1345,19 @@ textarea[data-terp="input"] {
|
|
|
791
1345
|
font-weight: var(--font-weight-normal);
|
|
792
1346
|
line-height: 1.25;
|
|
793
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
|
+
}
|
|
794
1361
|
[data-terp="appshell-main"] {
|
|
795
1362
|
flex-grow: 1;
|
|
796
1363
|
padding: var(--space-6);
|
|
@@ -805,6 +1372,402 @@ textarea[data-terp="input"] {
|
|
|
805
1372
|
color: var(--color-fg-subtle);
|
|
806
1373
|
font-size: var(--font-size-xs);
|
|
807
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
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/* The page frame ----------------------------------------------------------- */
|
|
1443
|
+
/* Every routed view is this shape: one header carrying the breadcrumb trail (when
|
|
1444
|
+
there is a path back up) and the title row, then the body. HubPage, OverviewPage
|
|
1445
|
+
and DetailPage are all this frame with a different trail.
|
|
1446
|
+
|
|
1447
|
+
The header is a <header> ELEMENT and Page has to keep it one, which is a
|
|
1448
|
+
constraint this sheet cannot express and the component records at the site: the
|
|
1449
|
+
layout contract's runtime slot check reads article.children and drops the header
|
|
1450
|
+
by TAG NAME, so re-rendering it as a marked <div> would put it back into the body
|
|
1451
|
+
set and fail every governed OverviewPage and DetailPage closed. Marking it is
|
|
1452
|
+
additive; retagging it is not. The body likewise takes no wrapper — not even a
|
|
1453
|
+
display: contents one, since that check is a DOM traversal and would see the node
|
|
1454
|
+
whether or not it generates a box.
|
|
1455
|
+
|
|
1456
|
+
align-content: start is what keeps the rows at the top of a page taller than its
|
|
1457
|
+
content — the loading and error frames, where the body is one small block. With
|
|
1458
|
+
the default the two rows would spread to fill the height. It needs something to
|
|
1459
|
+
stretch the article before it is observable at all, which is why page-loading and
|
|
1460
|
+
page-error render inside a grid box rather than a plain tall div. */
|
|
1461
|
+
[data-terp="page"] {
|
|
1462
|
+
display: grid;
|
|
1463
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1464
|
+
gap: var(--space-4);
|
|
1465
|
+
align-content: start;
|
|
1466
|
+
min-width: 0;
|
|
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
|
+
}
|
|
1482
|
+
[data-terp="page-header"] {
|
|
1483
|
+
display: grid;
|
|
1484
|
+
gap: var(--space-2);
|
|
1485
|
+
}
|
|
1486
|
+
/* The crumb row keeps a 2rem floor, and it is doing work rather than reserving
|
|
1487
|
+
space for its own sake: the trail is shorter than 2rem at font-size-sm, so
|
|
1488
|
+
dropping the floor closes the gap under the trail on every page that has one.
|
|
1489
|
+
Measured — removing it moves all six baselines with a trail and nothing else. */
|
|
1490
|
+
[data-terp="page-breadcrumbs"] {
|
|
1491
|
+
display: flex;
|
|
1492
|
+
align-items: center;
|
|
1493
|
+
min-height: 2rem;
|
|
1494
|
+
}
|
|
1495
|
+
/* Title left, the actions slot right, wrapping rather than overflowing when a long
|
|
1496
|
+
title meets a wide action cluster. */
|
|
1497
|
+
[data-terp="page-heading"] {
|
|
1498
|
+
display: flex;
|
|
1499
|
+
align-items: center;
|
|
1500
|
+
justify-content: space-between;
|
|
1501
|
+
gap: var(--space-3);
|
|
1502
|
+
flex-wrap: wrap;
|
|
1503
|
+
}
|
|
1504
|
+
/* The single h1 of the view. margin: 0 is load-bearing — the browser default h1
|
|
1505
|
+
margin would otherwise fight the header's own gap. */
|
|
1506
|
+
[data-terp="page-title"] {
|
|
1507
|
+
margin: 0;
|
|
1508
|
+
font-size: var(--font-size-lg);
|
|
1509
|
+
font-weight: var(--font-weight-semibold);
|
|
1510
|
+
letter-spacing: 0;
|
|
1511
|
+
color: var(--color-neutral-900);
|
|
1512
|
+
line-height: 1.3;
|
|
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
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/* The sign-in screen ------------------------------------------------------- */
|
|
1604
|
+
/* The one screen an unauthenticated user sees, and the only full-viewport page in
|
|
1605
|
+
the package: a 100vh grid centring one card. The reset layer's box-sizing note
|
|
1606
|
+
already names this page as the reason it exists — content-box plus 100vh plus
|
|
1607
|
+
padding overflows the viewport by exactly the padding, which is a phantom
|
|
1608
|
+
scrollbar on a page that fits.
|
|
1609
|
+
|
|
1610
|
+
The buttons fill their group, and that is a rule on the GROUP rather than a prop
|
|
1611
|
+
on Button, because it has exactly one consumer in the package. Button declares
|
|
1612
|
+
width: fit-content, which is a definite width — so grid stretch does NOT do this
|
|
1613
|
+
for free, and dropping the declaration shrinks all four buttons to their labels.
|
|
1614
|
+
Two selectors at (0,2,0) against the button's own (0,1,0), same layer, so
|
|
1615
|
+
specificity settles it and source order never enters into it. A block prop on
|
|
1616
|
+
Button would be a new public API minted for one internal caller; the sheet can
|
|
1617
|
+
already reach the thing, which is the test stage 4 set when it deleted Menu's
|
|
1618
|
+
style props.
|
|
1619
|
+
|
|
1620
|
+
The separator's ink moves from --color-neutral-500 to --color-fg-subtle, which is
|
|
1621
|
+
the rest of the migration ec36a2b started rather than a new decision: the two
|
|
1622
|
+
tokens are byte-identical in light and dark, and only fg-subtle has a declared
|
|
1623
|
+
pairing (subtle-on-surface) for the gate to measure. So the screenshot themes do
|
|
1624
|
+
not move and midnight and twilight get the value the gate has been measuring all
|
|
1625
|
+
along. The "or" is aria-hidden but it is visible text, so it is held to AA rather
|
|
1626
|
+
than treated as an ornament.
|
|
1627
|
+
|
|
1628
|
+
The error line has no specimen and cannot have one: the error is internal state set
|
|
1629
|
+
only in a catch, and sso.error needs a real failed callback, which needs a URL the
|
|
1630
|
+
lane owns. Its ink is gated statically instead — danger-on-card, declared for this
|
|
1631
|
+
surface and measured in all five themes. */
|
|
1632
|
+
[data-terp="login-view"] {
|
|
1633
|
+
min-height: 100vh;
|
|
1634
|
+
display: grid;
|
|
1635
|
+
place-items: center;
|
|
1636
|
+
padding: var(--space-6);
|
|
1637
|
+
background: var(--color-neutral-50);
|
|
1638
|
+
font-family: var(--font-family-sans);
|
|
1639
|
+
color: var(--color-neutral-900);
|
|
1640
|
+
}
|
|
1641
|
+
[data-terp="login-card"] {
|
|
1642
|
+
width: 100%;
|
|
1643
|
+
max-width: 24rem;
|
|
1644
|
+
display: grid;
|
|
1645
|
+
gap: var(--space-4);
|
|
1646
|
+
padding: var(--space-6);
|
|
1647
|
+
background: var(--color-neutral-0);
|
|
1648
|
+
border: 1px solid var(--color-neutral-200);
|
|
1649
|
+
border-radius: var(--radius-lg);
|
|
1650
|
+
box-shadow: var(--shadow-md);
|
|
1651
|
+
}
|
|
1652
|
+
[data-terp="login-brand"] {
|
|
1653
|
+
display: flex;
|
|
1654
|
+
align-items: center;
|
|
1655
|
+
gap: var(--space-2);
|
|
1656
|
+
color: var(--color-neutral-900);
|
|
1657
|
+
}
|
|
1658
|
+
[data-terp="login-title"] {
|
|
1659
|
+
margin: 0;
|
|
1660
|
+
font-size: var(--font-size-xl);
|
|
1661
|
+
font-weight: var(--font-weight-bold);
|
|
1662
|
+
letter-spacing: 0;
|
|
1663
|
+
}
|
|
1664
|
+
[data-terp="login-form"],
|
|
1665
|
+
[data-terp="login-sso"] {
|
|
1666
|
+
display: grid;
|
|
1667
|
+
gap: var(--space-3);
|
|
1668
|
+
}
|
|
1669
|
+
[data-terp="login-separator"] {
|
|
1670
|
+
display: flex;
|
|
1671
|
+
align-items: center;
|
|
1672
|
+
gap: var(--space-2);
|
|
1673
|
+
color: var(--color-fg-subtle);
|
|
1674
|
+
font-size: var(--font-size-xs);
|
|
1675
|
+
text-transform: uppercase;
|
|
1676
|
+
letter-spacing: 0.06em;
|
|
1677
|
+
}
|
|
1678
|
+
[data-terp="login-separator-rule"] {
|
|
1679
|
+
flex: 1;
|
|
1680
|
+
border-block-start: 1px solid var(--color-neutral-200);
|
|
1681
|
+
}
|
|
1682
|
+
[data-terp="login-error"] {
|
|
1683
|
+
margin: 0;
|
|
1684
|
+
color: var(--color-status-danger);
|
|
1685
|
+
font-size: var(--font-size-sm);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/* The profile screen ------------------------------------------------------- */
|
|
1689
|
+
/* The built-in /profile view: two cards, an avatar tile, and the identity lines.
|
|
1690
|
+
Its cards are its own rather than the Card component's — the same declarations on
|
|
1691
|
+
a different element — and folding the two together is a component decision, not a
|
|
1692
|
+
styling one, so it stays a follow-up rather than riding in on a migration whose
|
|
1693
|
+
whole contract is zero pixel movement.
|
|
1694
|
+
|
|
1695
|
+
overflow-wrap on the address is real and unobservable: the workbench session is a
|
|
1696
|
+
fixed user whose address is short, so no specimen can paint the case it exists for
|
|
1697
|
+
(an address has no spaces to break at, so a long one widens the card past its own
|
|
1698
|
+
max-width instead of wrapping). Asserted in the unit test as the marker it keys on;
|
|
1699
|
+
there is no picture of it and cannot be until the mock session is variable. */
|
|
1700
|
+
[data-terp="profile-card"] {
|
|
1701
|
+
display: grid;
|
|
1702
|
+
gap: var(--space-4);
|
|
1703
|
+
padding: var(--space-4);
|
|
1704
|
+
max-width: 32rem;
|
|
1705
|
+
background: var(--color-neutral-0);
|
|
1706
|
+
border: 1px solid var(--color-neutral-200);
|
|
1707
|
+
border-radius: var(--radius-lg);
|
|
1708
|
+
}
|
|
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"] {
|
|
1723
|
+
display: inline-flex;
|
|
1724
|
+
align-items: center;
|
|
1725
|
+
justify-content: center;
|
|
1726
|
+
width: 3.5rem;
|
|
1727
|
+
height: 3.5rem;
|
|
1728
|
+
flex-shrink: 0;
|
|
1729
|
+
border-radius: var(--radius-full);
|
|
1730
|
+
background: var(--color-brand-primary);
|
|
1731
|
+
color: var(--color-brand-primary-contrast);
|
|
1732
|
+
font-size: var(--font-size-lg);
|
|
1733
|
+
font-weight: var(--font-weight-medium);
|
|
1734
|
+
}
|
|
1735
|
+
[data-terp="avatar"][data-size="sm"] {
|
|
1736
|
+
width: 2rem;
|
|
1737
|
+
height: 2rem;
|
|
1738
|
+
font-size: var(--font-size-sm);
|
|
1739
|
+
}
|
|
1740
|
+
[data-terp="profile-email"] {
|
|
1741
|
+
overflow-wrap: anywhere;
|
|
1742
|
+
}
|
|
1743
|
+
[data-terp="profile-role"] {
|
|
1744
|
+
margin: 0;
|
|
1745
|
+
color: var(--color-neutral-600);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/* The built-in admin screens ----------------------------------------------- */
|
|
1749
|
+
/* The packaged /admin views. Three surfaces, and they were invisible to both
|
|
1750
|
+
ratchets for the entire migration — which is why they are here rather than in
|
|
1751
|
+
0.8.0. The worklist names files with NO marker at all, and every admin view
|
|
1752
|
+
rendered none, so it read as a view composition and was excluded on purpose. The
|
|
1753
|
+
ledger counts module-scope CSSProperties declarations, and these were four
|
|
1754
|
+
call-site literals plus one unannotated object. Neither gate was wrong; both were
|
|
1755
|
+
narrower than they looked, and the same commit widens the measure.
|
|
1756
|
+
|
|
1757
|
+
The form box is ONE marker across two files, because UserCreate and GroupCreate
|
|
1758
|
+
constrain their form to the same measure — the same surface twice, not two
|
|
1759
|
+
surfaces that happen to agree today. */
|
|
1760
|
+
[data-terp="admin-form"] {
|
|
1761
|
+
max-width: 32rem;
|
|
1762
|
+
}
|
|
1763
|
+
/* A section heading inside a detail screen: the members list, the permission
|
|
1764
|
+
grants. font-size-base rather than the UA default, which for an h2 is LARGER than
|
|
1765
|
+
the page's own h1 at font-size-lg — so without this a section outranks the view
|
|
1766
|
+
it sits in. */
|
|
1767
|
+
[data-terp="admin-section-title"] {
|
|
1768
|
+
margin: 0;
|
|
1769
|
+
font-size: var(--font-size-base);
|
|
1770
|
+
}
|
|
808
1771
|
|
|
809
1772
|
/* Hub cards --------------------------------------------------------------- */
|
|
810
1773
|
/* This whole family was in terp.state, resting declarations and all, for the same
|
|
@@ -872,7 +1835,9 @@ textarea[data-terp="input"] {
|
|
|
872
1835
|
[data-terp="hubcard"] {
|
|
873
1836
|
height: 100%;
|
|
874
1837
|
min-height: 0;
|
|
875
|
-
transition:
|
|
1838
|
+
transition:
|
|
1839
|
+
box-shadow var(--motion-duration-fast) var(--motion-easing-standard),
|
|
1840
|
+
transform var(--motion-duration-fast) var(--motion-easing-standard);
|
|
876
1841
|
}
|
|
877
1842
|
[data-terp="hubcard-body"] {
|
|
878
1843
|
display: grid;
|
|
@@ -886,7 +1851,7 @@ textarea[data-terp="input"] {
|
|
|
886
1851
|
background: var(--color-neutral-0);
|
|
887
1852
|
color: var(--color-neutral-900);
|
|
888
1853
|
box-sizing: border-box;
|
|
889
|
-
transition: border-color
|
|
1854
|
+
transition: border-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
890
1855
|
}
|
|
891
1856
|
/* -heading, not -title: in this sheet a heading is the BOX holding a title
|
|
892
1857
|
(card-heading, dataview-card-heading) and a title is the text box itself
|
|
@@ -916,7 +1881,7 @@ textarea[data-terp="input"] {
|
|
|
916
1881
|
color: var(--color-neutral-900);
|
|
917
1882
|
font-size: var(--font-size-base);
|
|
918
1883
|
font-weight: var(--font-weight-semibold);
|
|
919
|
-
transition: color
|
|
1884
|
+
transition: color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
920
1885
|
}
|
|
921
1886
|
/* neutral-600 rather than fg-muted, and it is not the tinted-surface case: this text
|
|
922
1887
|
sits on the card's own neutral-0 and measures 7.58 / 7.94 / 7.50 / 7.60 / 18.42. */
|
|
@@ -1154,6 +2119,46 @@ textarea[data-terp="input"] {
|
|
|
1154
2119
|
cursor: pointer;
|
|
1155
2120
|
color: var(--color-fg-subtle);
|
|
1156
2121
|
}
|
|
2122
|
+
/* The password field's reveal toggle, built on the search box above rather than beside
|
|
2123
|
+
it: same positioning context, same absolutely-placed control, same specificity
|
|
2124
|
+
argument. Only type="password" wraps, so every other input is still a bare element
|
|
2125
|
+
and the two child selectors in this sheet that reach for data-terp="input" — the
|
|
2126
|
+
toolbar search and the resource-list create field — can never meet a wrapper. */
|
|
2127
|
+
[data-terp="input-password"] {
|
|
2128
|
+
position: relative;
|
|
2129
|
+
display: inline-flex;
|
|
2130
|
+
align-items: center;
|
|
2131
|
+
}
|
|
2132
|
+
/* Room for the toggle. It must out-rank input[data-terp="input"] { padding: 0
|
|
2133
|
+
var(--space-3) } and does so on SPECIFICITY — two attributes (0,2,0) against an
|
|
2134
|
+
attribute plus a type (0,1,1) — the same trap and the same escape the search field
|
|
2135
|
+
documents above. Asymmetric on purpose: the glyph sits at the end, and reserving room
|
|
2136
|
+
at both ends would indent the value for nothing. */
|
|
2137
|
+
[data-terp="input-password"] > [data-terp="input"] {
|
|
2138
|
+
padding-inline-end: var(--space-6);
|
|
2139
|
+
width: 100%;
|
|
2140
|
+
}
|
|
2141
|
+
/* The toggle itself, the seventeenth element wearing the iconbutton marker. It takes
|
|
2142
|
+
that marker rather than one of its own because it is one: the shared rule already
|
|
2143
|
+
carries its transition, its hover wash and its disabled treatment, and duplicating
|
|
2144
|
+
those under a new name to avoid editing one enumeration would be the wrong trade. */
|
|
2145
|
+
/* Edge ships its own reveal control inside every password field, so without this the user gets
|
|
2146
|
+
two: the native eye sitting on top of ours, in a box sized for one glyph. Same class of fix as
|
|
2147
|
+
the number stepper this sheet already suppresses -- an unthemeable browser affordance the
|
|
2148
|
+
framework replaces rather than competes with. */
|
|
2149
|
+
input[data-terp="input"][type="password"]::-ms-reveal {
|
|
2150
|
+
display: none;
|
|
2151
|
+
}
|
|
2152
|
+
[data-terp="input-password"] > [data-terp="iconbutton"] {
|
|
2153
|
+
position: absolute;
|
|
2154
|
+
inset-inline-end: var(--space-1);
|
|
2155
|
+
display: inline-flex;
|
|
2156
|
+
padding: var(--space-1);
|
|
2157
|
+
background: transparent;
|
|
2158
|
+
border: none;
|
|
2159
|
+
cursor: pointer;
|
|
2160
|
+
color: var(--color-fg-subtle);
|
|
2161
|
+
}
|
|
1157
2162
|
/* "Refreshing…", and this is the one place the prefer-an-existing-DOM-attribute
|
|
1158
2163
|
rule is REFUSED with its own reasoning. [data-terp="dataview-toolbar"]
|
|
1159
2164
|
> [role="status"] looks textbook — the component does own this span's role — but
|
|
@@ -1222,6 +2227,28 @@ textarea[data-terp="input"] {
|
|
|
1222
2227
|
white-space: nowrap;
|
|
1223
2228
|
background: var(--color-neutral-0);
|
|
1224
2229
|
}
|
|
2230
|
+
/* A column's declared track. A MINIMUM rather than a width, because a specified width is only a
|
|
2231
|
+
preference under table-layout: auto and the algorithm shrinks it to fit — which is why the pixel
|
|
2232
|
+
hint this replaces did nothing at all, measured in the workbench at three columns asking for
|
|
2233
|
+
700px each and fitting the box exactly. What auto layout cannot take away is a minimum.
|
|
2234
|
+
|
|
2235
|
+
Three steps, and no more: these are the three bands the framework's own tables declare, and a
|
|
2236
|
+
step is additive to add and breaking to remove. In rem, so a declared track follows the root
|
|
2237
|
+
font size; the system columns below keep their pixels on purpose, being chrome rather than
|
|
2238
|
+
content, and converting them is a density pass with its own baselines.
|
|
2239
|
+
|
|
2240
|
+
Nothing here ever meets an inline width. A resized column stops emitting the attribute, so the
|
|
2241
|
+
user's own drag replaces the declared track instead of losing to it — the minimum would win the
|
|
2242
|
+
cascade, and a column springing back from a drag reads as a broken resizer. */
|
|
2243
|
+
[data-terp="dataview-table"] > thead > tr > th[data-width="xs"] {
|
|
2244
|
+
min-inline-size: 5rem;
|
|
2245
|
+
}
|
|
2246
|
+
[data-terp="dataview-table"] > thead > tr > th[data-width="sm"] {
|
|
2247
|
+
min-inline-size: 6.5rem;
|
|
2248
|
+
}
|
|
2249
|
+
[data-terp="dataview-table"] > thead > tr > th[data-width="md"] {
|
|
2250
|
+
min-inline-size: 9.5rem;
|
|
2251
|
+
}
|
|
1225
2252
|
[data-terp="dataview-row"] > td {
|
|
1226
2253
|
padding: var(--density-cell-pad-y) var(--density-cell-pad-x);
|
|
1227
2254
|
border-bottom: 1px solid var(--color-neutral-100);
|
|
@@ -1613,6 +2640,58 @@ th[data-terp="dataview-actions-cell"] > span {
|
|
|
1613
2640
|
color: var(--color-neutral-700);
|
|
1614
2641
|
}
|
|
1615
2642
|
|
|
2643
|
+
/* Resource list ------------------------------------------------------------ */
|
|
2644
|
+
/* The plain listing screen: a write-gated create row, then the rows. The DataView is
|
|
2645
|
+
the same job at scale, and this one stays deliberately simple, so its rules are
|
|
2646
|
+
geometry plus two inks.
|
|
2647
|
+
|
|
2648
|
+
The create row keeps its flex layout and the input keeps flex: 1 as a RULE rather
|
|
2649
|
+
than becoming a 1fr auto grid, and that is a move rather than a decision. With
|
|
2650
|
+
flex: 1 the input's basis is 0 so it may shrink below its intrinsic width; a 1fr
|
|
2651
|
+
track floors at min-content instead. The two agree at this list's 40rem cap, so
|
|
2652
|
+
swapping them would have changed nothing anybody could see until some narrower
|
|
2653
|
+
container found the difference — which is the kind of diff this migration exists
|
|
2654
|
+
not to introduce.
|
|
2655
|
+
|
|
2656
|
+
Neither paragraph resets its margin, and that is verbatim rather than an
|
|
2657
|
+
oversight: both are <p> elements whose default margin is what separates them from
|
|
2658
|
+
the form above and the rows below. Adding margin: 0 here would move the rows. */
|
|
2659
|
+
[data-terp="resource-list"] {
|
|
2660
|
+
display: grid;
|
|
2661
|
+
gap: var(--space-4);
|
|
2662
|
+
max-width: 40rem;
|
|
2663
|
+
}
|
|
2664
|
+
[data-terp="resource-list-create"] {
|
|
2665
|
+
display: flex;
|
|
2666
|
+
gap: var(--space-2);
|
|
2667
|
+
}
|
|
2668
|
+
[data-terp="resource-list-create"] > [data-terp="input"] {
|
|
2669
|
+
flex: 1;
|
|
2670
|
+
}
|
|
2671
|
+
[data-terp="resource-list-error"] {
|
|
2672
|
+
color: var(--color-status-danger);
|
|
2673
|
+
}
|
|
2674
|
+
[data-terp="resource-list-empty"] {
|
|
2675
|
+
color: var(--color-neutral-600);
|
|
2676
|
+
}
|
|
2677
|
+
[data-terp="resource-list-items"] {
|
|
2678
|
+
list-style: none;
|
|
2679
|
+
margin: 0;
|
|
2680
|
+
padding: 0;
|
|
2681
|
+
display: grid;
|
|
2682
|
+
gap: var(--space-2);
|
|
2683
|
+
}
|
|
2684
|
+
[data-terp="resource-list-row"] {
|
|
2685
|
+
display: flex;
|
|
2686
|
+
align-items: center;
|
|
2687
|
+
justify-content: space-between;
|
|
2688
|
+
gap: var(--space-3);
|
|
2689
|
+
padding: var(--space-3);
|
|
2690
|
+
border: 1px solid var(--color-neutral-200);
|
|
2691
|
+
border-radius: var(--radius-md);
|
|
2692
|
+
background: var(--color-neutral-0);
|
|
2693
|
+
}
|
|
2694
|
+
|
|
1616
2695
|
/* Empty / error / loading states ------------------------------------------- */
|
|
1617
2696
|
/* Same centred block, opposite messages: empty is a dashed outline on the page
|
|
1618
2697
|
surface because nothing is wrong, error is a filled danger wash because
|
|
@@ -1939,7 +3018,10 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
1939
3018
|
font-weight: var(--font-weight-medium);
|
|
1940
3019
|
line-height: 1.4;
|
|
1941
3020
|
box-shadow: var(--shadow-md);
|
|
1942
|
-
pointer-events: none
|
|
3021
|
+
/* No pointer-events: none. It was here, and it makes WCAG 1.4.13's Hoverable clause
|
|
3022
|
+
impossible by construction: a bubble the pointer cannot reach is a bubble nobody
|
|
3023
|
+
tracking with a pointer, or reading under magnification, can finish reading. The
|
|
3024
|
+
component keeps it open across the gap with a short close delay instead. */
|
|
1943
3025
|
white-space: normal;
|
|
1944
3026
|
}
|
|
1945
3027
|
|
|
@@ -1964,7 +3046,10 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
1964
3046
|
font-size: var(--font-size-sm);
|
|
1965
3047
|
font-weight: var(--font-weight-normal);
|
|
1966
3048
|
line-height: 1.25;
|
|
1967
|
-
transition:
|
|
3049
|
+
transition:
|
|
3050
|
+
background-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
3051
|
+
color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
3052
|
+
box-shadow var(--motion-duration-fast) var(--motion-easing-standard);
|
|
1968
3053
|
}
|
|
1969
3054
|
/* The panel's contents. This sits INSIDE popover-panel, which supplies the
|
|
1970
3055
|
surface — so the menu owns only the stacking of its items. */
|
|
@@ -1989,7 +3074,9 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
1989
3074
|
font-size: var(--font-size-sm);
|
|
1990
3075
|
font-weight: var(--font-weight-normal);
|
|
1991
3076
|
line-height: 1.25;
|
|
1992
|
-
transition:
|
|
3077
|
+
transition:
|
|
3078
|
+
background-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
3079
|
+
color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
1993
3080
|
}
|
|
1994
3081
|
/* Destructive is the one enumerable choice an item has, so it is an attribute.
|
|
1995
3082
|
The disabled treatment is a state rule keyed on :disabled, because the element
|
|
@@ -2042,7 +3129,12 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2042
3129
|
width: 100%;
|
|
2043
3130
|
padding: var(--space-2);
|
|
2044
3131
|
text-align: left;
|
|
2045
|
-
|
|
3132
|
+
/* The sidebar family, not the neutral one. This row renders inside the sidebar (and inside the
|
|
3133
|
+
header group under navPlacement="header", which takes the sidebar surface), so its ink and
|
|
3134
|
+
that background are a pairing in play — and the contrast gate can only measure a pairing it
|
|
3135
|
+
can name. Provably zero-diff: --color-neutral-900 and --color-sidebar-fg are byte-equal in
|
|
3136
|
+
all five themes. */
|
|
3137
|
+
color: var(--color-sidebar-fg);
|
|
2046
3138
|
border-color: transparent;
|
|
2047
3139
|
min-height: 0;
|
|
2048
3140
|
}
|
|
@@ -2052,19 +3144,6 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2052
3144
|
gap: 0;
|
|
2053
3145
|
padding: 0;
|
|
2054
3146
|
}
|
|
2055
|
-
[data-terp="user-menu-avatar"] {
|
|
2056
|
-
display: inline-flex;
|
|
2057
|
-
align-items: center;
|
|
2058
|
-
justify-content: center;
|
|
2059
|
-
width: 2rem;
|
|
2060
|
-
height: 2rem;
|
|
2061
|
-
flex-shrink: 0;
|
|
2062
|
-
border-radius: var(--radius-full);
|
|
2063
|
-
background: var(--color-brand-primary);
|
|
2064
|
-
color: var(--color-brand-primary-contrast);
|
|
2065
|
-
font-size: var(--font-size-sm);
|
|
2066
|
-
font-weight: var(--font-weight-medium);
|
|
2067
|
-
}
|
|
2068
3147
|
[data-terp="user-menu-identity"] {
|
|
2069
3148
|
display: grid;
|
|
2070
3149
|
min-width: 0;
|
|
@@ -2075,9 +3154,17 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2075
3154
|
text-overflow: ellipsis;
|
|
2076
3155
|
white-space: nowrap;
|
|
2077
3156
|
}
|
|
3157
|
+
/* Two surfaces, one marker. UserMenu renders this span in the trigger AND in the portalled
|
|
3158
|
+
panel, and only the first sits on the sidebar — the panel is in document.body, where the
|
|
3159
|
+
sidebar palette does not apply. So the sidebar copy is scoped (the portal puts the panel
|
|
3160
|
+
outside this selector by construction) and the panel keeps the neutral. The sheet already
|
|
3161
|
+
argues this exact split for the drawer close button. */
|
|
2078
3162
|
[data-terp="user-menu-role"] {
|
|
2079
3163
|
color: var(--color-neutral-600);
|
|
2080
3164
|
}
|
|
3165
|
+
[data-terp="user-menu"] [data-terp="user-menu-role"] {
|
|
3166
|
+
color: var(--color-sidebar-muted);
|
|
3167
|
+
}
|
|
2081
3168
|
/* The panel's identity block, and the panel's own geometry — both keyed on the
|
|
2082
3169
|
owner, because the portal put them outside every selector that could otherwise
|
|
2083
3170
|
reach them. */
|
|
@@ -2119,8 +3206,14 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2119
3206
|
rules ([data-terp="markdown"] p, ... ul), and a declaration added here would
|
|
2120
3207
|
simply do nothing with nothing to say so. And display: contents does not change
|
|
2121
3208
|
selector matching, only box generation: a parent's > * child selector now matches
|
|
2122
|
-
this wrapper rather than the blocks.
|
|
2123
|
-
|
|
3209
|
+
this wrapper rather than the blocks.
|
|
3210
|
+
|
|
3211
|
+
That last sentence used to end "Nothing in this sheet uses one, which is why the
|
|
3212
|
+
wrapper is free today", and it stopped being true when the measured content width
|
|
3213
|
+
shipped: that rule is a child-star selector on the page's body children. It matched it,
|
|
3214
|
+
found no box to give a width to, and let prose run full-bleed in a measured shell.
|
|
3215
|
+
The reach-through beside that rule is the fix; this wrapper is free of everything
|
|
3216
|
+
else.
|
|
2124
3217
|
|
|
2125
3218
|
Under SSR the sheet is not injected at all (the injector is document-guarded), so
|
|
2126
3219
|
a server-rendered page has this element as a block box until hydration. That is
|
|
@@ -2253,12 +3346,15 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2253
3346
|
margin-block-start: var(--space-2);
|
|
2254
3347
|
}
|
|
2255
3348
|
|
|
2256
|
-
/* Icon-only buttons.
|
|
3349
|
+
/* Icon-only buttons. Seventeen elements wear this marker and not one declares a
|
|
2257
3350
|
transition inline, so this belongs in terp.base — it sat in terp.state only
|
|
2258
3351
|
because that is where the hover rules needing it live. Same correction 817f572
|
|
2259
3352
|
made for Tabs and Breadcrumbs. */
|
|
2260
3353
|
[data-terp="iconbutton"] {
|
|
2261
|
-
transition:
|
|
3354
|
+
transition:
|
|
3355
|
+
background-color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
3356
|
+
color var(--motion-duration-fast) var(--motion-easing-standard),
|
|
3357
|
+
box-shadow var(--motion-duration-fast) var(--motion-easing-standard);
|
|
2262
3358
|
}
|
|
2263
3359
|
|
|
2264
3360
|
/* Popover ------------------------------------------------------------------ */
|
|
@@ -2292,9 +3388,12 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2292
3388
|
|
|
2293
3389
|
The stacking level is the token that was published for it. Every component in
|
|
2294
3390
|
the package hardcoded its own number while a full --z-index-* family sat
|
|
2295
|
-
unread.
|
|
2296
|
-
|
|
2297
|
-
--z-index-toast.
|
|
3391
|
+
unread. That is finished: AppShell reads --z-index-drawer, --z-index-backdrop
|
|
3392
|
+
and --z-index-sticky, the skip link reads --z-index-skip-link, and the toast
|
|
3393
|
+
viewport reads --z-index-toast. (This paragraph said "AppShell still writes
|
|
3394
|
+
50/40/30 ... and comes right with its own migration" for a release after that
|
|
3395
|
+
migration landed, which is the shape of stale comment worth naming: it read as
|
|
3396
|
+
a known gap rather than as a finished one.) Tooltip's z-index:
|
|
2298
3397
|
1 above is deliberately NOT a token — the tooltip is absolutely positioned
|
|
2299
3398
|
inside its own anchor, so 1 is a local lift within a stacking context rather
|
|
2300
3399
|
than a place in the app-wide order. */
|
|
@@ -2391,12 +3490,24 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2391
3490
|
opacity: 0.55;
|
|
2392
3491
|
cursor: not-allowed;
|
|
2393
3492
|
}
|
|
3493
|
+
/* A loading button IS disabled — the component sets both — so this has to sit here rather
|
|
3494
|
+
than in terp.base beside the other attribute-keyed button rules. In terp.base it would
|
|
3495
|
+
lose to the :disabled rule above on layer order and the cursor would silently stay
|
|
3496
|
+
not-allowed, which reads as "you may not" where the truth is "not yet". Declared after
|
|
3497
|
+
it so source order settles the tie the equal (0,2,0) specificity leaves.
|
|
3498
|
+
|
|
3499
|
+
Neither cursor is visible to any lane: Playwright's screenshots do not paint a pointer.
|
|
3500
|
+
The computed lane asserts both. */
|
|
3501
|
+
[data-terp="button"][data-loading="true"] {
|
|
3502
|
+
cursor: progress;
|
|
3503
|
+
}
|
|
2394
3504
|
|
|
2395
3505
|
/* Icon-only buttons: the shell's two header toggles, four pagination arrows, the
|
|
2396
3506
|
toast dismisser, the combobox's clear button, the calendar's two month arrows,
|
|
2397
|
-
the DataView's expand toggle, the view-options panel's two reorder arrows,
|
|
2398
|
-
|
|
2399
|
-
|
|
3507
|
+
the DataView's expand toggle, the view-options panel's two reorder arrows, the
|
|
3508
|
+
DataView toolbar's clear-search button and two layout toggles, and the password
|
|
3509
|
+
field's reveal toggle.
|
|
3510
|
+
SEVENTEEN SITES sharing a transition and nothing else — no shared SURFACE, because
|
|
2400
3511
|
each is styled by where it sits. Sites rather than elements: the reorder arrows
|
|
2401
3512
|
render twice per column row, so the element count is a function of how many
|
|
2402
3513
|
columns a view has, while the list of places to check is fixed.
|
|
@@ -2440,7 +3551,7 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2440
3551
|
|
|
2441
3552
|
The :disabled cursor no longer shouts, and re-deriving that is more useful than
|
|
2442
3553
|
trusting it. The question is never "has anything migrated" but "can any element
|
|
2443
|
-
this selector matches still beat it" — so: which of the
|
|
3554
|
+
this selector matches still beat it" — so: which of the seventeen can carry the
|
|
2444
3555
|
disabled attribute at all? The shell's toggles cannot (no disabled prop). The
|
|
2445
3556
|
toast dismisser cannot. The combobox's clear button renders only while the
|
|
2446
3557
|
field is enabled and takes no disabled of its own. The calendar's arrows page
|
|
@@ -2452,7 +3563,8 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2452
3563
|
background: var(--color-neutral-100);
|
|
2453
3564
|
color: var(--color-neutral-900);
|
|
2454
3565
|
}
|
|
2455
|
-
[data-terp="iconbutton"]:disabled
|
|
3566
|
+
[data-terp="iconbutton"]:disabled,
|
|
3567
|
+
[data-terp="iconbutton"][aria-disabled="true"] {
|
|
2456
3568
|
opacity: 0.4;
|
|
2457
3569
|
cursor: not-allowed;
|
|
2458
3570
|
}
|
|
@@ -2466,7 +3578,13 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2466
3578
|
only the text controls had migrated left a disabled Combobox painted exactly
|
|
2467
3579
|
like an enabled one and deleted the aria-invalid border outright. All six
|
|
2468
3580
|
now take their base from this sheet, so layer order is enough. */
|
|
2469
|
-
|
|
3581
|
+
/* The invalid exclusion is not tidying. This selector weighs (0,4,0) and the danger border below
|
|
3582
|
+
weighs (0,2,0), both unlayered against each other inside terp.state — so without the third
|
|
3583
|
+
:not() a pointer resting on a field that has just failed validation repaints its border from
|
|
3584
|
+
the danger token to a neutral grey, and the error state disappears for exactly as long as the
|
|
3585
|
+
user is pointing at the thing they need to fix. Narrowing the aggressor rather than adding a
|
|
3586
|
+
competing [aria-invalid="true"]:hover rule is this sheet's convention. */
|
|
3587
|
+
[data-terp="input"]:hover:not(:disabled):not(:focus):not([aria-invalid="true"]) {
|
|
2470
3588
|
border-color: var(--color-neutral-400);
|
|
2471
3589
|
}
|
|
2472
3590
|
[data-terp="input"]:focus,
|
|
@@ -2540,8 +3658,8 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2540
3658
|
The rail's scrollbar suppression moved from an attribute on the nav to the sidebar's,
|
|
2541
3659
|
because collapsed is one fact and it now has one owner. */
|
|
2542
3660
|
[data-terp="appshell-nav"] a:hover:not([aria-current="page"]) {
|
|
2543
|
-
background: var(--color-
|
|
2544
|
-
color: var(--color-
|
|
3661
|
+
background: var(--color-sidebar-accent);
|
|
3662
|
+
color: var(--color-sidebar-fg);
|
|
2545
3663
|
}
|
|
2546
3664
|
[data-terp="appshell-nav"] a[aria-current="page"] {
|
|
2547
3665
|
background: var(--color-brand-primary-soft);
|
|
@@ -2552,12 +3670,80 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2552
3670
|
overflow-x: hidden;
|
|
2553
3671
|
scrollbar-width: none;
|
|
2554
3672
|
}
|
|
3673
|
+
/* The rail's group separation. The label is visually hidden here (it joins the block above), so
|
|
3674
|
+
without this the groups are a single undifferentiated column of icons and the structure the
|
|
3675
|
+
expanded sidebar shows simply disappears at 4rem. A rule the LINE has to carry, because the
|
|
3676
|
+
label cannot: the divider is what is left of the label once the text is gone.
|
|
3677
|
+
|
|
3678
|
+
It replaces rather than adds to the expanded margin — same specificity family, one attribute
|
|
3679
|
+
more — so the rail does not pay 1rem per group in a column that is already scrolling. */
|
|
3680
|
+
[data-terp="appshell-sidebar"][data-collapsed="true"] [data-terp="appshell-nav-group"] + [data-terp="appshell-nav-group"] {
|
|
3681
|
+
margin-block-start: var(--space-2);
|
|
3682
|
+
padding-block-start: var(--space-2);
|
|
3683
|
+
border-block-start: 1px solid var(--color-sidebar-border);
|
|
3684
|
+
}
|
|
2555
3685
|
[data-terp="appshell-sidebar"][data-collapsed="true"] [data-terp="appshell-nav"]::-webkit-scrollbar {
|
|
2556
3686
|
width: 0;
|
|
2557
3687
|
height: 0;
|
|
2558
3688
|
}
|
|
2559
3689
|
[data-terp="appshell-brand"]:hover {
|
|
2560
|
-
background: var(--color-
|
|
3690
|
+
background: var(--color-sidebar-accent);
|
|
3691
|
+
}
|
|
3692
|
+
/* And its hover, for the same reason: the shared iconbutton hover wash is a neutral, which is
|
|
3693
|
+
the header toggle's context and not this one's. */
|
|
3694
|
+
[data-terp="appshell-brand-row"] > [data-terp="iconbutton"]:hover {
|
|
3695
|
+
background: var(--color-sidebar-accent);
|
|
3696
|
+
color: var(--color-sidebar-fg);
|
|
3697
|
+
}
|
|
3698
|
+
/* The skip link, visible only while focused.
|
|
3699
|
+
In terp.state, and that is not filing: the resting half is the shared visually-hidden block
|
|
3700
|
+
in terp.base, which sets position, a 1px box and clip, and un-hiding has to beat all of it.
|
|
3701
|
+
On specificity it would not — a selector list takes the specificity of the member that
|
|
3702
|
+
MATCHES, and for this element that member is [data-terp="appshell-skip-link"] at (0,1,0),
|
|
3703
|
+
the same weight as this rule. (An earlier version of this comment cited the list's (0,3,0)
|
|
3704
|
+
member, which is the collapsed-rail selector and never matches a skip link; that reading
|
|
3705
|
+
would have made the rules a source-order coin flip rather than a layer decision.) Layer
|
|
3706
|
+
order settles it with nothing to reason about.
|
|
3707
|
+
:focus-visible rather than :focus, matching the sheet's shared ring: a skip link reached by
|
|
3708
|
+
pointer is a link nobody asked to see.
|
|
3709
|
+
Above the sticky header (30) and its backdrop (40) so it is not painted under the chrome it
|
|
3710
|
+
sits over; below the drawer (50) because nothing should paint over an open modal. Stacking
|
|
3711
|
+
order does NOT keep it out of the drawer's focus trap and this comment used to say it did —
|
|
3712
|
+
z-index has no bearing on tab order. The link is simply not RENDERED while the drawer is
|
|
3713
|
+
open; see AppShell. */
|
|
3714
|
+
/* The skip link's target takes focus and must NOT paint the shared ring.
|
|
3715
|
+
The main element carries a data-terp marker and now a tabIndex of -1, which together put it
|
|
3716
|
+
in scope of the shared [data-terp]:focus-visible ring — so activating the skip link outlined
|
|
3717
|
+
the entire content column, header to footer, plus a 3px halo. Measured: it matches
|
|
3718
|
+
:focus-visible, with a 2px solid outline and rgba(37,99,235,0.35) 0 0 0 3px. The ring exists
|
|
3719
|
+
to say which CONTROL will take the next keystroke; a scroll target that was focused
|
|
3720
|
+
programmatically is not one, and the visible result of following a skip link should be the
|
|
3721
|
+
content rather than a box drawn around it.
|
|
3722
|
+
Scoped to this marker rather than to tabindex=-1 in general: other elements take -1 for other
|
|
3723
|
+
reasons and some of them are controls. */
|
|
3724
|
+
[data-terp="appshell-main"]:focus-visible {
|
|
3725
|
+
outline: none;
|
|
3726
|
+
box-shadow: none;
|
|
3727
|
+
}
|
|
3728
|
+
[data-terp="appshell-skip-link"]:focus-visible {
|
|
3729
|
+
position: fixed;
|
|
3730
|
+
top: var(--space-2);
|
|
3731
|
+
inset-inline-start: var(--space-2);
|
|
3732
|
+
z-index: var(--z-index-skip-link);
|
|
3733
|
+
width: auto;
|
|
3734
|
+
height: auto;
|
|
3735
|
+
margin: 0;
|
|
3736
|
+
padding: var(--space-2) var(--space-3);
|
|
3737
|
+
clip: auto;
|
|
3738
|
+
overflow: visible;
|
|
3739
|
+
background: var(--color-neutral-0);
|
|
3740
|
+
color: var(--color-fg-accent);
|
|
3741
|
+
border: var(--border-width-thin) solid var(--color-fg-accent);
|
|
3742
|
+
border-radius: var(--radius-md);
|
|
3743
|
+
font-family: var(--font-family-sans);
|
|
3744
|
+
font-size: var(--font-size-sm);
|
|
3745
|
+
font-weight: var(--font-weight-medium);
|
|
3746
|
+
text-decoration: none;
|
|
2561
3747
|
}
|
|
2562
3748
|
|
|
2563
3749
|
/* Tabs -------------------------------------------------------------------- */
|
|
@@ -2578,6 +3764,35 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2578
3764
|
cursor: not-allowed;
|
|
2579
3765
|
}
|
|
2580
3766
|
|
|
3767
|
+
/* Module navigation ------------------------------------------------------- */
|
|
3768
|
+
/* Which sub-page you are on, here for the same reason the selected tab is: the
|
|
3769
|
+
accent edge and the darker ink have to beat the resting pair.
|
|
3770
|
+
|
|
3771
|
+
Keyed on data-active, which ModuleNav writes, and deliberately NOT on the
|
|
3772
|
+
aria-current the same element also carries. That attribute has a second author:
|
|
3773
|
+
TanStack's link props spread the router's own active props LAST, after the
|
|
3774
|
+
caller's, so on a Link the router has the final word on aria-current. This is
|
|
3775
|
+
the breadcrumb lesson in its exact form — reuse a semantic only where the
|
|
3776
|
+
component is its sole author.
|
|
3777
|
+
|
|
3778
|
+
The two notions of "active" are not the same predicate, and they diverge in BOTH
|
|
3779
|
+
directions, which is worth knowing before touching either. activeOptions
|
|
3780
|
+
.includeSearch defaults to true, so the router additionally demands an exact
|
|
3781
|
+
query-string match that ModuleNav does not — the router is narrower there. And
|
|
3782
|
+
with exact matching the router compares through exactPathTest, which is
|
|
3783
|
+
removeTrailingSlash(a) === removeTrailingSlash(b), while ModuleNav compares
|
|
3784
|
+
pathname === item.to raw — so on a path with a trailing slash the ROUTER is
|
|
3785
|
+
active and ModuleNav is not, and this rule withholds the accent edge from a tab
|
|
3786
|
+
the router considers current. That second case is a real defect and it is older
|
|
3787
|
+
than this rule: the inline styling it replaced read the same isActive, so the
|
|
3788
|
+
behaviour is unchanged and only the reasoning was wrong. Fixing the predicate
|
|
3789
|
+
belongs with the navigation model, because that is what decides what "active"
|
|
3790
|
+
should mean. */
|
|
3791
|
+
[data-terp="module-nav-link"][data-active="true"] {
|
|
3792
|
+
color: var(--color-neutral-900);
|
|
3793
|
+
border-block-end-color: var(--color-fg-accent);
|
|
3794
|
+
}
|
|
3795
|
+
|
|
2581
3796
|
/* Hub cards --------------------------------------------------------------- */
|
|
2582
3797
|
/* The hover edge recolours hubcard-BODY, not the card.
|
|
2583
3798
|
|
|
@@ -2648,13 +3863,14 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2648
3863
|
}
|
|
2649
3864
|
|
|
2650
3865
|
/* The pager's disabled ink, scoped rather than added to the shared iconbutton
|
|
2651
|
-
rule above: of the
|
|
3866
|
+
rule above: of the seventeen sites wearing that marker only six can be disabled at
|
|
2652
3867
|
all — these four and the view-options panel's two reorder arrows, which carry
|
|
2653
3868
|
their own scoped ink below for the same reason — and giving the shared rule a
|
|
2654
3869
|
colour would change how a disabled calendar arrow looks the day one becomes
|
|
2655
3870
|
disableable. The shared rule supplies the opacity and the cursor; this supplies
|
|
2656
3871
|
the ink the pager had inline. */
|
|
2657
|
-
[data-terp="dataview-pager"] > [data-terp="iconbutton"]:disabled
|
|
3872
|
+
[data-terp="dataview-pager"] > [data-terp="iconbutton"]:disabled,
|
|
3873
|
+
[data-terp="dataview-pager"] > [data-terp="iconbutton"][aria-disabled="true"] {
|
|
2658
3874
|
color: var(--color-neutral-300);
|
|
2659
3875
|
}
|
|
2660
3876
|
|
|
@@ -2684,7 +3900,7 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2684
3900
|
table-layout: fixed;
|
|
2685
3901
|
}
|
|
2686
3902
|
[data-terp="dataview-table"] tbody tr {
|
|
2687
|
-
transition: background-color
|
|
3903
|
+
transition: background-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
2688
3904
|
}
|
|
2689
3905
|
[data-terp="dataview-table"] tbody tr:hover td {
|
|
2690
3906
|
background: var(--color-neutral-50);
|
|
@@ -2711,8 +3927,13 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2711
3927
|
is enough, and the card's tone rules lose to it on layer rather than on
|
|
2712
3928
|
specificity. Keeping the escalation past this point is the quiet failure: nothing
|
|
2713
3929
|
would render differently, the declaration would simply become unthemeable. */
|
|
3930
|
+
/* Both halves carry the data-clickable guard, and the card half did not. A card list stamps
|
|
3931
|
+
data-clickable only when onRowClick is set, but renders the selection checkbox on
|
|
3932
|
+
selectionEnabled alone — so in a selectable-but-not-clickable list, focusing a checkbox
|
|
3933
|
+
washed the whole card in brand-soft and buried its data-tone. Focus is not selection, and
|
|
3934
|
+
a card that does nothing when clicked has no "activate me" state to advertise. */
|
|
2714
3935
|
[data-terp="dataview-row"][data-clickable="true"]:focus-within td,
|
|
2715
|
-
[data-terp="dataview-card"]:focus-within {
|
|
3936
|
+
[data-terp="dataview-card"][data-clickable="true"]:focus-within {
|
|
2716
3937
|
background: var(--color-brand-primary-soft);
|
|
2717
3938
|
}
|
|
2718
3939
|
|