@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.test.ts
CHANGED
|
@@ -6,6 +6,17 @@ import { TERP_STYLES_ID, TERP_STYLES_CSS, injectTerpStyles } from "./styles";
|
|
|
6
6
|
/** The sheet with comments removed — prose must not satisfy a structural assertion. */
|
|
7
7
|
const css = TERP_STYLES_CSS.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* One source file, read as text, so a union declared in TypeScript can be compared against the
|
|
11
|
+
* rules the sheet declares for it. Narrow on purpose: this file needs exactly one module, and a
|
|
12
|
+
* `./**` glob would pull the 180 KB stylesheet in a second time.
|
|
13
|
+
*/
|
|
14
|
+
const sources = import.meta.glob("./dataview/types.ts", {
|
|
15
|
+
query: "?raw",
|
|
16
|
+
import: "default",
|
|
17
|
+
eager: true,
|
|
18
|
+
}) as Record<string, string>;
|
|
19
|
+
|
|
9
20
|
/**
|
|
10
21
|
* The body of one `@layer <name> { … }` block, brace-matched.
|
|
11
22
|
*
|
|
@@ -70,6 +81,11 @@ describe("injectTerpStyles", () => {
|
|
|
70
81
|
expect(sheet).toContain("scrollbar-color: var(--color-neutral-300) transparent");
|
|
71
82
|
expect(sheet).toContain("::-webkit-scrollbar");
|
|
72
83
|
expect(sheet).toContain("::-webkit-scrollbar-thumb");
|
|
84
|
+
// And reserves the gutter, so the content box is the same width on a page that
|
|
85
|
+
// scrolls and a page that fits. The structural half; the workbench's computed lane
|
|
86
|
+
// measures the reserved width itself, which is the half that can tell whether the
|
|
87
|
+
// browser honoured it.
|
|
88
|
+
expect(sheet).toContain("scrollbar-gutter: stable");
|
|
73
89
|
});
|
|
74
90
|
});
|
|
75
91
|
|
|
@@ -204,10 +220,19 @@ describe("cascade structure", () => {
|
|
|
204
220
|
it("keeps the shell declarations no lane can reach", () => {
|
|
205
221
|
// Three groups, and every one of them was established by mutation rather than assumed.
|
|
206
222
|
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
223
|
+
// Two of the three have since GAINED a baseline, and the reason is worth keeping because
|
|
224
|
+
// the earlier version of this comment said flatly that the screenshot lane could not reach
|
|
225
|
+
// them: it could not, while the viewport was pinned at 1280 for every specimen. A
|
|
226
|
+
// per-specimen `viewport` in the workbench is what changed, and what these two assertions
|
|
227
|
+
// are now is belt rather than the sole gate.
|
|
228
|
+
//
|
|
229
|
+
// The sidebar's flex-shrink. At the pinned 1280 the row has room to spare, so deleting it
|
|
230
|
+
// moves no baseline — measured, and still true. It bites above the mobile breakpoint and
|
|
231
|
+
// below wide, and `app-shell-narrow` (820x900, with a DataView supplying the content
|
|
232
|
+
// pressure a short paragraph does not) is that band: removing the declaration now repaints
|
|
233
|
+
// 124,797 pixels there, in both themes, and leaves the other three shell specimens
|
|
234
|
+
// untouched. A row under no pressure never asks a flex item whether it may shrink, which
|
|
235
|
+
// is why the specimen needed the table and not just the narrower window.
|
|
211
236
|
const base = layerBody("terp.base");
|
|
212
237
|
const at = base.indexOf('[data-terp="appshell-sidebar"] {');
|
|
213
238
|
expect(at, "the sidebar should have a base rule").toBeGreaterThan(-1);
|
|
@@ -228,10 +253,65 @@ describe("cascade structure", () => {
|
|
|
228
253
|
`z-index: var(${token})`,
|
|
229
254
|
);
|
|
230
255
|
}
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
256
|
+
// The shell's three geometry literals, same shape and the same reason. `15rem`, `4rem` and
|
|
257
|
+
// `3rem` are now published tokens an app moves from its own unlayered `theme.css` with no
|
|
258
|
+
// prop at all (ADR 0097 §1), and restating any of them as a number here would move no
|
|
259
|
+
// baseline — the values are identical, which is what made the conversion provably
|
|
260
|
+
// zero-diff — while quietly taking the knob away again. `tokens.guard.test.ts` holds the
|
|
261
|
+
// other direction: a shell token nothing reads fails there.
|
|
262
|
+
for (const [declaration, what] of [
|
|
263
|
+
["width: var(--shell-sidebar-width-expanded)", "the expanded sidebar"],
|
|
264
|
+
["width: var(--shell-sidebar-width-collapsed)", "the collapsed icon rail"],
|
|
265
|
+
["min-height: var(--shell-header-height)", "the sticky header's floor"],
|
|
266
|
+
] as const) {
|
|
267
|
+
expect(base, `${what} must read its token rather than restating the length`).toContain(
|
|
268
|
+
declaration,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
// And the content measure, which no structural check can reach through a marker: it is the
|
|
272
|
+
// one shell rule keyed on a descendant of an attribute rather than on a marker of its own,
|
|
273
|
+
// because the mechanism is deliberately NOT a new element (ADR 0097 §2). The `:not(header)`
|
|
274
|
+
// is the whole band: the header keeps the page grid's full track while its siblings take
|
|
275
|
+
// the measure.
|
|
276
|
+
// Selector AND declaration read out of ONE rule body, not as two independent substrings
|
|
277
|
+
// of the layer. Asserted separately, an empty measure rule plus the declaration moved onto
|
|
278
|
+
// some other rule during a consolidation would satisfy both — and the only baseline that
|
|
279
|
+
// moved would read as an intentional layout change.
|
|
280
|
+
const measureRule =
|
|
281
|
+
/\[data-terp="appshell"\]\[data-content-width="measured"\]\s*\n?\s*\[data-terp="page"\] > \*:not\(\[data-terp="page-header"\]\) \{([^}]*)\}/.exec(
|
|
282
|
+
base,
|
|
283
|
+
);
|
|
284
|
+
expect(measureRule, "the content measure must be one rule keyed on the shell's attribute").not
|
|
285
|
+
.toBeNull();
|
|
286
|
+
const measureBody = measureRule![1]!;
|
|
287
|
+
// WIDTH, not max-width, and this assertion is the gate on that. The selector weighs
|
|
288
|
+
// (0,4,0), so as a max-width it outranks every component declaring a narrower one —
|
|
289
|
+
// resource-list, admin-form, dialog and both text measures — and an admin-form inside a
|
|
290
|
+
// measured shell computed 1280px instead of 512px. As a width it composes, because CSS
|
|
291
|
+
// resolves max-width after width. Nothing renders differently for a child with no measure
|
|
292
|
+
// of its own, which is why only a probe found it.
|
|
293
|
+
expect(
|
|
294
|
+
measureBody,
|
|
295
|
+
"the measure must be a width, or it outranks every component's own narrower max-width",
|
|
296
|
+
).toContain("width: min(100%, var(--shell-content-max-width))");
|
|
297
|
+
expect(
|
|
298
|
+
base.includes("max-width: var(--shell-content-max-width)"),
|
|
299
|
+
"as a max-width the measure would widen resource-list, admin-form, dialog and Text",
|
|
300
|
+
).toBe(false);
|
|
301
|
+
// And the mobile half of the shell, where the three selectors below now differ from each
|
|
302
|
+
// other and the difference is the interesting part. The BEHAVIOUR is covered throughout by
|
|
303
|
+
// AppShell.test.tsx (focus containment, inert page, close-on-nav) with a stubbed
|
|
304
|
+
// matchMedia; what varies is the geometry.
|
|
305
|
+
//
|
|
306
|
+
// `appshell-main`'s tightened padding is painted: `app-shell-mobile` renders at 420x900,
|
|
307
|
+
// and moving that padding one step to the desktop value repaints 1,309 pixels there in both
|
|
308
|
+
// themes and nothing else.
|
|
309
|
+
//
|
|
310
|
+
// The other two DID wait for a door, and it arrived: `defaultDrawerOpen` plus the
|
|
311
|
+
// `app-shell-drawer-open` specimen (420x900, overlay) paint the drawer's own geometry and
|
|
312
|
+
// its backdrop for the first time. So this loop is belt for all three now rather than the
|
|
313
|
+
// sole gate for two of them — the message below still says "no baseline can hold it", which
|
|
314
|
+
// was true when it was written and is not any more.
|
|
235
315
|
for (const selector of [
|
|
236
316
|
'[data-terp="appshell"][data-variant="mobile"] [data-terp="appshell-sidebar"]',
|
|
237
317
|
'[data-terp="appshell-backdrop"]',
|
|
@@ -239,7 +319,8 @@ describe("cascade structure", () => {
|
|
|
239
319
|
]) {
|
|
240
320
|
expect(
|
|
241
321
|
declaresRuleFor(base, selector),
|
|
242
|
-
`${selector} is mobile-only
|
|
322
|
+
`${selector} is mobile-only; app-shell-mobile and app-shell-drawer-open are the only ` +
|
|
323
|
+
"pictures of it",
|
|
243
324
|
).toBe(true);
|
|
244
325
|
}
|
|
245
326
|
});
|
|
@@ -443,10 +524,28 @@ describe("cascade structure", () => {
|
|
|
443
524
|
}
|
|
444
525
|
// Every transition this sheet declares is either on a marked element or on one of the
|
|
445
526
|
// descendant selectors above. A new descendant rule with a transition has to join them.
|
|
446
|
-
|
|
447
|
-
|
|
527
|
+
// BOTH layers. This read only terp.base, so a transition declared in terp.state — where
|
|
528
|
+
// every hover, focus and selected treatment lives — was audited by nothing at all. The one
|
|
529
|
+
// that exists there today passed only because its selector happens to be in the explicit
|
|
530
|
+
// list above, which is luck rather than coverage.
|
|
531
|
+
//
|
|
532
|
+
// And the "marked" heuristic needs the trailing pseudo stripped before it is applied.
|
|
533
|
+
// `\s[a-z]+$` asks whether the selector ends in a bare element name preceded by whitespace,
|
|
534
|
+
// but `[data-terp="card"] span:hover` ends in `:hover` — the character before the final run
|
|
535
|
+
// is a colon, not a space — so the test scored it as marked and waved it through. Reduced
|
|
536
|
+
// motion cannot reach that rule: terp.motion is the last layer and beats any earlier one on
|
|
537
|
+
// the elements its selectors MATCH, and its list matches marked elements and four named
|
|
538
|
+
// descendants. A transition on any other descendant simply escapes.
|
|
539
|
+
const transitionRules = [layerBody("terp.base"), layerBody("terp.state")].flatMap((body) => [
|
|
540
|
+
...body.matchAll(/([^{}]+)\{([^{}]*transition:[^{}]*)\}/g),
|
|
541
|
+
]);
|
|
542
|
+
expect(transitionRules.length, "no transition rules were scanned at all").toBeGreaterThan(0);
|
|
543
|
+
for (const match of transitionRules) {
|
|
448
544
|
const selector = match[1].trim().replace(/\s+/g, " ");
|
|
449
|
-
|
|
545
|
+
// Strip trailing pseudo-classes / pseudo-elements, so the bare-element test sees the
|
|
546
|
+
// element the selector actually ends on.
|
|
547
|
+
const withoutPseudo = selector.replace(/(::?[a-z-]+(\([^)]*\))?)+$/, "");
|
|
548
|
+
const marked = withoutPseudo.includes("[data-terp") && !/\s[a-z]+$/.test(withoutPseudo);
|
|
450
549
|
expect(
|
|
451
550
|
marked || motion.includes(selector),
|
|
452
551
|
`${selector} declares a transition but reduced motion cannot reach it`,
|
|
@@ -454,18 +553,270 @@ describe("cascade structure", () => {
|
|
|
454
553
|
}
|
|
455
554
|
});
|
|
456
555
|
|
|
556
|
+
it("times every transition off the published motion scale, never a literal", () => {
|
|
557
|
+
// The scale shipped in 2a and was read by nothing: this sheet wrote `150ms ease`
|
|
558
|
+
// 28 times and `100ms ease` once across 16 declarations, and read a motion token
|
|
559
|
+
// zero times — so a Studio editor built from the manifest would have offered four
|
|
560
|
+
// duration controls that moved nothing. Wiring them was inert by construction:
|
|
561
|
+
// --motion-duration-fast IS 150ms, --motion-duration-instant IS 100ms and
|
|
562
|
+
// --motion-easing-standard IS ease, so every literal mapped onto a token pair.
|
|
563
|
+
//
|
|
564
|
+
// This gate has to be structural, and that is the whole reason it exists here
|
|
565
|
+
// rather than in a baseline. The screenshot lane runs with
|
|
566
|
+
// `animations: "disabled"`, so a duration is invisible to it: a wrong value — or
|
|
567
|
+
// literal number 30 — would move no pixel and nothing would say so.
|
|
568
|
+
//
|
|
569
|
+
// Scoped to `transition` on purpose. The spinner's `animation: terp-spin 0.8s` is
|
|
570
|
+
// a rotation period rather than an interaction step and the scale tops out at
|
|
571
|
+
// 400ms, so there is no token for it to name.
|
|
572
|
+
const declarations = [...css.matchAll(/transition:\s*([^;]+);/g)].map((match) => match[1]!);
|
|
573
|
+
expect(declarations.length).toBeGreaterThan(10);
|
|
574
|
+
expect(
|
|
575
|
+
declarations.filter((value) => /\d+m?s\b/.test(value)),
|
|
576
|
+
"a transition's duration belongs on the published motion scale, not in the rule",
|
|
577
|
+
).toEqual([]);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("puts the responsive Stack rules after the ones they override", () => {
|
|
581
|
+
// A stack with direction { narrow: "row", wide: "column" } carries BOTH
|
|
582
|
+
// data-direction="row" and data-direction-wide="column", and the two selectors weigh the
|
|
583
|
+
// same (0,2,0) — so nothing but source order decides which applies above the cutover.
|
|
584
|
+
// Backwards, the narrow value renders at every width, which looks exactly like the prop
|
|
585
|
+
// not working rather than like a cascade mistake.
|
|
586
|
+
//
|
|
587
|
+
// The same tie the focus ring and the loading cursor both turned on, and the third time
|
|
588
|
+
// it has mattered in this sheet: equal specificity in one layer is settled by position,
|
|
589
|
+
// and position is the one thing a reader cannot see from the rule.
|
|
590
|
+
const base = layerBody("terp.base");
|
|
591
|
+
const wideAt = base.indexOf("@media not all and (max-width: 768px)");
|
|
592
|
+
expect(wideAt, "the wide half of the responsive props should be in terp.base").toBeGreaterThan(
|
|
593
|
+
-1,
|
|
594
|
+
);
|
|
595
|
+
for (const selector of [
|
|
596
|
+
'[data-terp="stack"][data-direction="row"]',
|
|
597
|
+
'[data-terp="stack"][data-gap="8"]',
|
|
598
|
+
]) {
|
|
599
|
+
const at = base.indexOf(selector);
|
|
600
|
+
expect(at, `${selector} should exist`).toBeGreaterThan(-1);
|
|
601
|
+
expect(at, `${selector} must be declared before the wide block that overrides it`).toBeLessThan(
|
|
602
|
+
wideAt,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("declares a wide-half gap rule for every step SpaceToken allows", () => {
|
|
608
|
+
// The narrow half is covered by the roll-call below; this is its counterpart. A responsive
|
|
609
|
+
// gap whose wide step has no rule silently renders the narrow gap at every width — which
|
|
610
|
+
// is the failure mode of the whole responsive idea, and invisible unless a specimen happens
|
|
611
|
+
// to be recorded at both viewports.
|
|
612
|
+
const base = layerBody("terp.base");
|
|
613
|
+
const wide = base.slice(base.indexOf("@media not all and (max-width: 768px)"));
|
|
614
|
+
for (const token of [0, 1, 2, 3, 4, 6, 8]) {
|
|
615
|
+
expect(
|
|
616
|
+
declaresRuleFor(wide, `[data-terp="stack"][data-gap-wide="${token}"]`),
|
|
617
|
+
`the wide half has no rule for gap ${token}`,
|
|
618
|
+
).toBe(true);
|
|
619
|
+
}
|
|
620
|
+
for (const direction of ["column", "row"]) {
|
|
621
|
+
expect(
|
|
622
|
+
declaresRuleFor(wide, `[data-terp="stack"][data-direction-wide="${direction}"]`),
|
|
623
|
+
`the wide half has no rule for direction ${direction}`,
|
|
624
|
+
).toBe(true);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("pins the four Grid track floors, which no baseline can hold", () => {
|
|
629
|
+
// The grid specimens gate the MECHANISM and not the values, and the gap between those two
|
|
630
|
+
// was measured rather than assumed. An auto-fit grid quantises: a floor only changes the
|
|
631
|
+
// rendered column count when it crosses a threshold for the container's width. At
|
|
632
|
+
// `grid-min-column`'s 66rem, moving the md floor 20rem -> 22rem drops it from three columns
|
|
633
|
+
// to two and repaints 63,524 pixels — while moving it 20rem -> 21rem changes **nothing**,
|
|
634
|
+
// and all ten grid baselines pass. So the pictures prove the attribute selects a different
|
|
635
|
+
// floor; only this proves which floor.
|
|
636
|
+
//
|
|
637
|
+
// The same shape as the shell declarations below, and the reason the values are pinned here
|
|
638
|
+
// rather than promoted to tokens: four published tokens with one consumer is the vocabulary
|
|
639
|
+
// the density cell tokens were deleted for. They become tokens the day an app asks.
|
|
640
|
+
const base = layerBody("terp.base");
|
|
641
|
+
for (const [attribute, floor] of [
|
|
642
|
+
[null, "16rem"],
|
|
643
|
+
["xs", "10rem"],
|
|
644
|
+
["md", "20rem"],
|
|
645
|
+
["lg", "26rem"],
|
|
646
|
+
] as const) {
|
|
647
|
+
const selector =
|
|
648
|
+
attribute === null
|
|
649
|
+
? '[data-terp="grid"]'
|
|
650
|
+
: `[data-terp="grid"][data-min-column="${attribute}"]`;
|
|
651
|
+
const at = base.indexOf(`${selector} {`);
|
|
652
|
+
expect(at, `${selector} should have a rule`).toBeGreaterThan(-1);
|
|
653
|
+
expect(
|
|
654
|
+
base.slice(at, base.indexOf("}", at)),
|
|
655
|
+
`${selector} should floor its tracks at ${floor}`,
|
|
656
|
+
).toContain(`minmax(min(${floor}, 100%), 1fr)`);
|
|
657
|
+
}
|
|
658
|
+
// sm is the default and therefore the base rule, so it has no attribute of its own — and
|
|
659
|
+
// its floor is the same 16rem the hub grid uses, which is why the two agree by construction
|
|
660
|
+
// instead of by coincidence.
|
|
661
|
+
expect(
|
|
662
|
+
declaresRuleFor(base, '[data-terp="grid"][data-min-column="sm"]'),
|
|
663
|
+
"sm is the base rule; an attribute rule for it means the default is described twice",
|
|
664
|
+
).toBe(false);
|
|
665
|
+
const hub = base.indexOf('[data-terp="hubpage-grid"] {');
|
|
666
|
+
expect(base.slice(hub, base.indexOf("}", hub))).toContain("minmax(min(16rem, 100%), 1fr)");
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("floors every fixed Grid column at zero, not at min-content", () => {
|
|
670
|
+
// `repeat(N, 1fr)` looks equivalent and is not: a bare 1fr floors at the track's
|
|
671
|
+
// min-content size, so one long unbroken word in a cell widens its column and the grid
|
|
672
|
+
// overflows its container. A two-column form of long field labels walks straight into it,
|
|
673
|
+
// which is the case Grid exists for. minmax(0, 1fr) is the fix, and it is invisible to
|
|
674
|
+
// every baseline that does not happen to contain an overflowing word.
|
|
675
|
+
const base = layerBody("terp.base");
|
|
676
|
+
for (const count of [1, 2, 3, 4]) {
|
|
677
|
+
const selector = `[data-terp="grid"][data-columns="${count}"]`;
|
|
678
|
+
const at = base.indexOf(`${selector} {`);
|
|
679
|
+
expect(at, `${selector} should have a rule`).toBeGreaterThan(-1);
|
|
680
|
+
expect(
|
|
681
|
+
base.slice(at, base.indexOf("}", at)),
|
|
682
|
+
`${selector} must floor its tracks at 0, or a long word widens the column`,
|
|
683
|
+
).toContain("minmax(0, 1fr)");
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("declares a rule for every size Button names, and none for its default", () => {
|
|
688
|
+
// The same bargain the gap rules strike: the union and the sheet are two lists kept by
|
|
689
|
+
// hand, so widening ButtonSize without adding a rule would silently fall back to the
|
|
690
|
+
// standard geometry rather than fail.
|
|
691
|
+
//
|
|
692
|
+
// And the converse, which is the part worth pinning: md must have NO rule of its own. Its
|
|
693
|
+
// geometry is the base rule, exactly as "comfortable" is the token sheet's :root value, and
|
|
694
|
+
// a data-size="md" rule appearing here would mean the component had started stamping the
|
|
695
|
+
// attribute for its default — leaving two places that describe the standard control.
|
|
696
|
+
const base = layerBody("terp.base");
|
|
697
|
+
for (const size of ["sm", "lg"]) {
|
|
698
|
+
expect(
|
|
699
|
+
declaresRuleFor(base, `[data-terp="button"][data-size="${size}"]`),
|
|
700
|
+
`Button has no rule for size ${size}`,
|
|
701
|
+
).toBe(true);
|
|
702
|
+
}
|
|
703
|
+
expect(
|
|
704
|
+
declaresRuleFor(base, '[data-terp="button"][data-size="md"]'),
|
|
705
|
+
"md is the base rule; a rule of its own means the default is described twice",
|
|
706
|
+
).toBe(false);
|
|
707
|
+
// Sizes read the density token rather than heights of their own, which is what makes the
|
|
708
|
+
// two dimensions compose: the compact re-scoping moves the token these calc() off.
|
|
709
|
+
const at = base.indexOf('[data-terp="button"][data-size="sm"]');
|
|
710
|
+
expect(
|
|
711
|
+
base.slice(at, base.indexOf("}", at)),
|
|
712
|
+
"a size that hardcodes its height stops following density",
|
|
713
|
+
).toContain("var(--density-control-min-height)");
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("declares one avatar rule plus a small variant, and nothing for the default size", () => {
|
|
717
|
+
// The tile used to be two rules of eleven declarations each — profile-avatar and
|
|
718
|
+
// user-menu-avatar — identical but for a width, a height and a font size. Merging them is only
|
|
719
|
+
// zero-pixel if the variant keeps BOTH geometries, so this pins the shape of the pair: a base
|
|
720
|
+
// rule carrying md, and an sm rule carrying exactly the three declarations that differ.
|
|
721
|
+
const base = layerBody("terp.base");
|
|
722
|
+
expect(declaresRuleFor(base, '[data-terp="avatar"]')).toBe(true);
|
|
723
|
+
expect(declaresRuleFor(base, '[data-terp="avatar"][data-size="sm"]')).toBe(true);
|
|
724
|
+
// md is the base rule, as with Button. A rule of its own means the default is described twice.
|
|
725
|
+
expect(
|
|
726
|
+
declaresRuleFor(base, '[data-terp="avatar"][data-size="md"]'),
|
|
727
|
+
"md is the base rule; a rule of its own means the default is described twice",
|
|
728
|
+
).toBe(false);
|
|
729
|
+
const at = base.indexOf('[data-terp="avatar"][data-size="sm"]');
|
|
730
|
+
const body = base.slice(at, base.indexOf("}", at));
|
|
731
|
+
for (const declaration of ["width: 2rem", "height: 2rem", "var(--font-size-sm)"]) {
|
|
732
|
+
expect(body, `the small variant must restate ${declaration}`).toContain(declaration);
|
|
733
|
+
}
|
|
734
|
+
// And nothing else: a declaration here that the base already sets is a second description of
|
|
735
|
+
// the same tile, which is the duplication the merge removed.
|
|
736
|
+
expect(body).not.toContain("border-radius");
|
|
737
|
+
expect(body).not.toContain("background");
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it("declares a min-inline-size for every step ColumnWidth names, and nothing it does not", () => {
|
|
741
|
+
// The Button gate above notes that its union and its sheet are two lists kept by hand. This
|
|
742
|
+
// one reads the union out of the source instead, because a declared track that silently does
|
|
743
|
+
// nothing is exactly the defect this attribute exists to end: `meta.width` was a px hint that
|
|
744
|
+
// `table-layout: auto` shrank away, so widening ColumnWidth without a rule would restore the
|
|
745
|
+
// old behaviour under a new spelling.
|
|
746
|
+
const source = sources["./dataview/types.ts"];
|
|
747
|
+
const union = /export type ColumnWidth =([^;]+);/.exec(source);
|
|
748
|
+
expect(union, "ColumnWidth is not declared where this test looks for it").not.toBeNull();
|
|
749
|
+
const steps = [...(union?.[1] ?? "").matchAll(/"([a-z]+)"/g)].map((match) => match[1]);
|
|
750
|
+
expect(steps.length, "parsed no steps out of ColumnWidth").toBeGreaterThan(0);
|
|
751
|
+
|
|
752
|
+
const base = layerBody("terp.base");
|
|
753
|
+
for (const step of steps) {
|
|
754
|
+
const selector = `[data-terp="dataview-table"] > thead > tr > th[data-width="${step}"]`;
|
|
755
|
+
expect(declaresRuleFor(base, selector), `no rule for step ${step}`).toBe(true);
|
|
756
|
+
const at = base.indexOf(selector);
|
|
757
|
+
expect(
|
|
758
|
+
base.slice(at, base.indexOf("}", at)),
|
|
759
|
+
`step ${step} must bind as a minimum; a width is a preference auto layout shrinks away`,
|
|
760
|
+
).toContain("min-inline-size");
|
|
761
|
+
}
|
|
762
|
+
// And nothing beyond the union. `lg` and `content` were both drafted and declined for having
|
|
763
|
+
// no consumer, so a rule for either means the sheet grew a step the type cannot express.
|
|
764
|
+
for (const absent of ["lg", "xl", "content", "auto"]) {
|
|
765
|
+
expect(
|
|
766
|
+
declaresRuleFor(
|
|
767
|
+
base,
|
|
768
|
+
`[data-terp="dataview-table"] > thead > tr > th[data-width="${absent}"]`,
|
|
769
|
+
),
|
|
770
|
+
`the sheet declares a step ColumnWidth does not name: ${absent}`,
|
|
771
|
+
).toBe(false);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("keeps the loading cursor where it can beat the disabled cursor", () => {
|
|
776
|
+
// A loading button is also `:disabled` — the component sets both — and the disabled rule
|
|
777
|
+
// lives in terp.state. So a `data-loading` rule in terp.base loses on layer order and the
|
|
778
|
+
// cursor silently stays `not-allowed`, which tells a user "you may not" where the truth is
|
|
779
|
+
// "not yet". It has to be in terp.state, and after the disabled rule, because the two weigh
|
|
780
|
+
// the same (0,2,0) and nothing but source order separates them.
|
|
781
|
+
//
|
|
782
|
+
// This is the focus-ring lesson in miniature, and it is invisible to every other lane:
|
|
783
|
+
// Playwright's screenshots do not paint a pointer, so no baseline has ever held either
|
|
784
|
+
// cursor. The computed lane asserts the resolved values; this asserts the structure that
|
|
785
|
+
// produces them.
|
|
786
|
+
const state = layerBody("terp.state");
|
|
787
|
+
const base = layerBody("terp.base");
|
|
788
|
+
expect(
|
|
789
|
+
declaresRuleFor(base, '[data-terp="button"][data-loading="true"]'),
|
|
790
|
+
"in terp.base this rule loses to the disabled cursor and does nothing",
|
|
791
|
+
).toBe(false);
|
|
792
|
+
const disabledAt = state.indexOf('[data-terp="button"]:disabled');
|
|
793
|
+
const loadingAt = state.indexOf('[data-terp="button"][data-loading="true"]');
|
|
794
|
+
expect(disabledAt, "the disabled rule should be in terp.state").toBeGreaterThan(-1);
|
|
795
|
+
expect(loadingAt, "the loading rule should be in terp.state").toBeGreaterThan(-1);
|
|
796
|
+
expect(loadingAt).toBeGreaterThan(disabledAt);
|
|
797
|
+
});
|
|
798
|
+
|
|
457
799
|
it("declares a gap rule for every step SpaceToken allows", () => {
|
|
458
800
|
// gap moved from a computed inline value to a rule per step, so the union and the sheet
|
|
459
801
|
// are now two lists maintained by hand. Widening SpaceToken without adding rules would
|
|
460
802
|
// silently fall back to the default gap rather than fail.
|
|
461
803
|
const base = layerBody("terp.base");
|
|
462
804
|
for (const token of [0, 1, 2, 3, 4, 6, 8]) {
|
|
463
|
-
for (const marker of ["stack", "card"]) {
|
|
805
|
+
for (const marker of ["stack", "card", "grid"]) {
|
|
464
806
|
expect(
|
|
465
807
|
declaresRuleFor(base, `[data-terp="${marker}"][data-gap="${token}"]`),
|
|
466
808
|
`${marker} has no rule for gap ${token}`,
|
|
467
809
|
).toBe(true);
|
|
468
810
|
}
|
|
811
|
+
// Padding is the same bargain on the same scale, and it is the dimension Stack did not
|
|
812
|
+
// have at all — a padded region used to be reachable only through a Card, which brought
|
|
813
|
+
// its border and background whether or not they were wanted.
|
|
814
|
+
for (const marker of ["stack", "grid"]) {
|
|
815
|
+
expect(
|
|
816
|
+
declaresRuleFor(base, `[data-terp="${marker}"][data-padding="${token}"]`),
|
|
817
|
+
`${marker} has no rule for padding ${token}`,
|
|
818
|
+
).toBe(true);
|
|
819
|
+
}
|
|
469
820
|
}
|
|
470
821
|
});
|
|
471
822
|
|
|
@@ -497,13 +848,14 @@ describe("cascade structure", () => {
|
|
|
497
848
|
// DataViewCardList's inline background on the element it matches. The escalation
|
|
498
849
|
// came off when the card migrated, which is the LAST of the two rather than the
|
|
499
850
|
// first — and the card's tone rules now lose to it on layer instead.
|
|
500
|
-
'[data-terp="dataview-card"]:focus-within',
|
|
851
|
+
'[data-terp="dataview-card"][data-clickable="true"]:focus-within',
|
|
501
852
|
// Re-derived rather than assumed, because the condition is not "has something
|
|
502
|
-
// migrated" but "can any element this selector matches still beat it". Of the
|
|
853
|
+
// migrated" but "can any element this selector matches still beat it". Of the SEVENTEEN
|
|
503
854
|
// sites wearing the iconbutton marker, only six can carry the disabled attribute at all:
|
|
504
855
|
// the four pagination arrows and the two reorder arrows. The shell's toggles, the toast
|
|
505
|
-
// dismisser, the combobox's clear button, the calendar's month arrows, the expand toggle
|
|
506
|
-
//
|
|
856
|
+
// dismisser, the combobox's clear button, the calendar's month arrows, the expand toggle,
|
|
857
|
+
// the toolbar's clear-search button and two layout toggles and the password reveal have no
|
|
858
|
+
// disabled state —
|
|
507
859
|
// and each of the six set cursor inline until it migrated. The day a calendar arrow gains
|
|
508
860
|
// a min/max bound, this answer changes back.
|
|
509
861
|
'[data-terp="iconbutton"]:disabled',
|
|
@@ -534,11 +886,13 @@ describe("cascade structure", () => {
|
|
|
534
886
|
// assertions because both had a real consumer until it migrated.
|
|
535
887
|
//
|
|
536
888
|
// 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
|
-
//
|
|
889
|
+
// selector matches, and there is now none anywhere in the package. ConfirmDialog's dialog
|
|
890
|
+
// and Popover's panel were the two that mattered and took their surface from the sheet in
|
|
891
|
+
// stage 4; the three that remained — AppShell's drawer, DataViewCardList's card and
|
|
892
|
+
// LoginView's card — went with those three components' own migrations, the last of them
|
|
893
|
+
// with LoginView. The DataView card is worth remembering anyway: it is a div with onClick
|
|
894
|
+
// and no tabIndex, so it cannot match :focus-visible at all, which is why the sheet reaches
|
|
895
|
+
// it with :focus-within.
|
|
542
896
|
// Measured after removing it: a keyboard-focused primary button and a focused menu item both
|
|
543
897
|
// still compute rgba(37,99,235,0.35) 0 0 0 3px, because the LAYER carries the ring, not the
|
|
544
898
|
// escalation. That was always the claim in this file's header; the escalation was belt.
|
|
@@ -704,10 +1058,51 @@ describe("cascade structure", () => {
|
|
|
704
1058
|
// check below only asks that the block is non-empty, which a block of dead declarations
|
|
705
1059
|
// satisfies. Prose rhythm belongs in descendant rules.
|
|
706
1060
|
const base = layerBody("terp.base");
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1061
|
+
// Anchored to the STANDALONE rule, not to the first mention of the marker. A plain indexOf
|
|
1062
|
+
// finds whichever selector merely contains it — and one now does: the measured-width
|
|
1063
|
+
// reach-through that caps this wrapper's blocks is declared earlier in the layer, so the
|
|
1064
|
+
// loose locator sliced that rule's body instead and reported a width where it expected
|
|
1065
|
+
// display: contents. The assertion was right and its aim was not.
|
|
1066
|
+
const rule = [...base.matchAll(/([^{}]+)\{([^{}]*)\}/g)].find(
|
|
1067
|
+
(match) => match[1]!.trim() === '[data-terp="markdown"]',
|
|
1068
|
+
);
|
|
1069
|
+
expect(rule, 'no standalone [data-terp="markdown"] rule in terp.base').toBeDefined();
|
|
1070
|
+
expect(rule![2]!.trim()).toBe("display: contents;");
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
it("pins the values of five declarations no lane can reach", () => {
|
|
1074
|
+
// Presence is not enough for these five, and that is the distinction the roll-calls above
|
|
1075
|
+
// already draw for Button sizes and gap steps: a rule can be present and WRONG, and nothing
|
|
1076
|
+
// here would notice. Each of these is reachable through a published prop, and none of them
|
|
1077
|
+
// is painted by any specimen — so today their values could be changed to anything and the
|
|
1078
|
+
// whole suite stays green.
|
|
1079
|
+
//
|
|
1080
|
+
// Two disabled paints, both reachable (Combobox and DatePicker each take `disabled`), and
|
|
1081
|
+
// neither rendered in that state by any specimen. Mutation: change either colour token.
|
|
1082
|
+
const state = layerBody("terp.state");
|
|
1083
|
+
const ruleFor = (selector: string) => {
|
|
1084
|
+
const match = [...state.matchAll(/([^{}]+)\{([^{}]*)\}/g)].find(
|
|
1085
|
+
(rule) => rule[1]!.trim().replace(/\s+/g, " ") === selector,
|
|
1086
|
+
);
|
|
1087
|
+
expect(match, `${selector} has no rule of its own`).toBeDefined();
|
|
1088
|
+
return match![2]!;
|
|
1089
|
+
};
|
|
1090
|
+
expect(ruleFor('[data-terp="combobox-option"]:disabled')).toContain(
|
|
1091
|
+
"color: var(--color-neutral-400)",
|
|
1092
|
+
);
|
|
1093
|
+
expect(ruleFor('[data-terp="calendar-day"]:disabled')).toContain(
|
|
1094
|
+
"color: var(--color-neutral-300)",
|
|
1095
|
+
);
|
|
1096
|
+
|
|
1097
|
+
// The split archetype's two outer list widths. `md` is painted by the split-page specimen;
|
|
1098
|
+
// `sm` and `lg` are painted by nothing, and they live inside a wide-viewport media block the
|
|
1099
|
+
// pinned 1280 does reach — so the rules apply and simply have no picture. A renamed
|
|
1100
|
+
// attribute value would silently stop matching. Mutation: rename "sm" to "small", or change
|
|
1101
|
+
// either track measure.
|
|
1102
|
+
const wide = css.slice(css.indexOf('[data-terp="splitpage-panes"][data-list-width="sm"]'));
|
|
1103
|
+
expect(wide.slice(0, 120)).toContain("minmax(0, 18rem) minmax(0, 1fr)");
|
|
1104
|
+
const lg = css.slice(css.indexOf('[data-terp="splitpage-panes"][data-list-width="lg"]'));
|
|
1105
|
+
expect(lg.slice(0, 120)).toContain("minmax(0, 32rem) minmax(0, 1fr)");
|
|
711
1106
|
});
|
|
712
1107
|
|
|
713
1108
|
it("gives every migrated component a base rule in terp.base", () => {
|
|
@@ -718,16 +1113,21 @@ describe("cascade structure", () => {
|
|
|
718
1113
|
const base = layerBody("terp.base");
|
|
719
1114
|
for (const marker of [
|
|
720
1115
|
"button",
|
|
1116
|
+
"avatar",
|
|
721
1117
|
"badge",
|
|
722
1118
|
"alert",
|
|
723
1119
|
"tooltip",
|
|
724
1120
|
"input",
|
|
1121
|
+
"input-password",
|
|
725
1122
|
"field",
|
|
726
1123
|
"control-label",
|
|
727
1124
|
"checkbox",
|
|
728
1125
|
"radio",
|
|
729
1126
|
"switch",
|
|
730
1127
|
"stack",
|
|
1128
|
+
"appshell-skip-link",
|
|
1129
|
+
"splitpage-panes",
|
|
1130
|
+
"splitpane",
|
|
731
1131
|
"detail-list",
|
|
732
1132
|
"combobox",
|
|
733
1133
|
"combobox-list",
|
|
@@ -775,7 +1175,6 @@ describe("cascade structure", () => {
|
|
|
775
1175
|
"dialog-description",
|
|
776
1176
|
"dialog-actions",
|
|
777
1177
|
"user-menu",
|
|
778
|
-
"user-menu-avatar",
|
|
779
1178
|
"user-menu-email",
|
|
780
1179
|
"user-menu-header",
|
|
781
1180
|
"user-menu-identity",
|
|
@@ -837,6 +1236,98 @@ describe("cascade structure", () => {
|
|
|
837
1236
|
"appshell-header-group",
|
|
838
1237
|
"appshell-main",
|
|
839
1238
|
"appshell-footer",
|
|
1239
|
+
"page",
|
|
1240
|
+
"page-header",
|
|
1241
|
+
"page-breadcrumbs",
|
|
1242
|
+
"page-heading",
|
|
1243
|
+
"page-title",
|
|
1244
|
+
"resource-list",
|
|
1245
|
+
"resource-list-create",
|
|
1246
|
+
"resource-list-error",
|
|
1247
|
+
"resource-list-empty",
|
|
1248
|
+
"resource-list-items",
|
|
1249
|
+
"resource-list-row",
|
|
1250
|
+
"module-nav",
|
|
1251
|
+
"module-nav-list",
|
|
1252
|
+
"module-nav-link",
|
|
1253
|
+
"profile-card",
|
|
1254
|
+
"profile-email",
|
|
1255
|
+
"profile-role",
|
|
1256
|
+
"login-view",
|
|
1257
|
+
"login-card",
|
|
1258
|
+
"login-brand",
|
|
1259
|
+
"login-title",
|
|
1260
|
+
"login-form",
|
|
1261
|
+
"login-sso",
|
|
1262
|
+
"login-separator",
|
|
1263
|
+
"login-separator-rule",
|
|
1264
|
+
"login-error",
|
|
1265
|
+
"admin-form",
|
|
1266
|
+
"admin-section-title",
|
|
1267
|
+
"grid",
|
|
1268
|
+
"divider",
|
|
1269
|
+
"heading",
|
|
1270
|
+
"text",
|
|
1271
|
+
"code",
|
|
1272
|
+
"code-block",
|
|
1273
|
+
"link",
|
|
1274
|
+
"detail-list-row",
|
|
1275
|
+
// `appshell-nav-group-label` is here; `appshell-nav-group` deliberately is NOT, and the
|
|
1276
|
+
// omission joins `appshell-mark` and `appshell-skip-link` above rather than starting a
|
|
1277
|
+
// new kind of exception. The wrapper declares nothing on its own — in the sidebar it is a
|
|
1278
|
+
// plain block whose whole job is to be a sibling, so its two rules are a `+` pair and a
|
|
1279
|
+
// placement override, and it has no standalone selector to match. `declaresRuleFor` wants
|
|
1280
|
+
// exact selector equality, which is what makes it worth having: adding the family pattern
|
|
1281
|
+
// here to make the list look complete would fail, correctly.
|
|
1282
|
+
"appshell-nav-group-label",
|
|
1283
|
+
// The 32 below close a gap the roll-call carried quietly: it listed barely more than
|
|
1284
|
+
// three quarters of the pinned markers, so a quarter of the sheet's base blocks could be
|
|
1285
|
+
// DELETED and only the screenshot lane would notice — and only for a marker some specimen
|
|
1286
|
+
// happens to paint. (No figures here on purpose: both sides of that ratio have moved twice
|
|
1287
|
+
// since it was written, and a citation keeps where a tally rots.)
|
|
1288
|
+
// markers.test.ts cannot see a deleted rule either: removing a block only shrinks the
|
|
1289
|
+
// styled set, which still satisfies both of its directions.
|
|
1290
|
+
//
|
|
1291
|
+
// Seven pinned markers are deliberately NOT here, because they have no standalone base
|
|
1292
|
+
// rule to assert. appshell-nav-group declares nothing on its own (it is a
|
|
1293
|
+
// sibling-combinator pair plus a placement override); dataview-row, dataview-select-cell,
|
|
1294
|
+
// dataview-expand-cell and dataview-actions-cell are styled through their descendants
|
|
1295
|
+
// and attribute variants; and drawer-focus-start / drawer-focus-end appear only in the
|
|
1296
|
+
// shared visually-hidden selector list. Adding any of them here fails, correctly —
|
|
1297
|
+
// declaresRuleFor wants exact selector equality, which is what makes this list mean
|
|
1298
|
+
// something.
|
|
1299
|
+
"alert-body",
|
|
1300
|
+
"alert-icon",
|
|
1301
|
+
"alert-title",
|
|
1302
|
+
"appshell-mark",
|
|
1303
|
+
"breadcrumbs-separator",
|
|
1304
|
+
"button-icon",
|
|
1305
|
+
"calendar-header",
|
|
1306
|
+
"calendar-title",
|
|
1307
|
+
"calendar-weekday",
|
|
1308
|
+
"card-actions",
|
|
1309
|
+
"card-description",
|
|
1310
|
+
"card-heading",
|
|
1311
|
+
"combobox-empty",
|
|
1312
|
+
"combobox-field",
|
|
1313
|
+
"detail-list-term",
|
|
1314
|
+
"detail-list-value",
|
|
1315
|
+
"empty-state-description",
|
|
1316
|
+
"empty-state-icon",
|
|
1317
|
+
"empty-state-title",
|
|
1318
|
+
"error-state-description",
|
|
1319
|
+
"error-state-icon",
|
|
1320
|
+
"error-state-title",
|
|
1321
|
+
"field-error",
|
|
1322
|
+
"field-hint",
|
|
1323
|
+
"field-label",
|
|
1324
|
+
"field-label-text",
|
|
1325
|
+
"iconbutton",
|
|
1326
|
+
"loading-state-spinner",
|
|
1327
|
+
"radio-group",
|
|
1328
|
+
"radio-group-legend",
|
|
1329
|
+
"radio-group-options",
|
|
1330
|
+
"tooltip-anchor",
|
|
840
1331
|
]) {
|
|
841
1332
|
expect(
|
|
842
1333
|
declaresRuleFor(base, `[data-terp="${marker}"]`),
|