@terpjs/react-core 0.13.0 → 0.14.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/tokens.guard.test.ts
CHANGED
|
@@ -84,21 +84,24 @@ const UNREAD_TOKENS: Record<string, string[]> = {
|
|
|
84
84
|
"--shell-": [],
|
|
85
85
|
"--color-sidebar-": [],
|
|
86
86
|
// The whole semantic colour layer, which the comment above admitted was untracked and then
|
|
87
|
-
// left untracked.
|
|
88
|
-
// reader anywhere in the sheet
|
|
89
|
-
//
|
|
87
|
+
// left untracked. Booked at seventeen of the forty-eight published `--color-` tokens with no
|
|
88
|
+
// `var()` reader anywhere in the sheet — the exact `--color-fg-on-brand` shape that comment
|
|
89
|
+
// describes, seventeen times over. Fifteen now: the flat-card change wired
|
|
90
|
+
// `--color-interactive-hover` (the table row's hover, which had been spelling that value as a
|
|
91
|
+
// ramp step) and `--color-interactive-selected` (the selected row and the selection band,
|
|
92
|
+
// which had no colour of their own at all).
|
|
90
93
|
//
|
|
91
94
|
// Booked as one family rather than four narrower ones on purpose. `--color-bg-`,
|
|
92
|
-
// `--color-border
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
95
|
+
// `--color-border-` and `--color-chart-` are each STILL read by nothing, and the assertion
|
|
96
|
+
// below requires a tracked family to publish more than it books — a deliberate "is the prefix
|
|
97
|
+
// right?" check that a wholly-unread family would trip. The broader prefix subsumes them and
|
|
98
|
+
// still catches the sixteenth. `--color-sidebar-` stays above because it makes a narrower
|
|
99
|
+
// claim worth keeping: that family is read in full.
|
|
97
100
|
//
|
|
98
|
-
// None of these
|
|
99
|
-
// ahead of its consumers — four surface tokens, three borders,
|
|
101
|
+
// None of these fifteen is a defect on its own. They are a published vocabulary that shipped
|
|
102
|
+
// ahead of its consumers — four surface tokens, three borders, one interactive state, a
|
|
100
103
|
// five-step chart ramp and two neutrals — and the point of booking them is that the list can
|
|
101
|
-
// only shrink from here, so wiring one is visible and adding
|
|
104
|
+
// only shrink from here, so wiring one is visible and adding a sixteenth has to argue.
|
|
102
105
|
"--color-": [
|
|
103
106
|
"--color-bg-canvas",
|
|
104
107
|
"--color-bg-inset",
|
|
@@ -113,8 +116,6 @@ const UNREAD_TOKENS: Record<string, string[]> = {
|
|
|
113
116
|
"--color-chart-4",
|
|
114
117
|
"--color-chart-5",
|
|
115
118
|
"--color-interactive-active",
|
|
116
|
-
"--color-interactive-hover",
|
|
117
|
-
"--color-interactive-selected",
|
|
118
119
|
"--color-neutral-500",
|
|
119
120
|
"--color-neutral-800",
|
|
120
121
|
],
|
|
@@ -228,6 +229,45 @@ describe("design tokens", () => {
|
|
|
228
229
|
}
|
|
229
230
|
});
|
|
230
231
|
|
|
232
|
+
it("tightens the shell gutter for a phone rather than remapping it to itself", () => {
|
|
233
|
+
// --shell-gutter is published once and remapped once, on the shell root under
|
|
234
|
+
// data-variant="mobile". Existence is not the property worth holding: a remap that
|
|
235
|
+
// resolved back to the published value would leave every structural check green while
|
|
236
|
+
// the phone silently kept the desktop measure, and the whole reason the token exists is
|
|
237
|
+
// that the header, main, the footer and the page band all read it.
|
|
238
|
+
//
|
|
239
|
+
// Here rather than in styles.test.ts because the control is the PUBLISHED value, which
|
|
240
|
+
// means reading tokens.css — this file's subject, and this file runs in node where
|
|
241
|
+
// that read works.
|
|
242
|
+
const published = /--shell-gutter:\s*([^;]+);/.exec(tokensCss);
|
|
243
|
+
expect(published, "tokens.css must publish --shell-gutter").not.toBeNull();
|
|
244
|
+
const remap = /\[data-terp="appshell"\]\[data-variant="mobile"\] \{([^}]*)\}/.exec(sheet);
|
|
245
|
+
expect(remap, "the sheet must remap the gutter for a phone").not.toBeNull();
|
|
246
|
+
const remapped = /--shell-gutter:\s*([^;]+);/.exec(remap![1]!);
|
|
247
|
+
expect(remapped, "the remap must move --shell-gutter, not something else").not.toBeNull();
|
|
248
|
+
// It must stay on the spacing scale rather than becoming a literal, which is one way a
|
|
249
|
+
// shared measure rots.
|
|
250
|
+
expect(remapped![1]!.trim(), "the phone gutter must name a spacing token").toMatch(
|
|
251
|
+
/^var\(--space-\d+\)$/,
|
|
252
|
+
);
|
|
253
|
+
// RESOLVED, not spelled. Comparing the two strings was the first version of this and it
|
|
254
|
+
// could not fail the way that matters: `var(--space-4)` and `1rem` are different text and
|
|
255
|
+
// the same length, so publishing the gutter at the phone's own value passed while the
|
|
256
|
+
// remap became a no-op. Both sides resolve through tokens.css before they are compared.
|
|
257
|
+
const resolve = (value: string): string => {
|
|
258
|
+
const reference = /^var\((--[a-z0-9-]+)\)$/.exec(value.trim());
|
|
259
|
+
if (reference === null) return value.trim();
|
|
260
|
+
const declared = new RegExp(`${reference[1]!}:\\s*([^;]+);`).exec(tokensCss);
|
|
261
|
+
expect(declared, `${reference[1]!} must be declared in tokens.css`).not.toBeNull();
|
|
262
|
+
return declared![1]!.trim();
|
|
263
|
+
};
|
|
264
|
+
expect(
|
|
265
|
+
resolve(remapped![1]!),
|
|
266
|
+
"a remap that resolves to the published gutter is dead weight, and the phone silently "
|
|
267
|
+
+ "keeps the desktop measure",
|
|
268
|
+
).not.toBe(resolve(published![1]!));
|
|
269
|
+
});
|
|
270
|
+
|
|
231
271
|
it("holds the bare type literals at their recorded count", () => {
|
|
232
272
|
// The debt the type scale's arrival did not clear, as a ratchet. See BARE_TYPE_LITERALS:
|
|
233
273
|
// most of these map onto no step in the published scale, so converting them changes
|
package/src/ui/Card.test.tsx
CHANGED
|
@@ -36,6 +36,44 @@ describe("Card", () => {
|
|
|
36
36
|
expect(screen.getByRole("button", { name: "Nieuw" })).toBeInTheDocument();
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
it("keeps the actions slot a sibling of the heading, description or not", () => {
|
|
40
|
+
// The DOM half of a defect whose visible half was CSS. `actions` is documented as a slot in
|
|
41
|
+
// the header ROW, and it stopped being one the moment `description` was set: the heading was
|
|
42
|
+
// sized from its content, a block holding a title and a sentence is as wide as the sentence,
|
|
43
|
+
// and flex breaks lines on that width before it shrinks anything — so the header wrapped and
|
|
44
|
+
// the control landed underneath. Measured 103px against 48px for the same component one prop
|
|
45
|
+
// apart.
|
|
46
|
+
//
|
|
47
|
+
// What the sheet's fix needs from the markup is exactly this shape: the description inside
|
|
48
|
+
// the heading (so :has() can find it and so the two lines stay one block) and the actions
|
|
49
|
+
// slot OUTSIDE it, as a sibling. Nesting the slot in the heading, or lifting the description
|
|
50
|
+
// out of it, would each render plausibly and defeat the rule silently — which is why this is
|
|
51
|
+
// asserted structurally rather than left to a screenshot.
|
|
52
|
+
for (const description of [undefined, "Wat dit blok beschrijft."]) {
|
|
53
|
+
cleanup();
|
|
54
|
+
render(
|
|
55
|
+
<Card
|
|
56
|
+
title="Projecten"
|
|
57
|
+
description={description}
|
|
58
|
+
actions={<button type="button">Nieuw</button>}
|
|
59
|
+
>
|
|
60
|
+
inhoud
|
|
61
|
+
</Card>,
|
|
62
|
+
);
|
|
63
|
+
const header = screen
|
|
64
|
+
.getByRole("heading", { level: 3, name: "Projecten" })
|
|
65
|
+
.closest('[data-terp="card-header"]') as HTMLElement;
|
|
66
|
+
const heading = header.querySelector('[data-terp="card-heading"]') as HTMLElement;
|
|
67
|
+
const actions = header.querySelector('[data-terp="card-actions"]') as HTMLElement;
|
|
68
|
+
expect(actions.parentElement, "the actions slot is the heading's sibling").toBe(header);
|
|
69
|
+
expect(heading.contains(actions)).toBe(false);
|
|
70
|
+
expect(
|
|
71
|
+
heading.querySelector('[data-terp="card-description"]') !== null,
|
|
72
|
+
"the description lives inside the heading, which is what the :has() rule reads",
|
|
73
|
+
).toBe(description !== undefined);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
39
77
|
it("renders headerless with children only", () => {
|
|
40
78
|
render(<Card>alleen inhoud</Card>);
|
|
41
79
|
const card = screen.getByText("alleen inhoud").closest('[data-terp="card"]');
|
|
@@ -43,7 +81,7 @@ describe("Card", () => {
|
|
|
43
81
|
});
|
|
44
82
|
it("names the plain variant and leaves the boxed default unmarked", () => {
|
|
45
83
|
// The chrome-less titled region. A variant rather than a `Section` component of its own,
|
|
46
|
-
// because a chrome-less region is this element with
|
|
84
|
+
// because a chrome-less region is this element with two declarations removed — a second
|
|
47
85
|
// component would have meant six more markers describing the same DOM, and a `Surface` is
|
|
48
86
|
// a Card with no title, which this already is.
|
|
49
87
|
const { rerender } = render(<Card variant="plain" title="Plain">body</Card>);
|
package/src/ui/Card.tsx
CHANGED
|
@@ -21,7 +21,7 @@ export interface CardProps
|
|
|
21
21
|
* loses its full width.
|
|
22
22
|
*
|
|
23
23
|
* It is a variant rather than a `Section` component of its own, and that is a decision worth
|
|
24
|
-
* knowing. A chrome-less titled region is exactly this element with
|
|
24
|
+
* knowing. A chrome-less titled region is exactly this element with two declarations
|
|
25
25
|
* removed: `Card` already renders a `<section>` with an `<h3>` and stacks its children on
|
|
26
26
|
* the token scale. A second component would have meant six more markers describing the same
|
|
27
27
|
* DOM, and a `Surface` — the third name the diagnosis suggested — is a `Card` with no title,
|
|
@@ -32,7 +32,18 @@ export interface CardProps
|
|
|
32
32
|
title?: UiText;
|
|
33
33
|
/** Optional muted one-liner under the title (what this block is about). */
|
|
34
34
|
description?: UiText;
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Optional right-hand slot in the header row (filters, a legend, an action).
|
|
37
|
+
*
|
|
38
|
+
* It stays on the title's line whether or not there is a {@link CardProps.description}, and
|
|
39
|
+
* that took a rule rather than coming for free: the header wraps, and a heading block sized
|
|
40
|
+
* from its content is as wide as its longest line — a description sentence — so flex broke
|
|
41
|
+
* the line before it shrank anything and the slot landed underneath. Measured at a 103px
|
|
42
|
+
* header against 48px for the same component with the description removed. The heading's
|
|
43
|
+
* flex base is 0 now, so both fit on one line by construction, and the header aligns to
|
|
44
|
+
* `start` once a description is present so the control sits beside the title rather than
|
|
45
|
+
* floating in the middle of the block.
|
|
46
|
+
*/
|
|
36
47
|
actions?: ReactNode;
|
|
37
48
|
/** The rendered element — `"section"` by default (a titled block of a page). */
|
|
38
49
|
as?: "section" | "article" | "div" | "aside";
|
|
@@ -42,10 +53,11 @@ export interface CardProps
|
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
/**
|
|
45
|
-
* A token-styled
|
|
46
|
-
* sections visual separation (border
|
|
56
|
+
* A token-styled frame that groups one block of a page — the sanctioned way to give
|
|
57
|
+
* sections visual separation (a border, a radius and padding) without module CSS, and
|
|
47
58
|
* allowed directly in `OverviewPage` / `DetailPage` body slots under the `standard`
|
|
48
|
-
* layout contract.
|
|
59
|
+
* layout contract. It carries no fill: what shows through a card is the page's own
|
|
60
|
+
* canvas, so a card sits on a themed background instead of repainting it. An optional header row carries a semantic `<h3>` title, a muted
|
|
49
61
|
* description and an `actions` slot; the body stacks its children on the token
|
|
50
62
|
* spacing scale.
|
|
51
63
|
*/
|