@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
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useImperativeHandle,
|
|
8
|
+
useRef,
|
|
9
|
+
useState
|
|
10
|
+
} from "react";
|
|
11
|
+
import { cn } from "../../lib/cn.js";
|
|
12
|
+
const MIN_SCALE = 0.35;
|
|
13
|
+
const MAX_SCALE = 4;
|
|
14
|
+
const STEP = 1.25;
|
|
15
|
+
function DiagramCanvas({
|
|
16
|
+
children,
|
|
17
|
+
className,
|
|
18
|
+
height = "24rem",
|
|
19
|
+
onViewChange,
|
|
20
|
+
controls = true,
|
|
21
|
+
label = "Diagram canvas",
|
|
22
|
+
ref
|
|
23
|
+
}) {
|
|
24
|
+
const uid = useId().replace(/:/g, "");
|
|
25
|
+
const frame = useRef(null);
|
|
26
|
+
const [view, setView] = useState({ scale: 1, x: 0, y: 0 });
|
|
27
|
+
const drag = useRef(null);
|
|
28
|
+
const measure = useCallback(() => {
|
|
29
|
+
const box = frame.current?.getBoundingClientRect();
|
|
30
|
+
return box && box.width > 0 ? { width: box.width, height: box.height } : void 0;
|
|
31
|
+
}, []);
|
|
32
|
+
const apply = useCallback(
|
|
33
|
+
(next) => {
|
|
34
|
+
const measured = { ...next, frame: measure() };
|
|
35
|
+
setView(measured);
|
|
36
|
+
onViewChange?.(measured);
|
|
37
|
+
},
|
|
38
|
+
[measure, onViewChange]
|
|
39
|
+
);
|
|
40
|
+
const zoomAbout = useCallback(
|
|
41
|
+
(factor, originX, originY) => {
|
|
42
|
+
const box = frame.current?.getBoundingClientRect();
|
|
43
|
+
const cx = originX ?? (box ? box.width / 2 : 0);
|
|
44
|
+
const cy = originY ?? (box ? box.height / 2 : 0);
|
|
45
|
+
setView((current) => {
|
|
46
|
+
const scale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, current.scale * factor));
|
|
47
|
+
const ratio = scale / current.scale;
|
|
48
|
+
const next = {
|
|
49
|
+
scale,
|
|
50
|
+
x: cx - (cx - current.x) * ratio,
|
|
51
|
+
y: cy - (cy - current.y) * ratio,
|
|
52
|
+
frame: measure()
|
|
53
|
+
};
|
|
54
|
+
onViewChange?.(next);
|
|
55
|
+
return next;
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
[measure, onViewChange]
|
|
59
|
+
);
|
|
60
|
+
useImperativeHandle(
|
|
61
|
+
ref,
|
|
62
|
+
() => ({
|
|
63
|
+
zoomIn: () => zoomAbout(STEP),
|
|
64
|
+
zoomOut: () => zoomAbout(1 / STEP),
|
|
65
|
+
reset: () => apply({ scale: 1, x: 0, y: 0 }),
|
|
66
|
+
centerOn: (x, y) => {
|
|
67
|
+
const box = frame.current?.getBoundingClientRect();
|
|
68
|
+
if (!box) return;
|
|
69
|
+
setView((current) => {
|
|
70
|
+
const next = {
|
|
71
|
+
...current,
|
|
72
|
+
x: box.width / 2 - x * current.scale,
|
|
73
|
+
y: box.height / 2 - y * current.scale,
|
|
74
|
+
frame: measure()
|
|
75
|
+
};
|
|
76
|
+
onViewChange?.(next);
|
|
77
|
+
return next;
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
view
|
|
81
|
+
}),
|
|
82
|
+
[apply, measure, onViewChange, view, zoomAbout]
|
|
83
|
+
);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const node = frame.current;
|
|
86
|
+
if (!node) return;
|
|
87
|
+
const onWheel = (event) => {
|
|
88
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
const box = node.getBoundingClientRect();
|
|
91
|
+
zoomAbout(
|
|
92
|
+
event.deltaY < 0 ? STEP : 1 / STEP,
|
|
93
|
+
event.clientX - box.left,
|
|
94
|
+
event.clientY - box.top
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
node.addEventListener("wheel", onWheel, { passive: false });
|
|
98
|
+
return () => node.removeEventListener("wheel", onWheel);
|
|
99
|
+
}, [zoomAbout]);
|
|
100
|
+
const onPointerDown = (event) => {
|
|
101
|
+
if (event.button !== 0) return;
|
|
102
|
+
drag.current = { pointer: event.pointerId, x: event.clientX - view.x, y: event.clientY - view.y };
|
|
103
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
104
|
+
};
|
|
105
|
+
const onPointerMove = (event) => {
|
|
106
|
+
const state = drag.current;
|
|
107
|
+
if (!state || state.pointer !== event.pointerId) return;
|
|
108
|
+
apply({ ...view, x: event.clientX - state.x, y: event.clientY - state.y });
|
|
109
|
+
};
|
|
110
|
+
const endDrag = (event) => {
|
|
111
|
+
if (drag.current?.pointer !== event.pointerId) return;
|
|
112
|
+
drag.current = null;
|
|
113
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
114
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
return /* @__PURE__ */ jsxs(
|
|
118
|
+
"div",
|
|
119
|
+
{
|
|
120
|
+
className: cn(
|
|
121
|
+
"relative overflow-hidden rounded-(--radius-lg) border border-(--rule) bg-(--diagram-surface)",
|
|
122
|
+
className
|
|
123
|
+
),
|
|
124
|
+
style: { height },
|
|
125
|
+
children: [
|
|
126
|
+
/* @__PURE__ */ jsx(
|
|
127
|
+
"div",
|
|
128
|
+
{
|
|
129
|
+
ref: frame,
|
|
130
|
+
role: "group",
|
|
131
|
+
"aria-label": label,
|
|
132
|
+
"aria-describedby": `${uid}-hint`,
|
|
133
|
+
tabIndex: 0,
|
|
134
|
+
className: "size-full cursor-grab touch-none select-none outline-none focus-visible:ring-2 focus-visible:ring-(--ring) focus-visible:ring-inset active:cursor-grabbing",
|
|
135
|
+
onPointerDown,
|
|
136
|
+
onPointerMove,
|
|
137
|
+
onPointerUp: endDrag,
|
|
138
|
+
onPointerCancel: endDrag,
|
|
139
|
+
onKeyDown: (event) => {
|
|
140
|
+
const nudge = event.shiftKey ? 80 : 24;
|
|
141
|
+
switch (event.key) {
|
|
142
|
+
case "+":
|
|
143
|
+
case "=":
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
zoomAbout(STEP);
|
|
146
|
+
break;
|
|
147
|
+
case "-":
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
zoomAbout(1 / STEP);
|
|
150
|
+
break;
|
|
151
|
+
case "0":
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
apply({ scale: 1, x: 0, y: 0 });
|
|
154
|
+
break;
|
|
155
|
+
case "ArrowLeft":
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
apply({ ...view, x: view.x + nudge });
|
|
158
|
+
break;
|
|
159
|
+
case "ArrowRight":
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
apply({ ...view, x: view.x - nudge });
|
|
162
|
+
break;
|
|
163
|
+
case "ArrowUp":
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
apply({ ...view, y: view.y + nudge });
|
|
166
|
+
break;
|
|
167
|
+
case "ArrowDown":
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
apply({ ...view, y: view.y - nudge });
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
children: /* @__PURE__ */ jsx(
|
|
176
|
+
"div",
|
|
177
|
+
{
|
|
178
|
+
className: "origin-top-left will-change-transform",
|
|
179
|
+
style: { transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})` },
|
|
180
|
+
"data-diagram-stage": "",
|
|
181
|
+
children
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
),
|
|
186
|
+
/* @__PURE__ */ jsx("p", { id: `${uid}-hint`, className: "sr-only", children: "Drag to pan. Plus and minus zoom, zero resets, the arrow keys pan." }),
|
|
187
|
+
controls && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-3 end-3 flex items-center gap-px rounded-(--radius) border border-(--rule-2) bg-(--panel-bg) p-0.5", children: [
|
|
188
|
+
/* @__PURE__ */ jsx(CanvasButton, { label: "Zoom out", onClick: () => zoomAbout(1 / STEP), children: /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u2212" }) }),
|
|
189
|
+
/* @__PURE__ */ jsxs(
|
|
190
|
+
"button",
|
|
191
|
+
{
|
|
192
|
+
type: "button",
|
|
193
|
+
onClick: () => apply({ scale: 1, x: 0, y: 0 }),
|
|
194
|
+
"aria-label": "Reset the view",
|
|
195
|
+
className: "min-w-12 rounded-(--radius-xs) px-1.5 py-1 mono-meta text-(--ink-2) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--ring)",
|
|
196
|
+
children: [
|
|
197
|
+
Math.round(view.scale * 100),
|
|
198
|
+
"%"
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
),
|
|
202
|
+
/* @__PURE__ */ jsx(CanvasButton, { label: "Zoom in", onClick: () => zoomAbout(STEP), children: /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "+" }) })
|
|
203
|
+
] })
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
function CanvasButton({
|
|
209
|
+
label,
|
|
210
|
+
onClick,
|
|
211
|
+
children
|
|
212
|
+
}) {
|
|
213
|
+
return /* @__PURE__ */ jsx(
|
|
214
|
+
"button",
|
|
215
|
+
{
|
|
216
|
+
type: "button",
|
|
217
|
+
onClick,
|
|
218
|
+
"aria-label": label,
|
|
219
|
+
className: "flex size-7 items-center justify-center rounded-(--radius-xs) font-mono text-[13px] leading-none text-(--ink-2) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--ring)",
|
|
220
|
+
children
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
var DiagramCanvas_default = DiagramCanvas;
|
|
225
|
+
export {
|
|
226
|
+
DiagramCanvas,
|
|
227
|
+
DiagramCanvas_default as default
|
|
228
|
+
};
|
|
229
|
+
//# sourceMappingURL=DiagramCanvas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/diagrams/DiagramCanvas/DiagramCanvas.tsx"],"sourcesContent":["'use client'\n\nimport {\n useCallback,\n useEffect,\n useId,\n useImperativeHandle,\n useRef,\n useState,\n type PointerEvent as ReactPointerEvent,\n type ReactNode,\n type Ref,\n} from 'react'\nimport { cn } from '../../lib/cn'\n\nconst MIN_SCALE = 0.35\nconst MAX_SCALE = 4\nconst STEP = 1.25\n\nexport interface CanvasView {\n scale: number\n x: number\n y: number\n /**\n * How big the frame was, in CSS pixels, when this view was reported.\n *\n * Carried on the view because everything that consumes a view needs it and\n * nothing outside this component can measure it: a `DiagramMinimap`'s\n * viewport rectangle is the frame's size over the zoom, and a caller who had\n * to hand-declare that number was hand-declaring the one number that makes\n * the map lie. Absent until the frame has been measured — the first render\n * has no box yet — which is exactly when a map should draw nothing.\n */\n frame?: { width: number; height: number }\n}\n\n/** What a caller can do to the canvas from outside it. */\nexport interface DiagramCanvasHandle {\n zoomIn: () => void\n zoomOut: () => void\n reset: () => void\n /** Pans so a point in CONTENT coordinates sits in the middle of the frame. */\n centerOn: (x: number, y: number) => void\n view: CanvasView\n}\n\nexport interface DiagramCanvasProps {\n children: ReactNode\n className?: string\n /** How tall the frame is. Anything CSS accepts. */\n height?: string\n /** Told about every view change, for a minimap or a percentage readout. */\n onViewChange?: (view: CanvasView) => void\n /** Hides the built-in zoom controls, for a caller supplying its own. */\n controls?: boolean\n /** Names the region for assistive tech. */\n label?: string\n ref?: Ref<DiagramCanvasHandle>\n}\n\n/**\n * A frame that a picture larger than it can be moved around inside.\n *\n * Pan with a drag, zoom with the controls or with ⌘/Ctrl and the wheel, and\n * reset with a key. That is the whole of it — this is deliberately a VIEWPORT\n * and not a diagram editor: nothing here knows what a node is, so it works for\n * any oversized figure, an SVG, an image, a table that will not fold.\n *\n * Three decisions here are not obvious.\n *\n * **A plain wheel scrolls the page, not the diagram.** A canvas that swallows\n * the wheel is a scroll trap: a reader flicking down an article hits the figure\n * and the page stops moving for no reason they can see. Zooming needs the\n * modifier — which is also the platform gesture for zoom everywhere else, and\n * is what a trackpad pinch already sends.\n *\n * **The transform is on a wrapper, not on the content.** The child keeps its\n * own coordinate space, so a figure inside can still be measured, exported and\n * read by anything that walks it. Scaling the child directly would make every\n * `getBoundingClientRect` inside it a lie.\n *\n * **Keyboard first, and the frame is a real tab stop.** `+` / `-` / `0` and the\n * arrow keys move the view; the frame takes focus so they can be pressed at\n * all. A canvas that only answers a drag is a canvas half the readers cannot\n * operate.\n *\n * @example\n * <DiagramCanvas height=\"26rem\" label=\"Request path\">\n * <ArchitectureFigure spec={spec} heading={false} legend=\"hidden\" cards={false} />\n * </DiagramCanvas>\n */\nexport function DiagramCanvas({\n children,\n className,\n height = '24rem',\n onViewChange,\n controls = true,\n label = 'Diagram canvas',\n ref,\n}: DiagramCanvasProps) {\n const uid = useId().replace(/:/g, '')\n const frame = useRef<HTMLDivElement>(null)\n const [view, setView] = useState<CanvasView>({ scale: 1, x: 0, y: 0 })\n const drag = useRef<{ pointer: number; x: number; y: number } | null>(null)\n\n /** The frame's own size, or nothing when it has not been laid out yet. */\n const measure = useCallback(() => {\n const box = frame.current?.getBoundingClientRect()\n return box && box.width > 0 ? { width: box.width, height: box.height } : undefined\n }, [])\n\n const apply = useCallback(\n (next: CanvasView) => {\n const measured = { ...next, frame: measure() }\n setView(measured)\n onViewChange?.(measured)\n },\n [measure, onViewChange],\n )\n\n /**\n * Zooms about a fixed point in FRAME coordinates.\n *\n * The arithmetic is what makes the pointer stay on the thing it is over:\n * the content point under the cursor must land back under the cursor, so the\n * offset moves by the difference the scale change opened up. Without it, a\n * zoom drifts toward the origin and a reader chases the node they were\n * looking at.\n */\n const zoomAbout = useCallback(\n (factor: number, originX?: number, originY?: number) => {\n const box = frame.current?.getBoundingClientRect()\n const cx = originX ?? (box ? box.width / 2 : 0)\n const cy = originY ?? (box ? box.height / 2 : 0)\n\n setView((current) => {\n const scale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, current.scale * factor))\n const ratio = scale / current.scale\n const next = {\n scale,\n x: cx - (cx - current.x) * ratio,\n y: cy - (cy - current.y) * ratio,\n frame: measure(),\n }\n onViewChange?.(next)\n return next\n })\n },\n [measure, onViewChange],\n )\n\n useImperativeHandle(\n ref,\n () => ({\n zoomIn: () => zoomAbout(STEP),\n zoomOut: () => zoomAbout(1 / STEP),\n reset: () => apply({ scale: 1, x: 0, y: 0 }),\n centerOn: (x: number, y: number) => {\n const box = frame.current?.getBoundingClientRect()\n if (!box) return\n setView((current) => {\n const next = {\n ...current,\n x: box.width / 2 - x * current.scale,\n y: box.height / 2 - y * current.scale,\n frame: measure(),\n }\n onViewChange?.(next)\n return next\n })\n },\n view,\n }),\n [apply, measure, onViewChange, view, zoomAbout],\n )\n\n // Registered imperatively because React marks `wheel` passive, and a passive\n // listener cannot call preventDefault — so the modifier-wheel zoom would also\n // scroll the page underneath it.\n useEffect(() => {\n const node = frame.current\n if (!node) return\n\n const onWheel = (event: WheelEvent) => {\n if (!event.ctrlKey && !event.metaKey) return\n event.preventDefault()\n const box = node.getBoundingClientRect()\n zoomAbout(\n event.deltaY < 0 ? STEP : 1 / STEP,\n event.clientX - box.left,\n event.clientY - box.top,\n )\n }\n\n node.addEventListener('wheel', onWheel, { passive: false })\n return () => node.removeEventListener('wheel', onWheel)\n }, [zoomAbout])\n\n const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n if (event.button !== 0) return\n drag.current = { pointer: event.pointerId, x: event.clientX - view.x, y: event.clientY - view.y }\n event.currentTarget.setPointerCapture(event.pointerId)\n }\n\n const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n const state = drag.current\n if (!state || state.pointer !== event.pointerId) return\n apply({ ...view, x: event.clientX - state.x, y: event.clientY - state.y })\n }\n\n const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => {\n if (drag.current?.pointer !== event.pointerId) return\n drag.current = null\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId)\n }\n }\n\n return (\n <div\n className={cn(\n 'relative overflow-hidden rounded-(--radius-lg) border border-(--rule) bg-(--diagram-surface)',\n className,\n )}\n style={{ height }}\n >\n <div\n ref={frame}\n role=\"group\"\n aria-label={label}\n aria-describedby={`${uid}-hint`}\n tabIndex={0}\n className=\"size-full cursor-grab touch-none select-none outline-none focus-visible:ring-2 focus-visible:ring-(--ring) focus-visible:ring-inset active:cursor-grabbing\"\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={endDrag}\n onPointerCancel={endDrag}\n onKeyDown={(event) => {\n const nudge = event.shiftKey ? 80 : 24\n switch (event.key) {\n case '+':\n case '=':\n event.preventDefault()\n zoomAbout(STEP)\n break\n case '-':\n event.preventDefault()\n zoomAbout(1 / STEP)\n break\n case '0':\n event.preventDefault()\n apply({ scale: 1, x: 0, y: 0 })\n break\n case 'ArrowLeft':\n event.preventDefault()\n apply({ ...view, x: view.x + nudge })\n break\n case 'ArrowRight':\n event.preventDefault()\n apply({ ...view, x: view.x - nudge })\n break\n case 'ArrowUp':\n event.preventDefault()\n apply({ ...view, y: view.y + nudge })\n break\n case 'ArrowDown':\n event.preventDefault()\n apply({ ...view, y: view.y - nudge })\n break\n default:\n break\n }\n }}\n >\n <div\n className=\"origin-top-left will-change-transform\"\n style={{ transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})` }}\n data-diagram-stage=\"\"\n >\n {children}\n </div>\n </div>\n\n <p id={`${uid}-hint`} className=\"sr-only\">\n Drag to pan. Plus and minus zoom, zero resets, the arrow keys pan.\n </p>\n\n {controls && (\n <div className=\"absolute bottom-3 end-3 flex items-center gap-px rounded-(--radius) border border-(--rule-2) bg-(--panel-bg) p-0.5\">\n <CanvasButton label=\"Zoom out\" onClick={() => zoomAbout(1 / STEP)}>\n <span aria-hidden>−</span>\n </CanvasButton>\n <button\n type=\"button\"\n onClick={() => apply({ scale: 1, x: 0, y: 0 })}\n aria-label=\"Reset the view\"\n className=\"min-w-12 rounded-(--radius-xs) px-1.5 py-1 mono-meta text-(--ink-2) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--ring)\"\n >\n {Math.round(view.scale * 100)}%\n </button>\n <CanvasButton label=\"Zoom in\" onClick={() => zoomAbout(STEP)}>\n <span aria-hidden>+</span>\n </CanvasButton>\n </div>\n )}\n </div>\n )\n}\n\nfunction CanvasButton({\n label,\n onClick,\n children,\n}: {\n label: string\n onClick: () => void\n children: ReactNode\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n className=\"flex size-7 items-center justify-center rounded-(--radius-xs) font-mono text-[13px] leading-none text-(--ink-2) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--ring)\"\n >\n {children}\n </button>\n )\n}\n\nexport default DiagramCanvas\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { RefObject, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/** The files a figure can be taken away as. */
|
|
5
|
+
type ExportFormat = 'png' | 'jpeg' | 'webp' | 'svg' | 'share-card';
|
|
6
|
+
/** What came of an export, for a page that wants to say so. */
|
|
7
|
+
interface ExportResult {
|
|
8
|
+
format: ExportFormat;
|
|
9
|
+
/**
|
|
10
|
+
* Whether the pipeline named by `source` finished without throwing.
|
|
11
|
+
*
|
|
12
|
+
* Read it with `source`, because the two sources can promise different
|
|
13
|
+
* amounts. `built-in` means a file was handed to the browser. `caller` means
|
|
14
|
+
* an `onExport` handler resolved — and a handler that did nothing resolves
|
|
15
|
+
* exactly like one that wrote a file, which is a thing this component cannot
|
|
16
|
+
* see and therefore does not claim.
|
|
17
|
+
*/
|
|
18
|
+
ok: boolean;
|
|
19
|
+
error?: Error;
|
|
20
|
+
/** Which pipeline ran: this component's, or the caller's `onExport`. */
|
|
21
|
+
source: 'built-in' | 'caller';
|
|
22
|
+
}
|
|
23
|
+
interface DiagramExportMenuProps {
|
|
24
|
+
/**
|
|
25
|
+
* The `<svg>` to export, or an element containing exactly one.
|
|
26
|
+
*
|
|
27
|
+
* A ref rather than a selector, because a page can hold several figures and a
|
|
28
|
+
* selector would export whichever the document happened to reach first.
|
|
29
|
+
*/
|
|
30
|
+
targetRef: RefObject<HTMLElement | SVGSVGElement | null>;
|
|
31
|
+
/** Names the file, and is printed on the share card. */
|
|
32
|
+
title: string;
|
|
33
|
+
/** Replaces the trigger. */
|
|
34
|
+
trigger?: ReactNode;
|
|
35
|
+
className?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Runs instead of the built-in export — for a caller with its own pipeline.
|
|
38
|
+
*
|
|
39
|
+
* Taking it turns everything below into a no-op, INCLUDING the part that
|
|
40
|
+
* knows whether a file was produced: a result from this path reports
|
|
41
|
+
* `source: 'caller'` and an `ok` that means only "your handler resolved".
|
|
42
|
+
*/
|
|
43
|
+
onExport?: (format: ExportFormat) => void | Promise<void>;
|
|
44
|
+
/** Told what happened, so a page can raise a toast. */
|
|
45
|
+
onResult?: (result: ExportResult) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Painted behind the artwork. Defaults to the surface the reader is looking
|
|
48
|
+
* at; `null` exports on a transparent ground.
|
|
49
|
+
*
|
|
50
|
+
* JPEG has no alpha channel, so it is flattened onto the reader's own paper
|
|
51
|
+
* whatever this says — a transparent JPEG is a black one.
|
|
52
|
+
*/
|
|
53
|
+
background?: string | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Taking the figure off the page: five files, one menu.
|
|
57
|
+
*
|
|
58
|
+
* WHY THE MENU DOES THE WORK rather than handing back a format: every one of
|
|
59
|
+
* these exports is the same six steps — find the `<svg>`, walk it with
|
|
60
|
+
* `getComputedStyle` to bake the custom properties into real colours,
|
|
61
|
+
* serialise, rasterise, name the file, hand it to the browser — and only the
|
|
62
|
+
* last two differ between them. A menu that emitted `'png'` and left the caller
|
|
63
|
+
* to do the rest would be a menu that every consumer reimplements, badly, and
|
|
64
|
+
* the interesting half (a serialised SVG resolves `var(--ink)` to nothing and
|
|
65
|
+
* comes out invisible) is exactly the half a caller would not know to write.
|
|
66
|
+
*
|
|
67
|
+
* `onExport` is still there for a page with its own pipeline — a server-side
|
|
68
|
+
* renderer, a different frame size — and taking it turns everything below into
|
|
69
|
+
* a no-op, which is why a result from that path says `source: 'caller'`: this
|
|
70
|
+
* component cannot see whether the handler produced a file, so it does not
|
|
71
|
+
* report that it did.
|
|
72
|
+
*
|
|
73
|
+
* What each format actually is, stated rather than implied.
|
|
74
|
+
*
|
|
75
|
+
* **SVG** is the artwork with resolved colours. It is editable and it is the
|
|
76
|
+
* only lossless one, but it carries no web fonts: a machine without the family
|
|
77
|
+
* renders it in a fallback, so type metrics will differ.
|
|
78
|
+
*
|
|
79
|
+
* **The ground** is the reader's own surface unless `background` says
|
|
80
|
+
* otherwise. `background={null}` exports on transparency — for a figure being
|
|
81
|
+
* dropped onto a coloured page — except in JPEG, which has no alpha and is
|
|
82
|
+
* flattened onto paper whatever is asked for.
|
|
83
|
+
*
|
|
84
|
+
* **PNG, JPEG and WebP** are the browser's own rasteriser re-drawing that SVG
|
|
85
|
+
* at 2×. Not a screenshot — antialiasing and any effect a page stylesheet
|
|
86
|
+
* applied from OUTSIDE the `<svg>` are not in it. **JPEG has no alpha**, so it
|
|
87
|
+
* is flattened onto the resolved paper colour rather than onto black, which is
|
|
88
|
+
* what a transparent PNG becomes when a format with no transparency is asked
|
|
89
|
+
* to hold it.
|
|
90
|
+
*
|
|
91
|
+
* **The share card** is a 1200 × 630 frame with the title on it and the whole
|
|
92
|
+
* diagram letterboxed inside — never cropped. A card that cropped to fill the
|
|
93
|
+
* frame would be a picture of a different diagram.
|
|
94
|
+
*/
|
|
95
|
+
declare function DiagramExportMenu({ targetRef, title, trigger, className, onExport, onResult, background, }: DiagramExportMenuProps): react.JSX.Element;
|
|
96
|
+
/**
|
|
97
|
+
* One figure, one format, one file.
|
|
98
|
+
*
|
|
99
|
+
* Exported so a page can wire a keyboard shortcut or its own button to exactly
|
|
100
|
+
* what the menu does, without reimplementing the six steps.
|
|
101
|
+
*/
|
|
102
|
+
declare function exportFigure(target: HTMLElement | SVGSVGElement | null, title: string, format: ExportFormat, options?: {
|
|
103
|
+
background?: string | null;
|
|
104
|
+
}): Promise<void>;
|
|
105
|
+
|
|
106
|
+
export { DiagramExportMenu, type DiagramExportMenuProps, type ExportFormat, type ExportResult, DiagramExportMenu as default, exportFigure };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { cn } from "../../lib/cn.js";
|
|
5
|
+
import {
|
|
6
|
+
downloadBlob,
|
|
7
|
+
exportFilename,
|
|
8
|
+
rasterize,
|
|
9
|
+
readToken,
|
|
10
|
+
serializeSvg
|
|
11
|
+
} from "../../lib/svg-export.js";
|
|
12
|
+
import {
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuContent,
|
|
15
|
+
DropdownMenuItem,
|
|
16
|
+
DropdownMenuLabel,
|
|
17
|
+
DropdownMenuSeparator,
|
|
18
|
+
DropdownMenuTrigger
|
|
19
|
+
} from "../../components/DropdownMenu/DropdownMenu.js";
|
|
20
|
+
import { Button } from "../../components/Button/Button.js";
|
|
21
|
+
const SHARE_CARD = { width: 1200, height: 630 };
|
|
22
|
+
const ITEMS = [
|
|
23
|
+
{ group: "Image", format: "png", title: "PNG", hint: "Lossless, 2\xD7 for retina" },
|
|
24
|
+
{ group: "Image", format: "jpeg", title: "JPEG", hint: "Compact, flattened onto paper" },
|
|
25
|
+
{ group: "Image", format: "webp", title: "WebP", hint: "Smaller, modern browsers" },
|
|
26
|
+
{ group: "Vector", format: "svg", title: "SVG", hint: "Editable, colours resolved" },
|
|
27
|
+
{ group: "Share", format: "share-card", title: "Share card", hint: "1200 \xD7 630 PNG" }
|
|
28
|
+
];
|
|
29
|
+
function DiagramExportMenu({
|
|
30
|
+
targetRef,
|
|
31
|
+
title,
|
|
32
|
+
trigger,
|
|
33
|
+
className,
|
|
34
|
+
onExport,
|
|
35
|
+
onResult,
|
|
36
|
+
background
|
|
37
|
+
}) {
|
|
38
|
+
const [busy, setBusy] = useState(null);
|
|
39
|
+
async function run(format) {
|
|
40
|
+
const source = onExport ? "caller" : "built-in";
|
|
41
|
+
setBusy(format);
|
|
42
|
+
try {
|
|
43
|
+
if (onExport) {
|
|
44
|
+
await onExport(format);
|
|
45
|
+
} else {
|
|
46
|
+
await exportFigure(targetRef.current, title, format, { background });
|
|
47
|
+
}
|
|
48
|
+
onResult?.({ format, ok: true, source });
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
onResult?.({ format, ok: false, error: cause, source });
|
|
51
|
+
} finally {
|
|
52
|
+
setBusy(null);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const groups = [...new Set(ITEMS.map((item) => item.group))];
|
|
56
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
57
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: trigger ?? /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", className, children: "Export" }) }),
|
|
58
|
+
/* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", className: "w-64", children: groups.map((group, index) => /* @__PURE__ */ jsxs("div", { children: [
|
|
59
|
+
index > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
60
|
+
/* @__PURE__ */ jsx(DropdownMenuLabel, { children: group }),
|
|
61
|
+
ITEMS.filter((item) => item.group === group).map((item) => /* @__PURE__ */ jsxs(
|
|
62
|
+
DropdownMenuItem,
|
|
63
|
+
{
|
|
64
|
+
disabled: busy !== null,
|
|
65
|
+
onSelect: () => void run(item.format),
|
|
66
|
+
className: cn("flex-col items-start gap-0.5"),
|
|
67
|
+
children: [
|
|
68
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[13px] leading-tight text-(--ink)", children: [
|
|
69
|
+
item.title,
|
|
70
|
+
busy === item.format && " \u2026"
|
|
71
|
+
] }),
|
|
72
|
+
/* @__PURE__ */ jsx("span", { className: "mono-meta text-(--ink-3-aa)", children: item.hint })
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
item.format
|
|
76
|
+
))
|
|
77
|
+
] }, group)) })
|
|
78
|
+
] });
|
|
79
|
+
}
|
|
80
|
+
async function exportFigure(target, title, format, options = {}) {
|
|
81
|
+
const svg = findArtwork(target);
|
|
82
|
+
if (!svg) throw new Error("exportFigure: found no <svg> to export inside the given element");
|
|
83
|
+
const paper = readToken(svg, "--diagram-surface", "#ffffff");
|
|
84
|
+
const ink = readToken(svg, "--ink", "#101010");
|
|
85
|
+
const font = getComputedStyle(svg).fontFamily || "sans-serif";
|
|
86
|
+
const plate = options.background === null ? void 0 : options.background ?? paper;
|
|
87
|
+
if (format === "share-card") {
|
|
88
|
+
const markup2 = serializeSvg(svg, {
|
|
89
|
+
padding: 56,
|
|
90
|
+
background: plate,
|
|
91
|
+
title,
|
|
92
|
+
titleColor: ink,
|
|
93
|
+
titleFont: font,
|
|
94
|
+
frame: SHARE_CARD
|
|
95
|
+
});
|
|
96
|
+
const blob2 = await rasterize(markup2, "png", { scale: 1 });
|
|
97
|
+
downloadBlob(blob2, exportFilename(title, "png", "share-card"));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const markup = serializeSvg(svg, { padding: 20, background: plate });
|
|
101
|
+
if (format === "svg") {
|
|
102
|
+
downloadBlob(new Blob([markup], { type: "image/svg+xml;charset=utf-8" }), exportFilename(title, "svg"));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const blob = await rasterize(markup, format, {
|
|
106
|
+
scale: 2,
|
|
107
|
+
// JPEG has no alpha channel, so an unflattened export lands on black.
|
|
108
|
+
background: format === "jpeg" ? paper : void 0
|
|
109
|
+
});
|
|
110
|
+
downloadBlob(blob, exportFilename(title, format));
|
|
111
|
+
}
|
|
112
|
+
function findArtwork(element) {
|
|
113
|
+
if (!element) return null;
|
|
114
|
+
if (element.tagName.toLowerCase() === "svg") return element;
|
|
115
|
+
return element.querySelector("svg[data-diagram-artwork]") ?? element.querySelector("svg");
|
|
116
|
+
}
|
|
117
|
+
var DiagramExportMenu_default = DiagramExportMenu;
|
|
118
|
+
export {
|
|
119
|
+
DiagramExportMenu,
|
|
120
|
+
DiagramExportMenu_default as default,
|
|
121
|
+
exportFigure
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=DiagramExportMenu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/diagrams/DiagramExportMenu/DiagramExportMenu.tsx"],"sourcesContent":["'use client'\n\nimport { useState, type ReactNode, type RefObject } from 'react'\nimport { cn } from '../../lib/cn'\nimport {\n downloadBlob,\n exportFilename,\n rasterize,\n readToken,\n serializeSvg,\n} from '../../lib/svg-export'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../../components/DropdownMenu/DropdownMenu'\nimport { Button } from '../../components/Button/Button'\n\n/** The files a figure can be taken away as. */\nexport type ExportFormat = 'png' | 'jpeg' | 'webp' | 'svg' | 'share-card'\n\n/** What came of an export, for a page that wants to say so. */\nexport interface ExportResult {\n format: ExportFormat\n /**\n * Whether the pipeline named by `source` finished without throwing.\n *\n * Read it with `source`, because the two sources can promise different\n * amounts. `built-in` means a file was handed to the browser. `caller` means\n * an `onExport` handler resolved — and a handler that did nothing resolves\n * exactly like one that wrote a file, which is a thing this component cannot\n * see and therefore does not claim.\n */\n ok: boolean\n error?: Error\n /** Which pipeline ran: this component's, or the caller's `onExport`. */\n source: 'built-in' | 'caller'\n}\n\n/** A share card is a fixed frame, because the surfaces that consume one are. */\nconst SHARE_CARD = { width: 1200, height: 630 } as const\n\nconst ITEMS: { format: ExportFormat; title: string; hint: string; group: string }[] = [\n { group: 'Image', format: 'png', title: 'PNG', hint: 'Lossless, 2× for retina' },\n { group: 'Image', format: 'jpeg', title: 'JPEG', hint: 'Compact, flattened onto paper' },\n { group: 'Image', format: 'webp', title: 'WebP', hint: 'Smaller, modern browsers' },\n { group: 'Vector', format: 'svg', title: 'SVG', hint: 'Editable, colours resolved' },\n { group: 'Share', format: 'share-card', title: 'Share card', hint: '1200 × 630 PNG' },\n]\n\nexport interface DiagramExportMenuProps {\n /**\n * The `<svg>` to export, or an element containing exactly one.\n *\n * A ref rather than a selector, because a page can hold several figures and a\n * selector would export whichever the document happened to reach first.\n */\n targetRef: RefObject<HTMLElement | SVGSVGElement | null>\n /** Names the file, and is printed on the share card. */\n title: string\n /** Replaces the trigger. */\n trigger?: ReactNode\n className?: string\n /**\n * Runs instead of the built-in export — for a caller with its own pipeline.\n *\n * Taking it turns everything below into a no-op, INCLUDING the part that\n * knows whether a file was produced: a result from this path reports\n * `source: 'caller'` and an `ok` that means only \"your handler resolved\".\n */\n onExport?: (format: ExportFormat) => void | Promise<void>\n /** Told what happened, so a page can raise a toast. */\n onResult?: (result: ExportResult) => void\n /**\n * Painted behind the artwork. Defaults to the surface the reader is looking\n * at; `null` exports on a transparent ground.\n *\n * JPEG has no alpha channel, so it is flattened onto the reader's own paper\n * whatever this says — a transparent JPEG is a black one.\n */\n background?: string | null\n}\n\n/**\n * Taking the figure off the page: five files, one menu.\n *\n * WHY THE MENU DOES THE WORK rather than handing back a format: every one of\n * these exports is the same six steps — find the `<svg>`, walk it with\n * `getComputedStyle` to bake the custom properties into real colours,\n * serialise, rasterise, name the file, hand it to the browser — and only the\n * last two differ between them. A menu that emitted `'png'` and left the caller\n * to do the rest would be a menu that every consumer reimplements, badly, and\n * the interesting half (a serialised SVG resolves `var(--ink)` to nothing and\n * comes out invisible) is exactly the half a caller would not know to write.\n *\n * `onExport` is still there for a page with its own pipeline — a server-side\n * renderer, a different frame size — and taking it turns everything below into\n * a no-op, which is why a result from that path says `source: 'caller'`: this\n * component cannot see whether the handler produced a file, so it does not\n * report that it did.\n *\n * What each format actually is, stated rather than implied.\n *\n * **SVG** is the artwork with resolved colours. It is editable and it is the\n * only lossless one, but it carries no web fonts: a machine without the family\n * renders it in a fallback, so type metrics will differ.\n *\n * **The ground** is the reader's own surface unless `background` says\n * otherwise. `background={null}` exports on transparency — for a figure being\n * dropped onto a coloured page — except in JPEG, which has no alpha and is\n * flattened onto paper whatever is asked for.\n *\n * **PNG, JPEG and WebP** are the browser's own rasteriser re-drawing that SVG\n * at 2×. Not a screenshot — antialiasing and any effect a page stylesheet\n * applied from OUTSIDE the `<svg>` are not in it. **JPEG has no alpha**, so it\n * is flattened onto the resolved paper colour rather than onto black, which is\n * what a transparent PNG becomes when a format with no transparency is asked\n * to hold it.\n *\n * **The share card** is a 1200 × 630 frame with the title on it and the whole\n * diagram letterboxed inside — never cropped. A card that cropped to fill the\n * frame would be a picture of a different diagram.\n */\nexport function DiagramExportMenu({\n targetRef,\n title,\n trigger,\n className,\n onExport,\n onResult,\n background,\n}: DiagramExportMenuProps) {\n const [busy, setBusy] = useState<ExportFormat | null>(null)\n\n async function run(format: ExportFormat) {\n const source = onExport ? 'caller' : 'built-in'\n setBusy(format)\n try {\n if (onExport) {\n await onExport(format)\n } else {\n await exportFigure(targetRef.current, title, format, { background })\n }\n // `ok` says the pipeline finished, and `source` says whose. Reporting a\n // bare success for a handler that may have done nothing was this menu\n // claiming to know something only the caller can.\n onResult?.({ format, ok: true, source })\n } catch (cause) {\n // Never swallowed: a download is something the reader just asked for, and\n // a click that quietly does nothing is indistinguishable from a broken\n // button. The caller gets the error to put in front of them.\n onResult?.({ format, ok: false, error: cause as Error, source })\n } finally {\n setBusy(null)\n }\n }\n\n const groups = [...new Set(ITEMS.map((item) => item.group))]\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n {trigger ?? (\n <Button size=\"sm\" variant=\"secondary\" className={className}>\n Export\n </Button>\n )}\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-64\">\n {groups.map((group, index) => (\n <div key={group}>\n {index > 0 && <DropdownMenuSeparator />}\n <DropdownMenuLabel>{group}</DropdownMenuLabel>\n {/* Menu ROWS, not buttons that happen to sit inside a menu. Radix\n gives an item roving focus, typeahead and close-on-pick; a\n plain <button> inside a role=\"menu\" gets none of the three and\n leaves the menu open over the file it just wrote. */}\n {ITEMS.filter((item) => item.group === group).map((item) => (\n <DropdownMenuItem\n key={item.format}\n disabled={busy !== null}\n onSelect={() => void run(item.format)}\n className={cn('flex-col items-start gap-0.5')}\n >\n <span className=\"text-[13px] leading-tight text-(--ink)\">\n {item.title}\n {busy === item.format && ' …'}\n </span>\n <span className=\"mono-meta text-(--ink-3-aa)\">{item.hint}</span>\n </DropdownMenuItem>\n ))}\n </div>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/**\n * One figure, one format, one file.\n *\n * Exported so a page can wire a keyboard shortcut or its own button to exactly\n * what the menu does, without reimplementing the six steps.\n */\nexport async function exportFigure(\n target: HTMLElement | SVGSVGElement | null,\n title: string,\n format: ExportFormat,\n options: { background?: string | null } = {},\n): Promise<void> {\n const svg = findArtwork(target)\n if (!svg) throw new Error('exportFigure: found no <svg> to export inside the given element')\n\n // Read against the LIVE element, so the file comes out in whichever theme the\n // reader is looking at rather than in whichever one the code assumed.\n const paper = readToken(svg, '--diagram-surface', '#ffffff')\n const ink = readToken(svg, '--ink', '#101010')\n const font = getComputedStyle(svg).fontFamily || 'sans-serif'\n\n // `null` is a request, not a missing value: a figure dropped onto a coloured\n // page wants the page's own ground behind it, and a plate painted in every\n // format meant no export could ever give it one.\n const plate = options.background === null ? undefined : (options.background ?? paper)\n\n if (format === 'share-card') {\n const markup = serializeSvg(svg, {\n padding: 56,\n background: plate,\n title,\n titleColor: ink,\n titleFont: font,\n frame: SHARE_CARD,\n })\n const blob = await rasterize(markup, 'png', { scale: 1 })\n downloadBlob(blob, exportFilename(title, 'png', 'share-card'))\n return\n }\n\n const markup = serializeSvg(svg, { padding: 20, background: plate })\n\n if (format === 'svg') {\n downloadBlob(new Blob([markup], { type: 'image/svg+xml;charset=utf-8' }), exportFilename(title, 'svg'))\n return\n }\n\n const blob = await rasterize(markup, format, {\n scale: 2,\n // JPEG has no alpha channel, so an unflattened export lands on black.\n background: format === 'jpeg' ? paper : undefined,\n })\n downloadBlob(blob, exportFilename(title, format))\n}\n\n/**\n * The figure's own `<svg>`, not the first one in the subtree.\n *\n * The distinction is load-bearing: a toolbar sits inside the same wrapper and\n * every icon in it is an `<svg>`. Looking for the artwork marker first is what\n * stops an export of a diagram from being a picture of a chevron.\n */\nfunction findArtwork(element: HTMLElement | SVGSVGElement | null): SVGSVGElement | null {\n if (!element) return null\n if (element.tagName.toLowerCase() === 'svg') return element as SVGSVGElement\n return (\n element.querySelector<SVGSVGElement>('svg[data-diagram-artwork]') ??\n element.querySelector<SVGSVGElement>('svg')\n )\n}\n\nexport default DiagramExportMenu\n"],"mappings":"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","names":["markup","blob"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/** One fact about the selected thing: a short label and its value. */
|
|
5
|
+
interface InspectorFact {
|
|
6
|
+
label: string;
|
|
7
|
+
value: ReactNode;
|
|
8
|
+
/** Sets the value in mono — an id, a path, a version, a port. */
|
|
9
|
+
mono?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** A relationship the selected thing takes part in. */
|
|
12
|
+
interface InspectorLink {
|
|
13
|
+
id?: string;
|
|
14
|
+
/** Which way it runs, relative to the selected node. */
|
|
15
|
+
direction: 'in' | 'out';
|
|
16
|
+
label: string;
|
|
17
|
+
/** What sits at the other end. */
|
|
18
|
+
peer: string;
|
|
19
|
+
onSelect?: () => void;
|
|
20
|
+
}
|
|
21
|
+
interface DiagramInspectorProps {
|
|
22
|
+
/** The kicker over the title — what KIND of thing this panel is showing. */
|
|
23
|
+
eyebrow?: string;
|
|
24
|
+
title: string;
|
|
25
|
+
description?: ReactNode;
|
|
26
|
+
facts?: InspectorFact[];
|
|
27
|
+
links?: InspectorLink[];
|
|
28
|
+
/** Buttons under the facts — copy a link, open the source. */
|
|
29
|
+
actions?: ReactNode;
|
|
30
|
+
onClose?: () => void;
|
|
31
|
+
className?: string;
|
|
32
|
+
/** Renders as a floating panel over the surface rather than in the flow. */
|
|
33
|
+
floating?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* What the reader just picked, written out.
|
|
37
|
+
*
|
|
38
|
+
* A diagram can hold about eight words per node before it stops being a diagram
|
|
39
|
+
* and starts being a document with lines drawn on it. Everything past those
|
|
40
|
+
* eight — the port, the owning team, the file it was read out of, the six
|
|
41
|
+
* relationships it takes part in — belongs beside the picture rather than
|
|
42
|
+
* inside it, and this is that place.
|
|
43
|
+
*
|
|
44
|
+
* NOT A DIALOG, and that is deliberate. An inspector is a REGION that changes
|
|
45
|
+
* with the selection, not a modal. Giving it `role="dialog"` would trap focus
|
|
46
|
+
* and demand dismissal for something the reader never opened — they clicked a
|
|
47
|
+
* node, and the panel followed. So it is a labelled region with
|
|
48
|
+
* `aria-live="polite"`, which is what makes a screen reader announce the new
|
|
49
|
+
* selection without stealing the cursor from whatever the reader was doing.
|
|
50
|
+
*
|
|
51
|
+
* The relationships are real buttons when they carry `onSelect`, which is how
|
|
52
|
+
* the diagram becomes navigable from the keyboard: a reader can walk the graph
|
|
53
|
+
* peer by peer without ever touching the picture.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* <DiagramInspector
|
|
57
|
+
* eyebrow="Service"
|
|
58
|
+
* title="API Server"
|
|
59
|
+
* description="FastAPI, behind the load balancer."
|
|
60
|
+
* facts={[
|
|
61
|
+
* { label: 'Port', value: '8000', mono: true },
|
|
62
|
+
* { label: 'Id', value: 'api', mono: true },
|
|
63
|
+
* ]}
|
|
64
|
+
* links={[{ direction: 'out', label: 'SQL', peer: 'PostgreSQL' }]}
|
|
65
|
+
* onClose={() => setSelected(null)}
|
|
66
|
+
* />
|
|
67
|
+
*/
|
|
68
|
+
declare function DiagramInspector({ eyebrow, title, description, facts, links, actions, onClose, className, floating, }: DiagramInspectorProps): react.JSX.Element;
|
|
69
|
+
|
|
70
|
+
export { DiagramInspector, type DiagramInspectorProps, type InspectorFact, type InspectorLink, DiagramInspector as default };
|