@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,377 @@
|
|
|
1
|
+
import type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from 'react'
|
|
2
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react'
|
|
3
|
+
import { createPortal } from 'react-dom'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Display Case — SelectMenu
|
|
7
|
+
* An accessible custom single-select, built to the WAI-ARIA "select-only
|
|
8
|
+
* combobox" pattern: a `role="combobox"` trigger that owns a popup
|
|
9
|
+
* `role="listbox"` of `role="option"`s, with focus kept on the trigger and the
|
|
10
|
+
* active option tracked via `aria-activedescendant`. Screen readers announce and
|
|
11
|
+
* operate it like a native `<select>`, but it commits on click with no OS popup
|
|
12
|
+
* menu — so the bound value updates *instantly* (a native `<select>` on macOS
|
|
13
|
+
* defers its `change` event until the native menu finishes dismissing, which
|
|
14
|
+
* reads as a lag when the bound view updates live).
|
|
15
|
+
*
|
|
16
|
+
* Keyboard: ↑/↓ move the active option, Home/End jump to the ends, type-ahead
|
|
17
|
+
* matches by label, Enter/Space commit, Esc closes without change, Tab commits
|
|
18
|
+
* then moves on. The popup portals to `document.body` so an `overflow`-clipping
|
|
19
|
+
* or `position: fixed` ancestor can't trap it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type SelectMenuSize = 'sm' | 'md'
|
|
23
|
+
export type SelectMenuOption =
|
|
24
|
+
| string
|
|
25
|
+
/** A non-interactive option renders as a quiet group header — not selectable,
|
|
26
|
+
* skipped by keyboard nav and type-ahead. */
|
|
27
|
+
| { value: string; label?: ReactNode; disabled?: boolean }
|
|
28
|
+
|
|
29
|
+
export interface SelectMenuProps {
|
|
30
|
+
options: SelectMenuOption[]
|
|
31
|
+
value: string
|
|
32
|
+
onChange: (value: string) => void
|
|
33
|
+
size?: SelectMenuSize
|
|
34
|
+
disabled?: boolean
|
|
35
|
+
'aria-label'?: string
|
|
36
|
+
id?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface NormOption {
|
|
40
|
+
value: string
|
|
41
|
+
label: ReactNode
|
|
42
|
+
/** Lowercased text used for type-ahead matching. */
|
|
43
|
+
text: string
|
|
44
|
+
/** A header row: rendered, but not selectable / navigable / matchable. */
|
|
45
|
+
disabled: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalize(options: SelectMenuOption[]): NormOption[] {
|
|
49
|
+
return options.map((o) => {
|
|
50
|
+
if (typeof o === 'string')
|
|
51
|
+
return { value: o, label: o, text: o.toLowerCase(), disabled: false }
|
|
52
|
+
const text = typeof o.label === 'string' ? o.label : o.value
|
|
53
|
+
return {
|
|
54
|
+
value: o.value,
|
|
55
|
+
label: o.label ?? o.value,
|
|
56
|
+
text: text.toLowerCase(),
|
|
57
|
+
disabled: !!o.disabled,
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Next option (by index, wrapping) whose label starts with the typed buffer.
|
|
63
|
+
* Disabled headers are skipped. */
|
|
64
|
+
function findMatch(opts: NormOption[], buffer: string, from: number): number {
|
|
65
|
+
const n = opts.length
|
|
66
|
+
if (n === 0) return -1
|
|
67
|
+
// A single character cycles forward (skip the current match); a longer buffer
|
|
68
|
+
// may need to land on the current option, so start inclusive.
|
|
69
|
+
const startK = buffer.length === 1 ? 1 : 0
|
|
70
|
+
for (let k = startK; k <= n; k++) {
|
|
71
|
+
const i = (from + k) % n
|
|
72
|
+
if (!opts[i].disabled && opts[i].text.startsWith(buffer)) return i
|
|
73
|
+
}
|
|
74
|
+
return -1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** First selectable index, or 0 if none. */
|
|
78
|
+
function firstEnabled(opts: NormOption[]): number {
|
|
79
|
+
const i = opts.findIndex((o) => !o.disabled)
|
|
80
|
+
return i < 0 ? 0 : i
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Last selectable index, or `opts.length - 1` if none. */
|
|
84
|
+
function lastEnabled(opts: NormOption[]): number {
|
|
85
|
+
for (let i = opts.length - 1; i >= 0; i--) if (!opts[i].disabled) return i
|
|
86
|
+
return opts.length - 1
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Nearest selectable index strictly past `from` in `dir` (+1/-1); stays put if
|
|
90
|
+
* there is none, so a header can never become the active row. */
|
|
91
|
+
function moveEnabled(opts: NormOption[], from: number, dir: 1 | -1): number {
|
|
92
|
+
for (let i = from + dir; i >= 0 && i < opts.length; i += dir) {
|
|
93
|
+
if (!opts[i].disabled) return i
|
|
94
|
+
}
|
|
95
|
+
return from
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface Coords {
|
|
99
|
+
left: number
|
|
100
|
+
top: number
|
|
101
|
+
minWidth: number
|
|
102
|
+
maxHeight: number
|
|
103
|
+
placement: 'down' | 'up'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function SelectMenu({
|
|
107
|
+
options,
|
|
108
|
+
value,
|
|
109
|
+
onChange,
|
|
110
|
+
size = 'md',
|
|
111
|
+
disabled = false,
|
|
112
|
+
'aria-label': ariaLabel,
|
|
113
|
+
id,
|
|
114
|
+
}: SelectMenuProps) {
|
|
115
|
+
const opts = normalize(options)
|
|
116
|
+
const reactId = useId()
|
|
117
|
+
const baseId = id ?? reactId
|
|
118
|
+
const listboxId = `${baseId}-listbox`
|
|
119
|
+
const optionId = (i: number) => `${baseId}-opt-${i}`
|
|
120
|
+
|
|
121
|
+
const rawSelected = opts.findIndex((o) => o.value === value)
|
|
122
|
+
const selectedIndex = rawSelected >= 0 ? rawSelected : firstEnabled(opts)
|
|
123
|
+
|
|
124
|
+
const [open, setOpen] = useState(false)
|
|
125
|
+
const [activeIndex, setActiveIndex] = useState(selectedIndex)
|
|
126
|
+
const [coords, setCoords] = useState<Coords | null>(null)
|
|
127
|
+
|
|
128
|
+
const triggerRef = useRef<HTMLDivElement | null>(null)
|
|
129
|
+
const listRef = useRef<HTMLDivElement | null>(null)
|
|
130
|
+
const activeRef = useRef(activeIndex)
|
|
131
|
+
activeRef.current = activeIndex
|
|
132
|
+
// Type-ahead buffer + its reset timer.
|
|
133
|
+
const bufferRef = useRef('')
|
|
134
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
135
|
+
|
|
136
|
+
const openAt = (index: number) => {
|
|
137
|
+
setActiveIndex(index)
|
|
138
|
+
setOpen(true)
|
|
139
|
+
}
|
|
140
|
+
const close = (focusTrigger = true) => {
|
|
141
|
+
setOpen(false)
|
|
142
|
+
if (focusTrigger) triggerRef.current?.focus()
|
|
143
|
+
}
|
|
144
|
+
const commit = (index: number) => {
|
|
145
|
+
const opt = opts[index]
|
|
146
|
+
if (!opt || opt.disabled) return // headers aren't selectable
|
|
147
|
+
if (opt.value !== value) onChange(opt.value)
|
|
148
|
+
close()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const typeahead = (char: string) => {
|
|
152
|
+
bufferRef.current += char.toLowerCase()
|
|
153
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
154
|
+
timerRef.current = setTimeout(() => {
|
|
155
|
+
bufferRef.current = ''
|
|
156
|
+
}, 500)
|
|
157
|
+
const match = findMatch(opts, bufferRef.current, activeRef.current)
|
|
158
|
+
if (match >= 0) setActiveIndex(match)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Position the portaled popup against the trigger, flipping above when there
|
|
162
|
+
// isn't room below. Measured before paint so it never flashes mispositioned.
|
|
163
|
+
useLayoutEffect(() => {
|
|
164
|
+
if (!open || !triggerRef.current) return
|
|
165
|
+
const r = triggerRef.current.getBoundingClientRect()
|
|
166
|
+
const margin = 8
|
|
167
|
+
const spaceBelow = window.innerHeight - r.bottom - margin
|
|
168
|
+
const spaceAbove = r.top - margin
|
|
169
|
+
const up = spaceBelow < 160 && spaceAbove > spaceBelow
|
|
170
|
+
setCoords({
|
|
171
|
+
left: r.left,
|
|
172
|
+
top: up ? r.top : r.bottom,
|
|
173
|
+
minWidth: r.width,
|
|
174
|
+
maxHeight: Math.max(96, up ? spaceAbove : spaceBelow),
|
|
175
|
+
placement: up ? 'up' : 'down',
|
|
176
|
+
})
|
|
177
|
+
}, [open])
|
|
178
|
+
|
|
179
|
+
// Keep the active option scrolled into view as it moves. `optionId` is a
|
|
180
|
+
// stable id formatter (baseId is fixed for the component's life).
|
|
181
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: optionId is stable; open/activeIndex drive the effect
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (!open) return
|
|
184
|
+
document
|
|
185
|
+
.getElementById(optionId(activeIndex))
|
|
186
|
+
?.scrollIntoView({ block: 'nearest' })
|
|
187
|
+
}, [open, activeIndex])
|
|
188
|
+
|
|
189
|
+
// While open, dismiss on an outside press or on any ancestor scroll/resize
|
|
190
|
+
// (the popup is fixed-positioned, so a scroll would otherwise strand it).
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (!open) return
|
|
193
|
+
const onDown = (e: PointerEvent) => {
|
|
194
|
+
const t = e.target as Node
|
|
195
|
+
if (triggerRef.current?.contains(t) || listRef.current?.contains(t))
|
|
196
|
+
return
|
|
197
|
+
setOpen(false)
|
|
198
|
+
}
|
|
199
|
+
const onScroll = (e: Event) => {
|
|
200
|
+
// Ignore the popup's own internal scroll.
|
|
201
|
+
if (e.target instanceof Node && listRef.current?.contains(e.target))
|
|
202
|
+
return
|
|
203
|
+
setOpen(false)
|
|
204
|
+
}
|
|
205
|
+
const onResize = () => setOpen(false)
|
|
206
|
+
document.addEventListener('pointerdown', onDown, true)
|
|
207
|
+
window.addEventListener('scroll', onScroll, true)
|
|
208
|
+
window.addEventListener('resize', onResize)
|
|
209
|
+
return () => {
|
|
210
|
+
document.removeEventListener('pointerdown', onDown, true)
|
|
211
|
+
window.removeEventListener('scroll', onScroll, true)
|
|
212
|
+
window.removeEventListener('resize', onResize)
|
|
213
|
+
}
|
|
214
|
+
}, [open])
|
|
215
|
+
|
|
216
|
+
// Drop a stale type-ahead timer on unmount.
|
|
217
|
+
useEffect(
|
|
218
|
+
() => () => {
|
|
219
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
220
|
+
},
|
|
221
|
+
[],
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const isPrintable = (e: ReactKeyboardEvent) =>
|
|
225
|
+
e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey
|
|
226
|
+
|
|
227
|
+
const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
228
|
+
if (disabled) return
|
|
229
|
+
if (!open) {
|
|
230
|
+
switch (e.key) {
|
|
231
|
+
case 'ArrowDown':
|
|
232
|
+
case 'ArrowUp':
|
|
233
|
+
case 'Enter':
|
|
234
|
+
case ' ':
|
|
235
|
+
case 'Spacebar':
|
|
236
|
+
e.preventDefault()
|
|
237
|
+
openAt(selectedIndex)
|
|
238
|
+
return
|
|
239
|
+
case 'Home':
|
|
240
|
+
e.preventDefault()
|
|
241
|
+
openAt(firstEnabled(opts))
|
|
242
|
+
return
|
|
243
|
+
case 'End':
|
|
244
|
+
e.preventDefault()
|
|
245
|
+
openAt(lastEnabled(opts))
|
|
246
|
+
return
|
|
247
|
+
default:
|
|
248
|
+
if (isPrintable(e)) {
|
|
249
|
+
e.preventDefault()
|
|
250
|
+
setOpen(true)
|
|
251
|
+
typeahead(e.key)
|
|
252
|
+
}
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
switch (e.key) {
|
|
257
|
+
case 'ArrowDown':
|
|
258
|
+
e.preventDefault()
|
|
259
|
+
setActiveIndex((i) => moveEnabled(opts, i, 1))
|
|
260
|
+
break
|
|
261
|
+
case 'ArrowUp':
|
|
262
|
+
e.preventDefault()
|
|
263
|
+
setActiveIndex((i) => moveEnabled(opts, i, -1))
|
|
264
|
+
break
|
|
265
|
+
case 'Home':
|
|
266
|
+
e.preventDefault()
|
|
267
|
+
setActiveIndex(firstEnabled(opts))
|
|
268
|
+
break
|
|
269
|
+
case 'End':
|
|
270
|
+
e.preventDefault()
|
|
271
|
+
setActiveIndex(lastEnabled(opts))
|
|
272
|
+
break
|
|
273
|
+
case 'Enter':
|
|
274
|
+
case ' ':
|
|
275
|
+
case 'Spacebar':
|
|
276
|
+
e.preventDefault()
|
|
277
|
+
commit(activeIndex)
|
|
278
|
+
break
|
|
279
|
+
case 'Escape':
|
|
280
|
+
e.preventDefault()
|
|
281
|
+
close()
|
|
282
|
+
break
|
|
283
|
+
case 'Tab':
|
|
284
|
+
// Commit the active option, then let focus move on naturally.
|
|
285
|
+
commit(activeIndex)
|
|
286
|
+
break
|
|
287
|
+
default:
|
|
288
|
+
if (isPrintable(e)) {
|
|
289
|
+
e.preventDefault()
|
|
290
|
+
typeahead(e.key)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const onTriggerClick = () => {
|
|
296
|
+
if (disabled) return
|
|
297
|
+
if (open) setOpen(false)
|
|
298
|
+
else openAt(selectedIndex)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const selected = opts[selectedIndex]
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<span className="dcui-selectmenu dcui-select" data-size={size}>
|
|
305
|
+
<div
|
|
306
|
+
ref={triggerRef}
|
|
307
|
+
role="combobox"
|
|
308
|
+
tabIndex={disabled ? -1 : 0}
|
|
309
|
+
aria-haspopup="listbox"
|
|
310
|
+
aria-expanded={open}
|
|
311
|
+
aria-controls={listboxId}
|
|
312
|
+
aria-label={ariaLabel}
|
|
313
|
+
aria-disabled={disabled || undefined}
|
|
314
|
+
aria-activedescendant={open ? optionId(activeIndex) : undefined}
|
|
315
|
+
className="dcui-select-el dcui-selectmenu-trigger"
|
|
316
|
+
onClick={onTriggerClick}
|
|
317
|
+
onKeyDown={onKeyDown}>
|
|
318
|
+
<span className="dcui-selectmenu-value">
|
|
319
|
+
{selected?.label ?? value}
|
|
320
|
+
</span>
|
|
321
|
+
</div>
|
|
322
|
+
<span className="dcui-select-caret" aria-hidden="true">
|
|
323
|
+
▾
|
|
324
|
+
</span>
|
|
325
|
+
{open &&
|
|
326
|
+
coords &&
|
|
327
|
+
createPortal(
|
|
328
|
+
<div
|
|
329
|
+
ref={listRef}
|
|
330
|
+
id={listboxId}
|
|
331
|
+
role="listbox"
|
|
332
|
+
aria-label={ariaLabel}
|
|
333
|
+
className="dcui-selectmenu-list"
|
|
334
|
+
data-size={size}
|
|
335
|
+
style={{
|
|
336
|
+
left: coords.left,
|
|
337
|
+
minWidth: coords.minWidth,
|
|
338
|
+
maxHeight: coords.maxHeight,
|
|
339
|
+
...(coords.placement === 'up'
|
|
340
|
+
? { top: coords.top, transform: 'translateY(-100%)' }
|
|
341
|
+
: { top: coords.top }),
|
|
342
|
+
}}>
|
|
343
|
+
{opts.map((o, i) => {
|
|
344
|
+
const isSelected = !o.disabled && o.value === value
|
|
345
|
+
return (
|
|
346
|
+
// Options are pointer affordances only: focus stays on the
|
|
347
|
+
// combobox trigger (aria-activedescendant) and all keyboard
|
|
348
|
+
// handling lives there, so these two rules don't apply here.
|
|
349
|
+
// biome-ignore lint/a11y/useFocusableInteractive: combobox keeps focus; option is tracked via aria-activedescendant
|
|
350
|
+
// biome-ignore lint/a11y/useKeyWithClickEvents: keyboard handled on the combobox trigger, not per-option
|
|
351
|
+
<div
|
|
352
|
+
key={o.value}
|
|
353
|
+
id={optionId(i)}
|
|
354
|
+
role="option"
|
|
355
|
+
aria-selected={isSelected}
|
|
356
|
+
aria-disabled={o.disabled || undefined}
|
|
357
|
+
data-active={!o.disabled && i === activeIndex}
|
|
358
|
+
className="dcui-selectmenu-option"
|
|
359
|
+
onMouseEnter={
|
|
360
|
+
o.disabled ? undefined : () => setActiveIndex(i)
|
|
361
|
+
}
|
|
362
|
+
// Keep focus on the combobox trigger (the menu commits on click).
|
|
363
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
364
|
+
onClick={o.disabled ? undefined : () => commit(i)}>
|
|
365
|
+
<span className="dcui-selectmenu-check" aria-hidden="true">
|
|
366
|
+
{isSelected ? '✓' : ''}
|
|
367
|
+
</span>
|
|
368
|
+
{o.label}
|
|
369
|
+
</div>
|
|
370
|
+
)
|
|
371
|
+
})}
|
|
372
|
+
</div>,
|
|
373
|
+
document.body,
|
|
374
|
+
)}
|
|
375
|
+
</span>
|
|
376
|
+
)
|
|
377
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display Case design-system components — "The Vitrine".
|
|
3
|
+
* Self-contained, pure React components (each brings its own CSS). Used by the
|
|
4
|
+
* browse chrome and dogfooded as Display Case's own showcased components.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type { ButtonProps, ButtonSize, ButtonVariant } from './controls/Button'
|
|
8
|
+
export { Button } from './controls/Button'
|
|
9
|
+
export type {
|
|
10
|
+
IconButtonProps,
|
|
11
|
+
IconButtonSize,
|
|
12
|
+
IconButtonVariant,
|
|
13
|
+
} from './controls/IconButton'
|
|
14
|
+
export { IconButton } from './controls/IconButton'
|
|
15
|
+
export type { InputProps, InputSize } from './controls/Input'
|
|
16
|
+
export { Input } from './controls/Input'
|
|
17
|
+
export type {
|
|
18
|
+
SelectItem,
|
|
19
|
+
SelectOption,
|
|
20
|
+
SelectOptionGroup,
|
|
21
|
+
SelectProps,
|
|
22
|
+
SelectSize,
|
|
23
|
+
} from './controls/Select'
|
|
24
|
+
export { Select } from './controls/Select'
|
|
25
|
+
export type {
|
|
26
|
+
SelectMenuOption,
|
|
27
|
+
SelectMenuProps,
|
|
28
|
+
SelectMenuSize,
|
|
29
|
+
} from './controls/SelectMenu'
|
|
30
|
+
export { SelectMenu } from './controls/SelectMenu'
|
|
31
|
+
// Reusable Primer foundation specimens (ramps, swatches, type scale, glyphs…)
|
|
32
|
+
// — generic, prop-driven primitives for building a Primer.
|
|
33
|
+
export * from './primer-specimen'
|
|
34
|
+
export type { A11yBadgeProps } from './showcase/A11yBadge'
|
|
35
|
+
export { A11yBadge } from './showcase/A11yBadge'
|
|
36
|
+
export type { A11yPanelProps } from './showcase/A11yPanel'
|
|
37
|
+
export { A11yPanel } from './showcase/A11yPanel'
|
|
38
|
+
export type { ChipProps, ChipVariant } from './showcase/Chip'
|
|
39
|
+
export { Chip } from './showcase/Chip'
|
|
40
|
+
export type { EyebrowProps, EyebrowTone } from './showcase/Eyebrow'
|
|
41
|
+
export { Eyebrow } from './showcase/Eyebrow'
|
|
42
|
+
export type { FlowNavProps, FlowStep } from './showcase/FlowNav'
|
|
43
|
+
export { FlowNav } from './showcase/FlowNav'
|
|
44
|
+
export type { ImpactTagProps } from './showcase/ImpactTag'
|
|
45
|
+
export { ImpactTag, impactRank } from './showcase/ImpactTag'
|
|
46
|
+
export type { NavItemKind, NavItemProps } from './showcase/NavItem'
|
|
47
|
+
export { NavItem } from './showcase/NavItem'
|
|
48
|
+
export type { RenderAddressProps } from './showcase/RenderAddress'
|
|
49
|
+
export { RenderAddress } from './showcase/RenderAddress'
|
|
50
|
+
export type {
|
|
51
|
+
SegmentedOption,
|
|
52
|
+
SegmentedToggleProps,
|
|
53
|
+
} from './showcase/SegmentedToggle'
|
|
54
|
+
export { SegmentedToggle } from './showcase/SegmentedToggle'
|
|
55
|
+
export type { SidebarProps } from './showcase/Sidebar'
|
|
56
|
+
export { Sidebar } from './showcase/Sidebar'
|
|
57
|
+
export type { StageProps } from './showcase/Stage'
|
|
58
|
+
export { Stage } from './showcase/Stage'
|
|
59
|
+
export type {
|
|
60
|
+
TweakItem,
|
|
61
|
+
TweaksMode,
|
|
62
|
+
TweaksPanelProps,
|
|
63
|
+
} from './showcase/TweaksPanel'
|
|
64
|
+
export { Row as TweakRow, TweaksPanel } from './showcase/TweaksPanel'
|
|
65
|
+
export type { WordmarkProps } from './showcase/Wordmark'
|
|
66
|
+
export { Wordmark } from './showcase/Wordmark'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineCases, tweak } from '@awarebydefault/display-case'
|
|
2
|
+
import { ColorRamp, type ColorStop } from './ColorRamp'
|
|
3
|
+
|
|
4
|
+
const marigold: ColorStop[] = [
|
|
5
|
+
{ name: 'marigold-300', color: 'var(--dc-marigold-300)', caption: '#f6c878' },
|
|
6
|
+
{ name: 'marigold-400', color: 'var(--dc-marigold-400)', caption: '#f0a23b' },
|
|
7
|
+
{ name: 'marigold-500', color: 'var(--dc-marigold-500)', caption: '#e0820b' },
|
|
8
|
+
{
|
|
9
|
+
name: 'marigold-600',
|
|
10
|
+
color: 'var(--dc-marigold-600)',
|
|
11
|
+
caption: '#c2690a',
|
|
12
|
+
star: true,
|
|
13
|
+
},
|
|
14
|
+
{ name: 'marigold-700', color: 'var(--dc-marigold-700)', caption: '#9a4f0a' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const paper: ColorStop[] = [
|
|
18
|
+
{ name: 'paper-100', color: 'var(--dc-paper-100)', caption: '#f4f1e9' },
|
|
19
|
+
{ name: 'paper-300', color: 'var(--dc-paper-300)', caption: '#d6cebe' },
|
|
20
|
+
{ name: 'paper-500', color: 'var(--dc-paper-500)', caption: '#8a8073' },
|
|
21
|
+
{ name: 'paper-700', color: 'var(--dc-paper-700)', caption: '#4a423a' },
|
|
22
|
+
{ name: 'paper-900', color: 'var(--dc-paper-900)', caption: '#211d18' },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export default defineCases(
|
|
26
|
+
'ColorRamp',
|
|
27
|
+
{
|
|
28
|
+
Playground: {
|
|
29
|
+
tweaks: {
|
|
30
|
+
chipHeight: tweak.number(56),
|
|
31
|
+
star: tweak.boolean(true),
|
|
32
|
+
},
|
|
33
|
+
render: (t) => (
|
|
34
|
+
<ColorRamp
|
|
35
|
+
chipHeight={t.chipHeight}
|
|
36
|
+
stops={marigold.map((s) => ({ ...s, star: s.star && t.star }))}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
},
|
|
40
|
+
Accent: () => <ColorRamp stops={marigold} />,
|
|
41
|
+
Neutral: () => <ColorRamp stops={paper} chipHeight={48} />,
|
|
42
|
+
},
|
|
43
|
+
{ level: 'molecule' },
|
|
44
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
**ColorRamp** — a horizontal ramp of an ORDERED single-family palette (an accent ramp, a neutral ramp), each stop a chip over a label and optional caption. Reach for it when the stops have a meaningful sequence.
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<ColorRamp
|
|
5
|
+
chipHeight={56}
|
|
6
|
+
stops={[
|
|
7
|
+
{ name: 'marigold-500', color: 'var(--dc-marigold-500)', caption: '#e0820b' },
|
|
8
|
+
{ name: 'marigold-600', color: 'var(--dc-marigold-600)', caption: '#c2690a', star: true },
|
|
9
|
+
]}
|
|
10
|
+
/>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For unordered semantic role tokens, reach for `SwatchGrid`.
|
|
14
|
+
|
|
15
|
+
Keep each `color` as the whole `var(--dc-…)` literal so the token check resolves it. `star` marks the canonical stop with a marigold ★.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display Case — ColorRamp
|
|
3
|
+
* A horizontal ramp of colour stops with a label + caption under each chip.
|
|
4
|
+
* Generic specimen primitive for a Primer: feed it any ordered set of stops
|
|
5
|
+
* (an accent ramp, a neutral ramp, a brand palette) and it renders the swatches.
|
|
6
|
+
*
|
|
7
|
+
* Pass each stop's colour as a complete CSS value (`var(--dc-marigold-600)` or
|
|
8
|
+
* `#c2690a`) — keep the whole `var(...)` string literal rather than templating
|
|
9
|
+
* it, so the token-conformance check can statically resolve the reference.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface ColorStop {
|
|
13
|
+
/** Display label under the chip (also the React key). */
|
|
14
|
+
name: string
|
|
15
|
+
/** Complete CSS colour value painted on the chip, e.g. `var(--dc-marigold-600)`. */
|
|
16
|
+
color: string
|
|
17
|
+
/** Optional caption under the name — typically the resolved hex. */
|
|
18
|
+
caption?: string
|
|
19
|
+
/** Marks the canonical stop with a marigold star. */
|
|
20
|
+
star?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ColorRampProps {
|
|
24
|
+
stops: ColorStop[]
|
|
25
|
+
/** Chip height in pixels. */
|
|
26
|
+
chipHeight?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ColorRamp({ stops, chipHeight = 56 }: ColorRampProps) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className="dcpl-ramp"
|
|
33
|
+
style={{ gridTemplateColumns: `repeat(${stops.length}, 1fr)` }}>
|
|
34
|
+
{stops.map((s) => (
|
|
35
|
+
<div className="dcpl-sw" key={s.name}>
|
|
36
|
+
<div
|
|
37
|
+
className="dcpl-sw-chip"
|
|
38
|
+
style={{ height: `${chipHeight}px`, background: s.color }}
|
|
39
|
+
/>
|
|
40
|
+
<div className="dcpl-sw-meta">
|
|
41
|
+
<div className="dcpl-sw-name">
|
|
42
|
+
{s.name}
|
|
43
|
+
{s.star ? <span className="dcpl-sw-star"> ★</span> : null}
|
|
44
|
+
</div>
|
|
45
|
+
{s.caption ? <div className="dcpl-sw-hex">{s.caption}</div> : null}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineCases, tweak } from '@awarebydefault/display-case'
|
|
2
|
+
import { type DefEntry, DefinitionList } from './DefinitionList'
|
|
3
|
+
|
|
4
|
+
const voice: DefEntry[] = [
|
|
5
|
+
{
|
|
6
|
+
term: 'Voice',
|
|
7
|
+
description:
|
|
8
|
+
'Plain, confident, technical-but-warm. It explains why, briefly, then moves on.',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
term: 'Casing',
|
|
12
|
+
description: (
|
|
13
|
+
<>
|
|
14
|
+
Sentence case everywhere. The one exception is the{' '}
|
|
15
|
+
<strong>eyebrow label</strong>: uppercase mono with wide tracking.
|
|
16
|
+
</>
|
|
17
|
+
),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
term: 'Length',
|
|
21
|
+
description:
|
|
22
|
+
'Terse. Buttons are one or two words; labels are a single token.',
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export default defineCases(
|
|
27
|
+
'DefinitionList',
|
|
28
|
+
{
|
|
29
|
+
Playground: {
|
|
30
|
+
tweaks: { termWidth: tweak.text('7.5rem') },
|
|
31
|
+
render: (t) => (
|
|
32
|
+
<DefinitionList entries={voice} termWidth={t.termWidth || undefined} />
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
Voice: () => <DefinitionList entries={voice} />,
|
|
36
|
+
},
|
|
37
|
+
{ level: 'molecule' },
|
|
38
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
**DefinitionList** — a bordered list of term / description rows, the term a mono uppercase accent eyebrow; reach for it to lay out voice rules, content fundamentals, or any keyed reference prose.
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<DefinitionList
|
|
5
|
+
termWidth="7.5rem"
|
|
6
|
+
entries={[
|
|
7
|
+
{ term: 'Voice', description: 'Plain, confident, technical-but-warm.' },
|
|
8
|
+
{ term: 'Casing', description: <>Sentence case, except the <strong>eyebrow</strong>.</> },
|
|
9
|
+
]}
|
|
10
|
+
/>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Use this for free-form prose keyed by a term. For a fixed legend of dot-keyed statuses, reach for `StatusList`.
|
|
14
|
+
|
|
15
|
+
`description` accepts rich nodes (`<strong>` and other emphasis welcome), not just plain text.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Display Case — DefinitionList
|
|
5
|
+
* A bordered list of term / description rows. The term is a mono uppercase
|
|
6
|
+
* eyebrow in the accent colour; the description is body text that may include
|
|
7
|
+
* `<strong>` emphasis. Generic specimen primitive for a Primer — use it to lay
|
|
8
|
+
* out voice rules, content fundamentals, or any keyed reference.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface DefEntry {
|
|
12
|
+
/** Mono uppercase term shown in the accent colour (also the React key). */
|
|
13
|
+
term: string
|
|
14
|
+
/** Description body — plain text or rich nodes (e.g. `<strong>`). */
|
|
15
|
+
description: ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DefinitionListProps {
|
|
19
|
+
entries: DefEntry[]
|
|
20
|
+
/** Width of the term column (any CSS length). */
|
|
21
|
+
termWidth?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function DefinitionList({
|
|
25
|
+
entries,
|
|
26
|
+
termWidth = '7.5rem',
|
|
27
|
+
}: DefinitionListProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="dcpl-deflist">
|
|
30
|
+
{entries.map((e) => (
|
|
31
|
+
<div
|
|
32
|
+
className="dcpl-defrow"
|
|
33
|
+
key={e.term}
|
|
34
|
+
style={{ '--dcpl-term-width': termWidth } as CSSProperties}>
|
|
35
|
+
<div className="dcpl-defterm">{e.term}</div>
|
|
36
|
+
<div className="dcpl-defdesc">{e.description}</div>
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineCases } from '@awarebydefault/display-case'
|
|
2
|
+
import { FontFamilies, type FontFamily } from './FontFamilies'
|
|
3
|
+
|
|
4
|
+
const pairing: FontFamily[] = [
|
|
5
|
+
{
|
|
6
|
+
tag: 'Sans · UI',
|
|
7
|
+
sample: 'Display Case shows the work, not itself',
|
|
8
|
+
note: 'Hanken Grotesk, ui-sans-serif, system-ui…',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
tag: 'Mono · Code',
|
|
12
|
+
sample: '/render/<component>/<case>?theme=dark',
|
|
13
|
+
note: 'JetBrains Mono, ui-monospace, "SF Mono", Menlo…',
|
|
14
|
+
mono: true,
|
|
15
|
+
},
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export default defineCases(
|
|
19
|
+
'FontFamilies',
|
|
20
|
+
{
|
|
21
|
+
Pairing: () => <FontFamilies families={pairing} />,
|
|
22
|
+
},
|
|
23
|
+
{ level: 'molecule' },
|
|
24
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
**FontFamilies** — a stack of font-family rows, each a mono tag, a large sample in the family, and a mono note listing the stack; reach for it to document a type pairing (a UI sans, a code mono).
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<FontFamilies
|
|
5
|
+
families={[
|
|
6
|
+
{ tag: 'Sans · UI', sample: 'The quick brown fox', note: 'Hanken Grotesk, system-ui…' },
|
|
7
|
+
{ tag: 'Mono · Code', sample: 'render(case)', note: 'JetBrains Mono…', mono: true },
|
|
8
|
+
]}
|
|
9
|
+
/>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
This documents which families exist. For size steps reach for `TypeScale`; for weights reach for `WeightSpecimen`.
|