@misoto22/folio 0.16.0 → 0.17.1
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/article.css +1 -1
- 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/FigureBand/FigureBand.js +1 -1
- package/dist/components/FigureBand/FigureBand.js.map +1 -1
- package/dist/components/Heading/Heading.js +1 -1
- package/dist/components/Heading/Heading.js.map +1 -1
- package/dist/components/Pagination/Pagination.js +2 -2
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/Table/Table.js +1 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Tag/Tag.js +3 -3
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Text/Text.js +2 -2
- package/dist/components/Text/Text.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.js +2 -2
- package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
- package/dist/semantic.css +9 -0
- package/dist/styles.css +1 -1
- package/dist/themes.css +99 -0
- package/dist/tokens.css +29 -2
- package/dist/tokens.d.ts +9 -0
- package/dist/tokens.js +51 -2
- package/dist/tokens.json +51 -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 +8 -8
- package/dist/website-contact.css +9 -3
- package/dist/website-content.css +4 -1
- package/dist/website-conversation.css +6 -6
- package/dist/website-evidence.css +1 -1
- package/dist/website-media.css +13 -13
- package/dist/website-metrics.css +1 -1
- package/dist/website-music.css +1 -1
- package/dist/website-portfolio.css +4 -4
- package/dist/website-reading.css +13 -13
- package/dist/website-recovery.css +1 -1
- package/dist/website-shell.css +1 -1
- package/dist/website-timeline.css +3 -3
- 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.1
|
|
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.1
|
|
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.1",
|
|
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.1
|
|
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.
|
package/dist/article.css
CHANGED
|
@@ -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"]}
|
|
@@ -35,7 +35,7 @@ function FigureBand({
|
|
|
35
35
|
children: [
|
|
36
36
|
/* @__PURE__ */ jsx("dt", { className: "mb-2 font-sans text-[13px] font-normal leading-normal text-(--ink-3-aa)", children: figure.label }),
|
|
37
37
|
/* @__PURE__ */ jsx("dd", { className: cn("m-0 font-heading font-normal text-(--ink)", VALUE_SCALE[scale]), children: figure.value }),
|
|
38
|
-
figure.note !== void 0 && figure.note !== null && /* @__PURE__ */ jsx("dd", { className: "m-0 mt-2.5 font-sans text-[13px] font-
|
|
38
|
+
figure.note !== void 0 && figure.note !== null && /* @__PURE__ */ jsx("dd", { className: "m-0 mt-2.5 font-sans text-[13px] [font-weight:var(--body-weight)] leading-[1.5] text-(--ink-3-aa)", children: figure.note })
|
|
39
39
|
]
|
|
40
40
|
},
|
|
41
41
|
figure.id
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/FigureBand/FigureBand.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface Figure {\n /** Stable key; also what a caller keys its own data by. */\n id: string\n /** The label over the value. */\n label: string\n /** The value itself, set in the serif at the band's scale. */\n value: ReactNode\n /** The quiet line under it — a trend, a qualifier, a second fact. */\n note?: ReactNode\n}\n\nexport interface FigureBandProps extends Omit<HTMLAttributes<HTMLDListElement>, 'children'> {\n figures: Figure[]\n /**\n * How large the value is set.\n *\n * `lead` is for a band that is the point of its page — a stats headline,\n * where the numbers ARE the content. `sub` is for a band that supports the\n * page around it, and keeps the ladder honest: a supporting figure must not\n * be set at the same size as the page's own subject.\n */\n scale?: 'lead' | 'sub'\n /** Names the band for assistive tech when it has no visible heading of its own. */\n label?: string\n}\n\nconst VALUE_SCALE = {\n lead: 'text-[clamp(2rem,4cqi,2.75rem)] leading-[1.15] tracking-[-0.02em] tabular-nums',\n sub: 'text-[length:var(--fs-sub)] leading-[1.2]',\n} as const\n\n/** A framed, responsive group of related facts with semantic term/value pairs. */\nexport function FigureBand({\n figures,\n scale = 'lead',\n label,\n className,\n ...rest\n}: FigureBandProps) {\n if (figures.length === 0) return null\n\n // Balance six metrics into two complete rows instead of a four-plus-two grid.\n const columns = Math.min(4, Math.ceil(figures.length / Math.ceil(figures.length / 4)))\n const columnClass = {\n 1: '@xl:basis-full',\n 2: '@xl:basis-[calc(50%-.5px)]',\n 3: '@xl:basis-[calc(33.333333%-.667px)]',\n 4: '@xl:basis-[calc(25%-.75px)]',\n }[columns]\n\n return (\n <div className=\"folio-figure-band @container w-full\">\n <dl\n aria-label={label}\n className={cn(\n 'm-0 flex w-full flex-wrap gap-px overflow-hidden rounded-[10px] border border-(--rule) bg-(--rule)',\n className,\n )}\n {...rest}\n >\n {figures.map((figure) => (\n <div\n key={figure.id}\n className={cn('min-w-0 grow basis-[calc(50%-.5px)] bg-(--paper) px-5 py-5 @xl:px-6 @xl:py-6', columnClass)}\n >\n <dt className=\"mb-2 font-sans text-[13px] font-normal leading-normal text-(--ink-3-aa)\">{figure.label}</dt>\n <dd className={cn('m-0 font-heading font-normal text-(--ink)', VALUE_SCALE[scale])}>\n {figure.value}\n </dd>\n {figure.note !== undefined && figure.note !== null && (\n <dd className=\"m-0 mt-2.5 font-sans text-[13px] font-
|
|
1
|
+
{"version":3,"sources":["../../../src/components/FigureBand/FigureBand.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface Figure {\n /** Stable key; also what a caller keys its own data by. */\n id: string\n /** The label over the value. */\n label: string\n /** The value itself, set in the serif at the band's scale. */\n value: ReactNode\n /** The quiet line under it — a trend, a qualifier, a second fact. */\n note?: ReactNode\n}\n\nexport interface FigureBandProps extends Omit<HTMLAttributes<HTMLDListElement>, 'children'> {\n figures: Figure[]\n /**\n * How large the value is set.\n *\n * `lead` is for a band that is the point of its page — a stats headline,\n * where the numbers ARE the content. `sub` is for a band that supports the\n * page around it, and keeps the ladder honest: a supporting figure must not\n * be set at the same size as the page's own subject.\n */\n scale?: 'lead' | 'sub'\n /** Names the band for assistive tech when it has no visible heading of its own. */\n label?: string\n}\n\nconst VALUE_SCALE = {\n lead: 'text-[clamp(2rem,4cqi,2.75rem)] leading-[1.15] tracking-[-0.02em] tabular-nums',\n sub: 'text-[length:var(--fs-sub)] leading-[1.2]',\n} as const\n\n/** A framed, responsive group of related facts with semantic term/value pairs. */\nexport function FigureBand({\n figures,\n scale = 'lead',\n label,\n className,\n ...rest\n}: FigureBandProps) {\n if (figures.length === 0) return null\n\n // Balance six metrics into two complete rows instead of a four-plus-two grid.\n const columns = Math.min(4, Math.ceil(figures.length / Math.ceil(figures.length / 4)))\n const columnClass = {\n 1: '@xl:basis-full',\n 2: '@xl:basis-[calc(50%-.5px)]',\n 3: '@xl:basis-[calc(33.333333%-.667px)]',\n 4: '@xl:basis-[calc(25%-.75px)]',\n }[columns]\n\n return (\n <div className=\"folio-figure-band @container w-full\">\n <dl\n aria-label={label}\n className={cn(\n 'm-0 flex w-full flex-wrap gap-px overflow-hidden rounded-[10px] border border-(--rule) bg-(--rule)',\n className,\n )}\n {...rest}\n >\n {figures.map((figure) => (\n <div\n key={figure.id}\n className={cn('min-w-0 grow basis-[calc(50%-.5px)] bg-(--paper) px-5 py-5 @xl:px-6 @xl:py-6', columnClass)}\n >\n <dt className=\"mb-2 font-sans text-[13px] font-normal leading-normal text-(--ink-3-aa)\">{figure.label}</dt>\n <dd className={cn('m-0 font-heading font-normal text-(--ink)', VALUE_SCALE[scale])}>\n {figure.value}\n </dd>\n {figure.note !== undefined && figure.note !== null && (\n <dd className=\"m-0 mt-2.5 font-sans text-[13px] [font-weight:var(--body-weight)] leading-[1.5] text-(--ink-3-aa)\">\n {figure.note}\n </dd>\n )}\n </div>\n ))}\n </dl>\n </div>\n )\n}\n\nexport default FigureBand\n"],"mappings":"AAgEU,SAIE,KAJF;AA/DV,SAAS,UAAU;AA4BnB,MAAM,cAAc;AAAA,EAClB,MAAM;AAAA,EACN,KAAK;AACP;AAGO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,MAAI,QAAQ,WAAW,EAAG,QAAO;AAGjC,QAAM,UAAU,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,SAAS,KAAK,KAAK,QAAQ,SAAS,CAAC,CAAC,CAAC;AACrF,QAAM,cAAc;AAAA,IAClB,GAAG;AAAA,IACH,GAAG;AAAA,IACH,GAAG;AAAA,IACH,GAAG;AAAA,EACL,EAAE,OAAO;AAET,SACE,oBAAC,SAAI,WAAU,uCACb;AAAA,IAAC;AAAA;AAAA,MACC,cAAY;AAAA,MACZ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH,kBAAQ,IAAI,CAAC,WACZ;AAAA,QAAC;AAAA;AAAA,UAEC,WAAW,GAAG,gFAAgF,WAAW;AAAA,UAEzG;AAAA,gCAAC,QAAG,WAAU,2EAA2E,iBAAO,OAAM;AAAA,YACtG,oBAAC,QAAG,WAAW,GAAG,6CAA6C,YAAY,KAAK,CAAC,GAC9E,iBAAO,OACV;AAAA,YACC,OAAO,SAAS,UAAa,OAAO,SAAS,QAC5C,oBAAC,QAAG,WAAU,qGACX,iBAAO,MACV;AAAA;AAAA;AAAA,QAVG,OAAO;AAAA,MAYd,CACD;AAAA;AAAA,EACH,GACF;AAEJ;AAEA,IAAO,qBAAQ;","names":[]}
|
|
@@ -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":[]}
|
|
@@ -61,7 +61,7 @@ function Pagination({
|
|
|
61
61
|
{
|
|
62
62
|
"aria-hidden": true,
|
|
63
63
|
"data-folio-animated": true,
|
|
64
|
-
className: "absolute rounded-(--radius-pill) bg-(--
|
|
64
|
+
className: "absolute rounded-(--radius-pill) bg-(--action) transition-[transform,width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none",
|
|
65
65
|
style: {
|
|
66
66
|
transform: `translate(${indicator.offset}px, ${indicator.top}px)`,
|
|
67
67
|
width: indicator.width,
|
|
@@ -83,7 +83,7 @@ function Pagination({
|
|
|
83
83
|
className: cn(
|
|
84
84
|
STEP,
|
|
85
85
|
"mono-meta tabular-nums",
|
|
86
|
-
entry === page ? "border-transparent text-(--
|
|
86
|
+
entry === page ? "border-transparent text-(--action-foreground)" : "border-transparent text-(--ink-2) hover:border-(--rule-2) hover:text-(--ink)"
|
|
87
87
|
),
|
|
88
88
|
children: entry
|
|
89
89
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["'use client'\n\nimport { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'\nimport type { HTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { useSelectionIndicator } from '../../lib/useSelectionIndicator'\n\nexport interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {\n /** 1-based. */\n page: number\n /** Total pages. A value of 1 or less renders nothing. */\n pageCount: number\n onPageChange: (page: number) => void\n /** How many numbered pages sit either side of the current one. */\n siblings?: number\n label?: string\n /**\n * Names the two step controls.\n *\n * Each is a chevron and nothing else, so these strings are its entire\n * accessible name — the same reason `AppShell` exposes `openLabel` and\n * `closeLabel` rather than baking them in.\n */\n previousLabel?: string\n nextLabel?: string\n /**\n * Names one numbered page.\n *\n * A function rather than a template, because the number does not sit in the\n * same place in every language and neither does the noun around it —\n * `\"Page 3\"`, `\"第 3 页\"`, `\"Seite 3\"`. The visible digit is the same either\n * way; this is what a screen reader hears instead of a bare number.\n */\n pageLabel?: (page: number) => string\n}\n\nconst GAP = '…' as const\n\n/**\n * Builds the visible page list: always the first and last page, a window around\n * the current one, and an ellipsis wherever the sequence skips.\n *\n * Returned as numbers and a literal ellipsis rather than as pre-rendered nodes,\n * so the shape is testable without a DOM — the off-by-one at the window edges\n * is the whole difficulty of this component.\n */\nexport function paginationRange(page: number, pageCount: number, siblings = 1): (number | typeof GAP)[] {\n const first = 1\n const last = pageCount\n\n // Below the width the elided form would occupy — first, last, the window and\n // two ellipses — printing every page is both shorter and more useful. Without\n // this, a five-page list renders \"1 2 … 5\", which hides a page to save\n // nothing.\n const widest = 2 * siblings + 5\n if (pageCount <= widest) {\n return Array.from({ length: pageCount }, (_, index) => index + 1)\n }\n\n const start = Math.max(first, page - siblings)\n const end = Math.min(last, page + siblings)\n\n const pages: (number | typeof GAP)[] = []\n if (start > first) {\n pages.push(first)\n // A single skipped page is printed, not elided: \"1 … 3\" is longer than\n // \"1 2 3\" and tells the reader less.\n if (start > first + 2) pages.push(GAP)\n else if (start === first + 2) pages.push(first + 1)\n }\n for (let n = start; n <= end; n += 1) pages.push(n)\n if (end < last) {\n if (end < last - 2) pages.push(GAP)\n else if (end === last - 2) pages.push(last - 1)\n pages.push(last)\n }\n return pages\n}\n\nconst STEP =\n 'inline-flex size-(--control-h-sm) items-center justify-center rounded-(--radius-pill) border transition-colors duration-(--duration-fast) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none'\n\n/**\n * Numbered pagination.\n *\n * The current page is marked by one filled pill that TRAVELS between the\n * numbers rather than by a background switching off on one and on on another.\n * Two backgrounds cross-fading reads as two things changing; a shape moving\n * reads as the one thing that actually did.\n *\n * A `<nav>` wrapping a list, and the current page is a `<button aria-current>`\n * rather than a styled `<span>` — a reader jumping by landmark needs to find\n * the control, and a reader on the current page needs to be told they are\n * already there.\n *\n * Renders nothing at one page or fewer. A pager for a single page is furniture.\n *\n * Every string a reader hears is a prop. The chevrons carry no text, so\n * `previousLabel` and `nextLabel` are the whole of those two controls' names,\n * and `pageLabel` is a function because \"Page 3\" is a phrase whose parts move\n * around between languages.\n *\n * @example\n * <Pagination page={page} pageCount={12} onPageChange={setPage} />\n */\nexport function Pagination({\n page,\n pageCount,\n onPageChange,\n siblings = 1,\n label = 'Pagination',\n previousLabel = 'Previous page',\n nextLabel = 'Next page',\n pageLabel = (page) => `Page ${page}`,\n className,\n ...rest\n}: PaginationProps) {\n // Called before the early return, because a hook cannot be conditional — and\n // keyed on the page so the pill re-measures when the elided window shifts and\n // the numbers under it change.\n const [listRef, indicator] = useSelectionIndicator<HTMLOListElement>(String(page))\n\n if (pageCount <= 1) return null\n const pages = paginationRange(page, pageCount, siblings)\n\n return (\n <nav aria-label={label} className={cn('flex items-center gap-1.5', className)} {...rest}>\n <button\n type=\"button\"\n aria-label={previousLabel}\n disabled={page <= 1}\n onClick={() => onPageChange(page - 1)}\n className={cn(STEP, 'border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)')}\n >\n <RiArrowLeftSLine size={16} aria-hidden />\n </button>\n\n <ol ref={listRef} className=\"relative m-0 flex list-none items-center gap-1 p-0\">\n {indicator.ready && (\n <span\n aria-hidden\n data-folio-animated\n className=\"absolute rounded-(--radius-pill) bg-(--
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["'use client'\n\nimport { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'\nimport type { HTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { useSelectionIndicator } from '../../lib/useSelectionIndicator'\n\nexport interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {\n /** 1-based. */\n page: number\n /** Total pages. A value of 1 or less renders nothing. */\n pageCount: number\n onPageChange: (page: number) => void\n /** How many numbered pages sit either side of the current one. */\n siblings?: number\n label?: string\n /**\n * Names the two step controls.\n *\n * Each is a chevron and nothing else, so these strings are its entire\n * accessible name — the same reason `AppShell` exposes `openLabel` and\n * `closeLabel` rather than baking them in.\n */\n previousLabel?: string\n nextLabel?: string\n /**\n * Names one numbered page.\n *\n * A function rather than a template, because the number does not sit in the\n * same place in every language and neither does the noun around it —\n * `\"Page 3\"`, `\"第 3 页\"`, `\"Seite 3\"`. The visible digit is the same either\n * way; this is what a screen reader hears instead of a bare number.\n */\n pageLabel?: (page: number) => string\n}\n\nconst GAP = '…' as const\n\n/**\n * Builds the visible page list: always the first and last page, a window around\n * the current one, and an ellipsis wherever the sequence skips.\n *\n * Returned as numbers and a literal ellipsis rather than as pre-rendered nodes,\n * so the shape is testable without a DOM — the off-by-one at the window edges\n * is the whole difficulty of this component.\n */\nexport function paginationRange(page: number, pageCount: number, siblings = 1): (number | typeof GAP)[] {\n const first = 1\n const last = pageCount\n\n // Below the width the elided form would occupy — first, last, the window and\n // two ellipses — printing every page is both shorter and more useful. Without\n // this, a five-page list renders \"1 2 … 5\", which hides a page to save\n // nothing.\n const widest = 2 * siblings + 5\n if (pageCount <= widest) {\n return Array.from({ length: pageCount }, (_, index) => index + 1)\n }\n\n const start = Math.max(first, page - siblings)\n const end = Math.min(last, page + siblings)\n\n const pages: (number | typeof GAP)[] = []\n if (start > first) {\n pages.push(first)\n // A single skipped page is printed, not elided: \"1 … 3\" is longer than\n // \"1 2 3\" and tells the reader less.\n if (start > first + 2) pages.push(GAP)\n else if (start === first + 2) pages.push(first + 1)\n }\n for (let n = start; n <= end; n += 1) pages.push(n)\n if (end < last) {\n if (end < last - 2) pages.push(GAP)\n else if (end === last - 2) pages.push(last - 1)\n pages.push(last)\n }\n return pages\n}\n\nconst STEP =\n 'inline-flex size-(--control-h-sm) items-center justify-center rounded-(--radius-pill) border transition-colors duration-(--duration-fast) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none'\n\n/**\n * Numbered pagination.\n *\n * The current page is marked by one filled pill that TRAVELS between the\n * numbers rather than by a background switching off on one and on on another.\n * Two backgrounds cross-fading reads as two things changing; a shape moving\n * reads as the one thing that actually did.\n *\n * A `<nav>` wrapping a list, and the current page is a `<button aria-current>`\n * rather than a styled `<span>` — a reader jumping by landmark needs to find\n * the control, and a reader on the current page needs to be told they are\n * already there.\n *\n * Renders nothing at one page or fewer. A pager for a single page is furniture.\n *\n * Every string a reader hears is a prop. The chevrons carry no text, so\n * `previousLabel` and `nextLabel` are the whole of those two controls' names,\n * and `pageLabel` is a function because \"Page 3\" is a phrase whose parts move\n * around between languages.\n *\n * @example\n * <Pagination page={page} pageCount={12} onPageChange={setPage} />\n */\nexport function Pagination({\n page,\n pageCount,\n onPageChange,\n siblings = 1,\n label = 'Pagination',\n previousLabel = 'Previous page',\n nextLabel = 'Next page',\n pageLabel = (page) => `Page ${page}`,\n className,\n ...rest\n}: PaginationProps) {\n // Called before the early return, because a hook cannot be conditional — and\n // keyed on the page so the pill re-measures when the elided window shifts and\n // the numbers under it change.\n const [listRef, indicator] = useSelectionIndicator<HTMLOListElement>(String(page))\n\n if (pageCount <= 1) return null\n const pages = paginationRange(page, pageCount, siblings)\n\n return (\n <nav aria-label={label} className={cn('flex items-center gap-1.5', className)} {...rest}>\n <button\n type=\"button\"\n aria-label={previousLabel}\n disabled={page <= 1}\n onClick={() => onPageChange(page - 1)}\n className={cn(STEP, 'border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)')}\n >\n <RiArrowLeftSLine size={16} aria-hidden />\n </button>\n\n <ol ref={listRef} className=\"relative m-0 flex list-none items-center gap-1 p-0\">\n {indicator.ready && (\n <span\n aria-hidden\n data-folio-animated\n className=\"absolute rounded-(--radius-pill) bg-(--action) transition-[transform,width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none\"\n style={{\n transform: `translate(${indicator.offset}px, ${indicator.top}px)`,\n width: indicator.width,\n height: indicator.height,\n insetInlineStart: 0,\n top: 0,\n }}\n />\n )}\n {pages.map((entry, index) =>\n entry === GAP ? (\n <li key={`gap-${index}`} aria-hidden=\"true\" className=\"px-1 mono-meta text-(--ink-3-aa)\">\n {GAP}\n </li>\n ) : (\n <li key={entry} className=\"relative z-1\">\n <button\n type=\"button\"\n aria-current={entry === page ? 'page' : undefined}\n aria-label={pageLabel(entry)}\n data-indicator-active={entry === page ? 'true' : undefined}\n onClick={() => onPageChange(entry)}\n className={cn(\n STEP,\n 'mono-meta tabular-nums',\n entry === page\n ? 'border-transparent text-(--action-foreground)'\n : 'border-transparent text-(--ink-2) hover:border-(--rule-2) hover:text-(--ink)',\n )}\n >\n {entry}\n </button>\n </li>\n ),\n )}\n </ol>\n\n <button\n type=\"button\"\n aria-label={nextLabel}\n disabled={page >= pageCount}\n onClick={() => onPageChange(page + 1)}\n className={cn(STEP, 'border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)')}\n >\n <RiArrowRightSLine size={16} aria-hidden />\n </button>\n </nav>\n )\n}\n\nexport default Pagination\n"],"mappings":"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","names":["page"]}
|
|
@@ -7,7 +7,7 @@ const ALIGN = {
|
|
|
7
7
|
center: "text-center",
|
|
8
8
|
end: "text-end"
|
|
9
9
|
};
|
|
10
|
-
const ROWS = "[&_thead]:border-b [&_thead]:border-(--rule-
|
|
10
|
+
const ROWS = "[&_thead]:border-b [&_thead]:border-(--rule-divider) [&_tbody_tr]:border-b [&_tbody_tr]:border-(--rule) [&_tbody_tr:last-child]:border-b-0";
|
|
11
11
|
const SOFT_HEAD = "[&_thead]:border-(--rule-2)";
|
|
12
12
|
const COLUMNS = "[&_th]:border-e [&_td]:border-e [&_th]:border-(--rule) [&_td]:border-(--rule) [&_th:last-child]:border-e-0 [&_td:last-child]:border-e-0";
|
|
13
13
|
const BOXED = "rounded-(--radius) border border-(--rule-2) [--table-pad-x:0.75rem]";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["import { RiArrowDownLine, RiArrowUpLine, RiExpandUpDownLine } from '@remixicon/react'\nimport type { HTMLAttributes, TableHTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { warnBlankName } from '../../lib/warn'\n\nexport type TableAlign = 'start' | 'center' | 'end'\n\n/**\n * Which rules the table draws.\n *\n * `rows` is the default and the right answer for reading down a column: one\n * hairline between records and nothing else, so the eye tracks a line across\n * without a grid competing for it. `grid` adds the vertical rules a dense\n * numeric table needs to keep columns apart. `bordered` puts an edge around the\n * whole thing, for a table that sits loose on a page rather than inside a card\n * that already has one. `none` is for a table inside something that draws its\n * own structure.\n */\nexport type TableBorders = 'rows' | 'grid' | 'bordered' | 'bordered-grid' | 'none'\n\nconst ALIGN: Record<TableAlign, string> = {\n start: 'text-start',\n center: 'text-center',\n end: 'text-end',\n}\n\n/**\n * Every rule the table draws, expressed from ONE element.\n *\n * Not React context, which was the first attempt: `createContext` makes the\n * module client-only, which turned every table on every page into a hydration\n * boundary and took the static build down with it. Descendant selectors from\n * the wrapper reach the same cells, cost nothing at runtime, and keep the whole\n * component server-renderable.\n *\n * `--table-pad-x` rather than a padding class per mode, because the ruled\n * variants need symmetric padding inside their boxes while the plain one wants\n * a trailing gap and no leading one.\n */\nconst ROWS =\n '[&_thead]:border-b [&_thead]:border-(--rule-hard) [&_tbody_tr]:border-b [&_tbody_tr]:border-(--rule) [&_tbody_tr:last-child]:border-b-0'\nconst SOFT_HEAD = '[&_thead]:border-(--rule-2)'\nconst COLUMNS =\n '[&_th]:border-e [&_td]:border-e [&_th]:border-(--rule) [&_td]:border-(--rule) [&_th:last-child]:border-e-0 [&_td:last-child]:border-e-0'\nconst BOXED = 'rounded-(--radius) border border-(--rule-2) [--table-pad-x:0.75rem]'\n\nconst BORDERS: Record<TableBorders, string> = {\n rows: ROWS,\n grid: cn(ROWS, COLUMNS, '[--table-pad-x:0.75rem]'),\n bordered: cn(ROWS, SOFT_HEAD, BOXED),\n 'bordered-grid': cn(ROWS, SOFT_HEAD, COLUMNS, BOXED),\n none: '',\n}\n\nexport interface TableProps extends TableHTMLAttributes<HTMLTableElement> {\n /**\n * Describes the table for a screen reader, which cannot see the heading above\n * it. Rendered as a visually-hidden `<caption>` — required, because an\n * unnamed table in a page with three tables is unnavigable.\n */\n caption: string\n /** Prints the caption instead of hiding it. */\n showCaption?: boolean\n /**\n * Pins the header row while the body scrolls. Needs a bounded height on the\n * container — otherwise the page scrolls, not the table, and nothing sticks.\n */\n stickyHeader?: boolean\n /** Which rules to draw. See {@link TableBorders}. */\n borders?: TableBorders\n /** Tightens the row padding, for a table that is mostly numbers. */\n density?: 'comfortable' | 'compact'\n}\n\n/**\n * A ruled data table.\n *\n * Wrapped in its own horizontally scrolling container, because a table is the\n * one block that legitimately exceeds the measure — and a page that scrolls\n * sideways as a whole is a layout bug, while a table that does is a table.\n *\n * That container is focusable, and it has to be. A scrollable region whose\n * contents are not themselves focusable is unreachable by keyboard: there is no\n * element to Tab to and therefore no way to press an arrow key at it, so the\n * columns past the fold simply do not exist for anyone not using a mouse.\n *\n * It is also POSITIONED, which is not decoration. `sr-only` is\n * `position: absolute`, so a visually-hidden label in a cell resolves against\n * the nearest positioned ancestor — and with none between it and the document,\n * it escapes the scroll container and every `overflow-hidden` around it, and\n * widens the page by however far the table happens to be scrolled.\n *\n * No zebra striping at any border setting. In a monochrome system a striped row\n * is a second surface competing with the page ground, and the hairline between\n * rows is already enough to track a line across.\n *\n * @example\n * <Table caption=\"Deploy history\">\n * <THead><TR><TH>Commit</TH><TH align=\"end\" sortable sortDirection={dir} onSort={sort}>Duration</TH></TR></THead>\n * <TBody><TR><TD>a1b2c3d</TD><TD align=\"end\">2m 14s</TD></TR></TBody>\n * </Table>\n * @example\n * <Table caption=\"Inventory\" borders=\"bordered-grid\" density=\"compact\">…</Table>\n */\nexport function Table({\n caption,\n showCaption = false,\n stickyHeader = false,\n borders = 'rows',\n density = 'comfortable',\n className,\n children,\n ...rest\n}: TableProps) {\n warnBlankName('Table', 'caption', caption, 'the table is announced with no name and a reader arrives in it with no idea what it lists')\n return (\n <div\n tabIndex={0}\n role=\"region\"\n aria-label={caption}\n data-table-density={density}\n className={cn('relative w-full overflow-x-auto scroll-slim', BORDERS[borders])}\n >\n <table\n className={cn(\n 'w-full border-collapse text-start text-sm',\n stickyHeader &&\n '[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-(--z-rule) [&_thead]:bg-(--paper)',\n className,\n )}\n {...rest}\n >\n <caption className={cn(showCaption ? 'pb-3 text-start eyebrow text-(--ink-3-aa)' : 'sr-only')}>\n {caption}\n </caption>\n {children}\n </table>\n </div>\n )\n}\n\n/** Header group. The rule under it is drawn by the table's border setting. */\nexport function THead({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <thead className={className} {...rest} />\n}\n\nexport function TBody({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <tbody className={className} {...rest} />\n}\n\nexport function TR({ className, ...rest }: HTMLAttributes<HTMLTableRowElement>) {\n return <tr className={className} {...rest} />\n}\n\nexport type SortDirection = 'ascending' | 'descending' | 'none'\n\nexport interface THProps extends Omit<ThHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the column's contents sit against. Numbers belong at `end`. */\n align?: TableAlign\n /** Makes the label a button and shows the sort marker. */\n sortable?: boolean\n /**\n * Which way this column is sorted. Also set as `aria-sort`, which is the\n * only way a screen reader learns a table is ordered at all — a caret drawn\n * in the header tells it nothing.\n */\n sortDirection?: SortDirection\n onSort?: () => void\n}\n\n/**\n * Cell padding, in one string both cells share.\n *\n * `--table-pad-x` is 0 by default and set by the ruled variants, so the plain\n * table keeps its trailing gap and no leading one while a boxed table gets\n * symmetric padding inside its border.\n */\nconst CELL =\n 'px-[var(--table-pad-x,0)] [&:not(:last-child)]:pe-[calc(var(--table-pad-x,0px)+1.5rem)]'\n\nconst SORT_ICON = {\n ascending: RiArrowUpLine,\n descending: RiArrowDownLine,\n none: RiExpandUpDownLine,\n} as const\n\n/**\n * A column label. Mono and uppercase, so it never reads as data.\n *\n * When sortable, the label becomes a `<button>` INSIDE the `<th>` rather than\n * the `<th>` becoming clickable: a cell with a click handler is not focusable\n * and not announced as a control, so the sort exists only for a mouse.\n *\n * Sorting is per column and opt-in. A table where every header is a button\n * invites the reader to try sorting a column the data cannot be ordered by.\n */\nexport function TH({\n align = 'start',\n sortable = false,\n sortDirection = 'none',\n onSort,\n className,\n children,\n ...rest\n}: THProps) {\n const Icon = SORT_ICON[sortDirection]\n\n return (\n <th\n scope=\"col\"\n aria-sort={sortable ? sortDirection : undefined}\n className={cn(CELL, 'py-3 align-bottom eyebrow text-(--ink-3-aa)', ALIGN[align], className)}\n {...rest}\n >\n {sortable ? (\n <button\n type=\"button\"\n onClick={onSort}\n className={cn(\n 'group inline-flex items-center gap-1.5 eyebrow transition-colors duration-(--duration-fast) hover:text-(--ink)',\n sortDirection === 'none' ? 'text-(--ink-3-aa)' : 'text-(--ink)',\n )}\n >\n {children}\n <Icon\n size={12}\n aria-hidden\n className={cn(\n 'shrink-0 transition-opacity',\n sortDirection === 'none' && 'opacity-40 group-hover:opacity-100',\n )}\n />\n </button>\n ) : (\n children\n )}\n </th>\n )\n}\n\nexport interface TDProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the cell's contents sit against. Match the column's header. */\n align?: TableAlign\n}\n\n/**\n * A body cell.\n *\n * TOP-aligned, and that is a choice about which kind of table is the common\n * one here: a cell holding a paragraph. Top is what lines the first lines of a\n * row up with each other, and centring a two-word term against a four-line\n * description puts the term in the middle of nothing.\n *\n * It is the wrong default for the other kind — a row of one-line values with a\n * control in it. A 36px button next to 16px of text makes a 52px row, and every\n * other cell then hangs at the top of it with twenty pixels of nothing\n * underneath, which reads as a column that has slipped. Pass\n * `className=\"align-middle\"` on those rows; the data-table template does.\n */\nexport function TD({ align = 'start', className, ...rest }: TDProps) {\n return (\n <td\n className={cn(CELL, 'py-[var(--table-pad-y)] align-top text-(--ink-2)', ALIGN[align], className)}\n {...rest}\n />\n )\n}\n\nexport default Table\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["import { RiArrowDownLine, RiArrowUpLine, RiExpandUpDownLine } from '@remixicon/react'\nimport type { HTMLAttributes, TableHTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { warnBlankName } from '../../lib/warn'\n\nexport type TableAlign = 'start' | 'center' | 'end'\n\n/**\n * Which rules the table draws.\n *\n * `rows` is the default and the right answer for reading down a column: one\n * hairline between records and nothing else, so the eye tracks a line across\n * without a grid competing for it. `grid` adds the vertical rules a dense\n * numeric table needs to keep columns apart. `bordered` puts an edge around the\n * whole thing, for a table that sits loose on a page rather than inside a card\n * that already has one. `none` is for a table inside something that draws its\n * own structure.\n */\nexport type TableBorders = 'rows' | 'grid' | 'bordered' | 'bordered-grid' | 'none'\n\nconst ALIGN: Record<TableAlign, string> = {\n start: 'text-start',\n center: 'text-center',\n end: 'text-end',\n}\n\n/**\n * Every rule the table draws, expressed from ONE element.\n *\n * Not React context, which was the first attempt: `createContext` makes the\n * module client-only, which turned every table on every page into a hydration\n * boundary and took the static build down with it. Descendant selectors from\n * the wrapper reach the same cells, cost nothing at runtime, and keep the whole\n * component server-renderable.\n *\n * `--table-pad-x` rather than a padding class per mode, because the ruled\n * variants need symmetric padding inside their boxes while the plain one wants\n * a trailing gap and no leading one.\n */\nconst ROWS =\n '[&_thead]:border-b [&_thead]:border-(--rule-divider) [&_tbody_tr]:border-b [&_tbody_tr]:border-(--rule) [&_tbody_tr:last-child]:border-b-0'\nconst SOFT_HEAD = '[&_thead]:border-(--rule-2)'\nconst COLUMNS =\n '[&_th]:border-e [&_td]:border-e [&_th]:border-(--rule) [&_td]:border-(--rule) [&_th:last-child]:border-e-0 [&_td:last-child]:border-e-0'\nconst BOXED = 'rounded-(--radius) border border-(--rule-2) [--table-pad-x:0.75rem]'\n\nconst BORDERS: Record<TableBorders, string> = {\n rows: ROWS,\n grid: cn(ROWS, COLUMNS, '[--table-pad-x:0.75rem]'),\n bordered: cn(ROWS, SOFT_HEAD, BOXED),\n 'bordered-grid': cn(ROWS, SOFT_HEAD, COLUMNS, BOXED),\n none: '',\n}\n\nexport interface TableProps extends TableHTMLAttributes<HTMLTableElement> {\n /**\n * Describes the table for a screen reader, which cannot see the heading above\n * it. Rendered as a visually-hidden `<caption>` — required, because an\n * unnamed table in a page with three tables is unnavigable.\n */\n caption: string\n /** Prints the caption instead of hiding it. */\n showCaption?: boolean\n /**\n * Pins the header row while the body scrolls. Needs a bounded height on the\n * container — otherwise the page scrolls, not the table, and nothing sticks.\n */\n stickyHeader?: boolean\n /** Which rules to draw. See {@link TableBorders}. */\n borders?: TableBorders\n /** Tightens the row padding, for a table that is mostly numbers. */\n density?: 'comfortable' | 'compact'\n}\n\n/**\n * A ruled data table.\n *\n * Wrapped in its own horizontally scrolling container, because a table is the\n * one block that legitimately exceeds the measure — and a page that scrolls\n * sideways as a whole is a layout bug, while a table that does is a table.\n *\n * That container is focusable, and it has to be. A scrollable region whose\n * contents are not themselves focusable is unreachable by keyboard: there is no\n * element to Tab to and therefore no way to press an arrow key at it, so the\n * columns past the fold simply do not exist for anyone not using a mouse.\n *\n * It is also POSITIONED, which is not decoration. `sr-only` is\n * `position: absolute`, so a visually-hidden label in a cell resolves against\n * the nearest positioned ancestor — and with none between it and the document,\n * it escapes the scroll container and every `overflow-hidden` around it, and\n * widens the page by however far the table happens to be scrolled.\n *\n * No zebra striping at any border setting. In a monochrome system a striped row\n * is a second surface competing with the page ground, and the hairline between\n * rows is already enough to track a line across.\n *\n * @example\n * <Table caption=\"Deploy history\">\n * <THead><TR><TH>Commit</TH><TH align=\"end\" sortable sortDirection={dir} onSort={sort}>Duration</TH></TR></THead>\n * <TBody><TR><TD>a1b2c3d</TD><TD align=\"end\">2m 14s</TD></TR></TBody>\n * </Table>\n * @example\n * <Table caption=\"Inventory\" borders=\"bordered-grid\" density=\"compact\">…</Table>\n */\nexport function Table({\n caption,\n showCaption = false,\n stickyHeader = false,\n borders = 'rows',\n density = 'comfortable',\n className,\n children,\n ...rest\n}: TableProps) {\n warnBlankName('Table', 'caption', caption, 'the table is announced with no name and a reader arrives in it with no idea what it lists')\n return (\n <div\n tabIndex={0}\n role=\"region\"\n aria-label={caption}\n data-table-density={density}\n className={cn('relative w-full overflow-x-auto scroll-slim', BORDERS[borders])}\n >\n <table\n className={cn(\n 'w-full border-collapse text-start text-sm',\n stickyHeader &&\n '[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-(--z-rule) [&_thead]:bg-(--paper)',\n className,\n )}\n {...rest}\n >\n <caption className={cn(showCaption ? 'pb-3 text-start eyebrow text-(--ink-3-aa)' : 'sr-only')}>\n {caption}\n </caption>\n {children}\n </table>\n </div>\n )\n}\n\n/** Header group. The rule under it is drawn by the table's border setting. */\nexport function THead({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <thead className={className} {...rest} />\n}\n\nexport function TBody({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <tbody className={className} {...rest} />\n}\n\nexport function TR({ className, ...rest }: HTMLAttributes<HTMLTableRowElement>) {\n return <tr className={className} {...rest} />\n}\n\nexport type SortDirection = 'ascending' | 'descending' | 'none'\n\nexport interface THProps extends Omit<ThHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the column's contents sit against. Numbers belong at `end`. */\n align?: TableAlign\n /** Makes the label a button and shows the sort marker. */\n sortable?: boolean\n /**\n * Which way this column is sorted. Also set as `aria-sort`, which is the\n * only way a screen reader learns a table is ordered at all — a caret drawn\n * in the header tells it nothing.\n */\n sortDirection?: SortDirection\n onSort?: () => void\n}\n\n/**\n * Cell padding, in one string both cells share.\n *\n * `--table-pad-x` is 0 by default and set by the ruled variants, so the plain\n * table keeps its trailing gap and no leading one while a boxed table gets\n * symmetric padding inside its border.\n */\nconst CELL =\n 'px-[var(--table-pad-x,0)] [&:not(:last-child)]:pe-[calc(var(--table-pad-x,0px)+1.5rem)]'\n\nconst SORT_ICON = {\n ascending: RiArrowUpLine,\n descending: RiArrowDownLine,\n none: RiExpandUpDownLine,\n} as const\n\n/**\n * A column label. Mono and uppercase, so it never reads as data.\n *\n * When sortable, the label becomes a `<button>` INSIDE the `<th>` rather than\n * the `<th>` becoming clickable: a cell with a click handler is not focusable\n * and not announced as a control, so the sort exists only for a mouse.\n *\n * Sorting is per column and opt-in. A table where every header is a button\n * invites the reader to try sorting a column the data cannot be ordered by.\n */\nexport function TH({\n align = 'start',\n sortable = false,\n sortDirection = 'none',\n onSort,\n className,\n children,\n ...rest\n}: THProps) {\n const Icon = SORT_ICON[sortDirection]\n\n return (\n <th\n scope=\"col\"\n aria-sort={sortable ? sortDirection : undefined}\n className={cn(CELL, 'py-3 align-bottom eyebrow text-(--ink-3-aa)', ALIGN[align], className)}\n {...rest}\n >\n {sortable ? (\n <button\n type=\"button\"\n onClick={onSort}\n className={cn(\n 'group inline-flex items-center gap-1.5 eyebrow transition-colors duration-(--duration-fast) hover:text-(--ink)',\n sortDirection === 'none' ? 'text-(--ink-3-aa)' : 'text-(--ink)',\n )}\n >\n {children}\n <Icon\n size={12}\n aria-hidden\n className={cn(\n 'shrink-0 transition-opacity',\n sortDirection === 'none' && 'opacity-40 group-hover:opacity-100',\n )}\n />\n </button>\n ) : (\n children\n )}\n </th>\n )\n}\n\nexport interface TDProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the cell's contents sit against. Match the column's header. */\n align?: TableAlign\n}\n\n/**\n * A body cell.\n *\n * TOP-aligned, and that is a choice about which kind of table is the common\n * one here: a cell holding a paragraph. Top is what lines the first lines of a\n * row up with each other, and centring a two-word term against a four-line\n * description puts the term in the middle of nothing.\n *\n * It is the wrong default for the other kind — a row of one-line values with a\n * control in it. A 36px button next to 16px of text makes a 52px row, and every\n * other cell then hangs at the top of it with twenty pixels of nothing\n * underneath, which reads as a column that has slipped. Pass\n * `className=\"align-middle\"` on those rows; the data-table template does.\n */\nexport function TD({ align = 'start', className, ...rest }: TDProps) {\n return (\n <td\n className={cn(CELL, 'py-[var(--table-pad-y)] align-top text-(--ink-2)', ALIGN[align], className)}\n {...rest}\n />\n )\n}\n\nexport default Table\n"],"mappings":"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","names":[]}
|