@awarebydefault/display-case 1.0.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/LICENSE +21 -0
- package/README.md +309 -0
- package/display-case.prompt.md +64 -0
- package/docs/ai-agents.md +126 -0
- package/docs/cli.md +99 -0
- package/docs/configuration.md +410 -0
- package/docs/documentation-panel.md +50 -0
- package/docs/examples/README.md +14 -0
- package/docs/examples/multi-variant.case.tsx +30 -0
- package/docs/examples/plain.case.tsx +22 -0
- package/docs/examples/tweak-control.placard.md +80 -0
- package/docs/examples/tweaks.case.tsx +39 -0
- package/docs/hierarchy.md +59 -0
- package/docs/quick-start.md +78 -0
- package/docs/style-engines.md +180 -0
- package/docs/testing.md +245 -0
- package/docs/theming.md +97 -0
- package/docs/tweaks.md +75 -0
- package/docs/writing-cases.md +144 -0
- package/docs/writing-placard-docs.md +194 -0
- package/package.json +113 -0
- package/skills/display-case-author-case/README.md +20 -0
- package/skills/display-case-author-case/SKILL.md +40 -0
- package/skills/display-case-author-placard-doc/README.md +24 -0
- package/skills/display-case-author-placard-doc/SKILL.md +65 -0
- package/skills/display-case-review/README.md +19 -0
- package/skills/display-case-review/SKILL.md +30 -0
- package/skills/display-case-snapshot/README.md +20 -0
- package/skills/display-case-snapshot/SKILL.md +29 -0
- package/src/checks/a11y-scanner.test.ts +240 -0
- package/src/checks/a11y-scanner.ts +410 -0
- package/src/checks/check-text.test.ts +53 -0
- package/src/checks/check-text.ts +78 -0
- package/src/checks/check.test.ts +194 -0
- package/src/checks/check.ts +473 -0
- package/src/checks/providers/pixelmatch-diff.test.ts +79 -0
- package/src/checks/providers/pixelmatch-diff.ts +30 -0
- package/src/checks/providers/playwright-driver.ts +104 -0
- package/src/checks/ssr-check.test.ts +73 -0
- package/src/checks/ssr-check.ts +96 -0
- package/src/checks/structure-check.cross-package.test.ts +165 -0
- package/src/checks/structure-check.test.ts +651 -0
- package/src/checks/structure-check.ts +988 -0
- package/src/checks/tokens-check.test.ts +159 -0
- package/src/checks/tokens-check.ts +162 -0
- package/src/cli.ts +218 -0
- package/src/commands/agents.test.ts +24 -0
- package/src/commands/agents.ts +28 -0
- package/src/commands/init-run.test.ts +123 -0
- package/src/commands/init.test.ts +63 -0
- package/src/commands/init.ts +412 -0
- package/src/commands/publish.test.ts +210 -0
- package/src/commands/publish.ts +292 -0
- package/src/core/affected.test.ts +99 -0
- package/src/core/affected.ts +144 -0
- package/src/core/catalog.test.ts +152 -0
- package/src/core/catalog.ts +92 -0
- package/src/core/discovery.test.ts +184 -0
- package/src/core/discovery.ts +250 -0
- package/src/core/manifest.ts +41 -0
- package/src/core/mdx-lite/__fixtures__/box-stub.tsx +7 -0
- package/src/core/mdx-lite/index.ts +393 -0
- package/src/core/mdx-lite/mdx-lite.test.ts +345 -0
- package/src/core/mdx-plugin.test.ts +60 -0
- package/src/core/mdx-plugin.ts +30 -0
- package/src/flow-step.test-d.ts +39 -0
- package/src/index.test.ts +100 -0
- package/src/index.ts +564 -0
- package/src/render/collect-styles.emotion.test.tsx +114 -0
- package/src/render/collect-styles.test.tsx +72 -0
- package/src/render/collect-styles.ts +33 -0
- package/src/render/documents.test.ts +184 -0
- package/src/render/documents.ts +88 -0
- package/src/render/render-node.test.tsx +160 -0
- package/src/render/render-node.tsx +133 -0
- package/src/render/ssr-primer.test.tsx +25 -0
- package/src/render/ssr-primer.tsx +54 -0
- package/src/render/ssr-render.test.tsx +142 -0
- package/src/render/ssr-render.tsx +63 -0
- package/src/render/ssr-shell.test.tsx +57 -0
- package/src/render/ssr-shell.tsx +54 -0
- package/src/server/prod-server.ts +237 -0
- package/src/server/server.test.ts +117 -0
- package/src/server/server.ts +1039 -0
- package/src/style-engine.test-d.ts +37 -0
- package/src/testing/test-helpers.ts +27 -0
- package/src/types/pixelmatch.d.ts +12 -0
- package/src/ui/browser-entry.tsx +51 -0
- package/src/ui/chrome.css +485 -0
- package/src/ui/design-system/README.md +88 -0
- package/src/ui/design-system/components/controls/Button.case.tsx +52 -0
- package/src/ui/design-system/components/controls/Button.css +89 -0
- package/src/ui/design-system/components/controls/Button.placard.md +14 -0
- package/src/ui/design-system/components/controls/Button.test.tsx +45 -0
- package/src/ui/design-system/components/controls/Button.tsx +41 -0
- package/src/ui/design-system/components/controls/IconButton.case.tsx +52 -0
- package/src/ui/design-system/components/controls/IconButton.css +67 -0
- package/src/ui/design-system/components/controls/IconButton.placard.md +13 -0
- package/src/ui/design-system/components/controls/IconButton.test.tsx +39 -0
- package/src/ui/design-system/components/controls/IconButton.tsx +47 -0
- package/src/ui/design-system/components/controls/Input.case.tsx +50 -0
- package/src/ui/design-system/components/controls/Input.css +52 -0
- package/src/ui/design-system/components/controls/Input.placard.md +12 -0
- package/src/ui/design-system/components/controls/Input.test.tsx +43 -0
- package/src/ui/design-system/components/controls/Input.tsx +45 -0
- package/src/ui/design-system/components/controls/Select.case.tsx +48 -0
- package/src/ui/design-system/components/controls/Select.css +44 -0
- package/src/ui/design-system/components/controls/Select.placard.md +15 -0
- package/src/ui/design-system/components/controls/Select.test.tsx +57 -0
- package/src/ui/design-system/components/controls/Select.tsx +58 -0
- package/src/ui/design-system/components/controls/SelectMenu.case.tsx +100 -0
- package/src/ui/design-system/components/controls/SelectMenu.css +72 -0
- package/src/ui/design-system/components/controls/SelectMenu.placard.md +18 -0
- package/src/ui/design-system/components/controls/SelectMenu.test.tsx +66 -0
- package/src/ui/design-system/components/controls/SelectMenu.tsx +377 -0
- package/src/ui/design-system/components/index.ts +66 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.case.tsx +44 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.tsx +51 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.case.tsx +38 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.tsx +41 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.case.tsx +24 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.placard.md +12 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.tsx +41 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.case.tsx +27 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.placard.md +13 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.tsx +34 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.case.tsx +36 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.placard.md +7 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.tsx +36 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.case.tsx +20 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.placard.md +12 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.tsx +33 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.case.tsx +56 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.placard.md +17 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.tsx +45 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.case.tsx +17 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.placard.md +16 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.tsx +39 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.case.tsx +26 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.tsx +42 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.case.tsx +23 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.placard.md +14 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.tsx +34 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.case.tsx +28 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.tsx +46 -0
- package/src/ui/design-system/components/primer-specimen/index.ts +31 -0
- package/src/ui/design-system/components/primer-specimen/styles.css +476 -0
- package/src/ui/design-system/components/shell/A11yPage.case.tsx +237 -0
- package/src/ui/design-system/components/shell/A11yPage.placard.md +15 -0
- package/src/ui/design-system/components/shell/CaseTemplate.case.tsx +32 -0
- package/src/ui/design-system/components/shell/CaseTemplate.placard.md +5 -0
- package/src/ui/design-system/components/shell/CasesPage.case.tsx +141 -0
- package/src/ui/design-system/components/shell/CasesPage.placard.md +12 -0
- package/src/ui/design-system/components/shell/PrimerPage.case.tsx +22 -0
- package/src/ui/design-system/components/shell/PrimerPage.placard.md +3 -0
- package/src/ui/design-system/components/shell/PrimerTemplate.case.tsx +22 -0
- package/src/ui/design-system/components/shell/PrimerTemplate.placard.md +5 -0
- package/src/ui/design-system/components/shell/ShellView.case.tsx +57 -0
- package/src/ui/design-system/components/shell/ShellView.placard.md +5 -0
- package/src/ui/design-system/components/shell/ShellView.tsx +678 -0
- package/src/ui/design-system/components/shell/shell-fixtures.tsx +727 -0
- package/src/ui/design-system/components/showcase/A11yBadge.case.tsx +46 -0
- package/src/ui/design-system/components/showcase/A11yBadge.css +27 -0
- package/src/ui/design-system/components/showcase/A11yBadge.placard.md +11 -0
- package/src/ui/design-system/components/showcase/A11yBadge.test.tsx +31 -0
- package/src/ui/design-system/components/showcase/A11yBadge.tsx +41 -0
- package/src/ui/design-system/components/showcase/A11yPanel.case.tsx +121 -0
- package/src/ui/design-system/components/showcase/A11yPanel.css +198 -0
- package/src/ui/design-system/components/showcase/A11yPanel.placard.md +19 -0
- package/src/ui/design-system/components/showcase/A11yPanel.test.tsx +81 -0
- package/src/ui/design-system/components/showcase/A11yPanel.tsx +144 -0
- package/src/ui/design-system/components/showcase/Chip.case.tsx +48 -0
- package/src/ui/design-system/components/showcase/Chip.css +51 -0
- package/src/ui/design-system/components/showcase/Chip.placard.md +13 -0
- package/src/ui/design-system/components/showcase/Chip.test.tsx +46 -0
- package/src/ui/design-system/components/showcase/Chip.tsx +54 -0
- package/src/ui/design-system/components/showcase/Eyebrow.case.tsx +30 -0
- package/src/ui/design-system/components/showcase/Eyebrow.css +16 -0
- package/src/ui/design-system/components/showcase/Eyebrow.placard.md +10 -0
- package/src/ui/design-system/components/showcase/Eyebrow.test.tsx +38 -0
- package/src/ui/design-system/components/showcase/Eyebrow.tsx +29 -0
- package/src/ui/design-system/components/showcase/FlowNav.case.tsx +35 -0
- package/src/ui/design-system/components/showcase/FlowNav.css +29 -0
- package/src/ui/design-system/components/showcase/FlowNav.placard.md +13 -0
- package/src/ui/design-system/components/showcase/FlowNav.test.tsx +48 -0
- package/src/ui/design-system/components/showcase/FlowNav.tsx +58 -0
- package/src/ui/design-system/components/showcase/ImpactTag.case.tsx +19 -0
- package/src/ui/design-system/components/showcase/ImpactTag.css +36 -0
- package/src/ui/design-system/components/showcase/ImpactTag.placard.md +14 -0
- package/src/ui/design-system/components/showcase/ImpactTag.test.tsx +40 -0
- package/src/ui/design-system/components/showcase/ImpactTag.tsx +35 -0
- package/src/ui/design-system/components/showcase/NavItem.case.tsx +86 -0
- package/src/ui/design-system/components/showcase/NavItem.css +111 -0
- package/src/ui/design-system/components/showcase/NavItem.placard.md +13 -0
- package/src/ui/design-system/components/showcase/NavItem.test.tsx +65 -0
- package/src/ui/design-system/components/showcase/NavItem.tsx +95 -0
- package/src/ui/design-system/components/showcase/RenderAddress.case.tsx +21 -0
- package/src/ui/design-system/components/showcase/RenderAddress.css +35 -0
- package/src/ui/design-system/components/showcase/RenderAddress.placard.md +7 -0
- package/src/ui/design-system/components/showcase/RenderAddress.test.tsx +26 -0
- package/src/ui/design-system/components/showcase/RenderAddress.tsx +43 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.case.tsx +84 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.css +61 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.placard.md +21 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.test.tsx +81 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.tsx +75 -0
- package/src/ui/design-system/components/showcase/Sidebar.case.tsx +67 -0
- package/src/ui/design-system/components/showcase/Sidebar.css +6 -0
- package/src/ui/design-system/components/showcase/Sidebar.placard.md +14 -0
- package/src/ui/design-system/components/showcase/Sidebar.test.tsx +32 -0
- package/src/ui/design-system/components/showcase/Sidebar.tsx +30 -0
- package/src/ui/design-system/components/showcase/Stage.case.tsx +51 -0
- package/src/ui/design-system/components/showcase/Stage.css +91 -0
- package/src/ui/design-system/components/showcase/Stage.placard.md +15 -0
- package/src/ui/design-system/components/showcase/Stage.test.tsx +84 -0
- package/src/ui/design-system/components/showcase/Stage.tsx +97 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.case.tsx +81 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.css +169 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.placard.md +20 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.tsx +230 -0
- package/src/ui/design-system/components/showcase/Wordmark.case.tsx +42 -0
- package/src/ui/design-system/components/showcase/Wordmark.css +31 -0
- package/src/ui/design-system/components/showcase/Wordmark.placard.md +10 -0
- package/src/ui/design-system/components/showcase/Wordmark.test.tsx +22 -0
- package/src/ui/design-system/components/showcase/Wordmark.tsx +22 -0
- package/src/ui/design-system/primer-specimens/brand.tsx +26 -0
- package/src/ui/design-system/primer-specimens/colors.tsx +83 -0
- package/src/ui/design-system/primer-specimens/components.tsx +308 -0
- package/src/ui/design-system/primer-specimens/foundations.tsx +71 -0
- package/src/ui/design-system/primer-specimens/index.ts +25 -0
- package/src/ui/design-system/primer-specimens/showcase.tsx +68 -0
- package/src/ui/design-system/primer-specimens/spacing.tsx +101 -0
- package/src/ui/design-system/primer-specimens/type.tsx +75 -0
- package/src/ui/design-system/primer.mdx +236 -0
- package/src/ui/design-system/styles.css +14 -0
- package/src/ui/design-system/tokens/colors.css +172 -0
- package/src/ui/design-system/tokens/fonts.css +18 -0
- package/src/ui/design-system/tokens/spacing.css +48 -0
- package/src/ui/design-system/tokens/typography.css +49 -0
- package/src/ui/markdown.test.tsx +54 -0
- package/src/ui/markdown.tsx +19 -0
- package/src/ui/primer-mount.tsx +76 -0
- package/src/ui/primer.css +175 -0
- package/src/ui/primer.tsx +277 -0
- package/src/ui/render-mount.tsx +284 -0
- package/src/ui/shell-core.test.ts +340 -0
- package/src/ui/shell-core.ts +295 -0
- package/src/ui/shell.tsx +60 -0
- package/src/ui/test-ids.ts +53 -0
- package/src/ui/use-shell.ts +1230 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public authoring API for Display Case.
|
|
5
|
+
*
|
|
6
|
+
* Case files import only from here. Everything in this module is pure data +
|
|
7
|
+
* thin helpers — no DOM access, no server imports — so a case module is safe to
|
|
8
|
+
* import both in the browser bundle (to render) and in the Bun server process
|
|
9
|
+
* (to build the manifest, where render functions are never called).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── Hierarchy ───────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** Atomic Design levels, ordered by increasing composition. */
|
|
15
|
+
export const HIERARCHY_LEVELS = [
|
|
16
|
+
'atom',
|
|
17
|
+
'molecule',
|
|
18
|
+
'organism',
|
|
19
|
+
'template',
|
|
20
|
+
'page',
|
|
21
|
+
'flow',
|
|
22
|
+
] as const
|
|
23
|
+
|
|
24
|
+
export type HierarchyLevel = (typeof HIERARCHY_LEVELS)[number]
|
|
25
|
+
|
|
26
|
+
// ── Tweaks (typed controls) ───────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface TextTweak {
|
|
29
|
+
kind: 'text'
|
|
30
|
+
default: string
|
|
31
|
+
}
|
|
32
|
+
export interface BooleanTweak {
|
|
33
|
+
kind: 'boolean'
|
|
34
|
+
default: boolean
|
|
35
|
+
}
|
|
36
|
+
export interface NumberTweak {
|
|
37
|
+
kind: 'number'
|
|
38
|
+
default: number
|
|
39
|
+
}
|
|
40
|
+
export interface ChoiceTweak {
|
|
41
|
+
kind: 'choice'
|
|
42
|
+
options: string[]
|
|
43
|
+
default: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type TweakDescriptor =
|
|
47
|
+
| TextTweak
|
|
48
|
+
| BooleanTweak
|
|
49
|
+
| NumberTweak
|
|
50
|
+
| ChoiceTweak
|
|
51
|
+
|
|
52
|
+
export type TweakSchema = Record<string, TweakDescriptor>
|
|
53
|
+
|
|
54
|
+
/** Resolve a tweak schema to the value object handed to a render function. */
|
|
55
|
+
export type TweakValues<T extends TweakSchema> = {
|
|
56
|
+
[K in keyof T]: T[K] extends ChoiceTweak
|
|
57
|
+
? string
|
|
58
|
+
: T[K] extends TextTweak
|
|
59
|
+
? string
|
|
60
|
+
: T[K] extends NumberTweak
|
|
61
|
+
? number
|
|
62
|
+
: T[K] extends BooleanTweak
|
|
63
|
+
? boolean
|
|
64
|
+
: never
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Builders for the four serializable tweak kinds. */
|
|
68
|
+
export const tweak = {
|
|
69
|
+
text: (defaultValue = ''): TextTweak => ({
|
|
70
|
+
kind: 'text',
|
|
71
|
+
default: defaultValue,
|
|
72
|
+
}),
|
|
73
|
+
boolean: (defaultValue = false): BooleanTweak => ({
|
|
74
|
+
kind: 'boolean',
|
|
75
|
+
default: defaultValue,
|
|
76
|
+
}),
|
|
77
|
+
number: (defaultValue = 0): NumberTweak => ({
|
|
78
|
+
kind: 'number',
|
|
79
|
+
default: defaultValue,
|
|
80
|
+
}),
|
|
81
|
+
choice: <O extends string>(options: O[], defaultValue: O): ChoiceTweak => ({
|
|
82
|
+
kind: 'choice',
|
|
83
|
+
options,
|
|
84
|
+
default: defaultValue,
|
|
85
|
+
}),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Cases ─────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/** A case with no tweaks: a plain thunk returning the rendered variant. */
|
|
91
|
+
export type SimpleCase = () => ReactNode
|
|
92
|
+
|
|
93
|
+
/** A case with declared tweaks: receives the resolved tweak values. */
|
|
94
|
+
export interface TweakedCase<T extends TweakSchema = TweakSchema> {
|
|
95
|
+
tweaks: T
|
|
96
|
+
render: (values: TweakValues<T>) => ReactNode
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type Case = SimpleCase | TweakedCase
|
|
100
|
+
|
|
101
|
+
// ── Flows (interactive multi-step flows) ────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Advance the flow to another named step. Optional `overrides` re-enter the
|
|
105
|
+
* target step with specific tweak values (e.g. an error state); they are
|
|
106
|
+
* encoded into the step's address so the resulting state is reproducible.
|
|
107
|
+
*/
|
|
108
|
+
export type GotoFn = (
|
|
109
|
+
step: string,
|
|
110
|
+
overrides?: Record<string, string | number | boolean>,
|
|
111
|
+
) => void
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* One step of a flow. A step is a superset of a tweaked case: its `tweaks`
|
|
115
|
+
* defaults are the step's preset state, and `render` additionally receives a
|
|
116
|
+
* `goto` to wire into a presentational view's callbacks. `transitions` names the
|
|
117
|
+
* steps this step can advance to — the catalog's source of truth for the flow
|
|
118
|
+
* graph (kept declarative so the manifest builds without executing `render`).
|
|
119
|
+
*/
|
|
120
|
+
export interface FlowStep<T extends TweakSchema = TweakSchema> {
|
|
121
|
+
tweaks?: T
|
|
122
|
+
transitions?: string[]
|
|
123
|
+
render: (ctx: { values: TweakValues<T>; goto: GotoFn }) => ReactNode
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface CaseMeta {
|
|
127
|
+
level?: HierarchyLevel
|
|
128
|
+
/**
|
|
129
|
+
* Free-form area/layout tag passed to the decorator, for wrapping a case in
|
|
130
|
+
* app chrome (nav/header/footer). The decorator interprets the value (Display
|
|
131
|
+
* Case mandates no vocabulary). Takes precedence over folder-based detection
|
|
132
|
+
* via `sourcePath`; omit to fall back to that (or to render bare).
|
|
133
|
+
*/
|
|
134
|
+
area?: string
|
|
135
|
+
/**
|
|
136
|
+
* Declare this component's cases as browser-only: they need a browser to
|
|
137
|
+
* render (they touch `window`, layout measurement, canvas… *during render*,
|
|
138
|
+
* not just in effects) and so cannot be server-rendered before scripts run.
|
|
139
|
+
* Display Case renders them on the client instead — the same fallback a case
|
|
140
|
+
* that *throws* under server rendering gets — and the `ssr` check treats them
|
|
141
|
+
* as expected rather than a failure. Prefer keeping render pure (browser APIs
|
|
142
|
+
* belong in effects/handlers); use this only when a component genuinely can't.
|
|
143
|
+
*/
|
|
144
|
+
browserOnly?: boolean
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A discovered case module. This is the default-export shape every `*.case.tsx`
|
|
149
|
+
* file produces, and the unit the server reads to build the manifest.
|
|
150
|
+
*/
|
|
151
|
+
export interface CaseModule {
|
|
152
|
+
/** Display name of the showcased component. */
|
|
153
|
+
component: string
|
|
154
|
+
/** Place in the Atomic Design hierarchy; undeclared ⇒ "unclassified". */
|
|
155
|
+
level?: HierarchyLevel
|
|
156
|
+
/** Ordered cases (or flow steps), keyed by display name (insertion order preserved). */
|
|
157
|
+
cases: Record<string, Case | FlowStep>
|
|
158
|
+
/** True when this module is a flow: its cases are ordered, transitionable steps. */
|
|
159
|
+
isFlow: boolean
|
|
160
|
+
/** Source path relative to the package, injected by codegen (for area-aware chrome). */
|
|
161
|
+
sourcePath?: string
|
|
162
|
+
/** Free-form area/layout tag (see CaseMeta.area); overrides `sourcePath`. */
|
|
163
|
+
area?: string
|
|
164
|
+
/** Declared browser-only (see CaseMeta.browserOnly): skip server rendering and
|
|
165
|
+
* let the `ssr` check pass these cases instead of flagging them. */
|
|
166
|
+
browserOnly?: boolean
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Declare the cases for a single component.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* export default defineCases('Button', {
|
|
174
|
+
* Default: () => <Button>Save</Button>,
|
|
175
|
+
* }, { level: 'atom' })
|
|
176
|
+
*/
|
|
177
|
+
export function defineCases(
|
|
178
|
+
component: string,
|
|
179
|
+
cases: Record<string, Case>,
|
|
180
|
+
meta: CaseMeta = {},
|
|
181
|
+
): CaseModule {
|
|
182
|
+
return {
|
|
183
|
+
component,
|
|
184
|
+
cases,
|
|
185
|
+
level: meta.level,
|
|
186
|
+
isFlow: false,
|
|
187
|
+
area: meta.area,
|
|
188
|
+
browserOnly: meta.browserOnly,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Declare an interactive flow: an ordered set of named steps demonstrating a
|
|
194
|
+
* behavioural page or user flow. Each step is individually addressable and
|
|
195
|
+
* snapshottable; a step may declare preset `tweaks`, `transitions` to other
|
|
196
|
+
* steps, and wire its `goto` into a presentational view's callbacks. A flow
|
|
197
|
+
* whose steps declare no transitions is a static, walkable page sequence.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* export default defineFlow('Sign-in', {
|
|
201
|
+
* steps: {
|
|
202
|
+
* 'Request link': {
|
|
203
|
+
* render: ({ goto }) => <RequestLink onSubmit={() => goto('Check email')} />,
|
|
204
|
+
* },
|
|
205
|
+
* 'Check email': { render: () => <CheckEmail /> },
|
|
206
|
+
* },
|
|
207
|
+
* })
|
|
208
|
+
*/
|
|
209
|
+
export function defineFlow(
|
|
210
|
+
name: string,
|
|
211
|
+
// Steps may each declare a different tweak schema (e.g. via `flowStep`), so the
|
|
212
|
+
// record is over `FlowStep<any>` — a `FlowStep<{ error }>` is not assignable to
|
|
213
|
+
// the invariant `FlowStep<TweakSchema>`.
|
|
214
|
+
config: {
|
|
215
|
+
steps: Record<string, FlowStep<any>>
|
|
216
|
+
area?: string
|
|
217
|
+
browserOnly?: boolean
|
|
218
|
+
},
|
|
219
|
+
): CaseModule {
|
|
220
|
+
return {
|
|
221
|
+
component: name,
|
|
222
|
+
cases: config.steps,
|
|
223
|
+
level: 'flow',
|
|
224
|
+
isFlow: true,
|
|
225
|
+
area: config.area,
|
|
226
|
+
browserOnly: config.browserOnly,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Identity helper that infers a flow step's tweak schema from its own `tweaks`,
|
|
232
|
+
* so `render`'s `values` is typed per step (`values.error` is `boolean`, not a
|
|
233
|
+
* loose union). Wrap a step that reads typed `values`:
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* 'Check email': flowStep({
|
|
237
|
+
* tweaks: { error: tweak.boolean(false) },
|
|
238
|
+
* render: ({ values, goto }) => <CheckEmail error={values.error} … />,
|
|
239
|
+
* })
|
|
240
|
+
*
|
|
241
|
+
* A bare step object still works (its `values` is just loosely typed). `goto`
|
|
242
|
+
* and `transitions` targets are not key-checked at compile time — an unknown
|
|
243
|
+
* target renders the not-found step at runtime (see Case discovery).
|
|
244
|
+
*/
|
|
245
|
+
export function flowStep<T extends TweakSchema = Record<never, never>>(
|
|
246
|
+
step: FlowStep<T>,
|
|
247
|
+
): FlowStep<T> {
|
|
248
|
+
return step
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Snapshot providers (visual-regression backend) ──────────────────────────
|
|
252
|
+
|
|
253
|
+
/** Identity of the case being rendered, passed to snapshot providers. */
|
|
254
|
+
export interface CaseContext {
|
|
255
|
+
componentId: string
|
|
256
|
+
caseId: string
|
|
257
|
+
theme: 'light' | 'dark'
|
|
258
|
+
width: number
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** axe severity, worst → least: how seriously to take a violation. `null` when
|
|
262
|
+
* the driver doesn't classify it. */
|
|
263
|
+
export type A11yImpact = 'critical' | 'serious' | 'moderate' | 'minor'
|
|
264
|
+
|
|
265
|
+
export interface A11yViolation {
|
|
266
|
+
id: string
|
|
267
|
+
help: string
|
|
268
|
+
nodes: number
|
|
269
|
+
/** Severity, used to order results (worst first); `null` if unclassified. */
|
|
270
|
+
impact: A11yImpact | null
|
|
271
|
+
/** Per-node detail (the failing element and, for colour-contrast, the measured
|
|
272
|
+
* vs required values). Populated when the driver captures it; omitted by
|
|
273
|
+
* drivers that report only counts. Persisted in the on-disk cache and printed
|
|
274
|
+
* by the CLI so a finding is actionable without re-running — the live UI still
|
|
275
|
+
* shows only the summary above (`id`/`help`/`nodes`/`impact`). */
|
|
276
|
+
details?: A11yNodeDetail[]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** One failing node within an {@link A11yViolation}. */
|
|
280
|
+
export interface A11yNodeDetail {
|
|
281
|
+
/** CSS selector path to the failing element (axe's `target`, joined). */
|
|
282
|
+
target: string
|
|
283
|
+
/** Truncated `outerHTML` of the element, for identification. */
|
|
284
|
+
html: string
|
|
285
|
+
/** axe's human-readable explanation for this node, when available. */
|
|
286
|
+
failureSummary?: string
|
|
287
|
+
/** Present for `color-contrast` findings: the measured pair and threshold,
|
|
288
|
+
* so the exact failing colours are readable without opening a browser. */
|
|
289
|
+
contrast?: A11yContrast
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** The measured colour pair behind a `color-contrast` finding. */
|
|
293
|
+
export interface A11yContrast {
|
|
294
|
+
/** Foreground (text) colour, as axe reports it (e.g. `#8a8073`). */
|
|
295
|
+
foreground: string
|
|
296
|
+
/** Background colour behind the text. */
|
|
297
|
+
background: string
|
|
298
|
+
/** Measured contrast ratio (e.g. `3.71`). */
|
|
299
|
+
ratio: number
|
|
300
|
+
/** Ratio the text must meet to pass (`4.5` normal, `3` large). */
|
|
301
|
+
required: number
|
|
302
|
+
/** Computed font size axe used to pick the threshold (e.g. `12.0pt`). */
|
|
303
|
+
fontSize?: string
|
|
304
|
+
/** Computed font weight axe used to pick the threshold (e.g. `400`). */
|
|
305
|
+
fontWeight?: string
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Options for a single accessibility audit — shared by the CLI gate and the
|
|
309
|
+
* live in-app scanner so they agree on what counts as a violation. */
|
|
310
|
+
export interface AuditOptions {
|
|
311
|
+
/** Rule ids to exclude from the audit. */
|
|
312
|
+
exclude?: string[]
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** A page opened by a {@link RenderDriver}: capture an image, audit a11y. */
|
|
316
|
+
export interface RenderedPage {
|
|
317
|
+
screenshot(): Promise<Uint8Array>
|
|
318
|
+
/** Accessibility violations (WCAG A/AA); `[]` if the driver skips auditing. */
|
|
319
|
+
audit(opts?: AuditOptions): Promise<A11yViolation[]>
|
|
320
|
+
dispose(): Promise<void>
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Opens case render URLs and yields pages; reused across all cases. */
|
|
324
|
+
export interface RenderDriver {
|
|
325
|
+
open(url: string, ctx: CaseContext): Promise<RenderedPage>
|
|
326
|
+
close(): Promise<void>
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface DiffResult {
|
|
330
|
+
changed: boolean
|
|
331
|
+
mismatch?: number
|
|
332
|
+
/** Optional diff image the runner writes next to the baseline on a change. */
|
|
333
|
+
diffImage?: Uint8Array
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Compares a rendered case against its baseline. The second argument carries the
|
|
338
|
+
* case identity (Option B) — pure diffs ignore it; identity-aware ones (per-case
|
|
339
|
+
* tolerance, name-keyed hosted services) use it.
|
|
340
|
+
*/
|
|
341
|
+
export type DiffFn = (
|
|
342
|
+
input: { baseline: Uint8Array; actual: Uint8Array },
|
|
343
|
+
ctx: CaseContext & { baselinePath: string },
|
|
344
|
+
) => DiffResult | Promise<DiffResult>
|
|
345
|
+
|
|
346
|
+
export interface SnapshotProviders {
|
|
347
|
+
/** Render-driver factory; default = built-in Playwright + axe driver. */
|
|
348
|
+
driver?: () => RenderDriver | Promise<RenderDriver>
|
|
349
|
+
/** Image comparison; default = built-in pixelmatch/pngjs diff. */
|
|
350
|
+
diff?: DiffFn
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── Structure checks (static best-practice rules) ────────────────────────────
|
|
354
|
+
|
|
355
|
+
/** Identifier for each `--structure` best-practice rule. */
|
|
356
|
+
export type StructureRuleId =
|
|
357
|
+
// File / config rules
|
|
358
|
+
| 'case-placard-coverage'
|
|
359
|
+
| 'no-orphaned-placard-doc'
|
|
360
|
+
| 'primer-present-and-used'
|
|
361
|
+
| 'setup-present'
|
|
362
|
+
| 'config-paths-exist'
|
|
363
|
+
// Catalog-integrity rules
|
|
364
|
+
| 'levels-classified'
|
|
365
|
+
| 'cases-load'
|
|
366
|
+
| 'flow-transitions-resolve'
|
|
367
|
+
| 'flow-multi-step'
|
|
368
|
+
| 'unique-slugs'
|
|
369
|
+
| 'tweak-defaults-valid'
|
|
370
|
+
// Case-content rules
|
|
371
|
+
| 'interactive-cases-keyed'
|
|
372
|
+
// Composition (import-graph) rules — opt-in, default off
|
|
373
|
+
| 'atom-purity'
|
|
374
|
+
| 'no-downward-dependency'
|
|
375
|
+
| 'composes-lower-level'
|
|
376
|
+
| 'level-fit'
|
|
377
|
+
|
|
378
|
+
/** A finding either warns (reported, non-fatal) or errors (fails the run). */
|
|
379
|
+
export type StructureSeverity = 'warn' | 'error'
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Per-rule configuration:
|
|
383
|
+
* - `false` ⇒ disabled
|
|
384
|
+
* - `'warn'` / `'error'` ⇒ enabled, overriding the rule's default severity
|
|
385
|
+
* - an options object ⇒ enabled with per-rule overrides
|
|
386
|
+
*/
|
|
387
|
+
export type StructureRuleSetting =
|
|
388
|
+
| false
|
|
389
|
+
| StructureSeverity
|
|
390
|
+
| StructureRuleOptions
|
|
391
|
+
|
|
392
|
+
export interface StructureRuleOptions {
|
|
393
|
+
/** Override the rule's default severity. */
|
|
394
|
+
severity?: StructureSeverity
|
|
395
|
+
/** Package-relative globs whose matches this rule skips. */
|
|
396
|
+
ignore?: string[]
|
|
397
|
+
/**
|
|
398
|
+
* `level-fit` only: max lower-level components a level may compose before the
|
|
399
|
+
* rule suggests promotion. Unset levels use built-in defaults.
|
|
400
|
+
*/
|
|
401
|
+
thresholds?: Partial<Record<HierarchyLevel, number>>
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Phases selectable by `display-case check`. */
|
|
405
|
+
export type CheckPhase = 'tokens' | 'a11y' | 'visual' | 'structure' | 'ssr'
|
|
406
|
+
|
|
407
|
+
export interface CheckConfig {
|
|
408
|
+
/**
|
|
409
|
+
* Whether each phase participates in the default (no-phase-flag) run.
|
|
410
|
+
* Unset ⇒ included. Set `false` to opt a phase out of the default run; it can
|
|
411
|
+
* still be invoked explicitly by naming its flag.
|
|
412
|
+
*/
|
|
413
|
+
defaultPhases?: Partial<Record<CheckPhase, boolean>>
|
|
414
|
+
/** Structure-phase rule configuration; each rule is on (at its default severity) unless overridden. */
|
|
415
|
+
structure?: {
|
|
416
|
+
/** Treat every structure warning as an error for the run (CI strict mode). */
|
|
417
|
+
strict?: boolean
|
|
418
|
+
rules?: Partial<Record<StructureRuleId, StructureRuleSetting>>
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Style engines (render-time CSS-in-JS) ───────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Collects the styling a single server render emits and returns it as document
|
|
426
|
+
* `<head>` markup. One collector instance serves exactly one render, so its
|
|
427
|
+
* store is isolated — one case's render-time styling never leaks into another's
|
|
428
|
+
* document. See {@link StyleEngine}.
|
|
429
|
+
*/
|
|
430
|
+
export interface StyleCollector {
|
|
431
|
+
/**
|
|
432
|
+
* Wrap the tree about to be rendered in whatever provider the styling library
|
|
433
|
+
* needs, so its render-time styling accumulates in this collector's isolated
|
|
434
|
+
* store (e.g. an emotion `CacheProvider` over a fresh cache).
|
|
435
|
+
*/
|
|
436
|
+
wrap(node: ReactNode): ReactNode
|
|
437
|
+
/**
|
|
438
|
+
* Given the already-rendered markup, return the `<head>` markup (e.g.
|
|
439
|
+
* `<style data-…>…</style>` tags) carrying the styling that render used — placed
|
|
440
|
+
* verbatim, after the document's static styles, before scripting. Return `''`
|
|
441
|
+
* when the render produced none. MUST be idempotent: the tree renders inside
|
|
442
|
+
* `StrictMode` and may render twice.
|
|
443
|
+
*/
|
|
444
|
+
collect(renderedHtml: string): string
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* A factory invoked once per server render to produce an isolated
|
|
449
|
+
* {@link StyleCollector}. Configure one or more on
|
|
450
|
+
* {@link DisplayCaseConfig.styleEngines} to deliver render-time (CSS-in-JS)
|
|
451
|
+
* styling — emotion/Material UI, styled-components, and peers — before scripting.
|
|
452
|
+
* Pair with `decorator` for the client-side provider. See `docs/style-engines.md`.
|
|
453
|
+
*/
|
|
454
|
+
export type StyleEngine = () => StyleCollector
|
|
455
|
+
|
|
456
|
+
// ── Config ─────────────────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
export interface DisplayCaseConfig {
|
|
459
|
+
/** Title shown in the browsing chrome and the manifest. */
|
|
460
|
+
title: string
|
|
461
|
+
/** Globs (relative to the consumer package) that locate `*.case.tsx` files. */
|
|
462
|
+
roots: string[]
|
|
463
|
+
/**
|
|
464
|
+
* Path (relative to the consumer package) to an `.mdx` document rendered as
|
|
465
|
+
* the Primer — a long-form "wall text" reading page with embedded live
|
|
466
|
+
* specimens. The document may import any component (case files and arbitrary
|
|
467
|
+
* `.tsx`) and wraps each specimen in the `<Display>` contract (re-exported
|
|
468
|
+
* from this package). When set, the browse chrome shows a Primer / Cases
|
|
469
|
+
* mode switch in the sidebar.
|
|
470
|
+
*/
|
|
471
|
+
primer?: string
|
|
472
|
+
/**
|
|
473
|
+
* Which view the browse chrome lands on at the root path (`/`) when a Primer
|
|
474
|
+
* is configured: the Primer reading page (`'primer'`, the default) or the
|
|
475
|
+
* Cases library (`'cases'`). A deep link to a specific case always opens the
|
|
476
|
+
* library regardless. Ignored when no Primer is set — the library is then the
|
|
477
|
+
* only landing view.
|
|
478
|
+
*/
|
|
479
|
+
landing?: 'primer' | 'cases'
|
|
480
|
+
/** CSS entrypoints (relative to the consumer package) injected into previews. */
|
|
481
|
+
globalStyles?: string[]
|
|
482
|
+
/**
|
|
483
|
+
* Optional wrapper rendered around every case (e.g. a theme provider). Receives
|
|
484
|
+
* the active case's `level` and `sourcePath` so it can wrap page/flow cases in
|
|
485
|
+
* area-appropriate app chrome (nav/header) while leaving smaller components bare.
|
|
486
|
+
*/
|
|
487
|
+
decorator?: ComponentType<{
|
|
488
|
+
children: ReactNode
|
|
489
|
+
level?: HierarchyLevel
|
|
490
|
+
sourcePath?: string
|
|
491
|
+
area?: string
|
|
492
|
+
}>
|
|
493
|
+
/**
|
|
494
|
+
* Engines that collect render-time (CSS-in-JS) styling — emotion/Material UI,
|
|
495
|
+
* styled-components, and peers — during the pre-scripting server render and
|
|
496
|
+
* deliver it in the isolated render and primer documents before scripting, so
|
|
497
|
+
* those surfaces are styled without executing scripts (no flash, styled
|
|
498
|
+
* snapshots). Applied in array order (the first is outermost). Each is a
|
|
499
|
+
* factory invoked once per render for an isolated style store. Pair with
|
|
500
|
+
* `decorator` for the client-side provider. Omit for none (documents are then
|
|
501
|
+
* byte-identical to their engine-free form). See `docs/style-engines.md`.
|
|
502
|
+
*/
|
|
503
|
+
styleEngines?: StyleEngine[]
|
|
504
|
+
/**
|
|
505
|
+
* Where visual-regression baselines are stored, relative to the consumer
|
|
506
|
+
* package. Defaults to the gitignored cache at `.display-case/baselines`.
|
|
507
|
+
* Point at a committed directory to opt into shared / CI-gating baselines.
|
|
508
|
+
*/
|
|
509
|
+
baselineDir?: string
|
|
510
|
+
/** Design-token conformance options for the `--tokens` check. */
|
|
511
|
+
tokens?: {
|
|
512
|
+
/**
|
|
513
|
+
* Custom-property names the package may reference but does not itself
|
|
514
|
+
* define — e.g. tokens supplied by a host application's global stylesheet,
|
|
515
|
+
* or set by the browser. Listed names are treated as defined.
|
|
516
|
+
*/
|
|
517
|
+
allow?: string[]
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Override the visual-regression backend. When unset, the built-in default
|
|
521
|
+
* (Playwright + axe driver, pixelmatch/pngjs diff) is loaded lazily.
|
|
522
|
+
*/
|
|
523
|
+
providers?: SnapshotProviders
|
|
524
|
+
/**
|
|
525
|
+
* Check-command configuration: which phases run by default, and the structure
|
|
526
|
+
* phase's best-practice rules. Absent ⇒ every phase runs and every non-opt-in
|
|
527
|
+
* rule is enabled at its default severity.
|
|
528
|
+
*/
|
|
529
|
+
check?: CheckConfig
|
|
530
|
+
/**
|
|
531
|
+
* In-app accessibility surfacing on the running browse server. Off by default:
|
|
532
|
+
* the headless-browser + axe toolchain is an optional, lazily-loaded
|
|
533
|
+
* prerequisite, not an assumed dependency. `enabled` gates only the live
|
|
534
|
+
* surface (nav markers + Accessibility panel); the `check` CLI gate runs
|
|
535
|
+
* whenever invoked regardless of it. `themes` and `exclude` are shared scan
|
|
536
|
+
* parameters honored by BOTH the live surface and the CLI gate, so the panel
|
|
537
|
+
* and CI agree on what counts as a violation.
|
|
538
|
+
*/
|
|
539
|
+
a11y?: {
|
|
540
|
+
/** Surface accessibility results in the running browse chrome. Default false. */
|
|
541
|
+
enabled?: boolean
|
|
542
|
+
/** Themes to audit (live surface + CLI gate). Default: light and dark. */
|
|
543
|
+
themes?: ('light' | 'dark')[]
|
|
544
|
+
/** axe rule ids to exclude from audits (live surface + CLI gate). */
|
|
545
|
+
exclude?: string[]
|
|
546
|
+
/**
|
|
547
|
+
* How the navigation's accessibility markers are populated when the server
|
|
548
|
+
* starts (only meaningful with `enabled`). Default `'off'`.
|
|
549
|
+
* - `'off'` — no start-up population; a variant's marker appears only once
|
|
550
|
+
* that variant is viewed (the on-demand default).
|
|
551
|
+
* - `'cached'` — populate markers from reusable cached results at start-up,
|
|
552
|
+
* running no scans; uncached/stale variants stay unmarked
|
|
553
|
+
* until viewed.
|
|
554
|
+
* - `'refresh'`— at start-up scan every uncached or stale variant, surfacing
|
|
555
|
+
* each verdict as it lands, while reusing fresh cached results.
|
|
556
|
+
*/
|
|
557
|
+
startup?: 'off' | 'cached' | 'refresh'
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Identity helper that gives a config file full type-checking + inference. */
|
|
562
|
+
export function defineConfig(config: DisplayCaseConfig): DisplayCaseConfig {
|
|
563
|
+
return config
|
|
564
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'bun:test'
|
|
4
|
+
import createCache from '@emotion/cache'
|
|
5
|
+
import { CacheProvider, css } from '@emotion/react'
|
|
6
|
+
import createEmotionServer from '@emotion/server/create-instance'
|
|
7
|
+
import { type DisplayCaseConfig, defineCases, type StyleEngine } from '../index'
|
|
8
|
+
import { type DocAssets, renderDoc } from './documents'
|
|
9
|
+
import type { CaseTreeState } from './render-node'
|
|
10
|
+
import { makeCaseRenderer } from './ssr-render'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Real-library validation of the style-engine seam: the flagship emotion engine
|
|
14
|
+
* from `docs/style-engines.md`, verbatim, exercised through the actual case
|
|
15
|
+
* renderer and the production render document. This closes the server half of
|
|
16
|
+
* the spike (tasks 1.4 / 6.5) with `@emotion/react` + `@emotion/server` rather
|
|
17
|
+
* than a stub — proving render-time emotion styling is extracted and delivered
|
|
18
|
+
* in the document head before scripting. (Client adoption of the `data-emotion`
|
|
19
|
+
* tags is emotion's own runtime behavior, verified in a consuming repo.)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ── The flagship recipe, copied from docs/style-engines.md ──────────────────
|
|
23
|
+
const emotionEngine: StyleEngine = () => {
|
|
24
|
+
const cache = createCache({ key: 'css' })
|
|
25
|
+
cache.compat = true
|
|
26
|
+
const { extractCriticalToChunks, constructStyleTagsFromChunks } =
|
|
27
|
+
createEmotionServer(cache)
|
|
28
|
+
return {
|
|
29
|
+
wrap: (node) => <CacheProvider value={cache}>{node}</CacheProvider>,
|
|
30
|
+
collect: (html) =>
|
|
31
|
+
constructStyleTagsFromChunks(extractCriticalToChunks(html)),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const config: DisplayCaseConfig = {
|
|
36
|
+
title: 'T',
|
|
37
|
+
roots: [],
|
|
38
|
+
styleEngines: [emotionEngine],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const state = (over: Partial<CaseTreeState>): CaseTreeState => ({
|
|
42
|
+
componentId: 'box',
|
|
43
|
+
caseId: 'default',
|
|
44
|
+
width: null,
|
|
45
|
+
tweaks: {},
|
|
46
|
+
...over,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const assets: DocAssets = {
|
|
50
|
+
browser: '/b.js',
|
|
51
|
+
render: '/r.js',
|
|
52
|
+
primer: '/p.js',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A component styled by emotion at render time (the `css` prop).
|
|
56
|
+
const Hot = () => <div css={css({ color: 'rgb(12, 34, 56)' })}>hot</div>
|
|
57
|
+
const Cool = () => <div css={css({ color: 'rgb(98, 76, 54)' })}>cool</div>
|
|
58
|
+
|
|
59
|
+
describe('emotion style engine (real library)', () => {
|
|
60
|
+
test('extracts render-time emotion CSS into headStyles', () => {
|
|
61
|
+
const render = makeCaseRenderer(
|
|
62
|
+
[defineCases('Box', { Default: () => <Hot /> })],
|
|
63
|
+
config,
|
|
64
|
+
)
|
|
65
|
+
const result = render(state({}))
|
|
66
|
+
|
|
67
|
+
// The markup carries an emotion-generated class…
|
|
68
|
+
expect(result.html).toContain('hot')
|
|
69
|
+
expect(result.html).toContain('css-')
|
|
70
|
+
// …and the head styling carries real <style data-emotion> tags with the rule.
|
|
71
|
+
expect(result.headStyles).toContain('data-emotion')
|
|
72
|
+
expect(result.headStyles).toContain('rgb(12, 34, 56)')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('the extracted tags sit as a discrete block after the static <style>', () => {
|
|
76
|
+
const render = makeCaseRenderer(
|
|
77
|
+
[defineCases('Box', { Default: () => <Hot /> })],
|
|
78
|
+
config,
|
|
79
|
+
)
|
|
80
|
+
const result = render(state({}))
|
|
81
|
+
const html = renderDoc({
|
|
82
|
+
globalCss: '.g{}',
|
|
83
|
+
vitrineCss: '.v{}',
|
|
84
|
+
theme: 'light',
|
|
85
|
+
transparent: false,
|
|
86
|
+
fit: false,
|
|
87
|
+
markup: result.html,
|
|
88
|
+
ssr: true,
|
|
89
|
+
headStyles: result.headStyles,
|
|
90
|
+
assets,
|
|
91
|
+
})
|
|
92
|
+
// The real data-emotion tags land between the static block's close and </head>
|
|
93
|
+
// — verbatim, not folded into the base <style> (so client adoption works).
|
|
94
|
+
expect(html).toContain(`</style>${result.headStyles}</head>`)
|
|
95
|
+
expect(html).toContain('data-emotion')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('each render is isolated — one case never carries another’s emotion CSS', () => {
|
|
99
|
+
const render = makeCaseRenderer(
|
|
100
|
+
[
|
|
101
|
+
defineCases('Box', { Default: () => <Hot /> }),
|
|
102
|
+
defineCases('Chip', { Default: () => <Cool /> }),
|
|
103
|
+
],
|
|
104
|
+
config,
|
|
105
|
+
)
|
|
106
|
+
const hot = render(state({ componentId: 'box' }))
|
|
107
|
+
const cool = render(state({ componentId: 'chip' }))
|
|
108
|
+
|
|
109
|
+
expect(hot.headStyles).toContain('rgb(12, 34, 56)')
|
|
110
|
+
expect(hot.headStyles).not.toContain('rgb(98, 76, 54)')
|
|
111
|
+
expect(cool.headStyles).toContain('rgb(98, 76, 54)')
|
|
112
|
+
expect(cool.headStyles).not.toContain('rgb(12, 34, 56)')
|
|
113
|
+
})
|
|
114
|
+
})
|