@misoto22/design 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +87 -0
- package/CLAUDE.md +1 -0
- package/README.md +50 -5
- package/bin/misoto22-design.mjs +59 -13
- package/dist/agent/Accordion.md +24 -1
- package/dist/agent/Alert.md +23 -1
- package/dist/agent/AppShell.md +30 -2
- package/dist/agent/ArchitectureFigure.md +25 -2
- package/dist/agent/AreaChart.md +27 -2
- package/dist/agent/Article.md +26 -3
- package/dist/agent/AspectRatio.md +57 -0
- package/dist/agent/Avatar.md +25 -5
- package/dist/agent/Badge.md +22 -1
- package/dist/agent/BarChart.md +29 -2
- package/dist/agent/BarList.md +27 -1
- package/dist/agent/BigNumber.md +26 -2
- package/dist/agent/BoxPlot.md +24 -1
- package/dist/agent/Breadcrumb.md +24 -2
- package/dist/agent/BulletChart.md +23 -1
- package/dist/agent/Button.md +25 -1
- package/dist/agent/Calendar.md +37 -3
- package/dist/agent/Card.md +24 -2
- package/dist/agent/Checkbox.md +23 -2
- package/dist/agent/Code.md +50 -0
- package/dist/agent/CodeBlock.md +74 -0
- package/dist/agent/Collapsible.md +22 -1
- package/dist/agent/ColorPicker.md +75 -0
- package/dist/agent/Combobox.md +29 -3
- package/dist/agent/Command.md +25 -2
- package/dist/agent/ComposedChart.md +26 -2
- package/dist/agent/ContextMenu.md +37 -4
- package/dist/agent/DataflowFigure.md +22 -1
- package/dist/agent/DatePicker.md +34 -6
- package/dist/agent/DescriptionList.md +68 -0
- package/dist/agent/Diagram.md +27 -3
- package/dist/agent/DiagramCanvas.md +24 -1
- package/dist/agent/DiagramExportMenu.md +29 -3
- package/dist/agent/DiagramInspector.md +23 -1
- package/dist/agent/DiagramLegend.md +22 -1
- package/dist/agent/DiagramMinimap.md +24 -4
- package/dist/agent/DiagramToolbar.md +23 -1
- package/dist/agent/Dialog.md +26 -3
- package/dist/agent/DropdownMenu.md +45 -6
- package/dist/agent/EmptyState.md +26 -1
- package/dist/agent/ErrorState.md +27 -2
- package/dist/agent/Facet.md +25 -1
- package/dist/agent/Field.md +45 -4
- package/dist/agent/FigureBand.md +24 -1
- package/dist/agent/FloatingIconButton.md +23 -1
- package/dist/agent/FunnelChart.md +23 -1
- package/dist/agent/Heading.md +64 -0
- package/dist/agent/Heatmap.md +25 -1
- package/dist/agent/Histogram.md +26 -2
- package/dist/agent/Input.md +22 -1
- package/dist/agent/Kbd.md +19 -1
- package/dist/agent/LifecycleFigure.md +22 -1
- package/dist/agent/LineChart.md +26 -2
- package/dist/agent/LinkArrow.md +20 -1
- package/dist/agent/Markdown.md +68 -0
- package/dist/agent/NativeSelect.md +23 -2
- package/dist/agent/NavItem.md +21 -1
- package/dist/agent/NumberField.md +72 -0
- package/dist/agent/Pagination.md +28 -2
- package/dist/agent/PieChart.md +26 -2
- package/dist/agent/Popover.md +23 -1
- package/dist/agent/Progress.md +27 -3
- package/dist/agent/RadarChart.md +26 -2
- package/dist/agent/RadialChart.md +26 -3
- package/dist/agent/RadioGroup.md +23 -2
- package/dist/agent/SankeyChart.md +25 -2
- package/dist/agent/ScatterChart.md +27 -2
- package/dist/agent/ScrollArea.md +24 -3
- package/dist/agent/SearchableMenu.md +23 -1
- package/dist/agent/Select.md +29 -3
- package/dist/agent/Separator.md +28 -4
- package/dist/agent/SequenceFigure.md +23 -1
- package/dist/agent/Sheet.md +25 -2
- package/dist/agent/Sidebar.md +193 -0
- package/dist/agent/Skeleton.md +24 -2
- package/dist/agent/Slider.md +31 -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 +28 -3
- package/dist/agent/Tabs.md +24 -2
- package/dist/agent/Tag.md +40 -9
- package/dist/agent/Text.md +65 -0
- package/dist/agent/Textarea.md +20 -1
- package/dist/agent/Timestamp.md +64 -0
- package/dist/agent/Toast.md +26 -2
- package/dist/agent/ToggleGroup.md +24 -2
- package/dist/agent/Toolbar.md +71 -0
- package/dist/agent/Tooltip.md +23 -1
- package/dist/agent/TreemapChart.md +24 -1
- package/dist/agent/WaterfallChart.md +24 -1
- package/dist/agent/WorkflowFigure.md +24 -2
- package/dist/agent/catalog.json +5915 -699
- package/dist/agent/index.md +20 -8
- 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 +44 -9
- 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 +15 -11
- package/dist/charts/RadarChart/RadarChart.js.map +1 -1
- package/dist/charts/RadialChart/RadialChart.d.ts +22 -4
- package/dist/charts/RadialChart/RadialChart.js +28 -6
- package/dist/charts/RadialChart/RadialChart.js.map +1 -1
- package/dist/charts/SankeyChart/SankeyChart.d.ts +16 -4
- package/dist/charts/SankeyChart/SankeyChart.js +14 -6
- package/dist/charts/SankeyChart/SankeyChart.js.map +1 -1
- package/dist/charts/ScatterChart/ScatterChart.d.ts +24 -3
- package/dist/charts/ScatterChart/ScatterChart.js +21 -15
- package/dist/charts/ScatterChart/ScatterChart.js.map +1 -1
- package/dist/charts/Sparkline/Sparkline.js +5 -3
- package/dist/charts/Sparkline/Sparkline.js.map +1 -1
- package/dist/charts/TreemapChart/TreemapChart.d.ts +7 -2
- package/dist/charts/TreemapChart/TreemapChart.js +20 -11
- package/dist/charts/TreemapChart/TreemapChart.js.map +1 -1
- package/dist/charts/WaterfallChart/WaterfallChart.js +4 -2
- package/dist/charts/WaterfallChart/WaterfallChart.js.map +1 -1
- package/dist/charts/lib/export.d.ts +1 -0
- package/dist/charts/lib/figure.d.ts +21 -1
- package/dist/charts/lib/figure.js +5 -2
- package/dist/charts/lib/figure.js.map +1 -1
- package/dist/charts/lib/scale.d.ts +23 -0
- package/dist/charts/lib/scale.js +13 -0
- package/dist/charts/lib/scale.js.map +1 -0
- package/dist/charts/lib/selection.d.ts +20 -0
- package/dist/charts/lib/selection.js +18 -0
- package/dist/charts/lib/selection.js.map +1 -0
- package/dist/charts/lib/toolbar.d.ts +1 -0
- package/dist/components/Accordion/Accordion.js +8 -1
- package/dist/components/Accordion/Accordion.js.map +1 -1
- package/dist/components/AppShell/AppShell.d.ts +6 -0
- package/dist/components/AppShell/AppShell.js +34 -4
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/Article/Article.d.ts +11 -4
- package/dist/components/Article/Article.js +13 -0
- package/dist/components/Article/Article.js.map +1 -1
- package/dist/components/AspectRatio/AspectRatio.d.ts +46 -0
- package/dist/components/AspectRatio/AspectRatio.js +33 -0
- package/dist/components/AspectRatio/AspectRatio.js.map +1 -0
- package/dist/components/Avatar/Avatar.d.ts +12 -3
- package/dist/components/Avatar/Avatar.js +6 -1
- package/dist/components/Avatar/Avatar.js.map +1 -1
- package/dist/components/Breadcrumb/Breadcrumb.d.ts +6 -0
- package/dist/components/Breadcrumb/Breadcrumb.js +15 -0
- package/dist/components/Breadcrumb/Breadcrumb.js.map +1 -1
- package/dist/components/Button/Button.js +14 -0
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.d.ts +29 -3
- package/dist/components/Calendar/Calendar.js +60 -12
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Card/Card.d.ts +6 -0
- package/dist/components/Card/Card.js +1 -1
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts +5 -2
- package/dist/components/Checkbox/Checkbox.js +12 -2
- package/dist/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/components/Code/Code.d.ts +30 -0
- package/dist/components/Code/Code.js +21 -0
- package/dist/components/Code/Code.js.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.d.ts +141 -0
- package/dist/components/CodeBlock/CodeBlock.js +123 -0
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -0
- package/dist/components/Collapsible/Collapsible.js +10 -2
- package/dist/components/Collapsible/Collapsible.js.map +1 -1
- package/dist/components/ColorPicker/ColorPicker.d.ts +67 -0
- package/dist/components/ColorPicker/ColorPicker.js +334 -0
- package/dist/components/ColorPicker/ColorPicker.js.map +1 -0
- 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 +14 -2
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.d.ts +30 -5
- package/dist/components/ContextMenu/ContextMenu.js +33 -3
- 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 +21 -3
- package/dist/components/Dialog/Dialog.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.d.ts +49 -11
- package/dist/components/DropdownMenu/DropdownMenu.js +33 -3
- 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 +12 -3
- 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/NavItem/NavItem.js +1 -1
- package/dist/components/NavItem/NavItem.js.map +1 -1
- package/dist/components/NumberField/NumberField.d.ts +69 -0
- package/dist/components/NumberField/NumberField.js +162 -0
- package/dist/components/NumberField/NumberField.js.map +1 -0
- 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/Popover/Popover.js +1 -1
- package/dist/components/Popover/Popover.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 +21 -4
- 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 +12 -3
- package/dist/components/Sheet/Sheet.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.d.ts +349 -0
- package/dist/components/Sidebar/Sidebar.js +455 -0
- package/dist/components/Sidebar/Sidebar.js.map +1 -0
- 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 +42 -3
- package/dist/components/Slider/Slider.js +121 -30
- 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 +20 -0
- package/dist/components/Table/Table.js +3 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Tabs/Tabs.js +7 -1
- package/dist/components/Tabs/Tabs.js.map +1 -1
- package/dist/components/Tag/Tag.d.ts +68 -9
- package/dist/components/Tag/Tag.js +50 -10
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Text/Text.d.ts +62 -0
- package/dist/components/Text/Text.js +30 -0
- package/dist/components/Text/Text.js.map +1 -0
- package/dist/components/Timestamp/Timestamp.d.ts +59 -0
- package/dist/components/Timestamp/Timestamp.js +56 -0
- package/dist/components/Timestamp/Timestamp.js.map +1 -0
- package/dist/components/Toast/Toast.d.ts +9 -1
- package/dist/components/Toast/Toast.js +27 -4
- package/dist/components/Toast/Toast.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +6 -1
- package/dist/components/ToggleGroup/ToggleGroup.js +11 -1
- package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
- package/dist/components/Toolbar/Toolbar.d.ts +55 -0
- package/dist/components/Toolbar/Toolbar.js +43 -0
- package/dist/components/Toolbar/Toolbar.js.map +1 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.d.ts +6 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js +48 -8
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js.map +1 -1
- package/dist/diagrams/DataflowFigure/DataflowFigure.js +21 -2
- package/dist/diagrams/DataflowFigure/DataflowFigure.js.map +1 -1
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.d.ts +14 -0
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js +14 -7
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js.map +1 -1
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.d.ts +38 -4
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js +14 -16
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js.map +1 -1
- package/dist/diagrams/DiagramInspector/DiagramInspector.js +2 -2
- package/dist/diagrams/DiagramInspector/DiagramInspector.js.map +1 -1
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.d.ts +23 -7
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js +45 -18
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js.map +1 -1
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js +44 -12
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js.map +1 -1
- package/dist/diagrams/SequenceFigure/SequenceFigure.js +35 -5
- package/dist/diagrams/SequenceFigure/SequenceFigure.js.map +1 -1
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.d.ts +3 -1
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js +76 -28
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js.map +1 -1
- package/dist/diagrams/index.d.ts +1 -0
- package/dist/diagrams/index.js +10 -0
- package/dist/diagrams/index.js.map +1 -1
- package/dist/diagrams/lib/dev.d.ts +61 -0
- package/dist/diagrams/lib/dev.js +96 -0
- package/dist/diagrams/lib/dev.js.map +1 -0
- package/dist/diagrams/lib/frame.d.ts +48 -7
- package/dist/diagrams/lib/frame.js +17 -2
- package/dist/diagrams/lib/frame.js.map +1 -1
- package/dist/diagrams/lib/route.d.ts +22 -1
- package/dist/diagrams/lib/route.js +24 -1
- package/dist/diagrams/lib/route.js.map +1 -1
- package/dist/diagrams/lib/wires.js +9 -5
- package/dist/diagrams/lib/wires.js.map +1 -1
- package/dist/diagrams/spec.d.ts +64 -12
- package/dist/index.d.ts +19 -4
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/keyframes.css +57 -3
- package/dist/lib/color.d.ts +80 -0
- package/dist/lib/color.js +189 -0
- package/dist/lib/color.js.map +1 -0
- package/dist/lib/numeric.d.ts +35 -0
- package/dist/lib/numeric.js +29 -0
- package/dist/lib/numeric.js.map +1 -0
- package/dist/lib/overlay-container.d.ts +14 -4
- package/dist/lib/overlay-container.js.map +1 -1
- package/dist/lib/warn.d.ts +73 -0
- package/dist/lib/warn.js +38 -0
- package/dist/lib/warn.js.map +1 -0
- package/dist/semantic.css +17 -6
- package/dist/styles.css +1 -1
- package/dist/themes.css +8 -0
- package/dist/tokens.css +86 -5
- package/dist/tokens.d.ts +4 -1
- package/dist/tokens.js +56 -36
- package/dist/tokens.json +56 -36
- package/llms.txt +35 -0
- package/package.json +5 -2
- package/skills/misoto22-design/evals/evals.json +84 -0
- package/skills/misoto22-design/rules/a11y.md +55 -3
- package/skills/misoto22-design/rules/forms.md +8 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ways a diagram specification can typecheck and draw something else.
|
|
3
|
+
*
|
|
4
|
+
* A figure has no runtime validator and should not grow one: the specification
|
|
5
|
+
* is a plain object, the layout is arithmetic on numbers it already carries,
|
|
6
|
+
* and a schema check would be a second copy of the types. What it needs instead
|
|
7
|
+
* is the same thing the components have — a word at the moment the arithmetic
|
|
8
|
+
* is asked to do something the author cannot have meant.
|
|
9
|
+
*
|
|
10
|
+
* Every case here shares one shape: the renderer CAN produce a picture, so
|
|
11
|
+
* nothing throws, and the picture is not the one the specification describes.
|
|
12
|
+
* A lane id nothing declares resolves to lane 0 and the step is drawn in the
|
|
13
|
+
* wrong band; a stage past the last heading is drawn past the last heading; a
|
|
14
|
+
* message to a participant that does not exist is drawn nowhere at all. Each of
|
|
15
|
+
* those used to be silent, and silence is the one outcome ruled out: an author
|
|
16
|
+
* who cannot see the difference between "I placed this" and "the renderer gave
|
|
17
|
+
* up" has no way to find the mistake except by knowing what the picture should
|
|
18
|
+
* have looked like.
|
|
19
|
+
*
|
|
20
|
+
* All of it goes through `lib/warn`, so it is dev-only, printed once per
|
|
21
|
+
* problem, and dropped from a production bundle by the consumer's own bundler.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Says so when a figure is handed the same object it was handed last time, with
|
|
25
|
+
* different contents inside it.
|
|
26
|
+
*
|
|
27
|
+
* Every figure memoises its model on the specification's IDENTITY, which is the
|
|
28
|
+
* only key that costs nothing to compare — and which a `spec.components.push()`
|
|
29
|
+
* does not change. The figure then goes on drawing the picture it was first
|
|
30
|
+
* given, through any number of re-renders, with no error and no clue.
|
|
31
|
+
*
|
|
32
|
+
* The fingerprint is only computed in development, and only ever compared with
|
|
33
|
+
* the previous render's: it is a diagnostic, not the memo key. Making it the
|
|
34
|
+
* memo key would put a `JSON.stringify` of the whole specification in front of
|
|
35
|
+
* every production render, which is a far worse trade than asking the author to
|
|
36
|
+
* build a new object.
|
|
37
|
+
*/
|
|
38
|
+
declare function useSpecIdentity(spec: unknown, component: string): void;
|
|
39
|
+
/** Says so when a node names a lane the specification never declared. */
|
|
40
|
+
declare function warnUnknownLane(component: string, nodeId: string, lane: string): void;
|
|
41
|
+
/** Says so when a data-flow node sits past the last stage heading. */
|
|
42
|
+
declare function warnStageOutOfRange(component: string, nodeId: string, stage: number, stages: number): void;
|
|
43
|
+
/** Says so when two boxes were placed on exactly the same coordinate. */
|
|
44
|
+
declare function warnCollision(component: string, ids: string[]): void;
|
|
45
|
+
/**
|
|
46
|
+
* The edges whose two ends exist, with a word about the ones that do not.
|
|
47
|
+
*
|
|
48
|
+
* Routing already drops a line it cannot place — there is no box to draw it
|
|
49
|
+
* between — and the summary list used to iterate the specification instead, so
|
|
50
|
+
* the picture and its text equivalent disagreed: one showed nothing, the other
|
|
51
|
+
* reported a relationship to an id no node claims. Both halves now read this,
|
|
52
|
+
* which is what makes them agree by construction rather than by inspection.
|
|
53
|
+
*/
|
|
54
|
+
declare function liveEdges<E extends {
|
|
55
|
+
from: string;
|
|
56
|
+
to: string;
|
|
57
|
+
}>(component: string, edges: E[] | undefined, known: (id: string) => boolean): E[];
|
|
58
|
+
/** Says so when anything else in the spec points at an id nothing declares. */
|
|
59
|
+
declare function warnUnknownRef(component: string, field: string, id: string, effect: string): void;
|
|
60
|
+
|
|
61
|
+
export { liveEdges, useSpecIdentity, warnCollision, warnStageOutOfRange, warnUnknownLane, warnUnknownRef };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { DEV, warn } from "../../lib/warn.js";
|
|
4
|
+
function useSpecIdentity(spec, component) {
|
|
5
|
+
const last = useRef(null);
|
|
6
|
+
if (!DEV) return;
|
|
7
|
+
const print = fingerprint(spec);
|
|
8
|
+
const previous = last.current;
|
|
9
|
+
last.current = { spec, print };
|
|
10
|
+
if (previous && previous.spec === spec && previous.print !== print) {
|
|
11
|
+
warn({
|
|
12
|
+
code: "DIAGRAM_SPEC_MUTATED",
|
|
13
|
+
problem: `${component} was given the same spec object with different contents. The model is memoised on the object's identity, so the figure is still drawing what that object held on the first render.`,
|
|
14
|
+
field: `${component}.spec`,
|
|
15
|
+
fix: "Build a new spec object for the change \u2014 { ...spec, nodes: [...] } \u2014 rather than mutating the one already passed.",
|
|
16
|
+
component
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function fingerprint(value) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.stringify(value) ?? "";
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function warnUnknownLane(component, nodeId, lane) {
|
|
28
|
+
if (!DEV) return;
|
|
29
|
+
warn({
|
|
30
|
+
code: "DIAGRAM_LANE_UNKNOWN",
|
|
31
|
+
problem: `${component} was given the lane "${lane}" on "${nodeId}", which no lane in the specification declares. It falls back to the first lane, so the node is drawn in the right column and the wrong band.`,
|
|
32
|
+
field: `${component}.nodes[${nodeId}].lane`,
|
|
33
|
+
fix: `Use one of the ids in spec.lanes, or add a lane whose id is "${lane}".`,
|
|
34
|
+
component
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function warnStageOutOfRange(component, nodeId, stage, stages) {
|
|
38
|
+
if (!DEV) return;
|
|
39
|
+
warn({
|
|
40
|
+
code: "DIAGRAM_STAGE_OUT_OF_RANGE",
|
|
41
|
+
problem: `${component} was given stage ${stage} on "${nodeId}" against ${stages} declared stages. A node's x is computed from its own index, so it is drawn past the last heading \u2014 in a column the axis does not label.`,
|
|
42
|
+
field: `${component}.nodes[${nodeId}].stage`,
|
|
43
|
+
fix: `Use a stage between 0 and ${Math.max(0, stages - 1)}, or declare the stages the pipeline actually has.`,
|
|
44
|
+
component
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function warnCollision(component, ids) {
|
|
48
|
+
if (!DEV) return;
|
|
49
|
+
warn({
|
|
50
|
+
code: "DIAGRAM_CELL_COLLISION",
|
|
51
|
+
problem: `${component} placed ${ids.map((id) => `"${id}"`).join(" and ")} at the same coordinate, so one plate is drawn on top of the other. Both are still in the summary list, so the mistake exists only in the picture.`,
|
|
52
|
+
field: `${component}.components[${ids[0]}]`,
|
|
53
|
+
fix: "Give each of them its own row and col, or its own pos.",
|
|
54
|
+
component
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function liveEdges(component, edges, known) {
|
|
58
|
+
if (!edges) return [];
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const edge of edges) {
|
|
61
|
+
const missing = [edge.from, edge.to].filter((id) => !known(id));
|
|
62
|
+
if (missing.length === 0) {
|
|
63
|
+
out.push(edge);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (DEV) {
|
|
67
|
+
warn({
|
|
68
|
+
code: "DIAGRAM_EDGE_DANGLING",
|
|
69
|
+
problem: `${component} was given a relationship ${edge.from} \u2192 ${edge.to}, and ${missing.map((id) => `"${id}"`).join(" and ")} is not a node the specification declares. There is nothing to draw it between, so it is left out of the picture and out of the summary.`,
|
|
70
|
+
field: `${component}.edges[${edge.from}\u2192${edge.to}]`,
|
|
71
|
+
fix: "Correct the id, or add the node it names.",
|
|
72
|
+
component
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function warnUnknownRef(component, field, id, effect) {
|
|
79
|
+
if (!DEV) return;
|
|
80
|
+
warn({
|
|
81
|
+
code: "DIAGRAM_REF_UNKNOWN",
|
|
82
|
+
problem: `${component} was given "${id}" in ${field}, which no node in the specification declares, so ${effect}.`,
|
|
83
|
+
field: `${component}.${field}`,
|
|
84
|
+
fix: "Correct the id, or add the node it names.",
|
|
85
|
+
component
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
liveEdges,
|
|
90
|
+
useSpecIdentity,
|
|
91
|
+
warnCollision,
|
|
92
|
+
warnStageOutOfRange,
|
|
93
|
+
warnUnknownLane,
|
|
94
|
+
warnUnknownRef
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=dev.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/diagrams/lib/dev.ts"],"sourcesContent":["'use client'\n\nimport { useRef } from 'react'\nimport { DEV, warn } from '../../lib/warn'\n\n/**\n * The ways a diagram specification can typecheck and draw something else.\n *\n * A figure has no runtime validator and should not grow one: the specification\n * is a plain object, the layout is arithmetic on numbers it already carries,\n * and a schema check would be a second copy of the types. What it needs instead\n * is the same thing the components have — a word at the moment the arithmetic\n * is asked to do something the author cannot have meant.\n *\n * Every case here shares one shape: the renderer CAN produce a picture, so\n * nothing throws, and the picture is not the one the specification describes.\n * A lane id nothing declares resolves to lane 0 and the step is drawn in the\n * wrong band; a stage past the last heading is drawn past the last heading; a\n * message to a participant that does not exist is drawn nowhere at all. Each of\n * those used to be silent, and silence is the one outcome ruled out: an author\n * who cannot see the difference between \"I placed this\" and \"the renderer gave\n * up\" has no way to find the mistake except by knowing what the picture should\n * have looked like.\n *\n * All of it goes through `lib/warn`, so it is dev-only, printed once per\n * problem, and dropped from a production bundle by the consumer's own bundler.\n */\n\n/**\n * Says so when a figure is handed the same object it was handed last time, with\n * different contents inside it.\n *\n * Every figure memoises its model on the specification's IDENTITY, which is the\n * only key that costs nothing to compare — and which a `spec.components.push()`\n * does not change. The figure then goes on drawing the picture it was first\n * given, through any number of re-renders, with no error and no clue.\n *\n * The fingerprint is only computed in development, and only ever compared with\n * the previous render's: it is a diagnostic, not the memo key. Making it the\n * memo key would put a `JSON.stringify` of the whole specification in front of\n * every production render, which is a far worse trade than asking the author to\n * build a new object.\n */\nexport function useSpecIdentity(spec: unknown, component: string): void {\n const last = useRef<{ spec: unknown; print: string } | null>(null)\n if (!DEV) return\n\n const print = fingerprint(spec)\n const previous = last.current\n last.current = { spec, print }\n\n if (previous && previous.spec === spec && previous.print !== print) {\n warn({\n code: 'DIAGRAM_SPEC_MUTATED',\n problem: `${component} was given the same spec object with different contents. The model is memoised on the object's identity, so the figure is still drawing what that object held on the first render.`,\n field: `${component}.spec`,\n fix: 'Build a new spec object for the change — { ...spec, nodes: [...] } — rather than mutating the one already passed.',\n component,\n })\n }\n}\n\nfunction fingerprint(value: unknown): string {\n try {\n return JSON.stringify(value) ?? ''\n } catch {\n // A spec that cannot be serialised cannot be fingerprinted either. It is\n // not this function's job to complain about that.\n return ''\n }\n}\n\n/** Says so when a node names a lane the specification never declared. */\nexport function warnUnknownLane(component: string, nodeId: string, lane: string): void {\n if (!DEV) return\n warn({\n code: 'DIAGRAM_LANE_UNKNOWN',\n problem: `${component} was given the lane \"${lane}\" on \"${nodeId}\", which no lane in the specification declares. It falls back to the first lane, so the node is drawn in the right column and the wrong band.`,\n field: `${component}.nodes[${nodeId}].lane`,\n fix: `Use one of the ids in spec.lanes, or add a lane whose id is \"${lane}\".`,\n component,\n })\n}\n\n/** Says so when a data-flow node sits past the last stage heading. */\nexport function warnStageOutOfRange(\n component: string,\n nodeId: string,\n stage: number,\n stages: number,\n): void {\n if (!DEV) return\n warn({\n code: 'DIAGRAM_STAGE_OUT_OF_RANGE',\n problem: `${component} was given stage ${stage} on \"${nodeId}\" against ${stages} declared stages. A node's x is computed from its own index, so it is drawn past the last heading — in a column the axis does not label.`,\n field: `${component}.nodes[${nodeId}].stage`,\n fix: `Use a stage between 0 and ${Math.max(0, stages - 1)}, or declare the stages the pipeline actually has.`,\n component,\n })\n}\n\n/** Says so when two boxes were placed on exactly the same coordinate. */\nexport function warnCollision(component: string, ids: string[]): void {\n if (!DEV) return\n warn({\n code: 'DIAGRAM_CELL_COLLISION',\n problem: `${component} placed ${ids.map((id) => `\"${id}\"`).join(' and ')} at the same coordinate, so one plate is drawn on top of the other. Both are still in the summary list, so the mistake exists only in the picture.`,\n field: `${component}.components[${ids[0]}]`,\n fix: 'Give each of them its own row and col, or its own pos.',\n component,\n })\n}\n\n/**\n * The edges whose two ends exist, with a word about the ones that do not.\n *\n * Routing already drops a line it cannot place — there is no box to draw it\n * between — and the summary list used to iterate the specification instead, so\n * the picture and its text equivalent disagreed: one showed nothing, the other\n * reported a relationship to an id no node claims. Both halves now read this,\n * which is what makes them agree by construction rather than by inspection.\n */\nexport function liveEdges<E extends { from: string; to: string }>(\n component: string,\n edges: E[] | undefined,\n known: (id: string) => boolean,\n): E[] {\n if (!edges) return []\n const out: E[] = []\n for (const edge of edges) {\n const missing = [edge.from, edge.to].filter((id) => !known(id))\n if (missing.length === 0) {\n out.push(edge)\n continue\n }\n if (DEV) {\n warn({\n code: 'DIAGRAM_EDGE_DANGLING',\n problem: `${component} was given a relationship ${edge.from} → ${edge.to}, and ${missing.map((id) => `\"${id}\"`).join(' and ')} is not a node the specification declares. There is nothing to draw it between, so it is left out of the picture and out of the summary.`,\n field: `${component}.edges[${edge.from}→${edge.to}]`,\n fix: 'Correct the id, or add the node it names.',\n component,\n })\n }\n }\n return out\n}\n\n/** Says so when anything else in the spec points at an id nothing declares. */\nexport function warnUnknownRef(\n component: string,\n field: string,\n id: string,\n effect: string,\n): void {\n if (!DEV) return\n warn({\n code: 'DIAGRAM_REF_UNKNOWN',\n problem: `${component} was given \"${id}\" in ${field}, which no node in the specification declares, so ${effect}.`,\n field: `${component}.${field}`,\n fix: 'Correct the id, or add the node it names.',\n component,\n })\n}\n"],"mappings":";AAEA,SAAS,cAAc;AACvB,SAAS,KAAK,YAAY;AAwCnB,SAAS,gBAAgB,MAAe,WAAyB;AACtE,QAAM,OAAO,OAAgD,IAAI;AACjE,MAAI,CAAC,IAAK;AAEV,QAAM,QAAQ,YAAY,IAAI;AAC9B,QAAM,WAAW,KAAK;AACtB,OAAK,UAAU,EAAE,MAAM,MAAM;AAE7B,MAAI,YAAY,SAAS,SAAS,QAAQ,SAAS,UAAU,OAAO;AAClE,SAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS,GAAG,SAAS;AAAA,MACrB,OAAO,GAAG,SAAS;AAAA,MACnB,KAAK;AAAA,MACL;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,SAAS,YAAY,OAAwB;AAC3C,MAAI;AACF,WAAO,KAAK,UAAU,KAAK,KAAK;AAAA,EAClC,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,gBAAgB,WAAmB,QAAgB,MAAoB;AACrF,MAAI,CAAC,IAAK;AACV,OAAK;AAAA,IACH,MAAM;AAAA,IACN,SAAS,GAAG,SAAS,wBAAwB,IAAI,SAAS,MAAM;AAAA,IAChE,OAAO,GAAG,SAAS,UAAU,MAAM;AAAA,IACnC,KAAK,gEAAgE,IAAI;AAAA,IACzE;AAAA,EACF,CAAC;AACH;AAGO,SAAS,oBACd,WACA,QACA,OACA,QACM;AACN,MAAI,CAAC,IAAK;AACV,OAAK;AAAA,IACH,MAAM;AAAA,IACN,SAAS,GAAG,SAAS,oBAAoB,KAAK,QAAQ,MAAM,aAAa,MAAM;AAAA,IAC/E,OAAO,GAAG,SAAS,UAAU,MAAM;AAAA,IACnC,KAAK,6BAA6B,KAAK,IAAI,GAAG,SAAS,CAAC,CAAC;AAAA,IACzD;AAAA,EACF,CAAC;AACH;AAGO,SAAS,cAAc,WAAmB,KAAqB;AACpE,MAAI,CAAC,IAAK;AACV,OAAK;AAAA,IACH,MAAM;AAAA,IACN,SAAS,GAAG,SAAS,WAAW,IAAI,IAAI,CAAC,OAAO,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC;AAAA,IACxE,OAAO,GAAG,SAAS,eAAe,IAAI,CAAC,CAAC;AAAA,IACxC,KAAK;AAAA,IACL;AAAA,EACF,CAAC;AACH;AAWO,SAAS,UACd,WACA,OACA,OACK;AACL,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,QAAM,MAAW,CAAC;AAClB,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,CAAC,KAAK,MAAM,KAAK,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;AAC9D,QAAI,QAAQ,WAAW,GAAG;AACxB,UAAI,KAAK,IAAI;AACb;AAAA,IACF;AACA,QAAI,KAAK;AACP,WAAK;AAAA,QACH,MAAM;AAAA,QACN,SAAS,GAAG,SAAS,6BAA6B,KAAK,IAAI,WAAM,KAAK,EAAE,SAAS,QAAQ,IAAI,CAAC,OAAO,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC;AAAA,QAC7H,OAAO,GAAG,SAAS,UAAU,KAAK,IAAI,SAAI,KAAK,EAAE;AAAA,QACjD,KAAK;AAAA,QACL;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,eACd,WACA,OACA,IACA,QACM;AACN,MAAI,CAAC,IAAK;AACV,OAAK;AAAA,IACH,MAAM;AAAA,IACN,SAAS,GAAG,SAAS,eAAe,EAAE,QAAQ,KAAK,qDAAqD,MAAM;AAAA,IAC9G,OAAO,GAAG,SAAS,IAAI,KAAK;AAAA,IAC5B,KAAK;AAAA,IACL;AAAA,EACF,CAAC;AACH;","names":[]}
|
|
@@ -16,12 +16,7 @@ interface FigureModel {
|
|
|
16
16
|
extent: Box;
|
|
17
17
|
artwork: ReactNode;
|
|
18
18
|
/** Every drawn box, for the summary list. */
|
|
19
|
-
nodes:
|
|
20
|
-
id: string;
|
|
21
|
-
label: string;
|
|
22
|
-
sublabel?: string;
|
|
23
|
-
kind?: string;
|
|
24
|
-
}[];
|
|
19
|
+
nodes: FigureNode[];
|
|
25
20
|
/** Every drawn line, for the summary list. */
|
|
26
21
|
edges: {
|
|
27
22
|
from: string;
|
|
@@ -34,6 +29,45 @@ interface FigureModel {
|
|
|
34
29
|
label: string;
|
|
35
30
|
sample: ReactNode;
|
|
36
31
|
}[];
|
|
32
|
+
/**
|
|
33
|
+
* The figure's second axis, as a PARTITION of the nodes above.
|
|
34
|
+
*
|
|
35
|
+
* A workflow's lanes and a data flow's stages are not decoration around the
|
|
36
|
+
* boxes — they are the other half of every box's position, and a summary that
|
|
37
|
+
* lists the boxes alone has published one dimension of a two-dimensional
|
|
38
|
+
* picture. Giving the list this structure says it once, in the place a screen
|
|
39
|
+
* reader already walks, rather than repeating "in the CI lane" on every row.
|
|
40
|
+
*
|
|
41
|
+
* A node may appear in at most one band: the list is the keyboard's only
|
|
42
|
+
* route to a selection, and a node in two bands is two controls that do the
|
|
43
|
+
* same thing. Anything left out of every band is published after them.
|
|
44
|
+
*/
|
|
45
|
+
bands?: FigureBand[];
|
|
46
|
+
/**
|
|
47
|
+
* What the picture says that is neither a box nor a line.
|
|
48
|
+
*
|
|
49
|
+
* Boundaries, phase captions, segment captions, activation bars: each is a
|
|
50
|
+
* real statement drawn inside an `aria-hidden` `<svg>`, and each is a
|
|
51
|
+
* sentence rather than a row, so each figure writes its own.
|
|
52
|
+
*/
|
|
53
|
+
notes?: string[];
|
|
54
|
+
}
|
|
55
|
+
/** One drawn box, as the summary publishes it. */
|
|
56
|
+
interface FigureNode {
|
|
57
|
+
id: string;
|
|
58
|
+
label: string;
|
|
59
|
+
sublabel?: string;
|
|
60
|
+
kind?: string;
|
|
61
|
+
/** A short phrase the picture prints on the box: a step number. */
|
|
62
|
+
detail?: string;
|
|
63
|
+
}
|
|
64
|
+
/** One band of a figure's second axis: a lane, a stage. */
|
|
65
|
+
interface FigureBand {
|
|
66
|
+
/** What KIND of band this is — "Lane", "Stage" — since the label is a name. */
|
|
67
|
+
kind: string;
|
|
68
|
+
label: string;
|
|
69
|
+
/** The nodes inside it, in the order the figure draws them. */
|
|
70
|
+
ids: string[];
|
|
37
71
|
}
|
|
38
72
|
interface FigureChrome {
|
|
39
73
|
className?: string;
|
|
@@ -71,6 +105,13 @@ interface FigureChrome {
|
|
|
71
105
|
* picture, and it is why this component takes a model rather than markup — a
|
|
72
106
|
* frame handed a finished `<svg>` could not have written it.
|
|
73
107
|
*
|
|
108
|
+
* The list carries the figure's SECOND AXIS as its own structure. A workflow's
|
|
109
|
+
* lanes and a data flow's stages are half of where every box sits, and they are
|
|
110
|
+
* drawn inside the presentational `<svg>` — so a flat list of boxes published
|
|
111
|
+
* one dimension of a two-dimensional picture and called it the content. Nodes
|
|
112
|
+
* are grouped by their band, and the statements that group nothing — a
|
|
113
|
+
* boundary, a phase caption, an activation bar — follow as sentences.
|
|
114
|
+
*
|
|
74
115
|
* The list is visually hidden by default and NOT hidden from assistive
|
|
75
116
|
* technology, which is the opposite of what `aria-hidden` would do and the
|
|
76
117
|
* whole point.
|
|
@@ -94,4 +135,4 @@ declare function CardGrid({ cards, className }: {
|
|
|
94
135
|
className?: string;
|
|
95
136
|
}): react.JSX.Element;
|
|
96
137
|
|
|
97
|
-
export { CardGrid, DiagramFrame, type FigureChrome, type FigureModel };
|
|
138
|
+
export { CardGrid, DiagramFrame, type FigureBand, type FigureChrome, type FigureModel, type FigureNode };
|
|
@@ -22,6 +22,10 @@ function DiagramFrame({
|
|
|
22
22
|
const byId = new Map(model.nodes.map((node) => [node.id, node]));
|
|
23
23
|
const description = `${model.nodes.length} elements and ${model.edges.length} relationships. ${meta.subtitle ?? ""}`.trim();
|
|
24
24
|
const showLegend = legend !== "hidden" && model.legend.length > 0;
|
|
25
|
+
const bands = model.bands ?? [];
|
|
26
|
+
const banded = new Set(bands.flatMap((band) => band.ids));
|
|
27
|
+
const loose = bands.length > 0 ? model.nodes.filter((node) => !banded.has(node.id)) : [];
|
|
28
|
+
const row = (node) => /* @__PURE__ */ jsx("li", { children: onSelectNode ? /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onSelectNode(node.id), children: summaryLine(node) }) : summaryLine(node) }, node.id);
|
|
25
29
|
return /* @__PURE__ */ jsxs("figure", { className: cn("m-0 flex flex-col gap-5", className), children: [
|
|
26
30
|
heading && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
|
|
27
31
|
/* @__PURE__ */ jsx("h3", { className: "m-0 font-heading text-[length:var(--fs-sub)] font-normal leading-[1.15] tracking-[-0.015em] text-(--ink)", children: meta.title }),
|
|
@@ -45,13 +49,23 @@ function DiagramFrame({
|
|
|
45
49
|
) }),
|
|
46
50
|
/* @__PURE__ */ jsxs("div", { className: "sr-only", children: [
|
|
47
51
|
/* @__PURE__ */ jsx("p", { children: description }),
|
|
48
|
-
|
|
52
|
+
bands.length > 0 ? /* @__PURE__ */ jsxs("ul", { children: [
|
|
53
|
+
bands.map((band, index) => /* @__PURE__ */ jsxs("li", { children: [
|
|
54
|
+
/* @__PURE__ */ jsx("span", { children: `${band.kind}: ${band.label}` }),
|
|
55
|
+
/* @__PURE__ */ jsx("ul", { children: band.ids.flatMap((id) => {
|
|
56
|
+
const node = byId.get(id);
|
|
57
|
+
return node ? [row(node)] : [];
|
|
58
|
+
}) })
|
|
59
|
+
] }, `${band.label}-${index}`)),
|
|
60
|
+
loose.length > 0 && /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("ul", { children: loose.map(row) }) })
|
|
61
|
+
] }) : /* @__PURE__ */ jsx("ul", { children: model.nodes.map(row) }),
|
|
49
62
|
model.edges.length > 0 && /* @__PURE__ */ jsx("ul", { children: model.edges.map((edge, index) => /* @__PURE__ */ jsxs("li", { children: [
|
|
50
63
|
byId.get(edge.from)?.label ?? edge.from,
|
|
51
64
|
" \u2192 ",
|
|
52
65
|
byId.get(edge.to)?.label ?? edge.to,
|
|
53
66
|
edge.label ? `: ${edge.label}` : ""
|
|
54
|
-
] }, index)) })
|
|
67
|
+
] }, index)) }),
|
|
68
|
+
model.notes && model.notes.length > 0 && /* @__PURE__ */ jsx("ul", { children: model.notes.map((note, index) => /* @__PURE__ */ jsx("li", { children: note }, index)) })
|
|
55
69
|
] }),
|
|
56
70
|
showLegend && /* @__PURE__ */ jsx(DiagramLegend, { entries: model.legend }),
|
|
57
71
|
showCards && cards && cards.length > 0 && /* @__PURE__ */ jsx(CardGrid, { cards }),
|
|
@@ -62,6 +76,7 @@ function summaryLine(node) {
|
|
|
62
76
|
const parts = [node.label];
|
|
63
77
|
if (node.kind) parts.push(`(${node.kind})`);
|
|
64
78
|
if (node.sublabel) parts.push(`\u2014 ${node.sublabel}`);
|
|
79
|
+
if (node.detail) parts.push(`\u2014 ${node.detail}`);
|
|
65
80
|
return parts.join(" ");
|
|
66
81
|
}
|
|
67
82
|
function CardGrid({ cards, className }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/diagrams/lib/frame.tsx"],"sourcesContent":["'use client'\n\nimport { useId, type ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport type { DiagramCard, DiagramMeta, LegendMode } from '../spec'\nimport { viewBoxOf, type Box } from './geometry'\nimport { DiagramLegend } from '../DiagramLegend/DiagramLegend'\nimport { DiagramDefs } from './marks'\n\n/**\n * What a figure hands the frame around it.\n *\n * Five renderers produce this one shape, which is what lets the title, the\n * accessible summary, the key and the conclusion cards be written once. A\n * renderer's only job is placing marks; everything a reader needs AROUND the\n * picture is the same question in all five cases.\n */\nexport interface FigureModel {\n /** The extent the artwork was drawn into. */\n extent: Box\n artwork: ReactNode\n /** Every drawn box, for the summary list. */\n nodes: { id: string; label: string; sublabel?: string; kind?: string }[]\n /** Every drawn line, for the summary list. */\n edges: { from: string; to: string; label?: string }[]\n /** The kinds actually used, and how each is drawn. */\n legend: { key: string; label: string; sample: ReactNode }[]\n}\n\nexport interface FigureChrome {\n className?: string\n /** Prints the key under the figure. `auto` shows only the kinds in use. */\n legend?: LegendMode\n /** Prints the conclusion cards under the figure. Defaults to true. */\n cards?: boolean\n /** Prints the title and subtitle over the figure. Defaults to true. */\n heading?: boolean\n /** Node ids currently lit. Everything else is dimmed. */\n activeIds?: string[]\n /**\n * Called when a reader picks a node.\n *\n * Giving this turns the summary list from a description into a control: the\n * same list a screen reader reads becomes the keyboard's route to selection,\n * and the plates in the picture become the pointer's route to the same thing.\n */\n onSelectNode?: (id: string) => void\n}\n\n/**\n * The shell every figure sits in: a heading, the picture, a key, the cards, and\n * the part that makes the picture mean anything to a reader who cannot see it.\n *\n * ## The accessibility decision, stated rather than implied\n *\n * The `<svg>` carries `role=\"img\"` and a label, which makes its whole subtree\n * presentational. That is the honest description of what it is — a picture —\n * and it is what stops a screen reader from walking two hundred `<text>`\n * elements in drawing order, which is neither reading order nor useful.\n *\n * The diagram's CONTENT is then published beside it as an ordinary list: every\n * node with what kind of thing it is, and every relationship as \"A → B: over\n * HTTPS\". That list is where the meaning lives for anyone not looking at the\n * picture, and it is why this component takes a model rather than markup — a\n * frame handed a finished `<svg>` could not have written it.\n *\n * The list is visually hidden by default and NOT hidden from assistive\n * technology, which is the opposite of what `aria-hidden` would do and the\n * whole point.\n */\nexport function DiagramFrame({\n meta,\n model,\n cards,\n className,\n legend = 'auto',\n showCards = true,\n heading = true,\n activeIds,\n onSelectNode,\n}: {\n meta: DiagramMeta\n model: FigureModel\n cards?: DiagramCard[]\n showCards?: boolean\n} & Omit<FigureChrome, 'cards'>) {\n const uid = useId().replace(/:/g, '')\n const titleId = `${uid}-title`\n const descId = `${uid}-desc`\n\n const byId = new Map(model.nodes.map((node) => [node.id, node]))\n const description = `${model.nodes.length} elements and ${model.edges.length} relationships. ${\n meta.subtitle ?? ''\n }`.trim()\n\n const showLegend = legend !== 'hidden' && model.legend.length > 0\n\n return (\n <figure className={cn('m-0 flex flex-col gap-5', className)}>\n {/* The title is set in the editorial serif, which is the system's own\n answer to \"this is a heading\" — and it is what stops a figure from\n reading as a screenshot dropped into the page. The subtitle stays in\n the meta voice underneath it. */}\n {heading && (\n <div className=\"flex flex-col gap-1.5\">\n <h3 className=\"m-0 font-heading text-[length:var(--fs-sub)] font-normal leading-[1.15] tracking-[-0.015em] text-(--ink)\">\n {meta.title}\n </h3>\n {meta.subtitle && <span className=\"mono-meta text-(--ink-3-aa)\">{meta.subtitle}</span>}\n </div>\n )}\n\n <div className=\"overflow-x-auto rounded-(--radius-lg) border border-(--rule) bg-(--diagram-surface) p-[clamp(0.75rem,calc(1.6*var(--fluid)),1.25rem)] scroll-slim\">\n <svg\n viewBox={viewBoxOf(model.extent)}\n role=\"img\"\n aria-labelledby={`${titleId} ${descId}`}\n className=\"block h-auto w-full max-w-full font-sans\"\n data-diagram-artwork=\"\"\n >\n <title id={titleId}>{meta.title}</title>\n <desc id={descId}>{description}</desc>\n <DiagramDefs uid={uid} />\n {model.artwork}\n </svg>\n </div>\n\n {/* The picture's content, for anyone not looking at the picture. */}\n <div className=\"sr-only\">\n <p>{description}</p>\n <ul>\n {model.nodes.map((node) => (\n <li key={node.id}>\n {onSelectNode ? (\n <button type=\"button\" onClick={() => onSelectNode(node.id)}>\n {summaryLine(node)}\n </button>\n ) : (\n summaryLine(node)\n )}\n </li>\n ))}\n </ul>\n {model.edges.length > 0 && (\n <ul>\n {model.edges.map((edge, index) => (\n <li key={index}>\n {(byId.get(edge.from)?.label ?? edge.from)} → {(byId.get(edge.to)?.label ?? edge.to)}\n {edge.label ? `: ${edge.label}` : ''}\n </li>\n ))}\n </ul>\n )}\n </div>\n\n {showLegend && <DiagramLegend entries={model.legend} />}\n\n {showCards && cards && cards.length > 0 && <CardGrid cards={cards} />}\n\n {activeIds && activeIds.length > 0 && <span className=\"sr-only\" role=\"status\">{`${activeIds.length} highlighted`}</span>}\n </figure>\n )\n}\n\nfunction summaryLine(node: { label: string; sublabel?: string; kind?: string }): string {\n const parts = [node.label]\n if (node.kind) parts.push(`(${node.kind})`)\n if (node.sublabel) parts.push(`— ${node.sublabel}`)\n return parts.join(' ')\n}\n\n/**\n * The conclusion cards under a figure.\n *\n * Archify keys these by hue — cyan, emerald, rose. Here the key selects a MARK:\n * a filled square, a ring, a bar, a diagonal. Seven cards in a row stay seven\n * distinguishable cards on paper-white, and the same row survives being\n * printed.\n */\nexport function CardGrid({ cards, className }: { cards: DiagramCard[]; className?: string }) {\n return (\n <div\n className={cn(\n 'grid gap-3 sm:grid-cols-2 lg:grid-cols-3',\n className,\n )}\n >\n {cards.map((card) => (\n <div key={card.title} className=\"flex flex-col gap-2.5 border-t border-(--ink) pt-3\">\n <div className=\"flex items-center gap-2\">\n <CardMark shape={card.dot} />\n <span className=\"font-sans text-[13.5px] leading-[1.35] text-(--ink)\">{card.title}</span>\n </div>\n <ul className=\"m-0 flex list-none flex-col gap-1.5 p-0\">\n {card.items.map((item, index) => (\n <li\n key={index}\n className=\"text-[12.5px] leading-[1.55] text-(--ink-2) ps-3 -indent-3 before:content-['—'] before:pe-1.5 before:text-(--ink-3-aa)\"\n >\n {item}\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n )\n}\n\n/** Seven marks, one per archify dot name, and not one of them a hue. */\nfunction CardMark({ shape }: { shape: DiagramCard['dot'] }) {\n const common = 'size-2.5 shrink-0'\n switch (shape) {\n case 'cyan':\n return <span aria-hidden className={cn(common, 'bg-(--ink)')} />\n case 'emerald':\n return <span aria-hidden className={cn(common, 'rounded-full bg-(--ink)')} />\n case 'violet':\n return <span aria-hidden className={cn(common, 'rounded-full border border-(--ink)')} />\n case 'amber':\n return <span aria-hidden className={cn(common, 'border border-(--ink)')} />\n case 'rose':\n return <span aria-hidden className={cn(common, 'rotate-45 bg-(--ink)')} />\n case 'orange':\n return <span aria-hidden className={cn(common, 'rotate-45 border border-(--ink)')} />\n case 'slate':\n default:\n return <span aria-hidden className={cn('h-1 w-2.5 shrink-0 bg-(--ink-3-aa)')} />\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/diagrams/lib/frame.tsx"],"sourcesContent":["'use client'\n\nimport { useId, type ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport type { DiagramCard, DiagramMeta, LegendMode } from '../spec'\nimport { viewBoxOf, type Box } from './geometry'\nimport { DiagramLegend } from '../DiagramLegend/DiagramLegend'\nimport { DiagramDefs } from './marks'\n\n/**\n * What a figure hands the frame around it.\n *\n * Five renderers produce this one shape, which is what lets the title, the\n * accessible summary, the key and the conclusion cards be written once. A\n * renderer's only job is placing marks; everything a reader needs AROUND the\n * picture is the same question in all five cases.\n */\nexport interface FigureModel {\n /** The extent the artwork was drawn into. */\n extent: Box\n artwork: ReactNode\n /** Every drawn box, for the summary list. */\n nodes: FigureNode[]\n /** Every drawn line, for the summary list. */\n edges: { from: string; to: string; label?: string }[]\n /** The kinds actually used, and how each is drawn. */\n legend: { key: string; label: string; sample: ReactNode }[]\n /**\n * The figure's second axis, as a PARTITION of the nodes above.\n *\n * A workflow's lanes and a data flow's stages are not decoration around the\n * boxes — they are the other half of every box's position, and a summary that\n * lists the boxes alone has published one dimension of a two-dimensional\n * picture. Giving the list this structure says it once, in the place a screen\n * reader already walks, rather than repeating \"in the CI lane\" on every row.\n *\n * A node may appear in at most one band: the list is the keyboard's only\n * route to a selection, and a node in two bands is two controls that do the\n * same thing. Anything left out of every band is published after them.\n */\n bands?: FigureBand[]\n /**\n * What the picture says that is neither a box nor a line.\n *\n * Boundaries, phase captions, segment captions, activation bars: each is a\n * real statement drawn inside an `aria-hidden` `<svg>`, and each is a\n * sentence rather than a row, so each figure writes its own.\n */\n notes?: string[]\n}\n\n/** One drawn box, as the summary publishes it. */\nexport interface FigureNode {\n id: string\n label: string\n sublabel?: string\n kind?: string\n /** A short phrase the picture prints on the box: a step number. */\n detail?: string\n}\n\n/** One band of a figure's second axis: a lane, a stage. */\nexport interface FigureBand {\n /** What KIND of band this is — \"Lane\", \"Stage\" — since the label is a name. */\n kind: string\n label: string\n /** The nodes inside it, in the order the figure draws them. */\n ids: string[]\n}\n\nexport interface FigureChrome {\n className?: string\n /** Prints the key under the figure. `auto` shows only the kinds in use. */\n legend?: LegendMode\n /** Prints the conclusion cards under the figure. Defaults to true. */\n cards?: boolean\n /** Prints the title and subtitle over the figure. Defaults to true. */\n heading?: boolean\n /** Node ids currently lit. Everything else is dimmed. */\n activeIds?: string[]\n /**\n * Called when a reader picks a node.\n *\n * Giving this turns the summary list from a description into a control: the\n * same list a screen reader reads becomes the keyboard's route to selection,\n * and the plates in the picture become the pointer's route to the same thing.\n */\n onSelectNode?: (id: string) => void\n}\n\n/**\n * The shell every figure sits in: a heading, the picture, a key, the cards, and\n * the part that makes the picture mean anything to a reader who cannot see it.\n *\n * ## The accessibility decision, stated rather than implied\n *\n * The `<svg>` carries `role=\"img\"` and a label, which makes its whole subtree\n * presentational. That is the honest description of what it is — a picture —\n * and it is what stops a screen reader from walking two hundred `<text>`\n * elements in drawing order, which is neither reading order nor useful.\n *\n * The diagram's CONTENT is then published beside it as an ordinary list: every\n * node with what kind of thing it is, and every relationship as \"A → B: over\n * HTTPS\". That list is where the meaning lives for anyone not looking at the\n * picture, and it is why this component takes a model rather than markup — a\n * frame handed a finished `<svg>` could not have written it.\n *\n * The list carries the figure's SECOND AXIS as its own structure. A workflow's\n * lanes and a data flow's stages are half of where every box sits, and they are\n * drawn inside the presentational `<svg>` — so a flat list of boxes published\n * one dimension of a two-dimensional picture and called it the content. Nodes\n * are grouped by their band, and the statements that group nothing — a\n * boundary, a phase caption, an activation bar — follow as sentences.\n *\n * The list is visually hidden by default and NOT hidden from assistive\n * technology, which is the opposite of what `aria-hidden` would do and the\n * whole point.\n */\nexport function DiagramFrame({\n meta,\n model,\n cards,\n className,\n legend = 'auto',\n showCards = true,\n heading = true,\n activeIds,\n onSelectNode,\n}: {\n meta: DiagramMeta\n model: FigureModel\n cards?: DiagramCard[]\n showCards?: boolean\n} & Omit<FigureChrome, 'cards'>) {\n const uid = useId().replace(/:/g, '')\n const titleId = `${uid}-title`\n const descId = `${uid}-desc`\n\n const byId = new Map(model.nodes.map((node) => [node.id, node]))\n const description = `${model.nodes.length} elements and ${model.edges.length} relationships. ${\n meta.subtitle ?? ''\n }`.trim()\n\n const showLegend = legend !== 'hidden' && model.legend.length > 0\n\n // Bands partition the nodes; anything a figure left out of every band is\n // still published, after them. A node dropped from the list entirely would be\n // a node a keyboard cannot reach.\n const bands = model.bands ?? []\n const banded = new Set(bands.flatMap((band) => band.ids))\n const loose = bands.length > 0 ? model.nodes.filter((node) => !banded.has(node.id)) : []\n\n const row = (node: FigureNode) => (\n <li key={node.id}>\n {onSelectNode ? (\n <button type=\"button\" onClick={() => onSelectNode(node.id)}>\n {summaryLine(node)}\n </button>\n ) : (\n summaryLine(node)\n )}\n </li>\n )\n\n return (\n <figure className={cn('m-0 flex flex-col gap-5', className)}>\n {/* The title is set in the editorial serif, which is the system's own\n answer to \"this is a heading\" — and it is what stops a figure from\n reading as a screenshot dropped into the page. The subtitle stays in\n the meta voice underneath it. */}\n {heading && (\n <div className=\"flex flex-col gap-1.5\">\n <h3 className=\"m-0 font-heading text-[length:var(--fs-sub)] font-normal leading-[1.15] tracking-[-0.015em] text-(--ink)\">\n {meta.title}\n </h3>\n {meta.subtitle && <span className=\"mono-meta text-(--ink-3-aa)\">{meta.subtitle}</span>}\n </div>\n )}\n\n <div className=\"overflow-x-auto rounded-(--radius-lg) border border-(--rule) bg-(--diagram-surface) p-[clamp(0.75rem,calc(1.6*var(--fluid)),1.25rem)] scroll-slim\">\n <svg\n viewBox={viewBoxOf(model.extent)}\n role=\"img\"\n aria-labelledby={`${titleId} ${descId}`}\n className=\"block h-auto w-full max-w-full font-sans\"\n data-diagram-artwork=\"\"\n >\n <title id={titleId}>{meta.title}</title>\n <desc id={descId}>{description}</desc>\n <DiagramDefs uid={uid} />\n {model.artwork}\n </svg>\n </div>\n\n {/* The picture's content, for anyone not looking at the picture. */}\n <div className=\"sr-only\">\n <p>{description}</p>\n {bands.length > 0 ? (\n <ul>\n {bands.map((band, index) => (\n <li key={`${band.label}-${index}`}>\n <span>{`${band.kind}: ${band.label}`}</span>\n <ul>\n {band.ids.flatMap((id) => {\n const node = byId.get(id)\n return node ? [row(node)] : []\n })}\n </ul>\n </li>\n ))}\n {loose.length > 0 && (\n <li>\n <ul>{loose.map(row)}</ul>\n </li>\n )}\n </ul>\n ) : (\n <ul>{model.nodes.map(row)}</ul>\n )}\n {model.edges.length > 0 && (\n <ul>\n {model.edges.map((edge, index) => (\n <li key={index}>\n {(byId.get(edge.from)?.label ?? edge.from)} → {(byId.get(edge.to)?.label ?? edge.to)}\n {edge.label ? `: ${edge.label}` : ''}\n </li>\n ))}\n </ul>\n )}\n {model.notes && model.notes.length > 0 && (\n <ul>\n {model.notes.map((note, index) => (\n <li key={index}>{note}</li>\n ))}\n </ul>\n )}\n </div>\n\n {showLegend && <DiagramLegend entries={model.legend} />}\n\n {showCards && cards && cards.length > 0 && <CardGrid cards={cards} />}\n\n {activeIds && activeIds.length > 0 && <span className=\"sr-only\" role=\"status\">{`${activeIds.length} highlighted`}</span>}\n </figure>\n )\n}\n\nfunction summaryLine(node: FigureNode): string {\n const parts = [node.label]\n if (node.kind) parts.push(`(${node.kind})`)\n if (node.sublabel) parts.push(`— ${node.sublabel}`)\n if (node.detail) parts.push(`— ${node.detail}`)\n return parts.join(' ')\n}\n\n/**\n * The conclusion cards under a figure.\n *\n * Archify keys these by hue — cyan, emerald, rose. Here the key selects a MARK:\n * a filled square, a ring, a bar, a diagonal. Seven cards in a row stay seven\n * distinguishable cards on paper-white, and the same row survives being\n * printed.\n */\nexport function CardGrid({ cards, className }: { cards: DiagramCard[]; className?: string }) {\n return (\n <div\n className={cn(\n 'grid gap-3 sm:grid-cols-2 lg:grid-cols-3',\n className,\n )}\n >\n {cards.map((card) => (\n <div key={card.title} className=\"flex flex-col gap-2.5 border-t border-(--ink) pt-3\">\n <div className=\"flex items-center gap-2\">\n <CardMark shape={card.dot} />\n <span className=\"font-sans text-[13.5px] leading-[1.35] text-(--ink)\">{card.title}</span>\n </div>\n <ul className=\"m-0 flex list-none flex-col gap-1.5 p-0\">\n {card.items.map((item, index) => (\n <li\n key={index}\n className=\"text-[12.5px] leading-[1.55] text-(--ink-2) ps-3 -indent-3 before:content-['—'] before:pe-1.5 before:text-(--ink-3-aa)\"\n >\n {item}\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n )\n}\n\n/** Seven marks, one per archify dot name, and not one of them a hue. */\nfunction CardMark({ shape }: { shape: DiagramCard['dot'] }) {\n const common = 'size-2.5 shrink-0'\n switch (shape) {\n case 'cyan':\n return <span aria-hidden className={cn(common, 'bg-(--ink)')} />\n case 'emerald':\n return <span aria-hidden className={cn(common, 'rounded-full bg-(--ink)')} />\n case 'violet':\n return <span aria-hidden className={cn(common, 'rounded-full border border-(--ink)')} />\n case 'amber':\n return <span aria-hidden className={cn(common, 'border border-(--ink)')} />\n case 'rose':\n return <span aria-hidden className={cn(common, 'rotate-45 bg-(--ink)')} />\n case 'orange':\n return <span aria-hidden className={cn(common, 'rotate-45 border border-(--ink)')} />\n case 'slate':\n default:\n return <span aria-hidden className={cn('h-1 w-2.5 shrink-0 bg-(--ink-3-aa)')} />\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -48,6 +48,27 @@ interface RoutedEdge {
|
|
|
48
48
|
/** The last segment's direction, for the arrowhead. */
|
|
49
49
|
angle: number;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* The face a line should use when the author did not say.
|
|
53
|
+
*
|
|
54
|
+
* Whichever axis separates the two boxes by more wins, and the choice is made
|
|
55
|
+
* on the GAP rather than on centre distance. Two boxes side by side with a
|
|
56
|
+
* small vertical offset separate horizontally, which is what a reader expects;
|
|
57
|
+
* comparing centres would flip that pair to top/bottom as soon as the offset
|
|
58
|
+
* exceeded half a box.
|
|
59
|
+
*/
|
|
60
|
+
declare function autoSides(from: Box, to: Box): [Side, Side];
|
|
61
|
+
/**
|
|
62
|
+
* The two faces a line will actually use, author's choice or the router's.
|
|
63
|
+
*
|
|
64
|
+
* Exported because the SPREAD has to key on the real face and cannot wait for
|
|
65
|
+
* the router to decide it. Keyed on the word "auto" instead — which is what it
|
|
66
|
+
* used to do — every auto-routed line leaving one node counted as sharing one
|
|
67
|
+
* face, however far apart the faces they resolved to were. A node with one line
|
|
68
|
+
* going right and one going down had both of them nudged off centre to make
|
|
69
|
+
* room for each other on a face neither was using.
|
|
70
|
+
*/
|
|
71
|
+
declare function resolveSides(from: Box, to: Box, fromSide?: Side, toSide?: Side): [Side, Side];
|
|
51
72
|
/**
|
|
52
73
|
* The polyline as a path, with the corners turned rather than mitred.
|
|
53
74
|
*
|
|
@@ -82,4 +103,4 @@ interface PortUse {
|
|
|
82
103
|
*/
|
|
83
104
|
declare function spreadPorts(uses: PortUse[]): Map<string, number>;
|
|
84
105
|
|
|
85
|
-
export { type PortUse, type RouteRequest, type RoutedEdge, roundedPath, routeEdge, spreadPorts };
|
|
106
|
+
export { type PortUse, type RouteRequest, type RoutedEdge, autoSides, resolveSides, roundedPath, routeEdge, spreadPorts };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { round } from "./geometry.js";
|
|
2
2
|
const STUB = 18;
|
|
3
3
|
const CORNER = 8;
|
|
4
|
+
const STRAIGHT_SNAP = CORNER * 2;
|
|
4
5
|
function autoSides(from, to) {
|
|
5
6
|
const gapX = Math.max(to.x - (from.x + from.w), from.x - (to.x + to.w));
|
|
6
7
|
const gapY = Math.max(to.y - (from.y + from.h), from.y - (to.y + to.h));
|
|
@@ -9,6 +10,10 @@ function autoSides(from, to) {
|
|
|
9
10
|
}
|
|
10
11
|
return to.y >= from.y ? ["bottom", "top"] : ["top", "bottom"];
|
|
11
12
|
}
|
|
13
|
+
function resolveSides(from, to, fromSide, toSide) {
|
|
14
|
+
const [autoFrom, autoTo] = autoSides(from, to);
|
|
15
|
+
return [fromSide ?? autoFrom, toSide ?? autoTo];
|
|
16
|
+
}
|
|
12
17
|
function port(box, side, offset = 0.5) {
|
|
13
18
|
const t = Math.min(0.88, Math.max(0.12, offset));
|
|
14
19
|
switch (side) {
|
|
@@ -57,6 +62,20 @@ function connect(a, b, fromSide, toSide, from, to, channelX, channelY) {
|
|
|
57
62
|
}
|
|
58
63
|
return fromH ? [a, [b[0], a[1]], b] : [a, [a[0], b[1]], b];
|
|
59
64
|
}
|
|
65
|
+
function straighten(start, end, fromSide, toSide, to) {
|
|
66
|
+
const fromH = isHorizontal(fromSide);
|
|
67
|
+
if (fromH !== isHorizontal(toSide)) return end;
|
|
68
|
+
const opposed = fromH ? fromSide === "right" !== (toSide === "right") : fromSide === "bottom" !== (toSide === "bottom");
|
|
69
|
+
if (!opposed) return end;
|
|
70
|
+
if (fromH) {
|
|
71
|
+
if (Math.abs(start[1] - end[1]) > STRAIGHT_SNAP) return end;
|
|
72
|
+
if (start[1] < to.y + to.h * 0.12 || start[1] > to.y + to.h * 0.88) return end;
|
|
73
|
+
return [end[0], start[1]];
|
|
74
|
+
}
|
|
75
|
+
if (Math.abs(start[0] - end[0]) > STRAIGHT_SNAP) return end;
|
|
76
|
+
if (start[0] < to.x + to.w * 0.12 || start[0] > to.x + to.w * 0.88) return end;
|
|
77
|
+
return [start[0], end[1]];
|
|
78
|
+
}
|
|
60
79
|
function simplify(points) {
|
|
61
80
|
const out = [];
|
|
62
81
|
for (const point of points) {
|
|
@@ -126,7 +145,9 @@ function routeEdge(request) {
|
|
|
126
145
|
const fromSide = request.fromSide ?? autoFrom;
|
|
127
146
|
const toSide = request.toSide ?? autoTo;
|
|
128
147
|
const start = port(from, fromSide, request.fromOffset);
|
|
129
|
-
const
|
|
148
|
+
const rawEnd = port(to, toSide, request.toOffset);
|
|
149
|
+
const spread = request.fromOffset !== void 0 || request.toOffset !== void 0;
|
|
150
|
+
const end = spread ? rawEnd : straighten(start, rawEnd, fromSide, toSide, to);
|
|
130
151
|
let points;
|
|
131
152
|
if (mode === "straight") {
|
|
132
153
|
points = [start, end];
|
|
@@ -171,6 +192,8 @@ function spreadPorts(uses) {
|
|
|
171
192
|
return offsets;
|
|
172
193
|
}
|
|
173
194
|
export {
|
|
195
|
+
autoSides,
|
|
196
|
+
resolveSides,
|
|
174
197
|
roundedPath,
|
|
175
198
|
routeEdge,
|
|
176
199
|
spreadPorts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/diagrams/lib/route.ts"],"sourcesContent":["/**\n * Getting a line from one box to another.\n *\n * The routing rule this module keeps, and the reason it is worth having at all:\n * A SIDE IS A DIRECTION CONTRACT. A line that says it leaves a box's right face\n * must leave it going right, and a line that says it arrives at a top face must\n * arrive going down. A router that takes the shortest path between two anchor\n * points breaks that contract constantly — it produces a segment that grazes\n * the box it just left, and a reader cannot tell which of two adjacent boxes an\n * arrow belongs to. Every path below therefore begins and ends with a stub\n * perpendicular to its face, and the interesting geometry happens between the\n * two stubs.\n *\n * The second rule is about crowding. When several lines share one face they are\n * spread across it rather than stacked on its midpoint, because two arrowheads\n * landing on the same coordinate is one arrowhead as far as a reader is\n * concerned. Explicit routing controls opt out of the spread: an author who\n * pinned a channel has already decided where the line goes.\n */\n\nimport type { Point, RouteMode, Side } from '../spec'\nimport { round, type Box } from './geometry'\n\n/** How far a line runs straight out of a face before it is allowed to turn. */\nconst STUB = 18\n\n/** The turn radius on an orthogonal corner. */\nconst CORNER = 8\n\nexport interface RouteRequest {\n from: Box\n to: Box\n fromSide?: Side\n toSide?: Side\n mode?: RouteMode | string\n via?: Point[]\n channelX?: number\n channelY?: number\n /** 0–1 along the face, where this line's port sits. 0.5 is the midpoint. */\n fromOffset?: number\n toOffset?: number\n}\n\nexport interface RoutedEdge {\n /** The `d` attribute, corners already rounded. */\n d: string\n /** The polyline before rounding — what the label placer reads. */\n points: [number, number][]\n /** Where a label wants to sit, and how the segment under it runs. */\n label: { x: number; y: number; axis: 'x' | 'y' }\n /** The last segment's direction, for the arrowhead. */\n angle: number\n}\n\n/**\n * The face a line should use when the author did not say.\n *\n * Whichever axis separates the two boxes by more wins, and the choice is made\n * on the GAP rather than on centre distance. Two boxes side by side with a\n * small vertical offset separate horizontally, which is what a reader expects;\n * comparing centres would flip that pair to top/bottom as soon as the offset\n * exceeded half a box.\n */\nfunction autoSides(from: Box, to: Box): [Side, Side] {\n const gapX = Math.max(to.x - (from.x + from.w), from.x - (to.x + to.w))\n const gapY = Math.max(to.y - (from.y + from.h), from.y - (to.y + to.h))\n\n if (gapX >= gapY) {\n return to.x >= from.x ? ['right', 'left'] : ['left', 'right']\n }\n return to.y >= from.y ? ['bottom', 'top'] : ['top', 'bottom']\n}\n\n/** The point on a face where a line attaches, given its share of that face. */\nfunction port(box: Box, side: Side, offset = 0.5): [number, number] {\n const t = Math.min(0.88, Math.max(0.12, offset))\n switch (side) {\n case 'left':\n return [box.x, box.y + box.h * t]\n case 'right':\n return [box.x + box.w, box.y + box.h * t]\n case 'top':\n return [box.x + box.w * t, box.y]\n case 'bottom':\n default:\n return [box.x + box.w * t, box.y + box.h]\n }\n}\n\n/** One step out of a face, perpendicular to it. */\nfunction stubFrom(anchor: [number, number], side: Side, length = STUB): [number, number] {\n switch (side) {\n case 'left':\n return [anchor[0] - length, anchor[1]]\n case 'right':\n return [anchor[0] + length, anchor[1]]\n case 'top':\n return [anchor[0], anchor[1] - length]\n case 'bottom':\n default:\n return [anchor[0], anchor[1] + length]\n }\n}\n\nconst isHorizontal = (side: Side) => side === 'left' || side === 'right'\n\n/**\n * The polyline between two stub ends, kept orthogonal.\n *\n * Three cases, and they are the only three an axis-aligned router has:\n *\n * - **Facing across the same axis** (right→left, bottom→top). One dogleg\n * through a channel halfway between them, which is the shape everybody draws\n * by hand.\n * - **Turning a corner** (right→top). A single elbow, and WHICH elbow is\n * decided by the outgoing axis: leaving a vertical face means running\n * vertically first, or the line re-enters the box it just left.\n * - **Same-side or back-tracking** (right→right, or right→left with the target\n * behind). The channel goes OUTSIDE both boxes rather than between them,\n * because there is no room between them to go through.\n */\nfunction connect(\n a: [number, number],\n b: [number, number],\n fromSide: Side,\n toSide: Side,\n from: Box,\n to: Box,\n channelX?: number,\n channelY?: number,\n): [number, number][] {\n const fromH = isHorizontal(fromSide)\n const toH = isHorizontal(toSide)\n\n if (fromH && toH) {\n const forward =\n (fromSide === 'right' && b[0] >= a[0]) || (fromSide === 'left' && b[0] <= a[0])\n const x =\n channelX ??\n (forward\n ? (a[0] + b[0]) / 2\n : // Behind: run out past the far edge of both boxes and come back.\n fromSide === 'right'\n ? Math.max(a[0], to.x + to.w + STUB, from.x + from.w + STUB)\n : Math.min(a[0], to.x - STUB, from.x - STUB))\n if (Math.abs(a[1] - b[1]) < 0.5) return [a, b]\n return [a, [x, a[1]], [x, b[1]], b]\n }\n\n if (!fromH && !toH) {\n const forward =\n (fromSide === 'bottom' && b[1] >= a[1]) || (fromSide === 'top' && b[1] <= a[1])\n const y =\n channelY ??\n (forward\n ? (a[1] + b[1]) / 2\n : fromSide === 'bottom'\n ? Math.max(a[1], to.y + to.h + STUB, from.y + from.h + STUB)\n : Math.min(a[1], to.y - STUB, from.y - STUB))\n if (Math.abs(a[0] - b[0]) < 0.5) return [a, b]\n return [a, [a[0], y], [b[0], y], b]\n }\n\n // One elbow. The axis of the OUTGOING face decides which corner it is.\n return fromH ? [a, [b[0], a[1]], b] : [a, [a[0], b[1]], b]\n}\n\n/** Drops points that repeat, and collapses three points on one straight run into two. */\nfunction simplify(points: [number, number][]): [number, number][] {\n const out: [number, number][] = []\n for (const point of points) {\n const last = out[out.length - 1]\n if (last && Math.abs(last[0] - point[0]) < 0.5 && Math.abs(last[1] - point[1]) < 0.5) continue\n out.push(point)\n }\n for (let index = 1; index < out.length - 1; ) {\n const [ax, ay] = out[index - 1]!\n const [bx, by] = out[index]!\n const [cx, cy] = out[index + 1]!\n const collinear =\n (Math.abs(ax - bx) < 0.5 && Math.abs(bx - cx) < 0.5) ||\n (Math.abs(ay - by) < 0.5 && Math.abs(by - cy) < 0.5)\n if (collinear) out.splice(index, 1)\n else index += 1\n }\n return out\n}\n\n/**\n * The polyline as a path, with the corners turned rather than mitred.\n *\n * A quadratic through the corner point, pulled back along both legs by the\n * radius. The pull-back is clamped to a third of each leg so a short segment\n * between two turns cannot produce a curve that overshoots its own endpoint —\n * which renders as a hook and is the classic artefact of rounding a polyline\n * without checking the leg lengths.\n */\nexport function roundedPath(points: [number, number][], radius = CORNER): string {\n if (points.length === 0) return ''\n if (points.length === 1) return `M ${round(points[0]![0])} ${round(points[0]![1])}`\n\n let d = `M ${round(points[0]![0])} ${round(points[0]![1])}`\n\n for (let index = 1; index < points.length - 1; index += 1) {\n const previous = points[index - 1]!\n const corner = points[index]!\n const next = points[index + 1]!\n\n const inLength = Math.hypot(corner[0] - previous[0], corner[1] - previous[1])\n const outLength = Math.hypot(next[0] - corner[0], next[1] - corner[1])\n const r = Math.min(radius, inLength / 3, outLength / 3)\n\n if (r < 1) {\n d += ` L ${round(corner[0])} ${round(corner[1])}`\n continue\n }\n\n const enter: [number, number] = [\n corner[0] + ((previous[0] - corner[0]) / inLength) * r,\n corner[1] + ((previous[1] - corner[1]) / inLength) * r,\n ]\n const leave: [number, number] = [\n corner[0] + ((next[0] - corner[0]) / outLength) * r,\n corner[1] + ((next[1] - corner[1]) / outLength) * r,\n ]\n\n d += ` L ${round(enter[0])} ${round(enter[1])}`\n d += ` Q ${round(corner[0])} ${round(corner[1])} ${round(leave[0])} ${round(leave[1])}`\n }\n\n const end = points[points.length - 1]!\n return `${d} L ${round(end[0])} ${round(end[1])}`\n}\n\n/**\n * Where the line's wording goes: the middle of its longest segment.\n *\n * The longest segment is chosen rather than the path's own midpoint because the\n * midpoint of a dogleg often lands on a corner, and a label on a corner sits\n * across both legs and masks the turn — which is the one part of the line that\n * carries information about where it went.\n */\nfunction labelSpot(points: [number, number][]): { x: number; y: number; axis: 'x' | 'y' } {\n let best = { x: points[0]![0], y: points[0]![1], axis: 'x' as 'x' | 'y', length: -1 }\n\n for (let index = 0; index < points.length - 1; index += 1) {\n const a = points[index]!\n const b = points[index + 1]!\n const length = Math.hypot(b[0] - a[0], b[1] - a[1])\n if (length > best.length) {\n best = {\n x: (a[0] + b[0]) / 2,\n y: (a[1] + b[1]) / 2,\n axis: Math.abs(b[0] - a[0]) >= Math.abs(b[1] - a[1]) ? 'x' : 'y',\n length,\n }\n }\n }\n\n return { x: best.x, y: best.y, axis: best.axis }\n}\n\n/** Routes one relationship. */\nexport function routeEdge(request: RouteRequest): RoutedEdge {\n const { from, to, via, channelX, channelY, mode = 'auto' } = request\n\n const [autoFrom, autoTo] = autoSides(from, to)\n const fromSide = request.fromSide ?? autoFrom\n const toSide = request.toSide ?? autoTo\n\n const start = port(from, fromSide, request.fromOffset)\n const end = port(to, toSide, request.toOffset)\n\n let points: [number, number][]\n\n if (mode === 'straight') {\n // Straight means straight: centre to centre, clipped to both faces. No\n // stubs, because a stub would bend the one route whose whole promise is\n // that it does not bend.\n points = [start, end]\n } else {\n const outward = stubFrom(start, fromSide)\n const inward = stubFrom(end, toSide)\n\n if (via && via.length > 0) {\n // Waypoints are honoured literally and joined orthogonally. An author who\n // reached for `via` is overruling the router, and a router that then\n // \"improved\" the result would make the control useless.\n points = [start, outward]\n let cursor: [number, number] = outward\n for (const waypoint of via) {\n const next: [number, number] = [waypoint[0], waypoint[1]]\n points.push([next[0], cursor[1]], next)\n cursor = next\n }\n points.push([inward[0], cursor[1]], inward, end)\n } else {\n const forced =\n mode === 'orthogonal-h' ? ('h' as const) : mode === 'orthogonal-v' ? ('v' as const) : null\n\n const middle =\n forced === 'h'\n ? ([outward, [inward[0], outward[1]], inward] as [number, number][])\n : forced === 'v'\n ? ([outward, [outward[0], inward[1]], inward] as [number, number][])\n : connect(outward, inward, fromSide, toSide, from, to, channelX, channelY)\n\n points = [start, ...middle, end]\n }\n }\n\n points = simplify(points)\n\n const last = points[points.length - 1]!\n const previous = points[points.length - 2] ?? last\n const angle = (Math.atan2(last[1] - previous[1], last[0] - previous[0]) * 180) / Math.PI\n\n return { d: roundedPath(points), points, label: labelSpot(points), angle }\n}\n\n/** One line's use of one face: which face, and which end of which line it is. */\nexport interface PortUse {\n /** The face, as `nodeId:side`. Two lines sharing this string share a face. */\n face: string\n /** This endpoint, as `edgeIndex:from` or `edgeIndex:to`. */\n ref: string\n}\n\n/**\n * Which share of its face each line gets, when several use the same one.\n *\n * A single line keeps the midpoint — spreading one line only moves it off\n * centre for no reason. Lines that carry an explicit routing control are\n * excluded by the caller before they get here, so a pinned channel is never\n * nudged out from under its pin.\n *\n * Endpoints are keyed separately rather than by line, because a line's two ends\n * land on two different faces with two different amounts of company. Keying by\n * line gave both ends the same offset, which on a figure with one crowded face\n * and one empty one visibly skewed every arrow that touched the empty side.\n */\nexport function spreadPorts(uses: PortUse[]): Map<string, number> {\n const byFace = new Map<string, string[]>()\n for (const use of uses) {\n const list = byFace.get(use.face)\n if (list) list.push(use.ref)\n else byFace.set(use.face, [use.ref])\n }\n\n const offsets = new Map<string, number>()\n for (const list of byFace.values()) {\n if (list.length < 2) continue\n list.forEach((ref, position) => {\n offsets.set(ref, (position + 1) / (list.length + 1))\n })\n }\n return offsets\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/diagrams/lib/route.ts"],"sourcesContent":["/**\n * Getting a line from one box to another.\n *\n * The routing rule this module keeps, and the reason it is worth having at all:\n * A SIDE IS A DIRECTION CONTRACT. A line that says it leaves a box's right face\n * must leave it going right, and a line that says it arrives at a top face must\n * arrive going down. A router that takes the shortest path between two anchor\n * points breaks that contract constantly — it produces a segment that grazes\n * the box it just left, and a reader cannot tell which of two adjacent boxes an\n * arrow belongs to. Every path below therefore begins and ends with a stub\n * perpendicular to its face, and the interesting geometry happens between the\n * two stubs.\n *\n * The second rule is about crowding. When several lines share one face they are\n * spread across it rather than stacked on its midpoint, because two arrowheads\n * landing on the same coordinate is one arrowhead as far as a reader is\n * concerned. Explicit routing controls opt out of the spread: an author who\n * pinned a channel has already decided where the line goes.\n */\n\nimport type { Point, RouteMode, Side } from '../spec'\nimport { round, type Box } from './geometry'\n\n/** How far a line runs straight out of a face before it is allowed to turn. */\nconst STUB = 18\n\n/** The turn radius on an orthogonal corner. */\nconst CORNER = 8\n\n/**\n * How far out of true a line may be and still be drawn straight.\n *\n * Two boxes on the same row are rarely on exactly the same centre — a taller\n * label, a different icon, a row that grew by four units — and a router that\n * insists on the arithmetic answers a four-unit difference with a dogleg: two\n * corners, an S, and a line that looks like it is avoiding something. It is\n * not; it is being honest about four units, which is a fact no reader wanted.\n *\n * Two corner radii is the threshold, because that is the shortest dogleg that\n * can draw itself. Below it `roundedPath` clamps both curves to a third of a\n * leg already shorter than the radius, so what renders is not a pair of turns\n * but a kink. Straightening it loses nothing and removes the only thing in the\n * picture that was inviting a reader to wonder why.\n */\nconst STRAIGHT_SNAP = CORNER * 2\n\nexport interface RouteRequest {\n from: Box\n to: Box\n fromSide?: Side\n toSide?: Side\n mode?: RouteMode | string\n via?: Point[]\n channelX?: number\n channelY?: number\n /** 0–1 along the face, where this line's port sits. 0.5 is the midpoint. */\n fromOffset?: number\n toOffset?: number\n}\n\nexport interface RoutedEdge {\n /** The `d` attribute, corners already rounded. */\n d: string\n /** The polyline before rounding — what the label placer reads. */\n points: [number, number][]\n /** Where a label wants to sit, and how the segment under it runs. */\n label: { x: number; y: number; axis: 'x' | 'y' }\n /** The last segment's direction, for the arrowhead. */\n angle: number\n}\n\n/**\n * The face a line should use when the author did not say.\n *\n * Whichever axis separates the two boxes by more wins, and the choice is made\n * on the GAP rather than on centre distance. Two boxes side by side with a\n * small vertical offset separate horizontally, which is what a reader expects;\n * comparing centres would flip that pair to top/bottom as soon as the offset\n * exceeded half a box.\n */\nexport function autoSides(from: Box, to: Box): [Side, Side] {\n const gapX = Math.max(to.x - (from.x + from.w), from.x - (to.x + to.w))\n const gapY = Math.max(to.y - (from.y + from.h), from.y - (to.y + to.h))\n\n if (gapX >= gapY) {\n return to.x >= from.x ? ['right', 'left'] : ['left', 'right']\n }\n return to.y >= from.y ? ['bottom', 'top'] : ['top', 'bottom']\n}\n\n/**\n * The two faces a line will actually use, author's choice or the router's.\n *\n * Exported because the SPREAD has to key on the real face and cannot wait for\n * the router to decide it. Keyed on the word \"auto\" instead — which is what it\n * used to do — every auto-routed line leaving one node counted as sharing one\n * face, however far apart the faces they resolved to were. A node with one line\n * going right and one going down had both of them nudged off centre to make\n * room for each other on a face neither was using.\n */\nexport function resolveSides(\n from: Box,\n to: Box,\n fromSide?: Side,\n toSide?: Side,\n): [Side, Side] {\n const [autoFrom, autoTo] = autoSides(from, to)\n return [fromSide ?? autoFrom, toSide ?? autoTo]\n}\n\n/** The point on a face where a line attaches, given its share of that face. */\nfunction port(box: Box, side: Side, offset = 0.5): [number, number] {\n const t = Math.min(0.88, Math.max(0.12, offset))\n switch (side) {\n case 'left':\n return [box.x, box.y + box.h * t]\n case 'right':\n return [box.x + box.w, box.y + box.h * t]\n case 'top':\n return [box.x + box.w * t, box.y]\n case 'bottom':\n default:\n return [box.x + box.w * t, box.y + box.h]\n }\n}\n\n/** One step out of a face, perpendicular to it. */\nfunction stubFrom(anchor: [number, number], side: Side, length = STUB): [number, number] {\n switch (side) {\n case 'left':\n return [anchor[0] - length, anchor[1]]\n case 'right':\n return [anchor[0] + length, anchor[1]]\n case 'top':\n return [anchor[0], anchor[1] - length]\n case 'bottom':\n default:\n return [anchor[0], anchor[1] + length]\n }\n}\n\nconst isHorizontal = (side: Side) => side === 'left' || side === 'right'\n\n/**\n * The polyline between two stub ends, kept orthogonal.\n *\n * Three cases, and they are the only three an axis-aligned router has:\n *\n * - **Facing across the same axis** (right→left, bottom→top). One dogleg\n * through a channel halfway between them, which is the shape everybody draws\n * by hand.\n * - **Turning a corner** (right→top). A single elbow, and WHICH elbow is\n * decided by the outgoing axis: leaving a vertical face means running\n * vertically first, or the line re-enters the box it just left.\n * - **Same-side or back-tracking** (right→right, or right→left with the target\n * behind). The channel goes OUTSIDE both boxes rather than between them,\n * because there is no room between them to go through.\n */\nfunction connect(\n a: [number, number],\n b: [number, number],\n fromSide: Side,\n toSide: Side,\n from: Box,\n to: Box,\n channelX?: number,\n channelY?: number,\n): [number, number][] {\n const fromH = isHorizontal(fromSide)\n const toH = isHorizontal(toSide)\n\n if (fromH && toH) {\n const forward =\n (fromSide === 'right' && b[0] >= a[0]) || (fromSide === 'left' && b[0] <= a[0])\n const x =\n channelX ??\n (forward\n ? (a[0] + b[0]) / 2\n : // Behind: run out past the far edge of both boxes and come back.\n fromSide === 'right'\n ? Math.max(a[0], to.x + to.w + STUB, from.x + from.w + STUB)\n : Math.min(a[0], to.x - STUB, from.x - STUB))\n if (Math.abs(a[1] - b[1]) < 0.5) return [a, b]\n return [a, [x, a[1]], [x, b[1]], b]\n }\n\n if (!fromH && !toH) {\n const forward =\n (fromSide === 'bottom' && b[1] >= a[1]) || (fromSide === 'top' && b[1] <= a[1])\n const y =\n channelY ??\n (forward\n ? (a[1] + b[1]) / 2\n : fromSide === 'bottom'\n ? Math.max(a[1], to.y + to.h + STUB, from.y + from.h + STUB)\n : Math.min(a[1], to.y - STUB, from.y - STUB))\n if (Math.abs(a[0] - b[0]) < 0.5) return [a, b]\n return [a, [a[0], y], [b[0], y], b]\n }\n\n // One elbow. The axis of the OUTGOING face decides which corner it is.\n return fromH ? [a, [b[0], a[1]], b] : [a, [a[0], b[1]], b]\n}\n\n/**\n * Moves the arriving port onto the leaving one, when they are nearly in line.\n *\n * Only for faces that oppose each other on one axis — a corner turn is a real\n * turn and keeps its elbow — and only within the target face's usable span, so\n * a snap can never slide an arrowhead off the end of the box it points at.\n * Returns the port unchanged when neither holds, and the router then draws the\n * dogleg it would have drawn.\n *\n * The END moves rather than the start: a reader follows a line from its source,\n * and the source is where the emphasis and the label usually are.\n */\nfunction straighten(\n start: [number, number],\n end: [number, number],\n fromSide: Side,\n toSide: Side,\n to: Box,\n): [number, number] {\n const fromH = isHorizontal(fromSide)\n if (fromH !== isHorizontal(toSide)) return end\n // Facing each other, not the same side twice: right→right is a line that has\n // to go around, and there is nothing there to straighten.\n const opposed = fromH\n ? (fromSide === 'right') !== (toSide === 'right')\n : (fromSide === 'bottom') !== (toSide === 'bottom')\n if (!opposed) return end\n\n if (fromH) {\n if (Math.abs(start[1] - end[1]) > STRAIGHT_SNAP) return end\n if (start[1] < to.y + to.h * 0.12 || start[1] > to.y + to.h * 0.88) return end\n return [end[0], start[1]]\n }\n\n if (Math.abs(start[0] - end[0]) > STRAIGHT_SNAP) return end\n if (start[0] < to.x + to.w * 0.12 || start[0] > to.x + to.w * 0.88) return end\n return [start[0], end[1]]\n}\n\n/** Drops points that repeat, and collapses three points on one straight run into two. */\nfunction simplify(points: [number, number][]): [number, number][] {\n const out: [number, number][] = []\n for (const point of points) {\n const last = out[out.length - 1]\n if (last && Math.abs(last[0] - point[0]) < 0.5 && Math.abs(last[1] - point[1]) < 0.5) continue\n out.push(point)\n }\n for (let index = 1; index < out.length - 1; ) {\n const [ax, ay] = out[index - 1]!\n const [bx, by] = out[index]!\n const [cx, cy] = out[index + 1]!\n const collinear =\n (Math.abs(ax - bx) < 0.5 && Math.abs(bx - cx) < 0.5) ||\n (Math.abs(ay - by) < 0.5 && Math.abs(by - cy) < 0.5)\n if (collinear) out.splice(index, 1)\n else index += 1\n }\n return out\n}\n\n/**\n * The polyline as a path, with the corners turned rather than mitred.\n *\n * A quadratic through the corner point, pulled back along both legs by the\n * radius. The pull-back is clamped to a third of each leg so a short segment\n * between two turns cannot produce a curve that overshoots its own endpoint —\n * which renders as a hook and is the classic artefact of rounding a polyline\n * without checking the leg lengths.\n */\nexport function roundedPath(points: [number, number][], radius = CORNER): string {\n if (points.length === 0) return ''\n if (points.length === 1) return `M ${round(points[0]![0])} ${round(points[0]![1])}`\n\n let d = `M ${round(points[0]![0])} ${round(points[0]![1])}`\n\n for (let index = 1; index < points.length - 1; index += 1) {\n const previous = points[index - 1]!\n const corner = points[index]!\n const next = points[index + 1]!\n\n const inLength = Math.hypot(corner[0] - previous[0], corner[1] - previous[1])\n const outLength = Math.hypot(next[0] - corner[0], next[1] - corner[1])\n const r = Math.min(radius, inLength / 3, outLength / 3)\n\n if (r < 1) {\n d += ` L ${round(corner[0])} ${round(corner[1])}`\n continue\n }\n\n const enter: [number, number] = [\n corner[0] + ((previous[0] - corner[0]) / inLength) * r,\n corner[1] + ((previous[1] - corner[1]) / inLength) * r,\n ]\n const leave: [number, number] = [\n corner[0] + ((next[0] - corner[0]) / outLength) * r,\n corner[1] + ((next[1] - corner[1]) / outLength) * r,\n ]\n\n d += ` L ${round(enter[0])} ${round(enter[1])}`\n d += ` Q ${round(corner[0])} ${round(corner[1])} ${round(leave[0])} ${round(leave[1])}`\n }\n\n const end = points[points.length - 1]!\n return `${d} L ${round(end[0])} ${round(end[1])}`\n}\n\n/**\n * Where the line's wording goes: the middle of its longest segment.\n *\n * The longest segment is chosen rather than the path's own midpoint because the\n * midpoint of a dogleg often lands on a corner, and a label on a corner sits\n * across both legs and masks the turn — which is the one part of the line that\n * carries information about where it went.\n */\nfunction labelSpot(points: [number, number][]): { x: number; y: number; axis: 'x' | 'y' } {\n let best = { x: points[0]![0], y: points[0]![1], axis: 'x' as 'x' | 'y', length: -1 }\n\n for (let index = 0; index < points.length - 1; index += 1) {\n const a = points[index]!\n const b = points[index + 1]!\n const length = Math.hypot(b[0] - a[0], b[1] - a[1])\n if (length > best.length) {\n best = {\n x: (a[0] + b[0]) / 2,\n y: (a[1] + b[1]) / 2,\n axis: Math.abs(b[0] - a[0]) >= Math.abs(b[1] - a[1]) ? 'x' : 'y',\n length,\n }\n }\n }\n\n return { x: best.x, y: best.y, axis: best.axis }\n}\n\n/** Routes one relationship. */\nexport function routeEdge(request: RouteRequest): RoutedEdge {\n const { from, to, via, channelX, channelY, mode = 'auto' } = request\n\n const [autoFrom, autoTo] = autoSides(from, to)\n const fromSide = request.fromSide ?? autoFrom\n const toSide = request.toSide ?? autoTo\n\n const start = port(from, fromSide, request.fromOffset)\n const rawEnd = port(to, toSide, request.toOffset)\n // Not when either end was spread. Spreading exists to keep two arrowheads off\n // one coordinate, and a snap is exactly the move that would put them back —\n // so a face with company keeps its arithmetic.\n const spread = request.fromOffset !== undefined || request.toOffset !== undefined\n const end = spread ? rawEnd : straighten(start, rawEnd, fromSide, toSide, to)\n\n let points: [number, number][]\n\n if (mode === 'straight') {\n // Straight means straight: centre to centre, clipped to both faces. No\n // stubs, because a stub would bend the one route whose whole promise is\n // that it does not bend.\n points = [start, end]\n } else {\n const outward = stubFrom(start, fromSide)\n const inward = stubFrom(end, toSide)\n\n if (via && via.length > 0) {\n // Waypoints are honoured literally and joined orthogonally. An author who\n // reached for `via` is overruling the router, and a router that then\n // \"improved\" the result would make the control useless.\n points = [start, outward]\n let cursor: [number, number] = outward\n for (const waypoint of via) {\n const next: [number, number] = [waypoint[0], waypoint[1]]\n points.push([next[0], cursor[1]], next)\n cursor = next\n }\n points.push([inward[0], cursor[1]], inward, end)\n } else {\n const forced =\n mode === 'orthogonal-h' ? ('h' as const) : mode === 'orthogonal-v' ? ('v' as const) : null\n\n const middle =\n forced === 'h'\n ? ([outward, [inward[0], outward[1]], inward] as [number, number][])\n : forced === 'v'\n ? ([outward, [outward[0], inward[1]], inward] as [number, number][])\n : connect(outward, inward, fromSide, toSide, from, to, channelX, channelY)\n\n points = [start, ...middle, end]\n }\n }\n\n points = simplify(points)\n\n const last = points[points.length - 1]!\n const previous = points[points.length - 2] ?? last\n const angle = (Math.atan2(last[1] - previous[1], last[0] - previous[0]) * 180) / Math.PI\n\n return { d: roundedPath(points), points, label: labelSpot(points), angle }\n}\n\n/** One line's use of one face: which face, and which end of which line it is. */\nexport interface PortUse {\n /** The face, as `nodeId:side`. Two lines sharing this string share a face. */\n face: string\n /** This endpoint, as `edgeIndex:from` or `edgeIndex:to`. */\n ref: string\n}\n\n/**\n * Which share of its face each line gets, when several use the same one.\n *\n * A single line keeps the midpoint — spreading one line only moves it off\n * centre for no reason. Lines that carry an explicit routing control are\n * excluded by the caller before they get here, so a pinned channel is never\n * nudged out from under its pin.\n *\n * Endpoints are keyed separately rather than by line, because a line's two ends\n * land on two different faces with two different amounts of company. Keying by\n * line gave both ends the same offset, which on a figure with one crowded face\n * and one empty one visibly skewed every arrow that touched the empty side.\n */\nexport function spreadPorts(uses: PortUse[]): Map<string, number> {\n const byFace = new Map<string, string[]>()\n for (const use of uses) {\n const list = byFace.get(use.face)\n if (list) list.push(use.ref)\n else byFace.set(use.face, [use.ref])\n }\n\n const offsets = new Map<string, number>()\n for (const list of byFace.values()) {\n if (list.length < 2) continue\n list.forEach((ref, position) => {\n offsets.set(ref, (position + 1) / (list.length + 1))\n })\n }\n return offsets\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { edgeClasses, EdgeLabel, edgeMarker, edgeStroke } from "./marks.js";
|
|
3
|
-
import { routeEdge, spreadPorts } from "./route.js";
|
|
3
|
+
import { resolveSides, routeEdge, spreadPorts } from "./route.js";
|
|
4
4
|
function renderWires(wires, uid) {
|
|
5
5
|
const uses = [];
|
|
6
6
|
wires.forEach((wire, index) => {
|
|
7
7
|
const { edge } = wire;
|
|
8
8
|
const pinned = wire.keepWaypoints === true && Boolean(edge.via?.length || edge.channelX !== void 0 || edge.channelY !== void 0);
|
|
9
9
|
if (pinned) return;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const [fromSide, toSide] = resolveSides(
|
|
11
|
+
wire.from,
|
|
12
|
+
wire.to,
|
|
13
|
+
edge.fromSide,
|
|
14
|
+
edge.toSide
|
|
15
|
+
);
|
|
16
|
+
uses.push({ face: `${edge.from}:${fromSide}`, ref: `${index}:from` });
|
|
17
|
+
uses.push({ face: `${edge.to}:${toSide}`, ref: `${index}:to` });
|
|
14
18
|
});
|
|
15
19
|
const offsets = spreadPorts(uses);
|
|
16
20
|
const routed = wires.map((wire, index) => {
|