@terpjs/react-core 0.13.1 → 0.15.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 +4 -4
- package/package.json +2 -2
- package/src/AppShell.tsx +9 -1
- package/src/Breadcrumbs.test.tsx +30 -0
- package/src/Breadcrumbs.tsx +34 -6
- package/src/HubPage.test.tsx +14 -3
- package/src/Page.test.tsx +80 -12
- package/src/Page.tsx +89 -18
- package/src/layout.test.tsx +36 -0
- package/src/layout.tsx +39 -0
- package/src/markers.test.ts +2 -1
- package/src/styles.test.ts +350 -3
- package/src/styles.ts +398 -68
- package/src/tokens.guard.test.ts +53 -13
- package/src/ui/Card.test.tsx +39 -1
- package/src/ui/Card.tsx +17 -5
package/src/styles.ts
CHANGED
|
@@ -181,6 +181,20 @@ export const TERP_STYLES_CSS = `
|
|
|
181
181
|
--density-cell-pad-y: var(--density-compact-cell-pad-y);
|
|
182
182
|
--density-cell-pad-x: var(--density-compact-cell-pad-x);
|
|
183
183
|
}
|
|
184
|
+
/* The content column's gutter, tightened for a phone. One remap, and it replaces the two
|
|
185
|
+
base-plus-variant pairs the header and main each used to carry: everything that has to
|
|
186
|
+
start on the same vertical line now reads --shell-gutter and the variant moves the value
|
|
187
|
+
once. The band the page header renders also reads it as a NEGATIVE margin to bleed to the
|
|
188
|
+
column's edge, so a fourth and fifth copy of the same measure would have arrived with it —
|
|
189
|
+
which is what turned this from a nice-to-have into the mechanism.
|
|
190
|
+
|
|
191
|
+
UNLAYERED, beside the density remap, for the reason recorded there and not by analogy:
|
|
192
|
+
tokens.css declares --shell-gutter on :root and ships as an extracted <link> that PRECEDES
|
|
193
|
+
the injected sheet, so a layered copy of this rule would lose to it in a production build
|
|
194
|
+
while winning in dev. (0,2,0) against :root's (0,1,0) settles it here in either. */
|
|
195
|
+
[data-terp="appshell"][data-variant="mobile"] {
|
|
196
|
+
--shell-gutter: var(--space-4);
|
|
197
|
+
}
|
|
184
198
|
|
|
185
199
|
@layer terp.reset {
|
|
186
200
|
/* Document reset: the app shell owns the full canvas. Without this the
|
|
@@ -391,9 +405,12 @@ html {
|
|
|
391
405
|
border: 1px solid;
|
|
392
406
|
border-radius: var(--radius-md);
|
|
393
407
|
}
|
|
408
|
+
/* The neutral tone's wash is --color-neutral-100, the value Badge and a neutral-toned
|
|
409
|
+
row already spell for the same tone. It was --color-neutral-50, which is the canvas: on
|
|
410
|
+
a card with no fill the alert kept its border and lost its wash entirely. */
|
|
394
411
|
[data-terp="alert"][data-tone="neutral"] {
|
|
395
412
|
color: var(--color-neutral-600);
|
|
396
|
-
background: var(--color-neutral-
|
|
413
|
+
background: var(--color-neutral-100);
|
|
397
414
|
}
|
|
398
415
|
[data-terp="alert"][data-tone="info"] {
|
|
399
416
|
color: var(--color-status-info);
|
|
@@ -546,6 +563,56 @@ textarea[data-terp="input"] {
|
|
|
546
563
|
[data-terp="splitpage-panes"][data-list-width="lg"] {
|
|
547
564
|
grid-template-columns: minmax(0, 32rem) minmax(0, 1fr);
|
|
548
565
|
}
|
|
566
|
+
/* DetailList's multi-column half. Mobile-first, like SplitPage's panes above: the narrow
|
|
567
|
+
shape is one column with the term as a block above its value, which is the BASE rule and
|
|
568
|
+
therefore needs no query at all — this block is only the widths at which a shared label
|
|
569
|
+
column, or two pairs per row, fit.
|
|
570
|
+
|
|
571
|
+
They ran at every width before, and 430px is where that showed: four tracks in ~370px, and
|
|
572
|
+
detail-list-value's overflow-wrap: anywhere then broke words mid-token rather than letting
|
|
573
|
+
a value have its own line. The anywhere is right — it is what stops an unbreakable digest
|
|
574
|
+
overflowing its column — so the fix is to stop asking a phone to hold four tracks.
|
|
575
|
+
|
|
576
|
+
Why a viewport query and not a container query, which would be the more nearly right
|
|
577
|
+
instrument: container queries appear nowhere in this sheet, so introducing one for a single
|
|
578
|
+
component is a mechanism change that wants its own record rather than a line in a defect
|
|
579
|
+
fix, and the cutover the framework already has covers the width the failure was measured
|
|
580
|
+
at. It is also why DetailList reflows itself while Grid deliberately refuses to for a fixed
|
|
581
|
+
column count — Grid publishes columns="auto" as its responsive answer, and DetailList's
|
|
582
|
+
closed one-or-two has no such escape, so the reflow has to be the component's own.
|
|
583
|
+
|
|
584
|
+
PLACEMENT: the detail-list base rules are declared ~100 lines BELOW this block, so source
|
|
585
|
+
order cannot settle these. Specificity does, and deliberately: every selector here carries
|
|
586
|
+
the marker plus at least one attribute (0,2,0) against the base rule's (0,1,0). That is
|
|
587
|
+
exactly how splitpage-panes above already wins over its own base rule further down, and
|
|
588
|
+
styles.test.ts pins the property, because a reader cannot see it from the rule. The one
|
|
589
|
+
thing this block must NOT declare is row-gap: that belongs to the gap prop, whose roll-call
|
|
590
|
+
weighs the same (0,2,0) and is therefore declared later on purpose. */
|
|
591
|
+
[data-terp="detail-list"][data-columns="2"] {
|
|
592
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
593
|
+
column-gap: var(--space-4);
|
|
594
|
+
}
|
|
595
|
+
/* minmax(0, max-content) rather than a bare auto for the label track. An auto track floors
|
|
596
|
+
at min-content, which makes it the one track in this component that was never floored at
|
|
597
|
+
zero — so a label with nothing to break on widened the column and pushed the list past its
|
|
598
|
+
container, the same failure the value track's minmax(0, 1fr) was written to stop. Capping at
|
|
599
|
+
max-content also stops the label column claiming width no label is using: measured in a
|
|
600
|
+
565px card, two independently-sized auto label tracks left a value track of ~400px holding
|
|
601
|
+
~60px of text. */
|
|
602
|
+
[data-terp="detail-list"][data-layout="aligned"] {
|
|
603
|
+
grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
|
|
604
|
+
column-gap: var(--space-3);
|
|
605
|
+
}
|
|
606
|
+
[data-terp="detail-list"][data-layout="aligned"][data-columns="2"] {
|
|
607
|
+
grid-template-columns: repeat(2, minmax(0, max-content) minmax(0, 1fr));
|
|
608
|
+
}
|
|
609
|
+
/* display: contents is what makes the dt and dd grid items of the dl itself, so labels align
|
|
610
|
+
ACROSS rows without a DOM change — and it belongs in here rather than in the base rules
|
|
611
|
+
because it is the mechanism of the shared column, which exists only above the cutover.
|
|
612
|
+
Narrow, the row wrapper stays a block and each pair reads as two lines. */
|
|
613
|
+
[data-terp="detail-list"][data-layout="aligned"] [data-terp="detail-list-row"] {
|
|
614
|
+
display: contents;
|
|
615
|
+
}
|
|
549
616
|
}
|
|
550
617
|
|
|
551
618
|
/* Grids -------------------------------------------------------------------- */
|
|
@@ -645,26 +712,22 @@ textarea[data-terp="input"] {
|
|
|
645
712
|
the mechanism anywhere in the suite. */
|
|
646
713
|
overflow-wrap: anywhere;
|
|
647
714
|
}
|
|
648
|
-
/*
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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;
|
|
715
|
+
/* The tracks for two pairs per row, and for aligned, live in the wide-viewport block above
|
|
716
|
+
rather than here: one column is the narrow shape and therefore the base. What stays here is
|
|
717
|
+
everything that holds at every width.
|
|
718
|
+
|
|
719
|
+
Both non-inline layouts open their rows to --space-3. At the base --space-1 the distance
|
|
720
|
+
WITHIN a pair and the distance BETWEEN pairs were the same 4px, so a card of five labelled
|
|
721
|
+
values had nothing grouping it — five pairs read as ten equally-spaced lines. inline keeps
|
|
722
|
+
--space-1, because there a pair IS one line of a paragraph and 4px is the leading between
|
|
723
|
+
lines of one block; opening it would space out a run of sentences.
|
|
724
|
+
|
|
725
|
+
row-gap rather than gap, and the distinction is load-bearing: the column gap in aligned is
|
|
726
|
+
the label-to-value distance, and at two pairs per row it is the space between pair groups —
|
|
727
|
+
both owned by the rules in the wide block. A shorthand here would silently reset them. */
|
|
728
|
+
[data-terp="detail-list"][data-layout="aligned"],
|
|
729
|
+
[data-terp="detail-list"][data-layout="stacked"] {
|
|
730
|
+
row-gap: var(--space-3);
|
|
668
731
|
}
|
|
669
732
|
[data-terp="detail-list"][data-layout="aligned"] [data-terp="detail-list-term"],
|
|
670
733
|
[data-terp="detail-list"][data-layout="aligned"] [data-terp="detail-list-value"],
|
|
@@ -672,12 +735,37 @@ textarea[data-terp="input"] {
|
|
|
672
735
|
[data-terp="detail-list"][data-layout="stacked"] [data-terp="detail-list-value"] {
|
|
673
736
|
display: block;
|
|
674
737
|
}
|
|
675
|
-
/*
|
|
676
|
-
|
|
738
|
+
/* The label takes the muted, smaller, regular step so a pair reads as one unit rather than two
|
|
739
|
+
lines of equal weight — and aligned shares the rule rather than getting a second treatment
|
|
740
|
+
of its own. It did not, and that was the defect: an aligned term measured 16px / weight 500 /
|
|
741
|
+
near-black, which is the value's own typography, so a card of five labelled values rendered
|
|
742
|
+
as a wall of bold text with nothing telling a reader which half to read first.
|
|
743
|
+
|
|
744
|
+
inline is deliberately NOT here. There the term is half a sentence — the colon comes from
|
|
745
|
+
the ::after above — and muting half a sentence is a different defect from the one this fixes.
|
|
746
|
+
Two layouts diverging was the bug; three converging would be another. */
|
|
747
|
+
[data-terp="detail-list"][data-layout="aligned"] [data-terp="detail-list-term"],
|
|
677
748
|
[data-terp="detail-list"][data-layout="stacked"] [data-terp="detail-list-term"] {
|
|
678
749
|
font-size: var(--font-size-xs);
|
|
750
|
+
font-weight: var(--font-weight-normal);
|
|
679
751
|
color: var(--color-fg-muted);
|
|
680
752
|
}
|
|
753
|
+
/* The gap prop, and this block must stay AFTER the layout rules above. Both
|
|
754
|
+
[data-terp="detail-list"][data-gap="3"] and [data-terp="detail-list"][data-layout="aligned"]
|
|
755
|
+
weigh (0,2,0), so nothing but source order decides which row-gap a list carrying both
|
|
756
|
+
attributes renders — backwards, the prop silently does nothing, which looks like the prop not
|
|
757
|
+
working rather than like a cascade mistake. The same tie the responsive Stack rules turn on,
|
|
758
|
+
pinned the same way in styles.test.ts.
|
|
759
|
+
|
|
760
|
+
row-gap only, for the reason the layout rules above give. The prop is documented as the
|
|
761
|
+
distance BETWEEN pairs, and the column gap stays the layout's. */
|
|
762
|
+
[data-terp="detail-list"][data-gap="0"] { row-gap: var(--space-0); }
|
|
763
|
+
[data-terp="detail-list"][data-gap="1"] { row-gap: var(--space-1); }
|
|
764
|
+
[data-terp="detail-list"][data-gap="2"] { row-gap: var(--space-2); }
|
|
765
|
+
[data-terp="detail-list"][data-gap="3"] { row-gap: var(--space-3); }
|
|
766
|
+
[data-terp="detail-list"][data-gap="4"] { row-gap: var(--space-4); }
|
|
767
|
+
[data-terp="detail-list"][data-gap="6"] { row-gap: var(--space-6); }
|
|
768
|
+
[data-terp="detail-list"][data-gap="8"] { row-gap: var(--space-8); }
|
|
681
769
|
|
|
682
770
|
/* Checkboxes / radios / switches ------------------------------------------- */
|
|
683
771
|
/* One label shape for all three, so the marker is shared: the control differs,
|
|
@@ -727,11 +815,42 @@ textarea[data-terp="input"] {
|
|
|
727
815
|
}
|
|
728
816
|
|
|
729
817
|
/* Cards -------------------------------------------------------------------- */
|
|
818
|
+
/* The surface model, and this is the rule that states it: an in-flow block declares a
|
|
819
|
+
FRAME and no fill. Border, radius and padding are the whole of a card's chrome, and
|
|
820
|
+
what shows through it is the canvas the page already paints.
|
|
821
|
+
|
|
822
|
+
It used to paint --color-neutral-0 over a --color-neutral-50 canvas — a second colour
|
|
823
|
+
saying what the border already says, and it cost twice. A card dropped onto anything
|
|
824
|
+
that is itself a surface repainted it rather than sitting on it, which is the
|
|
825
|
+
frame-inside-a-frame the plain variant exists to escape; and an app that themed the
|
|
826
|
+
canvas found that its cards did not follow, because the fill named the OTHER end of
|
|
827
|
+
the ramp. Both go away with the declaration: the plain variant is now the base rule
|
|
828
|
+
minus a border and a padding, and a themed canvas reaches every block on the page.
|
|
829
|
+
|
|
830
|
+
Six rules follow the same line, and they are the framework's other in-flow blocks:
|
|
831
|
+
hubcard-body, profile-card, resource-list-row, empty-state, dataview-card and the full
|
|
832
|
+
DataView's table frame — whose header cells carried the same fill, invisibly, since a
|
|
833
|
+
filled frame sat directly behind them. Three kinds of element deliberately do NOT. An
|
|
834
|
+
overlay has to be opaque over whatever it covers (dialog, popover-panel, toast, the
|
|
835
|
+
combobox list, and appshell-header, which the page scrolls under); a control needs a
|
|
836
|
+
surface of its own to read as a control against the page (input, select, the secondary
|
|
837
|
+
button); and login-card is the one object on an otherwise empty canvas, where the fill
|
|
838
|
+
IS the object.
|
|
839
|
+
|
|
840
|
+
The washes moved with it, because a wash that painted --color-neutral-50 was painting
|
|
841
|
+
the canvas and on a card with no fill it painted nothing: the table's hover and
|
|
842
|
+
selection, the expanded row's panel, a code block, a neutral alert and a disabled
|
|
843
|
+
control each step one place along the ramp to --color-neutral-100 — which is already
|
|
844
|
+
this sheet's hover wash for every control — or to the --color-interactive-* token that
|
|
845
|
+
names the state. One step along the ramp rather than a value per rule, because the dark
|
|
846
|
+
themes invert it: the same step is darker than the canvas in light and lighter in dark,
|
|
847
|
+
midnight and twilight, which is the direction a recess and a wash want in each.
|
|
848
|
+
--color-bg-inset cannot do that job — in those three themes it is declared AS the
|
|
849
|
+
canvas value, so an inset named from it would be the one thing that disappears. */
|
|
730
850
|
[data-terp="card"] {
|
|
731
851
|
display: flex;
|
|
732
852
|
flex-direction: column;
|
|
733
853
|
gap: var(--space-3);
|
|
734
|
-
background: var(--color-neutral-0);
|
|
735
854
|
border: 1px solid var(--color-neutral-200);
|
|
736
855
|
border-radius: var(--radius-lg);
|
|
737
856
|
padding: var(--space-4);
|
|
@@ -751,19 +870,48 @@ textarea[data-terp="input"] {
|
|
|
751
870
|
flex-wrap: wrap;
|
|
752
871
|
gap: var(--space-3);
|
|
753
872
|
}
|
|
873
|
+
/* start rather than center once there is a description, and the condition is the whole point.
|
|
874
|
+
With a title alone, center is right: the actions slot is a control, so its box is
|
|
875
|
+
--density-control-min-height tall against a single line box, and start would leave the title
|
|
876
|
+
riding above it. With two or three lines of description the same declaration floats the
|
|
877
|
+
control in the middle of the block instead of beside the title it belongs to.
|
|
878
|
+
|
|
879
|
+
:has() rather than an attribute Card could stamp, for the reason control-label's disabled
|
|
880
|
+
states use it (see the note there): the header has no idea what its heading holds, and the
|
|
881
|
+
alternative is a prop describing the DOM back to the sheet. Specificity (0,2,0) beats the
|
|
882
|
+
base rule above, so this does not depend on its position. */
|
|
883
|
+
[data-terp="card-header"]:has([data-terp="card-description"]) {
|
|
884
|
+
align-items: start;
|
|
885
|
+
}
|
|
886
|
+
/* flex: 1 1 0, and the base size is the load-bearing half. Left at the initial 0 1 auto, the
|
|
887
|
+
heading's hypothetical main size is the max-content width of a block holding a title AND a
|
|
888
|
+
sentence of description — and flex breaks lines on hypothetical main sizes BEFORE it shrinks
|
|
889
|
+
anything, so with flex-wrap above the heading claimed the whole line and the actions slot
|
|
890
|
+
wrapped underneath it. Measured: a 103px header with the button below the description, where
|
|
891
|
+
the same component with no description rendered inline at 48px. Same prop, two results,
|
|
892
|
+
depending on whether a sibling prop was set.
|
|
893
|
+
|
|
894
|
+
A base size of 0 means both items fit on one line by construction, and the heading then grows
|
|
895
|
+
into whatever the actions slot does not use. min-width: 0 stays for the other half of that
|
|
896
|
+
story: a flex item's automatic minimum size is its content's, so a long unbreakable word in a
|
|
897
|
+
title would otherwise refuse to shrink past it. */
|
|
754
898
|
[data-terp="card-heading"] {
|
|
899
|
+
flex: 1 1 0;
|
|
755
900
|
min-width: 0;
|
|
756
901
|
}
|
|
757
|
-
/* Chrome off, heading kept.
|
|
902
|
+
/* Chrome off, heading kept. Two declarations removed rather than a second component
|
|
758
903
|
with six markers of its own describing the same DOM: a titled region inside something
|
|
759
904
|
that is already a surface wants no second border, and the commonest instance is a
|
|
760
905
|
section whose body is a DataView — boxed, the table gets a border inside a border and
|
|
761
906
|
loses the full width its own scroll container gives it.
|
|
762
907
|
|
|
908
|
+
Two, and it was three: background: none went with the base rule's fill. Un-declaring
|
|
909
|
+
what nothing declares is the shape ADR 0094 exists to avoid, and here it would also
|
|
910
|
+
have been the only line in the sheet still implying a card has a colour of its own.
|
|
911
|
+
|
|
763
912
|
padding: 0 rather than dropping the declaration, because the base rule sets it and an
|
|
764
913
|
absent value inherits nothing useful. */
|
|
765
914
|
[data-terp="card"][data-variant="plain"] {
|
|
766
|
-
background: none;
|
|
767
915
|
border-color: transparent;
|
|
768
916
|
padding: 0;
|
|
769
917
|
}
|
|
@@ -837,12 +985,16 @@ textarea[data-terp="input"] {
|
|
|
837
985
|
background: var(--color-neutral-100);
|
|
838
986
|
color: var(--color-fg-default);
|
|
839
987
|
}
|
|
988
|
+
/* The same wash as inline code, and that is new: the block was --color-neutral-50 while
|
|
989
|
+
the chip above it was --color-neutral-100, so two renderings of one thing disagreed —
|
|
990
|
+
and the block's half was the canvas, which a card with no fill would have shown
|
|
991
|
+
through. One value now, for both. */
|
|
840
992
|
[data-terp="code-block"] {
|
|
841
993
|
margin: 0;
|
|
842
994
|
padding: var(--space-3);
|
|
843
995
|
border: 1px solid var(--color-neutral-200);
|
|
844
996
|
border-radius: var(--radius-md);
|
|
845
|
-
background: var(--color-neutral-
|
|
997
|
+
background: var(--color-neutral-100);
|
|
846
998
|
overflow-x: auto;
|
|
847
999
|
font-size: var(--font-size-sm);
|
|
848
1000
|
line-height: var(--font-line-height-base);
|
|
@@ -892,9 +1044,15 @@ textarea[data-terp="input"] {
|
|
|
892
1044
|
[data-terp="card-actions"] {
|
|
893
1045
|
flex-shrink: 0;
|
|
894
1046
|
}
|
|
1047
|
+
/* lg, and it is no longer "the step below the page title" — page-title moved DOWN to sm in
|
|
1048
|
+
0.14.0 when the header became a band, so a section heading is now two steps above the view's
|
|
1049
|
+
own h1. That inversion is deliberate and belongs to the band: a page title there is chrome
|
|
1050
|
+
naming where you are, not the largest thing on the page, while a card title is the heading
|
|
1051
|
+
of content you are reading. The original reasoning for lg is unchanged: at base a section
|
|
1052
|
+
heading was typographically indistinguishable from the prose underneath it. */
|
|
895
1053
|
[data-terp="card-title"] {
|
|
896
1054
|
margin: 0;
|
|
897
|
-
font-size: var(--font-size-
|
|
1055
|
+
font-size: var(--font-size-lg);
|
|
898
1056
|
font-weight: var(--font-weight-semibold);
|
|
899
1057
|
line-height: 1.3;
|
|
900
1058
|
}
|
|
@@ -1296,6 +1454,30 @@ textarea[data-terp="input"] {
|
|
|
1296
1454
|
flex-grow: 1;
|
|
1297
1455
|
min-width: 0;
|
|
1298
1456
|
}
|
|
1457
|
+
/* The header's INLINE padding is the content column's gutter, and it is a shared measure
|
|
1458
|
+
rather than a value this rule is free to pick. Four boxes have to start on the same
|
|
1459
|
+
vertical line — this header, appshell-main, appshell-footer, and the band the page header
|
|
1460
|
+
renders, which reaches the column's edge by reading the same value as a negative margin.
|
|
1461
|
+
|
|
1462
|
+
It was var(--space-4) here against var(--space-6) on main and on the footer, which put
|
|
1463
|
+
the breadcrumb trail 0.5rem right of the header's own toggle on every desktop shell and
|
|
1464
|
+
lined it up with nothing: two of the three boxes already agreed and the header was the
|
|
1465
|
+
outlier. Not a clean indent either, which is why it read as broken rather than deliberate
|
|
1466
|
+
— the toggle is a 2.25rem box centring a 1em glyph at font-size-sm, so its BOX sat 8px
|
|
1467
|
+
left of the trail while its GLYPH sat 3px right of it. On mobile it happened to be
|
|
1468
|
+
correct, because main stepped down to var(--space-4) and met the header's fixed value by
|
|
1469
|
+
accident.
|
|
1470
|
+
|
|
1471
|
+
Every reader now names --shell-gutter and the variant moves the value once, at the remap
|
|
1472
|
+
above. That is a published contract token rather than a private property because
|
|
1473
|
+
tokens.guard.test.ts refuses a fallback-less var() against anything tokens.css does not
|
|
1474
|
+
declare — and a shell measure an app may want to move is what ADR 0097 §1 says a contract
|
|
1475
|
+
token is for. The block padding is untouched and stays var(--space-2) under the
|
|
1476
|
+
min-height floor.
|
|
1477
|
+
|
|
1478
|
+
No backticks anywhere above, and that is not a style preference: one here terminates
|
|
1479
|
+
TERP_STYLES_CSS and the parse then fails somewhere else entirely with "try inserting a
|
|
1480
|
+
semicolon". This comment cost that mistake once too, which is twice in one sheet. */
|
|
1299
1481
|
[data-terp="appshell-header"] {
|
|
1300
1482
|
position: sticky;
|
|
1301
1483
|
top: 0;
|
|
@@ -1305,7 +1487,7 @@ textarea[data-terp="input"] {
|
|
|
1305
1487
|
align-items: center;
|
|
1306
1488
|
justify-content: space-between;
|
|
1307
1489
|
gap: var(--space-3);
|
|
1308
|
-
padding: var(--space-2) var(--
|
|
1490
|
+
padding: var(--space-2) var(--shell-gutter);
|
|
1309
1491
|
min-height: var(--shell-header-height);
|
|
1310
1492
|
box-sizing: border-box;
|
|
1311
1493
|
background: var(--color-neutral-0);
|
|
@@ -1358,16 +1540,26 @@ textarea[data-terp="input"] {
|
|
|
1358
1540
|
[data-terp="appshell-brand-row"] > [data-terp="iconbutton"] {
|
|
1359
1541
|
color: var(--color-sidebar-muted);
|
|
1360
1542
|
}
|
|
1543
|
+
/* Main, the footer and the page band all read the same gutter as the header — see that rule
|
|
1544
|
+
for why they are one measure. The mobile override this pair used to carry is gone into the
|
|
1545
|
+
remap: --shell-gutter is 1.5rem on desktop and 1rem on a phone, which is exactly what main's
|
|
1546
|
+
two rules computed, so main does not move and it has one owner instead of two.
|
|
1547
|
+
|
|
1548
|
+
THE FOOTER DOES MOVE, and it is the one thing here that is a change rather than a
|
|
1549
|
+
refactor. It carried var(--space-6) with no mobile override, so its inline padding stayed
|
|
1550
|
+
1.5rem at every width while main stepped down to 1rem beside it; reading the gutter now
|
|
1551
|
+
tightens it to 1rem on a phone, 8px left of where it was. That is the point of the measure
|
|
1552
|
+
rather than a casualty of it — a footer indented past the content above it was the same
|
|
1553
|
+
disagreement this token exists to end — but no baseline covers it, because the 420x900
|
|
1554
|
+
phone specimens put the footer below the fold. Stated here because it is invisible to the
|
|
1555
|
+
lanes and would otherwise read as an accident to whoever finds it. */
|
|
1361
1556
|
[data-terp="appshell-main"] {
|
|
1362
1557
|
flex-grow: 1;
|
|
1363
|
-
padding: var(--
|
|
1558
|
+
padding: var(--shell-gutter);
|
|
1364
1559
|
min-width: 0;
|
|
1365
1560
|
}
|
|
1366
|
-
[data-terp="appshell"][data-variant="mobile"] [data-terp="appshell-main"] {
|
|
1367
|
-
padding: var(--space-4);
|
|
1368
|
-
}
|
|
1369
1561
|
[data-terp="appshell-footer"] {
|
|
1370
|
-
padding: var(--space-3) var(--
|
|
1562
|
+
padding: var(--space-3) var(--shell-gutter);
|
|
1371
1563
|
border-block-start: 1px solid var(--color-neutral-200);
|
|
1372
1564
|
color: var(--color-fg-subtle);
|
|
1373
1565
|
font-size: var(--font-size-xs);
|
|
@@ -1479,38 +1671,140 @@ textarea[data-terp="input"] {
|
|
|
1479
1671
|
[data-terp="page"][data-measure="narrow"] {
|
|
1480
1672
|
max-width: 32rem;
|
|
1481
1673
|
}
|
|
1674
|
+
/* The page band (ADR 0097 §2, amended by building it).
|
|
1675
|
+
|
|
1676
|
+
ONE row, and the row IS the title. The frame used to spend two: a crumb row with a 2rem
|
|
1677
|
+
floor, then an h1 row at font-size-xl. Those two rows said the same thing twice, and the
|
|
1678
|
+
duplication was in the component rather than in the eye of a reviewer — Page built its
|
|
1679
|
+
trail as [...breadcrumbs, { label: title }] and then rendered <h1>{title}</h1>, so every
|
|
1680
|
+
DetailPage in every app printed its own name as the leaf crumb and again 24px below it.
|
|
1681
|
+
Removing one of the two was therefore the whole design question, and the answer is that
|
|
1682
|
+
the TRAIL survives: it carries the path back up, which an h1 cannot, and its leaf is a
|
|
1683
|
+
heading, which a crumb need not be. So the leaf is the h1 (see page-title below) and the
|
|
1684
|
+
band is the trail with the page's badges, its lead line and its actions on the same line.
|
|
1685
|
+
|
|
1686
|
+
xl retires from page-title with it, and that is a reversal of 4b's type-scale decision
|
|
1687
|
+
rather than a side effect. That decision was right for the shape it was made in: when the
|
|
1688
|
+
title is the page's masthead, 24 / 18 / 16 / 14 is a scale and 18 / 16 / 16 / 14 is a
|
|
1689
|
+
list. In a band the title is CHROME — it names where you are, above content that starts
|
|
1690
|
+
immediately under a border — and a 24px leaf on a trail of 14px ancestors reads as
|
|
1691
|
+
small-small-BIG rather than as one trail. It also does not fit: the band is
|
|
1692
|
+
shell-header-height with space-2 of block padding, which leaves 2rem, and xl at
|
|
1693
|
+
line-height 1.3 is 1.95rem of it before a badge or a lead line asks for room.
|
|
1694
|
+
--font-size-xl keeps two readers (heading[data-size="xl"], login-title), so the top of the
|
|
1695
|
+
scale is still wired; an app that wants the masthead back redefines the marker from its own
|
|
1696
|
+
unlayered theme.css, exactly as the retired comment here said.
|
|
1697
|
+
|
|
1698
|
+
align-items: center rather than baseline, because the row mixes a heading, pill badges and
|
|
1699
|
+
a button cluster: baseline alignment lines up the text and leaves the pills sitting low.
|
|
1700
|
+
Wrapping is kept from the old heading row for the same reason it was there — a long title
|
|
1701
|
+
meeting a wide action cluster takes a second line instead of overflowing — and the band
|
|
1702
|
+
grows past its floor when it does. */
|
|
1482
1703
|
[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
1704
|
display: flex;
|
|
1705
|
+
flex-wrap: wrap;
|
|
1492
1706
|
align-items: center;
|
|
1493
|
-
|
|
1707
|
+
justify-content: space-between;
|
|
1708
|
+
gap: var(--space-2) var(--space-4);
|
|
1709
|
+
min-width: 0;
|
|
1494
1710
|
}
|
|
1495
|
-
/*
|
|
1496
|
-
|
|
1711
|
+
/* The band's left group: the trail (whose leaf is the h1), then badges, then the lead line.
|
|
1712
|
+
flex: 1 1 auto with min-width: 0 is what lets the lead line truncate instead of pushing
|
|
1713
|
+
the action cluster off the row. */
|
|
1497
1714
|
[data-terp="page-heading"] {
|
|
1498
1715
|
display: flex;
|
|
1499
|
-
align-items: center;
|
|
1500
|
-
justify-content: space-between;
|
|
1501
|
-
gap: var(--space-3);
|
|
1502
1716
|
flex-wrap: wrap;
|
|
1717
|
+
align-items: center;
|
|
1718
|
+
gap: var(--space-2) var(--space-3);
|
|
1719
|
+
flex: 1 1 auto;
|
|
1720
|
+
min-width: 0;
|
|
1503
1721
|
}
|
|
1504
|
-
/* The single h1 of the view
|
|
1505
|
-
margin
|
|
1722
|
+
/* The single h1 of the view, and now also the trail's current crumb: one node instead of two
|
|
1723
|
+
copies of one string. margin: 0 is load-bearing twice over — the browser default h1 margin
|
|
1724
|
+
would fight the band's own gap, and an h1 inside the trail's <li> would otherwise break the
|
|
1725
|
+
row. font-size is declared rather than inherited because the UA sheet sets h1 { font-size:
|
|
1726
|
+
2em } ON the element, and a declaration on the element beats an inherited value however
|
|
1727
|
+
specific the ancestor rule is: without this the leaf renders at twice the trail.
|
|
1728
|
+
|
|
1729
|
+
Semibold at the trail's own size, which is the whole "the trail is the title" idea in one
|
|
1730
|
+
declaration — heavier than its ancestors, not larger. */
|
|
1506
1731
|
[data-terp="page-title"] {
|
|
1507
1732
|
margin: 0;
|
|
1508
|
-
font-size: var(--font-size-
|
|
1733
|
+
font-size: var(--font-size-sm);
|
|
1509
1734
|
font-weight: var(--font-weight-semibold);
|
|
1510
1735
|
letter-spacing: 0;
|
|
1511
1736
|
color: var(--color-neutral-900);
|
|
1512
1737
|
line-height: 1.3;
|
|
1513
1738
|
}
|
|
1739
|
+
/* Status pills next to the title. A row of its own so a page passing several keeps them
|
|
1740
|
+
together when the band wraps, rather than letting one pill orphan onto the second line. */
|
|
1741
|
+
[data-terp="page-badges"] {
|
|
1742
|
+
display: flex;
|
|
1743
|
+
flex-wrap: wrap;
|
|
1744
|
+
align-items: center;
|
|
1745
|
+
gap: var(--space-1);
|
|
1746
|
+
min-width: 0;
|
|
1747
|
+
}
|
|
1748
|
+
/* The lead line: one short sentence about the page, truncated rather than wrapped, because
|
|
1749
|
+
the band has a height and prose that wraps would set it. A page whose explanation does not
|
|
1750
|
+
fit on one line is describing its body, and that belongs in the body. */
|
|
1751
|
+
[data-terp="page-description"] {
|
|
1752
|
+
margin: 0;
|
|
1753
|
+
flex: 1 1 auto;
|
|
1754
|
+
min-width: 0;
|
|
1755
|
+
color: var(--color-fg-subtle);
|
|
1756
|
+
font-size: var(--font-size-sm);
|
|
1757
|
+
overflow: hidden;
|
|
1758
|
+
white-space: nowrap;
|
|
1759
|
+
text-overflow: ellipsis;
|
|
1760
|
+
}
|
|
1761
|
+
/* The band's CHROME, which needs no shell above it and so is not gated on one. Its height
|
|
1762
|
+
and its border are what make the row a band rather than a title line, and they are correct
|
|
1763
|
+
wherever Page renders — the workbench's specimen cards and the unit tests included. That
|
|
1764
|
+
split is the repair of a real contradiction: these declarations used to sit inside the
|
|
1765
|
+
shell-gated rule below while the comment claimed "standalone, the band is still a bordered
|
|
1766
|
+
row at the header's height", which the rule made false and a test fifteen lines away in
|
|
1767
|
+
styles.test.ts pinned as false. The three page-header specimens were pictures of a plain
|
|
1768
|
+
flex row that the comment described as chrome.
|
|
1769
|
+
|
|
1770
|
+
box-sizing: border-box so the floor counts the padding, matching appshell-header, whose
|
|
1771
|
+
height this is reading. Without it the two are a padding apart and the "same height as the
|
|
1772
|
+
header above it" claim is off by 1rem.
|
|
1773
|
+
|
|
1774
|
+
:not([data-measure="narrow"]) because a form is capped WITH its header (ADR 0098 §3) — a
|
|
1775
|
+
Save button a screen-width from its field is worse than one over it — so a form gets the
|
|
1776
|
+
one-row band with no chrome at all: a title row, which is what it wants.
|
|
1777
|
+
|
|
1778
|
+
padding-BLOCK only. The inline gutter belongs to the bleed below, because standalone there
|
|
1779
|
+
is nothing to bleed into and an inline pad with no negative margin would inset the band's
|
|
1780
|
+
content from the body beneath it for no reason. */
|
|
1781
|
+
[data-terp="page"]:not([data-measure="narrow"]) > [data-terp="page-header"] {
|
|
1782
|
+
padding-block: var(--space-2);
|
|
1783
|
+
min-height: var(--shell-header-height);
|
|
1784
|
+
box-sizing: border-box;
|
|
1785
|
+
border-block-end: 1px solid var(--color-neutral-200);
|
|
1786
|
+
}
|
|
1787
|
+
/* The BLEED, which does need a shell, because the negative-margin idiom is only correct when
|
|
1788
|
+
the box being escaped is appshell-main and that box pads by exactly --shell-gutter. ADR
|
|
1789
|
+
0097 §2 kept "it works with no shell above it at all" as a property of the mechanism, and
|
|
1790
|
+
keying this half on the shell is what keeps it: standalone the band is the same bordered
|
|
1791
|
+
row at the same height, it simply does not reach past its container.
|
|
1792
|
+
|
|
1793
|
+
The surface comes with the bleed rather than with the chrome, and that is deliberate under
|
|
1794
|
+
0.14.0's surface model: a block paints no fill of its own, and this one is only chrome —
|
|
1795
|
+
the app header's companion — once there is an app header above it to pair with.
|
|
1796
|
+
|
|
1797
|
+
ONE rule for both variants, which is the payoff of the token rather than a tidy-up. As two
|
|
1798
|
+
literals this needed a mobile twin, and a negative margin whose sign has to agree with a
|
|
1799
|
+
padding declared three rules away is exactly the drift that put the trail 8px off the
|
|
1800
|
+
header in the first place. */
|
|
1801
|
+
[data-terp="appshell-main"]
|
|
1802
|
+
> [data-terp="page"]:not([data-measure="narrow"])
|
|
1803
|
+
> [data-terp="page-header"] {
|
|
1804
|
+
margin: calc(-1 * var(--shell-gutter)) calc(-1 * var(--shell-gutter)) 0;
|
|
1805
|
+
padding-inline: var(--shell-gutter);
|
|
1806
|
+
background: var(--color-neutral-0);
|
|
1807
|
+
}
|
|
1514
1808
|
/* The content measure, and the subheader band, which are ONE declaration rather than two
|
|
1515
1809
|
features (ADR 0097 §2). A full-width band only means anything once the column beside it is
|
|
1516
1810
|
constrained, and constraining the column is what leaves the header spanning the full track.
|
|
@@ -1702,7 +1996,6 @@ textarea[data-terp="input"] {
|
|
|
1702
1996
|
gap: var(--space-4);
|
|
1703
1997
|
padding: var(--space-4);
|
|
1704
1998
|
max-width: 32rem;
|
|
1705
|
-
background: var(--color-neutral-0);
|
|
1706
1999
|
border: 1px solid var(--color-neutral-200);
|
|
1707
2000
|
border-radius: var(--radius-lg);
|
|
1708
2001
|
}
|
|
@@ -1848,7 +2141,6 @@ textarea[data-terp="input"] {
|
|
|
1848
2141
|
padding: var(--space-4);
|
|
1849
2142
|
border: 1px solid var(--color-neutral-200);
|
|
1850
2143
|
border-radius: var(--radius-lg);
|
|
1851
|
-
background: var(--color-neutral-0);
|
|
1852
2144
|
color: var(--color-neutral-900);
|
|
1853
2145
|
box-sizing: border-box;
|
|
1854
2146
|
transition: border-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
@@ -1958,6 +2250,11 @@ textarea[data-terp="input"] {
|
|
|
1958
2250
|
leaves the toolbar inside a surface, and leaves two nested frames whenever the view
|
|
1959
2251
|
is empty (the empty state's dashed frame inside the card's solid one).
|
|
1960
2252
|
|
|
2253
|
+
What the slot carries is now a FRAME rather than a fill — border and radius, with the
|
|
2254
|
+
page showing through, per the card rule's note on the surface model. That changes the
|
|
2255
|
+
sentence above by one word and not the ownership it settles: the frame is the table's,
|
|
2256
|
+
not the view's.
|
|
2257
|
+
|
|
1961
2258
|
Keyed on [data-variant="full"] rather than the bare marker for the reason the
|
|
1962
2259
|
previous rule gave and which still holds: [data-variant="embedded"] must declare
|
|
1963
2260
|
nothing, and un-declaring a surface with background: transparent / border: 0 is the
|
|
@@ -1965,7 +2262,6 @@ textarea[data-terp="input"] {
|
|
|
1965
2262
|
[data-terp="dataview"][data-variant="full"] > [data-terp="dataview-scroll"],
|
|
1966
2263
|
[data-terp="dataview"][data-variant="full"] > [data-terp="dataview-error"],
|
|
1967
2264
|
[data-terp="dataview"][data-variant="full"] > [data-terp="dataview-skeleton"] {
|
|
1968
|
-
background: var(--color-neutral-0);
|
|
1969
2265
|
border: 1px solid var(--color-neutral-200);
|
|
1970
2266
|
border-radius: var(--radius-lg);
|
|
1971
2267
|
}
|
|
@@ -2072,10 +2368,15 @@ textarea[data-terp="input"] {
|
|
|
2072
2368
|
[data-terp="dataview-toolbar"][data-variant="selection"] {
|
|
2073
2369
|
/* Still a filled surface, because it marks a MODE and losing that would make
|
|
2074
2370
|
selection invisible — but now it is a surface of its own rather than a band of the
|
|
2075
|
-
card, so it takes the padding and radius that make it read as one.
|
|
2371
|
+
card, so it takes the padding and radius that make it read as one.
|
|
2372
|
+
|
|
2373
|
+
And the fill is the SELECTION token now, not --color-neutral-50: that value is the
|
|
2374
|
+
canvas, so the strip that floats on the canvas was painting a rectangle nobody could
|
|
2375
|
+
see. --color-interactive-selected is what the selected rows below it carry, which is
|
|
2376
|
+
the pairing this band wanted anyway — one mode, one colour, top to bottom. */
|
|
2076
2377
|
padding-inline: var(--density-cell-pad-x);
|
|
2077
2378
|
border-radius: var(--radius-md);
|
|
2078
|
-
background: var(--color-
|
|
2379
|
+
background: var(--color-interactive-selected);
|
|
2079
2380
|
}
|
|
2080
2381
|
[data-terp="dataview-toolbar-count"] {
|
|
2081
2382
|
font-weight: var(--font-weight-medium);
|
|
@@ -2253,7 +2554,6 @@ input[data-terp="input"][type="password"]::-ms-reveal {
|
|
|
2253
2554
|
letter-spacing: 0.04em;
|
|
2254
2555
|
border-bottom: 1px solid var(--color-neutral-200);
|
|
2255
2556
|
white-space: nowrap;
|
|
2256
|
-
background: var(--color-neutral-0);
|
|
2257
2557
|
}
|
|
2258
2558
|
/* A column's declared track. A MINIMUM rather than a width, because a specified width is only a
|
|
2259
2559
|
preference under table-layout: auto and the algorithm shrinks it to fit — which is why the pixel
|
|
@@ -2311,9 +2611,16 @@ input[data-terp="input"][type="password"]::-ms-reveal {
|
|
|
2311
2611
|
A row's own state outranks the selection tint, and the :not() is what says so
|
|
2312
2612
|
without depending on source order — both selectors weigh (0,2,0) otherwise,
|
|
2313
2613
|
which is precisely the trap the layer comment at the top of this file
|
|
2314
|
-
describes. Selection stays legible through the checkbox and data-selected.
|
|
2614
|
+
describes. Selection stays legible through the checkbox and data-selected.
|
|
2615
|
+
|
|
2616
|
+
The tint is --color-interactive-selected, the token's first reader. It was
|
|
2617
|
+
--color-neutral-50 — the canvas — which read as a tint only while the table sat on a
|
|
2618
|
+
filled card, and equalled the hover wash exactly, so a selected row and a pointed-at
|
|
2619
|
+
row looked the same. A brand-tinted row now says selected in every theme, and holds
|
|
2620
|
+
muted ink at AA there (5.19 at its narrowest, in midnight; declared as
|
|
2621
|
+
muted-on-selection so the gate measures it). */
|
|
2315
2622
|
[data-terp="dataview-row"][data-selected="true"]:not([data-tone]) {
|
|
2316
|
-
background: var(--color-
|
|
2623
|
+
background: var(--color-interactive-selected);
|
|
2317
2624
|
}
|
|
2318
2625
|
[data-terp="dataview-row"][data-tone="neutral"] {
|
|
2319
2626
|
background: var(--color-neutral-100);
|
|
@@ -2427,7 +2734,6 @@ th[data-terp="dataview-actions-cell"] > span {
|
|
|
2427
2734
|
display: grid;
|
|
2428
2735
|
gap: var(--space-2);
|
|
2429
2736
|
padding: var(--density-cell-pad-y) var(--density-cell-pad-x);
|
|
2430
|
-
background: var(--color-neutral-0);
|
|
2431
2737
|
border: 1px solid var(--color-neutral-200);
|
|
2432
2738
|
border-radius: var(--radius-lg);
|
|
2433
2739
|
box-shadow: var(--shadow-sm);
|
|
@@ -2559,7 +2865,7 @@ th[data-terp="dataview-actions-cell"] > span {
|
|
|
2559
2865
|
"this belongs to the row above" rather than as another row. */
|
|
2560
2866
|
[data-terp="dataview-expanded-cell"] {
|
|
2561
2867
|
padding: var(--density-cell-pad-y) var(--space-4);
|
|
2562
|
-
background: var(--color-neutral-
|
|
2868
|
+
background: var(--color-neutral-100);
|
|
2563
2869
|
border-block-end: 1px solid var(--color-neutral-200);
|
|
2564
2870
|
}
|
|
2565
2871
|
|
|
@@ -2743,13 +3049,13 @@ th[data-terp="dataview-actions-cell"] > span {
|
|
|
2743
3049
|
padding: var(--space-3);
|
|
2744
3050
|
border: 1px solid var(--color-neutral-200);
|
|
2745
3051
|
border-radius: var(--radius-md);
|
|
2746
|
-
background: var(--color-neutral-0);
|
|
2747
3052
|
}
|
|
2748
3053
|
|
|
2749
3054
|
/* Empty / error / loading states ------------------------------------------- */
|
|
2750
3055
|
/* Same centred block, opposite messages: empty is a dashed outline on the page
|
|
2751
3056
|
surface because nothing is wrong, error is a filled danger wash because
|
|
2752
|
-
something is.
|
|
3057
|
+
something is. Which is now literal — the outline is all of it, and the surface
|
|
3058
|
+
underneath is whatever the block was dropped on. */
|
|
2753
3059
|
[data-terp="empty-state"] {
|
|
2754
3060
|
display: grid;
|
|
2755
3061
|
justify-items: center;
|
|
@@ -2759,7 +3065,6 @@ th[data-terp="dataview-actions-cell"] > span {
|
|
|
2759
3065
|
color: var(--color-neutral-600);
|
|
2760
3066
|
border: 1px dashed var(--color-neutral-300);
|
|
2761
3067
|
border-radius: var(--radius-lg);
|
|
2762
|
-
background: var(--color-neutral-0);
|
|
2763
3068
|
}
|
|
2764
3069
|
/* Compact: a section's emptiness rather than the page's. Same frame and same words,
|
|
2765
3070
|
laid out as a row — the glyph beside the text instead of above it — so two of
|
|
@@ -3751,8 +4056,12 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
3751
4056
|
}
|
|
3752
4057
|
[data-terp="input"]:disabled {
|
|
3753
4058
|
/* background-color (not the background shorthand) so the Select's chevron,
|
|
3754
|
-
drawn as a background-image, survives the disabled state.
|
|
3755
|
-
|
|
4059
|
+
drawn as a background-image, survives the disabled state.
|
|
4060
|
+
|
|
4061
|
+
--color-neutral-100 rather than the canvas value it used to carry: a disabled field
|
|
4062
|
+
on a card with no fill would have been a hole in the page rather than a control with
|
|
4063
|
+
a wash, and the enabled field beside it paints the other end of the ramp. */
|
|
4064
|
+
background-color: var(--color-neutral-100);
|
|
3756
4065
|
color: var(--color-fg-subtle);
|
|
3757
4066
|
cursor: not-allowed;
|
|
3758
4067
|
}
|
|
@@ -4050,8 +4359,29 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
4050
4359
|
[data-terp="dataview-table"] tbody tr {
|
|
4051
4360
|
transition: background-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
4052
4361
|
}
|
|
4053
|
-
|
|
4054
|
-
|
|
4362
|
+
/* --color-interactive-hover, which is the semantic name for the value every control's
|
|
4363
|
+
hover in this sheet already spells, and the token's first reader. The row was one step
|
|
4364
|
+
lighter, on the canvas value, so once the table frame stopped painting a fill there was
|
|
4365
|
+
nothing for a hover to differ from.
|
|
4366
|
+
|
|
4367
|
+
THE ROW'S OWN COLOUR WINS, and the two :not() clauses are the whole of that rather than
|
|
4368
|
+
defensive noise. A row's tone and its selection tint are painted on the tr; this wash is
|
|
4369
|
+
painted on the td, and a cell background paints ABOVE its row's. So without the guards,
|
|
4370
|
+
pointing at a selected row repaints it #f1f5f9 over #eff6ff and it reads as unselected,
|
|
4371
|
+
and pointing at a danger-toned row erases the tone entirely. Both are data; the wash is
|
|
4372
|
+
an affordance, and an affordance may not overwrite what the row is telling you.
|
|
4373
|
+
|
|
4374
|
+
The selection half only became visible when --color-interactive-selected split away from
|
|
4375
|
+
--color-neutral-50: while both spelled the same value the collision painted identically
|
|
4376
|
+
and no baseline could have shown it. The tone half was live before that and equally
|
|
4377
|
+
unpicturable, because no specimen renders a hovered row at all.
|
|
4378
|
+
|
|
4379
|
+
A guarded row therefore shows no wash change and the cursor is what says it is hoverable
|
|
4380
|
+
there — which is exactly what this comment already claimed for the neutral tone, whose
|
|
4381
|
+
--color-neutral-100 happens to equal this token in every theme. That was true by
|
|
4382
|
+
coincidence for one tone; it is true by construction for all of them now. */
|
|
4383
|
+
[data-terp="dataview-table"] tbody tr:not([data-tone]):not([data-selected="true"]):hover td {
|
|
4384
|
+
background: var(--color-interactive-hover);
|
|
4055
4385
|
}
|
|
4056
4386
|
/* The row containing keyboard focus, highlighted because it is the one Enter would
|
|
4057
4387
|
open. Guarded on data-clickable, and the guard is not decoration: the row marker
|