@misoto22/design 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +87 -0
- package/CLAUDE.md +1 -0
- package/README.md +50 -5
- package/bin/misoto22-design.mjs +59 -13
- package/dist/agent/Accordion.md +24 -1
- package/dist/agent/Alert.md +23 -1
- package/dist/agent/AppShell.md +30 -2
- package/dist/agent/ArchitectureFigure.md +25 -2
- package/dist/agent/AreaChart.md +27 -2
- package/dist/agent/Article.md +26 -3
- package/dist/agent/AspectRatio.md +57 -0
- package/dist/agent/Avatar.md +25 -5
- package/dist/agent/Badge.md +22 -1
- package/dist/agent/BarChart.md +29 -2
- package/dist/agent/BarList.md +27 -1
- package/dist/agent/BigNumber.md +26 -2
- package/dist/agent/BoxPlot.md +24 -1
- package/dist/agent/Breadcrumb.md +24 -2
- package/dist/agent/BulletChart.md +23 -1
- package/dist/agent/Button.md +25 -1
- package/dist/agent/Calendar.md +37 -3
- package/dist/agent/Card.md +24 -2
- package/dist/agent/Checkbox.md +23 -2
- package/dist/agent/Code.md +50 -0
- package/dist/agent/CodeBlock.md +74 -0
- package/dist/agent/Collapsible.md +22 -1
- package/dist/agent/Combobox.md +29 -3
- package/dist/agent/Command.md +25 -2
- package/dist/agent/ComposedChart.md +26 -2
- package/dist/agent/ContextMenu.md +37 -4
- package/dist/agent/DataflowFigure.md +22 -1
- package/dist/agent/DatePicker.md +34 -6
- package/dist/agent/DescriptionList.md +68 -0
- package/dist/agent/Diagram.md +27 -3
- package/dist/agent/DiagramCanvas.md +24 -1
- package/dist/agent/DiagramExportMenu.md +29 -3
- package/dist/agent/DiagramInspector.md +23 -1
- package/dist/agent/DiagramLegend.md +22 -1
- package/dist/agent/DiagramMinimap.md +24 -4
- package/dist/agent/DiagramToolbar.md +23 -1
- package/dist/agent/Dialog.md +26 -3
- package/dist/agent/DropdownMenu.md +45 -6
- package/dist/agent/EmptyState.md +26 -1
- package/dist/agent/ErrorState.md +27 -2
- package/dist/agent/Facet.md +25 -1
- package/dist/agent/Field.md +45 -4
- package/dist/agent/FigureBand.md +24 -1
- package/dist/agent/FloatingIconButton.md +23 -1
- package/dist/agent/FunnelChart.md +23 -1
- package/dist/agent/Heading.md +64 -0
- package/dist/agent/Heatmap.md +25 -1
- package/dist/agent/Histogram.md +26 -2
- package/dist/agent/Input.md +22 -1
- package/dist/agent/Kbd.md +19 -1
- package/dist/agent/LifecycleFigure.md +22 -1
- package/dist/agent/LineChart.md +26 -2
- package/dist/agent/LinkArrow.md +20 -1
- package/dist/agent/Markdown.md +68 -0
- package/dist/agent/NativeSelect.md +23 -2
- package/dist/agent/NavItem.md +21 -1
- package/dist/agent/Pagination.md +28 -2
- package/dist/agent/PieChart.md +26 -2
- package/dist/agent/Popover.md +23 -1
- package/dist/agent/Progress.md +27 -3
- package/dist/agent/RadarChart.md +26 -2
- package/dist/agent/RadialChart.md +26 -3
- package/dist/agent/RadioGroup.md +23 -2
- package/dist/agent/SankeyChart.md +25 -2
- package/dist/agent/ScatterChart.md +27 -2
- package/dist/agent/ScrollArea.md +24 -3
- package/dist/agent/SearchableMenu.md +23 -1
- package/dist/agent/Select.md +29 -3
- package/dist/agent/Separator.md +28 -4
- package/dist/agent/SequenceFigure.md +23 -1
- package/dist/agent/Sheet.md +25 -2
- package/dist/agent/Skeleton.md +24 -2
- package/dist/agent/Slider.md +27 -4
- package/dist/agent/Sparkline.md +23 -1
- package/dist/agent/Spinner.md +24 -2
- package/dist/agent/StatusDot.md +20 -1
- package/dist/agent/StatusPill.md +28 -2
- package/dist/agent/Steps.md +24 -1
- package/dist/agent/Switch.md +21 -1
- package/dist/agent/Table.md +26 -3
- package/dist/agent/Tabs.md +24 -2
- package/dist/agent/Tag.md +40 -9
- package/dist/agent/Text.md +65 -0
- package/dist/agent/Textarea.md +20 -1
- package/dist/agent/Timestamp.md +64 -0
- package/dist/agent/Toast.md +26 -2
- package/dist/agent/ToggleGroup.md +24 -2
- package/dist/agent/Toolbar.md +71 -0
- package/dist/agent/Tooltip.md +23 -1
- package/dist/agent/TreemapChart.md +24 -1
- package/dist/agent/WaterfallChart.md +24 -1
- package/dist/agent/WorkflowFigure.md +24 -2
- package/dist/agent/catalog.json +5602 -745
- package/dist/agent/index.md +13 -4
- package/dist/article.css +16 -4
- package/dist/charts/AreaChart/AreaChart.d.ts +9 -2
- package/dist/charts/AreaChart/AreaChart.js +12 -13
- package/dist/charts/AreaChart/AreaChart.js.map +1 -1
- package/dist/charts/BarChart/BarChart.d.ts +14 -3
- package/dist/charts/BarChart/BarChart.js +20 -12
- package/dist/charts/BarChart/BarChart.js.map +1 -1
- package/dist/charts/BarList/BarList.d.ts +9 -1
- package/dist/charts/BarList/BarList.js +9 -1
- package/dist/charts/BarList/BarList.js.map +1 -1
- package/dist/charts/BigNumber/BigNumber.d.ts +19 -3
- package/dist/charts/BigNumber/BigNumber.js +25 -5
- package/dist/charts/BigNumber/BigNumber.js.map +1 -1
- package/dist/charts/BoxPlot/BoxPlot.js +4 -2
- package/dist/charts/BoxPlot/BoxPlot.js.map +1 -1
- package/dist/charts/BulletChart/BulletChart.js +28 -7
- package/dist/charts/BulletChart/BulletChart.js.map +1 -1
- package/dist/charts/ComposedChart/ComposedChart.d.ts +9 -2
- package/dist/charts/ComposedChart/ComposedChart.js +10 -10
- package/dist/charts/ComposedChart/ComposedChart.js.map +1 -1
- package/dist/charts/FunnelChart/FunnelChart.d.ts +7 -2
- package/dist/charts/FunnelChart/FunnelChart.js +6 -16
- package/dist/charts/FunnelChart/FunnelChart.js.map +1 -1
- package/dist/charts/Heatmap/Heatmap.d.ts +17 -3
- package/dist/charts/Heatmap/Heatmap.js +20 -11
- package/dist/charts/Heatmap/Heatmap.js.map +1 -1
- package/dist/charts/Histogram/Histogram.d.ts +24 -1
- package/dist/charts/Histogram/Histogram.js +43 -11
- package/dist/charts/Histogram/Histogram.js.map +1 -1
- package/dist/charts/LineChart/LineChart.d.ts +9 -2
- package/dist/charts/LineChart/LineChart.js +11 -12
- package/dist/charts/LineChart/LineChart.js.map +1 -1
- package/dist/charts/PieChart/PieChart.d.ts +15 -3
- package/dist/charts/PieChart/PieChart.js +14 -6
- package/dist/charts/PieChart/PieChart.js.map +1 -1
- package/dist/charts/RadarChart/RadarChart.d.ts +15 -3
- package/dist/charts/RadarChart/RadarChart.js +11 -9
- package/dist/charts/RadarChart/RadarChart.js.map +1 -1
- package/dist/charts/RadialChart/RadialChart.d.ts +22 -4
- package/dist/charts/RadialChart/RadialChart.js +28 -6
- package/dist/charts/RadialChart/RadialChart.js.map +1 -1
- package/dist/charts/SankeyChart/SankeyChart.d.ts +16 -4
- package/dist/charts/SankeyChart/SankeyChart.js +14 -6
- package/dist/charts/SankeyChart/SankeyChart.js.map +1 -1
- package/dist/charts/ScatterChart/ScatterChart.d.ts +24 -3
- package/dist/charts/ScatterChart/ScatterChart.js +21 -15
- package/dist/charts/ScatterChart/ScatterChart.js.map +1 -1
- package/dist/charts/Sparkline/Sparkline.js +5 -3
- package/dist/charts/Sparkline/Sparkline.js.map +1 -1
- package/dist/charts/TreemapChart/TreemapChart.d.ts +7 -2
- package/dist/charts/TreemapChart/TreemapChart.js +20 -11
- package/dist/charts/TreemapChart/TreemapChart.js.map +1 -1
- package/dist/charts/WaterfallChart/WaterfallChart.js +4 -2
- package/dist/charts/WaterfallChart/WaterfallChart.js.map +1 -1
- package/dist/charts/lib/export.d.ts +1 -0
- package/dist/charts/lib/figure.d.ts +21 -1
- package/dist/charts/lib/figure.js +5 -2
- package/dist/charts/lib/figure.js.map +1 -1
- package/dist/charts/lib/scale.d.ts +23 -0
- package/dist/charts/lib/scale.js +13 -0
- package/dist/charts/lib/scale.js.map +1 -0
- package/dist/charts/lib/selection.d.ts +20 -0
- package/dist/charts/lib/selection.js +18 -0
- package/dist/charts/lib/selection.js.map +1 -0
- package/dist/charts/lib/toolbar.d.ts +1 -0
- package/dist/components/Accordion/Accordion.js +8 -1
- package/dist/components/Accordion/Accordion.js.map +1 -1
- package/dist/components/AppShell/AppShell.d.ts +6 -0
- package/dist/components/AppShell/AppShell.js +34 -4
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/Article/Article.d.ts +11 -4
- package/dist/components/Article/Article.js +13 -0
- package/dist/components/Article/Article.js.map +1 -1
- package/dist/components/AspectRatio/AspectRatio.d.ts +46 -0
- package/dist/components/AspectRatio/AspectRatio.js +33 -0
- package/dist/components/AspectRatio/AspectRatio.js.map +1 -0
- package/dist/components/Avatar/Avatar.d.ts +12 -3
- package/dist/components/Avatar/Avatar.js +6 -1
- package/dist/components/Avatar/Avatar.js.map +1 -1
- package/dist/components/Breadcrumb/Breadcrumb.d.ts +6 -0
- package/dist/components/Breadcrumb/Breadcrumb.js +15 -0
- package/dist/components/Breadcrumb/Breadcrumb.js.map +1 -1
- package/dist/components/Button/Button.js +14 -0
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.d.ts +29 -3
- package/dist/components/Calendar/Calendar.js +60 -12
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Card/Card.d.ts +6 -0
- package/dist/components/Card/Card.js +1 -1
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts +5 -2
- package/dist/components/Checkbox/Checkbox.js +12 -2
- package/dist/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/components/Code/Code.d.ts +30 -0
- package/dist/components/Code/Code.js +21 -0
- package/dist/components/Code/Code.js.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.d.ts +141 -0
- package/dist/components/CodeBlock/CodeBlock.js +123 -0
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -0
- package/dist/components/Collapsible/Collapsible.js +10 -2
- package/dist/components/Collapsible/Collapsible.js.map +1 -1
- package/dist/components/Combobox/Combobox.d.ts +15 -1
- package/dist/components/Combobox/Combobox.js +25 -5
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.d.ts +4 -2
- package/dist/components/Command/Command.js +13 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.d.ts +30 -5
- package/dist/components/ContextMenu/ContextMenu.js +32 -2
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.d.ts +36 -4
- package/dist/components/DatePicker/DatePicker.js +55 -12
- package/dist/components/DatePicker/DatePicker.js.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.d.ts +73 -0
- package/dist/components/DescriptionList/DescriptionList.js +35 -0
- package/dist/components/DescriptionList/DescriptionList.js.map +1 -0
- package/dist/components/Diagram/Diagram.d.ts +8 -3
- package/dist/components/Diagram/Diagram.js +82 -10
- package/dist/components/Diagram/Diagram.js.map +1 -1
- package/dist/components/Dialog/Dialog.d.ts +18 -3
- package/dist/components/Dialog/Dialog.js +20 -2
- package/dist/components/Dialog/Dialog.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.d.ts +49 -11
- package/dist/components/DropdownMenu/DropdownMenu.js +32 -2
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/EmptyState/EmptyState.d.ts +25 -1
- package/dist/components/EmptyState/EmptyState.js +3 -1
- package/dist/components/EmptyState/EmptyState.js.map +1 -1
- package/dist/components/ErrorState/ErrorState.d.ts +22 -1
- package/dist/components/ErrorState/ErrorState.js +11 -2
- package/dist/components/ErrorState/ErrorState.js.map +1 -1
- package/dist/components/Field/Field.d.ts +70 -3
- package/dist/components/Field/Field.js +53 -12
- package/dist/components/Field/Field.js.map +1 -1
- package/dist/components/Field/field-control.d.ts +31 -0
- package/dist/components/Field/field-control.js +12 -0
- package/dist/components/Field/field-control.js.map +1 -0
- package/dist/components/FloatingIconButton/FloatingIconButton.js +2 -0
- package/dist/components/FloatingIconButton/FloatingIconButton.js.map +1 -1
- package/dist/components/Heading/Heading.d.ts +62 -0
- package/dist/components/Heading/Heading.js +44 -0
- package/dist/components/Heading/Heading.js.map +1 -0
- package/dist/components/Markdown/Markdown.d.ts +139 -0
- package/dist/components/Markdown/Markdown.js +135 -0
- package/dist/components/Markdown/Markdown.js.map +1 -0
- package/dist/components/Markdown/inline.d.ts +29 -0
- package/dist/components/Markdown/inline.js +74 -0
- package/dist/components/Markdown/inline.js.map +1 -0
- package/dist/components/Markdown/nodes.d.ts +65 -0
- package/dist/components/Markdown/nodes.js +1 -0
- package/dist/components/Markdown/nodes.js.map +1 -0
- package/dist/components/Markdown/parse.d.ts +24 -0
- package/dist/components/Markdown/parse.js +110 -0
- package/dist/components/Markdown/parse.js.map +1 -0
- package/dist/components/Markdown/slug.d.ts +15 -0
- package/dist/components/Markdown/slug.js +7 -0
- package/dist/components/Markdown/slug.js.map +1 -0
- package/dist/components/NativeSelect/NativeSelect.d.ts +6 -0
- package/dist/components/NativeSelect/NativeSelect.js +2 -3
- package/dist/components/NativeSelect/NativeSelect.js.map +1 -1
- package/dist/components/Pagination/Pagination.d.ts +24 -1
- package/dist/components/Pagination/Pagination.js +6 -3
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/Progress/Progress.d.ts +17 -5
- package/dist/components/Progress/Progress.js +18 -6
- package/dist/components/Progress/Progress.js.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts +6 -0
- package/dist/components/RadioGroup/RadioGroup.js +11 -1
- package/dist/components/RadioGroup/RadioGroup.js.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.d.ts +15 -0
- package/dist/components/ScrollArea/ScrollArea.js +2 -2
- package/dist/components/ScrollArea/ScrollArea.js.map +1 -1
- package/dist/components/SearchableMenu/SearchableMenu.d.ts +11 -2
- package/dist/components/SearchableMenu/SearchableMenu.js +24 -2
- package/dist/components/SearchableMenu/SearchableMenu.js.map +1 -1
- package/dist/components/Select/Select.d.ts +18 -2
- package/dist/components/Select/Select.js +20 -3
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Separator/Separator.d.ts +21 -3
- package/dist/components/Separator/Separator.js +9 -1
- package/dist/components/Separator/Separator.js.map +1 -1
- package/dist/components/Sheet/Sheet.d.ts +3 -0
- package/dist/components/Sheet/Sheet.js +11 -2
- package/dist/components/Sheet/Sheet.js.map +1 -1
- package/dist/components/Skeleton/Skeleton.d.ts +8 -1
- package/dist/components/Skeleton/Skeleton.js +1 -1
- package/dist/components/Skeleton/Skeleton.js.map +1 -1
- package/dist/components/Slider/Slider.d.ts +21 -3
- package/dist/components/Slider/Slider.js +54 -27
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Spinner/Spinner.d.ts +8 -0
- package/dist/components/Spinner/Spinner.js +3 -2
- package/dist/components/Spinner/Spinner.js.map +1 -1
- package/dist/components/StatusPill/StatusPill.d.ts +5 -0
- package/dist/components/StatusPill/StatusPill.js +6 -0
- package/dist/components/StatusPill/StatusPill.js.map +1 -1
- package/dist/components/Table/Table.d.ts +6 -0
- package/dist/components/Table/Table.js +3 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Tag/Tag.d.ts +68 -9
- package/dist/components/Tag/Tag.js +50 -10
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Text/Text.d.ts +62 -0
- package/dist/components/Text/Text.js +30 -0
- package/dist/components/Text/Text.js.map +1 -0
- package/dist/components/Timestamp/Timestamp.d.ts +59 -0
- package/dist/components/Timestamp/Timestamp.js +56 -0
- package/dist/components/Timestamp/Timestamp.js.map +1 -0
- package/dist/components/Toast/Toast.d.ts +9 -1
- package/dist/components/Toast/Toast.js +27 -4
- package/dist/components/Toast/Toast.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +6 -1
- package/dist/components/ToggleGroup/ToggleGroup.js +11 -1
- package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
- package/dist/components/Toolbar/Toolbar.d.ts +55 -0
- package/dist/components/Toolbar/Toolbar.js +43 -0
- package/dist/components/Toolbar/Toolbar.js.map +1 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.d.ts +6 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js +48 -8
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js.map +1 -1
- package/dist/diagrams/DataflowFigure/DataflowFigure.js +21 -2
- package/dist/diagrams/DataflowFigure/DataflowFigure.js.map +1 -1
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.d.ts +14 -0
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js +14 -7
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js.map +1 -1
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.d.ts +38 -4
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js +14 -16
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js.map +1 -1
- package/dist/diagrams/DiagramInspector/DiagramInspector.js +2 -2
- package/dist/diagrams/DiagramInspector/DiagramInspector.js.map +1 -1
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.d.ts +23 -7
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js +45 -18
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js.map +1 -1
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js +44 -12
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js.map +1 -1
- package/dist/diagrams/SequenceFigure/SequenceFigure.js +35 -5
- package/dist/diagrams/SequenceFigure/SequenceFigure.js.map +1 -1
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.d.ts +3 -1
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js +76 -28
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js.map +1 -1
- package/dist/diagrams/index.d.ts +1 -0
- package/dist/diagrams/index.js +10 -0
- package/dist/diagrams/index.js.map +1 -1
- package/dist/diagrams/lib/dev.d.ts +61 -0
- package/dist/diagrams/lib/dev.js +96 -0
- package/dist/diagrams/lib/dev.js.map +1 -0
- package/dist/diagrams/lib/frame.d.ts +48 -7
- package/dist/diagrams/lib/frame.js +17 -2
- package/dist/diagrams/lib/frame.js.map +1 -1
- package/dist/diagrams/lib/wires.js.map +1 -1
- package/dist/diagrams/spec.d.ts +64 -12
- package/dist/index.d.ts +16 -4
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/keyframes.css +57 -3
- package/dist/lib/overlay-container.d.ts +14 -4
- package/dist/lib/overlay-container.js.map +1 -1
- package/dist/lib/warn.d.ts +73 -0
- package/dist/lib/warn.js +38 -0
- package/dist/lib/warn.js.map +1 -0
- package/dist/semantic.css +17 -6
- package/dist/styles.css +1 -1
- package/dist/tokens.css +20 -2
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +39 -36
- package/dist/tokens.json +39 -36
- package/llms.txt +35 -0
- package/package.json +5 -2
- package/skills/misoto22-design/evals/evals.json +84 -0
- package/skills/misoto22-design/rules/a11y.md +55 -3
- package/skills/misoto22-design/rules/forms.md +5 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Toast/Toast.tsx"],"sourcesContent":["'use client'\n\nimport { Toaster as SonnerToaster, toast } from 'sonner'\nimport type { ToasterProps } from 'sonner'\nimport type { CSSProperties } from 'react'\n\n/**\n * Sonner's own custom properties, pointed at this system's tokens.\n *\n * Set as inline style on the Toaster rather than in a stylesheet: sonner\n * renders its list into a portal at the end of `<body>`, outside whatever\n * element a consumer's CSS is scoped to, and the inline style is the one place\n * guaranteed to reach it.\n *\n * Cast because this `@types/react` `CSSProperties` has no custom-property index\n * signature.\n */\nconst TOKEN_STYLE = {\n '--normal-bg': 'var(--paper)',\n '--normal-text': 'var(--ink)',\n '--normal-border': 'var(--rule-2)',\n '--success-bg': 'var(--paper)',\n '--success-text': 'var(--ok)',\n '--success-border': 'var(--rule-2)',\n '--error-bg': 'var(--paper)',\n '--error-text': 'var(--danger)',\n '--error-border': 'var(--rule-2)',\n
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Toast/Toast.tsx"],"sourcesContent":["'use client'\n\nimport { useSyncExternalStore } from 'react'\nimport { Toaster as SonnerToaster, toast } from 'sonner'\nimport type { ToasterProps } from 'sonner'\nimport type { CSSProperties } from 'react'\n\n/**\n * Sonner's own custom properties, pointed at this system's tokens.\n *\n * Set as inline style on the Toaster rather than in a stylesheet: sonner\n * renders its list into a portal at the end of `<body>`, outside whatever\n * element a consumer's CSS is scoped to, and the inline style is the one place\n * guaranteed to reach it.\n *\n * These four are the ones sonner reads unconditionally. The status pairs are\n * separate, below, because sonner does not.\n *\n * Cast because this `@types/react` `CSSProperties` has no custom-property index\n * signature.\n */\nconst TOKEN_STYLE = {\n '--normal-bg': 'var(--paper)',\n '--normal-text': 'var(--ink)',\n '--normal-border': 'var(--rule-2)',\n '--border-radius': 'var(--radius)',\n fontFamily: 'var(--sans)',\n} as CSSProperties\n\n/**\n * The status pairs, emitted only when something will read them.\n *\n * sonner scopes `--success-*` and `--error-*` to `[data-rich-colors=true]`, so\n * at this package's default they are four declarations that do nothing while\n * sitting in the element's style attribute looking like the source of a success\n * toast's colour. Conditional rather than deleted: a consumer who turns\n * `richColors` on should still get this system's status scale rather than\n * sonner's.\n */\nconst RICH_STYLE = {\n '--success-bg': 'var(--paper)',\n '--success-text': 'var(--ok)',\n '--success-border': 'var(--rule-2)',\n '--error-bg': 'var(--paper)',\n '--error-text': 'var(--danger)',\n '--error-border': 'var(--rule-2)',\n} as CSSProperties\n\n/**\n * The mode the APP is in, which is not the mode the operating system is in.\n *\n * `data-mode` on `<html>` is this system's switch: a pre-paint script resolves\n * it from storage, falling back to `prefers-color-scheme`, and a reader can\n * override the OS from the page. sonner's own `theme=\"system\"` reads\n * `prefers-color-scheme` directly, so a reader on a dark machine reading a\n * light page would get a dark toast over a white page — the same defect as the\n * one this exists to fix, pointing the other way.\n */\ntype Mode = 'light' | 'dark'\n\nfunction readMode(): Mode {\n if (typeof document === 'undefined') return 'light'\n return document.documentElement.dataset.mode === 'dark' ? 'dark' : 'light'\n}\n\n/** Light on a server, which is what the pre-paint script also assumes. */\nfunction readModeOnServer(): Mode {\n return 'light'\n}\n\nfunction subscribeToMode(onChange: () => void): () => void {\n if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return () => {}\n const observer = new MutationObserver(onChange)\n observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-mode'] })\n return () => observer.disconnect()\n}\n\n/**\n * Observed rather than read once: the Toaster is mounted at the app root and\n * outlives every theme switch a reader makes under it.\n */\nfunction useMode(): Mode {\n return useSyncExternalStore(subscribeToMode, readMode, readModeOnServer)\n}\n\n/**\n * Transient confirmations, mounted once near the app root.\n *\n * A toast is for something that succeeded and needs no response. Anything the\n * reader must act on belongs in the page — a toast is dismissed by time, and\n * time is not an acknowledgement.\n *\n * `theme` follows `data-mode`. sonner defaults to `light` and hard-codes the\n * description's colour per theme — `#3f3f3f`, overridden only under\n * `[data-sonner-theme=dark]` — so a Toaster that never passed `theme` put dark\n * grey text on this system's `--paper: #0d0d0d` and every\n * `toast(title, { description })` lost its second half. Pass `theme` yourself\n * and that wins; sonner's own `system` is the one value not to reach for, since\n * it reads the OS rather than the attribute the page is actually painted from.\n *\n * `richColors` stays off by default: sonner's rich palette is not this system's\n * status scale, and turning it on reintroduces chroma the White Reset spends\n * only on state.\n *\n * @example\n * // app root\n * <Toaster />\n * // anywhere\n * toast.success('Saved')\n */\nexport function Toaster({\n position = 'bottom-right',\n closeButton = true,\n richColors = false,\n theme,\n style,\n ...rest\n}: ToasterProps) {\n const mode = useMode()\n\n return (\n <SonnerToaster\n position={position}\n closeButton={closeButton}\n richColors={richColors}\n theme={theme ?? mode}\n style={{ ...TOKEN_STYLE, ...(richColors ? RICH_STYLE : null), ...style }}\n {...rest}\n />\n )\n}\n\nexport { toast }\nexport default Toaster\n"],"mappings":";AAyHI;AAvHJ,SAAS,4BAA4B;AACrC,SAAS,WAAW,eAAe,aAAa;AAkBhD,MAAM,cAAc;AAAA,EAClB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,YAAY;AACd;AAYA,MAAM,aAAa;AAAA,EACjB,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,kBAAkB;AACpB;AAcA,SAAS,WAAiB;AACxB,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,SAAO,SAAS,gBAAgB,QAAQ,SAAS,SAAS,SAAS;AACrE;AAGA,SAAS,mBAAyB;AAChC,SAAO;AACT;AAEA,SAAS,gBAAgB,UAAkC;AACzD,MAAI,OAAO,aAAa,eAAe,OAAO,qBAAqB,YAAa,QAAO,MAAM;AAAA,EAAC;AAC9F,QAAM,WAAW,IAAI,iBAAiB,QAAQ;AAC9C,WAAS,QAAQ,SAAS,iBAAiB,EAAE,YAAY,MAAM,iBAAiB,CAAC,WAAW,EAAE,CAAC;AAC/F,SAAO,MAAM,SAAS,WAAW;AACnC;AAMA,SAAS,UAAgB;AACvB,SAAO,qBAAqB,iBAAiB,UAAU,gBAAgB;AACzE;AA2BO,SAAS,QAAQ;AAAA,EACtB,WAAW;AAAA,EACX,cAAc;AAAA,EACd,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAiB;AACf,QAAM,OAAO,QAAQ;AAErB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,SAAS;AAAA,MAChB,OAAO,EAAE,GAAG,aAAa,GAAI,aAAa,aAAa,MAAO,GAAG,MAAM;AAAA,MACtE,GAAG;AAAA;AAAA,EACN;AAEJ;AAGA,IAAO,gBAAQ;","names":[]}
|
|
@@ -21,13 +21,18 @@ type ToggleGroupProps = ComponentProps<typeof ToggleGroupPrimitive.Root>;
|
|
|
21
21
|
* Distinct from `Tabs`, which switches PANELS and owns a tabpanel relationship.
|
|
22
22
|
* A toggle group changes a value.
|
|
23
23
|
*
|
|
24
|
+
* Inside a `Field` the strip takes its name from that label, by pointing back at
|
|
25
|
+
* it — the root is a div, and `<label for>` does not bind to one, so the words
|
|
26
|
+
* above it do not click through. Standing alone it still needs its own
|
|
27
|
+
* `aria-label`.
|
|
28
|
+
*
|
|
24
29
|
* @example
|
|
25
30
|
* <ToggleGroup type="single" defaultValue="grid" aria-label="Layout">
|
|
26
31
|
* <ToggleGroupItem value="grid">Grid</ToggleGroupItem>
|
|
27
32
|
* <ToggleGroupItem value="list">List</ToggleGroupItem>
|
|
28
33
|
* </ToggleGroup>
|
|
29
34
|
*/
|
|
30
|
-
declare function ToggleGroup({ className, children, ...props }: ToggleGroupProps): react.JSX.Element;
|
|
35
|
+
declare function ToggleGroup({ className, children, 'aria-required': ariaRequired, ...props }: ToggleGroupProps): react.JSX.Element;
|
|
31
36
|
type ToggleGroupItemProps = ComponentProps<typeof ToggleGroupPrimitive.Item>;
|
|
32
37
|
/**
|
|
33
38
|
* One segment.
|
|
@@ -3,9 +3,17 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
|
4
4
|
import { createContext, useContext, useState } from "react";
|
|
5
5
|
import { cn } from "../../lib/cn.js";
|
|
6
|
+
import { useFieldControl } from "../Field/field-control.js";
|
|
6
7
|
import { useSelectionIndicator } from "../../lib/useSelectionIndicator.js";
|
|
7
8
|
const SingleValueContext = createContext(null);
|
|
8
|
-
function ToggleGroup({
|
|
9
|
+
function ToggleGroup({
|
|
10
|
+
className,
|
|
11
|
+
children,
|
|
12
|
+
"aria-required": ariaRequired,
|
|
13
|
+
...props
|
|
14
|
+
}) {
|
|
15
|
+
const field = useFieldControl();
|
|
16
|
+
const named = props["aria-label"] != null || props["aria-labelledby"] != null;
|
|
9
17
|
const single = props.type === "single";
|
|
10
18
|
const [uncontrolled, setUncontrolled] = useState(
|
|
11
19
|
(single ? props.defaultValue : void 0) ?? ""
|
|
@@ -16,6 +24,8 @@ function ToggleGroup({ className, children, ...props }) {
|
|
|
16
24
|
ToggleGroupPrimitive.Root,
|
|
17
25
|
{
|
|
18
26
|
ref,
|
|
27
|
+
"aria-labelledby": named ? void 0 : field?.labelId,
|
|
28
|
+
"aria-required": props.type === "single" ? ariaRequired : void 0,
|
|
19
29
|
className: cn(
|
|
20
30
|
// `w-fit` as well as `inline-flex`: a flex or grid parent stretches its
|
|
21
31
|
// children to the track by default, and `inline-flex` does not opt out
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ToggleGroup/ToggleGroup.tsx"],"sourcesContent":["'use client'\n\nimport * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'\nimport { createContext, useContext, useState, type ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\nimport { useSelectionIndicator } from '../../lib/useSelectionIndicator'\n\nexport type ToggleGroupProps = ComponentProps<typeof ToggleGroupPrimitive.Root>\n\n/** Lets an item know whether the group tracks one value or several. */\nconst SingleValueContext = createContext<string | null>(null)\n\n/**\n * A segmented control: several options, one strip.\n *\n * `type=\"single\"` is a choice — Radix gives it radio semantics, and it is the\n * right shape for a view switcher or a density setting. `type=\"multiple\"` is a\n * set of independent toggles, which is a different thing announced differently;\n * choosing the wrong one is how a \"filter by tag\" control ends up telling a\n * screen reader that picking one tag unpicks the others.\n *\n * The two look different on purpose. A single-value strip moves ONE filled pill\n * between its options, so the eye follows a thing travelling; a multiple-value\n * strip fills each pressed option separately, because there is no single\n * selection to travel. Two people looking at a screenshot should be able to\n * tell which kind they are looking at, and before this they could not.\n *\n * Distinct from `Tabs`, which switches PANELS and owns a tabpanel relationship.\n * A toggle group changes a value.\n *\n * @example\n * <ToggleGroup type=\"single\" defaultValue=\"grid\" aria-label=\"Layout\">\n * <ToggleGroupItem value=\"grid\">Grid</ToggleGroupItem>\n * <ToggleGroupItem value=\"list\">List</ToggleGroupItem>\n * </ToggleGroup>\n */\nexport function ToggleGroup({
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ToggleGroup/ToggleGroup.tsx"],"sourcesContent":["'use client'\n\nimport * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'\nimport { createContext, useContext, useState, type ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\nimport { useFieldControl } from '../Field/field-control'\nimport { useSelectionIndicator } from '../../lib/useSelectionIndicator'\n\nexport type ToggleGroupProps = ComponentProps<typeof ToggleGroupPrimitive.Root>\n\n/** Lets an item know whether the group tracks one value or several. */\nconst SingleValueContext = createContext<string | null>(null)\n\n/**\n * A segmented control: several options, one strip.\n *\n * `type=\"single\"` is a choice — Radix gives it radio semantics, and it is the\n * right shape for a view switcher or a density setting. `type=\"multiple\"` is a\n * set of independent toggles, which is a different thing announced differently;\n * choosing the wrong one is how a \"filter by tag\" control ends up telling a\n * screen reader that picking one tag unpicks the others.\n *\n * The two look different on purpose. A single-value strip moves ONE filled pill\n * between its options, so the eye follows a thing travelling; a multiple-value\n * strip fills each pressed option separately, because there is no single\n * selection to travel. Two people looking at a screenshot should be able to\n * tell which kind they are looking at, and before this they could not.\n *\n * Distinct from `Tabs`, which switches PANELS and owns a tabpanel relationship.\n * A toggle group changes a value.\n *\n * Inside a `Field` the strip takes its name from that label, by pointing back at\n * it — the root is a div, and `<label for>` does not bind to one, so the words\n * above it do not click through. Standing alone it still needs its own\n * `aria-label`.\n *\n * @example\n * <ToggleGroup type=\"single\" defaultValue=\"grid\" aria-label=\"Layout\">\n * <ToggleGroupItem value=\"grid\">Grid</ToggleGroupItem>\n * <ToggleGroupItem value=\"list\">List</ToggleGroupItem>\n * </ToggleGroup>\n */\nexport function ToggleGroup({\n className,\n children,\n 'aria-required': ariaRequired,\n ...props\n}: ToggleGroupProps) {\n const field = useFieldControl()\n const named = props['aria-label'] != null || props['aria-labelledby'] != null\n\n // Tracked here rather than left to Radix alone, because the sliding pill has\n // to know which item to measure — and an uncontrolled group never tells\n // anyone what it picked.\n const single = props.type === 'single'\n const [uncontrolled, setUncontrolled] = useState<string>(\n (single ? (props.defaultValue as string | undefined) : undefined) ?? '',\n )\n const value = single ? ((props.value as string | undefined) ?? uncontrolled) : null\n const [ref, indicator] = useSelectionIndicator<HTMLDivElement>(value)\n\n const root = (\n <ToggleGroupPrimitive.Root\n ref={ref}\n aria-labelledby={named ? undefined : field?.labelId}\n // Only on the single-value strip. Radix gives that one role=\"radiogroup\",\n // which takes aria-required; a multiple-value strip is a role=\"toolbar\",\n // where the attribute is not allowed at all — so a Field's `required`\n // reaches one shape of this control and not the other.\n aria-required={props.type === 'single' ? ariaRequired : undefined}\n className={cn(\n // `w-fit` as well as `inline-flex`: a flex or grid parent stretches its\n // children to the track by default, and `inline-flex` does not opt out\n // of that. Without it the strip grew to whatever the widest sibling was\n // — a label above it — and left dead space after the last segment.\n 'relative inline-flex w-fit items-center gap-1 rounded-(--radius-pill) border border-(--rule-2) p-1',\n className,\n )}\n {...props}\n onValueChange={(next: never) => {\n if (single) setUncontrolled((next as string) ?? '')\n ;(props.onValueChange as ((v: never) => void) | undefined)?.(next)\n }}\n >\n {single && indicator.ready && (\n <span\n aria-hidden\n data-m22-animated\n className=\"absolute rounded-(--radius-pill) bg-(--accent) transition-[transform,width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none\"\n style={{\n transform: `translate(${indicator.offset}px, ${indicator.top}px)`,\n width: indicator.width,\n height: indicator.height,\n insetInlineStart: 0,\n top: 0,\n }}\n />\n )}\n {children}\n </ToggleGroupPrimitive.Root>\n )\n\n return single ? (\n <SingleValueContext.Provider value={value}>{root}</SingleValueContext.Provider>\n ) : (\n root\n )\n}\n\nexport type ToggleGroupItemProps = ComponentProps<typeof ToggleGroupPrimitive.Item>\n\n/**\n * One segment.\n *\n * In a single-value group the item draws no background of its own — the\n * travelling pill behind it does — so it only changes ink. In a multiple-value\n * group it fills, because there is nothing travelling.\n */\nexport function ToggleGroupItem({ className, value, ...props }: ToggleGroupItemProps) {\n const selected = useContext(SingleValueContext)\n const isSingle = selected !== null\n const active = isSingle && selected === value\n\n return (\n <ToggleGroupPrimitive.Item\n value={value}\n data-indicator-active={active ? 'true' : undefined}\n className={cn(\n 'relative z-1 inline-flex min-h-(--control-h-sm) items-center justify-center gap-2 rounded-(--radius-pill) px-3.5 text-sm transition-colors duration-(--duration-fast) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none',\n isSingle\n ? active\n ? 'text-(--accent-foreground)'\n : 'text-(--ink-3-aa) hover:text-(--ink)'\n : 'text-(--ink-3-aa) hover:text-(--ink) data-[state=on]:bg-(--accent) data-[state=on]:text-(--accent-foreground)',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport default ToggleGroup\n"],"mappings":";AA8DI,SAuBI,KAvBJ;AA5DJ,YAAY,0BAA0B;AACtC,SAAS,eAAe,YAAY,gBAAqC;AACzE,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAChC,SAAS,6BAA6B;AAKtC,MAAM,qBAAqB,cAA6B,IAAI;AA+BrD,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,GAAG;AACL,GAAqB;AACnB,QAAM,QAAQ,gBAAgB;AAC9B,QAAM,QAAQ,MAAM,YAAY,KAAK,QAAQ,MAAM,iBAAiB,KAAK;AAKzE,QAAM,SAAS,MAAM,SAAS;AAC9B,QAAM,CAAC,cAAc,eAAe,IAAI;AAAA,KACrC,SAAU,MAAM,eAAsC,WAAc;AAAA,EACvE;AACA,QAAM,QAAQ,SAAW,MAAM,SAAgC,eAAgB;AAC/E,QAAM,CAAC,KAAK,SAAS,IAAI,sBAAsC,KAAK;AAEpE,QAAM,OACJ;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC;AAAA,MACA,mBAAiB,QAAQ,SAAY,OAAO;AAAA,MAK5C,iBAAe,MAAM,SAAS,WAAW,eAAe;AAAA,MACxD,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,QAKT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MACJ,eAAe,CAAC,SAAgB;AAC9B,YAAI,OAAQ,iBAAiB,QAAmB,EAAE;AACjD,QAAC,MAAM,gBAAqD,IAAI;AAAA,MACnE;AAAA,MAEC;AAAA,kBAAU,UAAU,SACnB;AAAA,UAAC;AAAA;AAAA,YACC,eAAW;AAAA,YACX,qBAAiB;AAAA,YACjB,WAAU;AAAA,YACV,OAAO;AAAA,cACL,WAAW,aAAa,UAAU,MAAM,OAAO,UAAU,GAAG;AAAA,cAC5D,OAAO,UAAU;AAAA,cACjB,QAAQ,UAAU;AAAA,cAClB,kBAAkB;AAAA,cAClB,KAAK;AAAA,YACP;AAAA;AAAA,QACF;AAAA,QAED;AAAA;AAAA;AAAA,EACH;AAGF,SAAO,SACL,oBAAC,mBAAmB,UAAnB,EAA4B,OAAe,gBAAK,IAEjD;AAEJ;AAWO,SAAS,gBAAgB,EAAE,WAAW,OAAO,GAAG,MAAM,GAAyB;AACpF,QAAM,WAAW,WAAW,kBAAkB;AAC9C,QAAM,WAAW,aAAa;AAC9B,QAAM,SAAS,YAAY,aAAa;AAExC,SACE;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC;AAAA,MACA,yBAAuB,SAAS,SAAS;AAAA,MACzC,WAAW;AAAA,QACT;AAAA,QACA,WACI,SACE,+BACA,yCACF;AAAA,QACJ;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,IAAO,sBAAQ;","names":[]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which edge the bar sticks to, or neither.
|
|
6
|
+
*
|
|
7
|
+
* `bottom` is the form footer — the actions stay in reach while a long form
|
|
8
|
+
* scrolls under them. `top` is the filter bar over a list. `static` is the same
|
|
9
|
+
* bar with no stickiness, for a short page or a bar inside a dialog that is
|
|
10
|
+
* already the height of its content.
|
|
11
|
+
*/
|
|
12
|
+
type ToolbarPosition = 'bottom' | 'top' | 'static';
|
|
13
|
+
type ToolbarAlign = 'start' | 'center' | 'end' | 'between';
|
|
14
|
+
interface ToolbarProps extends HTMLAttributes<HTMLDivElement> {
|
|
15
|
+
/**
|
|
16
|
+
* Names the bar for a screen reader — "Form actions", "List filters".
|
|
17
|
+
* Required, because a group with no name is announced as "group" and a page
|
|
18
|
+
* with two of them is a page with two identical announcements.
|
|
19
|
+
*/
|
|
20
|
+
label: string;
|
|
21
|
+
/** Which edge the bar sticks to. See {@link ToolbarPosition}. */
|
|
22
|
+
position?: ToolbarPosition;
|
|
23
|
+
/** Where the contents sit along the inline axis. */
|
|
24
|
+
align?: ToolbarAlign;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The bar of actions at the edge of a working surface.
|
|
29
|
+
*
|
|
30
|
+
* Two page templates built the same thing independently — a sticky strip on
|
|
31
|
+
* `--paper` with a rule along the edge it sticks to — which is the signal that
|
|
32
|
+
* it belongs here rather than in each of them.
|
|
33
|
+
*
|
|
34
|
+
* The ground is opaque `--paper` and not a blur. Content scrolls UNDER this
|
|
35
|
+
* bar, so anything translucent puts the last row of a table behind the submit
|
|
36
|
+
* button and makes both unreadable; `FloatingIconButton` blurs because it
|
|
37
|
+
* floats over a gap, which is a different problem.
|
|
38
|
+
*
|
|
39
|
+
* It is not `role="toolbar"`. That role's contract is a single tab stop with
|
|
40
|
+
* arrow keys moving between the controls inside it, and this implements no such
|
|
41
|
+
* thing — declaring the role without the behaviour tells a screen-reader user
|
|
42
|
+
* to press arrow keys that do nothing. It is a named `group`, so the controls
|
|
43
|
+
* keep their own places in the tab order and the bar is still announced.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* <Toolbar label="Form actions">
|
|
47
|
+
* <Button variant="secondary">Cancel</Button>
|
|
48
|
+
* <Button type="submit">Save changes</Button>
|
|
49
|
+
* </Toolbar>
|
|
50
|
+
* @example
|
|
51
|
+
* <Toolbar label="List filters" position="top" align="between">…</Toolbar>
|
|
52
|
+
*/
|
|
53
|
+
declare function Toolbar({ label, position, align, className, children, ...rest }: ToolbarProps): react.JSX.Element;
|
|
54
|
+
|
|
55
|
+
export { Toolbar, type ToolbarAlign, type ToolbarPosition, type ToolbarProps, Toolbar as default };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../../lib/cn.js";
|
|
3
|
+
const POSITION = {
|
|
4
|
+
bottom: "sticky bottom-0 z-(--z-sticky) border-t border-(--rule-2)",
|
|
5
|
+
top: "sticky top-0 z-(--z-sticky) border-b border-(--rule-2)",
|
|
6
|
+
static: "border-t border-(--rule-2)"
|
|
7
|
+
};
|
|
8
|
+
const ALIGN = {
|
|
9
|
+
start: "justify-start",
|
|
10
|
+
center: "justify-center",
|
|
11
|
+
end: "justify-end",
|
|
12
|
+
between: "justify-between"
|
|
13
|
+
};
|
|
14
|
+
function Toolbar({
|
|
15
|
+
label,
|
|
16
|
+
position = "bottom",
|
|
17
|
+
align = "end",
|
|
18
|
+
className,
|
|
19
|
+
children,
|
|
20
|
+
...rest
|
|
21
|
+
}) {
|
|
22
|
+
return /* @__PURE__ */ jsx(
|
|
23
|
+
"div",
|
|
24
|
+
{
|
|
25
|
+
role: "group",
|
|
26
|
+
"aria-label": label,
|
|
27
|
+
className: cn(
|
|
28
|
+
"flex flex-wrap items-center gap-3 bg-(--paper) px-4 py-3",
|
|
29
|
+
POSITION[position],
|
|
30
|
+
ALIGN[align],
|
|
31
|
+
className
|
|
32
|
+
),
|
|
33
|
+
...rest,
|
|
34
|
+
children
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
var Toolbar_default = Toolbar;
|
|
39
|
+
export {
|
|
40
|
+
Toolbar,
|
|
41
|
+
Toolbar_default as default
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=Toolbar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Toolbar/Toolbar.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\n/**\n * Which edge the bar sticks to, or neither.\n *\n * `bottom` is the form footer — the actions stay in reach while a long form\n * scrolls under them. `top` is the filter bar over a list. `static` is the same\n * bar with no stickiness, for a short page or a bar inside a dialog that is\n * already the height of its content.\n */\nexport type ToolbarPosition = 'bottom' | 'top' | 'static'\n\nexport type ToolbarAlign = 'start' | 'center' | 'end' | 'between'\n\nexport interface ToolbarProps extends HTMLAttributes<HTMLDivElement> {\n /**\n * Names the bar for a screen reader — \"Form actions\", \"List filters\".\n * Required, because a group with no name is announced as \"group\" and a page\n * with two of them is a page with two identical announcements.\n */\n label: string\n /** Which edge the bar sticks to. See {@link ToolbarPosition}. */\n position?: ToolbarPosition\n /** Where the contents sit along the inline axis. */\n align?: ToolbarAlign\n children: ReactNode\n}\n\nconst POSITION: Record<ToolbarPosition, string> = {\n bottom: 'sticky bottom-0 z-(--z-sticky) border-t border-(--rule-2)',\n top: 'sticky top-0 z-(--z-sticky) border-b border-(--rule-2)',\n static: 'border-t border-(--rule-2)',\n}\n\nconst ALIGN: Record<ToolbarAlign, string> = {\n start: 'justify-start',\n center: 'justify-center',\n end: 'justify-end',\n between: 'justify-between',\n}\n\n/**\n * The bar of actions at the edge of a working surface.\n *\n * Two page templates built the same thing independently — a sticky strip on\n * `--paper` with a rule along the edge it sticks to — which is the signal that\n * it belongs here rather than in each of them.\n *\n * The ground is opaque `--paper` and not a blur. Content scrolls UNDER this\n * bar, so anything translucent puts the last row of a table behind the submit\n * button and makes both unreadable; `FloatingIconButton` blurs because it\n * floats over a gap, which is a different problem.\n *\n * It is not `role=\"toolbar\"`. That role's contract is a single tab stop with\n * arrow keys moving between the controls inside it, and this implements no such\n * thing — declaring the role without the behaviour tells a screen-reader user\n * to press arrow keys that do nothing. It is a named `group`, so the controls\n * keep their own places in the tab order and the bar is still announced.\n *\n * @example\n * <Toolbar label=\"Form actions\">\n * <Button variant=\"secondary\">Cancel</Button>\n * <Button type=\"submit\">Save changes</Button>\n * </Toolbar>\n * @example\n * <Toolbar label=\"List filters\" position=\"top\" align=\"between\">…</Toolbar>\n */\nexport function Toolbar({\n label,\n position = 'bottom',\n align = 'end',\n className,\n children,\n ...rest\n}: ToolbarProps) {\n return (\n <div\n role=\"group\"\n aria-label={label}\n className={cn(\n 'flex flex-wrap items-center gap-3 bg-(--paper) px-4 py-3',\n POSITION[position],\n ALIGN[align],\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n )\n}\n\nexport default Toolbar\n"],"mappings":"AA6EI;AA5EJ,SAAS,UAAU;AA4BnB,MAAM,WAA4C;AAAA,EAChD,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,QAAQ;AACV;AAEA,MAAM,QAAsC;AAAA,EAC1C,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,SAAS;AACX;AA4BO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA,WAAW;AAAA,EACX,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAiB;AACf,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW;AAAA,QACT;AAAA,QACA,SAAS,QAAQ;AAAA,QACjB,MAAM,KAAK;AAAA,QACX;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,kBAAQ;","names":[]}
|
|
@@ -21,6 +21,12 @@ interface ArchitectureFigureProps extends FigureChrome {
|
|
|
21
21
|
* the input, it is identical on the server and in the browser, and there is no
|
|
22
22
|
* layout shift on hydration because there is no layout to do.
|
|
23
23
|
*
|
|
24
|
+
* A component that declares NEITHER flows: it takes the next free cell in
|
|
25
|
+
* declaration order, wrapping at `layout.cols`. That is still arithmetic on
|
|
26
|
+
* numbers the specification carries rather than a solver — and the alternative
|
|
27
|
+
* was every unplaced component defaulting to row 0, column 0 and stacking into
|
|
28
|
+
* one plate.
|
|
29
|
+
*
|
|
24
30
|
* BOUNDARIES ARE DRAWN FIRST, AND DRAWN DIFFERENTLY. A `region` is where
|
|
25
31
|
* something RUNS — a VPC, a zone, a cluster — and is a solid frame. A
|
|
26
32
|
* `security-group` is what may REACH it, and is dashed. That is not decoration:
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId, useMemo } from "react";
|
|
4
|
+
import { liveEdges, useSpecIdentity, warnCollision } from "../lib/dev.js";
|
|
4
5
|
import { DiagramFrame } from "../lib/frame.js";
|
|
5
|
-
import { inflate, TYPE, union, wrapText } from "../lib/geometry.js";
|
|
6
|
+
import { inflate, round, TYPE, union, wrapText } from "../lib/geometry.js";
|
|
6
7
|
import { kindLegend, resolveLegend } from "../lib/legend.js";
|
|
7
8
|
import { BandFrame, NodePlate, PLATE, plateHeight } from "../lib/marks.js";
|
|
8
9
|
import { renderWires, wiresFor } from "../lib/wires.js";
|
|
@@ -26,6 +27,7 @@ function ArchitectureFigure({
|
|
|
26
27
|
onSelectNode
|
|
27
28
|
}) {
|
|
28
29
|
const uid = useId().replace(/:/g, "");
|
|
30
|
+
useSpecIdentity(spec, "ArchitectureFigure");
|
|
29
31
|
const model = useMemo(() => buildModel(spec, uid, activeIds, onSelectNode), [spec, uid, activeIds, onSelectNode]);
|
|
30
32
|
return /* @__PURE__ */ jsx(
|
|
31
33
|
DiagramFrame,
|
|
@@ -42,12 +44,34 @@ function ArchitectureFigure({
|
|
|
42
44
|
}
|
|
43
45
|
);
|
|
44
46
|
}
|
|
47
|
+
function flow(components, cols) {
|
|
48
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
49
|
+
for (const component of components) {
|
|
50
|
+
if (!component.pos && (component.row !== void 0 || component.col !== void 0)) {
|
|
51
|
+
claimed.add(`${component.row ?? 0}:${component.col ?? 0}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let cursor = 0;
|
|
55
|
+
return (component) => {
|
|
56
|
+
if (component.row !== void 0 || component.col !== void 0) {
|
|
57
|
+
return [component.row ?? 0, component.col ?? 0];
|
|
58
|
+
}
|
|
59
|
+
while (claimed.has(`${Math.floor(cursor / cols)}:${cursor % cols}`)) cursor += 1;
|
|
60
|
+
const cell = [Math.floor(cursor / cols), cursor % cols];
|
|
61
|
+
claimed.add(`${cell[0]}:${cell[1]}`);
|
|
62
|
+
cursor += 1;
|
|
63
|
+
return cell;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
45
66
|
function buildModel(spec, uid, activeIds, onSelectNode) {
|
|
46
67
|
const layout = { ...GRID, ...spec.layout };
|
|
47
68
|
const [originX, originY] = layout.origin;
|
|
69
|
+
const nextCell = flow(spec.components, Math.max(1, Math.round(layout.cols)));
|
|
48
70
|
const boxes = /* @__PURE__ */ new Map();
|
|
71
|
+
const cells = /* @__PURE__ */ new Map();
|
|
72
|
+
const named = new Map(spec.components.map((component) => [component.id, component.label]));
|
|
49
73
|
const placed = spec.components.map((component) => {
|
|
50
|
-
const labelWidth = component.size?.[0] ?? component.width ??
|
|
74
|
+
const labelWidth = component.size?.[0] ?? component.width ?? layout.cellW;
|
|
51
75
|
const lines = wrapText(component.label, TYPE.label, labelWidth - PLATE.padX * 2, 2);
|
|
52
76
|
const height = plateHeight(
|
|
53
77
|
lines.length,
|
|
@@ -55,21 +79,28 @@ function buildModel(spec, uid, activeIds, onSelectNode) {
|
|
|
55
79
|
Boolean(component.type || component.tag),
|
|
56
80
|
component.size?.[1] ?? component.height ?? 0
|
|
57
81
|
);
|
|
58
|
-
const
|
|
59
|
-
const
|
|
82
|
+
const [row, col] = nextCell(component);
|
|
83
|
+
const x = component.pos ? component.pos[0] : originX + col * (layout.cellW + layout.gapX);
|
|
84
|
+
const y = component.pos ? component.pos[1] : originY + row * (layout.cellH + layout.gapY);
|
|
60
85
|
const box = { x, y, w: labelWidth, h: height };
|
|
61
86
|
boxes.set(component.id, box);
|
|
87
|
+
cells.set(`${round(x)}:${round(y)}`, [...cells.get(`${round(x)}:${round(y)}`) ?? [], component.id]);
|
|
62
88
|
return { component, box };
|
|
63
89
|
});
|
|
90
|
+
for (const ids of cells.values()) {
|
|
91
|
+
if (ids.length > 1) warnCollision("ArchitectureFigure", ids);
|
|
92
|
+
}
|
|
64
93
|
const boundaries = (spec.boundaries ?? []).map((boundary) => {
|
|
65
|
-
const
|
|
94
|
+
const inside = boundary.wraps.filter((id) => boxes.has(id));
|
|
95
|
+
const wrapped = inside.map((id) => boxes.get(id)).filter((box) => Boolean(box));
|
|
66
96
|
const extent2 = union(wrapped);
|
|
67
|
-
return { boundary, box: extent2 ? inflate(extent2, boundary.pad ?? 28) : null };
|
|
97
|
+
return { boundary, inside, box: extent2 ? inflate(extent2, boundary.pad ?? 28) : null };
|
|
68
98
|
});
|
|
69
99
|
const absolute = new Set(
|
|
70
100
|
spec.components.filter((component) => component.pos).map((component) => component.id)
|
|
71
101
|
);
|
|
72
|
-
const
|
|
102
|
+
const connections = liveEdges("ArchitectureFigure", spec.connections, (id) => boxes.has(id));
|
|
103
|
+
const wires = wiresFor(connections, boxes, (connection) => ({
|
|
73
104
|
keepWaypoints: absolute.has(connection.from) && absolute.has(connection.to)
|
|
74
105
|
}));
|
|
75
106
|
const { paths, labels } = renderWires(wires, uid);
|
|
@@ -116,11 +147,20 @@ function buildModel(spec, uid, activeIds, onSelectNode) {
|
|
|
116
147
|
sublabel: component.sublabel,
|
|
117
148
|
kind: component.type
|
|
118
149
|
})),
|
|
119
|
-
edges:
|
|
150
|
+
edges: connections.map((connection) => ({
|
|
120
151
|
from: connection.from,
|
|
121
152
|
to: connection.to,
|
|
122
153
|
label: connection.label
|
|
123
154
|
})),
|
|
155
|
+
// A boundary is a statement — this runs there, that may reach it — drawn as
|
|
156
|
+
// a frame inside artwork a screen reader is told to skip. It groups nothing
|
|
157
|
+
// in the list, because two boundaries can hold the same component and a
|
|
158
|
+
// node published twice is a node the keyboard reaches twice.
|
|
159
|
+
notes: boundaries.map(({ boundary, inside }) => {
|
|
160
|
+
const kind = boundary.kind === "region" ? "Region" : "Security group";
|
|
161
|
+
const members = inside.map((id) => named.get(id) ?? id);
|
|
162
|
+
return members.length > 0 ? `${kind} ${boundary.label} encloses ${members.join(", ")}.` : `${kind} ${boundary.label} encloses nothing this figure draws.`;
|
|
163
|
+
}),
|
|
124
164
|
legend: kindLegend(
|
|
125
165
|
resolveLegend(used, spec.meta.legend?.mode, KINDS, spec.meta.legend?.entries),
|
|
126
166
|
spec.meta.legend?.entries
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/diagrams/ArchitectureFigure/ArchitectureFigure.tsx"],"sourcesContent":["'use client'\n\nimport { useId, useMemo } from 'react'\nimport { DiagramFrame, type FigureChrome, type FigureModel } from '../lib/frame'\nimport { inflate, TYPE, union, wrapText, type Box } from '../lib/geometry'\nimport { kindLegend, resolveLegend } from '../lib/legend'\nimport { BandFrame, NodePlate, PLATE, plateHeight } from '../lib/marks'\nimport { renderWires, wiresFor } from '../lib/wires'\nimport type { ArchitectureSpec, NodeKind } from '../spec'\n\nconst KINDS: NodeKind[] = [\n 'frontend',\n 'backend',\n 'database',\n 'cloud',\n 'security',\n 'messagebus',\n 'external',\n]\n\n/** What a cell is, when the specification does not say. */\nconst GRID = { origin: [40, 48] as [number, number], cols: 4, cellW: 184, cellH: 72, gapX: 64, gapY: 76 }\n\nexport interface ArchitectureFigureProps extends FigureChrome {\n spec: ArchitectureSpec\n}\n\n/**\n * A component map: services, datastores, boundaries, and what talks to what.\n *\n * Takes the same JSON an archify `architecture` specification carries —\n * `components`, `boundaries`, `connections` — and draws it in this system's own\n * terms: paper plates on a hairline, one reversed plate for the component the\n * diagram is about, and seven drawn sigils where archify uses seven hues.\n *\n * IT RENDERS ON A SERVER because every position is already in the\n * specification. A component gives a `row` and a `col` into a grid whose cell\n * size is either declared or defaulted, or it gives an absolute `pos` — nothing\n * here is solved for, relaxed, or measured. So the markup is a pure function of\n * the input, it is identical on the server and in the browser, and there is no\n * layout shift on hydration because there is no layout to do.\n *\n * BOUNDARIES ARE DRAWN FIRST, AND DRAWN DIFFERENTLY. A `region` is where\n * something RUNS — a VPC, a zone, a cluster — and is a solid frame. A\n * `security-group` is what may REACH it, and is dashed. That is not decoration:\n * an infrastructure diagram is very often read for exactly one of those two\n * questions, and a reader should be able to tell which line answers which\n * without reading either label.\n *\n * @example\n * <ArchitectureFigure\n * spec={{\n * meta: { title: 'Request path' },\n * components: [\n * { id: 'cdn', type: 'cloud', label: 'CloudFront', row: 0, col: 0 },\n * { id: 'api', type: 'backend', label: 'FastAPI', sublabel: 'ECS', row: 0, col: 1 },\n * { id: 'db', type: 'database', label: 'Postgres', row: 0, col: 2 },\n * ],\n * connections: [\n * { from: 'cdn', to: 'api', label: 'HTTPS' },\n * { from: 'api', to: 'db', label: 'SQL' },\n * ],\n * }}\n * />\n */\nexport function ArchitectureFigure({\n spec,\n className,\n legend = 'auto',\n cards = true,\n heading = true,\n activeIds,\n onSelectNode,\n}: ArchitectureFigureProps) {\n const uid = useId().replace(/:/g, '')\n const model = useMemo(() => buildModel(spec, uid, activeIds, onSelectNode), [spec, uid, activeIds, onSelectNode])\n\n return (\n <DiagramFrame\n meta={spec.meta}\n model={model}\n cards={spec.cards}\n showCards={cards}\n heading={heading}\n legend={legend}\n className={className}\n activeIds={activeIds}\n onSelectNode={onSelectNode}\n />\n )\n}\n\nfunction buildModel(\n spec: ArchitectureSpec,\n uid: string,\n activeIds: string[] | undefined,\n onSelectNode: ((id: string) => void) | undefined,\n): FigureModel {\n const layout = { ...GRID, ...spec.layout }\n const [originX, originY] = layout.origin\n\n const boxes = new Map<string, Box>()\n const placed = spec.components.map((component) => {\n const labelWidth = component.size?.[0] ?? component.width ?? GRID.cellW\n const lines = wrapText(component.label, TYPE.label, labelWidth - PLATE.padX * 2, 2)\n const height = plateHeight(\n lines.length,\n Boolean(component.sublabel),\n Boolean(component.type || component.tag),\n component.size?.[1] ?? component.height ?? 0,\n )\n\n const x = component.pos ? component.pos[0] : originX + (component.col ?? 0) * (layout.cellW + layout.gapX)\n const y = component.pos ? component.pos[1] : originY + (component.row ?? 0) * (layout.cellH + layout.gapY)\n\n const box: Box = { x, y, w: labelWidth, h: height }\n boxes.set(component.id, box)\n return { component, box }\n })\n\n const boundaries = (spec.boundaries ?? []).map((boundary) => {\n const wrapped = boundary.wraps.map((id) => boxes.get(id)).filter((box): box is Box => Boolean(box))\n const extent = union(wrapped)\n return { boundary, box: extent ? inflate(extent, boundary.pad ?? 28) : null }\n })\n\n // A waypoint is only meaningful when both ends were placed absolutely — see\n // `Wire.keepWaypoints`. In this diagram type that is common, which is why an\n // archify architecture specification's hand-tuned routes survive the move.\n const absolute = new Set(\n spec.components.filter((component) => component.pos).map((component) => component.id),\n )\n const wires = wiresFor(spec.connections, boxes, (connection) => ({\n keepWaypoints: absolute.has(connection.from) && absolute.has(connection.to),\n }))\n const { paths, labels } = renderWires(wires, uid)\n\n const drawn = [...boxes.values(), ...boundaries.map((entry) => entry.box).filter((box): box is Box => Boolean(box))]\n const extent = inflate(union(drawn) ?? { x: 0, y: 0, w: 400, h: 200 }, 22)\n\n const active = activeIds && activeIds.length > 0 ? new Set(activeIds) : null\n\n const artwork = (\n <>\n {boundaries.map(({ boundary, box }, index) =>\n box ? (\n <BandFrame\n key={`${boundary.label}-${index}`}\n box={box}\n label={boundary.label}\n dashed={boundary.kind === 'security-group'}\n />\n ) : null,\n )}\n {paths}\n {placed.map(({ component, box }) => (\n <NodePlate\n key={component.id}\n nodeId={component.id}\n box={box}\n label={component.label}\n sublabel={component.sublabel}\n tag={component.tag}\n kind={component.type}\n active={active?.has(component.id)}\n dimmed={active ? !active.has(component.id) : false}\n onSelect={onSelectNode ? () => onSelectNode(component.id) : undefined}\n />\n ))}\n {labels}\n </>\n )\n\n const used = [...new Set(spec.components.map((component) => component.type))]\n\n return {\n extent,\n artwork,\n nodes: spec.components.map((component) => ({\n id: component.id,\n label: component.label,\n sublabel: component.sublabel,\n kind: component.type,\n })),\n edges: (spec.connections ?? []).map((connection) => ({\n from: connection.from,\n to: connection.to,\n label: connection.label,\n })),\n legend: kindLegend(\n resolveLegend(used, spec.meta.legend?.mode, KINDS, spec.meta.legend?.entries),\n spec.meta.legend?.entries,\n ),\n }\n}\n\nexport default ArchitectureFigure\n"],"mappings":"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","names":["extent"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/diagrams/ArchitectureFigure/ArchitectureFigure.tsx"],"sourcesContent":["'use client'\n\nimport { useId, useMemo } from 'react'\nimport { liveEdges, useSpecIdentity, warnCollision } from '../lib/dev'\nimport { DiagramFrame, type FigureChrome, type FigureModel } from '../lib/frame'\nimport { inflate, round, TYPE, union, wrapText, type Box } from '../lib/geometry'\nimport { kindLegend, resolveLegend } from '../lib/legend'\nimport { BandFrame, NodePlate, PLATE, plateHeight } from '../lib/marks'\nimport { renderWires, wiresFor } from '../lib/wires'\nimport type { ArchitectureComponent, ArchitectureSpec, NodeKind } from '../spec'\n\nconst KINDS: NodeKind[] = [\n 'frontend',\n 'backend',\n 'database',\n 'cloud',\n 'security',\n 'messagebus',\n 'external',\n]\n\n/** What a cell is, when the specification does not say. */\nconst GRID = { origin: [40, 48] as [number, number], cols: 4, cellW: 184, cellH: 72, gapX: 64, gapY: 76 }\n\nexport interface ArchitectureFigureProps extends FigureChrome {\n spec: ArchitectureSpec\n}\n\n/**\n * A component map: services, datastores, boundaries, and what talks to what.\n *\n * Takes the same JSON an archify `architecture` specification carries —\n * `components`, `boundaries`, `connections` — and draws it in this system's own\n * terms: paper plates on a hairline, one reversed plate for the component the\n * diagram is about, and seven drawn sigils where archify uses seven hues.\n *\n * IT RENDERS ON A SERVER because every position is already in the\n * specification. A component gives a `row` and a `col` into a grid whose cell\n * size is either declared or defaulted, or it gives an absolute `pos` — nothing\n * here is solved for, relaxed, or measured. So the markup is a pure function of\n * the input, it is identical on the server and in the browser, and there is no\n * layout shift on hydration because there is no layout to do.\n *\n * A component that declares NEITHER flows: it takes the next free cell in\n * declaration order, wrapping at `layout.cols`. That is still arithmetic on\n * numbers the specification carries rather than a solver — and the alternative\n * was every unplaced component defaulting to row 0, column 0 and stacking into\n * one plate.\n *\n * BOUNDARIES ARE DRAWN FIRST, AND DRAWN DIFFERENTLY. A `region` is where\n * something RUNS — a VPC, a zone, a cluster — and is a solid frame. A\n * `security-group` is what may REACH it, and is dashed. That is not decoration:\n * an infrastructure diagram is very often read for exactly one of those two\n * questions, and a reader should be able to tell which line answers which\n * without reading either label.\n *\n * @example\n * <ArchitectureFigure\n * spec={{\n * meta: { title: 'Request path' },\n * components: [\n * { id: 'cdn', type: 'cloud', label: 'CloudFront', row: 0, col: 0 },\n * { id: 'api', type: 'backend', label: 'FastAPI', sublabel: 'ECS', row: 0, col: 1 },\n * { id: 'db', type: 'database', label: 'Postgres', row: 0, col: 2 },\n * ],\n * connections: [\n * { from: 'cdn', to: 'api', label: 'HTTPS' },\n * { from: 'api', to: 'db', label: 'SQL' },\n * ],\n * }}\n * />\n */\nexport function ArchitectureFigure({\n spec,\n className,\n legend = 'auto',\n cards = true,\n heading = true,\n activeIds,\n onSelectNode,\n}: ArchitectureFigureProps) {\n const uid = useId().replace(/:/g, '')\n useSpecIdentity(spec, 'ArchitectureFigure')\n const model = useMemo(() => buildModel(spec, uid, activeIds, onSelectNode), [spec, uid, activeIds, onSelectNode])\n\n return (\n <DiagramFrame\n meta={spec.meta}\n model={model}\n cards={spec.cards}\n showCards={cards}\n heading={heading}\n legend={legend}\n className={className}\n activeIds={activeIds}\n onSelectNode={onSelectNode}\n />\n )\n}\n\n/**\n * Where a component sits, for a specification that did not say.\n *\n * `row` and `col` are both optional and both defaulted to 0, which meant every\n * component that declared no placement was drawn in the same cell — a figure of\n * seven services rendering as one plate with six underneath it. Reading them as\n * \"put it in the next free cell\" is what `layout.cols` was always for.\n *\n * Cells an explicit `row`/`col` claims are reserved before the flow starts, so\n * an author who placed three of ten components keeps those three exactly where\n * they put them.\n */\nfunction flow(\n components: ArchitectureComponent[],\n cols: number,\n): (component: ArchitectureComponent) => [row: number, col: number] {\n const claimed = new Set<string>()\n for (const component of components) {\n if (!component.pos && (component.row !== undefined || component.col !== undefined)) {\n claimed.add(`${component.row ?? 0}:${component.col ?? 0}`)\n }\n }\n\n let cursor = 0\n return (component) => {\n if (component.row !== undefined || component.col !== undefined) {\n return [component.row ?? 0, component.col ?? 0]\n }\n while (claimed.has(`${Math.floor(cursor / cols)}:${cursor % cols}`)) cursor += 1\n const cell: [number, number] = [Math.floor(cursor / cols), cursor % cols]\n claimed.add(`${cell[0]}:${cell[1]}`)\n cursor += 1\n return cell\n }\n}\n\nfunction buildModel(\n spec: ArchitectureSpec,\n uid: string,\n activeIds: string[] | undefined,\n onSelectNode: ((id: string) => void) | undefined,\n): FigureModel {\n const layout = { ...GRID, ...spec.layout }\n const [originX, originY] = layout.origin\n const nextCell = flow(spec.components, Math.max(1, Math.round(layout.cols)))\n\n const boxes = new Map<string, Box>()\n const cells = new Map<string, string[]>()\n const named = new Map(spec.components.map((component) => [component.id, component.label]))\n const placed = spec.components.map((component) => {\n // `layout.cellW` sets the PITCH and the plate's own width. Reading the\n // module default here instead spaced the plates further apart at the width\n // they already had, which is a grid with gaps rather than wider boxes.\n const labelWidth = component.size?.[0] ?? component.width ?? layout.cellW\n const lines = wrapText(component.label, TYPE.label, labelWidth - PLATE.padX * 2, 2)\n const height = plateHeight(\n lines.length,\n Boolean(component.sublabel),\n Boolean(component.type || component.tag),\n component.size?.[1] ?? component.height ?? 0,\n )\n\n const [row, col] = nextCell(component)\n const x = component.pos ? component.pos[0] : originX + col * (layout.cellW + layout.gapX)\n const y = component.pos ? component.pos[1] : originY + row * (layout.cellH + layout.gapY)\n\n const box: Box = { x, y, w: labelWidth, h: height }\n boxes.set(component.id, box)\n cells.set(`${round(x)}:${round(y)}`, [...(cells.get(`${round(x)}:${round(y)}`) ?? []), component.id])\n return { component, box }\n })\n\n // Nothing else notices: two plates at one coordinate draw one on top of the\n // other, and both are still in the summary — so the mistake exists in the\n // picture and nowhere the author is looking. `warnCollision` is dev-only.\n for (const ids of cells.values()) {\n if (ids.length > 1) warnCollision('ArchitectureFigure', ids)\n }\n\n const boundaries = (spec.boundaries ?? []).map((boundary) => {\n const inside = boundary.wraps.filter((id) => boxes.has(id))\n const wrapped = inside.map((id) => boxes.get(id)).filter((box): box is Box => Boolean(box))\n const extent = union(wrapped)\n return { boundary, inside, box: extent ? inflate(extent, boundary.pad ?? 28) : null }\n })\n\n // A waypoint is only meaningful when both ends were placed absolutely — see\n // `Wire.keepWaypoints`. In this diagram type that is common, which is why an\n // archify architecture specification's hand-tuned routes survive the move.\n const absolute = new Set(\n spec.components.filter((component) => component.pos).map((component) => component.id),\n )\n const connections = liveEdges('ArchitectureFigure', spec.connections, (id) => boxes.has(id))\n const wires = wiresFor(connections, boxes, (connection) => ({\n keepWaypoints: absolute.has(connection.from) && absolute.has(connection.to),\n }))\n const { paths, labels } = renderWires(wires, uid)\n\n const drawn = [...boxes.values(), ...boundaries.map((entry) => entry.box).filter((box): box is Box => Boolean(box))]\n const extent = inflate(union(drawn) ?? { x: 0, y: 0, w: 400, h: 200 }, 22)\n\n const active = activeIds && activeIds.length > 0 ? new Set(activeIds) : null\n\n const artwork = (\n <>\n {boundaries.map(({ boundary, box }, index) =>\n box ? (\n <BandFrame\n key={`${boundary.label}-${index}`}\n box={box}\n label={boundary.label}\n dashed={boundary.kind === 'security-group'}\n />\n ) : null,\n )}\n {paths}\n {placed.map(({ component, box }) => (\n <NodePlate\n key={component.id}\n nodeId={component.id}\n box={box}\n label={component.label}\n sublabel={component.sublabel}\n tag={component.tag}\n kind={component.type}\n active={active?.has(component.id)}\n dimmed={active ? !active.has(component.id) : false}\n onSelect={onSelectNode ? () => onSelectNode(component.id) : undefined}\n />\n ))}\n {labels}\n </>\n )\n\n const used = [...new Set(spec.components.map((component) => component.type))]\n\n return {\n extent,\n artwork,\n nodes: spec.components.map((component) => ({\n id: component.id,\n label: component.label,\n sublabel: component.sublabel,\n kind: component.type,\n })),\n edges: connections.map((connection) => ({\n from: connection.from,\n to: connection.to,\n label: connection.label,\n })),\n // A boundary is a statement — this runs there, that may reach it — drawn as\n // a frame inside artwork a screen reader is told to skip. It groups nothing\n // in the list, because two boundaries can hold the same component and a\n // node published twice is a node the keyboard reaches twice.\n notes: boundaries.map(({ boundary, inside }) => {\n const kind = boundary.kind === 'region' ? 'Region' : 'Security group'\n const members = inside.map((id) => named.get(id) ?? id)\n return members.length > 0\n ? `${kind} ${boundary.label} encloses ${members.join(', ')}.`\n : `${kind} ${boundary.label} encloses nothing this figure draws.`\n }),\n legend: kindLegend(\n resolveLegend(used, spec.meta.legend?.mode, KINDS, spec.meta.legend?.entries),\n spec.meta.legend?.entries,\n ),\n }\n}\n\nexport default ArchitectureFigure\n"],"mappings":"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","names":["extent"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId, useMemo } from "react";
|
|
4
|
+
import { liveEdges, useSpecIdentity, warnStageOutOfRange } from "../lib/dev.js";
|
|
4
5
|
import { DiagramFrame } from "../lib/frame.js";
|
|
5
6
|
import { inflate, round, TYPE, union, wrapText } from "../lib/geometry.js";
|
|
6
7
|
import { kindLegend, resolveLegend } from "../lib/legend.js";
|
|
@@ -26,6 +27,7 @@ function DataflowFigure({
|
|
|
26
27
|
onSelectNode
|
|
27
28
|
}) {
|
|
28
29
|
const uid = useId().replace(/:/g, "");
|
|
30
|
+
useSpecIdentity(spec, "DataflowFigure");
|
|
29
31
|
const model = useMemo(
|
|
30
32
|
() => buildModel(spec, uid, activeIds, onSelectNode),
|
|
31
33
|
[spec, uid, activeIds, onSelectNode]
|
|
@@ -51,13 +53,17 @@ function buildModel(spec, uid, activeIds, onSelectNode) {
|
|
|
51
53
|
const w = node.width ?? GRID.colW;
|
|
52
54
|
const lines = wrapText(node.label, TYPE.label, w - PLATE.padX * 2, 2);
|
|
53
55
|
const h = plateHeight(lines.length, Boolean(node.sublabel), true, node.height ?? 0);
|
|
56
|
+
if (node.stage < 0 || node.stage >= spec.stages.length) {
|
|
57
|
+
warnStageOutOfRange("DataflowFigure", node.id, node.stage, spec.stages.length);
|
|
58
|
+
}
|
|
54
59
|
const x = GRID.originX + node.stage * (GRID.colW + GRID.gapX);
|
|
55
60
|
const y = GRID.originY + node.row * (GRID.rowH + GRID.nodeH) + (node.yOffset ?? 0);
|
|
56
61
|
const box = { x, y, w, h };
|
|
57
62
|
boxes.set(node.id, box);
|
|
58
63
|
return { node, box };
|
|
59
64
|
});
|
|
60
|
-
const
|
|
65
|
+
const flows = liveEdges("DataflowFigure", spec.flows, (id) => boxes.has(id));
|
|
66
|
+
const wires = wiresFor(flows, boxes);
|
|
61
67
|
const { paths, labels } = renderWires(wires, uid);
|
|
62
68
|
const extent = inflate(union([...boxes.values()]) ?? { x: 0, y: 0, w: 500, h: 240 }, 30);
|
|
63
69
|
const headerY = Math.min(GRID.originY - 30, extent.y + 28);
|
|
@@ -101,6 +107,18 @@ function buildModel(spec, uid, activeIds, onSelectNode) {
|
|
|
101
107
|
labels
|
|
102
108
|
] });
|
|
103
109
|
const used = [...new Set(spec.nodes.map((node) => node.type))];
|
|
110
|
+
const inStage = (index) => spec.nodes.filter((node) => node.stage === index).sort((a, b) => a.row - b.row).map((node) => node.id);
|
|
111
|
+
const stray = spec.nodes.filter((node) => node.stage < 0 || node.stage >= spec.stages.length).map((node) => node.id);
|
|
112
|
+
const bands = [
|
|
113
|
+
...spec.stages.map((stage, index) => ({
|
|
114
|
+
kind: "Stage",
|
|
115
|
+
label: stage.label,
|
|
116
|
+
ids: inStage(index)
|
|
117
|
+
})),
|
|
118
|
+
// Drawn past the last heading, so said that way rather than folded in
|
|
119
|
+
// beside nodes the axis does label.
|
|
120
|
+
...stray.length > 0 ? [{ kind: "Stage", label: "outside the declared stages", ids: stray }] : []
|
|
121
|
+
];
|
|
104
122
|
return {
|
|
105
123
|
extent: { ...extent, y: headerY - 22, h: extent.h + (extent.y - (headerY - 22)) },
|
|
106
124
|
artwork,
|
|
@@ -110,11 +128,12 @@ function buildModel(spec, uid, activeIds, onSelectNode) {
|
|
|
110
128
|
sublabel: node.sublabel,
|
|
111
129
|
kind: node.type
|
|
112
130
|
})),
|
|
113
|
-
edges:
|
|
131
|
+
edges: flows.map((flow) => ({
|
|
114
132
|
from: flow.from,
|
|
115
133
|
to: flow.to,
|
|
116
134
|
label: [flow.label, flow.classification].filter(Boolean).join(" \u2014 ") || void 0
|
|
117
135
|
})),
|
|
136
|
+
bands,
|
|
118
137
|
legend: kindLegend(
|
|
119
138
|
resolveLegend(used, spec.meta.legend?.mode, KINDS, spec.meta.legend?.entries),
|
|
120
139
|
spec.meta.legend?.entries
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/diagrams/DataflowFigure/DataflowFigure.tsx"],"sourcesContent":["'use client'\n\nimport { useId, useMemo } from 'react'\nimport { DiagramFrame, type FigureChrome, type FigureModel } from '../lib/frame'\nimport { inflate, round, TYPE, union, wrapText, type Box } from '../lib/geometry'\nimport { kindLegend, resolveLegend } from '../lib/legend'\nimport { NodePlate, PLATE, plateHeight } from '../lib/marks'\nimport { renderWires, wiresFor } from '../lib/wires'\nimport type { DataflowSpec, NodeKind } from '../spec'\n\nconst KINDS: NodeKind[] = [\n 'frontend',\n 'backend',\n 'database',\n 'cloud',\n 'security',\n 'messagebus',\n 'external',\n]\n\nconst GRID = { originX: 40, originY: 92, colW: 184, gapX: 78, rowH: 52, nodeH: 74 }\n\nexport interface DataflowFigureProps extends FigureChrome {\n spec: DataflowSpec\n}\n\n/**\n * A pipeline: where data comes from, what happens to it, and who ends up with\n * it.\n *\n * Structurally close to an architecture map and read for a completely different\n * question, which is why it is a separate renderer rather than a preset. An\n * architecture diagram is read for \"what talks to what\". A data-flow diagram is\n * read for \"what is IN this arrow\" — and that question is why `classification`\n * gets its own mono chip under the label rather than being folded into the\n * wording. `clickstream / PII touch` and `clickstream` are two different facts,\n * and a governance reviewer is looking for the second one.\n *\n * Stages are printed as column headings on a rule across the top. They are the\n * axis of the figure: a node's `stage` is its position along the pipeline, and\n * a reader should be able to answer \"how far has this got\" by looking up rather\n * than by tracing arrows.\n */\nexport function DataflowFigure({\n spec,\n className,\n legend = 'auto',\n cards = true,\n heading = true,\n activeIds,\n onSelectNode,\n}: DataflowFigureProps) {\n const uid = useId().replace(/:/g, '')\n const model = useMemo(\n () => buildModel(spec, uid, activeIds, onSelectNode),\n [spec, uid, activeIds, onSelectNode],\n )\n\n return (\n <DiagramFrame\n meta={spec.meta}\n model={model}\n cards={spec.cards}\n showCards={cards}\n heading={heading}\n legend={legend}\n className={className}\n activeIds={activeIds}\n onSelectNode={onSelectNode}\n />\n )\n}\n\nfunction buildModel(\n spec: DataflowSpec,\n uid: string,\n activeIds: string[] | undefined,\n onSelectNode: ((id: string) => void) | undefined,\n): FigureModel {\n const boxes = new Map<string, Box>()\n const placed = spec.nodes.map((node) => {\n const w = node.width ?? GRID.colW\n const lines = wrapText(node.label, TYPE.label, w - PLATE.padX * 2, 2)\n const h = plateHeight(lines.length, Boolean(node.sublabel), true, node.height ?? 0)\n\n const x = GRID.originX + node.stage * (GRID.colW + GRID.gapX)\n const y = GRID.originY + node.row * (GRID.rowH + GRID.nodeH) + (node.yOffset ?? 0)\n\n const box: Box = { x, y, w, h }\n boxes.set(node.id, box)\n return { node, box }\n })\n\n // Waypoints refer to the author's own grid, not this one — see\n // `Wire.keepWaypoints`. A data-flow specification always places by stage and\n // row, so there is never a case where honouring them would be correct.\n const wires = wiresFor(spec.flows, boxes)\n const { paths, labels } = renderWires(wires, uid)\n\n const extent = inflate(union([...boxes.values()]) ?? { x: 0, y: 0, w: 500, h: 240 }, 30)\n const headerY = Math.min(GRID.originY - 30, extent.y + 28)\n const active = activeIds && activeIds.length > 0 ? new Set(activeIds) : null\n\n const artwork = (\n <>\n {/* Stage headings, on one rule across the whole figure. */}\n <path\n d={`M ${round(extent.x + 14)} ${round(headerY + 8)} H ${round(extent.x + extent.w - 14)}`}\n className=\"stroke-(--diagram-rule) [stroke-width:1]\"\n aria-hidden=\"true\"\n />\n {spec.stages.map((stage, index) => (\n <text\n key={`${stage.label}-${index}`}\n x={round(GRID.originX + index * (GRID.colW + GRID.gapX))}\n y={round(headerY)}\n className=\"fill-(--diagram-ink-3) font-mono [font-size:10.5px] [letter-spacing:0.08em] uppercase\"\n aria-hidden=\"true\"\n >\n {stage.label}\n </text>\n ))}\n\n {paths}\n\n {placed.map(({ node, box }) => (\n <NodePlate\n key={node.id}\n nodeId={node.id}\n box={box}\n label={node.label}\n sublabel={node.sublabel}\n tag={node.tag}\n kind={node.type}\n active={active?.has(node.id)}\n dimmed={active ? !active.has(node.id) : false}\n onSelect={onSelectNode ? () => onSelectNode(node.id) : undefined}\n />\n ))}\n\n {labels}\n </>\n )\n\n const used = [...new Set(spec.nodes.map((node) => node.type))]\n\n return {\n extent: { ...extent, y: headerY - 22, h: extent.h + (extent.y - (headerY - 22)) },\n artwork,\n nodes: spec.nodes.map((node) => ({\n id: node.id,\n label: node.label,\n sublabel: node.sublabel,\n kind: node.type,\n })),\n edges: (spec.flows ?? []).map((flow) => ({\n from: flow.from,\n to: flow.to,\n label: [flow.label, flow.classification].filter(Boolean).join(' — ') || undefined,\n })),\n legend: kindLegend(\n resolveLegend(used, spec.meta.legend?.mode, KINDS, spec.meta.legend?.entries),\n spec.meta.legend?.entries,\n ),\n }\n}\n\nexport default DataflowFigure\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/diagrams/DataflowFigure/DataflowFigure.tsx"],"sourcesContent":["'use client'\n\nimport { useId, useMemo } from 'react'\nimport { liveEdges, useSpecIdentity, warnStageOutOfRange } from '../lib/dev'\nimport { DiagramFrame, type FigureBand, type FigureChrome, type FigureModel } from '../lib/frame'\nimport { inflate, round, TYPE, union, wrapText, type Box } from '../lib/geometry'\nimport { kindLegend, resolveLegend } from '../lib/legend'\nimport { NodePlate, PLATE, plateHeight } from '../lib/marks'\nimport { renderWires, wiresFor } from '../lib/wires'\nimport type { DataflowSpec, NodeKind } from '../spec'\n\nconst KINDS: NodeKind[] = [\n 'frontend',\n 'backend',\n 'database',\n 'cloud',\n 'security',\n 'messagebus',\n 'external',\n]\n\nconst GRID = { originX: 40, originY: 92, colW: 184, gapX: 78, rowH: 52, nodeH: 74 }\n\nexport interface DataflowFigureProps extends FigureChrome {\n spec: DataflowSpec\n}\n\n/**\n * A pipeline: where data comes from, what happens to it, and who ends up with\n * it.\n *\n * Structurally close to an architecture map and read for a completely different\n * question, which is why it is a separate renderer rather than a preset. An\n * architecture diagram is read for \"what talks to what\". A data-flow diagram is\n * read for \"what is IN this arrow\" — and that question is why `classification`\n * gets its own mono chip under the label rather than being folded into the\n * wording. `clickstream / PII touch` and `clickstream` are two different facts,\n * and a governance reviewer is looking for the second one.\n *\n * Stages are printed as column headings on a rule across the top. They are the\n * axis of the figure: a node's `stage` is its position along the pipeline, and\n * a reader should be able to answer \"how far has this got\" by looking up rather\n * than by tracing arrows.\n */\nexport function DataflowFigure({\n spec,\n className,\n legend = 'auto',\n cards = true,\n heading = true,\n activeIds,\n onSelectNode,\n}: DataflowFigureProps) {\n const uid = useId().replace(/:/g, '')\n useSpecIdentity(spec, 'DataflowFigure')\n const model = useMemo(\n () => buildModel(spec, uid, activeIds, onSelectNode),\n [spec, uid, activeIds, onSelectNode],\n )\n\n return (\n <DiagramFrame\n meta={spec.meta}\n model={model}\n cards={spec.cards}\n showCards={cards}\n heading={heading}\n legend={legend}\n className={className}\n activeIds={activeIds}\n onSelectNode={onSelectNode}\n />\n )\n}\n\nfunction buildModel(\n spec: DataflowSpec,\n uid: string,\n activeIds: string[] | undefined,\n onSelectNode: ((id: string) => void) | undefined,\n): FigureModel {\n const boxes = new Map<string, Box>()\n const placed = spec.nodes.map((node) => {\n const w = node.width ?? GRID.colW\n const lines = wrapText(node.label, TYPE.label, w - PLATE.padX * 2, 2)\n const h = plateHeight(lines.length, Boolean(node.sublabel), true, node.height ?? 0)\n\n // The stage is an INDEX into the headings, and the x is arithmetic on it\n // rather than a lookup — so a stage nothing declares is drawn a full column\n // past the last heading, under no heading at all.\n if (node.stage < 0 || node.stage >= spec.stages.length) {\n warnStageOutOfRange('DataflowFigure', node.id, node.stage, spec.stages.length)\n }\n\n const x = GRID.originX + node.stage * (GRID.colW + GRID.gapX)\n const y = GRID.originY + node.row * (GRID.rowH + GRID.nodeH) + (node.yOffset ?? 0)\n\n const box: Box = { x, y, w, h }\n boxes.set(node.id, box)\n return { node, box }\n })\n\n // Waypoints refer to the author's own grid, not this one — see\n // `Wire.keepWaypoints`. A data-flow specification always places by stage and\n // row, so there is never a case where honouring them would be correct.\n const flows = liveEdges('DataflowFigure', spec.flows, (id) => boxes.has(id))\n const wires = wiresFor(flows, boxes)\n const { paths, labels } = renderWires(wires, uid)\n\n const extent = inflate(union([...boxes.values()]) ?? { x: 0, y: 0, w: 500, h: 240 }, 30)\n const headerY = Math.min(GRID.originY - 30, extent.y + 28)\n const active = activeIds && activeIds.length > 0 ? new Set(activeIds) : null\n\n const artwork = (\n <>\n {/* Stage headings, on one rule across the whole figure. */}\n <path\n d={`M ${round(extent.x + 14)} ${round(headerY + 8)} H ${round(extent.x + extent.w - 14)}`}\n className=\"stroke-(--diagram-rule) [stroke-width:1]\"\n aria-hidden=\"true\"\n />\n {spec.stages.map((stage, index) => (\n <text\n key={`${stage.label}-${index}`}\n x={round(GRID.originX + index * (GRID.colW + GRID.gapX))}\n y={round(headerY)}\n className=\"fill-(--diagram-ink-3) font-mono [font-size:10.5px] [letter-spacing:0.08em] uppercase\"\n aria-hidden=\"true\"\n >\n {stage.label}\n </text>\n ))}\n\n {paths}\n\n {placed.map(({ node, box }) => (\n <NodePlate\n key={node.id}\n nodeId={node.id}\n box={box}\n label={node.label}\n sublabel={node.sublabel}\n tag={node.tag}\n kind={node.type}\n active={active?.has(node.id)}\n dimmed={active ? !active.has(node.id) : false}\n onSelect={onSelectNode ? () => onSelectNode(node.id) : undefined}\n />\n ))}\n\n {labels}\n </>\n )\n\n const used = [...new Set(spec.nodes.map((node) => node.type))]\n\n // The stages ARE the list's structure. \"How far has this got\" is the question\n // the axis exists to answer, and it was printed only as headings inside\n // artwork a screen reader is told to skip.\n const inStage = (index: number) =>\n spec.nodes\n .filter((node) => node.stage === index)\n .sort((a, b) => a.row - b.row)\n .map((node) => node.id)\n\n const stray = spec.nodes\n .filter((node) => node.stage < 0 || node.stage >= spec.stages.length)\n .map((node) => node.id)\n\n const bands: FigureBand[] = [\n ...spec.stages.map((stage, index) => ({\n kind: 'Stage',\n label: stage.label,\n ids: inStage(index),\n })),\n // Drawn past the last heading, so said that way rather than folded in\n // beside nodes the axis does label.\n ...(stray.length > 0\n ? [{ kind: 'Stage', label: 'outside the declared stages', ids: stray }]\n : []),\n ]\n\n return {\n extent: { ...extent, y: headerY - 22, h: extent.h + (extent.y - (headerY - 22)) },\n artwork,\n nodes: spec.nodes.map((node) => ({\n id: node.id,\n label: node.label,\n sublabel: node.sublabel,\n kind: node.type,\n })),\n edges: flows.map((flow) => ({\n from: flow.from,\n to: flow.to,\n label: [flow.label, flow.classification].filter(Boolean).join(' — ') || undefined,\n })),\n bands,\n legend: kindLegend(\n resolveLegend(used, spec.meta.legend?.mode, KINDS, spec.meta.legend?.entries),\n spec.meta.legend?.entries,\n ),\n }\n}\n\nexport default DataflowFigure\n"],"mappings":"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","names":[]}
|
|
@@ -5,6 +5,20 @@ interface CanvasView {
|
|
|
5
5
|
scale: number;
|
|
6
6
|
x: number;
|
|
7
7
|
y: number;
|
|
8
|
+
/**
|
|
9
|
+
* How big the frame was, in CSS pixels, when this view was reported.
|
|
10
|
+
*
|
|
11
|
+
* Carried on the view because everything that consumes a view needs it and
|
|
12
|
+
* nothing outside this component can measure it: a `DiagramMinimap`'s
|
|
13
|
+
* viewport rectangle is the frame's size over the zoom, and a caller who had
|
|
14
|
+
* to hand-declare that number was hand-declaring the one number that makes
|
|
15
|
+
* the map lie. Absent until the frame has been measured — the first render
|
|
16
|
+
* has no box yet — which is exactly when a map should draw nothing.
|
|
17
|
+
*/
|
|
18
|
+
frame?: {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
};
|
|
8
22
|
}
|
|
9
23
|
/** What a caller can do to the canvas from outside it. */
|
|
10
24
|
interface DiagramCanvasHandle {
|
|
@@ -25,12 +25,17 @@ function DiagramCanvas({
|
|
|
25
25
|
const frame = useRef(null);
|
|
26
26
|
const [view, setView] = useState({ scale: 1, x: 0, y: 0 });
|
|
27
27
|
const drag = useRef(null);
|
|
28
|
+
const measure = useCallback(() => {
|
|
29
|
+
const box = frame.current?.getBoundingClientRect();
|
|
30
|
+
return box && box.width > 0 ? { width: box.width, height: box.height } : void 0;
|
|
31
|
+
}, []);
|
|
28
32
|
const apply = useCallback(
|
|
29
33
|
(next) => {
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
const measured = { ...next, frame: measure() };
|
|
35
|
+
setView(measured);
|
|
36
|
+
onViewChange?.(measured);
|
|
32
37
|
},
|
|
33
|
-
[onViewChange]
|
|
38
|
+
[measure, onViewChange]
|
|
34
39
|
);
|
|
35
40
|
const zoomAbout = useCallback(
|
|
36
41
|
(factor, originX, originY) => {
|
|
@@ -43,13 +48,14 @@ function DiagramCanvas({
|
|
|
43
48
|
const next = {
|
|
44
49
|
scale,
|
|
45
50
|
x: cx - (cx - current.x) * ratio,
|
|
46
|
-
y: cy - (cy - current.y) * ratio
|
|
51
|
+
y: cy - (cy - current.y) * ratio,
|
|
52
|
+
frame: measure()
|
|
47
53
|
};
|
|
48
54
|
onViewChange?.(next);
|
|
49
55
|
return next;
|
|
50
56
|
});
|
|
51
57
|
},
|
|
52
|
-
[onViewChange]
|
|
58
|
+
[measure, onViewChange]
|
|
53
59
|
);
|
|
54
60
|
useImperativeHandle(
|
|
55
61
|
ref,
|
|
@@ -64,7 +70,8 @@ function DiagramCanvas({
|
|
|
64
70
|
const next = {
|
|
65
71
|
...current,
|
|
66
72
|
x: box.width / 2 - x * current.scale,
|
|
67
|
-
y: box.height / 2 - y * current.scale
|
|
73
|
+
y: box.height / 2 - y * current.scale,
|
|
74
|
+
frame: measure()
|
|
68
75
|
};
|
|
69
76
|
onViewChange?.(next);
|
|
70
77
|
return next;
|
|
@@ -72,7 +79,7 @@ function DiagramCanvas({
|
|
|
72
79
|
},
|
|
73
80
|
view
|
|
74
81
|
}),
|
|
75
|
-
[apply, onViewChange, view, zoomAbout]
|
|
82
|
+
[apply, measure, onViewChange, view, zoomAbout]
|
|
76
83
|
);
|
|
77
84
|
useEffect(() => {
|
|
78
85
|
const node = frame.current;
|