@misoto22/design 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +87 -0
- package/CLAUDE.md +1 -0
- package/README.md +50 -5
- package/bin/misoto22-design.mjs +59 -13
- package/dist/agent/Accordion.md +24 -1
- package/dist/agent/Alert.md +23 -1
- package/dist/agent/AppShell.md +30 -2
- package/dist/agent/ArchitectureFigure.md +25 -2
- package/dist/agent/AreaChart.md +27 -2
- package/dist/agent/Article.md +26 -3
- package/dist/agent/AspectRatio.md +57 -0
- package/dist/agent/Avatar.md +25 -5
- package/dist/agent/Badge.md +22 -1
- package/dist/agent/BarChart.md +29 -2
- package/dist/agent/BarList.md +27 -1
- package/dist/agent/BigNumber.md +26 -2
- package/dist/agent/BoxPlot.md +24 -1
- package/dist/agent/Breadcrumb.md +24 -2
- package/dist/agent/BulletChart.md +23 -1
- package/dist/agent/Button.md +25 -1
- package/dist/agent/Calendar.md +37 -3
- package/dist/agent/Card.md +24 -2
- package/dist/agent/Checkbox.md +23 -2
- package/dist/agent/Code.md +50 -0
- package/dist/agent/CodeBlock.md +74 -0
- package/dist/agent/Collapsible.md +22 -1
- package/dist/agent/Combobox.md +29 -3
- package/dist/agent/Command.md +25 -2
- package/dist/agent/ComposedChart.md +26 -2
- package/dist/agent/ContextMenu.md +37 -4
- package/dist/agent/DataflowFigure.md +22 -1
- package/dist/agent/DatePicker.md +34 -6
- package/dist/agent/DescriptionList.md +68 -0
- package/dist/agent/Diagram.md +27 -3
- package/dist/agent/DiagramCanvas.md +24 -1
- package/dist/agent/DiagramExportMenu.md +29 -3
- package/dist/agent/DiagramInspector.md +23 -1
- package/dist/agent/DiagramLegend.md +22 -1
- package/dist/agent/DiagramMinimap.md +24 -4
- package/dist/agent/DiagramToolbar.md +23 -1
- package/dist/agent/Dialog.md +26 -3
- package/dist/agent/DropdownMenu.md +45 -6
- package/dist/agent/EmptyState.md +26 -1
- package/dist/agent/ErrorState.md +27 -2
- package/dist/agent/Facet.md +25 -1
- package/dist/agent/Field.md +45 -4
- package/dist/agent/FigureBand.md +24 -1
- package/dist/agent/FloatingIconButton.md +23 -1
- package/dist/agent/FunnelChart.md +23 -1
- package/dist/agent/Heading.md +64 -0
- package/dist/agent/Heatmap.md +25 -1
- package/dist/agent/Histogram.md +26 -2
- package/dist/agent/Input.md +22 -1
- package/dist/agent/Kbd.md +19 -1
- package/dist/agent/LifecycleFigure.md +22 -1
- package/dist/agent/LineChart.md +26 -2
- package/dist/agent/LinkArrow.md +20 -1
- package/dist/agent/Markdown.md +68 -0
- package/dist/agent/NativeSelect.md +23 -2
- package/dist/agent/NavItem.md +21 -1
- package/dist/agent/Pagination.md +28 -2
- package/dist/agent/PieChart.md +26 -2
- package/dist/agent/Popover.md +23 -1
- package/dist/agent/Progress.md +27 -3
- package/dist/agent/RadarChart.md +26 -2
- package/dist/agent/RadialChart.md +26 -3
- package/dist/agent/RadioGroup.md +23 -2
- package/dist/agent/SankeyChart.md +25 -2
- package/dist/agent/ScatterChart.md +27 -2
- package/dist/agent/ScrollArea.md +24 -3
- package/dist/agent/SearchableMenu.md +23 -1
- package/dist/agent/Select.md +29 -3
- package/dist/agent/Separator.md +28 -4
- package/dist/agent/SequenceFigure.md +23 -1
- package/dist/agent/Sheet.md +25 -2
- package/dist/agent/Skeleton.md +24 -2
- package/dist/agent/Slider.md +27 -4
- package/dist/agent/Sparkline.md +23 -1
- package/dist/agent/Spinner.md +24 -2
- package/dist/agent/StatusDot.md +20 -1
- package/dist/agent/StatusPill.md +28 -2
- package/dist/agent/Steps.md +24 -1
- package/dist/agent/Switch.md +21 -1
- package/dist/agent/Table.md +26 -3
- package/dist/agent/Tabs.md +24 -2
- package/dist/agent/Tag.md +40 -9
- package/dist/agent/Text.md +65 -0
- package/dist/agent/Textarea.md +20 -1
- package/dist/agent/Timestamp.md +64 -0
- package/dist/agent/Toast.md +26 -2
- package/dist/agent/ToggleGroup.md +24 -2
- package/dist/agent/Toolbar.md +71 -0
- package/dist/agent/Tooltip.md +23 -1
- package/dist/agent/TreemapChart.md +24 -1
- package/dist/agent/WaterfallChart.md +24 -1
- package/dist/agent/WorkflowFigure.md +24 -2
- package/dist/agent/catalog.json +5602 -745
- package/dist/agent/index.md +13 -4
- package/dist/article.css +16 -4
- package/dist/charts/AreaChart/AreaChart.d.ts +9 -2
- package/dist/charts/AreaChart/AreaChart.js +12 -13
- package/dist/charts/AreaChart/AreaChart.js.map +1 -1
- package/dist/charts/BarChart/BarChart.d.ts +14 -3
- package/dist/charts/BarChart/BarChart.js +20 -12
- package/dist/charts/BarChart/BarChart.js.map +1 -1
- package/dist/charts/BarList/BarList.d.ts +9 -1
- package/dist/charts/BarList/BarList.js +9 -1
- package/dist/charts/BarList/BarList.js.map +1 -1
- package/dist/charts/BigNumber/BigNumber.d.ts +19 -3
- package/dist/charts/BigNumber/BigNumber.js +25 -5
- package/dist/charts/BigNumber/BigNumber.js.map +1 -1
- package/dist/charts/BoxPlot/BoxPlot.js +4 -2
- package/dist/charts/BoxPlot/BoxPlot.js.map +1 -1
- package/dist/charts/BulletChart/BulletChart.js +28 -7
- package/dist/charts/BulletChart/BulletChart.js.map +1 -1
- package/dist/charts/ComposedChart/ComposedChart.d.ts +9 -2
- package/dist/charts/ComposedChart/ComposedChart.js +10 -10
- package/dist/charts/ComposedChart/ComposedChart.js.map +1 -1
- package/dist/charts/FunnelChart/FunnelChart.d.ts +7 -2
- package/dist/charts/FunnelChart/FunnelChart.js +6 -16
- package/dist/charts/FunnelChart/FunnelChart.js.map +1 -1
- package/dist/charts/Heatmap/Heatmap.d.ts +17 -3
- package/dist/charts/Heatmap/Heatmap.js +20 -11
- package/dist/charts/Heatmap/Heatmap.js.map +1 -1
- package/dist/charts/Histogram/Histogram.d.ts +24 -1
- package/dist/charts/Histogram/Histogram.js +43 -11
- package/dist/charts/Histogram/Histogram.js.map +1 -1
- package/dist/charts/LineChart/LineChart.d.ts +9 -2
- package/dist/charts/LineChart/LineChart.js +11 -12
- package/dist/charts/LineChart/LineChart.js.map +1 -1
- package/dist/charts/PieChart/PieChart.d.ts +15 -3
- package/dist/charts/PieChart/PieChart.js +14 -6
- package/dist/charts/PieChart/PieChart.js.map +1 -1
- package/dist/charts/RadarChart/RadarChart.d.ts +15 -3
- package/dist/charts/RadarChart/RadarChart.js +11 -9
- package/dist/charts/RadarChart/RadarChart.js.map +1 -1
- package/dist/charts/RadialChart/RadialChart.d.ts +22 -4
- package/dist/charts/RadialChart/RadialChart.js +28 -6
- package/dist/charts/RadialChart/RadialChart.js.map +1 -1
- package/dist/charts/SankeyChart/SankeyChart.d.ts +16 -4
- package/dist/charts/SankeyChart/SankeyChart.js +14 -6
- package/dist/charts/SankeyChart/SankeyChart.js.map +1 -1
- package/dist/charts/ScatterChart/ScatterChart.d.ts +24 -3
- package/dist/charts/ScatterChart/ScatterChart.js +21 -15
- package/dist/charts/ScatterChart/ScatterChart.js.map +1 -1
- package/dist/charts/Sparkline/Sparkline.js +5 -3
- package/dist/charts/Sparkline/Sparkline.js.map +1 -1
- package/dist/charts/TreemapChart/TreemapChart.d.ts +7 -2
- package/dist/charts/TreemapChart/TreemapChart.js +20 -11
- package/dist/charts/TreemapChart/TreemapChart.js.map +1 -1
- package/dist/charts/WaterfallChart/WaterfallChart.js +4 -2
- package/dist/charts/WaterfallChart/WaterfallChart.js.map +1 -1
- package/dist/charts/lib/export.d.ts +1 -0
- package/dist/charts/lib/figure.d.ts +21 -1
- package/dist/charts/lib/figure.js +5 -2
- package/dist/charts/lib/figure.js.map +1 -1
- package/dist/charts/lib/scale.d.ts +23 -0
- package/dist/charts/lib/scale.js +13 -0
- package/dist/charts/lib/scale.js.map +1 -0
- package/dist/charts/lib/selection.d.ts +20 -0
- package/dist/charts/lib/selection.js +18 -0
- package/dist/charts/lib/selection.js.map +1 -0
- package/dist/charts/lib/toolbar.d.ts +1 -0
- package/dist/components/Accordion/Accordion.js +8 -1
- package/dist/components/Accordion/Accordion.js.map +1 -1
- package/dist/components/AppShell/AppShell.d.ts +6 -0
- package/dist/components/AppShell/AppShell.js +34 -4
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/Article/Article.d.ts +11 -4
- package/dist/components/Article/Article.js +13 -0
- package/dist/components/Article/Article.js.map +1 -1
- package/dist/components/AspectRatio/AspectRatio.d.ts +46 -0
- package/dist/components/AspectRatio/AspectRatio.js +33 -0
- package/dist/components/AspectRatio/AspectRatio.js.map +1 -0
- package/dist/components/Avatar/Avatar.d.ts +12 -3
- package/dist/components/Avatar/Avatar.js +6 -1
- package/dist/components/Avatar/Avatar.js.map +1 -1
- package/dist/components/Breadcrumb/Breadcrumb.d.ts +6 -0
- package/dist/components/Breadcrumb/Breadcrumb.js +15 -0
- package/dist/components/Breadcrumb/Breadcrumb.js.map +1 -1
- package/dist/components/Button/Button.js +14 -0
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.d.ts +29 -3
- package/dist/components/Calendar/Calendar.js +60 -12
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Card/Card.d.ts +6 -0
- package/dist/components/Card/Card.js +1 -1
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts +5 -2
- package/dist/components/Checkbox/Checkbox.js +12 -2
- package/dist/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/components/Code/Code.d.ts +30 -0
- package/dist/components/Code/Code.js +21 -0
- package/dist/components/Code/Code.js.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.d.ts +141 -0
- package/dist/components/CodeBlock/CodeBlock.js +123 -0
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -0
- package/dist/components/Collapsible/Collapsible.js +10 -2
- package/dist/components/Collapsible/Collapsible.js.map +1 -1
- package/dist/components/Combobox/Combobox.d.ts +15 -1
- package/dist/components/Combobox/Combobox.js +25 -5
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.d.ts +4 -2
- package/dist/components/Command/Command.js +13 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.d.ts +30 -5
- package/dist/components/ContextMenu/ContextMenu.js +32 -2
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.d.ts +36 -4
- package/dist/components/DatePicker/DatePicker.js +55 -12
- package/dist/components/DatePicker/DatePicker.js.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.d.ts +73 -0
- package/dist/components/DescriptionList/DescriptionList.js +35 -0
- package/dist/components/DescriptionList/DescriptionList.js.map +1 -0
- package/dist/components/Diagram/Diagram.d.ts +8 -3
- package/dist/components/Diagram/Diagram.js +82 -10
- package/dist/components/Diagram/Diagram.js.map +1 -1
- package/dist/components/Dialog/Dialog.d.ts +18 -3
- package/dist/components/Dialog/Dialog.js +20 -2
- package/dist/components/Dialog/Dialog.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.d.ts +49 -11
- package/dist/components/DropdownMenu/DropdownMenu.js +32 -2
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/EmptyState/EmptyState.d.ts +25 -1
- package/dist/components/EmptyState/EmptyState.js +3 -1
- package/dist/components/EmptyState/EmptyState.js.map +1 -1
- package/dist/components/ErrorState/ErrorState.d.ts +22 -1
- package/dist/components/ErrorState/ErrorState.js +11 -2
- package/dist/components/ErrorState/ErrorState.js.map +1 -1
- package/dist/components/Field/Field.d.ts +70 -3
- package/dist/components/Field/Field.js +53 -12
- package/dist/components/Field/Field.js.map +1 -1
- package/dist/components/Field/field-control.d.ts +31 -0
- package/dist/components/Field/field-control.js +12 -0
- package/dist/components/Field/field-control.js.map +1 -0
- package/dist/components/FloatingIconButton/FloatingIconButton.js +2 -0
- package/dist/components/FloatingIconButton/FloatingIconButton.js.map +1 -1
- package/dist/components/Heading/Heading.d.ts +62 -0
- package/dist/components/Heading/Heading.js +44 -0
- package/dist/components/Heading/Heading.js.map +1 -0
- package/dist/components/Markdown/Markdown.d.ts +139 -0
- package/dist/components/Markdown/Markdown.js +135 -0
- package/dist/components/Markdown/Markdown.js.map +1 -0
- package/dist/components/Markdown/inline.d.ts +29 -0
- package/dist/components/Markdown/inline.js +74 -0
- package/dist/components/Markdown/inline.js.map +1 -0
- package/dist/components/Markdown/nodes.d.ts +65 -0
- package/dist/components/Markdown/nodes.js +1 -0
- package/dist/components/Markdown/nodes.js.map +1 -0
- package/dist/components/Markdown/parse.d.ts +24 -0
- package/dist/components/Markdown/parse.js +110 -0
- package/dist/components/Markdown/parse.js.map +1 -0
- package/dist/components/Markdown/slug.d.ts +15 -0
- package/dist/components/Markdown/slug.js +7 -0
- package/dist/components/Markdown/slug.js.map +1 -0
- package/dist/components/NativeSelect/NativeSelect.d.ts +6 -0
- package/dist/components/NativeSelect/NativeSelect.js +2 -3
- package/dist/components/NativeSelect/NativeSelect.js.map +1 -1
- package/dist/components/Pagination/Pagination.d.ts +24 -1
- package/dist/components/Pagination/Pagination.js +6 -3
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/Progress/Progress.d.ts +17 -5
- package/dist/components/Progress/Progress.js +18 -6
- package/dist/components/Progress/Progress.js.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts +6 -0
- package/dist/components/RadioGroup/RadioGroup.js +11 -1
- package/dist/components/RadioGroup/RadioGroup.js.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.d.ts +15 -0
- package/dist/components/ScrollArea/ScrollArea.js +2 -2
- package/dist/components/ScrollArea/ScrollArea.js.map +1 -1
- package/dist/components/SearchableMenu/SearchableMenu.d.ts +11 -2
- package/dist/components/SearchableMenu/SearchableMenu.js +24 -2
- package/dist/components/SearchableMenu/SearchableMenu.js.map +1 -1
- package/dist/components/Select/Select.d.ts +18 -2
- package/dist/components/Select/Select.js +20 -3
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Separator/Separator.d.ts +21 -3
- package/dist/components/Separator/Separator.js +9 -1
- package/dist/components/Separator/Separator.js.map +1 -1
- package/dist/components/Sheet/Sheet.d.ts +3 -0
- package/dist/components/Sheet/Sheet.js +11 -2
- package/dist/components/Sheet/Sheet.js.map +1 -1
- package/dist/components/Skeleton/Skeleton.d.ts +8 -1
- package/dist/components/Skeleton/Skeleton.js +1 -1
- package/dist/components/Skeleton/Skeleton.js.map +1 -1
- package/dist/components/Slider/Slider.d.ts +21 -3
- package/dist/components/Slider/Slider.js +54 -27
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Spinner/Spinner.d.ts +8 -0
- package/dist/components/Spinner/Spinner.js +3 -2
- package/dist/components/Spinner/Spinner.js.map +1 -1
- package/dist/components/StatusPill/StatusPill.d.ts +5 -0
- package/dist/components/StatusPill/StatusPill.js +6 -0
- package/dist/components/StatusPill/StatusPill.js.map +1 -1
- package/dist/components/Table/Table.d.ts +6 -0
- package/dist/components/Table/Table.js +3 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Tag/Tag.d.ts +68 -9
- package/dist/components/Tag/Tag.js +50 -10
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Text/Text.d.ts +62 -0
- package/dist/components/Text/Text.js +30 -0
- package/dist/components/Text/Text.js.map +1 -0
- package/dist/components/Timestamp/Timestamp.d.ts +59 -0
- package/dist/components/Timestamp/Timestamp.js +56 -0
- package/dist/components/Timestamp/Timestamp.js.map +1 -0
- package/dist/components/Toast/Toast.d.ts +9 -1
- package/dist/components/Toast/Toast.js +27 -4
- package/dist/components/Toast/Toast.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +6 -1
- package/dist/components/ToggleGroup/ToggleGroup.js +11 -1
- package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
- package/dist/components/Toolbar/Toolbar.d.ts +55 -0
- package/dist/components/Toolbar/Toolbar.js +43 -0
- package/dist/components/Toolbar/Toolbar.js.map +1 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.d.ts +6 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js +48 -8
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js.map +1 -1
- package/dist/diagrams/DataflowFigure/DataflowFigure.js +21 -2
- package/dist/diagrams/DataflowFigure/DataflowFigure.js.map +1 -1
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.d.ts +14 -0
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js +14 -7
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js.map +1 -1
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.d.ts +38 -4
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js +14 -16
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js.map +1 -1
- package/dist/diagrams/DiagramInspector/DiagramInspector.js +2 -2
- package/dist/diagrams/DiagramInspector/DiagramInspector.js.map +1 -1
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.d.ts +23 -7
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js +45 -18
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js.map +1 -1
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js +44 -12
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js.map +1 -1
- package/dist/diagrams/SequenceFigure/SequenceFigure.js +35 -5
- package/dist/diagrams/SequenceFigure/SequenceFigure.js.map +1 -1
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.d.ts +3 -1
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js +76 -28
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js.map +1 -1
- package/dist/diagrams/index.d.ts +1 -0
- package/dist/diagrams/index.js +10 -0
- package/dist/diagrams/index.js.map +1 -1
- package/dist/diagrams/lib/dev.d.ts +61 -0
- package/dist/diagrams/lib/dev.js +96 -0
- package/dist/diagrams/lib/dev.js.map +1 -0
- package/dist/diagrams/lib/frame.d.ts +48 -7
- package/dist/diagrams/lib/frame.js +17 -2
- package/dist/diagrams/lib/frame.js.map +1 -1
- package/dist/diagrams/lib/wires.js.map +1 -1
- package/dist/diagrams/spec.d.ts +64 -12
- package/dist/index.d.ts +16 -4
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/keyframes.css +57 -3
- package/dist/lib/overlay-container.d.ts +14 -4
- package/dist/lib/overlay-container.js.map +1 -1
- package/dist/lib/warn.d.ts +73 -0
- package/dist/lib/warn.js +38 -0
- package/dist/lib/warn.js.map +1 -0
- package/dist/semantic.css +17 -6
- package/dist/styles.css +1 -1
- package/dist/tokens.css +20 -2
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +39 -36
- package/dist/tokens.json +39 -36
- package/llms.txt +35 -0
- package/package.json +5 -2
- package/skills/misoto22-design/evals/evals.json +84 -0
- package/skills/misoto22-design/rules/a11y.md +55 -3
- package/skills/misoto22-design/rules/forms.md +5 -0
|
@@ -45,6 +45,12 @@ interface TableProps extends TableHTMLAttributes<HTMLTableElement> {
|
|
|
45
45
|
* element to Tab to and therefore no way to press an arrow key at it, so the
|
|
46
46
|
* columns past the fold simply do not exist for anyone not using a mouse.
|
|
47
47
|
*
|
|
48
|
+
* It is also POSITIONED, which is not decoration. `sr-only` is
|
|
49
|
+
* `position: absolute`, so a visually-hidden label in a cell resolves against
|
|
50
|
+
* the nearest positioned ancestor — and with none between it and the document,
|
|
51
|
+
* it escapes the scroll container and every `overflow-hidden` around it, and
|
|
52
|
+
* widens the page by however far the table happens to be scrolled.
|
|
53
|
+
*
|
|
48
54
|
* No zebra striping at any border setting. In a monochrome system a striped row
|
|
49
55
|
* is a second surface competing with the page ground, and the hairline between
|
|
50
56
|
* rows is already enough to track a line across.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
|
|
3
3
|
import { cn } from "../../lib/cn.js";
|
|
4
|
+
import { warnBlankName } from "../../lib/warn.js";
|
|
4
5
|
const ALIGN = {
|
|
5
6
|
start: "text-start",
|
|
6
7
|
center: "text-center",
|
|
@@ -27,6 +28,7 @@ function Table({
|
|
|
27
28
|
children,
|
|
28
29
|
...rest
|
|
29
30
|
}) {
|
|
31
|
+
warnBlankName("Table", "caption", caption, "the table is announced with no name and a reader arrives in it with no idea what it lists");
|
|
30
32
|
return /* @__PURE__ */ jsx(
|
|
31
33
|
"div",
|
|
32
34
|
{
|
|
@@ -34,7 +36,7 @@ function Table({
|
|
|
34
36
|
role: "region",
|
|
35
37
|
"aria-label": caption,
|
|
36
38
|
"data-table-density": density,
|
|
37
|
-
className: cn("w-full overflow-x-auto scroll-slim", BORDERS[borders]),
|
|
39
|
+
className: cn("relative w-full overflow-x-auto scroll-slim", BORDERS[borders]),
|
|
38
40
|
children: /* @__PURE__ */ jsxs(
|
|
39
41
|
"table",
|
|
40
42
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react'\nimport type { HTMLAttributes, TableHTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\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 * 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 return (\n <div\n tabIndex={0}\n role=\"region\"\n aria-label={caption}\n data-table-density={density}\n className={cn('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: ArrowUp,\n descending: ArrowDown,\n none: ChevronsUpDown,\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 strokeWidth={2}\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\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":"AAmHM,SASE,KATF;AAnHN,SAAS,WAAW,SAAS,sBAAsB;AAEnD,SAAS,UAAU;AAiBnB,MAAM,QAAoC;AAAA,EACxC,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AACP;AAeA,MAAM,OACJ;AACF,MAAM,YAAY;AAClB,MAAM,UACJ;AACF,MAAM,QAAQ;AAEd,MAAM,UAAwC;AAAA,EAC5C,MAAM;AAAA,EACN,MAAM,GAAG,MAAM,SAAS,yBAAyB;AAAA,EACjD,UAAU,GAAG,MAAM,WAAW,KAAK;AAAA,EACnC,iBAAiB,GAAG,MAAM,WAAW,SAAS,KAAK;AAAA,EACnD,MAAM;AACR;AA8CO,SAAS,MAAM;AAAA,EACpB;AAAA,EACA,cAAc;AAAA,EACd,eAAe;AAAA,EACf,UAAU;AAAA,EACV,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAe;AACb,SACE;AAAA,IAAC;AAAA;AAAA,MACC,UAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,sBAAoB;AAAA,MACpB,WAAW,GAAG,sCAAsC,QAAQ,OAAO,CAAC;AAAA,MAEpE;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,gBACE;AAAA,YACF;AAAA,UACF;AAAA,UACC,GAAG;AAAA,UAEJ;AAAA,gCAAC,aAAQ,WAAW,GAAG,cAAc,8CAA8C,SAAS,GACzF,mBACH;AAAA,YACC;AAAA;AAAA;AAAA,MACH;AAAA;AAAA,EACF;AAEJ;AAGO,SAAS,MAAM,EAAE,WAAW,GAAG,KAAK,GAA4C;AACrF,SAAO,oBAAC,WAAM,WAAuB,GAAG,MAAM;AAChD;AAEO,SAAS,MAAM,EAAE,WAAW,GAAG,KAAK,GAA4C;AACrF,SAAO,oBAAC,WAAM,WAAuB,GAAG,MAAM;AAChD;AAEO,SAAS,GAAG,EAAE,WAAW,GAAG,KAAK,GAAwC;AAC9E,SAAO,oBAAC,QAAG,WAAuB,GAAG,MAAM;AAC7C;AAyBA,MAAM,OACJ;AAEF,MAAM,YAAY;AAAA,EAChB,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,MAAM;AACR;AAYO,SAAS,GAAG;AAAA,EACjB,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAY;AACV,QAAM,OAAO,UAAU,aAAa;AAEpC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,aAAW,WAAW,gBAAgB;AAAA,MACtC,WAAW,GAAG,MAAM,+CAA+C,MAAM,KAAK,GAAG,SAAS;AAAA,MACzF,GAAG;AAAA,MAEH,qBACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,WAAW;AAAA,YACT;AAAA,YACA,kBAAkB,SAAS,sBAAsB;AAAA,UACnD;AAAA,UAEC;AAAA;AAAA,YACD;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,aAAa;AAAA,gBACb,eAAW;AAAA,gBACX,WAAW;AAAA,kBACT;AAAA,kBACA,kBAAkB,UAAU;AAAA,gBAC9B;AAAA;AAAA,YACF;AAAA;AAAA;AAAA,MACF,IAEA;AAAA;AAAA,EAEJ;AAEJ;AAOO,SAAS,GAAG,EAAE,QAAQ,SAAS,WAAW,GAAG,KAAK,GAAY;AACnE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,MAAM,oDAAoD,MAAM,KAAK,GAAG,SAAS;AAAA,MAC9F,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,IAAO,gBAAQ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-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: ArrowUp,\n descending: ArrowDown,\n none: ChevronsUpDown,\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 strokeWidth={2}\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\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":"AA2HM,SASE,KATF;AA3HN,SAAS,WAAW,SAAS,sBAAsB;AAEnD,SAAS,UAAU;AACnB,SAAS,qBAAqB;AAiB9B,MAAM,QAAoC;AAAA,EACxC,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AACP;AAeA,MAAM,OACJ;AACF,MAAM,YAAY;AAClB,MAAM,UACJ;AACF,MAAM,QAAQ;AAEd,MAAM,UAAwC;AAAA,EAC5C,MAAM;AAAA,EACN,MAAM,GAAG,MAAM,SAAS,yBAAyB;AAAA,EACjD,UAAU,GAAG,MAAM,WAAW,KAAK;AAAA,EACnC,iBAAiB,GAAG,MAAM,WAAW,SAAS,KAAK;AAAA,EACnD,MAAM;AACR;AAoDO,SAAS,MAAM;AAAA,EACpB;AAAA,EACA,cAAc;AAAA,EACd,eAAe;AAAA,EACf,UAAU;AAAA,EACV,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAe;AACb,gBAAc,SAAS,WAAW,SAAS,2FAA2F;AACtI,SACE;AAAA,IAAC;AAAA;AAAA,MACC,UAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,sBAAoB;AAAA,MACpB,WAAW,GAAG,+CAA+C,QAAQ,OAAO,CAAC;AAAA,MAE7E;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,gBACE;AAAA,YACF;AAAA,UACF;AAAA,UACC,GAAG;AAAA,UAEJ;AAAA,gCAAC,aAAQ,WAAW,GAAG,cAAc,8CAA8C,SAAS,GACzF,mBACH;AAAA,YACC;AAAA;AAAA;AAAA,MACH;AAAA;AAAA,EACF;AAEJ;AAGO,SAAS,MAAM,EAAE,WAAW,GAAG,KAAK,GAA4C;AACrF,SAAO,oBAAC,WAAM,WAAuB,GAAG,MAAM;AAChD;AAEO,SAAS,MAAM,EAAE,WAAW,GAAG,KAAK,GAA4C;AACrF,SAAO,oBAAC,WAAM,WAAuB,GAAG,MAAM;AAChD;AAEO,SAAS,GAAG,EAAE,WAAW,GAAG,KAAK,GAAwC;AAC9E,SAAO,oBAAC,QAAG,WAAuB,GAAG,MAAM;AAC7C;AAyBA,MAAM,OACJ;AAEF,MAAM,YAAY;AAAA,EAChB,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,MAAM;AACR;AAYO,SAAS,GAAG;AAAA,EACjB,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAY;AACV,QAAM,OAAO,UAAU,aAAa;AAEpC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,aAAW,WAAW,gBAAgB;AAAA,MACtC,WAAW,GAAG,MAAM,+CAA+C,MAAM,KAAK,GAAG,SAAS;AAAA,MACzF,GAAG;AAAA,MAEH,qBACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,WAAW;AAAA,YACT;AAAA,YACA,kBAAkB,SAAS,sBAAsB;AAAA,UACnD;AAAA,UAEC;AAAA;AAAA,YACD;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,aAAa;AAAA,gBACb,eAAW;AAAA,gBACX,WAAW;AAAA,kBACT;AAAA,kBACA,kBAAkB,UAAU;AAAA,gBAC9B;AAAA;AAAA,YACF;AAAA;AAAA;AAAA,MACF,IAEA;AAAA;AAAA,EAEJ;AAEJ;AAOO,SAAS,GAAG,EAAE,QAAQ,SAAS,WAAW,GAAG,KAAK,GAAY;AACnE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,MAAM,oDAAoD,MAAM,KAAK,GAAG,SAAS;AAAA,MAC9F,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,IAAO,gBAAQ;","names":[]}
|
|
@@ -1,28 +1,87 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
-
interface
|
|
4
|
+
interface TagBase extends Omit<HTMLAttributes<HTMLElement>, 'onClick'> {
|
|
5
5
|
children: ReactNode;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Renders the pressed/selected look, and declares the chip a toggle.
|
|
8
|
+
*
|
|
9
|
+
* An interactive chip reads `aria-pressed` off this same value, so the accent
|
|
10
|
+
* fill and the state a screen reader hears cannot drift apart. Leave it off
|
|
11
|
+
* for a chip whose `onClick` navigates or opens something: a control that is
|
|
12
|
+
* neither pressed nor unpressed is better announced as neither than as
|
|
13
|
+
* "not pressed".
|
|
14
|
+
*/
|
|
7
15
|
active?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Makes the chip ITSELF the filter control.
|
|
18
|
+
*
|
|
19
|
+
* This prop exists because the alternative did not survive contact with
|
|
20
|
+
* `onRemove`. The advice used to be to wrap the tag in a button at the call
|
|
21
|
+
* site, which is fine on its own and invalid the moment the chip is also
|
|
22
|
+
* removable: the remove control is a real `<button>`, so the wrapper puts a
|
|
23
|
+
* button inside a button — markup the parser splits into siblings, leaving a
|
|
24
|
+
* DOM neither the author nor the accessibility tree expects.
|
|
25
|
+
*
|
|
26
|
+
* With this the wrapper is never written. The chip carries the click, the
|
|
27
|
+
* focus ring and the pressed state on the element that draws them, and a
|
|
28
|
+
* removable filter chip renders its two controls side by side instead of one
|
|
29
|
+
* inside the other.
|
|
30
|
+
*/
|
|
31
|
+
onClick?: () => void;
|
|
8
32
|
}
|
|
9
33
|
/**
|
|
10
|
-
*
|
|
34
|
+
* The removable form: a chip for one thing the reader has already chosen.
|
|
35
|
+
*
|
|
36
|
+
* `removeLabel` is required alongside `onRemove` and not optional with a
|
|
37
|
+
* default, because "Remove" repeated down a row of filters is eight controls a
|
|
38
|
+
* screen reader cannot tell apart. Write the subject into it — "Remove
|
|
39
|
+
* TypeScript filter".
|
|
40
|
+
*/
|
|
41
|
+
interface TagRemovable {
|
|
42
|
+
/** Called when the reader dismisses the chip. */
|
|
43
|
+
onRemove: () => void;
|
|
44
|
+
/** The remove button's accessible name. Name the subject, not the action. */
|
|
45
|
+
removeLabel: string;
|
|
46
|
+
}
|
|
47
|
+
interface TagStatic {
|
|
48
|
+
onRemove?: never;
|
|
49
|
+
removeLabel?: never;
|
|
50
|
+
}
|
|
51
|
+
type TagProps = TagBase & (TagRemovable | TagStatic);
|
|
52
|
+
/**
|
|
53
|
+
* A subject label — a topic, a technology, a filter facet — that can filter
|
|
54
|
+
* with `onClick` and be dismissed with `onRemove`.
|
|
11
55
|
*
|
|
12
56
|
* Distinct from `Badge`, which carries a state or a count. A tag names what
|
|
13
57
|
* something is ABOUT, so several sit together in a row and the reader scans
|
|
14
58
|
* them; a badge is one fact about one record.
|
|
15
59
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
60
|
+
* This is where a `Token` component would have gone. It was not built: a token
|
|
61
|
+
* is a tag with a remove button, and the difference between the two is one
|
|
62
|
+
* prop, not one component. The system already ships three things that look
|
|
63
|
+
* alike — `Badge`, `Tag`, `StatusPill` — and a fourth whose whole distinction
|
|
64
|
+
* is an X on the end would be the one a call site picks by coin toss.
|
|
65
|
+
*
|
|
66
|
+
* Presentational until it is given a handler, and the component owns both
|
|
67
|
+
* interactive cases rather than leaving one to a wrapper at the call site.
|
|
68
|
+
* That is not a convenience: a wrapping `<button>` around a chip that already
|
|
69
|
+
* holds the remove `<button>` is a button inside a button, which no parser
|
|
70
|
+
* keeps and no accessibility tree reports the way it was written. Given both,
|
|
71
|
+
* the chip renders the label and the X as SIBLING buttons — the label takes
|
|
72
|
+
* the leading padding with it, so it is the whole of the chip up to the X
|
|
73
|
+
* rather than the words with dead padding around them.
|
|
20
74
|
*
|
|
21
75
|
* @example
|
|
22
76
|
* <Tag>TypeScript</Tag>
|
|
23
77
|
* @example
|
|
24
|
-
* <
|
|
78
|
+
* <Tag active={on} onClick={toggle}>Rust</Tag>
|
|
79
|
+
* @example
|
|
80
|
+
* <Tag onRemove={() => drop('rust')} removeLabel="Remove Rust filter">Rust</Tag>
|
|
81
|
+
* @example
|
|
82
|
+
* // Both: a filter the reader can toggle and also take out of the row.
|
|
83
|
+
* <Tag active={on} onClick={toggle} onRemove={drop} removeLabel="Remove Rust filter">Rust</Tag>
|
|
25
84
|
*/
|
|
26
|
-
declare function Tag({ children, active, className, ...rest }: TagProps): react.JSX.Element;
|
|
85
|
+
declare function Tag({ children, active, onClick, onRemove, removeLabel, className, ...rest }: TagProps): react.JSX.Element;
|
|
27
86
|
|
|
28
87
|
export { Tag, type TagProps, Tag as default };
|
|
@@ -1,18 +1,58 @@
|
|
|
1
|
-
import { jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { X } from "lucide-react";
|
|
2
3
|
import { cn } from "../../lib/cn.js";
|
|
3
|
-
function Tag({ children, active
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
function Tag({ children, active, onClick, onRemove, removeLabel, className, ...rest }) {
|
|
5
|
+
const split = Boolean(onClick && onRemove);
|
|
6
|
+
const chip = cn(
|
|
7
|
+
"inline-flex items-center rounded-(--radius-sm) font-mono text-xs tracking-wide transition-colors duration-(--duration-fast)",
|
|
8
|
+
active ? "bg-(--accent) text-(--accent-foreground)" : "bg-(--stone) text-(--ink-3-aa)",
|
|
9
|
+
onRemove ? "gap-1 pe-1.5" : "pe-2.5",
|
|
10
|
+
// Handed to the label button in the split form, so the padding is part of
|
|
11
|
+
// the target rather than a ring of dead space around it.
|
|
12
|
+
!split && "py-1 ps-2.5",
|
|
13
|
+
className
|
|
14
|
+
);
|
|
15
|
+
const remove = onRemove ? /* @__PURE__ */ jsx(
|
|
16
|
+
"button",
|
|
6
17
|
{
|
|
18
|
+
type: "button",
|
|
19
|
+
onClick: onRemove,
|
|
20
|
+
"aria-label": removeLabel,
|
|
7
21
|
className: cn(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
// The drawn box is 16px, which is under the 24px minimum a pointer
|
|
23
|
+
// target has to clear (WCAG 2.5.8). The inset pseudo-element takes
|
|
24
|
+
// the hit area out to 24 without changing what is drawn or pushing
|
|
25
|
+
// the chips apart — the 44px floor is for a standalone control, and
|
|
26
|
+
// a chip is 24 tall to begin with.
|
|
27
|
+
"relative inline-flex size-4 shrink-0 items-center justify-center rounded-(--radius-xs)",
|
|
28
|
+
"before:absolute before:inset-[-4px] before:content-['']",
|
|
29
|
+
"opacity-60 transition-opacity duration-(--duration-fast) hover:opacity-100 focus-visible:opacity-100"
|
|
11
30
|
),
|
|
12
|
-
|
|
13
|
-
children
|
|
31
|
+
children: /* @__PURE__ */ jsx(X, { size: 12, strokeWidth: 2.5, "aria-hidden": true })
|
|
14
32
|
}
|
|
15
|
-
);
|
|
33
|
+
) : null;
|
|
34
|
+
if (split) {
|
|
35
|
+
return /* @__PURE__ */ jsxs("span", { className: chip, ...rest, children: [
|
|
36
|
+
/* @__PURE__ */ jsx(
|
|
37
|
+
"button",
|
|
38
|
+
{
|
|
39
|
+
type: "button",
|
|
40
|
+
onClick,
|
|
41
|
+
"aria-pressed": active,
|
|
42
|
+
className: "rounded-(--radius-sm) py-1 ps-2.5 font-mono text-xs tracking-wide",
|
|
43
|
+
children
|
|
44
|
+
}
|
|
45
|
+
),
|
|
46
|
+
remove
|
|
47
|
+
] });
|
|
48
|
+
}
|
|
49
|
+
if (onClick) {
|
|
50
|
+
return /* @__PURE__ */ jsx("button", { type: "button", onClick, "aria-pressed": active, className: chip, ...rest, children });
|
|
51
|
+
}
|
|
52
|
+
return /* @__PURE__ */ jsxs("span", { className: chip, ...rest, children: [
|
|
53
|
+
children,
|
|
54
|
+
remove
|
|
55
|
+
] });
|
|
16
56
|
}
|
|
17
57
|
var Tag_default = Tag;
|
|
18
58
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Tag/Tag.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Tag/Tag.tsx"],"sourcesContent":["import { X } from 'lucide-react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\ninterface TagBase extends Omit<HTMLAttributes<HTMLElement>, 'onClick'> {\n children: ReactNode\n /**\n * Renders the pressed/selected look, and declares the chip a toggle.\n *\n * An interactive chip reads `aria-pressed` off this same value, so the accent\n * fill and the state a screen reader hears cannot drift apart. Leave it off\n * for a chip whose `onClick` navigates or opens something: a control that is\n * neither pressed nor unpressed is better announced as neither than as\n * \"not pressed\".\n */\n active?: boolean\n /**\n * Makes the chip ITSELF the filter control.\n *\n * This prop exists because the alternative did not survive contact with\n * `onRemove`. The advice used to be to wrap the tag in a button at the call\n * site, which is fine on its own and invalid the moment the chip is also\n * removable: the remove control is a real `<button>`, so the wrapper puts a\n * button inside a button — markup the parser splits into siblings, leaving a\n * DOM neither the author nor the accessibility tree expects.\n *\n * With this the wrapper is never written. The chip carries the click, the\n * focus ring and the pressed state on the element that draws them, and a\n * removable filter chip renders its two controls side by side instead of one\n * inside the other.\n */\n onClick?: () => void\n}\n\n/**\n * The removable form: a chip for one thing the reader has already chosen.\n *\n * `removeLabel` is required alongside `onRemove` and not optional with a\n * default, because \"Remove\" repeated down a row of filters is eight controls a\n * screen reader cannot tell apart. Write the subject into it — \"Remove\n * TypeScript filter\".\n */\ninterface TagRemovable {\n /** Called when the reader dismisses the chip. */\n onRemove: () => void\n /** The remove button's accessible name. Name the subject, not the action. */\n removeLabel: string\n}\n\ninterface TagStatic {\n onRemove?: never\n removeLabel?: never\n}\n\nexport type TagProps = TagBase & (TagRemovable | TagStatic)\n\n/**\n * A subject label — a topic, a technology, a filter facet — that can filter\n * with `onClick` and be dismissed with `onRemove`.\n *\n * Distinct from `Badge`, which carries a state or a count. A tag names what\n * something is ABOUT, so several sit together in a row and the reader scans\n * them; a badge is one fact about one record.\n *\n * This is where a `Token` component would have gone. It was not built: a token\n * is a tag with a remove button, and the difference between the two is one\n * prop, not one component. The system already ships three things that look\n * alike — `Badge`, `Tag`, `StatusPill` — and a fourth whose whole distinction\n * is an X on the end would be the one a call site picks by coin toss.\n *\n * Presentational until it is given a handler, and the component owns both\n * interactive cases rather than leaving one to a wrapper at the call site.\n * That is not a convenience: a wrapping `<button>` around a chip that already\n * holds the remove `<button>` is a button inside a button, which no parser\n * keeps and no accessibility tree reports the way it was written. Given both,\n * the chip renders the label and the X as SIBLING buttons — the label takes\n * the leading padding with it, so it is the whole of the chip up to the X\n * rather than the words with dead padding around them.\n *\n * @example\n * <Tag>TypeScript</Tag>\n * @example\n * <Tag active={on} onClick={toggle}>Rust</Tag>\n * @example\n * <Tag onRemove={() => drop('rust')} removeLabel=\"Remove Rust filter\">Rust</Tag>\n * @example\n * // Both: a filter the reader can toggle and also take out of the row.\n * <Tag active={on} onClick={toggle} onRemove={drop} removeLabel=\"Remove Rust filter\">Rust</Tag>\n */\nexport function Tag({ children, active, onClick, onRemove, removeLabel, className, ...rest }: TagProps) {\n // Two controls cannot nest, so a chip that is both a filter and a selection\n // splits its label out into a button of its own beside the remove button.\n const split = Boolean(onClick && onRemove)\n\n const chip = cn(\n 'inline-flex items-center rounded-(--radius-sm) font-mono text-xs tracking-wide transition-colors duration-(--duration-fast)',\n active ? 'bg-(--accent) text-(--accent-foreground)' : 'bg-(--stone) text-(--ink-3-aa)',\n onRemove ? 'gap-1 pe-1.5' : 'pe-2.5',\n // Handed to the label button in the split form, so the padding is part of\n // the target rather than a ring of dead space around it.\n !split && 'py-1 ps-2.5',\n className,\n )\n\n const remove = onRemove ? (\n <button\n type=\"button\"\n onClick={onRemove}\n aria-label={removeLabel}\n className={cn(\n // The drawn box is 16px, which is under the 24px minimum a pointer\n // target has to clear (WCAG 2.5.8). The inset pseudo-element takes\n // the hit area out to 24 without changing what is drawn or pushing\n // the chips apart — the 44px floor is for a standalone control, and\n // a chip is 24 tall to begin with.\n 'relative inline-flex size-4 shrink-0 items-center justify-center rounded-(--radius-xs)',\n \"before:absolute before:inset-[-4px] before:content-['']\",\n 'opacity-60 transition-opacity duration-(--duration-fast) hover:opacity-100 focus-visible:opacity-100',\n )}\n >\n <X size={12} strokeWidth={2.5} aria-hidden />\n </button>\n ) : null\n\n if (split) {\n return (\n <span className={chip} {...rest}>\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className=\"rounded-(--radius-sm) py-1 ps-2.5 font-mono text-xs tracking-wide\"\n >\n {children}\n </button>\n {remove}\n </span>\n )\n }\n\n if (onClick) {\n return (\n <button type=\"button\" onClick={onClick} aria-pressed={active} className={chip} {...rest}>\n {children}\n </button>\n )\n }\n\n return (\n <span className={chip} {...rest}>\n {children}\n {remove}\n </span>\n )\n}\n\nexport default Tag\n"],"mappings":"AAwHM,cAMA,YANA;AAxHN,SAAS,SAAS;AAElB,SAAS,UAAU;AAuFZ,SAAS,IAAI,EAAE,UAAU,QAAQ,SAAS,UAAU,aAAa,WAAW,GAAG,KAAK,GAAa;AAGtG,QAAM,QAAQ,QAAQ,WAAW,QAAQ;AAEzC,QAAM,OAAO;AAAA,IACX;AAAA,IACA,SAAS,6CAA6C;AAAA,IACtD,WAAW,iBAAiB;AAAA;AAAA;AAAA,IAG5B,CAAC,SAAS;AAAA,IACV;AAAA,EACF;AAEA,QAAM,SAAS,WACb;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,cAAY;AAAA,MACZ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEA,8BAAC,KAAE,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC;AAAA;AAAA,EAC7C,IACE;AAEJ,MAAI,OAAO;AACT,WACE,qBAAC,UAAK,WAAW,MAAO,GAAG,MACzB;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL;AAAA,UACA,gBAAc;AAAA,UACd,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MACC;AAAA,OACH;AAAA,EAEJ;AAEA,MAAI,SAAS;AACX,WACE,oBAAC,YAAO,MAAK,UAAS,SAAkB,gBAAc,QAAQ,WAAW,MAAO,GAAG,MAChF,UACH;AAAA,EAEJ;AAEA,SACE,qBAAC,UAAK,WAAW,MAAO,GAAG,MACxB;AAAA;AAAA,IACA;AAAA,KACH;AAEJ;AAEA,IAAO,cAAQ;","names":[]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A rung of the type scale, named for what it is FOR rather than for a pixel
|
|
6
|
+
* value. `base` is body copy; `lead` is the standfirst that carries a piece and
|
|
7
|
+
* is the only step here that touches the heading ladder (`--fs-item`, its
|
|
8
|
+
* bottom rung). Nothing above that: a paragraph larger than a record title is a
|
|
9
|
+
* heading that forgot to be one.
|
|
10
|
+
*/
|
|
11
|
+
type TextSize = 'xs' | 'sm' | 'base' | 'lead';
|
|
12
|
+
/**
|
|
13
|
+
* A rung of the ink ladder. Three, because the ladder has three.
|
|
14
|
+
*
|
|
15
|
+
* `muted` is `--ink-3-aa` and never `--ink-3`. The two look identical on paper
|
|
16
|
+
* and are not the same token: `--ink-3` is a translucent tint that inherits
|
|
17
|
+
* whatever is under it, so it clears AA on the page ground and quietly fails on
|
|
18
|
+
* a card, a plate, or a code block.
|
|
19
|
+
*/
|
|
20
|
+
type TextTone = 'body' | 'strong' | 'muted';
|
|
21
|
+
type TextElement = 'p' | 'span' | 'div' | 'li' | 'figcaption';
|
|
22
|
+
interface TextProps extends HTMLAttributes<HTMLElement> {
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
/** Which rung of the type scale. See {@link TextSize}. */
|
|
25
|
+
size?: TextSize;
|
|
26
|
+
/** Which rung of the ink ladder. See {@link TextTone}. */
|
|
27
|
+
tone?: TextTone;
|
|
28
|
+
/**
|
|
29
|
+
* The element, and ONLY the element.
|
|
30
|
+
*
|
|
31
|
+
* Changing it changes what the markup means — a `span` inside a sentence, an
|
|
32
|
+
* `li` inside a list someone else opened — and changes nothing about the
|
|
33
|
+
* look. That separation is the whole point: the alternative is a `<p>` nested
|
|
34
|
+
* inside a `<p>`, which the HTML parser silently splits into two, or a
|
|
35
|
+
* paragraph faked out of a `<div>` because the real element brought a size
|
|
36
|
+
* with it.
|
|
37
|
+
*/
|
|
38
|
+
as?: TextElement;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A paragraph, or a run of text that wants the system's voice.
|
|
42
|
+
*
|
|
43
|
+
* The step between `Article` and raw JSX. `Article` styles a whole reading
|
|
44
|
+
* column from element selectors and is the right answer for a post; this is for
|
|
45
|
+
* the single paragraph that is NOT in a column — a card's description, a
|
|
46
|
+
* dialog's explanation, the line under an empty state.
|
|
47
|
+
*
|
|
48
|
+
* The default tone is `body` (`--ink-2`), not `--ink`. A page whose paragraphs
|
|
49
|
+
* are all full-strength ink has spent the top of the ladder on its body copy
|
|
50
|
+
* and has nothing left for the headings, which is the single most common way a
|
|
51
|
+
* monochrome page loses its hierarchy.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* <Text>Twelve releases this quarter, none rolled back.</Text>
|
|
55
|
+
* @example
|
|
56
|
+
* <Text size="lead" tone="strong">A monochrome system for software and writing.</Text>
|
|
57
|
+
* @example
|
|
58
|
+
* <Text as="span" size="sm" tone="muted">Updated just now</Text>
|
|
59
|
+
*/
|
|
60
|
+
declare function Text({ children, size, tone, as, className, ...rest }: TextProps): react.JSX.Element;
|
|
61
|
+
|
|
62
|
+
export { Text, type TextElement, type TextProps, type TextSize, type TextTone, Text as default };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../../lib/cn.js";
|
|
3
|
+
const SIZE = {
|
|
4
|
+
xs: "text-xs leading-[1.6]",
|
|
5
|
+
sm: "text-sm leading-[1.65]",
|
|
6
|
+
base: "text-base font-light leading-[1.75]",
|
|
7
|
+
lead: "text-[length:var(--fs-item)] font-light leading-[1.6]"
|
|
8
|
+
};
|
|
9
|
+
const TONE = {
|
|
10
|
+
body: "text-(--ink-2)",
|
|
11
|
+
strong: "text-(--ink)",
|
|
12
|
+
muted: "text-(--ink-3-aa)"
|
|
13
|
+
};
|
|
14
|
+
function Text({
|
|
15
|
+
children,
|
|
16
|
+
size = "base",
|
|
17
|
+
tone = "body",
|
|
18
|
+
as = "p",
|
|
19
|
+
className,
|
|
20
|
+
...rest
|
|
21
|
+
}) {
|
|
22
|
+
const Comp = as;
|
|
23
|
+
return /* @__PURE__ */ jsx(Comp, { className: cn("m-0", SIZE[size], TONE[tone], className), ...rest, children });
|
|
24
|
+
}
|
|
25
|
+
var Text_default = Text;
|
|
26
|
+
export {
|
|
27
|
+
Text,
|
|
28
|
+
Text_default as default
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=Text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Text/Text.tsx"],"sourcesContent":["import type { ElementType, HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\n/**\n * A rung of the type scale, named for what it is FOR rather than for a pixel\n * value. `base` is body copy; `lead` is the standfirst that carries a piece and\n * is the only step here that touches the heading ladder (`--fs-item`, its\n * bottom rung). Nothing above that: a paragraph larger than a record title is a\n * heading that forgot to be one.\n */\nexport type TextSize = 'xs' | 'sm' | 'base' | 'lead'\n\n/**\n * A rung of the ink ladder. Three, because the ladder has three.\n *\n * `muted` is `--ink-3-aa` and never `--ink-3`. The two look identical on paper\n * and are not the same token: `--ink-3` is a translucent tint that inherits\n * whatever is under it, so it clears AA on the page ground and quietly fails on\n * a card, a plate, or a code block.\n */\nexport type TextTone = 'body' | 'strong' | 'muted'\n\nexport type TextElement = 'p' | 'span' | 'div' | 'li' | 'figcaption'\n\n/**\n * Weight travels with the size, deliberately.\n *\n * The system sets long-form copy at 300 — it is what gives a page of prose its\n * air. At 12 and 14px that same weight is a hairline: the strokes thin out\n * below the point where a light face is a choice and turn into a legibility\n * bug. So the two small steps step back up to 400, and a caller never has to\n * know that the trade exists.\n */\nconst SIZE: Record<TextSize, string> = {\n xs: 'text-xs leading-[1.6]',\n sm: 'text-sm leading-[1.65]',\n base: 'text-base font-light leading-[1.75]',\n lead: 'text-[length:var(--fs-item)] font-light leading-[1.6]',\n}\n\nconst TONE: Record<TextTone, string> = {\n body: 'text-(--ink-2)',\n strong: 'text-(--ink)',\n muted: 'text-(--ink-3-aa)',\n}\n\nexport interface TextProps extends HTMLAttributes<HTMLElement> {\n children?: ReactNode\n /** Which rung of the type scale. See {@link TextSize}. */\n size?: TextSize\n /** Which rung of the ink ladder. See {@link TextTone}. */\n tone?: TextTone\n /**\n * The element, and ONLY the element.\n *\n * Changing it changes what the markup means — a `span` inside a sentence, an\n * `li` inside a list someone else opened — and changes nothing about the\n * look. That separation is the whole point: the alternative is a `<p>` nested\n * inside a `<p>`, which the HTML parser silently splits into two, or a\n * paragraph faked out of a `<div>` because the real element brought a size\n * with it.\n */\n as?: TextElement\n}\n\n/**\n * A paragraph, or a run of text that wants the system's voice.\n *\n * The step between `Article` and raw JSX. `Article` styles a whole reading\n * column from element selectors and is the right answer for a post; this is for\n * the single paragraph that is NOT in a column — a card's description, a\n * dialog's explanation, the line under an empty state.\n *\n * The default tone is `body` (`--ink-2`), not `--ink`. A page whose paragraphs\n * are all full-strength ink has spent the top of the ladder on its body copy\n * and has nothing left for the headings, which is the single most common way a\n * monochrome page loses its hierarchy.\n *\n * @example\n * <Text>Twelve releases this quarter, none rolled back.</Text>\n * @example\n * <Text size=\"lead\" tone=\"strong\">A monochrome system for software and writing.</Text>\n * @example\n * <Text as=\"span\" size=\"sm\" tone=\"muted\">Updated just now</Text>\n */\nexport function Text({\n children,\n size = 'base',\n tone = 'body',\n as = 'p',\n className,\n ...rest\n}: TextProps) {\n const Comp = as as ElementType\n\n return (\n <Comp className={cn('m-0', SIZE[size], TONE[tone], className)} {...rest}>\n {children}\n </Comp>\n )\n}\n\nexport default Text\n"],"mappings":"AAgGI;AA/FJ,SAAS,UAAU;AAgCnB,MAAM,OAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,MAAM;AACR;AAEA,MAAM,OAAiC;AAAA,EACrC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AACT;AAyCO,SAAS,KAAK;AAAA,EACnB;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP,KAAK;AAAA,EACL;AAAA,EACA,GAAG;AACL,GAAc;AACZ,QAAM,OAAO;AAEb,SACE,oBAAC,QAAK,WAAW,GAAG,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,GAAI,GAAG,MAChE,UACH;AAEJ;AAEA,IAAO,eAAQ;","names":[]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { TimeHTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How the instant reads.
|
|
6
|
+
*
|
|
7
|
+
* `auto` is relative while the gap is small enough to be useful and absolute
|
|
8
|
+
* once it is not — "3 hours ago" answers the question a reader is actually
|
|
9
|
+
* asking about a recent record, and "2 years ago" does not.
|
|
10
|
+
*/
|
|
11
|
+
type TimestampFormat = 'auto' | 'relative' | 'absolute';
|
|
12
|
+
interface TimestampProps extends Omit<TimeHTMLAttributes<HTMLTimeElement>, 'dateTime' | 'children'> {
|
|
13
|
+
/** The instant. A `Date`, an ISO string, or epoch milliseconds. */
|
|
14
|
+
value: Date | string | number;
|
|
15
|
+
/** How the instant reads. See {@link TimestampFormat}. */
|
|
16
|
+
format?: TimestampFormat;
|
|
17
|
+
/**
|
|
18
|
+
* How far from now `auto` still prints a relative time, in milliseconds.
|
|
19
|
+
* Seven days by default — past a week the calendar date is the more useful
|
|
20
|
+
* fact, and it is also the one that stops changing.
|
|
21
|
+
*/
|
|
22
|
+
relativeWithin?: number;
|
|
23
|
+
/** Adds the clock time to the absolute form. */
|
|
24
|
+
showTime?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A date or a time, rendered the one way the system renders them.
|
|
28
|
+
*
|
|
29
|
+
* Every list of records needs this, and `new Date().toLocaleString()` at the
|
|
30
|
+
* call site is precisely how a product ends up with four date formats on one
|
|
31
|
+
* screen.
|
|
32
|
+
*
|
|
33
|
+
* **On hydration.** Both halves of a formatted date are environment-dependent:
|
|
34
|
+
* a relative time depends on when it is read, and even an absolute one depends
|
|
35
|
+
* on the reader's locale and time zone, none of which a static build knows. So
|
|
36
|
+
* the first paint — the one the server produces and the one the client must
|
|
37
|
+
* reproduce exactly — is the ISO calendar date in UTC, sliced straight out of
|
|
38
|
+
* the ISO string with no `Intl` anywhere near it. Both sides compute it from
|
|
39
|
+
* the same characters, so they cannot disagree. The locale-aware and relative
|
|
40
|
+
* forms are applied after mount, in an effect, where there is a reader to be
|
|
41
|
+
* local to. This package statically exports every page, so the alternative is a
|
|
42
|
+
* hydration mismatch on any page with a date on it.
|
|
43
|
+
*
|
|
44
|
+
* The `datetime` attribute is the full ISO instant from the first render
|
|
45
|
+
* onwards and never changes, so a screen reader, a crawler, or anything else
|
|
46
|
+
* parsing the markup gets the exact moment whether or not the effect has run.
|
|
47
|
+
*
|
|
48
|
+
* It formats once per mount. A hundred rows each holding a ticking interval to
|
|
49
|
+
* keep "3 minutes ago" honest is a cost nobody asked for; a list that must tick
|
|
50
|
+
* should re-key or re-render from above.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* <Timestamp value={deploy.finishedAt} />
|
|
54
|
+
* @example
|
|
55
|
+
* <Timestamp value="2026-01-14T09:30:00Z" format="absolute" showTime />
|
|
56
|
+
*/
|
|
57
|
+
declare function Timestamp({ value, format, relativeWithin, showTime, className, ...rest }: TimestampProps): react.JSX.Element;
|
|
58
|
+
|
|
59
|
+
export { Timestamp, type TimestampFormat, type TimestampProps, Timestamp as default };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { cn } from "../../lib/cn.js";
|
|
5
|
+
const UNITS = [
|
|
6
|
+
["year", 31556952e3],
|
|
7
|
+
["month", 2629746e3],
|
|
8
|
+
["week", 6048e5],
|
|
9
|
+
["day", 864e5],
|
|
10
|
+
["hour", 36e5],
|
|
11
|
+
["minute", 6e4]
|
|
12
|
+
];
|
|
13
|
+
function relative(deltaMs) {
|
|
14
|
+
const format = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
|
|
15
|
+
const gap = Math.abs(deltaMs);
|
|
16
|
+
for (const [unit, size] of UNITS) {
|
|
17
|
+
if (gap >= size) return format.format(Math.trunc(deltaMs / size), unit);
|
|
18
|
+
}
|
|
19
|
+
return format.format(0, "second");
|
|
20
|
+
}
|
|
21
|
+
function absolute(date, showTime) {
|
|
22
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
23
|
+
dateStyle: "medium",
|
|
24
|
+
...showTime ? { timeStyle: "short" } : {}
|
|
25
|
+
}).format(date);
|
|
26
|
+
}
|
|
27
|
+
function Timestamp({
|
|
28
|
+
value,
|
|
29
|
+
format = "auto",
|
|
30
|
+
relativeWithin = 6048e5,
|
|
31
|
+
showTime = false,
|
|
32
|
+
className,
|
|
33
|
+
...rest
|
|
34
|
+
}) {
|
|
35
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
36
|
+
const time = date.getTime();
|
|
37
|
+
const valid = !Number.isNaN(time);
|
|
38
|
+
const [local, setLocal] = useState(false);
|
|
39
|
+
useEffect(() => setLocal(true), []);
|
|
40
|
+
if (!valid) {
|
|
41
|
+
return /* @__PURE__ */ jsx("span", { className: cn("text-(--ink-3-aa)", className), ...rest, children: "\u2014" });
|
|
42
|
+
}
|
|
43
|
+
const iso = date.toISOString();
|
|
44
|
+
return /* @__PURE__ */ jsx("time", { dateTime: iso, className: cn("tabular-nums", className), ...rest, children: local ? read(date, format, relativeWithin, showTime) : iso.slice(0, 10) });
|
|
45
|
+
}
|
|
46
|
+
function read(date, format, relativeWithin, showTime) {
|
|
47
|
+
const delta = date.getTime() - Date.now();
|
|
48
|
+
const asRelative = format === "relative" || format === "auto" && Math.abs(delta) < relativeWithin;
|
|
49
|
+
return asRelative ? relative(delta) : absolute(date, showTime);
|
|
50
|
+
}
|
|
51
|
+
var Timestamp_default = Timestamp;
|
|
52
|
+
export {
|
|
53
|
+
Timestamp,
|
|
54
|
+
Timestamp_default as default
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=Timestamp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Timestamp/Timestamp.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useState } from 'react'\nimport type { TimeHTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\n\n/**\n * How the instant reads.\n *\n * `auto` is relative while the gap is small enough to be useful and absolute\n * once it is not — \"3 hours ago\" answers the question a reader is actually\n * asking about a recent record, and \"2 years ago\" does not.\n */\nexport type TimestampFormat = 'auto' | 'relative' | 'absolute'\n\nexport interface TimestampProps\n extends Omit<TimeHTMLAttributes<HTMLTimeElement>, 'dateTime' | 'children'> {\n /** The instant. A `Date`, an ISO string, or epoch milliseconds. */\n value: Date | string | number\n /** How the instant reads. See {@link TimestampFormat}. */\n format?: TimestampFormat\n /**\n * How far from now `auto` still prints a relative time, in milliseconds.\n * Seven days by default — past a week the calendar date is the more useful\n * fact, and it is also the one that stops changing.\n */\n relativeWithin?: number\n /** Adds the clock time to the absolute form. */\n showTime?: boolean\n}\n\n/**\n * The units, largest first, with the milliseconds in one of each.\n *\n * The month and year lengths are the average ones — 30.44 and 365.24 days —\n * because a relative time is a rounded statement about a gap, not a calendar\n * calculation, and \"2 months ago\" must not become \"1 month ago\" because\n * February was short.\n */\nconst UNITS = [\n ['year', 31_556_952_000],\n ['month', 2_629_746_000],\n ['week', 604_800_000],\n ['day', 86_400_000],\n ['hour', 3_600_000],\n ['minute', 60_000],\n] as const\n\n/**\n * The gap in words, in the reader's own language.\n *\n * `Intl.RelativeTimeFormat` rather than a table of English strings: it is in\n * every runtime this package targets, costs nothing to ship, and `numeric:\n * 'auto'` is what turns \"1 day ago\" into \"yesterday\".\n *\n * Truncated, not rounded — ninety seconds is \"1 minute ago\", because a reader\n * who has just watched something happen is not helped by being told it was two\n * minutes. Anything under a minute is \"now\": the seconds are noise, and they\n * are also the only part that would be wrong a moment later.\n */\nfunction relative(deltaMs: number): string {\n const format = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })\n const gap = Math.abs(deltaMs)\n\n for (const [unit, size] of UNITS) {\n if (gap >= size) return format.format(Math.trunc(deltaMs / size), unit)\n }\n return format.format(0, 'second')\n}\n\n/** The calendar date, in the reader's own locale and time zone. */\nfunction absolute(date: Date, showTime: boolean): string {\n return new Intl.DateTimeFormat(undefined, {\n dateStyle: 'medium',\n ...(showTime ? { timeStyle: 'short' as const } : {}),\n }).format(date)\n}\n\n/**\n * A date or a time, rendered the one way the system renders them.\n *\n * Every list of records needs this, and `new Date().toLocaleString()` at the\n * call site is precisely how a product ends up with four date formats on one\n * screen.\n *\n * **On hydration.** Both halves of a formatted date are environment-dependent:\n * a relative time depends on when it is read, and even an absolute one depends\n * on the reader's locale and time zone, none of which a static build knows. So\n * the first paint — the one the server produces and the one the client must\n * reproduce exactly — is the ISO calendar date in UTC, sliced straight out of\n * the ISO string with no `Intl` anywhere near it. Both sides compute it from\n * the same characters, so they cannot disagree. The locale-aware and relative\n * forms are applied after mount, in an effect, where there is a reader to be\n * local to. This package statically exports every page, so the alternative is a\n * hydration mismatch on any page with a date on it.\n *\n * The `datetime` attribute is the full ISO instant from the first render\n * onwards and never changes, so a screen reader, a crawler, or anything else\n * parsing the markup gets the exact moment whether or not the effect has run.\n *\n * It formats once per mount. A hundred rows each holding a ticking interval to\n * keep \"3 minutes ago\" honest is a cost nobody asked for; a list that must tick\n * should re-key or re-render from above.\n *\n * @example\n * <Timestamp value={deploy.finishedAt} />\n * @example\n * <Timestamp value=\"2026-01-14T09:30:00Z\" format=\"absolute\" showTime />\n */\nexport function Timestamp({\n value,\n format = 'auto',\n relativeWithin = 604_800_000,\n showTime = false,\n className,\n ...rest\n}: TimestampProps) {\n const date = value instanceof Date ? value : new Date(value)\n const time = date.getTime()\n const valid = !Number.isNaN(time)\n\n // False through the server render and the hydrating render, true from the\n // first effect on. That is the whole hydration contract in one flag.\n const [local, setLocal] = useState(false)\n useEffect(() => setLocal(true), [])\n\n if (!valid) {\n // Not \"Invalid Date\" — that is the browser's internal string and an\n // engineering artefact in front of a reader. A value nothing can parse is a\n // missing value, and a record UI already has a glyph for one. There is no\n // <time> here either: an element whose datetime cannot be written is not a\n // time.\n return (\n <span className={cn('text-(--ink-3-aa)', className)} {...rest}>\n —\n </span>\n )\n }\n\n const iso = date.toISOString()\n\n return (\n <time dateTime={iso} className={cn('tabular-nums', className)} {...rest}>\n {local ? read(date, format, relativeWithin, showTime) : iso.slice(0, 10)}\n </time>\n )\n}\n\n/**\n * The text a reader who has a clock and a locale gets. Only ever called after\n * mount — everything in here reads `Date.now()` or the ambient locale, which is\n * exactly what the server cannot know.\n */\nfunction read(\n date: Date,\n format: TimestampFormat,\n relativeWithin: number,\n showTime: boolean,\n): string {\n const delta = date.getTime() - Date.now()\n const asRelative = format === 'relative' || (format === 'auto' && Math.abs(delta) < relativeWithin)\n return asRelative ? relative(delta) : absolute(date, showTime)\n}\n\nexport default Timestamp\n"],"mappings":";AAqIM;AAnIN,SAAS,WAAW,gBAAgB;AAEpC,SAAS,UAAU;AAmCnB,MAAM,QAAQ;AAAA,EACZ,CAAC,QAAQ,UAAc;AAAA,EACvB,CAAC,SAAS,SAAa;AAAA,EACvB,CAAC,QAAQ,MAAW;AAAA,EACpB,CAAC,OAAO,KAAU;AAAA,EAClB,CAAC,QAAQ,IAAS;AAAA,EAClB,CAAC,UAAU,GAAM;AACnB;AAcA,SAAS,SAAS,SAAyB;AACzC,QAAM,SAAS,IAAI,KAAK,mBAAmB,QAAW,EAAE,SAAS,OAAO,CAAC;AACzE,QAAM,MAAM,KAAK,IAAI,OAAO;AAE5B,aAAW,CAAC,MAAM,IAAI,KAAK,OAAO;AAChC,QAAI,OAAO,KAAM,QAAO,OAAO,OAAO,KAAK,MAAM,UAAU,IAAI,GAAG,IAAI;AAAA,EACxE;AACA,SAAO,OAAO,OAAO,GAAG,QAAQ;AAClC;AAGA,SAAS,SAAS,MAAY,UAA2B;AACvD,SAAO,IAAI,KAAK,eAAe,QAAW;AAAA,IACxC,WAAW;AAAA,IACX,GAAI,WAAW,EAAE,WAAW,QAAiB,IAAI,CAAC;AAAA,EACpD,CAAC,EAAE,OAAO,IAAI;AAChB;AAiCO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA,SAAS;AAAA,EACT,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,GAAmB;AACjB,QAAM,OAAO,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;AAC3D,QAAM,OAAO,KAAK,QAAQ;AAC1B,QAAM,QAAQ,CAAC,OAAO,MAAM,IAAI;AAIhC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AACxC,YAAU,MAAM,SAAS,IAAI,GAAG,CAAC,CAAC;AAElC,MAAI,CAAC,OAAO;AAMV,WACE,oBAAC,UAAK,WAAW,GAAG,qBAAqB,SAAS,GAAI,GAAG,MAAM,oBAE/D;AAAA,EAEJ;AAEA,QAAM,MAAM,KAAK,YAAY;AAE7B,SACE,oBAAC,UAAK,UAAU,KAAK,WAAW,GAAG,gBAAgB,SAAS,GAAI,GAAG,MAChE,kBAAQ,KAAK,MAAM,QAAQ,gBAAgB,QAAQ,IAAI,IAAI,MAAM,GAAG,EAAE,GACzE;AAEJ;AAOA,SAAS,KACP,MACA,QACA,gBACA,UACQ;AACR,QAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,IAAI;AACxC,QAAM,aAAa,WAAW,cAAe,WAAW,UAAU,KAAK,IAAI,KAAK,IAAI;AACpF,SAAO,aAAa,SAAS,KAAK,IAAI,SAAS,MAAM,QAAQ;AAC/D;AAEA,IAAO,oBAAQ;","names":[]}
|
|
@@ -9,6 +9,14 @@ export { toast } from 'sonner';
|
|
|
9
9
|
* reader must act on belongs in the page — a toast is dismissed by time, and
|
|
10
10
|
* time is not an acknowledgement.
|
|
11
11
|
*
|
|
12
|
+
* `theme` follows `data-mode`. sonner defaults to `light` and hard-codes the
|
|
13
|
+
* description's colour per theme — `#3f3f3f`, overridden only under
|
|
14
|
+
* `[data-sonner-theme=dark]` — so a Toaster that never passed `theme` put dark
|
|
15
|
+
* grey text on this system's `--paper: #0d0d0d` and every
|
|
16
|
+
* `toast(title, { description })` lost its second half. Pass `theme` yourself
|
|
17
|
+
* and that wins; sonner's own `system` is the one value not to reach for, since
|
|
18
|
+
* it reads the OS rather than the attribute the page is actually painted from.
|
|
19
|
+
*
|
|
12
20
|
* `richColors` stays off by default: sonner's rich palette is not this system's
|
|
13
21
|
* status scale, and turning it on reintroduces chroma the White Reset spends
|
|
14
22
|
* only on state.
|
|
@@ -19,6 +27,6 @@ export { toast } from 'sonner';
|
|
|
19
27
|
* // anywhere
|
|
20
28
|
* toast.success('Saved')
|
|
21
29
|
*/
|
|
22
|
-
declare function Toaster({ position, closeButton, richColors, style, ...rest }: ToasterProps): react.JSX.Element;
|
|
30
|
+
declare function Toaster({ position, closeButton, richColors, theme, style, ...rest }: ToasterProps): react.JSX.Element;
|
|
23
31
|
|
|
24
32
|
export { Toaster, Toaster as default };
|
|
@@ -1,33 +1,56 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
3
4
|
import { Toaster as SonnerToaster, toast } from "sonner";
|
|
4
5
|
const TOKEN_STYLE = {
|
|
5
6
|
"--normal-bg": "var(--paper)",
|
|
6
7
|
"--normal-text": "var(--ink)",
|
|
7
8
|
"--normal-border": "var(--rule-2)",
|
|
9
|
+
"--border-radius": "var(--radius)",
|
|
10
|
+
fontFamily: "var(--sans)"
|
|
11
|
+
};
|
|
12
|
+
const RICH_STYLE = {
|
|
8
13
|
"--success-bg": "var(--paper)",
|
|
9
14
|
"--success-text": "var(--ok)",
|
|
10
15
|
"--success-border": "var(--rule-2)",
|
|
11
16
|
"--error-bg": "var(--paper)",
|
|
12
17
|
"--error-text": "var(--danger)",
|
|
13
|
-
"--error-border": "var(--rule-2)"
|
|
14
|
-
"--border-radius": "var(--radius)",
|
|
15
|
-
fontFamily: "var(--sans)"
|
|
18
|
+
"--error-border": "var(--rule-2)"
|
|
16
19
|
};
|
|
20
|
+
function readMode() {
|
|
21
|
+
if (typeof document === "undefined") return "light";
|
|
22
|
+
return document.documentElement.dataset.mode === "dark" ? "dark" : "light";
|
|
23
|
+
}
|
|
24
|
+
function readModeOnServer() {
|
|
25
|
+
return "light";
|
|
26
|
+
}
|
|
27
|
+
function subscribeToMode(onChange) {
|
|
28
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {
|
|
29
|
+
};
|
|
30
|
+
const observer = new MutationObserver(onChange);
|
|
31
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-mode"] });
|
|
32
|
+
return () => observer.disconnect();
|
|
33
|
+
}
|
|
34
|
+
function useMode() {
|
|
35
|
+
return useSyncExternalStore(subscribeToMode, readMode, readModeOnServer);
|
|
36
|
+
}
|
|
17
37
|
function Toaster({
|
|
18
38
|
position = "bottom-right",
|
|
19
39
|
closeButton = true,
|
|
20
40
|
richColors = false,
|
|
41
|
+
theme,
|
|
21
42
|
style,
|
|
22
43
|
...rest
|
|
23
44
|
}) {
|
|
45
|
+
const mode = useMode();
|
|
24
46
|
return /* @__PURE__ */ jsx(
|
|
25
47
|
SonnerToaster,
|
|
26
48
|
{
|
|
27
49
|
position,
|
|
28
50
|
closeButton,
|
|
29
51
|
richColors,
|
|
30
|
-
|
|
52
|
+
theme: theme ?? mode,
|
|
53
|
+
style: { ...TOKEN_STYLE, ...richColors ? RICH_STYLE : null, ...style },
|
|
31
54
|
...rest
|
|
32
55
|
}
|
|
33
56
|
);
|