@misoto22/design 0.6.1 → 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 +62 -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 +71 -0
- 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 +48 -0
- 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 +70 -0
- package/dist/agent/DiagramExportMenu.md +64 -0
- package/dist/agent/DiagramInspector.md +72 -0
- package/dist/agent/DiagramLegend.md +55 -0
- package/dist/agent/DiagramMinimap.md +68 -0
- package/dist/agent/DiagramToolbar.md +69 -0
- 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 +49 -0
- 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 +50 -0
- 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 +50 -0
- package/dist/agent/catalog.json +6086 -1009
- package/dist/agent/index.md +27 -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/index.d.ts +2 -1
- package/dist/charts/lib/export.d.ts +34 -43
- package/dist/charts/lib/export.js +16 -158
- package/dist/charts/lib/export.js.map +1 -1
- 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 +55 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js +175 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js.map +1 -0
- package/dist/diagrams/DataflowFigure/DataflowFigure.d.ts +28 -0
- package/dist/diagrams/DataflowFigure/DataflowFigure.js +148 -0
- package/dist/diagrams/DataflowFigure/DataflowFigure.js.map +1 -0
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.d.ts +78 -0
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js +229 -0
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js.map +1 -0
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.d.ts +106 -0
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js +123 -0
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js.map +1 -0
- package/dist/diagrams/DiagramInspector/DiagramInspector.d.ts +70 -0
- package/dist/diagrams/DiagramInspector/DiagramInspector.js +94 -0
- package/dist/diagrams/DiagramInspector/DiagramInspector.js.map +1 -0
- package/dist/diagrams/DiagramLegend/DiagramLegend.d.ts +42 -0
- package/dist/diagrams/DiagramLegend/DiagramLegend.js +35 -0
- package/dist/diagrams/DiagramLegend/DiagramLegend.js.map +1 -0
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.d.ts +77 -0
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js +106 -0
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js.map +1 -0
- package/dist/diagrams/DiagramToolbar/DiagramToolbar.d.ts +48 -0
- package/dist/diagrams/DiagramToolbar/DiagramToolbar.js +53 -0
- package/dist/diagrams/DiagramToolbar/DiagramToolbar.js.map +1 -0
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.d.ts +31 -0
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js +234 -0
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js.map +1 -0
- package/dist/diagrams/SequenceFigure/SequenceFigure.d.ts +38 -0
- package/dist/diagrams/SequenceFigure/SequenceFigure.js +261 -0
- package/dist/diagrams/SequenceFigure/SequenceFigure.js.map +1 -0
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.d.ts +36 -0
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js +259 -0
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js.map +1 -0
- package/dist/diagrams/index.d.ts +17 -0
- package/dist/diagrams/index.js +29 -0
- package/dist/diagrams/index.js.map +1 -0
- 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 +138 -0
- package/dist/diagrams/lib/frame.js +131 -0
- package/dist/diagrams/lib/frame.js.map +1 -0
- package/dist/diagrams/lib/geometry.d.ts +114 -0
- package/dist/diagrams/lib/geometry.js +113 -0
- package/dist/diagrams/lib/geometry.js.map +1 -0
- package/dist/diagrams/lib/legend.d.ts +32 -0
- package/dist/diagrams/lib/legend.js +84 -0
- package/dist/diagrams/lib/legend.js.map +1 -0
- package/dist/diagrams/lib/marks.d.ts +207 -0
- package/dist/diagrams/lib/marks.js +480 -0
- package/dist/diagrams/lib/marks.js.map +1 -0
- package/dist/diagrams/lib/route.d.ts +85 -0
- package/dist/diagrams/lib/route.js +178 -0
- package/dist/diagrams/lib/route.js.map +1 -0
- package/dist/diagrams/lib/wires.d.ts +59 -0
- package/dist/diagrams/lib/wires.js +90 -0
- package/dist/diagrams/lib/wires.js.map +1 -0
- package/dist/diagrams/spec.d.ts +405 -0
- package/dist/diagrams/spec.js +1 -0
- package/dist/diagrams/spec.js.map +1 -0
- 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/svg-export.d.ts +138 -0
- package/dist/lib/svg-export.js +198 -0
- package/dist/lib/svg-export.js.map +1 -0
- 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 +51 -6
- package/dist/styles.css +1 -1
- package/dist/tokens.css +20 -2
- package/dist/tokens.d.ts +17 -1
- package/dist/tokens.js +121 -36
- package/dist/tokens.json +121 -36
- package/llms.txt +35 -0
- package/package.json +9 -2
- package/skills/misoto22-design/SKILL.md +21 -7
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/charts/lib/export.ts"],"sourcesContent":["import type { ChartColumn } from './figure'\n\n/**\n * Taking a chart off the page: a PNG of the plot, or a CSV of its rows.\n *\n * Nothing here touches the DOM at module scope. Every browser API — `document`,\n * `canvas`, `URL.createObjectURL` — is reached for inside a function body, so\n * this module is safe to import from a server component and only fails when a\n * consumer actually asks for a file on a server, which is a call that cannot\n * succeed anywhere.\n */\n\nconst SVG_NS = 'http://www.w3.org/2000/svg'\n\n/**\n * The properties copied from the live DOM onto the serialised clone.\n *\n * Paint, geometry-adjacent paint, and type. Layout is deliberately absent: an\n * SVG's geometry is in its attributes, which the clone already carries, and\n * copying computed `width`/`height`/`transform` onto every node would fight\n * them.\n */\nconst PAINTED_PROPERTIES = [\n 'display',\n 'visibility',\n 'opacity',\n 'color',\n 'fill',\n 'fill-opacity',\n 'fill-rule',\n 'stroke',\n 'stroke-opacity',\n 'stroke-width',\n 'stroke-linecap',\n 'stroke-linejoin',\n 'stroke-dasharray',\n 'stroke-dashoffset',\n 'stop-color',\n 'stop-opacity',\n 'flood-color',\n 'flood-opacity',\n 'mask',\n 'filter',\n 'clip-path',\n 'clip-rule',\n 'paint-order',\n 'mix-blend-mode',\n 'shape-rendering',\n 'text-anchor',\n 'dominant-baseline',\n 'font-family',\n 'font-size',\n 'font-style',\n 'font-weight',\n 'letter-spacing',\n 'text-transform',\n] as const\n\nexport interface ChartPngOptions {\n /**\n * Printed above the plot in the exported image. Pass the figure's title: a\n * PNG travels into a slide deck with none of the page's context around it,\n * and an unnamed plot in a deck is a plot nobody can cite.\n */\n title?: string\n /** Multiplier on the on-screen size. 2 is a retina-sharp export. */\n scale?: number\n /**\n * Painted behind the plot. Defaults to the resolved `--chart-surface`, which\n * is what makes a dark-mode export come out dark rather than transparent.\n */\n background?: string\n /** Space around the plot, in CSS pixels. */\n padding?: number\n}\n\n/**\n * The chart's `<svg>`, rasterised to a PNG blob.\n *\n * ## The problem this function exists to solve\n *\n * Serialising the `<svg>` and handing it to an `<img>` puts it in a document\n * of its own. That document has none of the page's stylesheets, so every\n * `var(--color-desktop-0)`, `var(--chart-fill)` and `var(--ink)` in the markup\n * resolves to nothing — and a paint that resolves to nothing is not a fallback\n * colour, it is an invisible mark. Serialise a chart naively and you get an\n * empty rectangle with axes on it.\n *\n * ## The solution\n *\n * Walk the live tree and the clone in step, and on every node write\n * `getComputedStyle`'s answer for the painted properties into an inline\n * `style`. Computed values have already had `var()` substituted by the engine,\n * so `fill: var(--color-desktop-0)` arrives as `fill: rgb(16, 16, 16)` — and,\n * because the walk reads the LIVE element, it reads it under whichever theme\n * is currently applied. Light and dark come out right for free, with no theme\n * argument anywhere in this file.\n *\n * `<stop>` elements are walked too, which is the half that is easy to miss: a\n * series is painted `fill=\"url(#…-colors-desktop)\"`, the url survives\n * serialisation because the gradient is inside the same `<svg>`, and it is the\n * gradient's STOPS that carry the custom properties.\n *\n * ## What this cannot capture, honestly\n *\n * - **Anything drawn in HTML.** The legend, the tooltip, the brush strip and\n * the hidden data table are DOM, not SVG. The PNG is the plot.\n * - **Web fonts.** The isolated document cannot fetch the page's `@font-face`\n * sources, so text is rasterised in whatever the resolved family stack finds\n * locally. Type metrics will differ from the screen.\n * - **Cross-origin images.** A chart with an external `<image>` in it taints\n * the canvas and `toBlob` throws; there is no way around it from script.\n * - **The exact rendering.** This is the browser's SVG rasteriser re-drawing\n * the markup, not a screenshot. Antialiasing, `backdrop-filter` and any\n * effect a page stylesheet applied from outside the `<svg>` are not in it.\n *\n * @example\n * const blob = await chartToPng(plotRef.current, { title: 'Visitors per month' })\n * downloadBlob(blob, 'visitors-per-month.png')\n */\nexport async function chartToPng(\n element: HTMLElement | SVGSVGElement,\n options: ChartPngOptions = {},\n): Promise<Blob> {\n const { title, scale = 2, background, padding = 16 } = options\n\n const svg = findPlotSvg(element)\n if (!svg) throw new Error('chartToPng: found no <svg> to export inside the element it was given')\n\n const box = svg.getBoundingClientRect()\n const width = Math.ceil(box.width)\n const height = Math.ceil(box.height)\n if (width === 0 || height === 0) {\n // A chart that has not been measured yet has no size to export. Silently\n // producing a 0×0 PNG would look like a broken download rather than a\n // too-early call.\n throw new Error('chartToPng: the chart has no measured size yet — export it after it renders')\n }\n\n const clone = svg.cloneNode(true) as SVGSVGElement\n inlinePaint(svg, clone)\n clone.setAttribute('width', String(width))\n clone.setAttribute('height', String(height))\n\n const titleBand = title ? 30 : 0\n const totalWidth = width + padding * 2\n const totalHeight = height + padding * 2 + titleBand\n\n const page = document.createElementNS(SVG_NS, 'svg')\n page.setAttribute('xmlns', SVG_NS)\n page.setAttribute('width', String(totalWidth))\n page.setAttribute('height', String(totalHeight))\n page.setAttribute('viewBox', `0 0 ${totalWidth} ${totalHeight}`)\n\n const plate = document.createElementNS(SVG_NS, 'rect')\n plate.setAttribute('width', String(totalWidth))\n plate.setAttribute('height', String(totalHeight))\n plate.setAttribute('fill', background ?? readToken(element, '--chart-surface', '#ffffff'))\n page.append(plate)\n\n if (title) {\n const caption = document.createElementNS(SVG_NS, 'text')\n caption.setAttribute('x', String(padding))\n caption.setAttribute('y', String(padding + 14))\n caption.setAttribute('fill', readToken(element, '--ink', '#101010'))\n caption.setAttribute('font-size', '13')\n caption.setAttribute('font-weight', '500')\n caption.setAttribute('font-family', getComputedStyle(element).fontFamily || 'sans-serif')\n caption.textContent = title\n page.append(caption)\n }\n\n const group = document.createElementNS(SVG_NS, 'g')\n group.setAttribute('transform', `translate(${padding}, ${padding + titleBand})`)\n group.append(clone)\n page.append(group)\n\n const markup = new XMLSerializer().serializeToString(page)\n const url = URL.createObjectURL(\n new Blob([`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n${markup}`], {\n type: 'image/svg+xml;charset=utf-8',\n }),\n )\n\n try {\n const image = await loadImage(url)\n const canvas = document.createElement('canvas')\n canvas.width = Math.ceil(totalWidth * scale)\n canvas.height = Math.ceil(totalHeight * scale)\n\n const context = canvas.getContext('2d')\n if (!context) throw new Error('chartToPng: this browser gave back no 2D canvas context')\n context.drawImage(image, 0, 0, canvas.width, canvas.height)\n\n return await canvasToBlob(canvas)\n } finally {\n URL.revokeObjectURL(url)\n }\n}\n\n/**\n * The plot's `<svg>`, not the first `<svg>` in the subtree.\n *\n * The distinction matters because the toolbar sits inside the same wrapper and\n * every icon in it is an `<svg>`. Narrowing to the measured chart box first is\n * what stops an export of a chart from being a picture of a magnifying glass.\n */\nfunction findPlotSvg(element: HTMLElement | SVGSVGElement): SVGSVGElement | null {\n if (element.tagName.toLowerCase() === 'svg') return element as SVGSVGElement\n const scope = element.querySelector('[data-slot=\"chart\"]') ?? element\n return scope.querySelector<SVGSVGElement>('svg.recharts-surface') ?? scope.querySelector('svg')\n}\n\n/** Copies the resolved paint of every node in `live` onto the matching node in `clone`. */\nfunction inlinePaint(live: Element, clone: Element): void {\n const computed = getComputedStyle(live)\n let css = ''\n for (const property of PAINTED_PROPERTIES) {\n const value = computed.getPropertyValue(property)\n if (value) css += `${property}:${value};`\n }\n if (css) clone.setAttribute('style', css)\n\n // The clone is a faithful deep copy, so child lists line up by index.\n const children = live.children\n const clones = clone.children\n for (let index = 0; index < children.length; index += 1) {\n const liveChild = children[index]\n const cloneChild = clones[index]\n if (liveChild && cloneChild) inlinePaint(liveChild, cloneChild)\n }\n}\n\n/**\n * A custom property, resolved against the live element.\n *\n * `var()` inside a custom property is substituted at computed-value time, so\n * `--chart-surface: var(--paper)` reads back as a colour. The guard is for the\n * case where it did not — an unregistered name, or a value the engine left as\n * a token stream — because writing `var(--paper)` into a serialised SVG paints\n * nothing at all.\n */\nfunction readToken(element: Element, name: string, fallback: string): string {\n const value = getComputedStyle(element).getPropertyValue(name).trim()\n return value && !value.includes('var(') ? value : fallback\n}\n\nfunction loadImage(url: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const image = new Image()\n image.addEventListener('load', () => resolve(image))\n image.addEventListener('error', () =>\n reject(new Error('chartToPng: the browser refused to decode the serialised chart')),\n )\n image.src = url\n })\n}\n\nfunction canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {\n return new Promise((resolve, reject) => {\n try {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob)\n else reject(new Error('chartToPng: the canvas produced no PNG data'))\n }, 'image/png')\n } catch (cause) {\n // A cross-origin image anywhere in the plot taints the canvas and this\n // throws SecurityError. Nothing in script can untaint it, so the honest\n // move is to say which failure it was.\n reject(\n new Error(\n 'chartToPng: the canvas is tainted, which means the chart contains a cross-origin image',\n { cause },\n ),\n )\n }\n })\n}\n\n/**\n * A UTF-8 byte order mark.\n *\n * Excel does not sniff encodings. Handed a CSV with no BOM it decodes the file\n * with the machine's legacy code page, so \"Müller\", \"東京\" and \"€\" arrive as\n * mojibake on most Windows installs — and the reader's conclusion is that the\n * export is broken, not that the encoding was guessed. The BOM is the only\n * in-band signal Excel honours, and every other tool skips it silently.\n */\nconst UTF8_BOM = '\\uFEFF'\n\n/**\n * The chart's rows as RFC 4180 CSV.\n *\n * Quotes a field only when it needs quoting — a comma, a double quote, or a\n * line break in it — doubles any quote inside, and separates records with CRLF\n * as the RFC requires rather than with the `\\n` a Unix habit would reach for.\n *\n * Numbers are written unformatted on purpose. `1,234` would need quoting and\n * would then land in the spreadsheet as text, which is the one thing a numeric\n * export must not do; formatting belongs to the chart, not to the file.\n *\n * A column's `label` is a `ReactNode`, so only a string or a number can be a\n * header. Anything else falls back to the column's key, which is a worse\n * header than the label but a better one than an empty cell.\n *\n * @example\n * const csv = chartToCsv(data, [{ key: 'desktop', label: 'Desktop' }])\n * downloadBlob(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'visitors.csv')\n */\nexport function chartToCsv(rows: Record<string, unknown>[], columns: ChartColumn[]): string {\n if (columns.length === 0) return UTF8_BOM\n\n const lines = [columns.map((column) => csvField(headerText(column))).join(',')]\n\n for (const row of rows) {\n lines.push(columns.map((column) => csvField(cellText(row[column.key]))).join(','))\n }\n\n // The RFC makes the last CRLF optional; writing it matches what a\n // spreadsheet emits and keeps the file from ending mid-record in a terminal.\n return `${UTF8_BOM}${lines.join('\\r\\n')}\\r\\n`\n}\n\n/** One field, quoted only when the RFC says it has to be. */\nfunction csvField(value: string): string {\n if (!/[\",\\r\\n]/.test(value)) return value\n return `\"${value.replace(/\"/g, '\"\"')}\"`\n}\n\n/** A column's header text, falling back to its key when the label is markup. */\nfunction headerText(column: ChartColumn): string {\n const { label } = column\n if (typeof label === 'string') return label\n if (typeof label === 'number') return String(label)\n return column.key\n}\n\n/** A cell value as text. Anything that is not a primitive reads as empty. */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : ''\n if (typeof value === 'string' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n return ''\n}\n\n/**\n * Hands a blob to the browser as a download.\n *\n * Throws rather than no-oping without a `document`: a download is something a\n * person just asked for, and a silent return would look to them like a click\n * that did nothing.\n */\nexport function downloadBlob(blob: Blob, filename: string): void {\n if (typeof document === 'undefined') {\n throw new Error('downloadBlob: needs a browser document — call it from an event handler')\n }\n\n const url = URL.createObjectURL(blob)\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = filename\n anchor.rel = 'noopener'\n anchor.style.display = 'none'\n document.body.append(anchor)\n anchor.click()\n anchor.remove()\n\n // Revoking in the same tick cancels the download in Safari; one turn of the\n // event loop is enough for the navigation to have started.\n setTimeout(() => URL.revokeObjectURL(url), 0)\n}\n\n/**\n * A figure's title as a filename.\n *\n * Lowercase, ASCII-safe and hyphenated, because a downloaded file crosses into\n * shells, zip archives and Windows paths where a title's spaces and slashes\n * are someone else's problem.\n */\nexport function exportFilename(title: string, extension: string): string {\n const slug = title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n return `${slug || 'chart'}.${extension}`\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/lib/export.ts"],"sourcesContent":["import type { ChartColumn } from './figure'\nimport {\n downloadBlob,\n exportFilename as figureFilename,\n rasterize,\n readToken,\n serializeSvg,\n} from '../../lib/svg-export'\n\n/**\n * Taking a chart off the page: a PNG of the plot, or a CSV of its rows.\n *\n * The general half — walking a live tree to inline its resolved paint,\n * serialising a standalone SVG document, rasterising one through a canvas — is\n * not chart-shaped at all, and lives in `src/lib/svg-export.ts` shared with the\n * diagrams entry. What is left here is what only a Recharts chart knows: which\n * `<svg>` in the subtree is the plot, what a row of chart data looks like as a\n * CSV record, and that the ground behind an exported plot is `--chart-surface`.\n *\n * Nothing here touches the DOM at module scope. Every browser API is reached\n * for inside a function body, so this module is safe to import from a server\n * component and only fails when a consumer actually asks for a file on a\n * server, which is a call that cannot succeed anywhere.\n */\n\nexport interface ChartPngOptions {\n /**\n * Printed above the plot in the exported image. Pass the figure's title: a\n * PNG travels into a slide deck with none of the page's context around it,\n * and an unnamed plot in a deck is a plot nobody can cite.\n */\n title?: string\n /** Multiplier on the on-screen size. 2 is a retina-sharp export. */\n scale?: number\n /**\n * Painted behind the plot. Defaults to the resolved `--chart-surface`, which\n * is what makes a dark-mode export come out dark rather than transparent.\n */\n background?: string\n /** Space around the plot, in CSS pixels. */\n padding?: number\n}\n\n/**\n * The chart's `<svg>`, rasterised to a PNG blob.\n *\n * Three chart-specific decisions and nothing else. `serializeSvg` does the\n * work, and its comment carries the reason a naive `XMLSerializer` export of a\n * themed chart comes out as an empty rectangle with axes on it.\n *\n * 1. WHICH `<svg>` — the plot, not the first one in the subtree. See\n * `findPlotSvg`.\n * 2. WHAT GROUND — `--chart-surface`, resolved against the live element, which\n * is what makes a dark-mode export come out dark rather than transparent.\n * 3. WHAT INK AND FACE for the caption — `--ink` and the element's own\n * resolved family, so the exported title matches the page it came from.\n *\n * ## What this cannot capture, honestly\n *\n * - **Anything drawn in HTML.** The legend, the tooltip, the brush strip and\n * the hidden data table are DOM, not SVG. The PNG is the plot.\n * - **Web fonts, cross-origin images, and the exact rendering.** See\n * `serializeSvg` and `rasterize`.\n *\n * @example\n * const blob = await chartToPng(plotRef.current, { title: 'Visitors per month' })\n * downloadBlob(blob, exportFilename('Visitors per month', 'png'))\n */\nexport async function chartToPng(\n element: HTMLElement | SVGSVGElement,\n options: ChartPngOptions = {},\n): Promise<Blob> {\n const { title, scale = 2, background, padding = 16 } = options\n\n const svg = findPlotSvg(element)\n if (!svg) throw new Error('chartToPng: found no <svg> to export inside the element it was given')\n\n const markup = serializeSvg(svg, {\n padding,\n background: background ?? readToken(element, '--chart-surface', '#ffffff'),\n title,\n titleColor: readToken(element, '--ink', '#101010'),\n titleFont: getComputedStyle(element).fontFamily || 'sans-serif',\n })\n\n return rasterize(markup, 'png', { scale })\n}\n\n/**\n * The plot's `<svg>`, not the first `<svg>` in the subtree.\n *\n * Two narrowings, and both are load-bearing. The toolbar sits outside the plot\n * wrapper and every control in it is an `<svg>`, so the scope comes first;\n * INSIDE the wrapper the legend draws its own swatches, so `.recharts-surface`\n * comes second. Either one missing exports a picture of an icon, which looks\n * like a working download until somebody opens the file.\n */\nfunction findPlotSvg(element: HTMLElement | SVGSVGElement): SVGSVGElement | null {\n if (element.tagName.toLowerCase() === 'svg') return element as SVGSVGElement\n const scope = element.querySelector('[data-slot=\"chart\"]') ?? element\n return scope.querySelector<SVGSVGElement>('svg.recharts-surface') ?? scope.querySelector('svg')\n}\n\n/**\n * A UTF-8 byte order mark.\n *\n * Excel does not sniff encodings. Handed a CSV with no BOM it decodes the file\n * with the machine's legacy code page, so \"Müller\", \"東京\" and \"€\" arrive as\n * mojibake on most Windows installs — and the reader's conclusion is that the\n * export is broken, not that the encoding was guessed. The BOM is the only\n * in-band signal Excel honours, and every other tool skips it silently.\n */\nconst UTF8_BOM = '\\uFEFF'\n\n/**\n * The chart's rows as RFC 4180 CSV.\n *\n * Quotes a field only when it needs quoting — a comma, a double quote, or a\n * line break in it — doubles any quote inside, and separates records with CRLF\n * as the RFC requires rather than with the `\\n` a Unix habit would reach for.\n *\n * Numbers are written unformatted on purpose. `1,234` would need quoting and\n * would then land in the spreadsheet as text, which is the one thing a numeric\n * export must not do; formatting belongs to the chart, not to the file.\n *\n * A column's `label` is a `ReactNode`, so only a string or a number can be a\n * header. Anything else falls back to the column's key, which is a worse\n * header than the label but a better one than an empty cell.\n *\n * @example\n * const csv = chartToCsv(data, [{ key: 'desktop', label: 'Desktop' }])\n * downloadBlob(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'visitors.csv')\n */\nexport function chartToCsv(rows: Record<string, unknown>[], columns: ChartColumn[]): string {\n if (columns.length === 0) return UTF8_BOM\n\n const lines = [columns.map((column) => csvField(headerText(column))).join(',')]\n\n for (const row of rows) {\n lines.push(columns.map((column) => csvField(cellText(row[column.key]))).join(','))\n }\n\n // The RFC makes the last CRLF optional; writing it matches what a\n // spreadsheet emits and keeps the file from ending mid-record in a terminal.\n return `${UTF8_BOM}${lines.join('\\r\\n')}\\r\\n`\n}\n\n/** One field, quoted only when the RFC says it has to be. */\nfunction csvField(value: string): string {\n if (!/[\",\\r\\n]/.test(value)) return value\n return `\"${value.replace(/\"/g, '\"\"')}\"`\n}\n\n/** A column's header text, falling back to its key when the label is markup. */\nfunction headerText(column: ChartColumn): string {\n const { label } = column\n if (typeof label === 'string') return label\n if (typeof label === 'number') return String(label)\n return column.key\n}\n\n/** A cell value as text. Anything that is not a primitive reads as empty. */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : ''\n if (typeof value === 'string' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n return ''\n}\n\nexport { downloadBlob }\n\n/**\n * A figure's title as a filename.\n *\n * The shared slugger, with `chart` rather than `figure` as the fallback for a\n * title that slugs to nothing — an export from this entry is a chart, and the\n * name a reader finds in their downloads folder should say so.\n */\nexport function exportFilename(title: string, extension: string): string {\n return figureFilename(title, extension, 'chart')\n}\n"],"mappings":"AACA;AAAA,EACE;AAAA,EACA,kBAAkB;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA6DP,eAAsB,WACpB,SACA,UAA2B,CAAC,GACb;AACf,QAAM,EAAE,OAAO,QAAQ,GAAG,YAAY,UAAU,GAAG,IAAI;AAEvD,QAAM,MAAM,YAAY,OAAO;AAC/B,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,sEAAsE;AAEhG,QAAM,SAAS,aAAa,KAAK;AAAA,IAC/B;AAAA,IACA,YAAY,cAAc,UAAU,SAAS,mBAAmB,SAAS;AAAA,IACzE;AAAA,IACA,YAAY,UAAU,SAAS,SAAS,SAAS;AAAA,IACjD,WAAW,iBAAiB,OAAO,EAAE,cAAc;AAAA,EACrD,CAAC;AAED,SAAO,UAAU,QAAQ,OAAO,EAAE,MAAM,CAAC;AAC3C;AAWA,SAAS,YAAY,SAA4D;AAC/E,MAAI,QAAQ,QAAQ,YAAY,MAAM,MAAO,QAAO;AACpD,QAAM,QAAQ,QAAQ,cAAc,qBAAqB,KAAK;AAC9D,SAAO,MAAM,cAA6B,sBAAsB,KAAK,MAAM,cAAc,KAAK;AAChG;AAWA,MAAM,WAAW;AAqBV,SAAS,WAAW,MAAiC,SAAgC;AAC1F,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,QAAM,QAAQ,CAAC,QAAQ,IAAI,CAAC,WAAW,SAAS,WAAW,MAAM,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC;AAE9E,aAAW,OAAO,MAAM;AACtB,UAAM,KAAK,QAAQ,IAAI,CAAC,WAAW,SAAS,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC;AAAA,EACnF;AAIA,SAAO,GAAG,QAAQ,GAAG,MAAM,KAAK,MAAM,CAAC;AAAA;AACzC;AAGA,SAAS,SAAS,OAAuB;AACvC,MAAI,CAAC,WAAW,KAAK,KAAK,EAAG,QAAO;AACpC,SAAO,IAAI,MAAM,QAAQ,MAAM,IAAI,CAAC;AACtC;AAGA,SAAS,WAAW,QAA6B;AAC/C,QAAM,EAAE,MAAM,IAAI;AAClB,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,KAAK;AAClD,SAAO,OAAO;AAChB;AAGA,SAAS,SAAS,OAAwB;AACxC,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,KAAK,IAAI,OAAO,KAAK,IAAI;AAC/E,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAAW,QAAO,OAAO,KAAK;AAChF,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,SAAO;AACT;AAWO,SAAS,eAAe,OAAe,WAA2B;AACvE,SAAO,eAAe,OAAO,WAAW,OAAO;AACjD;","names":[]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
+
import { ChartEmptyProps } from './empty.js';
|
|
3
4
|
|
|
4
5
|
interface ChartColumn {
|
|
5
6
|
/** The row field this column reads. */
|
|
@@ -47,6 +48,19 @@ interface ChartFigureProps {
|
|
|
47
48
|
children: ReactNode;
|
|
48
49
|
/** The table view. Omit and it is generated; `false` opts out. */
|
|
49
50
|
table?: Omit<ChartDataTableProps, 'caption'> | false;
|
|
51
|
+
/**
|
|
52
|
+
* Whether there is nothing to draw. The plot is replaced by `empty` when it
|
|
53
|
+
* is true.
|
|
54
|
+
*
|
|
55
|
+
* Named as a question rather than derived from `table.rows`, because a chart
|
|
56
|
+
* mid-load has no rows either and wants its skeleton, not a message.
|
|
57
|
+
*/
|
|
58
|
+
isEmpty?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* What stands in for the plot when there is nothing to draw. `false` keeps
|
|
61
|
+
* the plot, for a chart whose emptiness is itself the reading.
|
|
62
|
+
*/
|
|
63
|
+
empty?: ChartEmptyProps | false;
|
|
50
64
|
}
|
|
51
65
|
/**
|
|
52
66
|
* The figure a chart is rendered inside: its name, its plot, and its data as a
|
|
@@ -55,7 +69,13 @@ interface ChartFigureProps {
|
|
|
55
69
|
* Every chart root in the package wraps itself in this, so "the chart is
|
|
56
70
|
* announced and its numbers are reachable" is a property of the system rather
|
|
57
71
|
* than something each call site has to remember to add.
|
|
72
|
+
*
|
|
73
|
+
* The empty state is here for the same reason. Seven of the twenty charts had
|
|
74
|
+
* no `empty` at all — zero rows drew a named figure over a blank box, and
|
|
75
|
+
* `ChartDataTable` renders nothing below one row, so the picture and its text
|
|
76
|
+
* equivalent went silent together and the reader could not tell "no data" from
|
|
77
|
+
* "failed to load". A chart that forgets it now has to forget it here, once.
|
|
58
78
|
*/
|
|
59
|
-
declare function ChartFigure({ title, showTitle, description, className, children, table, }: ChartFigureProps): react.JSX.Element;
|
|
79
|
+
declare function ChartFigure({ title, showTitle, description, className, children, table, isEmpty, empty, }: ChartFigureProps): react.JSX.Element;
|
|
60
80
|
|
|
61
81
|
export { type ChartColumn, ChartDataTable, type ChartDataTableProps, ChartFigure, type ChartFigureProps };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useId } from "react";
|
|
3
3
|
import { cn } from "../../lib/cn.js";
|
|
4
|
+
import { ChartEmpty } from "./empty.js";
|
|
4
5
|
function ChartDataTable({ caption, rows, columns, rowKey }) {
|
|
5
6
|
if (rows.length === 0 || columns.length === 0) return null;
|
|
6
7
|
return /* @__PURE__ */ jsxs("table", { className: "sr-only", children: [
|
|
@@ -27,7 +28,9 @@ function ChartFigure({
|
|
|
27
28
|
description,
|
|
28
29
|
className,
|
|
29
30
|
children,
|
|
30
|
-
table
|
|
31
|
+
table,
|
|
32
|
+
isEmpty = false,
|
|
33
|
+
empty
|
|
31
34
|
}) {
|
|
32
35
|
const captionId = `chart-title-${useId().replace(/:/g, "")}`;
|
|
33
36
|
return (
|
|
@@ -47,7 +50,7 @@ function ChartFigure({
|
|
|
47
50
|
]
|
|
48
51
|
}
|
|
49
52
|
),
|
|
50
|
-
children,
|
|
53
|
+
isEmpty && empty !== false ? /* @__PURE__ */ jsx(ChartEmpty, { ...empty || {} }) : children,
|
|
51
54
|
table && /* @__PURE__ */ jsx(ChartDataTable, { caption: title, ...table })
|
|
52
55
|
] })
|
|
53
56
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/charts/lib/figure.tsx"],"sourcesContent":["import { useId, type ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface ChartColumn {\n /** The row field this column reads. */\n key: string\n /** What the column is called in the table header. */\n label: ReactNode\n}\n\nexport interface ChartDataTableProps {\n caption: string\n rows: Record<string, unknown>[]\n columns: ChartColumn[]\n /** The field naming each row — the category, the date, the bucket. */\n rowKey?: string\n}\n\n/**\n * The chart's data, as a table, for a reader who cannot see the chart.\n *\n * An SVG plot is a picture: `accessibilityLayer` gives Recharts' own keyboard\n * cursor and announces one point at a time, which is navigation rather than\n * access to the figures. This is the table view — visually hidden, in the\n * accessibility tree, and generated from the same rows the marks are drawn\n * from, so the two cannot disagree.\n *\n * `sr-only` rather than `hidden`: `display: none` removes it from the\n * accessibility tree as well as from the page, which would defeat the purpose.\n */\nexport function ChartDataTable({ caption, rows, columns, rowKey }: ChartDataTableProps) {\n if (rows.length === 0 || columns.length === 0) return null\n\n return (\n <table className=\"sr-only\">\n <caption>{caption}</caption>\n <thead>\n <tr>\n {rowKey && <th scope=\"col\">{rowKey}</th>}\n {columns.map((column) => (\n <th key={column.key} scope=\"col\">\n {column.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, index) => (\n <tr key={index}>\n {rowKey && <th scope=\"row\">{cellText(row[rowKey]) || String(index + 1)}</th>}\n {columns.map((column) => (\n <td key={column.key}>{cellText(row[column.key])}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n )\n}\n\n/** A cell value as text. Anything that is not a primitive reads as empty. */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'number') return value.toLocaleString()\n if (typeof value === 'string' || typeof value === 'boolean') return String(value)\n return ''\n}\n\nexport interface ChartFigureProps {\n /**\n * What the chart shows, in a sentence a reader could act on — \"Visitors per\n * month, 2026\" rather than \"Chart\".\n *\n * Required, and required for the same reason `Table.caption` and\n * `Progress.label` are: a plot with no name announces nothing, and it is not\n * a thing a call site can be trusted to remember. Hidden by default, because\n * most call sites already print a heading above the chart; `showTitle` puts\n * it back when they do not.\n */\n title: string\n /** Prints the title above the plot instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n className?: string\n children: ReactNode\n /** The table view. Omit and it is generated; `false` opts out. */\n table?: Omit<ChartDataTableProps, 'caption'> | false\n}\n\n/**\n * The figure a chart is rendered inside: its name, its plot, and its data as a\n * table.\n *\n * Every chart root in the package wraps itself in this, so \"the chart is\n * announced and its numbers are reachable\" is a property of the system rather\n * than something each call site has to remember to add.\n */\nexport function ChartFigure({\n title,\n showTitle = false,\n description,\n className,\n children,\n table,\n}: ChartFigureProps) {\n const captionId = `chart-title-${useId().replace(/:/g, '')}`\n\n return (\n // Named explicitly rather than relying on the caption. A `<figcaption>` IS\n // the figure's accessible name per the spec, but support for deriving it is\n // uneven across assistive technology — and a chart whose name resolves in\n // only some screen readers is a chart with no name.\n <figure aria-labelledby={captionId} className={cn('flex min-h-0 w-full flex-col', className)}>\n <figcaption\n id={captionId}\n className={cn(!showTitle && 'sr-only', showTitle && 'mb-3 flex flex-col gap-1')}\n >\n <span className={cn(showTitle && 'font-heading text-[length:var(--fs-item)] text-(--ink)')}>{title}</span>\n {description && (\n <span className={cn(showTitle && 'mono-meta text-(--ink-3-aa)')}>{description}</span>\n )}\n </figcaption>\n {children}\n {table && <ChartDataTable caption={title} {...table} />}\n </figure>\n )\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/lib/figure.tsx"],"sourcesContent":["import { useId, type ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { ChartEmpty, type ChartEmptyProps } from './empty'\n\nexport interface ChartColumn {\n /** The row field this column reads. */\n key: string\n /** What the column is called in the table header. */\n label: ReactNode\n}\n\nexport interface ChartDataTableProps {\n caption: string\n rows: Record<string, unknown>[]\n columns: ChartColumn[]\n /** The field naming each row — the category, the date, the bucket. */\n rowKey?: string\n}\n\n/**\n * The chart's data, as a table, for a reader who cannot see the chart.\n *\n * An SVG plot is a picture: `accessibilityLayer` gives Recharts' own keyboard\n * cursor and announces one point at a time, which is navigation rather than\n * access to the figures. This is the table view — visually hidden, in the\n * accessibility tree, and generated from the same rows the marks are drawn\n * from, so the two cannot disagree.\n *\n * `sr-only` rather than `hidden`: `display: none` removes it from the\n * accessibility tree as well as from the page, which would defeat the purpose.\n */\nexport function ChartDataTable({ caption, rows, columns, rowKey }: ChartDataTableProps) {\n if (rows.length === 0 || columns.length === 0) return null\n\n return (\n <table className=\"sr-only\">\n <caption>{caption}</caption>\n <thead>\n <tr>\n {rowKey && <th scope=\"col\">{rowKey}</th>}\n {columns.map((column) => (\n <th key={column.key} scope=\"col\">\n {column.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, index) => (\n <tr key={index}>\n {rowKey && <th scope=\"row\">{cellText(row[rowKey]) || String(index + 1)}</th>}\n {columns.map((column) => (\n <td key={column.key}>{cellText(row[column.key])}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n )\n}\n\n/** A cell value as text. Anything that is not a primitive reads as empty. */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'number') return value.toLocaleString()\n if (typeof value === 'string' || typeof value === 'boolean') return String(value)\n return ''\n}\n\nexport interface ChartFigureProps {\n /**\n * What the chart shows, in a sentence a reader could act on — \"Visitors per\n * month, 2026\" rather than \"Chart\".\n *\n * Required, and required for the same reason `Table.caption` and\n * `Progress.label` are: a plot with no name announces nothing, and it is not\n * a thing a call site can be trusted to remember. Hidden by default, because\n * most call sites already print a heading above the chart; `showTitle` puts\n * it back when they do not.\n */\n title: string\n /** Prints the title above the plot instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n className?: string\n children: ReactNode\n /** The table view. Omit and it is generated; `false` opts out. */\n table?: Omit<ChartDataTableProps, 'caption'> | false\n /**\n * Whether there is nothing to draw. The plot is replaced by `empty` when it\n * is true.\n *\n * Named as a question rather than derived from `table.rows`, because a chart\n * mid-load has no rows either and wants its skeleton, not a message.\n */\n isEmpty?: boolean\n /**\n * What stands in for the plot when there is nothing to draw. `false` keeps\n * the plot, for a chart whose emptiness is itself the reading.\n */\n empty?: ChartEmptyProps | false\n}\n\n/**\n * The figure a chart is rendered inside: its name, its plot, and its data as a\n * table.\n *\n * Every chart root in the package wraps itself in this, so \"the chart is\n * announced and its numbers are reachable\" is a property of the system rather\n * than something each call site has to remember to add.\n *\n * The empty state is here for the same reason. Seven of the twenty charts had\n * no `empty` at all — zero rows drew a named figure over a blank box, and\n * `ChartDataTable` renders nothing below one row, so the picture and its text\n * equivalent went silent together and the reader could not tell \"no data\" from\n * \"failed to load\". A chart that forgets it now has to forget it here, once.\n */\nexport function ChartFigure({\n title,\n showTitle = false,\n description,\n className,\n children,\n table,\n isEmpty = false,\n empty,\n}: ChartFigureProps) {\n const captionId = `chart-title-${useId().replace(/:/g, '')}`\n\n return (\n // Named explicitly rather than relying on the caption. A `<figcaption>` IS\n // the figure's accessible name per the spec, but support for deriving it is\n // uneven across assistive technology — and a chart whose name resolves in\n // only some screen readers is a chart with no name.\n <figure aria-labelledby={captionId} className={cn('flex min-h-0 w-full flex-col', className)}>\n <figcaption\n id={captionId}\n className={cn(!showTitle && 'sr-only', showTitle && 'mb-3 flex flex-col gap-1')}\n >\n <span className={cn(showTitle && 'font-heading text-[length:var(--fs-item)] text-(--ink)')}>{title}</span>\n {description && (\n <span className={cn(showTitle && 'mono-meta text-(--ink-3-aa)')}>{description}</span>\n )}\n </figcaption>\n {isEmpty && empty !== false ? <ChartEmpty {...(empty || {})} /> : children}\n {table && <ChartDataTable caption={title} {...table} />}\n </figure>\n )\n}\n"],"mappings":"AAoCM,cAEE,YAFF;AApCN,SAAS,aAA6B;AACtC,SAAS,UAAU;AACnB,SAAS,kBAAwC;AA6B1C,SAAS,eAAe,EAAE,SAAS,MAAM,SAAS,OAAO,GAAwB;AACtF,MAAI,KAAK,WAAW,KAAK,QAAQ,WAAW,EAAG,QAAO;AAEtD,SACE,qBAAC,WAAM,WAAU,WACf;AAAA,wBAAC,aAAS,mBAAQ;AAAA,IAClB,oBAAC,WACC,+BAAC,QACE;AAAA,gBAAU,oBAAC,QAAG,OAAM,OAAO,kBAAO;AAAA,MAClC,QAAQ,IAAI,CAAC,WACZ,oBAAC,QAAoB,OAAM,OACxB,iBAAO,SADD,OAAO,GAEhB,CACD;AAAA,OACH,GACF;AAAA,IACA,oBAAC,WACE,eAAK,IAAI,CAAC,KAAK,UACd,qBAAC,QACE;AAAA,gBAAU,oBAAC,QAAG,OAAM,OAAO,mBAAS,IAAI,MAAM,CAAC,KAAK,OAAO,QAAQ,CAAC,GAAE;AAAA,MACtE,QAAQ,IAAI,CAAC,WACZ,oBAAC,QAAqB,mBAAS,IAAI,OAAO,GAAG,CAAC,KAArC,OAAO,GAAgC,CACjD;AAAA,SAJM,KAKT,CACD,GACH;AAAA,KACF;AAEJ;AAGA,SAAS,SAAS,OAAwB;AACxC,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,MAAM,eAAe;AAC3D,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAAW,QAAO,OAAO,KAAK;AAChF,SAAO;AACT;AAmDO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AACF,GAAqB;AACnB,QAAM,YAAY,eAAe,MAAM,EAAE,QAAQ,MAAM,EAAE,CAAC;AAE1D;AAAA;AAAA;AAAA;AAAA;AAAA,IAKE,qBAAC,YAAO,mBAAiB,WAAW,WAAW,GAAG,gCAAgC,SAAS,GACzF;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,IAAI;AAAA,UACJ,WAAW,GAAG,CAAC,aAAa,WAAW,aAAa,0BAA0B;AAAA,UAE9E;AAAA,gCAAC,UAAK,WAAW,GAAG,aAAa,wDAAwD,GAAI,iBAAM;AAAA,YAClG,eACC,oBAAC,UAAK,WAAW,GAAG,aAAa,6BAA6B,GAAI,uBAAY;AAAA;AAAA;AAAA,MAElF;AAAA,MACC,WAAW,UAAU,QAAQ,oBAAC,cAAY,GAAI,SAAS,CAAC,GAAI,IAAK;AAAA,MACjE,SAAS,oBAAC,kBAAe,SAAS,OAAQ,GAAG,OAAO;AAAA,OACvD;AAAA;AAEJ;","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a value sits on a linear scale, and what to do when the scale has no
|
|
3
|
+
* width.
|
|
4
|
+
*
|
|
5
|
+
* The three engine-less forms — `Sparkline`, `Heatmap`, `BulletChart` — each
|
|
6
|
+
* normalise a value into a 0-to-1 position, and each had answered the
|
|
7
|
+
* degenerate case differently: the heatmap put a constant grid in the middle of
|
|
8
|
+
* its ramp, the bullet chart put a constant measure at the start of its track,
|
|
9
|
+
* and the sparkline divided by a substituted `1` and so drew every flat run
|
|
10
|
+
* along the floor. A flat run along the floor is the worst of the three,
|
|
11
|
+
* because in a column of sparklines "unchanged" and "pinned at its worst" is
|
|
12
|
+
* the one distinction the reader is scanning for.
|
|
13
|
+
*
|
|
14
|
+
* The middle is the honest answer: a scale with no width cannot rank anything,
|
|
15
|
+
* so no position on it is more true than another, and the centre is the only
|
|
16
|
+
* one that does not read as a verdict.
|
|
17
|
+
*/
|
|
18
|
+
/** Where `value` sits between `min` and `max`, 0 to 1. Not held to the ends. */
|
|
19
|
+
declare function fraction(value: number, min: number, max: number): number;
|
|
20
|
+
/** `fraction`, held inside the track. */
|
|
21
|
+
declare function clampedFraction(value: number, min: number, max: number): number;
|
|
22
|
+
|
|
23
|
+
export { clampedFraction, fraction };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function fraction(value, min, max) {
|
|
2
|
+
const span = max - min;
|
|
3
|
+
if (span === 0) return 0.5;
|
|
4
|
+
return (value - min) / span;
|
|
5
|
+
}
|
|
6
|
+
function clampedFraction(value, min, max) {
|
|
7
|
+
return Math.min(1, Math.max(0, fraction(value, min, max)));
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
clampedFraction,
|
|
11
|
+
fraction
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=scale.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/lib/scale.ts"],"sourcesContent":["/**\n * Where a value sits on a linear scale, and what to do when the scale has no\n * width.\n *\n * The three engine-less forms — `Sparkline`, `Heatmap`, `BulletChart` — each\n * normalise a value into a 0-to-1 position, and each had answered the\n * degenerate case differently: the heatmap put a constant grid in the middle of\n * its ramp, the bullet chart put a constant measure at the start of its track,\n * and the sparkline divided by a substituted `1` and so drew every flat run\n * along the floor. A flat run along the floor is the worst of the three,\n * because in a column of sparklines \"unchanged\" and \"pinned at its worst\" is\n * the one distinction the reader is scanning for.\n *\n * The middle is the honest answer: a scale with no width cannot rank anything,\n * so no position on it is more true than another, and the centre is the only\n * one that does not read as a verdict.\n */\n\n/** Where `value` sits between `min` and `max`, 0 to 1. Not held to the ends. */\nexport function fraction(value: number, min: number, max: number): number {\n const span = max - min\n if (span === 0) return 0.5\n return (value - min) / span\n}\n\n/** `fraction`, held inside the track. */\nexport function clampedFraction(value: number, min: number, max: number): number {\n return Math.min(1, Math.max(0, fraction(value, min, max)))\n}\n"],"mappings":"AAmBO,SAAS,SAAS,OAAe,KAAa,KAAqB;AACxE,QAAM,OAAO,MAAM;AACnB,MAAI,SAAS,EAAG,QAAO;AACvB,UAAQ,QAAQ,OAAO;AACzB;AAGO,SAAS,gBAAgB,OAAe,KAAa,KAAqB;AAC/E,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,OAAO,KAAK,GAAG,CAAC,CAAC;AAC3D;","names":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A chart's selected series, sector, bar or node — controlled or not.
|
|
3
|
+
*
|
|
4
|
+
* Nine charts each grew a `defaultSelected*` prop and a `useState` seeded from
|
|
5
|
+
* it, and none of them grew the other half. A default seeds the state ONCE:
|
|
6
|
+
* pass a new one and nothing happens, because the state was already
|
|
7
|
+
* initialised. So a call site that wanted a chart's selection to follow a
|
|
8
|
+
* filter, a route or a sibling chart had no way to say so — the prop that looks
|
|
9
|
+
* like the way to do it silently is not.
|
|
10
|
+
*
|
|
11
|
+
* The pair is the React convention and it is the whole fix: pass `selected` to
|
|
12
|
+
* drive it from outside, or leave it undefined and the chart keeps its own.
|
|
13
|
+
*
|
|
14
|
+
* @param selected The controlled value. `undefined` leaves the chart in charge.
|
|
15
|
+
* @param fallback What an uncontrolled chart starts on.
|
|
16
|
+
* @param onChange Fires on every change, controlled or not.
|
|
17
|
+
*/
|
|
18
|
+
declare function useChartSelection(selected: string | null | undefined, fallback: string | null, onChange: ((selected: string | null) => void) | undefined): [string | null, (next: string | null) => void];
|
|
19
|
+
|
|
20
|
+
export { useChartSelection };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
function useChartSelection(selected, fallback, onChange) {
|
|
4
|
+
const [own, setOwn] = useState(fallback);
|
|
5
|
+
const isControlled = selected !== void 0;
|
|
6
|
+
const select = useCallback(
|
|
7
|
+
(next) => {
|
|
8
|
+
setOwn(next);
|
|
9
|
+
onChange?.(next);
|
|
10
|
+
},
|
|
11
|
+
[onChange]
|
|
12
|
+
);
|
|
13
|
+
return [isControlled ? selected : own, select];
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
useChartSelection
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/lib/selection.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useState } from 'react'\n\n/**\n * A chart's selected series, sector, bar or node — controlled or not.\n *\n * Nine charts each grew a `defaultSelected*` prop and a `useState` seeded from\n * it, and none of them grew the other half. A default seeds the state ONCE:\n * pass a new one and nothing happens, because the state was already\n * initialised. So a call site that wanted a chart's selection to follow a\n * filter, a route or a sibling chart had no way to say so — the prop that looks\n * like the way to do it silently is not.\n *\n * The pair is the React convention and it is the whole fix: pass `selected` to\n * drive it from outside, or leave it undefined and the chart keeps its own.\n *\n * @param selected The controlled value. `undefined` leaves the chart in charge.\n * @param fallback What an uncontrolled chart starts on.\n * @param onChange Fires on every change, controlled or not.\n */\nexport function useChartSelection(\n selected: string | null | undefined,\n fallback: string | null,\n onChange: ((selected: string | null) => void) | undefined,\n): [string | null, (next: string | null) => void] {\n const [own, setOwn] = useState<string | null>(fallback)\n const isControlled = selected !== undefined\n\n const select = useCallback(\n (next: string | null) => {\n // The controlled chart still updates its own copy. It is never read while\n // `selected` is given, and it means an unmount of the controlling prop\n // does not throw the selection away.\n setOwn(next)\n onChange?.(next)\n },\n [onChange],\n )\n\n return [isControlled ? selected : own, select]\n}\n"],"mappings":";AAEA,SAAS,aAAa,gBAAgB;AAmB/B,SAAS,kBACd,UACA,UACA,UACgD;AAChD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAwB,QAAQ;AACtD,QAAM,eAAe,aAAa;AAElC,QAAM,SAAS;AAAA,IACb,CAAC,SAAwB;AAIvB,aAAO,IAAI;AACX,iBAAW,IAAI;AAAA,IACjB;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAEA,SAAO,CAAC,eAAe,WAAW,KAAK,MAAM;AAC/C;","names":[]}
|
|
@@ -23,7 +23,14 @@ function AccordionItem({ title, children, className, ...rest }) {
|
|
|
23
23
|
}
|
|
24
24
|
)
|
|
25
25
|
] }) }),
|
|
26
|
-
/* @__PURE__ */ jsx(
|
|
26
|
+
/* @__PURE__ */ jsx(
|
|
27
|
+
AccordionPrimitive.Content,
|
|
28
|
+
{
|
|
29
|
+
"data-m22-animated": true,
|
|
30
|
+
className: "overflow-hidden text-sm leading-relaxed text-(--ink-2) data-[state=closed]:animate-[m22-accordion-up_var(--duration-base)_var(--ease)] data-[state=open]:animate-[m22-accordion-down_var(--duration-base)_var(--ease)]",
|
|
31
|
+
children: /* @__PURE__ */ jsx("div", { className: "pb-4 pe-8", children })
|
|
32
|
+
}
|
|
33
|
+
)
|
|
27
34
|
]
|
|
28
35
|
}
|
|
29
36
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Accordion/Accordion.tsx"],"sourcesContent":["'use client'\n\nimport * as AccordionPrimitive from '@radix-ui/react-accordion'\nimport { Plus } from 'lucide-react'\nimport type { ComponentProps, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\n/**\n * Radix root, re-exported. Pass `type=\"single\" collapsible` for an FAQ and\n * `type=\"multiple\"` for a settings stack; Radix's own discriminated union then\n * types `value` correctly for each.\n */\nexport const Accordion = AccordionPrimitive.Root\n\nexport interface AccordionItemProps\n extends Omit<ComponentProps<typeof AccordionPrimitive.Item>, 'children' | 'title'> {\n /** The row's summary — what the reader clicks. Named `title` rather than\n * inherited from the DOM attribute of the same name, which is a tooltip. */\n title: ReactNode\n children: ReactNode\n}\n\n/**\n * One disclosure row: a hairline-ruled trigger and its panel.\n *\n * The marker is a plus that rotates into a minus, not a chevron. A chevron says\n * \"there is more below\"; a plus says \"this opens\" — and in a stack of rows the\n * difference decides whether the reader expects navigation or expansion.\n *\n * The panel animates on Radix's own `--radix-accordion-content-height`, so it\n * opens to its real height without measuring anything at the call site.\n *\n * @example\n * <Accordion type=\"single\" collapsible>\n * <AccordionItem value=\"ship\" title=\"How do I ship it?\">Push to main.</AccordionItem>\n * </Accordion>\n */\nexport function AccordionItem({ title, children, className, ...rest }: AccordionItemProps) {\n return (\n <AccordionPrimitive.Item\n className={cn('border-b border-(--rule)', className)}\n {...rest}\n >\n <AccordionPrimitive.Header className=\"m-0\">\n <AccordionPrimitive.Trigger className=\"group flex w-full items-center justify-between gap-4 py-4 text-start font-sans text-sm text-(--ink) transition-colors duration-(--duration-fast) hover:text-(--ink-2)\">\n {title}\n <Plus\n size={16}\n strokeWidth={1.5}\n aria-hidden\n className=\"shrink-0 text-(--ink-3-aa) transition-transform duration-(--duration-base) ease-(--ease-out-expo) group-data-[state=open]:rotate-45\"\n />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n <AccordionPrimitive.Content
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Accordion/Accordion.tsx"],"sourcesContent":["'use client'\n\nimport * as AccordionPrimitive from '@radix-ui/react-accordion'\nimport { Plus } from 'lucide-react'\nimport type { ComponentProps, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\n/**\n * Radix root, re-exported. Pass `type=\"single\" collapsible` for an FAQ and\n * `type=\"multiple\"` for a settings stack; Radix's own discriminated union then\n * types `value` correctly for each.\n */\nexport const Accordion = AccordionPrimitive.Root\n\nexport interface AccordionItemProps\n extends Omit<ComponentProps<typeof AccordionPrimitive.Item>, 'children' | 'title'> {\n /** The row's summary — what the reader clicks. Named `title` rather than\n * inherited from the DOM attribute of the same name, which is a tooltip. */\n title: ReactNode\n children: ReactNode\n}\n\n/**\n * One disclosure row: a hairline-ruled trigger and its panel.\n *\n * The marker is a plus that rotates into a minus, not a chevron. A chevron says\n * \"there is more below\"; a plus says \"this opens\" — and in a stack of rows the\n * difference decides whether the reader expects navigation or expansion.\n *\n * The panel animates on Radix's own `--radix-accordion-content-height`, so it\n * opens to its real height without measuring anything at the call site.\n *\n * @example\n * <Accordion type=\"single\" collapsible>\n * <AccordionItem value=\"ship\" title=\"How do I ship it?\">Push to main.</AccordionItem>\n * </Accordion>\n */\nexport function AccordionItem({ title, children, className, ...rest }: AccordionItemProps) {\n return (\n <AccordionPrimitive.Item\n className={cn('border-b border-(--rule)', className)}\n {...rest}\n >\n <AccordionPrimitive.Header className=\"m-0\">\n <AccordionPrimitive.Trigger className=\"group flex w-full items-center justify-between gap-4 py-4 text-start font-sans text-sm text-(--ink) transition-colors duration-(--duration-fast) hover:text-(--ink-2)\">\n {title}\n <Plus\n size={16}\n strokeWidth={1.5}\n aria-hidden\n className=\"shrink-0 text-(--ink-3-aa) transition-transform duration-(--duration-base) ease-(--ease-out-expo) group-data-[state=open]:rotate-45\"\n />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n <AccordionPrimitive.Content\n data-m22-animated\n className=\"overflow-hidden text-sm leading-relaxed text-(--ink-2) data-[state=closed]:animate-[m22-accordion-up_var(--duration-base)_var(--ease)] data-[state=open]:animate-[m22-accordion-down_var(--duration-base)_var(--ease)]\"\n >\n <div className=\"pb-4 pe-8\">{children}</div>\n </AccordionPrimitive.Content>\n </AccordionPrimitive.Item>\n )\n}\n\nexport default Accordion\n"],"mappings":";AA4CQ,SAEE,KAFF;AA1CR,YAAY,wBAAwB;AACpC,SAAS,YAAY;AAErB,SAAS,UAAU;AAOZ,MAAM,YAAY,mBAAmB;AAyBrC,SAAS,cAAc,EAAE,OAAO,UAAU,WAAW,GAAG,KAAK,GAAuB;AACzF,SACE;AAAA,IAAC,mBAAmB;AAAA,IAAnB;AAAA,MACC,WAAW,GAAG,4BAA4B,SAAS;AAAA,MAClD,GAAG;AAAA,MAEJ;AAAA,4BAAC,mBAAmB,QAAnB,EAA0B,WAAU,OACnC,+BAAC,mBAAmB,SAAnB,EAA2B,WAAU,yKACnC;AAAA;AAAA,UACD;AAAA,YAAC;AAAA;AAAA,cACC,MAAM;AAAA,cACN,aAAa;AAAA,cACb,eAAW;AAAA,cACX,WAAU;AAAA;AAAA,UACZ;AAAA,WACF,GACF;AAAA,QACA;AAAA,UAAC,mBAAmB;AAAA,UAAnB;AAAA,YACC,qBAAiB;AAAA,YACjB,WAAU;AAAA,YAEV,8BAAC,SAAI,WAAU,aAAa,UAAS;AAAA;AAAA,QACvC;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,oBAAQ;","names":[]}
|
|
@@ -42,6 +42,12 @@ interface AppShellProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
42
42
|
* cannot leave. The scrim is a `<button>` for the same reason — a `<div>` with
|
|
43
43
|
* an `onClick` is not reachable by keyboard and not announced as anything.
|
|
44
44
|
*
|
|
45
|
+
* Below `md` the closed drawer is `inert`. Sliding it off-screen is a visual
|
|
46
|
+
* state and nothing more: without that attribute every link in it stays
|
|
47
|
+
* focusable and stays in the accessibility tree, so Tab from the toggle walks
|
|
48
|
+
* into a menu nobody can see. Closing it also returns focus to the toggle,
|
|
49
|
+
* because the element focus was on is the element that just left.
|
|
50
|
+
*
|
|
45
51
|
* @example
|
|
46
52
|
* <AppShell brand={<Wordmark />} sidebar={<NavItem href="/">Home</NavItem>}>
|
|
47
53
|
* <Card>…</Card>
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Menu, X } from "lucide-react";
|
|
4
|
-
import { useEffect, useId, useState } from "react";
|
|
4
|
+
import { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import { cn } from "../../lib/cn.js";
|
|
6
|
+
const DESKTOP = "(min-width: 768px)";
|
|
7
|
+
function cannotMeasure() {
|
|
8
|
+
return typeof window === "undefined" || typeof window.matchMedia !== "function";
|
|
9
|
+
}
|
|
10
|
+
function subscribeToWidth(onChange) {
|
|
11
|
+
if (cannotMeasure()) return () => {
|
|
12
|
+
};
|
|
13
|
+
const list = window.matchMedia(DESKTOP);
|
|
14
|
+
list.addEventListener("change", onChange);
|
|
15
|
+
return () => list.removeEventListener("change", onChange);
|
|
16
|
+
}
|
|
17
|
+
function readIsDrawer() {
|
|
18
|
+
if (cannotMeasure()) return false;
|
|
19
|
+
return !window.matchMedia(DESKTOP).matches;
|
|
20
|
+
}
|
|
21
|
+
function readIsDrawerOnServer() {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function useIsDrawer() {
|
|
25
|
+
return useSyncExternalStore(subscribeToWidth, readIsDrawer, readIsDrawerOnServer);
|
|
26
|
+
}
|
|
6
27
|
function AppShell({
|
|
7
28
|
sidebar,
|
|
8
29
|
topbar,
|
|
@@ -18,14 +39,20 @@ function AppShell({
|
|
|
18
39
|
}) {
|
|
19
40
|
const [open, setOpen] = useState(false);
|
|
20
41
|
const sidebarId = useId();
|
|
42
|
+
const toggle = useRef(null);
|
|
43
|
+
const isDrawer = useIsDrawer();
|
|
44
|
+
const close = useCallback(() => {
|
|
45
|
+
setOpen(false);
|
|
46
|
+
toggle.current?.focus();
|
|
47
|
+
}, []);
|
|
21
48
|
useEffect(() => {
|
|
22
49
|
if (!open) return;
|
|
23
50
|
const onKey = (event) => {
|
|
24
|
-
if (event.key === "Escape")
|
|
51
|
+
if (event.key === "Escape") close();
|
|
25
52
|
};
|
|
26
53
|
document.addEventListener("keydown", onKey);
|
|
27
54
|
return () => document.removeEventListener("keydown", onKey);
|
|
28
|
-
}, [open]);
|
|
55
|
+
}, [open, close]);
|
|
29
56
|
return /* @__PURE__ */ jsxs(
|
|
30
57
|
"div",
|
|
31
58
|
{
|
|
@@ -37,7 +64,7 @@ function AppShell({
|
|
|
37
64
|
{
|
|
38
65
|
type: "button",
|
|
39
66
|
"aria-label": closeLabel,
|
|
40
|
-
onClick:
|
|
67
|
+
onClick: close,
|
|
41
68
|
className: "fixed inset-0 z-(--z-scrim) bg-(--scrim) md:hidden"
|
|
42
69
|
}
|
|
43
70
|
),
|
|
@@ -46,6 +73,8 @@ function AppShell({
|
|
|
46
73
|
{
|
|
47
74
|
id: sidebarId,
|
|
48
75
|
"aria-label": sidebarLabel,
|
|
76
|
+
inert: isDrawer && !open,
|
|
77
|
+
"data-m22-animated": true,
|
|
49
78
|
className: cn(
|
|
50
79
|
"fixed inset-y-0 start-0 z-(--z-modal) flex w-60 flex-col border-e border-(--rule) bg-(--paper) transition-transform duration-(--duration-slow) ease-(--ease-out-expo)",
|
|
51
80
|
// The drawer slides in from the edge reading STARTS at, so in an
|
|
@@ -75,6 +104,7 @@ function AppShell({
|
|
|
75
104
|
"aria-label": open ? closeLabel : openLabel,
|
|
76
105
|
"aria-expanded": open,
|
|
77
106
|
"aria-controls": sidebarId,
|
|
107
|
+
ref: toggle,
|
|
78
108
|
onClick: () => setOpen((previous) => !previous),
|
|
79
109
|
className: "-ms-2.5 grid size-11 place-items-center rounded-(--radius) text-(--ink-2) transition-colors duration-(--duration-fast) hover:text-(--ink) md:hidden",
|
|
80
110
|
children: open ? /* @__PURE__ */ jsx(X, { size: 20, strokeWidth: 1.5, "aria-hidden": true }) : /* @__PURE__ */ jsx(Menu, { size: 20, strokeWidth: 1.5, "aria-hidden": true })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["'use client'\n\nimport { Menu, X } from 'lucide-react'\nimport { useEffect, useId, useState } from 'react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Navigation content for the sidebar (e.g. a stack of `NavItem`s). */\n sidebar: ReactNode\n /** Optional content for the sticky topbar, laid out after the mobile toggle. */\n topbar?: ReactNode\n /** Optional brand lockup pinned to the top of the sidebar. */\n brand?: ReactNode\n /**\n * Which element the content well renders as.\n *\n * `main` is right for the shell of an application, and is the default. A\n * document may contain exactly one `main` landmark, so an AppShell rendered\n * INSIDE another page — a preview on a documentation site, a screenshot\n * harness — must pass `div`, or the page has two and assistive tech can no\n * longer answer \"where is the content\".\n */\n contentAs?: 'main' | 'div'\n /**\n * Names the sidebar landmark.\n *\n * Two `complementary` landmarks with the same name cannot be told apart, and\n * a shell rendered inside another page — a preview, a screenshot harness —\n * makes exactly that pair. It is also the only way a non-English app gets a\n * landmark name its readers can read.\n */\n sidebarLabel?: string\n /** Names the navigation landmark inside the sidebar. */\n navLabel?: string\n /** The drawer toggle, closed and open. */\n openLabel?: string\n closeLabel?: string\n children: ReactNode\n}\n\n/**\n * Two columns on a desktop, one column and a drawer on a phone.\n *\n * The drawer closes on Escape as well as on the scrim, because a drawer that\n * only closes by tapping outside it strands a keyboard user inside a menu they\n * cannot leave. The scrim is a `<button>` for the same reason — a `<div>` with\n * an `onClick` is not reachable by keyboard and not announced as anything.\n *\n * @example\n * <AppShell brand={<Wordmark />} sidebar={<NavItem href=\"/\">Home</NavItem>}>\n * <Card>…</Card>\n * </AppShell>\n */\nexport function AppShell({\n sidebar,\n topbar,\n brand,\n contentAs: Content = 'main',\n sidebarLabel = 'Sidebar',\n navLabel = 'Primary',\n openLabel = 'Open navigation',\n closeLabel = 'Close navigation',\n children,\n className,\n ...rest\n}: AppShellProps) {\n const [open, setOpen] = useState(false)\n const sidebarId = useId()\n\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false)\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [open])\n\n return (\n <div\n className={cn('min-h-svh bg-(--paper) md:grid md:grid-cols-[15rem_1fr]', className)}\n {...rest}\n >\n {open && (\n <button\n type=\"button\"\n aria-label={closeLabel}\n onClick={() => setOpen(false)}\n className=\"fixed inset-0 z-(--z-scrim) bg-(--scrim) md:hidden\"\n />\n )}\n\n <aside\n id={sidebarId}\n aria-label={sidebarLabel}\n className={cn(\n 'fixed inset-y-0 start-0 z-(--z-modal) flex w-60 flex-col border-e border-(--rule) bg-(--paper) transition-transform duration-(--duration-slow) ease-(--ease-out-expo)',\n // The drawer slides in from the edge reading STARTS at, so in an\n // right-to-left document it comes from the right.\n open ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full',\n 'md:static md:z-auto md:translate-x-0',\n )}\n >\n {brand && (\n <div className=\"flex h-14 items-center border-b border-(--rule) px-5\">{brand}</div>\n )}\n <nav\n aria-label={navLabel}\n className=\"flex flex-1 flex-col gap-1 overflow-y-auto p-3 scroll-slim\"\n >\n {sidebar}\n </nav>\n </aside>\n\n <div className=\"flex min-w-0 flex-col\">\n <header className=\"sticky top-0 z-(--z-sticky) flex h-14 items-center gap-3 border-b border-(--rule) bg-(--paper)/85 px-(--page-pad) backdrop-blur\">\n <button\n type=\"button\"\n aria-label={open ? closeLabel : openLabel}\n aria-expanded={open}\n aria-controls={sidebarId}\n onClick={() => setOpen((previous) => !previous)}\n className=\"-ms-2.5 grid size-11 place-items-center rounded-(--radius) text-(--ink-2) transition-colors duration-(--duration-fast) hover:text-(--ink) md:hidden\"\n >\n {open ? <X size={20} strokeWidth={1.5} aria-hidden /> : <Menu size={20} strokeWidth={1.5} aria-hidden />}\n </button>\n {topbar}\n </header>\n\n <Content className=\"mx-auto w-full max-w-(--w-page) px-(--page-pad) py-8\">{children}</Content>\n </div>\n </div>\n )\n}\n\nexport default AppShell\n"],"mappings":";AAqFQ,cAQF,YARE;AAnFR,SAAS,MAAM,SAAS;AACxB,SAAS,WAAW,OAAO,gBAAgB;AAE3C,SAAS,UAAU;AAiDZ,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,UAAU;AAAA,EACrB,eAAe;AAAA,EACf,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,YAAY,MAAM;AAExB,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,CAAC,UAAyB;AACtC,UAAI,MAAM,QAAQ,SAAU,SAAQ,KAAK;AAAA,IAC3C;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,MAAM,SAAS,oBAAoB,WAAW,KAAK;AAAA,EAC5D,GAAG,CAAC,IAAI,CAAC;AAET,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,2DAA2D,SAAS;AAAA,MACjF,GAAG;AAAA,MAEH;AAAA,gBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAY;AAAA,YACZ,SAAS,MAAM,QAAQ,KAAK;AAAA,YAC5B,WAAU;AAAA;AAAA,QACZ;AAAA,QAGF;AAAA,UAAC;AAAA;AAAA,YACC,IAAI;AAAA,YACJ,cAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA;AAAA;AAAA,cAGA,OAAO,kBAAkB;AAAA,cACzB;AAAA,YACF;AAAA,YAEC;AAAA,uBACC,oBAAC,SAAI,WAAU,wDAAwD,iBAAM;AAAA,cAE/E;AAAA,gBAAC;AAAA;AAAA,kBACC,cAAY;AAAA,kBACZ,WAAU;AAAA,kBAET;AAAA;AAAA,cACH;AAAA;AAAA;AAAA,QACF;AAAA,QAEA,qBAAC,SAAI,WAAU,yBACb;AAAA,+BAAC,YAAO,WAAU,mIAChB;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO,aAAa;AAAA,gBAChC,iBAAe;AAAA,gBACf,iBAAe;AAAA,gBACf,SAAS,MAAM,QAAQ,CAAC,aAAa,CAAC,QAAQ;AAAA,gBAC9C,WAAU;AAAA,gBAET,iBAAO,oBAAC,KAAE,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC,IAAK,oBAAC,QAAK,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC;AAAA;AAAA,YACxG;AAAA,YACC;AAAA,aACH;AAAA,UAEA,oBAAC,WAAQ,WAAU,wDAAwD,UAAS;AAAA,WACtF;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,mBAAQ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["'use client'\n\nimport { Menu, X } from 'lucide-react'\nimport { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } from 'react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\n/**\n * The width the sidebar stops being a drawer at — `--breakpoint-md`, the same\n * 768px every `md:` variant below is compiled against. Written out rather than\n * read from the token, because a media query string is not a custom property\n * and `matchMedia` cannot resolve one.\n */\nconst DESKTOP = '(min-width: 768px)'\n\n/** True where nothing can answer the question: a server, or a DOM without it. */\nfunction cannotMeasure(): boolean {\n return typeof window === 'undefined' || typeof window.matchMedia !== 'function'\n}\n\nfunction subscribeToWidth(onChange: () => void): () => void {\n if (cannotMeasure()) return () => {}\n const list = window.matchMedia(DESKTOP)\n list.addEventListener('change', onChange)\n return () => list.removeEventListener('change', onChange)\n}\n\n/**\n * Falls back to \"not a drawer\", which is the safe answer rather than the likely\n * one: an environment that cannot measure must not take an entire application's\n * navigation out of the accessibility tree on a guess.\n */\nfunction readIsDrawer(): boolean {\n if (cannotMeasure()) return false\n return !window.matchMedia(DESKTOP).matches\n}\n\nfunction readIsDrawerOnServer(): boolean {\n return false\n}\n\n/**\n * True while the sidebar is a drawer rather than a column.\n *\n * `open` alone cannot answer this. It is false on a desktop too, where the\n * sidebar is a visible grid column — so anything keyed on `open` by itself\n * would hide the navigation of every desktop app in the package.\n *\n * `useSyncExternalStore` rather than an effect: the server has no viewport, so\n * it renders the desktop answer and React reconciles the real one on hydration\n * instead of the two silently disagreeing.\n */\nfunction useIsDrawer(): boolean {\n return useSyncExternalStore(subscribeToWidth, readIsDrawer, readIsDrawerOnServer)\n}\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Navigation content for the sidebar (e.g. a stack of `NavItem`s). */\n sidebar: ReactNode\n /** Optional content for the sticky topbar, laid out after the mobile toggle. */\n topbar?: ReactNode\n /** Optional brand lockup pinned to the top of the sidebar. */\n brand?: ReactNode\n /**\n * Which element the content well renders as.\n *\n * `main` is right for the shell of an application, and is the default. A\n * document may contain exactly one `main` landmark, so an AppShell rendered\n * INSIDE another page — a preview on a documentation site, a screenshot\n * harness — must pass `div`, or the page has two and assistive tech can no\n * longer answer \"where is the content\".\n */\n contentAs?: 'main' | 'div'\n /**\n * Names the sidebar landmark.\n *\n * Two `complementary` landmarks with the same name cannot be told apart, and\n * a shell rendered inside another page — a preview, a screenshot harness —\n * makes exactly that pair. It is also the only way a non-English app gets a\n * landmark name its readers can read.\n */\n sidebarLabel?: string\n /** Names the navigation landmark inside the sidebar. */\n navLabel?: string\n /** The drawer toggle, closed and open. */\n openLabel?: string\n closeLabel?: string\n children: ReactNode\n}\n\n/**\n * Two columns on a desktop, one column and a drawer on a phone.\n *\n * The drawer closes on Escape as well as on the scrim, because a drawer that\n * only closes by tapping outside it strands a keyboard user inside a menu they\n * cannot leave. The scrim is a `<button>` for the same reason — a `<div>` with\n * an `onClick` is not reachable by keyboard and not announced as anything.\n *\n * Below `md` the closed drawer is `inert`. Sliding it off-screen is a visual\n * state and nothing more: without that attribute every link in it stays\n * focusable and stays in the accessibility tree, so Tab from the toggle walks\n * into a menu nobody can see. Closing it also returns focus to the toggle,\n * because the element focus was on is the element that just left.\n *\n * @example\n * <AppShell brand={<Wordmark />} sidebar={<NavItem href=\"/\">Home</NavItem>}>\n * <Card>…</Card>\n * </AppShell>\n */\nexport function AppShell({\n sidebar,\n topbar,\n brand,\n contentAs: Content = 'main',\n sidebarLabel = 'Sidebar',\n navLabel = 'Primary',\n openLabel = 'Open navigation',\n closeLabel = 'Close navigation',\n children,\n className,\n ...rest\n}: AppShellProps) {\n const [open, setOpen] = useState(false)\n const sidebarId = useId()\n const toggle = useRef<HTMLButtonElement>(null)\n const isDrawer = useIsDrawer()\n\n /**\n * Both ways out, so both return focus.\n *\n * Escape used to leave the reader standing on a link that had just slid off\n * screen; the scrim is worse, because the scrim itself is the focused element\n * and it unmounts, dropping focus to `<body>`. Either way the drawer is about\n * to become inert, and focus inside an inert subtree is focus the browser\n * throws away.\n */\n const close = useCallback(() => {\n setOpen(false)\n toggle.current?.focus()\n }, [])\n\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape') close()\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [open, close])\n\n return (\n <div\n className={cn('min-h-svh bg-(--paper) md:grid md:grid-cols-[15rem_1fr]', className)}\n {...rest}\n >\n {open && (\n <button\n type=\"button\"\n aria-label={closeLabel}\n onClick={close}\n className=\"fixed inset-0 z-(--z-scrim) bg-(--scrim) md:hidden\"\n />\n )}\n\n <aside\n id={sidebarId}\n aria-label={sidebarLabel}\n // Only below `md`, and only while it is shut. Above the breakpoint this\n // element is the page's navigation column, and inert would delete it.\n inert={isDrawer && !open}\n // The transition is motion like any other, and the reduced-motion rule\n // in keyframes.css matches on this attribute.\n data-m22-animated\n className={cn(\n 'fixed inset-y-0 start-0 z-(--z-modal) flex w-60 flex-col border-e border-(--rule) bg-(--paper) transition-transform duration-(--duration-slow) ease-(--ease-out-expo)',\n // The drawer slides in from the edge reading STARTS at, so in an\n // right-to-left document it comes from the right.\n open ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full',\n 'md:static md:z-auto md:translate-x-0',\n )}\n >\n {brand && (\n <div className=\"flex h-14 items-center border-b border-(--rule) px-5\">{brand}</div>\n )}\n <nav\n aria-label={navLabel}\n className=\"flex flex-1 flex-col gap-1 overflow-y-auto p-3 scroll-slim\"\n >\n {sidebar}\n </nav>\n </aside>\n\n <div className=\"flex min-w-0 flex-col\">\n <header className=\"sticky top-0 z-(--z-sticky) flex h-14 items-center gap-3 border-b border-(--rule) bg-(--paper)/85 px-(--page-pad) backdrop-blur\">\n <button\n type=\"button\"\n aria-label={open ? closeLabel : openLabel}\n aria-expanded={open}\n aria-controls={sidebarId}\n ref={toggle}\n onClick={() => setOpen((previous) => !previous)}\n className=\"-ms-2.5 grid size-11 place-items-center rounded-(--radius) text-(--ink-2) transition-colors duration-(--duration-fast) hover:text-(--ink) md:hidden\"\n >\n {open ? <X size={20} strokeWidth={1.5} aria-hidden /> : <Menu size={20} strokeWidth={1.5} aria-hidden />}\n </button>\n {topbar}\n </header>\n\n <Content className=\"mx-auto w-full max-w-(--w-page) px-(--page-pad) py-8\">{children}</Content>\n </div>\n </div>\n )\n}\n\nexport default AppShell\n"],"mappings":"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","names":[]}
|
|
@@ -32,10 +32,17 @@ interface ArticleProps extends HTMLAttributes<HTMLElement> {
|
|
|
32
32
|
* hand-rolling a prose stylesheet is three prose stylesheets that drift.
|
|
33
33
|
*
|
|
34
34
|
* Composable either way: pass `html` for a rendered string, or children for
|
|
35
|
-
* real elements
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* the
|
|
35
|
+
* real elements — one or the other, never both. `html` wins when both arrive,
|
|
36
|
+
* and says so in development: `html=""` is still `html`, which made losing a
|
|
37
|
+
* page of children a thing that could happen without a word. A post that mixes
|
|
38
|
+
* the two — prose with a `Diagram` dropped into the middle — renders its blocks
|
|
39
|
+
* in order and gives each one an `Article`. Inside one, these rules BEAT a component's utilities:
|
|
40
|
+
* `article.css` is imported unlayered while Tailwind's utilities sit in
|
|
41
|
+
* `@layer utilities`, and an unlayered rule wins over a layered one whatever
|
|
42
|
+
* either one's specificity is. That is the mechanism rather than an accident —
|
|
43
|
+
* it is what lets a `Markdown` paragraph, a `Text` carrying `m-0`, give its
|
|
44
|
+
* margin up to the article's rhythm. A component that has to hold a property
|
|
45
|
+
* inside an article needs an inline style, not a class.
|
|
39
46
|
*
|
|
40
47
|
* @example
|
|
41
48
|
* <Article html={renderedMarkdown} />
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { DEV, warn } from "../../lib/warn.js";
|
|
3
|
+
function rendersSomething(children) {
|
|
4
|
+
return children !== void 0 && children !== null && children !== false && children !== "";
|
|
5
|
+
}
|
|
2
6
|
function Article({ html, as: Comp = "article", className, children, ...rest }) {
|
|
3
7
|
if (html !== void 0) {
|
|
8
|
+
if (DEV && rendersSomething(children)) {
|
|
9
|
+
warn({
|
|
10
|
+
code: "ARTICLE_HTML_AND_CHILDREN",
|
|
11
|
+
problem: "Article was given html and children together, so the children are not rendered at all. html is written with dangerouslySetInnerHTML, and there is no wrapper that could hold both \u2014 the rhythm is a child combinator, so anything put around the rendered string costs every block inside it its spacing.",
|
|
12
|
+
field: "html",
|
|
13
|
+
fix: "Render the two in order as two Articles, or drop whichever of html and children is not the content. An empty html string still counts as html.",
|
|
14
|
+
component: "Article"
|
|
15
|
+
});
|
|
16
|
+
}
|
|
4
17
|
return /* @__PURE__ */ jsx(
|
|
5
18
|
Comp,
|
|
6
19
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Article/Article.tsx"],"sourcesContent":["import type { HTMLAttributes } from 'react'\n\nexport interface ArticleProps extends HTMLAttributes<HTMLElement> {\n /**\n * Rendered HTML from a Markdown or MDX pipeline.\n *\n * Trusted markup only. This sets `dangerouslySetInnerHTML`, so it must come\n * from content you control — a repository's own posts, a CMS you author —\n * and never from a reader. Untrusted Markdown has to be sanitised before it\n * reaches here; that is a pipeline decision, not a component one, and a\n * sanitiser bolted on inside a styling primitive would be the wrong place to\n * make it and the easiest place to get it wrong.\n */\n html?: string\n /** The element to render. `article` by default; pass `div` for a fragment. */\n as?: 'article' | 'div' | 'section'\n}\n\n/**\n * The long-form reading surface.\n *\n * Everything a Markdown pipeline can emit — headings, prose, lists, tables,\n * quotations, code, figures, footnotes, MathML — set in this system's type,\n * colour and rules. The styles live in `article.css` rather than in a class\n * string, because the input is not JSX: there is no component to hang a class\n * on when the markup arrived as a string, so the contract has to be the element\n * names themselves.\n *\n * That file is published on its own as `@misoto22/design/article.css`, so a\n * site with its own Markdown pipeline can take the reading surface without\n * taking the components — which is the case this exists for. Three sites\n * hand-rolling a prose stylesheet is three prose stylesheets that drift.\n *\n * Composable either way: pass `html` for a rendered string, or children for\n * real elements. A post that mixes the two — prose with a `Diagram` dropped
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Article/Article.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from 'react'\nimport { DEV, warn } from '../../lib/warn'\n\nexport interface ArticleProps extends HTMLAttributes<HTMLElement> {\n /**\n * Rendered HTML from a Markdown or MDX pipeline.\n *\n * Trusted markup only. This sets `dangerouslySetInnerHTML`, so it must come\n * from content you control — a repository's own posts, a CMS you author —\n * and never from a reader. Untrusted Markdown has to be sanitised before it\n * reaches here; that is a pipeline decision, not a component one, and a\n * sanitiser bolted on inside a styling primitive would be the wrong place to\n * make it and the easiest place to get it wrong.\n */\n html?: string\n /** The element to render. `article` by default; pass `div` for a fragment. */\n as?: 'article' | 'div' | 'section'\n}\n\n/**\n * Whether `children` would actually put something on the page.\n *\n * `null`, `false` and `''` are what a conditional child evaluates to when it\n * decided not to render, so none of them is a second Article and none of them\n * is worth a warning.\n */\nfunction rendersSomething(children: ReactNode): boolean {\n return children !== undefined && children !== null && children !== false && children !== ''\n}\n\n/**\n * The long-form reading surface.\n *\n * Everything a Markdown pipeline can emit — headings, prose, lists, tables,\n * quotations, code, figures, footnotes, MathML — set in this system's type,\n * colour and rules. The styles live in `article.css` rather than in a class\n * string, because the input is not JSX: there is no component to hang a class\n * on when the markup arrived as a string, so the contract has to be the element\n * names themselves.\n *\n * That file is published on its own as `@misoto22/design/article.css`, so a\n * site with its own Markdown pipeline can take the reading surface without\n * taking the components — which is the case this exists for. Three sites\n * hand-rolling a prose stylesheet is three prose stylesheets that drift.\n *\n * Composable either way: pass `html` for a rendered string, or children for\n * real elements — one or the other, never both. `html` wins when both arrive,\n * and says so in development: `html=\"\"` is still `html`, which made losing a\n * page of children a thing that could happen without a word. A post that mixes\n * the two — prose with a `Diagram` dropped into the middle — renders its blocks\n * in order and gives each one an `Article`. Inside one, these rules BEAT a component's utilities:\n * `article.css` is imported unlayered while Tailwind's utilities sit in\n * `@layer utilities`, and an unlayered rule wins over a layered one whatever\n * either one's specificity is. That is the mechanism rather than an accident —\n * it is what lets a `Markdown` paragraph, a `Text` carrying `m-0`, give its\n * margin up to the article's rhythm. A component that has to hold a property\n * inside an article needs an inline style, not a class.\n *\n * @example\n * <Article html={renderedMarkdown} />\n * @example\n * <Article>\n * <h1>The White Reset</h1>\n * <p className=\"lead\">A monochrome system for software and writing.</p>\n * <Diagram spec={spec} />\n * </Article>\n */\nexport function Article({ html, as: Comp = 'article', className, children, ...rest }: ArticleProps) {\n // One or the other, never both. The rhythm rules are child combinators, so a\n // wrapper around the rendered string — even a `display: contents` one, which\n // removes the box but not the DOM node — would take every heading and\n // paragraph out of the article's direct children and cost them their\n // spacing. A page with both is a page with two Articles, and it says so\n // below rather than dropping half the page without a word.\n if (html !== undefined) {\n if (DEV && rendersSomething(children)) {\n warn({\n code: 'ARTICLE_HTML_AND_CHILDREN',\n problem:\n 'Article was given html and children together, so the children are not rendered at all. html is written with dangerouslySetInnerHTML, and there is no wrapper that could hold both — the rhythm is a child combinator, so anything put around the rendered string costs every block inside it its spacing.',\n field: 'html',\n fix: 'Render the two in order as two Articles, or drop whichever of html and children is not the content. An empty html string still counts as html.',\n component: 'Article',\n })\n }\n\n return (\n <Comp\n data-m22-article\n className={className}\n dangerouslySetInnerHTML={{ __html: html }}\n {...rest}\n />\n )\n }\n\n return (\n <Comp data-m22-article className={className} {...rest}>\n {children}\n </Comp>\n )\n}\n\nexport default Article\n"],"mappings":"AAuFM;AAtFN,SAAS,KAAK,YAAY;AAyB1B,SAAS,iBAAiB,UAA8B;AACtD,SAAO,aAAa,UAAa,aAAa,QAAQ,aAAa,SAAS,aAAa;AAC3F;AAuCO,SAAS,QAAQ,EAAE,MAAM,IAAI,OAAO,WAAW,WAAW,UAAU,GAAG,KAAK,GAAiB;AAOlG,MAAI,SAAS,QAAW;AACtB,QAAI,OAAO,iBAAiB,QAAQ,GAAG;AACrC,WAAK;AAAA,QACH,MAAM;AAAA,QACN,SACE;AAAA,QACF,OAAO;AAAA,QACP,KAAK;AAAA,QACL,WAAW;AAAA,MACb,CAAC;AAAA,IACH;AAEA,WACE;AAAA,MAAC;AAAA;AAAA,QACC,oBAAgB;AAAA,QAChB;AAAA,QACA,yBAAyB,EAAE,QAAQ,KAAK;AAAA,QACvC,GAAG;AAAA;AAAA,IACN;AAAA,EAEJ;AAEA,SACE,oBAAC,QAAK,oBAAgB,MAAC,WAAuB,GAAG,MAC9C,UACH;AAEJ;AAEA,IAAO,kBAAQ;","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface AspectRatioProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/**
|
|
6
|
+
* Width over height. A number (`16 / 9`) or the CSS form (`'16 / 9'`).
|
|
7
|
+
*
|
|
8
|
+
* It is a style rather than a class because Tailwind can only generate what
|
|
9
|
+
* it can read verbatim in the source, and this value arrives at runtime.
|
|
10
|
+
*/
|
|
11
|
+
ratio?: number | string;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A box that keeps its shape whatever is inside it.
|
|
16
|
+
*
|
|
17
|
+
* The one layout primitive that is genuinely hard to hand-roll. The `padding-
|
|
18
|
+
* top: 56.25%` trick everyone reaches for is a percentage of the WIDTH, which
|
|
19
|
+
* is why it works at all and also why it silently breaks the moment the box is
|
|
20
|
+
* a flex or grid child — and it takes the element's own padding with it. The
|
|
21
|
+
* modern `aspect-ratio` property does the same job in one line, and only holds
|
|
22
|
+
* if nothing inside forces a height, which is what the absolute positioning
|
|
23
|
+
* below guarantees.
|
|
24
|
+
*
|
|
25
|
+
* So: the box declares the ratio, and every direct child is stretched to fill
|
|
26
|
+
* it and taken out of flow. That means a child with no intrinsic size at all —
|
|
27
|
+
* an empty `<div>`, a map that measures its container, a skeleton — still gets
|
|
28
|
+
* the full box, and an `<img>` or `<video>` is cropped to cover it rather than
|
|
29
|
+
* letterboxed. Content that must not be cropped should set `object-contain` on
|
|
30
|
+
* itself.
|
|
31
|
+
*
|
|
32
|
+
* Reach for this when the height must be known before the content loads: a
|
|
33
|
+
* media grid that would otherwise reflow every time an image arrives is the
|
|
34
|
+
* usual case, and that reflow is the layout shift a Core Web Vitals score is
|
|
35
|
+
* measuring.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* <AspectRatio ratio={16 / 9}><img src={cover} alt="" /></AspectRatio>
|
|
39
|
+
* @example
|
|
40
|
+
* <AspectRatio ratio="1 / 1" className="rounded-(--radius) border border-(--rule-2)">
|
|
41
|
+
* <Skeleton />
|
|
42
|
+
* </AspectRatio>
|
|
43
|
+
*/
|
|
44
|
+
declare function AspectRatio({ ratio, className, style, children, ...rest }: AspectRatioProps): react.JSX.Element;
|
|
45
|
+
|
|
46
|
+
export { AspectRatio, type AspectRatioProps, AspectRatio as default };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../../lib/cn.js";
|
|
3
|
+
function AspectRatio({
|
|
4
|
+
ratio = 16 / 9,
|
|
5
|
+
className,
|
|
6
|
+
style,
|
|
7
|
+
children,
|
|
8
|
+
...rest
|
|
9
|
+
}) {
|
|
10
|
+
const box = { aspectRatio: String(ratio), ...style };
|
|
11
|
+
return /* @__PURE__ */ jsx(
|
|
12
|
+
"div",
|
|
13
|
+
{
|
|
14
|
+
style: box,
|
|
15
|
+
className: cn(
|
|
16
|
+
"relative w-full overflow-hidden",
|
|
17
|
+
// Every direct child fills the box, so nothing inside can push the
|
|
18
|
+
// height around and break the ratio the box just declared.
|
|
19
|
+
"[&>*]:absolute [&>*]:inset-0 [&>*]:size-full",
|
|
20
|
+
"[&>img]:object-cover [&>video]:object-cover",
|
|
21
|
+
className
|
|
22
|
+
),
|
|
23
|
+
...rest,
|
|
24
|
+
children
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
var AspectRatio_default = AspectRatio;
|
|
29
|
+
export {
|
|
30
|
+
AspectRatio,
|
|
31
|
+
AspectRatio_default as default
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=AspectRatio.js.map
|