@misoto22/folio 0.16.0 → 0.17.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/dist/agent/Accordion.md +1 -1
- package/dist/agent/Actions.md +1 -1
- package/dist/agent/Alert.md +1 -1
- package/dist/agent/AppShell.md +1 -1
- package/dist/agent/ArchitectureFigure.md +1 -1
- package/dist/agent/AreaChart.md +1 -1
- package/dist/agent/Article.md +1 -1
- package/dist/agent/AspectRatio.md +1 -1
- package/dist/agent/Avatar.md +1 -1
- package/dist/agent/Badge.md +1 -1
- package/dist/agent/BarChart.md +1 -1
- package/dist/agent/BarList.md +1 -1
- package/dist/agent/BigNumber.md +1 -1
- package/dist/agent/BoxPlot.md +1 -1
- package/dist/agent/Breadcrumb.md +1 -1
- package/dist/agent/BulletChart.md +1 -1
- package/dist/agent/Button.md +1 -1
- package/dist/agent/Calendar.md +1 -1
- package/dist/agent/CalendarHeatmap.md +1 -1
- package/dist/agent/Card.md +1 -1
- package/dist/agent/Checkbox.md +1 -1
- package/dist/agent/Code.md +1 -1
- package/dist/agent/CodeBlock.md +1 -1
- package/dist/agent/Collapsible.md +1 -1
- package/dist/agent/Collection.md +1 -1
- package/dist/agent/ColorPicker.md +1 -1
- package/dist/agent/Combobox.md +1 -1
- package/dist/agent/Command.md +1 -1
- package/dist/agent/ComposedChart.md +1 -1
- package/dist/agent/Contact.md +1 -1
- package/dist/agent/Content.md +1 -1
- package/dist/agent/ContextMenu.md +1 -1
- package/dist/agent/Conversation.md +1 -1
- package/dist/agent/DataflowFigure.md +1 -1
- package/dist/agent/DatePicker.md +1 -1
- package/dist/agent/DescriptionList.md +1 -1
- package/dist/agent/Diagram.md +1 -1
- package/dist/agent/DiagramCanvas.md +1 -1
- package/dist/agent/DiagramExportMenu.md +1 -1
- package/dist/agent/DiagramInspector.md +1 -1
- package/dist/agent/DiagramLegend.md +1 -1
- package/dist/agent/DiagramMinimap.md +1 -1
- package/dist/agent/DiagramToolbar.md +1 -1
- package/dist/agent/Dialog.md +1 -1
- package/dist/agent/DropdownMenu.md +1 -1
- package/dist/agent/EmptyState.md +1 -1
- package/dist/agent/ErrorState.md +1 -1
- package/dist/agent/Evidence.md +1 -1
- package/dist/agent/Facet.md +1 -1
- package/dist/agent/Field.md +1 -1
- package/dist/agent/FigureBand.md +1 -1
- package/dist/agent/FloatingIconButton.md +1 -1
- package/dist/agent/FunnelChart.md +1 -1
- package/dist/agent/Heading.md +1 -1
- package/dist/agent/Heatmap.md +1 -1
- package/dist/agent/Histogram.md +1 -1
- package/dist/agent/Input.md +1 -1
- package/dist/agent/Kbd.md +1 -1
- package/dist/agent/LifecycleFigure.md +1 -1
- package/dist/agent/LineChart.md +1 -1
- package/dist/agent/LinkArrow.md +1 -1
- package/dist/agent/Markdown.md +1 -1
- package/dist/agent/Media.md +1 -1
- package/dist/agent/Metric.md +1 -1
- package/dist/agent/Metrics.md +1 -1
- package/dist/agent/Music.md +1 -1
- package/dist/agent/NativeSelect.md +1 -1
- package/dist/agent/NavItem.md +1 -1
- package/dist/agent/NumberField.md +1 -1
- package/dist/agent/PageHeader.md +1 -1
- package/dist/agent/Pagination.md +1 -1
- package/dist/agent/PieChart.md +1 -1
- package/dist/agent/Popover.md +1 -1
- package/dist/agent/Portfolio.md +1 -1
- package/dist/agent/PortfolioIndex.md +1 -1
- package/dist/agent/Progress.md +1 -1
- package/dist/agent/RadarChart.md +1 -1
- package/dist/agent/RadialChart.md +1 -1
- package/dist/agent/RadioGroup.md +1 -1
- package/dist/agent/Reading.md +1 -1
- package/dist/agent/Recovery.md +1 -1
- package/dist/agent/SankeyChart.md +1 -1
- package/dist/agent/ScatterChart.md +1 -1
- package/dist/agent/ScrollArea.md +1 -1
- package/dist/agent/SearchPalette.md +1 -1
- package/dist/agent/SearchableMenu.md +1 -1
- package/dist/agent/Select.md +1 -1
- package/dist/agent/Separator.md +1 -1
- package/dist/agent/SequenceFigure.md +1 -1
- package/dist/agent/Sheet.md +1 -1
- package/dist/agent/Sidebar.md +1 -1
- package/dist/agent/SiteNavigation.md +1 -1
- package/dist/agent/SiteShell.md +1 -1
- package/dist/agent/Skeleton.md +1 -1
- package/dist/agent/Slider.md +1 -1
- package/dist/agent/Sparkline.md +1 -1
- package/dist/agent/Spinner.md +1 -1
- package/dist/agent/StatusDot.md +1 -1
- package/dist/agent/StatusPill.md +1 -1
- package/dist/agent/Steps.md +1 -1
- package/dist/agent/Switch.md +1 -1
- package/dist/agent/Table.md +1 -1
- package/dist/agent/Tabs.md +1 -1
- package/dist/agent/Tag.md +1 -1
- package/dist/agent/Text.md +1 -1
- package/dist/agent/Textarea.md +1 -1
- package/dist/agent/Timeline.md +1 -1
- package/dist/agent/Timestamp.md +1 -1
- package/dist/agent/Toast.md +1 -1
- package/dist/agent/ToggleGroup.md +1 -1
- package/dist/agent/Toolbar.md +1 -1
- package/dist/agent/Tooltip.md +1 -1
- package/dist/agent/TreemapChart.md +1 -1
- package/dist/agent/WaterfallChart.md +1 -1
- package/dist/agent/Workbench.md +142 -0
- package/dist/agent/WorkflowFigure.md +1 -1
- package/dist/agent/catalog.json +51 -2
- package/dist/agent/index.md +6 -3
- package/dist/components/Badge/Badge.js +1 -1
- package/dist/components/Badge/Badge.js.map +1 -1
- package/dist/components/Button/Button.js +1 -1
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Heading/Heading.js +1 -1
- package/dist/components/Heading/Heading.js.map +1 -1
- package/dist/components/Tag/Tag.js +2 -2
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/semantic.css +7 -0
- package/dist/styles.css +1 -1
- package/dist/themes.css +93 -0
- package/dist/tokens.css +16 -2
- package/dist/tokens.d.ts +7 -0
- package/dist/tokens.js +38 -2
- package/dist/tokens.json +38 -2
- package/dist/website/Workbench/Chronology.d.ts +32 -0
- package/dist/website/Workbench/Chronology.js +32 -0
- package/dist/website/Workbench/Chronology.js.map +1 -0
- package/dist/website/Workbench/ContactRow.d.ts +13 -0
- package/dist/website/Workbench/ContactRow.js +11 -0
- package/dist/website/Workbench/ContactRow.js.map +1 -0
- package/dist/website/Workbench/PhotoMosaic.d.ts +13 -0
- package/dist/website/Workbench/PhotoMosaic.js +15 -0
- package/dist/website/Workbench/PhotoMosaic.js.map +1 -0
- package/dist/website/Workbench/Reveal.d.ts +19 -0
- package/dist/website/Workbench/Reveal.js +29 -0
- package/dist/website/Workbench/Reveal.js.map +1 -0
- package/dist/website/Workbench/ToolStrip.d.ts +18 -0
- package/dist/website/Workbench/ToolStrip.js +15 -0
- package/dist/website/Workbench/ToolStrip.js.map +1 -0
- package/dist/website/Workbench/Workbench.d.ts +23 -0
- package/dist/website/Workbench/Workbench.js +22 -0
- package/dist/website/Workbench/Workbench.js.map +1 -0
- package/dist/website/Workbench/WorkbenchCard.d.ts +29 -0
- package/dist/website/Workbench/WorkbenchCard.js +21 -0
- package/dist/website/Workbench/WorkbenchCard.js.map +1 -0
- package/dist/website/Workbench/WorkbenchEntries.d.ts +22 -0
- package/dist/website/Workbench/WorkbenchEntries.js +24 -0
- package/dist/website/Workbench/WorkbenchEntries.js.map +1 -0
- package/dist/website/Workbench/WorkbenchFigure.d.ts +20 -0
- package/dist/website/Workbench/WorkbenchFigure.js +15 -0
- package/dist/website/Workbench/WorkbenchFigure.js.map +1 -0
- package/dist/website/Workbench/WorkbenchHero.d.ts +24 -0
- package/dist/website/Workbench/WorkbenchHero.js +24 -0
- package/dist/website/Workbench/WorkbenchHero.js.map +1 -0
- package/dist/website/Workbench/theme.d.ts +22 -0
- package/dist/website/Workbench/theme.js +12 -0
- package/dist/website/Workbench/theme.js.map +1 -0
- package/dist/website/index.d.ts +11 -0
- package/dist/website/index.js +11 -0
- package/dist/website/index.js.map +1 -1
- package/dist/website-collection.css +6 -6
- package/dist/website-content.css +4 -1
- package/dist/website-conversation.css +1 -1
- package/dist/website-media.css +12 -12
- package/dist/website-portfolio.css +4 -4
- package/dist/website-reading.css +10 -10
- package/dist/website-shell.css +1 -1
- package/dist/website-workbench.css +162 -0
- package/dist/website.css +1 -0
- package/package.json +1 -1
- package/skills/folio-design/rules/tokens.md +4 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Workbench
|
|
2
|
+
|
|
3
|
+
Section rhythm, hero, cards, entries, timeline, mosaic and scroll reveal for the Workbench preset.
|
|
4
|
+
|
|
5
|
+
- Kind: pattern
|
|
6
|
+
- Group: Website
|
|
7
|
+
- Import: `import { WorkbenchSection } from '@misoto22/folio/website'`
|
|
8
|
+
- Version: 0.17.0
|
|
9
|
+
- Docs: https://ui.misoto22.com/components/workbench/
|
|
10
|
+
|
|
11
|
+
## When to reach for it
|
|
12
|
+
|
|
13
|
+
Lay a portfolio page on one 12-column grid with two-tone headings, and apply WORKBENCH_THEME on the root.
|
|
14
|
+
|
|
15
|
+
## Best practices
|
|
16
|
+
|
|
17
|
+
### Do
|
|
18
|
+
|
|
19
|
+
- Wrap each section in WorkbenchSection so headings and content share the same columns.
|
|
20
|
+
|
|
21
|
+
### Don’t
|
|
22
|
+
|
|
23
|
+
- Do not hide content until JavaScript runs; Reveal only hides elements that start below the fold after hydration.
|
|
24
|
+
|
|
25
|
+
## WorkbenchSection
|
|
26
|
+
|
|
27
|
+
The Workbench section: a hairline, a two-tone heading in the first four columns, and the content on an eight-column subgrid beside it. Every section on a Workbench page uses this one rhythm, which is what keeps them aligned.
|
|
28
|
+
|
|
29
|
+
### Props
|
|
30
|
+
|
|
31
|
+
- `title` (required) — `ReactNode`. The heading's first, ink-coloured clause.
|
|
32
|
+
- `lead` — `ReactNode`. The muted continuation that follows the title in the same line.
|
|
33
|
+
- `action` — `ReactNode`. One accent link under the heading, such as "All projects →".
|
|
34
|
+
- `level` — `2 | 3` default `2`. Heading level; sections on a page are h2.
|
|
35
|
+
- `id` — `string`.
|
|
36
|
+
- `children` (required) — `ReactNode`.
|
|
37
|
+
|
|
38
|
+
## Chronology
|
|
39
|
+
|
|
40
|
+
Roles and studies as bars on one year axis, one lane each. Roles are filled, studies outlined, and a current role is ink with an accent end. The bars are a list underneath, so a screen reader hears titles and dates rather than geometry.
|
|
41
|
+
|
|
42
|
+
### Props
|
|
43
|
+
|
|
44
|
+
- `label` (required) — `string`. Accessible name of the chart.
|
|
45
|
+
- `from` (required) — `number`. First and last year on the axis, inclusive.
|
|
46
|
+
- `to` (required) — `number`.
|
|
47
|
+
- `items` (required) — `ChronologyItem[]`.
|
|
48
|
+
- `currentLabel` — `ReactNode`. Text marking a current item, such as "Now".
|
|
49
|
+
|
|
50
|
+
## ContactRow
|
|
51
|
+
|
|
52
|
+
An address and one action on a single line of the content subgrid.
|
|
53
|
+
|
|
54
|
+
### Props
|
|
55
|
+
|
|
56
|
+
- `address` (required) — `ReactNode`. The address, set large — usually a `mailto:` link.
|
|
57
|
+
- `action` — `ReactNode`. One button beside it.
|
|
58
|
+
|
|
59
|
+
## PhotoMosaic
|
|
60
|
+
|
|
61
|
+
One large photograph beside a 2×2 of smaller ones, all on the content subgrid.
|
|
62
|
+
|
|
63
|
+
### Props
|
|
64
|
+
|
|
65
|
+
- `children` (required) — `ReactNode[]`. Exactly the photographs to show: the first is the large one, the next four fill a 2×2 beside it.
|
|
66
|
+
- `caption` — `ReactNode`. Caption under the large photograph.
|
|
67
|
+
|
|
68
|
+
## Reveal
|
|
69
|
+
|
|
70
|
+
Fades and lifts its child the first time it scrolls into view. The server renders the child visible. Only after hydration, and only for an element that starts below the fold, is it hidden and then revealed — so a page without JavaScript, a crawler and anything already on screen never see an empty frame. Reduced motion skips the whole thing, and the duration is the motion axis's own, so `data-motion="still"` makes it instant.
|
|
71
|
+
|
|
72
|
+
### Props
|
|
73
|
+
|
|
74
|
+
- `children` (required) — `ReactElement`. One element; it keeps its own tag and receives the reveal state.
|
|
75
|
+
|
|
76
|
+
## ToolStrip
|
|
77
|
+
|
|
78
|
+
A row of equal tiles, one monochrome tool mark each, under a centred caption.
|
|
79
|
+
|
|
80
|
+
### Props
|
|
81
|
+
|
|
82
|
+
- `label` (required) — `ReactNode`.
|
|
83
|
+
- `items` (required) — `ToolStripItem[]`.
|
|
84
|
+
|
|
85
|
+
## WorkbenchCard
|
|
86
|
+
|
|
87
|
+
An image-led record. The title's link is stretched over the card, so the card is one target without nesting interactive elements.
|
|
88
|
+
|
|
89
|
+
### Props
|
|
90
|
+
|
|
91
|
+
- `cover` (required) — `ReactNode`. A cover image, or a `WorkbenchTile` when there is no picture.
|
|
92
|
+
- `title` (required) — `ReactNode`. The title, usually a link; the whole card becomes its hit area.
|
|
93
|
+
- `description` — `ReactNode`.
|
|
94
|
+
- `meta` — `ReactNode`.
|
|
95
|
+
|
|
96
|
+
## WorkbenchCardGrid
|
|
97
|
+
|
|
98
|
+
Two cards per row on the content subgrid, one on a phone.
|
|
99
|
+
|
|
100
|
+
### Props
|
|
101
|
+
|
|
102
|
+
- `children` (required) — `ReactNode`.
|
|
103
|
+
|
|
104
|
+
## WorkbenchTile
|
|
105
|
+
|
|
106
|
+
A typographic stand-in cover: ink ground, a fine grid, one word.
|
|
107
|
+
|
|
108
|
+
### Props
|
|
109
|
+
|
|
110
|
+
- `label` (required) — `ReactNode`. Large word set on the tile, such as a product name.
|
|
111
|
+
|
|
112
|
+
## WorkbenchEntries
|
|
113
|
+
|
|
114
|
+
A featured entry set as a tile, with the next few entries stacked beside it.
|
|
115
|
+
|
|
116
|
+
### Props
|
|
117
|
+
|
|
118
|
+
- `featured` — `WorkbenchEntry`. Shown as a typographic notebook tile; the rest list beside it.
|
|
119
|
+
- `entries` (required) — `WorkbenchEntry[]`.
|
|
120
|
+
|
|
121
|
+
## WorkbenchFigure
|
|
122
|
+
|
|
123
|
+
A photograph shown as a photograph: its own proportion, an 8px corner, and a caption line beneath — never a backdrop for something else.
|
|
124
|
+
|
|
125
|
+
### Props
|
|
126
|
+
|
|
127
|
+
- `children` (required) — `ReactElement`. The image element (or a host image component); it fills the frame.
|
|
128
|
+
- `title` — `ReactNode`. Caption start: the photograph's title.
|
|
129
|
+
- `meta` — `ReactNode`. Caption end: place and year, right-aligned.
|
|
130
|
+
- `ratio` — `'wide' | 'landscape' | 'portrait' | 'panorama'` default `'landscape'`. Frame proportion.
|
|
131
|
+
|
|
132
|
+
## WorkbenchHero
|
|
133
|
+
|
|
134
|
+
The Workbench opening: a two-tone statement, its actions, a few counts and one photograph.
|
|
135
|
+
|
|
136
|
+
### Props
|
|
137
|
+
|
|
138
|
+
- `statement` (required) — `ReactNode`. The ink clause of the statement; it is the page's h1.
|
|
139
|
+
- `lead` — `ReactNode`. The muted continuation of the statement.
|
|
140
|
+
- `actions` — `ReactNode`. Up to two buttons or links, primary first.
|
|
141
|
+
- `stats` — `WorkbenchStat[]` default `[]`. Small counts beside the statement, such as projects and posts.
|
|
142
|
+
- `figure` — `ReactNode`. One captioned photograph under the statement — a `WorkbenchFigure`.
|
|
@@ -5,7 +5,7 @@ A process in lanes and phases, with the main path drawn heavier than its branche
|
|
|
5
5
|
- Kind: component
|
|
6
6
|
- Group: Diagrams
|
|
7
7
|
- Import: `import { WorkflowFigure } from '@misoto22/folio/diagrams'`
|
|
8
|
-
- Version: 0.
|
|
8
|
+
- Version: 0.17.0
|
|
9
9
|
- Docs: https://ui.misoto22.com/components/workflow-figure/
|
|
10
10
|
- Related: lifecycle-figure, sequence-figure, steps
|
|
11
11
|
|
package/dist/agent/catalog.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.17.0",
|
|
3
3
|
"groups": [
|
|
4
4
|
"Actions",
|
|
5
5
|
"Forms",
|
|
@@ -7507,6 +7507,25 @@
|
|
|
7507
7507
|
}
|
|
7508
7508
|
],
|
|
7509
7509
|
"kind": "pattern"
|
|
7510
|
+
},
|
|
7511
|
+
{
|
|
7512
|
+
"slug": "workbench",
|
|
7513
|
+
"entry": "@misoto22/folio/website",
|
|
7514
|
+
"name": "Workbench",
|
|
7515
|
+
"group": "Website",
|
|
7516
|
+
"summary": "Section rhythm, hero, cards, entries, timeline, mosaic and scroll reveal for the Workbench preset.",
|
|
7517
|
+
"when": "Lay a portfolio page on one 12-column grid with two-tone headings, and apply WORKBENCH_THEME on the root.",
|
|
7518
|
+
"practices": [
|
|
7519
|
+
{
|
|
7520
|
+
"kind": "do",
|
|
7521
|
+
"text": "Wrap each section in WorkbenchSection so headings and content share the same columns."
|
|
7522
|
+
},
|
|
7523
|
+
{
|
|
7524
|
+
"kind": "dont",
|
|
7525
|
+
"text": "Do not hide content until JavaScript runs; Reveal only hides elements that start below the fold after hydration."
|
|
7526
|
+
}
|
|
7527
|
+
],
|
|
7528
|
+
"kind": "pattern"
|
|
7510
7529
|
}
|
|
7511
7530
|
],
|
|
7512
7531
|
"exports": {
|
|
@@ -7892,7 +7911,19 @@
|
|
|
7892
7911
|
"ProfileInterests": "Timeline",
|
|
7893
7912
|
"ProfileFigure": "Timeline",
|
|
7894
7913
|
"Timeline": "Timeline",
|
|
7895
|
-
"TimelineItem": "Timeline"
|
|
7914
|
+
"TimelineItem": "Timeline",
|
|
7915
|
+
"WorkbenchSection": "Workbench",
|
|
7916
|
+
"Chronology": "Workbench",
|
|
7917
|
+
"ContactRow": "Workbench",
|
|
7918
|
+
"PhotoMosaic": "Workbench",
|
|
7919
|
+
"Reveal": "Workbench",
|
|
7920
|
+
"ToolStrip": "Workbench",
|
|
7921
|
+
"WorkbenchCard": "Workbench",
|
|
7922
|
+
"WorkbenchCardGrid": "Workbench",
|
|
7923
|
+
"WorkbenchTile": "Workbench",
|
|
7924
|
+
"WorkbenchEntries": "Workbench",
|
|
7925
|
+
"WorkbenchFigure": "Workbench",
|
|
7926
|
+
"WorkbenchHero": "Workbench"
|
|
7896
7927
|
},
|
|
7897
7928
|
"themeAxes": [
|
|
7898
7929
|
{
|
|
@@ -7900,6 +7931,7 @@
|
|
|
7900
7931
|
"values": [
|
|
7901
7932
|
"clay",
|
|
7902
7933
|
"cobalt",
|
|
7934
|
+
"ember",
|
|
7903
7935
|
"forest",
|
|
7904
7936
|
"ink",
|
|
7905
7937
|
"moss",
|
|
@@ -7907,6 +7939,14 @@
|
|
|
7907
7939
|
],
|
|
7908
7940
|
"unset": "ink"
|
|
7909
7941
|
},
|
|
7942
|
+
{
|
|
7943
|
+
"axis": "data-action",
|
|
7944
|
+
"values": [
|
|
7945
|
+
"accent",
|
|
7946
|
+
"ink"
|
|
7947
|
+
],
|
|
7948
|
+
"unset": "accent"
|
|
7949
|
+
},
|
|
7910
7950
|
{
|
|
7911
7951
|
"axis": "data-chart-palette",
|
|
7912
7952
|
"values": [
|
|
@@ -7961,6 +8001,7 @@
|
|
|
7961
8001
|
"values": [
|
|
7962
8002
|
"cool",
|
|
7963
8003
|
"glass",
|
|
8004
|
+
"linen",
|
|
7964
8005
|
"paper",
|
|
7965
8006
|
"warm"
|
|
7966
8007
|
],
|
|
@@ -7981,6 +8022,14 @@
|
|
|
7981
8022
|
"grotesk"
|
|
7982
8023
|
],
|
|
7983
8024
|
"unset": "editorial"
|
|
8025
|
+
},
|
|
8026
|
+
{
|
|
8027
|
+
"axis": "data-voice",
|
|
8028
|
+
"values": [
|
|
8029
|
+
"display",
|
|
8030
|
+
"quiet"
|
|
8031
|
+
],
|
|
8032
|
+
"unset": "display"
|
|
7984
8033
|
}
|
|
7985
8034
|
],
|
|
7986
8035
|
"warningCodes": [
|
package/dist/agent/index.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @misoto22/folio 0.
|
|
1
|
+
# @misoto22/folio 0.17.0
|
|
2
2
|
|
|
3
3
|
> Monochrome design system: portable CSS tokens and accessible React 19
|
|
4
4
|
> primitives. Components ship compiled — import them, do not copy them.
|
|
@@ -16,16 +16,18 @@ npx @misoto22/folio docs <Component>
|
|
|
16
16
|
Attributes, independent, and they work on any element — not just the root.
|
|
17
17
|
An unset axis is the default.
|
|
18
18
|
|
|
19
|
-
- `data-accent`: `clay` `cobalt` `forest` `ink` `moss` `plum` (unset = ink)
|
|
19
|
+
- `data-accent`: `clay` `cobalt` `ember` `forest` `ink` `moss` `plum` (unset = ink)
|
|
20
|
+
- `data-action`: `accent` `ink` (unset = accent)
|
|
20
21
|
- `data-chart-palette`: `accent` `chroma` (unset = the neutral series ramp)
|
|
21
22
|
- `data-density`: `comfortable` `compact` (unset = comfortable)
|
|
22
23
|
- `data-mode`: `dark` `light` (unset = follows the app)
|
|
23
24
|
- `data-motion`: `snappy` `still` (unset = calm)
|
|
24
25
|
- `data-radius`: `round` `sharp` (unset = the default radius ladder)
|
|
25
26
|
- `data-rules`: `firm` `hairline` `quiet` (unset = hairline)
|
|
26
|
-
- `data-surface`: `cool` `glass` `paper` `warm` (unset = paper)
|
|
27
|
+
- `data-surface`: `cool` `glass` `linen` `paper` `warm` (unset = paper)
|
|
27
28
|
- `data-table-density`: `compact` (unset = comfortable — whatever `data-density` is doing around it)
|
|
28
29
|
- `data-type`: `bookish` `editorial` `grotesk` (unset = editorial)
|
|
30
|
+
- `data-voice`: `display` `quiet` (unset = display)
|
|
29
31
|
|
|
30
32
|
`data-accent` re-points one token, `--clay`, which every component reads
|
|
31
33
|
through `--accent`. A brand the package has never seen re-points it in a
|
|
@@ -180,3 +182,4 @@ stylesheet instead; the attribute is the shipped set, not the only way in.
|
|
|
180
182
|
- **Actions** — Share action menus and a scroll-to-top control.
|
|
181
183
|
- **Conversation** — Conversation threads, answers, citations, composers and floating chat surfaces.
|
|
182
184
|
- **Evidence** — Expandable traces, source stages, scores and timing summaries.
|
|
185
|
+
- **Workbench** — Section rhythm, hero, cards, entries, timeline, mosaic and scroll reveal for the Workbench preset.
|
|
@@ -12,7 +12,7 @@ function Badge({ children, tone = "neutral", className, ...rest }) {
|
|
|
12
12
|
"span",
|
|
13
13
|
{
|
|
14
14
|
className: cn(
|
|
15
|
-
"inline-flex items-center gap-1.5 rounded-(--radius-sm) border px-2.5 py-1 font-
|
|
15
|
+
"inline-flex items-center gap-1.5 rounded-(--radius-sm) border px-2.5 py-1 font-(family-name:--label-font) text-xs tracking-[calc(0.025em*var(--label-tracking-scale))]",
|
|
16
16
|
TONE[tone],
|
|
17
17
|
className
|
|
18
18
|
),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Badge/Badge.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport type BadgeTone = 'neutral' | 'success' | 'warning' | 'danger' | 'outline'\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n children: ReactNode\n /**\n * `neutral` is the default and the right answer nearly always. The three\n * status tones are the system's only chroma and are bound to STATE — a badge\n * that is red because the design wanted a red badge is the thing this scale\n * exists to prevent.\n */\n tone?: BadgeTone\n}\n\nconst TONE: Record<BadgeTone, string> = {\n neutral: 'bg-(--stone) text-(--ink-2) border-transparent',\n success: 'bg-(--ok-soft) text-(--ok) border-transparent',\n warning: 'bg-(--warn-soft) text-(--warn) border-transparent',\n danger: 'bg-(--danger-soft) text-(--danger) border-transparent',\n outline: 'bg-transparent text-(--ink-2) border-(--rule-2)',\n}\n\n/**\n * A count, a state, a small inline marker.\n *\n * Mono and small, so it reads as metadata rather than as content. A badge is\n * not interactive — if it can be clicked or dismissed it is a `Tag` or a\n * button, and giving this one an `onClick` produces a control a keyboard cannot\n * reach.\n *\n * @example\n * <Badge>12</Badge>\n * @example\n * <Badge tone=\"success\">Deployed</Badge>\n */\nexport function Badge({ children, tone = 'neutral', className, ...rest }: BadgeProps) {\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-(--radius-sm) border px-2.5 py-1 font-
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Badge/Badge.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport type BadgeTone = 'neutral' | 'success' | 'warning' | 'danger' | 'outline'\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n children: ReactNode\n /**\n * `neutral` is the default and the right answer nearly always. The three\n * status tones are the system's only chroma and are bound to STATE — a badge\n * that is red because the design wanted a red badge is the thing this scale\n * exists to prevent.\n */\n tone?: BadgeTone\n}\n\nconst TONE: Record<BadgeTone, string> = {\n neutral: 'bg-(--stone) text-(--ink-2) border-transparent',\n success: 'bg-(--ok-soft) text-(--ok) border-transparent',\n warning: 'bg-(--warn-soft) text-(--warn) border-transparent',\n danger: 'bg-(--danger-soft) text-(--danger) border-transparent',\n outline: 'bg-transparent text-(--ink-2) border-(--rule-2)',\n}\n\n/**\n * A count, a state, a small inline marker.\n *\n * Mono and small, so it reads as metadata rather than as content. A badge is\n * not interactive — if it can be clicked or dismissed it is a `Tag` or a\n * button, and giving this one an `onClick` produces a control a keyboard cannot\n * reach.\n *\n * @example\n * <Badge>12</Badge>\n * @example\n * <Badge tone=\"success\">Deployed</Badge>\n */\nexport function Badge({ children, tone = 'neutral', className, ...rest }: BadgeProps) {\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-(--radius-sm) border px-2.5 py-1 font-(family-name:--label-font) text-xs tracking-[calc(0.025em*var(--label-tracking-scale))]',\n TONE[tone],\n className,\n )}\n {...rest}\n >\n {children}\n </span>\n )\n}\n\nexport default Badge\n"],"mappings":"AAuCI;AAtCJ,SAAS,UAAU;AAenB,MAAM,OAAkC;AAAA,EACtC,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AACX;AAeO,SAAS,MAAM,EAAE,UAAU,OAAO,WAAW,WAAW,GAAG,KAAK,GAAe;AACpF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,KAAK,IAAI;AAAA,QACT;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,gBAAQ;","names":[]}
|
|
@@ -5,7 +5,7 @@ import { DEV, warn } from "../../lib/warn.js";
|
|
|
5
5
|
import { Spinner } from "../Spinner/Spinner.js";
|
|
6
6
|
const BASE = "inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none";
|
|
7
7
|
const VARIANT = {
|
|
8
|
-
primary: "bg-(--
|
|
8
|
+
primary: "bg-(--action) text-(--action-foreground) border border-(--action) hover:opacity-85",
|
|
9
9
|
secondary: "border border-(--rule-2) text-(--ink) bg-transparent hover:border-(--ink)",
|
|
10
10
|
ghost: "border border-transparent text-(--ink-2) hover:text-(--ink) hover:bg-(--stone)",
|
|
11
11
|
/* The one place chroma is allowed on a control, and only because the action
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from '@radix-ui/react-slot'\nimport type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { DEV, warn } from '../../lib/warn'\nimport { Spinner } from '../Spinner/Spinner'\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'\nexport type ButtonSize = 'sm' | 'md' | 'lg'\n\n/**\n * The base every variant shares.\n *\n * It reads `--radius`, the CONTROL step, which is the same corner an `Input`,\n * a `Select` trigger and a `Textarea` draw. It used to be a pill, and a pill\n * beside an 8px field is two different ideas of what a control is — the one\n * inconsistency in the system that every reader noticed and none of the\n * component pages could explain. Everything on the control line now agrees,\n * and the capsule is kept for the things that genuinely are one: a badge, a\n * status pill, a segmented strip, a progress track.\n *\n * It does not move on hover. An earlier base lifted a pixel and asked for a\n * shadow the White Reset does not have — `--shadow` resolves to `none`, so that\n * hover state was invisible on every button in the system while still costing a\n * transition. Opacity carries the filled hover; the border carries the outlined\n * one.\n *\n * The transition names its properties rather than using `all`, so a layout\n * change (a width that grows when the label becomes \"Saving…\") is instant\n * instead of sliding.\n */\nconst BASE =\n 'inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none'\n\nconst VARIANT: Record<ButtonVariant, string> = {\n primary: 'bg-(--accent) text-(--accent-foreground) border border-(--accent) hover:opacity-85',\n secondary: 'border border-(--rule-2) text-(--ink) bg-transparent hover:border-(--ink)',\n ghost: 'border border-transparent text-(--ink-2) hover:text-(--ink) hover:bg-(--stone)',\n /* The one place chroma is allowed on a control, and only because the action\n is destructive — that is a state, not a brand colour. */\n danger: 'bg-(--danger) text-(--paper) border border-(--danger) hover:opacity-85',\n}\n\n/**\n * `min-h` rather than `h`, so a button that wraps to two lines grows instead of\n * clipping.\n *\n * The heights come from the density tokens rather than from a literal, so a\n * subtree marked `data-density=\"compact\"` shrinks its controls without a single\n * call site being told. At the default density `md` is 44px — the pointer\n * target WCAG 2.5.5 asks for.\n *\n * `--control-lh` is what makes those numbers true rather than aspirational. A\n * floor only binds while the box is under it, and the box is the line plus the\n * padding plus the border — so a label inheriting the body's 1.6 leading put\n * `sm` at 39px against its own 36px token, `md` at 46 against 44 and `lg` at 50\n * against 48. Every size was two to three pixels taller than the number\n * documenting it, which is also how a masthead sized from `--control-h-sm`\n * ended up with a search field standing proud of every icon beside it.\n */\nconst SIZE: Record<ButtonSize, string> = {\n sm: 'min-h-(--control-h-sm) px-(--control-px-sm) py-(--control-py-sm) text-[13px] leading-(--control-lh)',\n md: 'min-h-(--control-h-md) px-(--control-px-md) py-(--control-py-md) text-sm leading-(--control-lh)',\n lg: 'min-h-(--control-h-lg) px-(--control-px-lg) py-(--control-py-lg) text-[15px] leading-(--control-lh)',\n}\n\n/** Square, because an icon has no label to give the box its width. */\nconst ICON_SIZE: Record<ButtonSize, string> = {\n sm: 'size-(--control-h-sm) p-0',\n md: 'size-(--control-h-md) p-0',\n lg: 'size-(--control-h-lg) p-0',\n}\n\nconst SPINNER_SIZE: Record<ButtonSize, 'sm' | 'md'> = { sm: 'sm', md: 'sm', lg: 'md' }\n\ninterface CommonProps {\n children?: ReactNode\n /**\n * Which action this is. `primary` is the one thing the screen wants you to\n * do, so there is at most one per view; `danger` is reserved for destructive\n * actions and is the only place chroma is allowed on a control.\n */\n variant?: ButtonVariant\n /** 36 / 44 / 48px tall. `md` meets the pointer-target floor on its own. */\n size?: ButtonSize\n /** Optional mono keycap glyph rendered after the label (e.g. \"P\"). */\n keycap?: string\n /**\n * Swaps the leading content for a spinner and blocks interaction. The label\n * stays put — a button that empties out while it works loses its width, and\n * the page reflows under the pointer that just clicked it.\n */\n loading?: boolean\n /**\n * Square control with no label. REQUIRES `aria-label`: an icon-only button\n * with no accessible name is invisible to a screen reader, and this is the\n * single most common way a design system ships an unusable control.\n */\n iconOnly?: boolean\n /**\n * Render the child element instead of a `<button>`, keeping these styles.\n * Use it to hand the styling to a router's `Link` — `asChild` is what keeps\n * this package free of any one router.\n *\n * The decoration slots (`keycap`, `loading`) are not injected into a slotted\n * child; compose them inside it yourself.\n *\n * @example\n * <Button asChild><Link href=\"/work\">Work</Link></Button>\n */\n asChild?: boolean\n className?: string\n}\n\ntype ButtonAsLink = CommonProps &\n Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className' | 'children'> & { href: string }\n\ntype ButtonAsButton = CommonProps &\n Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'> & { href?: undefined }\n\nexport type ButtonProps = ButtonAsLink | ButtonAsButton\n\nfunction Keycap({ glyph }: { glyph: string }) {\n return (\n <span className=\"rounded-(--radius-sm) border border-current px-2 py-0.5 font-mono text-[11px] opacity-55\">\n {glyph}\n </span>\n )\n}\n\n/**\n * The system's action.\n *\n * Renders a `<button>`, or an `<a>` when given `href`, or whatever you hand it\n * via `asChild`. No router is imported, so the package stays framework-agnostic\n * and a Next or React Router app wires its own `Link` at the call site.\n *\n * Server-component friendly: there is no client boundary here, so it renders in\n * a static page as well as an interactive one.\n *\n * @example\n * <Button variant=\"secondary\" keycap=\"P\">View projects</Button>\n * @example\n * <Button loading disabled>Saving…</Button>\n * @example\n * <Button iconOnly aria-label=\"Close\"><X size={16} /></Button>\n */\nexport function Button(props: ButtonProps) {\n const {\n children,\n variant = 'primary',\n size = 'md',\n keycap,\n loading = false,\n iconOnly = false,\n asChild = false,\n className,\n } = props\n\n const cls = cn(BASE, VARIANT[variant], iconOnly ? ICON_SIZE[size] : SIZE[size], className)\n\n if (DEV && iconOnly) {\n // The control renders correctly and is announced as \"button\" and nothing\n // else. Nothing about the rendered page says so, which is why this is the\n // single most common way a design system ships an unusable control.\n const named =\n typeof props['aria-label'] === 'string' && props['aria-label'].trim() !== ''\n const described = typeof props['aria-labelledby'] === 'string'\n if (!named && !described) {\n warn({\n code: 'BUTTON_ICON_ONLY_UNNAMED',\n problem:\n 'An iconOnly Button has no text, so without an accessible name a screen reader announces it as \"button\" and nothing else.',\n field: 'aria-label',\n fix: 'Add aria-label with the action the button performs, e.g. aria-label=\"Delete invoice\".',\n component: 'Button',\n })\n }\n }\n\n // A filled ground needs a spinner in the inherited colour; an outlined one\n // reads better against the page's own rule scale.\n const spinnerTone = variant === 'primary' || variant === 'danger' ? 'current' : 'default'\n const decoration = (\n <>\n {loading && <Spinner size={SPINNER_SIZE[size]} tone={spinnerTone} label={null} />}\n {children}\n {keycap && <Keycap glyph={keycap} />}\n </>\n )\n\n if (asChild) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <Slot className={cls} {...rest}>\n {children}\n </Slot>\n )\n }\n\n if (props.href !== undefined) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, href, ...rest } = props\n return (\n <a\n href={href}\n className={cls}\n // A link cannot be `disabled`; `aria-disabled` plus the pointer-events\n // rule in BASE is the accessible equivalent.\n aria-disabled={loading || undefined}\n {...rest}\n >\n {decoration}\n </a>\n )\n }\n\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <button\n type={rest.type ?? 'button'}\n aria-busy={loading || undefined}\n disabled={rest.disabled || loading}\n className={cls}\n {...rest}\n >\n {decoration}\n </button>\n )\n}\n\nexport default Button\n"],"mappings":"AA2HI,SA4DA,UA5DA,KA4DA,YA5DA;AA3HJ,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,KAAK,YAAY;AAC1B,SAAS,eAAe;AA0BxB,MAAM,OACJ;AAEF,MAAM,UAAyC;AAAA,EAC7C,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA;AAAA;AAAA,EAGP,QAAQ;AACV;AAmBA,MAAM,OAAmC;AAAA,EACvC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGA,MAAM,YAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAAgD,EAAE,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK;AAiDrF,SAAS,OAAO,EAAE,MAAM,GAAsB;AAC5C,SACE,oBAAC,UAAK,WAAU,4FACb,iBACH;AAEJ;AAmBO,SAAS,OAAO,OAAoB;AACzC,QAAM;AAAA,IACJ;AAAA,IACA,UAAU;AAAA,IACV,OAAO;AAAA,IACP;AAAA,IACA,UAAU;AAAA,IACV,WAAW;AAAA,IACX,UAAU;AAAA,IACV;AAAA,EACF,IAAI;AAEJ,QAAM,MAAM,GAAG,MAAM,QAAQ,OAAO,GAAG,WAAW,UAAU,IAAI,IAAI,KAAK,IAAI,GAAG,SAAS;AAEzF,MAAI,OAAO,UAAU;AAInB,UAAM,QACJ,OAAO,MAAM,YAAY,MAAM,YAAY,MAAM,YAAY,EAAE,KAAK,MAAM;AAC5E,UAAM,YAAY,OAAO,MAAM,iBAAiB,MAAM;AACtD,QAAI,CAAC,SAAS,CAAC,WAAW;AACxB,WAAK;AAAA,QACH,MAAM;AAAA,QACN,SACE;AAAA,QACF,OAAO;AAAA,QACP,KAAK;AAAA,QACL,WAAW;AAAA,MACb,CAAC;AAAA,IACH;AAAA,EACF;AAIA,QAAM,cAAc,YAAY,aAAa,YAAY,WAAW,YAAY;AAChF,QAAM,aACJ,iCACG;AAAA,eAAW,oBAAC,WAAQ,MAAM,aAAa,IAAI,GAAG,MAAM,aAAa,OAAO,MAAM;AAAA,IAC9E;AAAA,IACA,UAAU,oBAAC,UAAO,OAAO,QAAQ;AAAA,KACpC;AAGF,MAAI,SAAS;AACX,UAAM,EAAE,UAAUA,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,GAAGC,MAAK,IAAI;AAC7H,WACE,oBAAC,QAAK,WAAW,KAAM,GAAGA,OACvB,UACH;AAAA,EAEJ;AAEA,MAAI,MAAM,SAAS,QAAW;AAC5B,UAAM,EAAE,UAAUR,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,MAAM,GAAGC,MAAK,IAAI;AACnI,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,QAGX,iBAAe,WAAW;AAAA,QACzB,GAAGA;AAAA,QAEH;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,EAAE,UAAU,IAAI,SAAS,IAAI,MAAM,IAAI,QAAQ,IAAI,SAAS,IAAI,UAAU,IAAI,SAAS,IAAI,WAAW,KAAK,GAAG,KAAK,IAAI;AAC7H,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,KAAK,QAAQ;AAAA,MACnB,aAAW,WAAW;AAAA,MACtB,UAAU,KAAK,YAAY;AAAA,MAC3B,WAAW;AAAA,MACV,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,iBAAQ;","names":["_c","_v","_s","_k","_l","_i","_a","_cn","rest"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from '@radix-ui/react-slot'\nimport type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { DEV, warn } from '../../lib/warn'\nimport { Spinner } from '../Spinner/Spinner'\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'\nexport type ButtonSize = 'sm' | 'md' | 'lg'\n\n/**\n * The base every variant shares.\n *\n * It reads `--radius`, the CONTROL step, which is the same corner an `Input`,\n * a `Select` trigger and a `Textarea` draw. It used to be a pill, and a pill\n * beside an 8px field is two different ideas of what a control is — the one\n * inconsistency in the system that every reader noticed and none of the\n * component pages could explain. Everything on the control line now agrees,\n * and the capsule is kept for the things that genuinely are one: a badge, a\n * status pill, a segmented strip, a progress track.\n *\n * It does not move on hover. An earlier base lifted a pixel and asked for a\n * shadow the White Reset does not have — `--shadow` resolves to `none`, so that\n * hover state was invisible on every button in the system while still costing a\n * transition. Opacity carries the filled hover; the border carries the outlined\n * one.\n *\n * The transition names its properties rather than using `all`, so a layout\n * change (a width that grows when the label becomes \"Saving…\") is instant\n * instead of sliding.\n */\nconst BASE =\n 'inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none'\n\nconst VARIANT: Record<ButtonVariant, string> = {\n primary: 'bg-(--action) text-(--action-foreground) border border-(--action) hover:opacity-85',\n secondary: 'border border-(--rule-2) text-(--ink) bg-transparent hover:border-(--ink)',\n ghost: 'border border-transparent text-(--ink-2) hover:text-(--ink) hover:bg-(--stone)',\n /* The one place chroma is allowed on a control, and only because the action\n is destructive — that is a state, not a brand colour. */\n danger: 'bg-(--danger) text-(--paper) border border-(--danger) hover:opacity-85',\n}\n\n/**\n * `min-h` rather than `h`, so a button that wraps to two lines grows instead of\n * clipping.\n *\n * The heights come from the density tokens rather than from a literal, so a\n * subtree marked `data-density=\"compact\"` shrinks its controls without a single\n * call site being told. At the default density `md` is 44px — the pointer\n * target WCAG 2.5.5 asks for.\n *\n * `--control-lh` is what makes those numbers true rather than aspirational. A\n * floor only binds while the box is under it, and the box is the line plus the\n * padding plus the border — so a label inheriting the body's 1.6 leading put\n * `sm` at 39px against its own 36px token, `md` at 46 against 44 and `lg` at 50\n * against 48. Every size was two to three pixels taller than the number\n * documenting it, which is also how a masthead sized from `--control-h-sm`\n * ended up with a search field standing proud of every icon beside it.\n */\nconst SIZE: Record<ButtonSize, string> = {\n sm: 'min-h-(--control-h-sm) px-(--control-px-sm) py-(--control-py-sm) text-[13px] leading-(--control-lh)',\n md: 'min-h-(--control-h-md) px-(--control-px-md) py-(--control-py-md) text-sm leading-(--control-lh)',\n lg: 'min-h-(--control-h-lg) px-(--control-px-lg) py-(--control-py-lg) text-[15px] leading-(--control-lh)',\n}\n\n/** Square, because an icon has no label to give the box its width. */\nconst ICON_SIZE: Record<ButtonSize, string> = {\n sm: 'size-(--control-h-sm) p-0',\n md: 'size-(--control-h-md) p-0',\n lg: 'size-(--control-h-lg) p-0',\n}\n\nconst SPINNER_SIZE: Record<ButtonSize, 'sm' | 'md'> = { sm: 'sm', md: 'sm', lg: 'md' }\n\ninterface CommonProps {\n children?: ReactNode\n /**\n * Which action this is. `primary` is the one thing the screen wants you to\n * do, so there is at most one per view; `danger` is reserved for destructive\n * actions and is the only place chroma is allowed on a control.\n */\n variant?: ButtonVariant\n /** 36 / 44 / 48px tall. `md` meets the pointer-target floor on its own. */\n size?: ButtonSize\n /** Optional mono keycap glyph rendered after the label (e.g. \"P\"). */\n keycap?: string\n /**\n * Swaps the leading content for a spinner and blocks interaction. The label\n * stays put — a button that empties out while it works loses its width, and\n * the page reflows under the pointer that just clicked it.\n */\n loading?: boolean\n /**\n * Square control with no label. REQUIRES `aria-label`: an icon-only button\n * with no accessible name is invisible to a screen reader, and this is the\n * single most common way a design system ships an unusable control.\n */\n iconOnly?: boolean\n /**\n * Render the child element instead of a `<button>`, keeping these styles.\n * Use it to hand the styling to a router's `Link` — `asChild` is what keeps\n * this package free of any one router.\n *\n * The decoration slots (`keycap`, `loading`) are not injected into a slotted\n * child; compose them inside it yourself.\n *\n * @example\n * <Button asChild><Link href=\"/work\">Work</Link></Button>\n */\n asChild?: boolean\n className?: string\n}\n\ntype ButtonAsLink = CommonProps &\n Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className' | 'children'> & { href: string }\n\ntype ButtonAsButton = CommonProps &\n Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'> & { href?: undefined }\n\nexport type ButtonProps = ButtonAsLink | ButtonAsButton\n\nfunction Keycap({ glyph }: { glyph: string }) {\n return (\n <span className=\"rounded-(--radius-sm) border border-current px-2 py-0.5 font-mono text-[11px] opacity-55\">\n {glyph}\n </span>\n )\n}\n\n/**\n * The system's action.\n *\n * Renders a `<button>`, or an `<a>` when given `href`, or whatever you hand it\n * via `asChild`. No router is imported, so the package stays framework-agnostic\n * and a Next or React Router app wires its own `Link` at the call site.\n *\n * Server-component friendly: there is no client boundary here, so it renders in\n * a static page as well as an interactive one.\n *\n * @example\n * <Button variant=\"secondary\" keycap=\"P\">View projects</Button>\n * @example\n * <Button loading disabled>Saving…</Button>\n * @example\n * <Button iconOnly aria-label=\"Close\"><X size={16} /></Button>\n */\nexport function Button(props: ButtonProps) {\n const {\n children,\n variant = 'primary',\n size = 'md',\n keycap,\n loading = false,\n iconOnly = false,\n asChild = false,\n className,\n } = props\n\n const cls = cn(BASE, VARIANT[variant], iconOnly ? ICON_SIZE[size] : SIZE[size], className)\n\n if (DEV && iconOnly) {\n // The control renders correctly and is announced as \"button\" and nothing\n // else. Nothing about the rendered page says so, which is why this is the\n // single most common way a design system ships an unusable control.\n const named =\n typeof props['aria-label'] === 'string' && props['aria-label'].trim() !== ''\n const described = typeof props['aria-labelledby'] === 'string'\n if (!named && !described) {\n warn({\n code: 'BUTTON_ICON_ONLY_UNNAMED',\n problem:\n 'An iconOnly Button has no text, so without an accessible name a screen reader announces it as \"button\" and nothing else.',\n field: 'aria-label',\n fix: 'Add aria-label with the action the button performs, e.g. aria-label=\"Delete invoice\".',\n component: 'Button',\n })\n }\n }\n\n // A filled ground needs a spinner in the inherited colour; an outlined one\n // reads better against the page's own rule scale.\n const spinnerTone = variant === 'primary' || variant === 'danger' ? 'current' : 'default'\n const decoration = (\n <>\n {loading && <Spinner size={SPINNER_SIZE[size]} tone={spinnerTone} label={null} />}\n {children}\n {keycap && <Keycap glyph={keycap} />}\n </>\n )\n\n if (asChild) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <Slot className={cls} {...rest}>\n {children}\n </Slot>\n )\n }\n\n if (props.href !== undefined) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, href, ...rest } = props\n return (\n <a\n href={href}\n className={cls}\n // A link cannot be `disabled`; `aria-disabled` plus the pointer-events\n // rule in BASE is the accessible equivalent.\n aria-disabled={loading || undefined}\n {...rest}\n >\n {decoration}\n </a>\n )\n }\n\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <button\n type={rest.type ?? 'button'}\n aria-busy={loading || undefined}\n disabled={rest.disabled || loading}\n className={cls}\n {...rest}\n >\n {decoration}\n </button>\n )\n}\n\nexport default Button\n"],"mappings":"AA2HI,SA4DA,UA5DA,KA4DA,YA5DA;AA3HJ,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,KAAK,YAAY;AAC1B,SAAS,eAAe;AA0BxB,MAAM,OACJ;AAEF,MAAM,UAAyC;AAAA,EAC7C,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA;AAAA;AAAA,EAGP,QAAQ;AACV;AAmBA,MAAM,OAAmC;AAAA,EACvC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGA,MAAM,YAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAAgD,EAAE,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK;AAiDrF,SAAS,OAAO,EAAE,MAAM,GAAsB;AAC5C,SACE,oBAAC,UAAK,WAAU,4FACb,iBACH;AAEJ;AAmBO,SAAS,OAAO,OAAoB;AACzC,QAAM;AAAA,IACJ;AAAA,IACA,UAAU;AAAA,IACV,OAAO;AAAA,IACP;AAAA,IACA,UAAU;AAAA,IACV,WAAW;AAAA,IACX,UAAU;AAAA,IACV;AAAA,EACF,IAAI;AAEJ,QAAM,MAAM,GAAG,MAAM,QAAQ,OAAO,GAAG,WAAW,UAAU,IAAI,IAAI,KAAK,IAAI,GAAG,SAAS;AAEzF,MAAI,OAAO,UAAU;AAInB,UAAM,QACJ,OAAO,MAAM,YAAY,MAAM,YAAY,MAAM,YAAY,EAAE,KAAK,MAAM;AAC5E,UAAM,YAAY,OAAO,MAAM,iBAAiB,MAAM;AACtD,QAAI,CAAC,SAAS,CAAC,WAAW;AACxB,WAAK;AAAA,QACH,MAAM;AAAA,QACN,SACE;AAAA,QACF,OAAO;AAAA,QACP,KAAK;AAAA,QACL,WAAW;AAAA,MACb,CAAC;AAAA,IACH;AAAA,EACF;AAIA,QAAM,cAAc,YAAY,aAAa,YAAY,WAAW,YAAY;AAChF,QAAM,aACJ,iCACG;AAAA,eAAW,oBAAC,WAAQ,MAAM,aAAa,IAAI,GAAG,MAAM,aAAa,OAAO,MAAM;AAAA,IAC9E;AAAA,IACA,UAAU,oBAAC,UAAO,OAAO,QAAQ;AAAA,KACpC;AAGF,MAAI,SAAS;AACX,UAAM,EAAE,UAAUA,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,GAAGC,MAAK,IAAI;AAC7H,WACE,oBAAC,QAAK,WAAW,KAAM,GAAGA,OACvB,UACH;AAAA,EAEJ;AAEA,MAAI,MAAM,SAAS,QAAW;AAC5B,UAAM,EAAE,UAAUR,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,MAAM,GAAGC,MAAK,IAAI;AACnI,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,QAGX,iBAAe,WAAW;AAAA,QACzB,GAAGA;AAAA,QAEH;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,EAAE,UAAU,IAAI,SAAS,IAAI,MAAM,IAAI,QAAQ,IAAI,SAAS,IAAI,UAAU,IAAI,SAAS,IAAI,WAAW,KAAK,GAAG,KAAK,IAAI;AAC7H,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,KAAK,QAAQ;AAAA,MACnB,aAAW,WAAW;AAAA,MACtB,UAAU,KAAK,YAAY;AAAA,MAC3B,WAAW;AAAA,MACV,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,iBAAQ;","names":["_c","_v","_s","_k","_l","_i","_a","_cn","rest"]}
|
|
@@ -10,7 +10,7 @@ const SIZE = {
|
|
|
10
10
|
// `font-heading` both set font-family, and a custom utility is invisible to
|
|
11
11
|
// tailwind-merge — so the two would land in the same cascade layer at the
|
|
12
12
|
// same specificity and source order would pick the winner.
|
|
13
|
-
label: "font-
|
|
13
|
+
label: "font-(family-name:--label-font) text-[11px] [text-transform:var(--label-case)] leading-[1.6] tracking-[calc(0.2em*var(--label-tracking-scale))] text-(--ink-3-aa)"
|
|
14
14
|
};
|
|
15
15
|
const LADDER = {
|
|
16
16
|
1: "title",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Heading/Heading.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6\n\n/**\n * A step on the type ladder, named for the thing it heads.\n *\n * `title` is the top and there is nothing above it — a page has exactly one\n * thing larger than its own records. `label` is not on the size ladder at all:\n * it is the mono kicker, which is what a fifth-level heading should look like,\n * because a serif heading three steps down is indistinguishable from bold body\n * copy.\n */\nexport type HeadingSize = 'title' | 'lead' | 'heading' | 'sub' | 'item' | 'label'\n\nconst SIZE: Record<HeadingSize, string> = {\n title: 'text-[length:var(--fs-title)] leading-[1.1] tracking-[-0.02em]',\n lead: 'text-[length:var(--fs-lead)] leading-[1.15] tracking-[-0.015em]',\n heading: 'text-[length:var(--fs-heading)] leading-[1.2]',\n sub: 'text-[length:var(--fs-sub)] leading-[1.25]',\n item: 'text-[length:var(--fs-item)] leading-[1.3]',\n // Spelled out rather than reaching for the `eyebrow` utility: it and\n // `font-heading` both set font-family, and a custom utility is invisible to\n // tailwind-merge — so the two would land in the same cascade layer at the\n // same specificity and source order would pick the winner.\n label: 'font-
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Heading/Heading.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6\n\n/**\n * A step on the type ladder, named for the thing it heads.\n *\n * `title` is the top and there is nothing above it — a page has exactly one\n * thing larger than its own records. `label` is not on the size ladder at all:\n * it is the mono kicker, which is what a fifth-level heading should look like,\n * because a serif heading three steps down is indistinguishable from bold body\n * copy.\n */\nexport type HeadingSize = 'title' | 'lead' | 'heading' | 'sub' | 'item' | 'label'\n\nconst SIZE: Record<HeadingSize, string> = {\n title: 'text-[length:var(--fs-title)] leading-[1.1] tracking-[-0.02em]',\n lead: 'text-[length:var(--fs-lead)] leading-[1.15] tracking-[-0.015em]',\n heading: 'text-[length:var(--fs-heading)] leading-[1.2]',\n sub: 'text-[length:var(--fs-sub)] leading-[1.25]',\n item: 'text-[length:var(--fs-item)] leading-[1.3]',\n // Spelled out rather than reaching for the `eyebrow` utility: it and\n // `font-heading` both set font-family, and a custom utility is invisible to\n // tailwind-merge — so the two would land in the same cascade layer at the\n // same specificity and source order would pick the winner.\n label: 'font-(family-name:--label-font) text-[11px] [text-transform:var(--label-case)] leading-[1.6] tracking-[calc(0.2em*var(--label-tracking-scale))] text-(--ink-3-aa)',\n}\n\n/**\n * What each level looks like when nobody says otherwise — and the reason this\n * component exists rather than six styled tags.\n *\n * Read the gaps. `1 → title` then `2 → heading` SKIPS `lead`, because the\n * ladder's steps sit close together on purpose: it separates records of the\n * same kind, not a heading from its own sub-heading. `--fs-lead` over\n * `--fs-heading` is a ratio of 1.14 and reads as a rendering accident;\n * `--fs-title` over `--fs-heading` is 1.86 and reads as a hierarchy.\n *\n * So the correct pairing is what a caller gets for free, and stepping off the\n * ladder costs a prop and is visible in review. This is the same map\n * `article.css` applies to rendered Markdown, which is what makes a component\n * page and a post read as one publication.\n */\nconst LADDER: Record<HeadingLevel, HeadingSize> = {\n 1: 'title',\n 2: 'heading',\n 3: 'sub',\n 4: 'item',\n 5: 'label',\n 6: 'label',\n}\n\nexport interface HeadingProps extends HTMLAttributes<HTMLHeadingElement> {\n children?: ReactNode\n /**\n * The element: `1` renders `<h1>`, `6` renders `<h6>`.\n *\n * This is the document outline and nothing else — it is what a screen reader\n * navigates by, so it follows the section the heading opens, never the size\n * it wants to be. A heading two levels down inside an `<h2>` section is an\n * `<h3>` even when the design calls for something enormous.\n */\n level: HeadingLevel\n /**\n * The size, when the outline and the design genuinely disagree.\n *\n * Defaults from `level` through the ladder above, which is the answer nearly\n * always. Reach for this when a semantically-correct `h3` opens a page and\n * has to look like one — not to squeeze a fourth step between two that are\n * already only 14% apart.\n */\n size?: HeadingSize\n}\n\n/**\n * A heading, with its element and its size decided separately.\n *\n * Every heading component that takes one number gets this wrong in one of two\n * directions: either the outline is bent to reach a size (an `<h1>` in the\n * middle of a page because the design wanted big type), or the size is bent to\n * keep the outline (a section title set at 20px because it is the fourth level\n * down). Both are one prop away from being right, so there are two.\n *\n * `level` is the document. `size` is the page. The default binds them through\n * the system's ladder, so writing only `level` is correct — and the moment they\n * come apart, the call site says so.\n *\n * Set in the editorial serif at weight 400, like every heading in the system.\n * It also carries `scroll-margin-top`, so a heading given an `id` and linked\n * from a table of contents comes to rest below the masthead rather than under\n * it.\n *\n * @example\n * <Heading level={1}>The White Reset</Heading>\n * @example\n * // Third level in the outline, page-title sized.\n * <Heading level={3} size=\"title\">Colour</Heading>\n */\nexport function Heading({ children, level, size, className, ...rest }: HeadingProps) {\n const Comp = `h${level}` as const\n const step = size ?? LADDER[level]\n\n return (\n <Comp\n className={cn(\n 'm-0 scroll-mt-[var(--scroll-offset)] font-heading font-normal text-(--ink)',\n SIZE[step],\n className,\n )}\n {...rest}\n >\n {children}\n </Comp>\n )\n}\n\nexport default Heading\n"],"mappings":"AAwGI;AAvGJ,SAAS,UAAU;AAenB,MAAM,OAAoC;AAAA,EACxC,OAAO;AAAA,EACP,MAAM;AAAA,EACN,SAAS;AAAA,EACT,KAAK;AAAA,EACL,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAKN,OAAO;AACT;AAiBA,MAAM,SAA4C;AAAA,EAChD,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AAgDO,SAAS,QAAQ,EAAE,UAAU,OAAO,MAAM,WAAW,GAAG,KAAK,GAAiB;AACnF,QAAM,OAAO,IAAI,KAAK;AACtB,QAAM,OAAO,QAAQ,OAAO,KAAK;AAEjC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,KAAK,IAAI;AAAA,QACT;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,kBAAQ;","names":[]}
|
|
@@ -4,7 +4,7 @@ import { cn } from "../../lib/cn.js";
|
|
|
4
4
|
function Tag({ children, active, onClick, onRemove, removeLabel, className, ...rest }) {
|
|
5
5
|
const split = Boolean(onClick && onRemove);
|
|
6
6
|
const chip = cn(
|
|
7
|
-
"inline-flex items-center rounded-(--radius-sm) font-
|
|
7
|
+
"inline-flex items-center rounded-(--radius-sm) font-(family-name:--label-font) text-xs tracking-[calc(0.025em*var(--label-tracking-scale))] transition-colors duration-(--duration-fast)",
|
|
8
8
|
active ? "bg-(--accent) text-(--accent-foreground)" : "bg-(--stone) text-(--ink-3-aa)",
|
|
9
9
|
onRemove ? "gap-1 pe-1.5" : "pe-2.5",
|
|
10
10
|
// Handed to the label button in the split form, so the padding is part of
|
|
@@ -39,7 +39,7 @@ function Tag({ children, active, onClick, onRemove, removeLabel, className, ...r
|
|
|
39
39
|
type: "button",
|
|
40
40
|
onClick,
|
|
41
41
|
"aria-pressed": active,
|
|
42
|
-
className: "rounded-(--radius-sm) py-1 ps-2.5 font-
|
|
42
|
+
className: "rounded-(--radius-sm) py-1 ps-2.5 font-(family-name:--label-font) text-xs tracking-[calc(0.025em*var(--label-tracking-scale))]",
|
|
43
43
|
children
|
|
44
44
|
}
|
|
45
45
|
),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Tag/Tag.tsx"],"sourcesContent":["import { RiCloseLine } from '@remixicon/react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\ninterface TagBase extends Omit<HTMLAttributes<HTMLElement>, 'onClick'> {\n children: ReactNode\n /**\n * Renders the pressed/selected look, and declares the chip a toggle.\n *\n * An interactive chip reads `aria-pressed` off this same value, so the accent\n * fill and the state a screen reader hears cannot drift apart. Leave it off\n * for a chip whose `onClick` navigates or opens something: a control that is\n * neither pressed nor unpressed is better announced as neither than as\n * \"not pressed\".\n */\n active?: boolean\n /**\n * Makes the chip ITSELF the filter control.\n *\n * This prop exists because the alternative did not survive contact with\n * `onRemove`. The advice used to be to wrap the tag in a button at the call\n * site, which is fine on its own and invalid the moment the chip is also\n * removable: the remove control is a real `<button>`, so the wrapper puts a\n * button inside a button — markup the parser splits into siblings, leaving a\n * DOM neither the author nor the accessibility tree expects.\n *\n * With this the wrapper is never written. The chip carries the click, the\n * focus ring and the pressed state on the element that draws them, and a\n * removable filter chip renders its two controls side by side instead of one\n * inside the other.\n */\n onClick?: () => void\n}\n\n/**\n * The removable form: a chip for one thing the reader has already chosen.\n *\n * `removeLabel` is required alongside `onRemove` and not optional with a\n * default, because \"Remove\" repeated down a row of filters is eight controls a\n * screen reader cannot tell apart. Write the subject into it — \"Remove\n * TypeScript filter\".\n */\ninterface TagRemovable {\n /** Called when the reader dismisses the chip. */\n onRemove: () => void\n /** The remove button's accessible name. Name the subject, not the action. */\n removeLabel: string\n}\n\ninterface TagStatic {\n onRemove?: never\n removeLabel?: never\n}\n\nexport type TagProps = TagBase & (TagRemovable | TagStatic)\n\n/**\n * A subject label — a topic, a technology, a filter facet — that can filter\n * with `onClick` and be dismissed with `onRemove`.\n *\n * Distinct from `Badge`, which carries a state or a count. A tag names what\n * something is ABOUT, so several sit together in a row and the reader scans\n * them; a badge is one fact about one record.\n *\n * This is where a `Token` component would have gone. It was not built: a token\n * is a tag with a remove button, and the difference between the two is one\n * prop, not one component. The system already ships three things that look\n * alike — `Badge`, `Tag`, `StatusPill` — and a fourth whose whole distinction\n * is an X on the end would be the one a call site picks by coin toss.\n *\n * Presentational until it is given a handler, and the component owns both\n * interactive cases rather than leaving one to a wrapper at the call site.\n * That is not a convenience: a wrapping `<button>` around a chip that already\n * holds the remove `<button>` is a button inside a button, which no parser\n * keeps and no accessibility tree reports the way it was written. Given both,\n * the chip renders the label and the X as SIBLING buttons — the label takes\n * the leading padding with it, so it is the whole of the chip up to the X\n * rather than the words with dead padding around them.\n *\n * @example\n * <Tag>TypeScript</Tag>\n * @example\n * <Tag active={on} onClick={toggle}>Rust</Tag>\n * @example\n * <Tag onRemove={() => drop('rust')} removeLabel=\"Remove Rust filter\">Rust</Tag>\n * @example\n * // Both: a filter the reader can toggle and also take out of the row.\n * <Tag active={on} onClick={toggle} onRemove={drop} removeLabel=\"Remove Rust filter\">Rust</Tag>\n */\nexport function Tag({ children, active, onClick, onRemove, removeLabel, className, ...rest }: TagProps) {\n // Two controls cannot nest, so a chip that is both a filter and a selection\n // splits its label out into a button of its own beside the remove button.\n const split = Boolean(onClick && onRemove)\n\n const chip = cn(\n 'inline-flex items-center rounded-(--radius-sm) font-
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Tag/Tag.tsx"],"sourcesContent":["import { RiCloseLine } from '@remixicon/react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\ninterface TagBase extends Omit<HTMLAttributes<HTMLElement>, 'onClick'> {\n children: ReactNode\n /**\n * Renders the pressed/selected look, and declares the chip a toggle.\n *\n * An interactive chip reads `aria-pressed` off this same value, so the accent\n * fill and the state a screen reader hears cannot drift apart. Leave it off\n * for a chip whose `onClick` navigates or opens something: a control that is\n * neither pressed nor unpressed is better announced as neither than as\n * \"not pressed\".\n */\n active?: boolean\n /**\n * Makes the chip ITSELF the filter control.\n *\n * This prop exists because the alternative did not survive contact with\n * `onRemove`. The advice used to be to wrap the tag in a button at the call\n * site, which is fine on its own and invalid the moment the chip is also\n * removable: the remove control is a real `<button>`, so the wrapper puts a\n * button inside a button — markup the parser splits into siblings, leaving a\n * DOM neither the author nor the accessibility tree expects.\n *\n * With this the wrapper is never written. The chip carries the click, the\n * focus ring and the pressed state on the element that draws them, and a\n * removable filter chip renders its two controls side by side instead of one\n * inside the other.\n */\n onClick?: () => void\n}\n\n/**\n * The removable form: a chip for one thing the reader has already chosen.\n *\n * `removeLabel` is required alongside `onRemove` and not optional with a\n * default, because \"Remove\" repeated down a row of filters is eight controls a\n * screen reader cannot tell apart. Write the subject into it — \"Remove\n * TypeScript filter\".\n */\ninterface TagRemovable {\n /** Called when the reader dismisses the chip. */\n onRemove: () => void\n /** The remove button's accessible name. Name the subject, not the action. */\n removeLabel: string\n}\n\ninterface TagStatic {\n onRemove?: never\n removeLabel?: never\n}\n\nexport type TagProps = TagBase & (TagRemovable | TagStatic)\n\n/**\n * A subject label — a topic, a technology, a filter facet — that can filter\n * with `onClick` and be dismissed with `onRemove`.\n *\n * Distinct from `Badge`, which carries a state or a count. A tag names what\n * something is ABOUT, so several sit together in a row and the reader scans\n * them; a badge is one fact about one record.\n *\n * This is where a `Token` component would have gone. It was not built: a token\n * is a tag with a remove button, and the difference between the two is one\n * prop, not one component. The system already ships three things that look\n * alike — `Badge`, `Tag`, `StatusPill` — and a fourth whose whole distinction\n * is an X on the end would be the one a call site picks by coin toss.\n *\n * Presentational until it is given a handler, and the component owns both\n * interactive cases rather than leaving one to a wrapper at the call site.\n * That is not a convenience: a wrapping `<button>` around a chip that already\n * holds the remove `<button>` is a button inside a button, which no parser\n * keeps and no accessibility tree reports the way it was written. Given both,\n * the chip renders the label and the X as SIBLING buttons — the label takes\n * the leading padding with it, so it is the whole of the chip up to the X\n * rather than the words with dead padding around them.\n *\n * @example\n * <Tag>TypeScript</Tag>\n * @example\n * <Tag active={on} onClick={toggle}>Rust</Tag>\n * @example\n * <Tag onRemove={() => drop('rust')} removeLabel=\"Remove Rust filter\">Rust</Tag>\n * @example\n * // Both: a filter the reader can toggle and also take out of the row.\n * <Tag active={on} onClick={toggle} onRemove={drop} removeLabel=\"Remove Rust filter\">Rust</Tag>\n */\nexport function Tag({ children, active, onClick, onRemove, removeLabel, className, ...rest }: TagProps) {\n // Two controls cannot nest, so a chip that is both a filter and a selection\n // splits its label out into a button of its own beside the remove button.\n const split = Boolean(onClick && onRemove)\n\n const chip = cn(\n 'inline-flex items-center rounded-(--radius-sm) font-(family-name:--label-font) text-xs tracking-[calc(0.025em*var(--label-tracking-scale))] transition-colors duration-(--duration-fast)',\n active ? 'bg-(--accent) text-(--accent-foreground)' : 'bg-(--stone) text-(--ink-3-aa)',\n onRemove ? 'gap-1 pe-1.5' : 'pe-2.5',\n // Handed to the label button in the split form, so the padding is part of\n // the target rather than a ring of dead space around it.\n !split && 'py-1 ps-2.5',\n className,\n )\n\n const remove = onRemove ? (\n <button\n type=\"button\"\n onClick={onRemove}\n aria-label={removeLabel}\n className={cn(\n // The drawn box is 16px, which is under the 24px minimum a pointer\n // target has to clear (WCAG 2.5.8). The inset pseudo-element takes\n // the hit area out to 24 without changing what is drawn or pushing\n // the chips apart — the 44px floor is for a standalone control, and\n // a chip is 24 tall to begin with.\n 'relative inline-flex size-4 shrink-0 items-center justify-center rounded-(--radius-xs)',\n \"before:absolute before:inset-[-4px] before:content-['']\",\n 'opacity-60 transition-opacity duration-(--duration-fast) hover:opacity-100 focus-visible:opacity-100',\n )}\n >\n <RiCloseLine size={12} aria-hidden />\n </button>\n ) : null\n\n if (split) {\n return (\n <span className={chip} {...rest}>\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className=\"rounded-(--radius-sm) py-1 ps-2.5 font-(family-name:--label-font) text-xs tracking-[calc(0.025em*var(--label-tracking-scale))]\"\n >\n {children}\n </button>\n {remove}\n </span>\n )\n }\n\n if (onClick) {\n return (\n <button type=\"button\" onClick={onClick} aria-pressed={active} className={chip} {...rest}>\n {children}\n </button>\n )\n }\n\n return (\n <span className={chip} {...rest}>\n {children}\n {remove}\n </span>\n )\n}\n\nexport default Tag\n"],"mappings":"AAwHM,cAMA,YANA;AAxHN,SAAS,mBAAmB;AAE5B,SAAS,UAAU;AAuFZ,SAAS,IAAI,EAAE,UAAU,QAAQ,SAAS,UAAU,aAAa,WAAW,GAAG,KAAK,GAAa;AAGtG,QAAM,QAAQ,QAAQ,WAAW,QAAQ;AAEzC,QAAM,OAAO;AAAA,IACX;AAAA,IACA,SAAS,6CAA6C;AAAA,IACtD,WAAW,iBAAiB;AAAA;AAAA;AAAA,IAG5B,CAAC,SAAS;AAAA,IACV;AAAA,EACF;AAEA,QAAM,SAAS,WACb;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,cAAY;AAAA,MACZ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEA,8BAAC,eAAY,MAAM,IAAI,eAAW,MAAC;AAAA;AAAA,EACrC,IACE;AAEJ,MAAI,OAAO;AACT,WACE,qBAAC,UAAK,WAAW,MAAO,GAAG,MACzB;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL;AAAA,UACA,gBAAc;AAAA,UACd,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MACC;AAAA,OACH;AAAA,EAEJ;AAEA,MAAI,SAAS;AACX,WACE,oBAAC,YAAO,MAAK,UAAS,SAAkB,gBAAc,QAAQ,WAAW,MAAO,GAAG,MAChF,UACH;AAAA,EAEJ;AAEA,SACE,qBAAC,UAAK,WAAW,MAAO,GAAG,MACxB;AAAA;AAAA,IACA;AAAA,KACH;AAEJ;AAEA,IAAO,cAAQ;","names":[]}
|
package/dist/semantic.css
CHANGED
|
@@ -69,6 +69,13 @@
|
|
|
69
69
|
writes the next panel. */
|
|
70
70
|
--accent-on-muted: var(--red-ink);
|
|
71
71
|
|
|
72
|
+
/* The fill of the one primary action on a screen. It follows the accent
|
|
73
|
+
unless `data-action` says otherwise: a preset that keeps its accent for
|
|
74
|
+
links and the current state can hand the primary button back to ink,
|
|
75
|
+
without a checked box or the current page losing the accent. */
|
|
76
|
+
--action: var(--accent);
|
|
77
|
+
--action-foreground: var(--accent-foreground);
|
|
78
|
+
|
|
72
79
|
/* ─── Status ─── bound to state, never to brand. ───────────────────────── */
|
|
73
80
|
--success: var(--ok);
|
|
74
81
|
--success-wash: var(--ok-soft);
|