@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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type A11yImpact, defineCases } from '@awarebydefault/display-case'
|
|
2
|
+
import { ImpactTag } from './ImpactTag'
|
|
3
|
+
|
|
4
|
+
const IMPACTS: A11yImpact[] = ['critical', 'serious', 'moderate', 'minor']
|
|
5
|
+
|
|
6
|
+
export default defineCases(
|
|
7
|
+
'ImpactTag',
|
|
8
|
+
{
|
|
9
|
+
// The full severity scale, worst → least (the order a sorted list shows).
|
|
10
|
+
'All severities': () => (
|
|
11
|
+
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
|
12
|
+
{IMPACTS.map((impact) => (
|
|
13
|
+
<ImpactTag key={impact} impact={impact} />
|
|
14
|
+
))}
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
},
|
|
18
|
+
{ level: 'atom' },
|
|
19
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
.dcui-impact-tag {
|
|
2
|
+
flex: 0 0 auto;
|
|
3
|
+
align-self: center;
|
|
4
|
+
min-width: 4.2rem;
|
|
5
|
+
text-align: center;
|
|
6
|
+
padding: 0 var(--dc-space-2);
|
|
7
|
+
border-radius: var(--dc-radius-sm);
|
|
8
|
+
font-family: var(--dc-font-mono);
|
|
9
|
+
font-size: var(--dc-text-2xs);
|
|
10
|
+
font-weight: var(--dc-weight-medium);
|
|
11
|
+
text-transform: uppercase;
|
|
12
|
+
letter-spacing: 0.04em;
|
|
13
|
+
/* Each severity carries its own text colour so the fill+text pair clears AA
|
|
14
|
+
theme-independently — a single --dc-brand-fg (white in light, ink in dark)
|
|
15
|
+
could not, since the fills span a light amber to a dark red. The fills are
|
|
16
|
+
the fixed ramp hues (not theme tokens), so the gradient — critical hottest
|
|
17
|
+
→ minor coolest — and the contrast both hold in light and dark alike. */
|
|
18
|
+
color: #ffffff;
|
|
19
|
+
background: var(--dc-paper-700);
|
|
20
|
+
}
|
|
21
|
+
.dcui-impact-tag[data-impact="critical"] {
|
|
22
|
+
background: var(--dc-red-700);
|
|
23
|
+
color: #ffffff;
|
|
24
|
+
}
|
|
25
|
+
.dcui-impact-tag[data-impact="serious"] {
|
|
26
|
+
background: var(--dc-red-600);
|
|
27
|
+
color: #ffffff;
|
|
28
|
+
}
|
|
29
|
+
.dcui-impact-tag[data-impact="moderate"] {
|
|
30
|
+
background: var(--dc-amber-500);
|
|
31
|
+
color: var(--dc-paper-900);
|
|
32
|
+
}
|
|
33
|
+
.dcui-impact-tag[data-impact="minor"] {
|
|
34
|
+
background: var(--dc-paper-700);
|
|
35
|
+
color: #ffffff;
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
**ImpactTag** — a severity tag for an accessibility violation, colour-graded by axe `impact` so the worst findings read hottest. Used in the [A11yPanel](./A11yPanel.placard.md) violation list.
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<ImpactTag impact="critical" />
|
|
5
|
+
<ImpactTag impact="serious" />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
`impact` is one of `critical` · `serious` · `moderate` · `minor` (the `A11yImpact` type). critical is the deepest red, minor the calmest.
|
|
9
|
+
|
|
10
|
+
The companion `impactRank(impact)` returns a sort key (worst = 0, unclassified last) — sort a violation list with it before mapping each to a tag:
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
[...violations].sort((a, b) => impactRank(a.impact) - impactRank(b.impact))
|
|
14
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { ImpactTag, impactRank } from './ImpactTag'
|
|
4
|
+
|
|
5
|
+
describe('impactRank', () => {
|
|
6
|
+
test('orders the impacts worst-first', () => {
|
|
7
|
+
expect(impactRank('critical')).toBeLessThan(impactRank('serious'))
|
|
8
|
+
expect(impactRank('serious')).toBeLessThan(impactRank('moderate'))
|
|
9
|
+
expect(impactRank('moderate')).toBeLessThan(impactRank('minor'))
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('sorts an unclassified (null) impact last', () => {
|
|
13
|
+
expect(impactRank(null)).toBeGreaterThan(impactRank('minor'))
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('is usable as an Array.sort comparator', () => {
|
|
17
|
+
const sorted = ['minor', 'critical', 'moderate', 'serious'].sort(
|
|
18
|
+
(a, b) => impactRank(a as never) - impactRank(b as never),
|
|
19
|
+
)
|
|
20
|
+
expect(sorted).toEqual(['critical', 'serious', 'moderate', 'minor'])
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('ImpactTag', () => {
|
|
25
|
+
test('labels the tag with the impact and grades it via data-impact', () => {
|
|
26
|
+
const html = renderToStaticMarkup(<ImpactTag impact="critical" />)
|
|
27
|
+
expect(html).toContain('data-impact="critical"')
|
|
28
|
+
expect(html).toContain('title="Severity: critical"')
|
|
29
|
+
expect(html).toContain('>critical<')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('reflects each severity level', () => {
|
|
33
|
+
expect(renderToStaticMarkup(<ImpactTag impact="minor" />)).toContain(
|
|
34
|
+
'data-impact="minor"',
|
|
35
|
+
)
|
|
36
|
+
expect(renderToStaticMarkup(<ImpactTag impact="moderate" />)).toContain(
|
|
37
|
+
'data-impact="moderate"',
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { A11yImpact } from '../../../../index'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Display Case — ImpactTag
|
|
5
|
+
* A severity tag for an accessibility violation, colour-graded by axe impact
|
|
6
|
+
* (critical → minor) so the worst findings read hottest. Used in the
|
|
7
|
+
* {@link A11yPanel} violation list; `impactRank` orders a list worst-first.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Worst → least, so a list sorts top-down. Unclassified (null) sorts last.
|
|
11
|
+
const RANK: Record<A11yImpact, number> = {
|
|
12
|
+
critical: 0,
|
|
13
|
+
serious: 1,
|
|
14
|
+
moderate: 2,
|
|
15
|
+
minor: 3,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Sort key for an impact (worst first); `null`/unclassified sorts last. */
|
|
19
|
+
export const impactRank = (impact: A11yImpact | null): number =>
|
|
20
|
+
impact ? RANK[impact] : 4
|
|
21
|
+
|
|
22
|
+
export interface ImpactTagProps {
|
|
23
|
+
impact: A11yImpact
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ImpactTag({ impact }: ImpactTagProps) {
|
|
27
|
+
return (
|
|
28
|
+
<span
|
|
29
|
+
className="dcui-impact-tag"
|
|
30
|
+
data-impact={impact}
|
|
31
|
+
title={`Severity: ${impact}`}>
|
|
32
|
+
{impact}
|
|
33
|
+
</span>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { defineCases, tweak } from '@awarebydefault/display-case'
|
|
2
|
+
import { NavItem } from './NavItem'
|
|
3
|
+
import { Sidebar } from './Sidebar'
|
|
4
|
+
|
|
5
|
+
export default defineCases(
|
|
6
|
+
'NavItem',
|
|
7
|
+
{
|
|
8
|
+
// Wrapped in Sidebar — NavItem rows are transparent and only read correctly
|
|
9
|
+
// against the sidebar's `--dc-bg-subtle` surface.
|
|
10
|
+
Playground: {
|
|
11
|
+
tweaks: {
|
|
12
|
+
kind: tweak.choice(['component', 'case'], 'component'),
|
|
13
|
+
label: tweak.text('Button'),
|
|
14
|
+
withCount: tweak.boolean(true),
|
|
15
|
+
count: tweak.number(4),
|
|
16
|
+
alert: tweak.number(0),
|
|
17
|
+
current: tweak.boolean(false),
|
|
18
|
+
expanded: tweak.boolean(false),
|
|
19
|
+
},
|
|
20
|
+
render: (t) => (
|
|
21
|
+
<Sidebar style={{ width: '15rem' }}>
|
|
22
|
+
<NavItem
|
|
23
|
+
kind={t.kind as 'component' | 'case'}
|
|
24
|
+
label={t.label}
|
|
25
|
+
count={t.withCount ? t.count : undefined}
|
|
26
|
+
alert={t.alert}
|
|
27
|
+
current={t.current}
|
|
28
|
+
expanded={t.expanded}
|
|
29
|
+
onToggle={() => {}}
|
|
30
|
+
onSelect={() => {}}
|
|
31
|
+
/>
|
|
32
|
+
</Sidebar>
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
// Accessibility-violation markers. Collapsed, a component shows the summed
|
|
36
|
+
// count (the danger pill). Expanded, the parent shows a plain dot while the
|
|
37
|
+
// per-variant counts move onto the case rows. See the "A11y page" exhibit
|
|
38
|
+
// for the markers in the full chrome.
|
|
39
|
+
Alert: () => (
|
|
40
|
+
<Sidebar style={{ width: '15rem' }}>
|
|
41
|
+
<NavItem
|
|
42
|
+
kind="component"
|
|
43
|
+
label="Input"
|
|
44
|
+
count={2}
|
|
45
|
+
alert={5}
|
|
46
|
+
onToggle={() => {}}
|
|
47
|
+
onSelect={() => {}}
|
|
48
|
+
/>
|
|
49
|
+
<NavItem
|
|
50
|
+
kind="component"
|
|
51
|
+
label="Button"
|
|
52
|
+
count={3}
|
|
53
|
+
alert="dot"
|
|
54
|
+
expanded
|
|
55
|
+
onToggle={() => {}}
|
|
56
|
+
onSelect={() => {}}
|
|
57
|
+
/>
|
|
58
|
+
<NavItem kind="case" label="Playground" alert={2} onSelect={() => {}} />
|
|
59
|
+
<NavItem kind="case" label="Variants" alert={3} onSelect={() => {}} />
|
|
60
|
+
<NavItem kind="case" label="Sizes" onSelect={() => {}} />
|
|
61
|
+
</Sidebar>
|
|
62
|
+
),
|
|
63
|
+
Tree: () => (
|
|
64
|
+
<Sidebar style={{ width: '15rem' }}>
|
|
65
|
+
<NavItem
|
|
66
|
+
kind="component"
|
|
67
|
+
label="Button"
|
|
68
|
+
count={4}
|
|
69
|
+
expanded
|
|
70
|
+
onToggle={() => {}}
|
|
71
|
+
onSelect={() => {}}
|
|
72
|
+
/>
|
|
73
|
+
<NavItem kind="case" label="Playground" onSelect={() => {}} />
|
|
74
|
+
<NavItem kind="case" label="Variants" current onSelect={() => {}} />
|
|
75
|
+
<NavItem kind="case" label="Sizes" onSelect={() => {}} />
|
|
76
|
+
<NavItem
|
|
77
|
+
kind="component"
|
|
78
|
+
label="Checkbox"
|
|
79
|
+
onToggle={() => {}}
|
|
80
|
+
onSelect={() => {}}
|
|
81
|
+
/>
|
|
82
|
+
</Sidebar>
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
{ level: 'molecule' },
|
|
86
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
.dcui-navrow {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: var(--dc-space-1);
|
|
5
|
+
width: 100%;
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
position: relative;
|
|
8
|
+
}
|
|
9
|
+
.dcui-nav-disclosure {
|
|
10
|
+
flex: 0 0 auto;
|
|
11
|
+
display: inline-flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
width: 20px;
|
|
15
|
+
height: 22px;
|
|
16
|
+
border: 0;
|
|
17
|
+
background: none;
|
|
18
|
+
color: var(--dc-fg-subtle);
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
border-radius: var(--dc-radius-sm);
|
|
21
|
+
padding: 0;
|
|
22
|
+
transition:
|
|
23
|
+
color var(--dc-transition-fast),
|
|
24
|
+
background var(--dc-transition-fast);
|
|
25
|
+
}
|
|
26
|
+
.dcui-nav-disclosure:hover {
|
|
27
|
+
color: var(--dc-fg);
|
|
28
|
+
background: var(--dc-hover);
|
|
29
|
+
}
|
|
30
|
+
/* A non-expandable component row (single case) keeps the chevron's footprint so
|
|
31
|
+
its name still lines up under expandable siblings — just with no glyph. */
|
|
32
|
+
.dcui-nav-disclosure-spacer {
|
|
33
|
+
flex: 0 0 auto;
|
|
34
|
+
width: 20px;
|
|
35
|
+
height: 22px;
|
|
36
|
+
}
|
|
37
|
+
.dcui-chevron {
|
|
38
|
+
font-family: var(--dc-font-mono);
|
|
39
|
+
font-size: 0.65rem;
|
|
40
|
+
line-height: 1;
|
|
41
|
+
transition: transform var(--dc-transition-fast);
|
|
42
|
+
}
|
|
43
|
+
.dcui-chevron[data-expanded="true"] {
|
|
44
|
+
transform: rotate(90deg);
|
|
45
|
+
}
|
|
46
|
+
.dcui-nav-name {
|
|
47
|
+
flex: 1;
|
|
48
|
+
min-width: 0;
|
|
49
|
+
display: inline-flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: var(--dc-space-3);
|
|
52
|
+
text-align: left;
|
|
53
|
+
font-family: var(--dc-font-sans);
|
|
54
|
+
font-size: var(--dc-text-base);
|
|
55
|
+
font-weight: var(--dc-weight-medium);
|
|
56
|
+
color: var(--dc-fg);
|
|
57
|
+
border: 0;
|
|
58
|
+
background: none;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
padding: 0.25rem var(--dc-space-3);
|
|
61
|
+
border-radius: var(--dc-radius-sm);
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
transition:
|
|
64
|
+
background var(--dc-transition-fast),
|
|
65
|
+
color var(--dc-transition-fast);
|
|
66
|
+
}
|
|
67
|
+
/* The name text itself truncates; the alert pill beside it never shrinks. */
|
|
68
|
+
.dcui-nav-label {
|
|
69
|
+
min-width: 0;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
text-overflow: ellipsis;
|
|
72
|
+
white-space: nowrap;
|
|
73
|
+
}
|
|
74
|
+
.dcui-nav-name:hover {
|
|
75
|
+
background: var(--dc-hover);
|
|
76
|
+
}
|
|
77
|
+
.dcui-nav-count {
|
|
78
|
+
font-family: var(--dc-font-mono);
|
|
79
|
+
font-size: var(--dc-text-xs);
|
|
80
|
+
color: var(--dc-fg-subtle);
|
|
81
|
+
flex: 0 0 auto;
|
|
82
|
+
padding-right: var(--dc-space-3);
|
|
83
|
+
}
|
|
84
|
+
/* Case rows indent so their text lines up under the component name (chevron
|
|
85
|
+
width + row gap, then the name's own text padding). */
|
|
86
|
+
.dcui-navrow[data-kind="case"] {
|
|
87
|
+
padding-left: calc(20px + var(--dc-space-1));
|
|
88
|
+
}
|
|
89
|
+
.dcui-navrow[data-kind="case"] .dcui-nav-name {
|
|
90
|
+
font-weight: var(--dc-weight-normal);
|
|
91
|
+
font-size: var(--dc-text-sm);
|
|
92
|
+
color: var(--dc-fg-muted);
|
|
93
|
+
}
|
|
94
|
+
.dcui-navrow[data-kind="case"] .dcui-nav-name:hover {
|
|
95
|
+
color: var(--dc-fg);
|
|
96
|
+
}
|
|
97
|
+
.dcui-navrow[data-current="true"] .dcui-nav-name {
|
|
98
|
+
color: var(--dc-brand);
|
|
99
|
+
font-weight: var(--dc-weight-medium);
|
|
100
|
+
}
|
|
101
|
+
.dcui-navrow[data-current="true"]::before {
|
|
102
|
+
content: "";
|
|
103
|
+
position: absolute;
|
|
104
|
+
left: 0;
|
|
105
|
+
top: 50%;
|
|
106
|
+
transform: translateY(-50%);
|
|
107
|
+
width: 2px;
|
|
108
|
+
height: 1rem;
|
|
109
|
+
border-radius: 1px;
|
|
110
|
+
background: var(--dc-brand);
|
|
111
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
**NavItem** — one row in the sidebar tree; reach for it to render a component or case entry in the navigation.
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<NavItem kind="component" label="Button" count={4} expanded
|
|
5
|
+
onToggle={toggle} onSelect={select} />
|
|
6
|
+
<NavItem kind="case" label="Variants" current onSelect={select} />
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`kind="component"` (the default) renders a disclosure chevron + name + optional case `count`; `kind="case"` renders an indented case link aligned under the component name. The chevron and `onToggle` exist only for `kind="component"`. The active row (`current`) is marigold with a left tick.
|
|
10
|
+
|
|
11
|
+
Place inside `Sidebar`.
|
|
12
|
+
|
|
13
|
+
`onSelect` (name click) and `onToggle` (chevron click) are argument-less. A11y: component rows expose `aria-expanded` with a dynamic Expand/Collapse label; the name button gets `aria-current` when `current`.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { NavItem } from './NavItem'
|
|
4
|
+
|
|
5
|
+
describe('NavItem', () => {
|
|
6
|
+
test('a toggleable component row leads with a disclosure chevron', () => {
|
|
7
|
+
const html = renderToStaticMarkup(
|
|
8
|
+
<NavItem label="Button" expanded={false} onToggle={() => {}} count={3} />,
|
|
9
|
+
)
|
|
10
|
+
expect(html).toContain('class="dcui-nav-disclosure"')
|
|
11
|
+
expect(html).toContain('aria-expanded="false"')
|
|
12
|
+
expect(html).toContain('aria-label="Expand Button"')
|
|
13
|
+
expect(html).toContain('data-kind="component"')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('the chevron aria-label flips with the expanded state', () => {
|
|
17
|
+
const html = renderToStaticMarkup(
|
|
18
|
+
<NavItem label="Button" expanded onToggle={() => {}} />,
|
|
19
|
+
)
|
|
20
|
+
expect(html).toContain('aria-label="Collapse Button"')
|
|
21
|
+
expect(html).toContain('data-expanded="true"')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('a single-case component row swaps the chevron for an aligning spacer', () => {
|
|
25
|
+
const html = renderToStaticMarkup(<NavItem label="Solo" />)
|
|
26
|
+
expect(html).toContain('class="dcui-nav-disclosure-spacer"')
|
|
27
|
+
expect(html).not.toContain('class="dcui-nav-disclosure"')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('a case row is indented and carries no disclosure', () => {
|
|
31
|
+
const html = renderToStaticMarkup(<NavItem kind="case" label="default" />)
|
|
32
|
+
expect(html).toContain('data-kind="case"')
|
|
33
|
+
expect(html).not.toContain('dcui-nav-disclosure')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('the active row sets data-current and aria-current', () => {
|
|
37
|
+
const html = renderToStaticMarkup(<NavItem label="Button" current />)
|
|
38
|
+
expect(html).toContain('data-current="true"')
|
|
39
|
+
expect(html).toContain('aria-current="true"')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('renders the a11y badge only for a positive count or "dot"', () => {
|
|
43
|
+
expect(renderToStaticMarkup(<NavItem label="x" alert={2} />)).toContain(
|
|
44
|
+
'dcui-a11y-badge',
|
|
45
|
+
)
|
|
46
|
+
expect(renderToStaticMarkup(<NavItem label="x" alert="dot" />)).toContain(
|
|
47
|
+
'data-dot="true"',
|
|
48
|
+
)
|
|
49
|
+
expect(renderToStaticMarkup(<NavItem label="x" alert={0} />)).not.toContain(
|
|
50
|
+
'dcui-a11y-badge',
|
|
51
|
+
)
|
|
52
|
+
expect(renderToStaticMarkup(<NavItem label="x" />)).not.toContain(
|
|
53
|
+
'dcui-a11y-badge',
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('renders the case count only when provided', () => {
|
|
58
|
+
expect(
|
|
59
|
+
renderToStaticMarkup(<NavItem label="x" onToggle={() => {}} count={5} />),
|
|
60
|
+
).toContain('dcui-nav-count')
|
|
61
|
+
expect(
|
|
62
|
+
renderToStaticMarkup(<NavItem label="x" onToggle={() => {}} />),
|
|
63
|
+
).not.toContain('dcui-nav-count')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { A11yBadge } from './A11yBadge'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Display Case — NavItem
|
|
6
|
+
* One row in the sidebar tree. `kind="component"` renders a disclosure chevron +
|
|
7
|
+
* name (+ optional case count); when no `onToggle` is given (a component with a
|
|
8
|
+
* single case) the chevron is replaced by a same-width spacer and the count is
|
|
9
|
+
* omitted, so the row reads as a plain leaf that still aligns with its siblings.
|
|
10
|
+
* `kind="case"` renders an indented case link that lines up under the component
|
|
11
|
+
* name. The active row is marigold with a left tick.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type NavItemKind = 'component' | 'case'
|
|
15
|
+
|
|
16
|
+
export interface NavItemProps {
|
|
17
|
+
kind?: NavItemKind
|
|
18
|
+
label: ReactNode
|
|
19
|
+
count?: ReactNode
|
|
20
|
+
current?: boolean
|
|
21
|
+
expanded?: boolean
|
|
22
|
+
/** Accessibility marker. A positive number renders a counted danger pill; the
|
|
23
|
+
* string `'dot'` renders an unnumbered danger dot — used on an *expanded*
|
|
24
|
+
* component whose per-variant counts have moved onto its case rows, so the
|
|
25
|
+
* parent flags "issues here" without competing with the child numbers. Omit
|
|
26
|
+
* or `0` for none. */
|
|
27
|
+
alert?: number | 'dot'
|
|
28
|
+
onSelect?: () => void
|
|
29
|
+
onToggle?: () => void
|
|
30
|
+
/** `data-testid` for the select (name) button. */
|
|
31
|
+
testId?: string
|
|
32
|
+
/** `data-testid` for the disclosure chevron (component rows only). */
|
|
33
|
+
toggleTestId?: string
|
|
34
|
+
/** `data-testid` for the a11y-violation marker. */
|
|
35
|
+
alertTestId?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function NavItem({
|
|
39
|
+
kind = 'component',
|
|
40
|
+
label,
|
|
41
|
+
count,
|
|
42
|
+
current = false,
|
|
43
|
+
expanded = false,
|
|
44
|
+
alert,
|
|
45
|
+
onSelect,
|
|
46
|
+
onToggle,
|
|
47
|
+
testId,
|
|
48
|
+
toggleTestId,
|
|
49
|
+
alertTestId,
|
|
50
|
+
}: NavItemProps) {
|
|
51
|
+
// The a11y marker (a counted pill, or a bare 'dot' on an expanded parent) is
|
|
52
|
+
// the standalone A11yBadge; render it only for a positive count / 'dot'.
|
|
53
|
+
const showAlert = alert === 'dot' || (typeof alert === 'number' && alert > 0)
|
|
54
|
+
// Component rows lead with a disclosure chevron; a component with no `onToggle`
|
|
55
|
+
// (a single case) gets a same-width spacer instead so its name still aligns
|
|
56
|
+
// under expandable siblings. Case rows lead with nothing.
|
|
57
|
+
let disclosure: ReactNode = null
|
|
58
|
+
if (kind === 'component') {
|
|
59
|
+
disclosure = onToggle ? (
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
className="dcui-nav-disclosure"
|
|
63
|
+
aria-label={expanded ? `Collapse ${label}` : `Expand ${label}`}
|
|
64
|
+
aria-expanded={expanded}
|
|
65
|
+
data-testid={toggleTestId}
|
|
66
|
+
onClick={onToggle}>
|
|
67
|
+
<span className="dcui-chevron" data-expanded={expanded}>
|
|
68
|
+
▸
|
|
69
|
+
</span>
|
|
70
|
+
</button>
|
|
71
|
+
) : (
|
|
72
|
+
<span className="dcui-nav-disclosure-spacer" aria-hidden="true" />
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
className="dcui-navrow"
|
|
78
|
+
data-kind={kind}
|
|
79
|
+
data-current={current ? 'true' : undefined}>
|
|
80
|
+
{disclosure}
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
className="dcui-nav-name"
|
|
84
|
+
aria-current={current ? 'true' : undefined}
|
|
85
|
+
data-testid={testId}
|
|
86
|
+
onClick={onSelect}>
|
|
87
|
+
<span className="dcui-nav-label">{label}</span>
|
|
88
|
+
{showAlert && alert !== undefined && (
|
|
89
|
+
<A11yBadge value={alert} testId={alertTestId} />
|
|
90
|
+
)}
|
|
91
|
+
</button>
|
|
92
|
+
{count != null ? <span className="dcui-nav-count">{count}</span> : null}
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineCases, tweak } from '@awarebydefault/display-case'
|
|
2
|
+
import { RenderAddress } from './RenderAddress'
|
|
3
|
+
|
|
4
|
+
export default defineCases(
|
|
5
|
+
'RenderAddress',
|
|
6
|
+
{
|
|
7
|
+
Playground: {
|
|
8
|
+
tweaks: {
|
|
9
|
+
method: tweak.text('GET'),
|
|
10
|
+
url: tweak.text(
|
|
11
|
+
'/render/button/playground?theme=light&t.variant=accent',
|
|
12
|
+
),
|
|
13
|
+
},
|
|
14
|
+
render: (t) => <RenderAddress method={t.method} url={t.url} />,
|
|
15
|
+
},
|
|
16
|
+
Default: () => (
|
|
17
|
+
<RenderAddress url="/render/button/playground?theme=light&t.variant=accent" />
|
|
18
|
+
),
|
|
19
|
+
},
|
|
20
|
+
{ level: 'molecule' },
|
|
21
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.dcui-address {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: var(--dc-space-4);
|
|
5
|
+
width: 100%;
|
|
6
|
+
/* border-box so the 100% width + padding + border stays inside the container
|
|
7
|
+
(matches the docked tweaks panel's right edge instead of overflowing it). */
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
border: var(--dc-border-line);
|
|
10
|
+
border-radius: var(--dc-radius-sm);
|
|
11
|
+
background: var(--dc-surface);
|
|
12
|
+
padding: var(--dc-space-3) var(--dc-space-4);
|
|
13
|
+
}
|
|
14
|
+
/* A solid accent tag (the accent Button's fill), but inert — it's just a label. */
|
|
15
|
+
.dcui-address-method {
|
|
16
|
+
flex: 0 0 auto;
|
|
17
|
+
line-height: 1;
|
|
18
|
+
font-family: var(--dc-font-mono);
|
|
19
|
+
font-size: var(--dc-text-xs);
|
|
20
|
+
font-weight: var(--dc-weight-medium);
|
|
21
|
+
color: var(--dc-brand-fg);
|
|
22
|
+
background: var(--dc-brand);
|
|
23
|
+
padding: 0.3125rem var(--dc-space-3);
|
|
24
|
+
border-radius: var(--dc-radius-sm);
|
|
25
|
+
}
|
|
26
|
+
.dcui-address-url {
|
|
27
|
+
font-family: var(--dc-font-mono);
|
|
28
|
+
font-size: var(--dc-text-sm);
|
|
29
|
+
color: var(--dc-fg);
|
|
30
|
+
flex: 1;
|
|
31
|
+
min-width: 0;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
text-overflow: ellipsis;
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
**RenderAddress** — a monospace address bar with an HTTP method tag, a URL that scrolls if it overflows, and a copy button; reach for it to show a deterministic render URL (or any endpoint) the reader can copy.
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<RenderAddress method="GET" url="/render/button/playground?theme=light" />
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Clicking the button copies `url` to the clipboard, flips the glyph to ✓ for ~1.2s, then reverts. It degrades silently when the clipboard is unavailable (e.g. an isolated frame) — the address still reads.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { RenderAddress } from './RenderAddress'
|
|
4
|
+
|
|
5
|
+
describe('RenderAddress', () => {
|
|
6
|
+
test('renders the URL with a default GET method tag and a copy control', () => {
|
|
7
|
+
const html = renderToStaticMarkup(
|
|
8
|
+
<RenderAddress url="/render/button/default" />,
|
|
9
|
+
)
|
|
10
|
+
expect(html).toContain('class="dcui-address-method"')
|
|
11
|
+
expect(html).toContain('>GET<')
|
|
12
|
+
expect(html).toContain('/render/button/default')
|
|
13
|
+
expect(html).toContain('aria-label="Copy address"')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('renders the copy glyph in its un-copied resting state', () => {
|
|
17
|
+
const html = renderToStaticMarkup(<RenderAddress url="/x" />)
|
|
18
|
+
expect(html).toContain('⧉')
|
|
19
|
+
expect(html).not.toContain('✓')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('reflects a custom method tag', () => {
|
|
23
|
+
const html = renderToStaticMarkup(<RenderAddress url="/x" method="POST" />)
|
|
24
|
+
expect(html).toContain('>POST<')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { IconButton } from '../controls/IconButton'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Display Case — RenderAddress
|
|
6
|
+
* A monospace address bar: an HTTP method tag, a URL that truncates with an
|
|
7
|
+
* ellipsis if it overflows, and a copy button. Shows a deterministic render URL (or any
|
|
8
|
+
* endpoint) the reader can copy — the browse chrome uses it for the live
|
|
9
|
+
* exhibit's address.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface RenderAddressProps {
|
|
13
|
+
/** The address shown and copied. */
|
|
14
|
+
url: string
|
|
15
|
+
/** HTTP method tag on the left. */
|
|
16
|
+
method?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function RenderAddress({ url, method = 'GET' }: RenderAddressProps) {
|
|
20
|
+
const [copied, setCopied] = useState(false)
|
|
21
|
+
const copy = () => {
|
|
22
|
+
try {
|
|
23
|
+
navigator.clipboard?.writeText(url)
|
|
24
|
+
} catch {
|
|
25
|
+
// Clipboard may be unavailable in an isolated frame — the address still reads.
|
|
26
|
+
}
|
|
27
|
+
setCopied(true)
|
|
28
|
+
setTimeout(() => setCopied(false), 1200)
|
|
29
|
+
}
|
|
30
|
+
return (
|
|
31
|
+
<div className="dcui-address">
|
|
32
|
+
<span className="dcui-address-method">{method}</span>
|
|
33
|
+
<span className="dcui-address-url">{url}</span>
|
|
34
|
+
<IconButton
|
|
35
|
+
glyph={copied ? '✓' : '⧉'}
|
|
36
|
+
label="Copy address"
|
|
37
|
+
variant="bare"
|
|
38
|
+
size="sm"
|
|
39
|
+
onClick={copy}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|