@terpjs/react-core 0.8.0 → 0.9.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 +6 -2
- package/package.json +2 -2
- package/src/Authorized.test.tsx +63 -1
- package/src/Authorized.tsx +35 -2
- package/src/LoginView.tsx +22 -75
- package/src/ModuleNav.test.tsx +19 -0
- package/src/ModuleNav.tsx +10 -35
- package/src/Page.test.tsx +9 -6
- package/src/Page.tsx +15 -39
- package/src/ProfileView.test.tsx +15 -0
- package/src/ProfileView.tsx +8 -33
- package/src/ResourceList.tsx +13 -24
- package/src/admin/AuditLogAdmin.tsx +1 -10
- package/src/admin/GroupCreate.tsx +1 -1
- package/src/admin/GroupDetail.tsx +2 -2
- package/src/admin/UserCreate.tsx +1 -1
- package/src/admin/admin.test.tsx +31 -0
- package/src/download.test.tsx +153 -0
- package/src/download.tsx +132 -0
- package/src/files.tsx +2 -11
- package/src/index.ts +7 -1
- package/src/markers.test.ts +118 -12
- package/src/routeSearch.ts +73 -0
- package/src/routeTypes.ts +50 -6
- package/src/router.test.tsx +191 -1
- package/src/router.tsx +75 -9
- package/src/sso.test.tsx +6 -3
- package/src/styles.test.ts +37 -5
- package/src/styles.ts +356 -6
- package/src/ui/Button.test.tsx +5 -3
package/src/sso.test.tsx
CHANGED
|
@@ -120,9 +120,12 @@ describe("SSO login (ADR 0058)", () => {
|
|
|
120
120
|
</TerpProvider>,
|
|
121
121
|
);
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
// Announced, not merely displayed. This is the one screen whose user is not signed in
|
|
124
|
+
// yet, so a failure they cannot see leaves them with no signal at all that the sign-in
|
|
125
|
+
// did not happen — the form simply sits there. ResourceList's error has carried
|
|
126
|
+
// role="alert" since it existed; this one carried nothing.
|
|
127
|
+
const failure = await screen.findByRole("alert");
|
|
128
|
+
expect(failure).toHaveTextContent("Single sign-on failed. Try again.");
|
|
126
129
|
expect(window.location.pathname).toBe("/");
|
|
127
130
|
});
|
|
128
131
|
});
|
package/src/styles.test.ts
CHANGED
|
@@ -534,11 +534,13 @@ describe("cascade structure", () => {
|
|
|
534
534
|
// assertions because both had a real consumer until it migrated.
|
|
535
535
|
//
|
|
536
536
|
// The focus ring's !important could only ever beat an INLINE box-shadow on an element the
|
|
537
|
-
// selector matches, and there is none
|
|
538
|
-
// the two
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
537
|
+
// selector matches, and there is now none anywhere in the package. ConfirmDialog's dialog
|
|
538
|
+
// and Popover's panel were the two that mattered and took their surface from the sheet in
|
|
539
|
+
// stage 4; the three that remained — AppShell's drawer, DataViewCardList's card and
|
|
540
|
+
// LoginView's card — went with those three components' own migrations, the last of them
|
|
541
|
+
// with LoginView. The DataView card is worth remembering anyway: it is a div with onClick
|
|
542
|
+
// and no tabIndex, so it cannot match :focus-visible at all, which is why the sheet reaches
|
|
543
|
+
// it with :focus-within.
|
|
542
544
|
// Measured after removing it: a keyboard-focused primary button and a focused menu item both
|
|
543
545
|
// still compute rgba(37,99,235,0.35) 0 0 0 3px, because the LAYER carries the ring, not the
|
|
544
546
|
// escalation. That was always the claim in this file's header; the escalation was belt.
|
|
@@ -837,6 +839,36 @@ describe("cascade structure", () => {
|
|
|
837
839
|
"appshell-header-group",
|
|
838
840
|
"appshell-main",
|
|
839
841
|
"appshell-footer",
|
|
842
|
+
"page",
|
|
843
|
+
"page-header",
|
|
844
|
+
"page-breadcrumbs",
|
|
845
|
+
"page-heading",
|
|
846
|
+
"page-title",
|
|
847
|
+
"resource-list",
|
|
848
|
+
"resource-list-create",
|
|
849
|
+
"resource-list-error",
|
|
850
|
+
"resource-list-empty",
|
|
851
|
+
"resource-list-items",
|
|
852
|
+
"resource-list-row",
|
|
853
|
+
"module-nav",
|
|
854
|
+
"module-nav-list",
|
|
855
|
+
"module-nav-link",
|
|
856
|
+
"profile-card",
|
|
857
|
+
"profile-avatar",
|
|
858
|
+
"profile-email",
|
|
859
|
+
"profile-role",
|
|
860
|
+
"login-view",
|
|
861
|
+
"login-card",
|
|
862
|
+
"login-brand",
|
|
863
|
+
"login-title",
|
|
864
|
+
"login-form",
|
|
865
|
+
"login-sso",
|
|
866
|
+
"login-separator",
|
|
867
|
+
"login-separator-rule",
|
|
868
|
+
"login-error",
|
|
869
|
+
"admin-form",
|
|
870
|
+
"admin-section-title",
|
|
871
|
+
"admin-payload",
|
|
840
872
|
]) {
|
|
841
873
|
expect(
|
|
842
874
|
declaresRuleFor(base, `[data-terp="${marker}"]`),
|
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
|
|
@@ -519,6 +525,35 @@ textarea[data-terp="input"] {
|
|
|
519
525
|
color: var(--color-neutral-900);
|
|
520
526
|
}
|
|
521
527
|
|
|
528
|
+
/* Module navigation -------------------------------------------------------- */
|
|
529
|
+
/* Secondary tabs for a module's sub-pages, and very nearly the same object as tab
|
|
530
|
+
above: a transparent 2px edge the active item colours, muted ink the active item
|
|
531
|
+
darkens. The difference is that these are router links rather than buttons, and
|
|
532
|
+
that difference decides where the active state is keyed — see terp.state.
|
|
533
|
+
|
|
534
|
+
The edges are logical (border-block-end) to match tab and appshell-footer rather
|
|
535
|
+
than the physical borderBottom the component declared. Identical in every writing
|
|
536
|
+
mode this framework ships, and the sheet already had one convention. */
|
|
537
|
+
[data-terp="module-nav"] {
|
|
538
|
+
border-block-end: 1px solid var(--color-neutral-200);
|
|
539
|
+
}
|
|
540
|
+
[data-terp="module-nav-list"] {
|
|
541
|
+
list-style: none;
|
|
542
|
+
margin: 0;
|
|
543
|
+
padding: 0;
|
|
544
|
+
display: flex;
|
|
545
|
+
flex-wrap: wrap;
|
|
546
|
+
gap: var(--space-3);
|
|
547
|
+
}
|
|
548
|
+
[data-terp="module-nav-link"] {
|
|
549
|
+
display: inline-flex;
|
|
550
|
+
align-items: center;
|
|
551
|
+
padding: var(--space-2) 0;
|
|
552
|
+
color: var(--color-neutral-600);
|
|
553
|
+
text-decoration: none;
|
|
554
|
+
border-block-end: 2px solid transparent;
|
|
555
|
+
}
|
|
556
|
+
|
|
522
557
|
/* Breadcrumbs -------------------------------------------------------------- */
|
|
523
558
|
/* The trail owns its whole subtree, so the list and its items are addressed
|
|
524
559
|
structurally and the current crumb by the aria-current it already carries —
|
|
@@ -806,6 +841,240 @@ textarea[data-terp="input"] {
|
|
|
806
841
|
font-size: var(--font-size-xs);
|
|
807
842
|
}
|
|
808
843
|
|
|
844
|
+
/* The page frame ----------------------------------------------------------- */
|
|
845
|
+
/* Every routed view is this shape: one header carrying the breadcrumb trail (when
|
|
846
|
+
there is a path back up) and the title row, then the body. HubPage, OverviewPage
|
|
847
|
+
and DetailPage are all this frame with a different trail.
|
|
848
|
+
|
|
849
|
+
The header is a <header> ELEMENT and Page has to keep it one, which is a
|
|
850
|
+
constraint this sheet cannot express and the component records at the site: the
|
|
851
|
+
layout contract's runtime slot check reads article.children and drops the header
|
|
852
|
+
by TAG NAME, so re-rendering it as a marked <div> would put it back into the body
|
|
853
|
+
set and fail every governed OverviewPage and DetailPage closed. Marking it is
|
|
854
|
+
additive; retagging it is not. The body likewise takes no wrapper — not even a
|
|
855
|
+
display: contents one, since that check is a DOM traversal and would see the node
|
|
856
|
+
whether or not it generates a box.
|
|
857
|
+
|
|
858
|
+
align-content: start is what keeps the rows at the top of a page taller than its
|
|
859
|
+
content — the loading and error frames, where the body is one small block. With
|
|
860
|
+
the default the two rows would spread to fill the height. It needs something to
|
|
861
|
+
stretch the article before it is observable at all, which is why page-loading and
|
|
862
|
+
page-error render inside a grid box rather than a plain tall div. */
|
|
863
|
+
[data-terp="page"] {
|
|
864
|
+
display: grid;
|
|
865
|
+
grid-template-columns: minmax(0, 1fr);
|
|
866
|
+
gap: var(--space-4);
|
|
867
|
+
align-content: start;
|
|
868
|
+
min-width: 0;
|
|
869
|
+
}
|
|
870
|
+
[data-terp="page-header"] {
|
|
871
|
+
display: grid;
|
|
872
|
+
gap: var(--space-2);
|
|
873
|
+
}
|
|
874
|
+
/* The crumb row keeps a 2rem floor, and it is doing work rather than reserving
|
|
875
|
+
space for its own sake: the trail is shorter than 2rem at font-size-sm, so
|
|
876
|
+
dropping the floor closes the gap under the trail on every page that has one.
|
|
877
|
+
Measured — removing it moves all six baselines with a trail and nothing else. */
|
|
878
|
+
[data-terp="page-breadcrumbs"] {
|
|
879
|
+
display: flex;
|
|
880
|
+
align-items: center;
|
|
881
|
+
min-height: 2rem;
|
|
882
|
+
}
|
|
883
|
+
/* Title left, the actions slot right, wrapping rather than overflowing when a long
|
|
884
|
+
title meets a wide action cluster. */
|
|
885
|
+
[data-terp="page-heading"] {
|
|
886
|
+
display: flex;
|
|
887
|
+
align-items: center;
|
|
888
|
+
justify-content: space-between;
|
|
889
|
+
gap: var(--space-3);
|
|
890
|
+
flex-wrap: wrap;
|
|
891
|
+
}
|
|
892
|
+
/* The single h1 of the view. margin: 0 is load-bearing — the browser default h1
|
|
893
|
+
margin would otherwise fight the header's own gap. */
|
|
894
|
+
[data-terp="page-title"] {
|
|
895
|
+
margin: 0;
|
|
896
|
+
font-size: var(--font-size-lg);
|
|
897
|
+
font-weight: var(--font-weight-semibold);
|
|
898
|
+
letter-spacing: 0;
|
|
899
|
+
color: var(--color-neutral-900);
|
|
900
|
+
line-height: 1.3;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/* The sign-in screen ------------------------------------------------------- */
|
|
904
|
+
/* The one screen an unauthenticated user sees, and the only full-viewport page in
|
|
905
|
+
the package: a 100vh grid centring one card. The reset layer's box-sizing note
|
|
906
|
+
already names this page as the reason it exists — content-box plus 100vh plus
|
|
907
|
+
padding overflows the viewport by exactly the padding, which is a phantom
|
|
908
|
+
scrollbar on a page that fits.
|
|
909
|
+
|
|
910
|
+
The buttons fill their group, and that is a rule on the GROUP rather than a prop
|
|
911
|
+
on Button, because it has exactly one consumer in the package. Button declares
|
|
912
|
+
width: fit-content, which is a definite width — so grid stretch does NOT do this
|
|
913
|
+
for free, and dropping the declaration shrinks all four buttons to their labels.
|
|
914
|
+
Two selectors at (0,2,0) against the button's own (0,1,0), same layer, so
|
|
915
|
+
specificity settles it and source order never enters into it. A block prop on
|
|
916
|
+
Button would be a new public API minted for one internal caller; the sheet can
|
|
917
|
+
already reach the thing, which is the test stage 4 set when it deleted Menu's
|
|
918
|
+
style props.
|
|
919
|
+
|
|
920
|
+
The separator's ink moves from --color-neutral-500 to --color-fg-subtle, which is
|
|
921
|
+
the rest of the migration ec36a2b started rather than a new decision: the two
|
|
922
|
+
tokens are byte-identical in light and dark, and only fg-subtle has a declared
|
|
923
|
+
pairing (subtle-on-surface) for the gate to measure. So the screenshot themes do
|
|
924
|
+
not move and midnight and twilight get the value the gate has been measuring all
|
|
925
|
+
along. The "or" is aria-hidden but it is visible text, so it is held to AA rather
|
|
926
|
+
than treated as an ornament.
|
|
927
|
+
|
|
928
|
+
The error line has no specimen and cannot have one: the error is internal state set
|
|
929
|
+
only in a catch, and sso.error needs a real failed callback, which needs a URL the
|
|
930
|
+
lane owns. Its ink is gated statically instead — danger-on-card, declared for this
|
|
931
|
+
surface and measured in all five themes. */
|
|
932
|
+
[data-terp="login-view"] {
|
|
933
|
+
min-height: 100vh;
|
|
934
|
+
display: grid;
|
|
935
|
+
place-items: center;
|
|
936
|
+
padding: var(--space-6);
|
|
937
|
+
background: var(--color-neutral-50);
|
|
938
|
+
font-family: var(--font-family-sans);
|
|
939
|
+
color: var(--color-neutral-900);
|
|
940
|
+
}
|
|
941
|
+
[data-terp="login-card"] {
|
|
942
|
+
width: 100%;
|
|
943
|
+
max-width: 24rem;
|
|
944
|
+
display: grid;
|
|
945
|
+
gap: var(--space-4);
|
|
946
|
+
padding: var(--space-6);
|
|
947
|
+
background: var(--color-neutral-0);
|
|
948
|
+
border: 1px solid var(--color-neutral-200);
|
|
949
|
+
border-radius: var(--radius-lg);
|
|
950
|
+
box-shadow: var(--shadow-md);
|
|
951
|
+
}
|
|
952
|
+
[data-terp="login-brand"] {
|
|
953
|
+
display: flex;
|
|
954
|
+
align-items: center;
|
|
955
|
+
gap: var(--space-2);
|
|
956
|
+
color: var(--color-neutral-900);
|
|
957
|
+
}
|
|
958
|
+
[data-terp="login-title"] {
|
|
959
|
+
margin: 0;
|
|
960
|
+
font-size: var(--font-size-xl);
|
|
961
|
+
font-weight: var(--font-weight-bold);
|
|
962
|
+
letter-spacing: 0;
|
|
963
|
+
}
|
|
964
|
+
[data-terp="login-form"],
|
|
965
|
+
[data-terp="login-sso"] {
|
|
966
|
+
display: grid;
|
|
967
|
+
gap: var(--space-3);
|
|
968
|
+
}
|
|
969
|
+
[data-terp="login-form"] > [data-terp="button"],
|
|
970
|
+
[data-terp="login-sso"] > [data-terp="button"] {
|
|
971
|
+
width: 100%;
|
|
972
|
+
}
|
|
973
|
+
[data-terp="login-separator"] {
|
|
974
|
+
display: flex;
|
|
975
|
+
align-items: center;
|
|
976
|
+
gap: var(--space-2);
|
|
977
|
+
color: var(--color-fg-subtle);
|
|
978
|
+
font-size: var(--font-size-xs);
|
|
979
|
+
text-transform: uppercase;
|
|
980
|
+
letter-spacing: 0.06em;
|
|
981
|
+
}
|
|
982
|
+
[data-terp="login-separator-rule"] {
|
|
983
|
+
flex: 1;
|
|
984
|
+
border-block-start: 1px solid var(--color-neutral-200);
|
|
985
|
+
}
|
|
986
|
+
[data-terp="login-error"] {
|
|
987
|
+
margin: 0;
|
|
988
|
+
color: var(--color-status-danger);
|
|
989
|
+
font-size: var(--font-size-sm);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/* The profile screen ------------------------------------------------------- */
|
|
993
|
+
/* The built-in /profile view: two cards, an avatar tile, and the identity lines.
|
|
994
|
+
Its cards are its own rather than the Card component's — the same declarations on
|
|
995
|
+
a different element — and folding the two together is a component decision, not a
|
|
996
|
+
styling one, so it stays a follow-up rather than riding in on a migration whose
|
|
997
|
+
whole contract is zero pixel movement.
|
|
998
|
+
|
|
999
|
+
overflow-wrap on the address is real and unobservable: the workbench session is a
|
|
1000
|
+
fixed user whose address is short, so no specimen can paint the case it exists for
|
|
1001
|
+
(an address has no spaces to break at, so a long one widens the card past its own
|
|
1002
|
+
max-width instead of wrapping). Asserted in the unit test as the marker it keys on;
|
|
1003
|
+
there is no picture of it and cannot be until the mock session is variable. */
|
|
1004
|
+
[data-terp="profile-card"] {
|
|
1005
|
+
display: grid;
|
|
1006
|
+
gap: var(--space-4);
|
|
1007
|
+
padding: var(--space-4);
|
|
1008
|
+
max-width: 32rem;
|
|
1009
|
+
background: var(--color-neutral-0);
|
|
1010
|
+
border: 1px solid var(--color-neutral-200);
|
|
1011
|
+
border-radius: var(--radius-lg);
|
|
1012
|
+
}
|
|
1013
|
+
/* The initials tile. It is aria-hidden, so axe skips it by design and the declared
|
|
1014
|
+
pairing is the only thing measuring its ink: brand-primary-contrast on
|
|
1015
|
+
brand-primary is primary-button-label, which the contrast gate holds at AA in all
|
|
1016
|
+
five themes. Exactly the shape of NavIcon's fallback tile, which failed at 1.60
|
|
1017
|
+
for as long as nothing declared it. */
|
|
1018
|
+
[data-terp="profile-avatar"] {
|
|
1019
|
+
display: inline-flex;
|
|
1020
|
+
align-items: center;
|
|
1021
|
+
justify-content: center;
|
|
1022
|
+
width: 3.5rem;
|
|
1023
|
+
height: 3.5rem;
|
|
1024
|
+
flex-shrink: 0;
|
|
1025
|
+
border-radius: var(--radius-full);
|
|
1026
|
+
background: var(--color-brand-primary);
|
|
1027
|
+
color: var(--color-brand-primary-contrast);
|
|
1028
|
+
font-size: var(--font-size-lg);
|
|
1029
|
+
font-weight: var(--font-weight-medium);
|
|
1030
|
+
}
|
|
1031
|
+
[data-terp="profile-email"] {
|
|
1032
|
+
overflow-wrap: anywhere;
|
|
1033
|
+
}
|
|
1034
|
+
[data-terp="profile-role"] {
|
|
1035
|
+
margin: 0;
|
|
1036
|
+
color: var(--color-neutral-600);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/* The built-in admin screens ----------------------------------------------- */
|
|
1040
|
+
/* The packaged /admin views. Three surfaces, and they were invisible to both
|
|
1041
|
+
ratchets for the entire migration — which is why they are here rather than in
|
|
1042
|
+
0.8.0. The worklist names files with NO marker at all, and every admin view
|
|
1043
|
+
rendered none, so it read as a view composition and was excluded on purpose. The
|
|
1044
|
+
ledger counts module-scope CSSProperties declarations, and these were four
|
|
1045
|
+
call-site literals plus one unannotated object. Neither gate was wrong; both were
|
|
1046
|
+
narrower than they looked, and the same commit widens the measure.
|
|
1047
|
+
|
|
1048
|
+
The form box is ONE marker across two files, because UserCreate and GroupCreate
|
|
1049
|
+
constrain their form to the same measure — the same surface twice, not two
|
|
1050
|
+
surfaces that happen to agree today. */
|
|
1051
|
+
[data-terp="admin-form"] {
|
|
1052
|
+
max-width: 32rem;
|
|
1053
|
+
}
|
|
1054
|
+
/* A section heading inside a detail screen: the members list, the permission
|
|
1055
|
+
grants. font-size-base rather than the UA default, which for an h2 is LARGER than
|
|
1056
|
+
the page's own h1 at font-size-lg — so without this a section outranks the view
|
|
1057
|
+
it sits in. */
|
|
1058
|
+
[data-terp="admin-section-title"] {
|
|
1059
|
+
margin: 0;
|
|
1060
|
+
font-size: var(--font-size-base);
|
|
1061
|
+
}
|
|
1062
|
+
/* The audit event's JSON payload. No font-family: it is a <pre>, so the UA
|
|
1063
|
+
stylesheet's monospace already applies and the inline object set none either.
|
|
1064
|
+
|
|
1065
|
+
font-size-sm loses the inline fallback the object carried (0.875rem) and no other
|
|
1066
|
+
rule in this sheet has one. The fallback could never fire — tokens.guard.test.ts
|
|
1067
|
+
refuses any var() in react-core naming a property the contract does not publish,
|
|
1068
|
+
so the token is always there. It recorded an author's doubt, not an option. */
|
|
1069
|
+
[data-terp="admin-payload"] {
|
|
1070
|
+
margin: 0;
|
|
1071
|
+
padding: var(--space-3);
|
|
1072
|
+
background: var(--color-neutral-100);
|
|
1073
|
+
border-radius: var(--radius-md);
|
|
1074
|
+
font-size: var(--font-size-sm);
|
|
1075
|
+
overflow-x: auto;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
809
1078
|
/* Hub cards --------------------------------------------------------------- */
|
|
810
1079
|
/* This whole family was in terp.state, resting declarations and all, for the same
|
|
811
1080
|
reason the icon button's transition was: that is where the hover rules needing it
|
|
@@ -1613,6 +1882,58 @@ th[data-terp="dataview-actions-cell"] > span {
|
|
|
1613
1882
|
color: var(--color-neutral-700);
|
|
1614
1883
|
}
|
|
1615
1884
|
|
|
1885
|
+
/* Resource list ------------------------------------------------------------ */
|
|
1886
|
+
/* The plain listing screen: a write-gated create row, then the rows. The DataView is
|
|
1887
|
+
the same job at scale, and this one stays deliberately simple, so its rules are
|
|
1888
|
+
geometry plus two inks.
|
|
1889
|
+
|
|
1890
|
+
The create row keeps its flex layout and the input keeps flex: 1 as a RULE rather
|
|
1891
|
+
than becoming a 1fr auto grid, and that is a move rather than a decision. With
|
|
1892
|
+
flex: 1 the input's basis is 0 so it may shrink below its intrinsic width; a 1fr
|
|
1893
|
+
track floors at min-content instead. The two agree at this list's 40rem cap, so
|
|
1894
|
+
swapping them would have changed nothing anybody could see until some narrower
|
|
1895
|
+
container found the difference — which is the kind of diff this migration exists
|
|
1896
|
+
not to introduce.
|
|
1897
|
+
|
|
1898
|
+
Neither paragraph resets its margin, and that is verbatim rather than an
|
|
1899
|
+
oversight: both are <p> elements whose default margin is what separates them from
|
|
1900
|
+
the form above and the rows below. Adding margin: 0 here would move the rows. */
|
|
1901
|
+
[data-terp="resource-list"] {
|
|
1902
|
+
display: grid;
|
|
1903
|
+
gap: var(--space-4);
|
|
1904
|
+
max-width: 40rem;
|
|
1905
|
+
}
|
|
1906
|
+
[data-terp="resource-list-create"] {
|
|
1907
|
+
display: flex;
|
|
1908
|
+
gap: var(--space-2);
|
|
1909
|
+
}
|
|
1910
|
+
[data-terp="resource-list-create"] > [data-terp="input"] {
|
|
1911
|
+
flex: 1;
|
|
1912
|
+
}
|
|
1913
|
+
[data-terp="resource-list-error"] {
|
|
1914
|
+
color: var(--color-status-danger);
|
|
1915
|
+
}
|
|
1916
|
+
[data-terp="resource-list-empty"] {
|
|
1917
|
+
color: var(--color-neutral-600);
|
|
1918
|
+
}
|
|
1919
|
+
[data-terp="resource-list-items"] {
|
|
1920
|
+
list-style: none;
|
|
1921
|
+
margin: 0;
|
|
1922
|
+
padding: 0;
|
|
1923
|
+
display: grid;
|
|
1924
|
+
gap: var(--space-2);
|
|
1925
|
+
}
|
|
1926
|
+
[data-terp="resource-list-row"] {
|
|
1927
|
+
display: flex;
|
|
1928
|
+
align-items: center;
|
|
1929
|
+
justify-content: space-between;
|
|
1930
|
+
gap: var(--space-3);
|
|
1931
|
+
padding: var(--space-3);
|
|
1932
|
+
border: 1px solid var(--color-neutral-200);
|
|
1933
|
+
border-radius: var(--radius-md);
|
|
1934
|
+
background: var(--color-neutral-0);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1616
1937
|
/* Empty / error / loading states ------------------------------------------- */
|
|
1617
1938
|
/* Same centred block, opposite messages: empty is a dashed outline on the page
|
|
1618
1939
|
surface because nothing is wrong, error is a filled danger wash because
|
|
@@ -2578,6 +2899,35 @@ button[data-terp="input"][data-placeholder="true"] {
|
|
|
2578
2899
|
cursor: not-allowed;
|
|
2579
2900
|
}
|
|
2580
2901
|
|
|
2902
|
+
/* Module navigation ------------------------------------------------------- */
|
|
2903
|
+
/* Which sub-page you are on, here for the same reason the selected tab is: the
|
|
2904
|
+
accent edge and the darker ink have to beat the resting pair.
|
|
2905
|
+
|
|
2906
|
+
Keyed on data-active, which ModuleNav writes, and deliberately NOT on the
|
|
2907
|
+
aria-current the same element also carries. That attribute has a second author:
|
|
2908
|
+
TanStack's link props spread the router's own active props LAST, after the
|
|
2909
|
+
caller's, so on a Link the router has the final word on aria-current. This is
|
|
2910
|
+
the breadcrumb lesson in its exact form — reuse a semantic only where the
|
|
2911
|
+
component is its sole author.
|
|
2912
|
+
|
|
2913
|
+
The two notions of "active" are not the same predicate, and they diverge in BOTH
|
|
2914
|
+
directions, which is worth knowing before touching either. activeOptions
|
|
2915
|
+
.includeSearch defaults to true, so the router additionally demands an exact
|
|
2916
|
+
query-string match that ModuleNav does not — the router is narrower there. And
|
|
2917
|
+
with exact matching the router compares through exactPathTest, which is
|
|
2918
|
+
removeTrailingSlash(a) === removeTrailingSlash(b), while ModuleNav compares
|
|
2919
|
+
pathname === item.to raw — so on a path with a trailing slash the ROUTER is
|
|
2920
|
+
active and ModuleNav is not, and this rule withholds the accent edge from a tab
|
|
2921
|
+
the router considers current. That second case is a real defect and it is older
|
|
2922
|
+
than this rule: the inline styling it replaced read the same isActive, so the
|
|
2923
|
+
behaviour is unchanged and only the reasoning was wrong. Fixing the predicate
|
|
2924
|
+
belongs with the navigation model, because that is what decides what "active"
|
|
2925
|
+
should mean. */
|
|
2926
|
+
[data-terp="module-nav-link"][data-active="true"] {
|
|
2927
|
+
color: var(--color-neutral-900);
|
|
2928
|
+
border-block-end-color: var(--color-fg-accent);
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2581
2931
|
/* Hub cards --------------------------------------------------------------- */
|
|
2582
2932
|
/* The hover edge recolours hubcard-BODY, not the card.
|
|
2583
2933
|
|
package/src/ui/Button.test.tsx
CHANGED
|
@@ -39,9 +39,11 @@ describe("Button", () => {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it("still forwards an explicit style, so framework callers keep their escape", () => {
|
|
42
|
-
// The sheet owns the base
|
|
43
|
-
//
|
|
44
|
-
//
|
|
42
|
+
// The sheet owns the base, and the escape still has to work: a caller may pass a measured
|
|
43
|
+
// value the sheet has no business owning, and it wins because a style attribute outranks
|
|
44
|
+
// any author rule (ADR 0094 §3). It is no longer LoginView's full-width submit, which is
|
|
45
|
+
// what this comment used to cite — a fixed 100% is layout policy rather than a measured
|
|
46
|
+
// value, and the sheet could reach it, so it is a rule on the login form now.
|
|
45
47
|
render(<Button style={{ width: "100%" }}>Wide</Button>);
|
|
46
48
|
expect(screen.getByRole("button", { name: "Wide" }).style.width).toBe("100%");
|
|
47
49
|
});
|