@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,52 @@
|
|
|
1
|
+
import { defineCases, tweak } from '@awarebydefault/display-case'
|
|
2
|
+
import { Button } from './Button'
|
|
3
|
+
|
|
4
|
+
export default defineCases(
|
|
5
|
+
'Button',
|
|
6
|
+
{
|
|
7
|
+
Playground: {
|
|
8
|
+
tweaks: {
|
|
9
|
+
label: tweak.text('Button'),
|
|
10
|
+
variant: tweak.choice(
|
|
11
|
+
['ghost', 'primary', 'accent', 'subtle'],
|
|
12
|
+
'ghost',
|
|
13
|
+
),
|
|
14
|
+
size: tweak.choice(['sm', 'md', 'lg'], 'md'),
|
|
15
|
+
pressed: tweak.boolean(false),
|
|
16
|
+
disabled: tweak.boolean(false),
|
|
17
|
+
},
|
|
18
|
+
render: (t) => (
|
|
19
|
+
<Button
|
|
20
|
+
variant={t.variant as 'ghost' | 'primary' | 'accent' | 'subtle'}
|
|
21
|
+
size={t.size as 'sm' | 'md' | 'lg'}
|
|
22
|
+
aria-pressed={t.pressed}
|
|
23
|
+
disabled={t.disabled}>
|
|
24
|
+
{t.label}
|
|
25
|
+
</Button>
|
|
26
|
+
),
|
|
27
|
+
},
|
|
28
|
+
Variants: () => (
|
|
29
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
30
|
+
<Button>Ghost</Button>
|
|
31
|
+
<Button variant="primary">Primary</Button>
|
|
32
|
+
<Button variant="accent">Accent</Button>
|
|
33
|
+
<Button variant="subtle">Subtle</Button>
|
|
34
|
+
</div>
|
|
35
|
+
),
|
|
36
|
+
Sizes: () => (
|
|
37
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
38
|
+
<Button size="sm">Small</Button>
|
|
39
|
+
<Button size="md">Medium</Button>
|
|
40
|
+
<Button size="lg">Large</Button>
|
|
41
|
+
</div>
|
|
42
|
+
),
|
|
43
|
+
Toggle: () => (
|
|
44
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
45
|
+
<Button aria-pressed={false}>Off</Button>
|
|
46
|
+
<Button aria-pressed>On</Button>
|
|
47
|
+
</div>
|
|
48
|
+
),
|
|
49
|
+
Disabled: () => <Button disabled>Disabled</Button>,
|
|
50
|
+
},
|
|
51
|
+
{ level: 'atom' },
|
|
52
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
.dcui-btn {
|
|
2
|
+
--_bg: var(--dc-surface);
|
|
3
|
+
--_fg: var(--dc-fg);
|
|
4
|
+
--_bd: var(--dc-border);
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
gap: var(--dc-space-2);
|
|
9
|
+
font-family: var(--dc-font-sans);
|
|
10
|
+
font-size: var(--dc-text-base);
|
|
11
|
+
font-weight: var(--dc-weight-medium);
|
|
12
|
+
line-height: 1;
|
|
13
|
+
color: var(--_fg);
|
|
14
|
+
background: var(--_bg);
|
|
15
|
+
border: 1px solid var(--_bd);
|
|
16
|
+
border-radius: var(--dc-radius-sm);
|
|
17
|
+
padding: 0 var(--dc-space-6);
|
|
18
|
+
height: 30px;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
white-space: nowrap;
|
|
21
|
+
transition:
|
|
22
|
+
background var(--dc-transition-fast),
|
|
23
|
+
border-color var(--dc-transition-fast),
|
|
24
|
+
color var(--dc-transition-fast),
|
|
25
|
+
transform var(--dc-transition-fast);
|
|
26
|
+
}
|
|
27
|
+
.dcui-btn:hover {
|
|
28
|
+
background: var(--dc-hover);
|
|
29
|
+
}
|
|
30
|
+
.dcui-btn:active {
|
|
31
|
+
transform: translateY(0.5px);
|
|
32
|
+
}
|
|
33
|
+
.dcui-btn:focus-visible {
|
|
34
|
+
outline: 2px solid var(--dc-focus-ring);
|
|
35
|
+
outline-offset: 1px;
|
|
36
|
+
}
|
|
37
|
+
.dcui-btn[data-size="sm"] {
|
|
38
|
+
height: 26px;
|
|
39
|
+
font-size: var(--dc-text-sm);
|
|
40
|
+
padding: 0 var(--dc-space-4);
|
|
41
|
+
}
|
|
42
|
+
.dcui-btn[data-size="lg"] {
|
|
43
|
+
height: 36px;
|
|
44
|
+
padding: 0 var(--dc-space-8);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.dcui-btn[data-variant="primary"] {
|
|
48
|
+
--_bg: var(--dc-ink);
|
|
49
|
+
--_fg: var(--dc-ink-fg);
|
|
50
|
+
--_bd: var(--dc-ink);
|
|
51
|
+
}
|
|
52
|
+
.dcui-btn[data-variant="primary"]:hover {
|
|
53
|
+
background: var(--dc-ink-hover);
|
|
54
|
+
border-color: var(--dc-ink-hover);
|
|
55
|
+
}
|
|
56
|
+
.dcui-btn[data-variant="accent"] {
|
|
57
|
+
--_bg: var(--dc-brand);
|
|
58
|
+
--_fg: var(--dc-brand-fg);
|
|
59
|
+
--_bd: var(--dc-brand);
|
|
60
|
+
}
|
|
61
|
+
.dcui-btn[data-variant="accent"]:hover {
|
|
62
|
+
background: var(--dc-brand-hover);
|
|
63
|
+
border-color: var(--dc-brand-hover);
|
|
64
|
+
}
|
|
65
|
+
.dcui-btn[data-variant="subtle"] {
|
|
66
|
+
--_bg: transparent;
|
|
67
|
+
--_bd: transparent;
|
|
68
|
+
--_fg: var(--dc-fg-muted);
|
|
69
|
+
}
|
|
70
|
+
.dcui-btn[data-variant="subtle"]:hover {
|
|
71
|
+
background: var(--dc-hover);
|
|
72
|
+
color: var(--dc-fg);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Toggle "on" — marigold (the active selection colour). */
|
|
76
|
+
.dcui-btn[aria-pressed="true"] {
|
|
77
|
+
--_fg: var(--dc-brand);
|
|
78
|
+
--_bd: var(--dc-brand);
|
|
79
|
+
--_bg: var(--dc-brand-subtle);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dcui-btn:disabled {
|
|
83
|
+
opacity: 0.45;
|
|
84
|
+
cursor: not-allowed;
|
|
85
|
+
}
|
|
86
|
+
.dcui-btn:disabled:hover {
|
|
87
|
+
background: var(--_bg);
|
|
88
|
+
transform: none;
|
|
89
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
**Button** — the quiet, bordered text control the Display Case chrome leans on; reach for it for any labelled action.
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<Button>Docs</Button>
|
|
5
|
+
<Button variant="primary">Run check</Button>
|
|
6
|
+
<Button variant="accent">Send magic link</Button>
|
|
7
|
+
<Button aria-pressed>Grid</Button>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Variants: `ghost` (default, recedes) · `primary` (warm ink, the emphatic action) · `accent` (marigold, rare standout) · `subtle` (borderless). A toggle button lights marigold when you pass `aria-pressed`.
|
|
11
|
+
|
|
12
|
+
For a glyph-only square control, reach for `IconButton`.
|
|
13
|
+
|
|
14
|
+
It is a real `<button>` — pass `disabled`, `onClick`, `aria-pressed`, etc.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { Button } from './Button'
|
|
4
|
+
|
|
5
|
+
describe('Button', () => {
|
|
6
|
+
test('defaults to a ghost, medium, type=button control', () => {
|
|
7
|
+
const html = renderToStaticMarkup(<Button>Go</Button>)
|
|
8
|
+
expect(html).toContain('class="dcui-btn"')
|
|
9
|
+
expect(html).toContain('data-variant="ghost"')
|
|
10
|
+
expect(html).toContain('data-size="md"')
|
|
11
|
+
expect(html).toContain('type="button"')
|
|
12
|
+
expect(html).toContain('Go')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('reflects the variant and size props as data attributes', () => {
|
|
16
|
+
const html = renderToStaticMarkup(
|
|
17
|
+
<Button variant="accent" size="lg">
|
|
18
|
+
Buy
|
|
19
|
+
</Button>,
|
|
20
|
+
)
|
|
21
|
+
expect(html).toContain('data-variant="accent"')
|
|
22
|
+
expect(html).toContain('data-size="lg"')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('a caller-supplied type overrides the button default', () => {
|
|
26
|
+
const html = renderToStaticMarkup(<Button type="submit">Send</Button>)
|
|
27
|
+
expect(html).toContain('type="submit"')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('renders the marigold toggle-on state from aria-pressed', () => {
|
|
31
|
+
const html = renderToStaticMarkup(<Button aria-pressed={true}>On</Button>)
|
|
32
|
+
expect(html).toContain('aria-pressed="true"')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('passes arbitrary button attributes through to the element', () => {
|
|
36
|
+
const html = renderToStaticMarkup(
|
|
37
|
+
<Button disabled data-testid="x" title="hint">
|
|
38
|
+
Off
|
|
39
|
+
</Button>,
|
|
40
|
+
)
|
|
41
|
+
expect(html).toContain('disabled')
|
|
42
|
+
expect(html).toContain('data-testid="x"')
|
|
43
|
+
expect(html).toContain('title="hint"')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Display Case — Button
|
|
5
|
+
* The quiet, bordered control the chrome leans on. `ghost` is the default (it
|
|
6
|
+
* recedes); `primary` (warm ink) and `accent` (marigold) are for the rare
|
|
7
|
+
* emphatic action; `subtle` is borderless. A toggle button lights marigold via
|
|
8
|
+
* `aria-pressed`.
|
|
9
|
+
*
|
|
10
|
+
* Styling lives in the sibling `Button.css`, concatenated into the Vitrine
|
|
11
|
+
* stylesheet and inlined into every document head server-side (see
|
|
12
|
+
* `readVitrineCss` in server.ts) — no runtime injection.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type ButtonVariant = 'ghost' | 'primary' | 'accent' | 'subtle'
|
|
16
|
+
export type ButtonSize = 'sm' | 'md' | 'lg'
|
|
17
|
+
|
|
18
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
19
|
+
variant?: ButtonVariant
|
|
20
|
+
size?: ButtonSize
|
|
21
|
+
children?: ReactNode
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Button({
|
|
25
|
+
variant = 'ghost',
|
|
26
|
+
size = 'md',
|
|
27
|
+
type = 'button',
|
|
28
|
+
children,
|
|
29
|
+
...rest
|
|
30
|
+
}: ButtonProps) {
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
type={type}
|
|
34
|
+
className="dcui-btn"
|
|
35
|
+
data-variant={variant}
|
|
36
|
+
data-size={size}
|
|
37
|
+
{...rest}>
|
|
38
|
+
{children}
|
|
39
|
+
</button>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineCases, tweak } from '@awarebydefault/display-case'
|
|
2
|
+
import { IconButton } from './IconButton'
|
|
3
|
+
|
|
4
|
+
export default defineCases(
|
|
5
|
+
'IconButton',
|
|
6
|
+
{
|
|
7
|
+
Playground: {
|
|
8
|
+
tweaks: {
|
|
9
|
+
glyph: tweak.text('✕'),
|
|
10
|
+
label: tweak.text('Close'),
|
|
11
|
+
size: tweak.choice(['sm', 'md', 'lg'], 'md'),
|
|
12
|
+
variant: tweak.choice(['outline', 'bare'], 'outline'),
|
|
13
|
+
active: tweak.boolean(false),
|
|
14
|
+
disabled: tweak.boolean(false),
|
|
15
|
+
},
|
|
16
|
+
render: (t) => (
|
|
17
|
+
<IconButton
|
|
18
|
+
glyph={t.glyph}
|
|
19
|
+
label={t.label}
|
|
20
|
+
size={t.size as 'sm' | 'md' | 'lg'}
|
|
21
|
+
variant={t.variant as 'outline' | 'bare'}
|
|
22
|
+
active={t.active}
|
|
23
|
+
disabled={t.disabled}
|
|
24
|
+
/>
|
|
25
|
+
),
|
|
26
|
+
},
|
|
27
|
+
Glyphs: () => (
|
|
28
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
29
|
+
<IconButton glyph="☰" label="Toggle navigation" />
|
|
30
|
+
<IconButton glyph="⟲" label="Rotate" />
|
|
31
|
+
<IconButton glyph="✕" label="Close" />
|
|
32
|
+
<IconButton glyph="+" label="Zoom in" />
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
Sizes: () => (
|
|
36
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
37
|
+
<IconButton glyph="✕" label="Small" size="sm" />
|
|
38
|
+
<IconButton glyph="✕" label="Medium" size="md" />
|
|
39
|
+
<IconButton glyph="✕" label="Large" size="lg" />
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
States: () => (
|
|
43
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
44
|
+
<IconButton glyph="⬓" label="Default" />
|
|
45
|
+
<IconButton glyph="▭" label="Active" active />
|
|
46
|
+
<IconButton glyph="▭" label="Bare" variant="bare" />
|
|
47
|
+
<IconButton glyph="✕" label="Disabled" disabled />
|
|
48
|
+
</div>
|
|
49
|
+
),
|
|
50
|
+
},
|
|
51
|
+
{ level: 'atom' },
|
|
52
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
.dcui-iconbtn {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
width: 30px;
|
|
6
|
+
height: 30px;
|
|
7
|
+
font-family: var(--dc-font-mono);
|
|
8
|
+
font-size: var(--dc-text-md);
|
|
9
|
+
line-height: 1;
|
|
10
|
+
color: var(--dc-fg);
|
|
11
|
+
background: var(--dc-surface);
|
|
12
|
+
border: 1px solid var(--dc-border);
|
|
13
|
+
border-radius: var(--dc-radius-sm);
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
transition:
|
|
16
|
+
background var(--dc-transition-fast),
|
|
17
|
+
color var(--dc-transition-fast),
|
|
18
|
+
border-color var(--dc-transition-fast),
|
|
19
|
+
transform var(--dc-transition-fast);
|
|
20
|
+
}
|
|
21
|
+
.dcui-iconbtn:hover {
|
|
22
|
+
background: var(--dc-hover);
|
|
23
|
+
}
|
|
24
|
+
.dcui-iconbtn:active {
|
|
25
|
+
background: var(--dc-active);
|
|
26
|
+
transform: translateY(0.5px);
|
|
27
|
+
}
|
|
28
|
+
.dcui-iconbtn:focus-visible {
|
|
29
|
+
outline: 2px solid var(--dc-focus-ring);
|
|
30
|
+
outline-offset: 1px;
|
|
31
|
+
}
|
|
32
|
+
.dcui-iconbtn[data-size="sm"] {
|
|
33
|
+
width: 26px;
|
|
34
|
+
height: 26px;
|
|
35
|
+
font-size: var(--dc-text-base);
|
|
36
|
+
}
|
|
37
|
+
.dcui-iconbtn[data-size="lg"] {
|
|
38
|
+
width: 36px;
|
|
39
|
+
height: 36px;
|
|
40
|
+
font-size: var(--dc-text-lg);
|
|
41
|
+
}
|
|
42
|
+
.dcui-iconbtn[aria-pressed="true"],
|
|
43
|
+
.dcui-iconbtn[data-active="true"] {
|
|
44
|
+
color: var(--dc-brand);
|
|
45
|
+
border-color: var(--dc-brand);
|
|
46
|
+
background: var(--dc-brand-subtle);
|
|
47
|
+
}
|
|
48
|
+
.dcui-iconbtn[data-variant="bare"] {
|
|
49
|
+
border-color: transparent;
|
|
50
|
+
background: transparent;
|
|
51
|
+
color: var(--dc-fg-muted);
|
|
52
|
+
}
|
|
53
|
+
.dcui-iconbtn[data-variant="bare"]:hover {
|
|
54
|
+
background: var(--dc-hover);
|
|
55
|
+
color: var(--dc-fg);
|
|
56
|
+
}
|
|
57
|
+
.dcui-iconbtn:disabled {
|
|
58
|
+
opacity: 0.45;
|
|
59
|
+
cursor: not-allowed;
|
|
60
|
+
}
|
|
61
|
+
.dcui-iconbtn:disabled:hover {
|
|
62
|
+
background: var(--dc-surface);
|
|
63
|
+
transform: none;
|
|
64
|
+
}
|
|
65
|
+
.dcui-iconbtn[data-variant="bare"]:disabled:hover {
|
|
66
|
+
background: transparent;
|
|
67
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
**IconButton** — a square control carrying a single Unicode glyph (☰ ⟲ ✕ + −); reach for it for compact, glyph-only chrome actions. Display Case uses no icon font and no SVG — just glyphs.
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<IconButton glyph="☰" label="Toggle navigation" />
|
|
5
|
+
<IconButton glyph="⟲" label="Rotate" variant="bare" />
|
|
6
|
+
<IconButton glyph="▭" label="Dock" active />
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`label` is required — it is the accessible name, since the button shows no text. Variants: `outline` (default) · `bare` (no border).
|
|
10
|
+
|
|
11
|
+
`active` is persistent emphasis (a row that stays lit); `aria-pressed` is a true toggle (on/off). Both light marigold. For text or labelled actions, use `Button`.
|
|
12
|
+
|
|
13
|
+
It is a real `<button>` — pass `disabled`, `onClick`, etc.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { IconButton } from './IconButton'
|
|
4
|
+
|
|
5
|
+
describe('IconButton', () => {
|
|
6
|
+
test('renders the glyph and exposes the required label as the accessible name', () => {
|
|
7
|
+
const html = renderToStaticMarkup(<IconButton glyph="☰" label="Menu" />)
|
|
8
|
+
expect(html).toContain('class="dcui-iconbtn"')
|
|
9
|
+
expect(html).toContain('aria-label="Menu"')
|
|
10
|
+
expect(html).toContain('☰')
|
|
11
|
+
expect(html).toContain('type="button"')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('defaults to the outline variant at medium size', () => {
|
|
15
|
+
const html = renderToStaticMarkup(<IconButton glyph="+" label="Add" />)
|
|
16
|
+
expect(html).toContain('data-variant="outline"')
|
|
17
|
+
expect(html).toContain('data-size="md"')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('falls back to children when no glyph is given', () => {
|
|
21
|
+
const html = renderToStaticMarkup(<IconButton label="Close">✕</IconButton>)
|
|
22
|
+
expect(html).toContain('✕')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('lights the active state via data-active only when active', () => {
|
|
26
|
+
const on = renderToStaticMarkup(<IconButton glyph="●" label="On" active />)
|
|
27
|
+
const off = renderToStaticMarkup(<IconButton glyph="●" label="Off" />)
|
|
28
|
+
expect(on).toContain('data-active="true"')
|
|
29
|
+
expect(off).not.toContain('data-active')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('reflects the bare variant and size', () => {
|
|
33
|
+
const html = renderToStaticMarkup(
|
|
34
|
+
<IconButton glyph="⟲" label="Reset" variant="bare" size="sm" />,
|
|
35
|
+
)
|
|
36
|
+
expect(html).toContain('data-variant="bare"')
|
|
37
|
+
expect(html).toContain('data-size="sm"')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Display Case — IconButton
|
|
5
|
+
* A square control carrying a single Unicode glyph (☰ ⟲ ✕ + −). Display Case
|
|
6
|
+
* uses no icon font and no SVG icons — just glyphs. `bare` drops the border;
|
|
7
|
+
* `active` / `aria-pressed` light it marigold.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type IconButtonSize = 'sm' | 'md' | 'lg'
|
|
11
|
+
export type IconButtonVariant = 'outline' | 'bare'
|
|
12
|
+
|
|
13
|
+
export interface IconButtonProps
|
|
14
|
+
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'aria-label'> {
|
|
15
|
+
/** The glyph to render (alternatively pass `children`). */
|
|
16
|
+
glyph?: ReactNode
|
|
17
|
+
size?: IconButtonSize
|
|
18
|
+
variant?: IconButtonVariant
|
|
19
|
+
/** Persistent "on" state (marigold), for non-toggle emphasis. */
|
|
20
|
+
active?: boolean
|
|
21
|
+
/** Accessible name — required since the button is glyph-only. */
|
|
22
|
+
label: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function IconButton({
|
|
26
|
+
glyph,
|
|
27
|
+
size = 'md',
|
|
28
|
+
variant = 'outline',
|
|
29
|
+
active = false,
|
|
30
|
+
type = 'button',
|
|
31
|
+
label,
|
|
32
|
+
children,
|
|
33
|
+
...rest
|
|
34
|
+
}: IconButtonProps) {
|
|
35
|
+
return (
|
|
36
|
+
<button
|
|
37
|
+
type={type}
|
|
38
|
+
className="dcui-iconbtn"
|
|
39
|
+
data-size={size}
|
|
40
|
+
data-variant={variant}
|
|
41
|
+
data-active={active ? 'true' : undefined}
|
|
42
|
+
aria-label={label}
|
|
43
|
+
{...rest}>
|
|
44
|
+
{glyph ?? children}
|
|
45
|
+
</button>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineCases, tweak } from '@awarebydefault/display-case'
|
|
2
|
+
import { Input } from './Input'
|
|
3
|
+
|
|
4
|
+
export default defineCases(
|
|
5
|
+
'Input',
|
|
6
|
+
{
|
|
7
|
+
Playground: {
|
|
8
|
+
tweaks: {
|
|
9
|
+
placeholder: tweak.text('filter by name'),
|
|
10
|
+
value: tweak.text(''),
|
|
11
|
+
prefix: tweak.text(''),
|
|
12
|
+
suffix: tweak.text(''),
|
|
13
|
+
type: tweak.choice(['text', 'number', 'search'], 'text'),
|
|
14
|
+
size: tweak.choice(['sm', 'md'], 'md'),
|
|
15
|
+
disabled: tweak.boolean(false),
|
|
16
|
+
},
|
|
17
|
+
render: (t) => (
|
|
18
|
+
<Input
|
|
19
|
+
placeholder={t.placeholder}
|
|
20
|
+
defaultValue={t.value || undefined}
|
|
21
|
+
prefix={t.prefix || undefined}
|
|
22
|
+
suffix={t.suffix || undefined}
|
|
23
|
+
type={t.type}
|
|
24
|
+
size={t.size as 'sm' | 'md'}
|
|
25
|
+
disabled={t.disabled}
|
|
26
|
+
wrapperStyle={{ width: '14rem' }}
|
|
27
|
+
/>
|
|
28
|
+
),
|
|
29
|
+
},
|
|
30
|
+
Default: () => <Input placeholder="filter by name" />,
|
|
31
|
+
WithAffixes: () => (
|
|
32
|
+
<Input
|
|
33
|
+
aria-label="Width in pixels"
|
|
34
|
+
type="number"
|
|
35
|
+
defaultValue={1280}
|
|
36
|
+
prefix="W"
|
|
37
|
+
suffix="px"
|
|
38
|
+
wrapperStyle={{ width: '7rem' }}
|
|
39
|
+
/>
|
|
40
|
+
),
|
|
41
|
+
Sizes: () => (
|
|
42
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
43
|
+
<Input size="sm" placeholder="Small" />
|
|
44
|
+
<Input size="md" placeholder="Medium" />
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
Disabled: () => <Input placeholder="disabled" disabled />,
|
|
48
|
+
},
|
|
49
|
+
{ level: 'atom' },
|
|
50
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
.dcui-field {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: var(--dc-space-2);
|
|
5
|
+
font-family: var(--dc-font-sans);
|
|
6
|
+
font-size: var(--dc-text-base);
|
|
7
|
+
color: var(--dc-fg);
|
|
8
|
+
background: var(--dc-surface);
|
|
9
|
+
border: 1px solid var(--dc-border);
|
|
10
|
+
border-radius: var(--dc-radius-sm);
|
|
11
|
+
padding: 0 var(--dc-space-4);
|
|
12
|
+
height: 30px;
|
|
13
|
+
transition:
|
|
14
|
+
border-color var(--dc-transition-fast),
|
|
15
|
+
box-shadow var(--dc-transition-fast);
|
|
16
|
+
}
|
|
17
|
+
.dcui-field:focus-within {
|
|
18
|
+
border-color: var(--dc-focus-ring);
|
|
19
|
+
box-shadow: 0 0 0 2px var(--dc-brand-subtle);
|
|
20
|
+
}
|
|
21
|
+
.dcui-field[data-size="sm"] {
|
|
22
|
+
height: 26px;
|
|
23
|
+
font-size: var(--dc-text-sm);
|
|
24
|
+
}
|
|
25
|
+
.dcui-field[aria-disabled="true"] {
|
|
26
|
+
opacity: 0.5;
|
|
27
|
+
}
|
|
28
|
+
.dcui-field-input {
|
|
29
|
+
flex: 1;
|
|
30
|
+
min-width: 0;
|
|
31
|
+
font: inherit;
|
|
32
|
+
color: inherit;
|
|
33
|
+
background: none;
|
|
34
|
+
border: 0;
|
|
35
|
+
outline: none;
|
|
36
|
+
padding: 0;
|
|
37
|
+
width: 100%;
|
|
38
|
+
}
|
|
39
|
+
.dcui-field-input::placeholder {
|
|
40
|
+
color: var(--dc-fg-subtle);
|
|
41
|
+
}
|
|
42
|
+
.dcui-field-affix {
|
|
43
|
+
font-family: var(--dc-font-mono);
|
|
44
|
+
font-size: var(--dc-text-xs);
|
|
45
|
+
color: var(--dc-fg-muted);
|
|
46
|
+
flex: 0 0 auto;
|
|
47
|
+
}
|
|
48
|
+
/* tame the native number spinner so it doesn't fight the chrome */
|
|
49
|
+
.dcui-field-input[type="number"]::-webkit-inner-spin-button,
|
|
50
|
+
.dcui-field-input[type="number"]::-webkit-outer-spin-button {
|
|
51
|
+
opacity: 0.4;
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
**Input** — a single-line text/number field with optional mono `prefix`/`suffix` affixes sitting inside the bordered, marigold-focus box (e.g. the device-dimension fields, `1280 × 800`).
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<Input placeholder="filter by name" value={q} onChange={(e) => setQ(e.target.value)} />
|
|
5
|
+
<Input type="number" prefix="W" suffix="px" defaultValue={1280} />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
`onChange` emits a native event — read `event.target.value`. Controlled via `value` + `onChange`; uncontrolled via `defaultValue`. It renders no label: the caller supplies a `<label>` or `aria-label`.
|
|
9
|
+
|
|
10
|
+
For a fixed set of choices, use `Select`.
|
|
11
|
+
|
|
12
|
+
Sizes: `sm` · `md` (default). Use `wrapperStyle`/`wrapperClassName` to size the field box; other native `<input>` props spread onto the inner input.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { Input } from './Input'
|
|
4
|
+
|
|
5
|
+
describe('Input', () => {
|
|
6
|
+
test('wraps a borderless input in the field shell at the default size', () => {
|
|
7
|
+
const html = renderToStaticMarkup(<Input placeholder="name" />)
|
|
8
|
+
expect(html).toContain('class="dcui-field"')
|
|
9
|
+
expect(html).toContain('data-size="md"')
|
|
10
|
+
expect(html).toContain('class="dcui-field-input"')
|
|
11
|
+
expect(html).toContain('placeholder="name"')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('renders the prefix and suffix affix slots only when provided', () => {
|
|
15
|
+
const both = renderToStaticMarkup(<Input prefix="W" suffix="px" />)
|
|
16
|
+
const affixes = both.match(/dcui-field-affix/g) ?? []
|
|
17
|
+
expect(affixes).toHaveLength(2)
|
|
18
|
+
expect(both).toContain('>W<')
|
|
19
|
+
expect(both).toContain('>px<')
|
|
20
|
+
|
|
21
|
+
const none = renderToStaticMarkup(<Input />)
|
|
22
|
+
expect(none).not.toContain('dcui-field-affix')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('disabling marks the wrapper aria-disabled and the input disabled', () => {
|
|
26
|
+
const html = renderToStaticMarkup(<Input disabled />)
|
|
27
|
+
expect(html).toContain('aria-disabled="true"')
|
|
28
|
+
expect(html).toContain('disabled')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('appends a caller class to the wrapper rather than replacing it', () => {
|
|
32
|
+
const html = renderToStaticMarkup(<Input wrapperClassName="w-32" />)
|
|
33
|
+
expect(html).toContain('class="dcui-field w-32"')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('forwards native input attributes to the inner field', () => {
|
|
37
|
+
const html = renderToStaticMarkup(
|
|
38
|
+
<Input type="number" value={800} readOnly />,
|
|
39
|
+
)
|
|
40
|
+
expect(html).toContain('type="number"')
|
|
41
|
+
expect(html).toContain('value="800"')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CSSProperties, InputHTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Display Case — Input
|
|
5
|
+
* Text / number field. Supports a leading label slot and a trailing unit suffix
|
|
6
|
+
* (used by the device-dimension fields: 1280 × 800). The border + marigold
|
|
7
|
+
* focus ring live on the wrapper so prefix/suffix sit inside the field.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type InputSize = 'sm' | 'md'
|
|
11
|
+
|
|
12
|
+
export interface InputProps
|
|
13
|
+
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'> {
|
|
14
|
+
size?: InputSize
|
|
15
|
+
/** Leading label slot (mono). */
|
|
16
|
+
prefix?: ReactNode
|
|
17
|
+
/** Trailing unit suffix (mono). */
|
|
18
|
+
suffix?: ReactNode
|
|
19
|
+
/** Style applied to the field wrapper (e.g. a fixed width). */
|
|
20
|
+
wrapperStyle?: CSSProperties
|
|
21
|
+
/** Class applied to the field wrapper. */
|
|
22
|
+
wrapperClassName?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Input({
|
|
26
|
+
size = 'md',
|
|
27
|
+
prefix,
|
|
28
|
+
suffix,
|
|
29
|
+
disabled = false,
|
|
30
|
+
wrapperStyle,
|
|
31
|
+
wrapperClassName,
|
|
32
|
+
...rest
|
|
33
|
+
}: InputProps) {
|
|
34
|
+
return (
|
|
35
|
+
<span
|
|
36
|
+
className={['dcui-field', wrapperClassName].filter(Boolean).join(' ')}
|
|
37
|
+
data-size={size}
|
|
38
|
+
aria-disabled={disabled ? 'true' : undefined}
|
|
39
|
+
style={wrapperStyle}>
|
|
40
|
+
{prefix ? <span className="dcui-field-affix">{prefix}</span> : null}
|
|
41
|
+
<input className="dcui-field-input" disabled={disabled} {...rest} />
|
|
42
|
+
{suffix ? <span className="dcui-field-affix">{suffix}</span> : null}
|
|
43
|
+
</span>
|
|
44
|
+
)
|
|
45
|
+
}
|