@flanksource/clicky-ui 0.3.44 → 0.3.46
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/README.md +24 -0
- package/dist/ai.cjs +4 -2
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.js +3 -1
- package/dist/components/MdxEditorField.cjs +40 -22
- package/dist/components/MdxEditorField.cjs.map +1 -1
- package/dist/components/MdxEditorField.d.ts.map +1 -1
- package/dist/components/MdxEditorField.js +41 -23
- package/dist/components/MdxEditorField.js.map +1 -1
- package/dist/components/json-schema-form-item-summary.cjs +6 -1
- package/dist/components/json-schema-form-item-summary.cjs.map +1 -1
- package/dist/components/json-schema-form-item-summary.d.ts.map +1 -1
- package/dist/components/json-schema-form-item-summary.js +6 -1
- package/dist/components/json-schema-form-item-summary.js.map +1 -1
- package/dist/data/AccessMark.cjs +23 -0
- package/dist/data/AccessMark.cjs.map +1 -0
- package/dist/data/AccessMark.d.ts +11 -0
- package/dist/data/AccessMark.d.ts.map +1 -0
- package/dist/data/AccessMark.js +23 -0
- package/dist/data/AccessMark.js.map +1 -0
- package/dist/data/CodeBlock.cjs +3 -2
- package/dist/data/CodeBlock.cjs.map +1 -1
- package/dist/data/CodeBlock.d.ts +3 -1
- package/dist/data/CodeBlock.d.ts.map +1 -1
- package/dist/data/CodeBlock.js +3 -2
- package/dist/data/CodeBlock.js.map +1 -1
- package/dist/data/CodeDiff.cjs +116 -23
- package/dist/data/CodeDiff.cjs.map +1 -1
- package/dist/data/CodeDiff.d.ts +26 -0
- package/dist/data/CodeDiff.d.ts.map +1 -1
- package/dist/data/CodeDiff.js +116 -23
- package/dist/data/CodeDiff.js.map +1 -1
- package/dist/data/GraphDiagram.cjs +75 -11
- package/dist/data/GraphDiagram.cjs.map +1 -1
- package/dist/data/GraphDiagram.d.ts +17 -2
- package/dist/data/GraphDiagram.d.ts.map +1 -1
- package/dist/data/GraphDiagram.js +76 -12
- package/dist/data/GraphDiagram.js.map +1 -1
- package/dist/data/GraphDiagramGroups.cjs +100 -0
- package/dist/data/GraphDiagramGroups.cjs.map +1 -0
- package/dist/data/GraphDiagramGroups.d.ts +24 -0
- package/dist/data/GraphDiagramGroups.d.ts.map +1 -0
- package/dist/data/GraphDiagramGroups.js +100 -0
- package/dist/data/GraphDiagramGroups.js.map +1 -0
- package/dist/data/GraphDiagramNodes.cjs +46 -51
- package/dist/data/GraphDiagramNodes.cjs.map +1 -1
- package/dist/data/GraphDiagramNodes.d.ts +8 -9
- package/dist/data/GraphDiagramNodes.d.ts.map +1 -1
- package/dist/data/GraphDiagramNodes.js +46 -51
- package/dist/data/GraphDiagramNodes.js.map +1 -1
- package/dist/data/JsonView.cjs +118 -49
- package/dist/data/JsonView.cjs.map +1 -1
- package/dist/data/JsonView.d.ts +13 -10
- package/dist/data/JsonView.d.ts.map +1 -1
- package/dist/data/JsonView.js +118 -49
- package/dist/data/JsonView.js.map +1 -1
- package/dist/data/MarkdownEditor.cjs +2 -2
- package/dist/data/MarkdownEditor.cjs.map +1 -1
- package/dist/data/MarkdownEditor.js +2 -2
- package/dist/data/MarkdownEditor.js.map +1 -1
- package/dist/data/TreeNode.cjs +1 -1
- package/dist/data/TreeNode.cjs.map +1 -1
- package/dist/data/TreeNode.d.ts.map +1 -1
- package/dist/data/TreeNode.js +1 -1
- package/dist/data/TreeNode.js.map +1 -1
- package/dist/data/ai/AdvancedChatConfig.cjs +90 -249
- package/dist/data/ai/AdvancedChatConfig.cjs.map +1 -1
- package/dist/data/ai/AdvancedChatConfig.d.ts +2 -5
- package/dist/data/ai/AdvancedChatConfig.d.ts.map +1 -1
- package/dist/data/ai/AdvancedChatConfig.js +91 -250
- package/dist/data/ai/AdvancedChatConfig.js.map +1 -1
- package/dist/data/ai/ChatButton.cjs +2 -0
- package/dist/data/ai/ChatButton.cjs.map +1 -1
- package/dist/data/ai/ChatButton.d.ts +1 -1
- package/dist/data/ai/ChatButton.d.ts.map +1 -1
- package/dist/data/ai/ChatButton.js +2 -0
- package/dist/data/ai/ChatButton.js.map +1 -1
- package/dist/data/ai/ChatFab.cjs +5 -1
- package/dist/data/ai/ChatFab.cjs.map +1 -1
- package/dist/data/ai/ChatFab.d.ts +5 -1
- package/dist/data/ai/ChatFab.d.ts.map +1 -1
- package/dist/data/ai/ChatFab.js +5 -1
- package/dist/data/ai/ChatFab.js.map +1 -1
- package/dist/data/ai/ChatWindow.cjs +2 -8
- package/dist/data/ai/ChatWindow.cjs.map +1 -1
- package/dist/data/ai/ChatWindow.d.ts.map +1 -1
- package/dist/data/ai/ChatWindow.js +2 -8
- package/dist/data/ai/ChatWindow.js.map +1 -1
- package/dist/data/ai/ChatWindow.runtime.cjs +0 -5
- package/dist/data/ai/ChatWindow.runtime.cjs.map +1 -1
- package/dist/data/ai/ChatWindow.runtime.d.ts +0 -2
- package/dist/data/ai/ChatWindow.runtime.d.ts.map +1 -1
- package/dist/data/ai/ChatWindow.runtime.js +0 -5
- package/dist/data/ai/ChatWindow.runtime.js.map +1 -1
- package/dist/data/ai/PromptRunEditor/runtimeActions.cjs +2 -2
- package/dist/data/ai/PromptRunEditor/runtimeActions.cjs.map +1 -1
- package/dist/data/ai/PromptRunEditor/runtimeActions.d.ts.map +1 -1
- package/dist/data/ai/PromptRunEditor/runtimeActions.js +2 -2
- package/dist/data/ai/PromptRunEditor/runtimeActions.js.map +1 -1
- package/dist/data/ai/SessionInspector.approvals.cjs +3 -6
- package/dist/data/ai/SessionInspector.approvals.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.approvals.js +3 -6
- package/dist/data/ai/SessionInspector.approvals.js.map +1 -1
- package/dist/data/ai/SessionViewer.cjs +13 -4
- package/dist/data/ai/SessionViewer.cjs.map +1 -1
- package/dist/data/ai/SessionViewer.d.ts +5 -1
- package/dist/data/ai/SessionViewer.d.ts.map +1 -1
- package/dist/data/ai/SessionViewer.header.cjs +1 -1
- package/dist/data/ai/SessionViewer.header.cjs.map +1 -1
- package/dist/data/ai/SessionViewer.header.js +1 -1
- package/dist/data/ai/SessionViewer.header.js.map +1 -1
- package/dist/data/ai/SessionViewer.js +13 -4
- package/dist/data/ai/SessionViewer.js.map +1 -1
- package/dist/data/ai/SessionViewer.model.cjs +2 -0
- package/dist/data/ai/SessionViewer.model.cjs.map +1 -1
- package/dist/data/ai/SessionViewer.model.d.ts +1 -0
- package/dist/data/ai/SessionViewer.model.d.ts.map +1 -1
- package/dist/data/ai/SessionViewer.model.js +2 -0
- package/dist/data/ai/SessionViewer.model.js.map +1 -1
- package/dist/data/ai/SessionViewer.rows.cjs +41 -27
- package/dist/data/ai/SessionViewer.rows.cjs.map +1 -1
- package/dist/data/ai/SessionViewer.rows.d.ts +4 -2
- package/dist/data/ai/SessionViewer.rows.d.ts.map +1 -1
- package/dist/data/ai/SessionViewer.rows.js +41 -27
- package/dist/data/ai/SessionViewer.rows.js.map +1 -1
- package/dist/data/ai/SessionViewer.tokens.cjs +35 -0
- package/dist/data/ai/SessionViewer.tokens.cjs.map +1 -0
- package/dist/data/ai/SessionViewer.tokens.d.ts +5 -0
- package/dist/data/ai/SessionViewer.tokens.d.ts.map +1 -0
- package/dist/data/ai/SessionViewer.tokens.js +35 -0
- package/dist/data/ai/SessionViewer.tokens.js.map +1 -0
- package/dist/data/ai/SessionViewer.tool-cost.cjs +128 -0
- package/dist/data/ai/SessionViewer.tool-cost.cjs.map +1 -0
- package/dist/data/ai/SessionViewer.tool-cost.d.ts +6 -0
- package/dist/data/ai/SessionViewer.tool-cost.d.ts.map +1 -0
- package/dist/data/ai/SessionViewer.tool-cost.js +128 -0
- package/dist/data/ai/SessionViewer.tool-cost.js.map +1 -0
- package/dist/data/ai/SessionViewer.tool-details.cjs +3 -1
- package/dist/data/ai/SessionViewer.tool-details.cjs.map +1 -1
- package/dist/data/ai/SessionViewer.tool-details.d.ts.map +1 -1
- package/dist/data/ai/SessionViewer.tool-details.js +3 -1
- package/dist/data/ai/SessionViewer.tool-details.js.map +1 -1
- package/dist/data/ai/SessionViewer.tool-row.cjs +55 -35
- package/dist/data/ai/SessionViewer.tool-row.cjs.map +1 -1
- package/dist/data/ai/SessionViewer.tool-row.d.ts +2 -1
- package/dist/data/ai/SessionViewer.tool-row.d.ts.map +1 -1
- package/dist/data/ai/SessionViewer.tool-row.js +55 -35
- package/dist/data/ai/SessionViewer.tool-row.js.map +1 -1
- package/dist/data/ai/SessionViewer.unified.d.ts +4 -0
- package/dist/data/ai/SessionViewer.unified.d.ts.map +1 -1
- package/dist/data/ai/SessionViewerMenu.cjs +18 -1
- package/dist/data/ai/SessionViewerMenu.cjs.map +1 -1
- package/dist/data/ai/SessionViewerMenu.d.ts +6 -1
- package/dist/data/ai/SessionViewerMenu.d.ts.map +1 -1
- package/dist/data/ai/SessionViewerMenu.js +18 -1
- package/dist/data/ai/SessionViewerMenu.js.map +1 -1
- package/dist/data/ai/ToolPreferences.cjs +149 -143
- package/dist/data/ai/ToolPreferences.cjs.map +1 -1
- package/dist/data/ai/ToolPreferences.d.ts +20 -6
- package/dist/data/ai/ToolPreferences.d.ts.map +1 -1
- package/dist/data/ai/ToolPreferences.js +151 -146
- package/dist/data/ai/ToolPreferences.js.map +1 -1
- package/dist/data/ai/ToolPreferences.model.cjs +14 -10
- package/dist/data/ai/ToolPreferences.model.cjs.map +1 -1
- package/dist/data/ai/ToolPreferences.model.d.ts +1 -0
- package/dist/data/ai/ToolPreferences.model.d.ts.map +1 -1
- package/dist/data/ai/ToolPreferences.model.js +15 -11
- package/dist/data/ai/ToolPreferences.model.js.map +1 -1
- package/dist/data/ai/ToolPreferencesList.cjs +1 -0
- package/dist/data/ai/ToolPreferencesList.cjs.map +1 -1
- package/dist/data/ai/ToolPreferencesList.d.ts +4 -1
- package/dist/data/ai/ToolPreferencesList.d.ts.map +1 -1
- package/dist/data/ai/ToolPreferencesList.js +2 -1
- package/dist/data/ai/ToolPreferencesList.js.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.cjs +52 -53
- package/dist/data/ai/ToolSchemaBrowser.cjs.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.d.ts +6 -3
- package/dist/data/ai/ToolSchemaBrowser.d.ts.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.directory-header.cjs +9 -13
- package/dist/data/ai/ToolSchemaBrowser.directory-header.cjs.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.directory-header.d.ts +5 -2
- package/dist/data/ai/ToolSchemaBrowser.directory-header.d.ts.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.directory-header.js +9 -13
- package/dist/data/ai/ToolSchemaBrowser.directory-header.js.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.js +53 -54
- package/dist/data/ai/ToolSchemaBrowser.js.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.model.cjs +19 -27
- package/dist/data/ai/ToolSchemaBrowser.model.cjs.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.model.d.ts +9 -6
- package/dist/data/ai/ToolSchemaBrowser.model.d.ts.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.model.js +20 -28
- package/dist/data/ai/ToolSchemaBrowser.model.js.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.sidebar.cjs +12 -213
- package/dist/data/ai/ToolSchemaBrowser.sidebar.cjs.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.sidebar.d.ts +1 -3
- package/dist/data/ai/ToolSchemaBrowser.sidebar.d.ts.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.sidebar.js +13 -214
- package/dist/data/ai/ToolSchemaBrowser.sidebar.js.map +1 -1
- package/dist/data/ai/ToolSchemaBrowser.strategies.cjs +54 -0
- package/dist/data/ai/ToolSchemaBrowser.strategies.cjs.map +1 -0
- package/dist/data/ai/ToolSchemaBrowser.strategies.d.ts +9 -0
- package/dist/data/ai/ToolSchemaBrowser.strategies.d.ts.map +1 -0
- package/dist/data/ai/ToolSchemaBrowser.strategies.js +54 -0
- package/dist/data/ai/ToolSchemaBrowser.strategies.js.map +1 -0
- package/dist/data/ai/ToolSchemaBrowser.tree.cjs +179 -0
- package/dist/data/ai/ToolSchemaBrowser.tree.cjs.map +1 -0
- package/dist/data/ai/ToolSchemaBrowser.tree.d.ts +20 -0
- package/dist/data/ai/ToolSchemaBrowser.tree.d.ts.map +1 -0
- package/dist/data/ai/ToolSchemaBrowser.tree.js +179 -0
- package/dist/data/ai/ToolSchemaBrowser.tree.js.map +1 -0
- package/dist/data/ai/index.d.ts +2 -2
- package/dist/data/ai/index.d.ts.map +1 -1
- package/dist/data/ai/runtime-profiles/PermissionStrategiesEditor.cjs +8 -28
- package/dist/data/ai/runtime-profiles/PermissionStrategiesEditor.cjs.map +1 -1
- package/dist/data/ai/runtime-profiles/PermissionStrategiesEditor.d.ts.map +1 -1
- package/dist/data/ai/runtime-profiles/PermissionStrategiesEditor.js +9 -29
- package/dist/data/ai/runtime-profiles/PermissionStrategiesEditor.js.map +1 -1
- package/dist/data/ai/session-token-sizing.d.ts +32 -0
- package/dist/data/ai/session-token-sizing.d.ts.map +1 -0
- package/dist/data/ai/use-session-token-sizing.cjs +73 -0
- package/dist/data/ai/use-session-token-sizing.cjs.map +1 -0
- package/dist/data/ai/use-session-token-sizing.d.ts +21 -0
- package/dist/data/ai/use-session-token-sizing.d.ts.map +1 -0
- package/dist/data/ai/use-session-token-sizing.js +73 -0
- package/dist/data/ai/use-session-token-sizing.js.map +1 -0
- package/dist/data/call-graph/CallGraph.cjs +269 -0
- package/dist/data/call-graph/CallGraph.cjs.map +1 -0
- package/dist/data/call-graph/CallGraph.d.ts +78 -0
- package/dist/data/call-graph/CallGraph.d.ts.map +1 -0
- package/dist/data/call-graph/CallGraph.js +269 -0
- package/dist/data/call-graph/CallGraph.js.map +1 -0
- package/dist/data/call-graph/CallGraphGlyphs.cjs +39 -0
- package/dist/data/call-graph/CallGraphGlyphs.cjs.map +1 -0
- package/dist/data/call-graph/CallGraphGlyphs.d.ts +19 -0
- package/dist/data/call-graph/CallGraphGlyphs.d.ts.map +1 -0
- package/dist/data/call-graph/CallGraphGlyphs.js +39 -0
- package/dist/data/call-graph/CallGraphGlyphs.js.map +1 -0
- package/dist/data/call-graph/CallGraphMembers.cjs +51 -0
- package/dist/data/call-graph/CallGraphMembers.cjs.map +1 -0
- package/dist/data/call-graph/CallGraphMembers.d.ts +22 -0
- package/dist/data/call-graph/CallGraphMembers.d.ts.map +1 -0
- package/dist/data/call-graph/CallGraphMembers.js +51 -0
- package/dist/data/call-graph/CallGraphMembers.js.map +1 -0
- package/dist/data/call-graph/CallGraphParts.cjs +204 -0
- package/dist/data/call-graph/CallGraphParts.cjs.map +1 -0
- package/dist/data/call-graph/CallGraphParts.d.ts +44 -0
- package/dist/data/call-graph/CallGraphParts.d.ts.map +1 -0
- package/dist/data/call-graph/CallGraphParts.js +204 -0
- package/dist/data/call-graph/CallGraphParts.js.map +1 -0
- package/dist/data/call-graph/CallGraphToolbar.cjs +198 -0
- package/dist/data/call-graph/CallGraphToolbar.cjs.map +1 -0
- package/dist/data/call-graph/CallGraphToolbar.d.ts +55 -0
- package/dist/data/call-graph/CallGraphToolbar.d.ts.map +1 -0
- package/dist/data/call-graph/CallGraphToolbar.js +198 -0
- package/dist/data/call-graph/CallGraphToolbar.js.map +1 -0
- package/dist/data/call-graph/call-graph-exclude.cjs +75 -0
- package/dist/data/call-graph/call-graph-exclude.cjs.map +1 -0
- package/dist/data/call-graph/call-graph-exclude.d.ts +31 -0
- package/dist/data/call-graph/call-graph-exclude.d.ts.map +1 -0
- package/dist/data/call-graph/call-graph-exclude.js +75 -0
- package/dist/data/call-graph/call-graph-exclude.js.map +1 -0
- package/dist/data/call-graph/call-graph-labels.cjs +96 -0
- package/dist/data/call-graph/call-graph-labels.cjs.map +1 -0
- package/dist/data/call-graph/call-graph-labels.d.ts +53 -0
- package/dist/data/call-graph/call-graph-labels.d.ts.map +1 -0
- package/dist/data/call-graph/call-graph-labels.js +96 -0
- package/dist/data/call-graph/call-graph-labels.js.map +1 -0
- package/dist/data/call-graph/call-graph-model.cjs +312 -0
- package/dist/data/call-graph/call-graph-model.cjs.map +1 -0
- package/dist/data/call-graph/call-graph-model.d.ts +138 -0
- package/dist/data/call-graph/call-graph-model.d.ts.map +1 -0
- package/dist/data/call-graph/call-graph-model.js +312 -0
- package/dist/data/call-graph/call-graph-model.js.map +1 -0
- package/dist/data/call-graph/call-graph-vocabulary.cjs +195 -0
- package/dist/data/call-graph/call-graph-vocabulary.cjs.map +1 -0
- package/dist/data/call-graph/call-graph-vocabulary.d.ts +76 -0
- package/dist/data/call-graph/call-graph-vocabulary.d.ts.map +1 -0
- package/dist/data/call-graph/call-graph-vocabulary.js +195 -0
- package/dist/data/call-graph/call-graph-vocabulary.js.map +1 -0
- package/dist/data/call-graph/call-graph.fixtures.d.ts +63 -0
- package/dist/data/call-graph/call-graph.fixtures.d.ts.map +1 -0
- package/dist/data/call-graph/index.d.ts +7 -0
- package/dist/data/call-graph/index.d.ts.map +1 -0
- package/dist/data/call-graph/types.cjs +5 -0
- package/dist/data/call-graph/types.cjs.map +1 -0
- package/dist/data/call-graph/types.d.ts +140 -0
- package/dist/data/call-graph/types.d.ts.map +1 -0
- package/dist/data/call-graph/types.js +5 -0
- package/dist/data/call-graph/types.js.map +1 -0
- package/dist/data/call-graph/use-call-graph.cjs +93 -0
- package/dist/data/call-graph/use-call-graph.cjs.map +1 -0
- package/dist/data/call-graph/use-call-graph.d.ts +24 -0
- package/dist/data/call-graph/use-call-graph.d.ts.map +1 -0
- package/dist/data/call-graph/use-call-graph.js +93 -0
- package/dist/data/call-graph/use-call-graph.js.map +1 -0
- package/dist/data/chat/Chat.cjs +39 -15
- package/dist/data/chat/Chat.cjs.map +1 -1
- package/dist/data/chat/Chat.d.ts.map +1 -1
- package/dist/data/chat/Chat.fixtures.cjs +2 -2
- package/dist/data/chat/Chat.fixtures.cjs.map +1 -1
- package/dist/data/chat/Chat.fixtures.js +2 -2
- package/dist/data/chat/Chat.fixtures.js.map +1 -1
- package/dist/data/chat/Chat.js +40 -16
- package/dist/data/chat/Chat.js.map +1 -1
- package/dist/data/chat/ChatRuntimeToolbar.cjs +3 -6
- package/dist/data/chat/ChatRuntimeToolbar.cjs.map +1 -1
- package/dist/data/chat/ChatRuntimeToolbar.d.ts.map +1 -1
- package/dist/data/chat/ChatRuntimeToolbar.js +3 -6
- package/dist/data/chat/ChatRuntimeToolbar.js.map +1 -1
- package/dist/data/chat/ContextMeter.bar.cjs +5 -8
- package/dist/data/chat/ContextMeter.bar.cjs.map +1 -1
- package/dist/data/chat/ContextMeter.bar.d.ts +1 -1
- package/dist/data/chat/ContextMeter.bar.d.ts.map +1 -1
- package/dist/data/chat/ContextMeter.bar.js +5 -8
- package/dist/data/chat/ContextMeter.bar.js.map +1 -1
- package/dist/data/chat/ContextMeter.cjs +18 -19
- package/dist/data/chat/ContextMeter.cjs.map +1 -1
- package/dist/data/chat/ContextMeter.d.ts +1 -1
- package/dist/data/chat/ContextMeter.d.ts.map +1 -1
- package/dist/data/chat/ContextMeter.js +18 -19
- package/dist/data/chat/ContextMeter.js.map +1 -1
- package/dist/data/chat/Conversation.cjs +2 -1
- package/dist/data/chat/Conversation.cjs.map +1 -1
- package/dist/data/chat/Conversation.d.ts +4 -1
- package/dist/data/chat/Conversation.d.ts.map +1 -1
- package/dist/data/chat/Conversation.js +2 -1
- package/dist/data/chat/Conversation.js.map +1 -1
- package/dist/data/chat/Message.cjs +39 -12
- package/dist/data/chat/Message.cjs.map +1 -1
- package/dist/data/chat/Message.d.ts +1 -1
- package/dist/data/chat/Message.d.ts.map +1 -1
- package/dist/data/chat/Message.js +40 -13
- package/dist/data/chat/Message.js.map +1 -1
- package/dist/data/chat/ToolCall.cjs +60 -5
- package/dist/data/chat/ToolCall.cjs.map +1 -1
- package/dist/data/chat/ToolCall.d.ts +7 -2
- package/dist/data/chat/ToolCall.d.ts.map +1 -1
- package/dist/data/chat/ToolCall.js +60 -5
- package/dist/data/chat/ToolCall.js.map +1 -1
- package/dist/data/chat/approval.cjs.map +1 -1
- package/dist/data/chat/approval.d.ts +2 -2
- package/dist/data/chat/approval.d.ts.map +1 -1
- package/dist/data/chat/approval.js.map +1 -1
- package/dist/data/chat/tool-policy.cjs +59 -2
- package/dist/data/chat/tool-policy.cjs.map +1 -1
- package/dist/data/chat/tool-policy.d.ts +9 -0
- package/dist/data/chat/tool-policy.d.ts.map +1 -1
- package/dist/data/chat/tool-policy.js +59 -2
- package/dist/data/chat/tool-policy.js.map +1 -1
- package/dist/data/chat/types.cjs +6 -0
- package/dist/data/chat/types.cjs.map +1 -1
- package/dist/data/chat/types.d.ts +13 -4
- package/dist/data/chat/types.d.ts.map +1 -1
- package/dist/data/chat/types.js +7 -1
- package/dist/data/chat/types.js.map +1 -1
- package/dist/data/chat/usage-snapshot.cjs +11 -6
- package/dist/data/chat/usage-snapshot.cjs.map +1 -1
- package/dist/data/chat/usage-snapshot.d.ts +0 -1
- package/dist/data/chat/usage-snapshot.d.ts.map +1 -1
- package/dist/data/chat/usage-snapshot.js +11 -6
- package/dist/data/chat/usage-snapshot.js.map +1 -1
- package/dist/data/data-access.cjs +8 -0
- package/dist/data/data-access.cjs.map +1 -0
- package/dist/data/data-access.d.ts +5 -0
- package/dist/data/data-access.d.ts.map +1 -0
- package/dist/data/data-access.js +8 -0
- package/dist/data/data-access.js.map +1 -0
- package/dist/data/diagnostics/StackFrameRow.cjs +3 -3
- package/dist/data/diagnostics/StackFrameRow.cjs.map +1 -1
- package/dist/data/diagnostics/StackFrameRow.js +3 -3
- package/dist/data/diagnostics/StackFrameRow.js.map +1 -1
- package/dist/data/git/GitChangeList.cjs +6 -2
- package/dist/data/git/GitChangeList.cjs.map +1 -1
- package/dist/data/git/GitChangeList.d.ts +6 -1
- package/dist/data/git/GitChangeList.d.ts.map +1 -1
- package/dist/data/git/GitChangeList.js +6 -2
- package/dist/data/git/GitChangeList.js.map +1 -1
- package/dist/data/graph-columns-layout.cjs +61 -14
- package/dist/data/graph-columns-layout.cjs.map +1 -1
- package/dist/data/graph-columns-layout.d.ts +42 -0
- package/dist/data/graph-columns-layout.d.ts.map +1 -1
- package/dist/data/graph-columns-layout.js +61 -14
- package/dist/data/graph-columns-layout.js.map +1 -1
- package/dist/data/graph-diagram-model.cjs +5 -0
- package/dist/data/graph-diagram-model.cjs.map +1 -1
- package/dist/data/graph-diagram-model.d.ts +24 -0
- package/dist/data/graph-diagram-model.d.ts.map +1 -1
- package/dist/data/graph-diagram-model.js +5 -0
- package/dist/data/graph-diagram-model.js.map +1 -1
- package/dist/data/graph-edge-routing.cjs +9 -3
- package/dist/data/graph-edge-routing.cjs.map +1 -1
- package/dist/data/graph-edge-routing.d.ts +2 -0
- package/dist/data/graph-edge-routing.d.ts.map +1 -1
- package/dist/data/graph-edge-routing.js +9 -3
- package/dist/data/graph-edge-routing.js.map +1 -1
- package/dist/data/graph-group-window.cjs +60 -0
- package/dist/data/graph-group-window.cjs.map +1 -0
- package/dist/data/graph-group-window.d.ts +33 -0
- package/dist/data/graph-group-window.d.ts.map +1 -0
- package/dist/data/graph-group-window.js +60 -0
- package/dist/data/graph-group-window.js.map +1 -0
- package/dist/data/json-view/JsonSource.cjs +84 -0
- package/dist/data/json-view/JsonSource.cjs.map +1 -0
- package/dist/data/json-view/JsonSource.d.ts +7 -0
- package/dist/data/json-view/JsonSource.d.ts.map +1 -0
- package/dist/data/json-view/JsonSource.js +84 -0
- package/dist/data/json-view/JsonSource.js.map +1 -0
- package/dist/data/json-view/parse-prefix.cjs +132 -0
- package/dist/data/json-view/parse-prefix.cjs.map +1 -0
- package/dist/data/json-view/parse-prefix.d.ts +11 -0
- package/dist/data/json-view/parse-prefix.d.ts.map +1 -0
- package/dist/data/json-view/parse-prefix.js +132 -0
- package/dist/data/json-view/parse-prefix.js.map +1 -0
- package/dist/data/json-view/parse-source.cjs +62 -0
- package/dist/data/json-view/parse-source.cjs.map +1 -0
- package/dist/data/json-view/parse-source.d.ts +16 -0
- package/dist/data/json-view/parse-source.d.ts.map +1 -0
- package/dist/data/json-view/parse-source.js +62 -0
- package/dist/data/json-view/parse-source.js.map +1 -0
- package/dist/data/json-view/story-fixtures.d.ts +6 -0
- package/dist/data/json-view/story-fixtures.d.ts.map +1 -0
- package/dist/data/json-view/types.d.ts +14 -0
- package/dist/data/json-view/types.d.ts.map +1 -0
- package/dist/data/runtime/RuntimeBar.cjs +2 -5
- package/dist/data/runtime/RuntimeBar.cjs.map +1 -1
- package/dist/data/runtime/RuntimeBar.d.ts +2 -4
- package/dist/data/runtime/RuntimeBar.d.ts.map +1 -1
- package/dist/data/runtime/RuntimeBar.fields.cjs +2 -2
- package/dist/data/runtime/RuntimeBar.fields.cjs.map +1 -1
- package/dist/data/runtime/RuntimeBar.fields.js +2 -2
- package/dist/data/runtime/RuntimeBar.fields.js.map +1 -1
- package/dist/data/runtime/RuntimeBar.js +2 -5
- package/dist/data/runtime/RuntimeBar.js.map +1 -1
- package/dist/data/runtime/RuntimeBar.selection.cjs +4 -1
- package/dist/data/runtime/RuntimeBar.selection.cjs.map +1 -1
- package/dist/data/runtime/RuntimeBar.selection.d.ts.map +1 -1
- package/dist/data/runtime/RuntimeBar.selection.js +5 -2
- package/dist/data/runtime/RuntimeBar.selection.js.map +1 -1
- package/dist/data/runtime/RuntimeBarActions.cjs +1 -5
- package/dist/data/runtime/RuntimeBarActions.cjs.map +1 -1
- package/dist/data/runtime/RuntimeBarActions.d.ts +1 -1
- package/dist/data/runtime/RuntimeBarActions.d.ts.map +1 -1
- package/dist/data/runtime/RuntimeBarActions.js +1 -5
- package/dist/data/runtime/RuntimeBarActions.js.map +1 -1
- package/dist/data/runtime/RuntimeBarIdentity.cjs +6 -14
- package/dist/data/runtime/RuntimeBarIdentity.cjs.map +1 -1
- package/dist/data/runtime/RuntimeBarIdentity.d.ts +0 -1
- package/dist/data/runtime/RuntimeBarIdentity.d.ts.map +1 -1
- package/dist/data/runtime/RuntimeBarIdentity.js +6 -14
- package/dist/data/runtime/RuntimeBarIdentity.js.map +1 -1
- package/dist/data/runtime/RuntimeBarLayout.cjs +8 -27
- package/dist/data/runtime/RuntimeBarLayout.cjs.map +1 -1
- package/dist/data/runtime/RuntimeBarLayout.d.ts.map +1 -1
- package/dist/data/runtime/RuntimeBarLayout.js +8 -27
- package/dist/data/runtime/RuntimeBarLayout.js.map +1 -1
- package/dist/data/runtime/RuntimeBarSegment.cjs +1 -5
- package/dist/data/runtime/RuntimeBarSegment.cjs.map +1 -1
- package/dist/data/runtime/RuntimeBarSegment.d.ts.map +1 -1
- package/dist/data/runtime/RuntimeBarSegment.js +1 -5
- package/dist/data/runtime/RuntimeBarSegment.js.map +1 -1
- package/dist/data/runtime/use-runtime-bar-overflow.cjs +4 -10
- package/dist/data/runtime/use-runtime-bar-overflow.cjs.map +1 -1
- package/dist/data/runtime/use-runtime-bar-overflow.d.ts +1 -2
- package/dist/data/runtime/use-runtime-bar-overflow.d.ts.map +1 -1
- package/dist/data/runtime/use-runtime-bar-overflow.js +4 -10
- package/dist/data/runtime/use-runtime-bar-overflow.js.map +1 -1
- package/dist/data/test-runner/TestRunner.fixtures.d.ts +0 -20
- package/dist/data/test-runner/TestRunner.fixtures.d.ts.map +1 -1
- package/dist/data/test-runner/TestRunner.large-fixtures.d.ts +22 -0
- package/dist/data/test-runner/TestRunner.large-fixtures.d.ts.map +1 -0
- package/dist/data/use-group-windows.cjs +50 -0
- package/dist/data/use-group-windows.cjs.map +1 -0
- package/dist/data/use-group-windows.d.ts +27 -0
- package/dist/data/use-group-windows.d.ts.map +1 -0
- package/dist/data/use-group-windows.js +50 -0
- package/dist/data/use-group-windows.js.map +1 -0
- package/dist/data/use-pan-zoom.cjs +23 -10
- package/dist/data/use-pan-zoom.cjs.map +1 -1
- package/dist/data/use-pan-zoom.d.ts +16 -2
- package/dist/data/use-pan-zoom.d.ts.map +1 -1
- package/dist/data/use-pan-zoom.js +23 -10
- package/dist/data/use-pan-zoom.js.map +1 -1
- package/dist/data/version-info.cjs +3 -3
- package/dist/data/version-info.js +3 -3
- package/dist/data.cjs +36 -2
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.ts +5 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +35 -1
- package/dist/data.js.map +1 -1
- package/dist/hooks/event-hub.cjs +229 -0
- package/dist/hooks/event-hub.cjs.map +1 -0
- package/dist/hooks/event-hub.d.ts +26 -0
- package/dist/hooks/event-hub.d.ts.map +1 -0
- package/dist/hooks/event-hub.js +229 -0
- package/dist/hooks/event-hub.js.map +1 -0
- package/dist/hooks.cjs +2 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.ts +1 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +40 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +37 -1
- package/dist/index.js.map +1 -1
- package/dist/layout/Workspace.cjs +13 -4
- package/dist/layout/Workspace.cjs.map +1 -1
- package/dist/layout/Workspace.d.ts +7 -2
- package/dist/layout/Workspace.d.ts.map +1 -1
- package/dist/layout/Workspace.js +13 -4
- package/dist/layout/Workspace.js.map +1 -1
- package/dist/lib/permission-strategies.cjs +71 -11
- package/dist/lib/permission-strategies.cjs.map +1 -1
- package/dist/lib/permission-strategies.d.ts +3 -0
- package/dist/lib/permission-strategies.d.ts.map +1 -1
- package/dist/lib/permission-strategies.js +71 -11
- package/dist/lib/permission-strategies.js.map +1 -1
- package/dist/overlay/modalStack.cjs +13 -6
- package/dist/overlay/modalStack.cjs.map +1 -1
- package/dist/overlay/modalStack.d.ts.map +1 -1
- package/dist/overlay/modalStack.js +13 -6
- package/dist/overlay/modalStack.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +5 -3
- package/dist/data/ai/ToolSchemaBrowser.selection-toolbar.cjs +0 -57
- package/dist/data/ai/ToolSchemaBrowser.selection-toolbar.cjs.map +0 -1
- package/dist/data/ai/ToolSchemaBrowser.selection-toolbar.d.ts +0 -7
- package/dist/data/ai/ToolSchemaBrowser.selection-toolbar.d.ts.map +0 -1
- package/dist/data/ai/ToolSchemaBrowser.selection-toolbar.js +0 -57
- package/dist/data/ai/ToolSchemaBrowser.selection-toolbar.js.map +0 -1
- package/dist/data/runtime/RuntimeBar.context.cjs +0 -8
- package/dist/data/runtime/RuntimeBar.context.cjs.map +0 -1
- package/dist/data/runtime/RuntimeBar.context.d.ts +0 -2
- package/dist/data/runtime/RuntimeBar.context.d.ts.map +0 -1
- package/dist/data/runtime/RuntimeBar.context.js +0 -8
- package/dist/data/runtime/RuntimeBar.context.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-diagram-model.cjs","sources":["../../src/data/graph-diagram-model.ts"],"sourcesContent":["import type { KeyboardEvent, ReactNode } from \"react\";\nimport type { BadgeTone } from \"./Badge\";\nimport type { GraphLayoutPosition } from \"./graph-layout\";\n\nexport interface GraphDiagramNode {\n id: string;\n label: ReactNode;\n /** Drawn before the label, on the same line. Give the icon its own `title` so it names itself. */\n icon?: ReactNode;\n detail?: ReactNode;\n badge?: ReactNode;\n tone?: BadgeTone;\n shape?: \"pill\" | \"rect\";\n /** Accessible name for the node, when `label` alone isn't enough. */\n ariaLabel?: string;\n /** Full text for the node, shown as its native tooltip. */\n title?: string;\n /** Column of the node. Required by the \"columns\" layout; may be negative. */\n level?: number;\n /** \"columns\" layout: nodes sharing a group are stacked together and boxed. */\n group?: string;\n /** Dimmed styling, for a node that leads nowhere further: external, unresolved, not expandable. */\n muted?: boolean;\n /** Neighbours not drawn yet. Renders a `+N` control on the node's outer side. */\n expandCount?: number;\n}\n\nexport interface GraphDiagramEdge {\n id: string;\n from: string;\n to: string;\n /** Short text shown in a pill on the edge. */\n label?: string;\n /** Drawn in the pill before the label. An edge with an icon and no label still gets a pill. */\n icon?: ReactNode;\n /** What `icon` says, in words. It precedes the label in the edge's accessible name. */\n iconLabel?: string;\n /** Full text for the edge, shown as its native tooltip. */\n title?: string;\n tone?: BadgeTone;\n dashed?: boolean;\n}\n\nexport interface GraphDiagramGroup {\n id: string;\n label: ReactNode;\n /** Full text for the group, shown as its caption's native tooltip: what a shortened `label` stands for. */\n title?: string;\n}\n\nexport type GraphDiagramLayout = \"ring\" | \"columns\";\n\n/**\n * Whether edges compete for attention or take turns. With focus on, an edge\n * shows its label pill only while it is in focus — selected, hovered, or\n * attached to the selected or hovered node — and while anything is in focus\n * the other edges fade. \"auto\" turns it on past `DENSE_EDGE_COUNT` edges.\n */\nexport type GraphDiagramEdgeFocus = \"auto\" | \"on\" | \"off\";\n\n/** The most edges a diagram draws with every pill showing under `edgeFocus=\"auto\"`. */\nexport const DENSE_EDGE_COUNT = 32;\n\nexport function edgeFocusOn(mode: GraphDiagramEdgeFocus, edgeCount: number): boolean {\n return mode === \"on\" || (mode === \"auto\" && edgeCount > DENSE_EDGE_COUNT);\n}\n\n/** The edges in focus: the named ones, and every edge attached to a named node. */\nexport function focusedEdgeIds(\n edges: readonly { id: string; from: string; to: string }[],\n focus: { nodes: readonly (string | undefined)[]; edges: readonly (string | undefined)[] },\n): Set<string> {\n return new Set(\n edges\n .filter((edge) => focus.edges.includes(edge.id) || focus.nodes.includes(edge.from) || focus.nodes.includes(edge.to))\n .map((edge) => edge.id),\n );\n}\n\n/** Maps a point in layout coordinates to the CSS offsets of the stage it is drawn on. */\nexport type GraphPlacer = (point: GraphLayoutPosition) => { left: number | string; top: number | string };\n\nexport const TONE_TEXT_CLASS: Record<BadgeTone, string> = {\n neutral: \"text-muted-foreground\",\n success: \"text-emerald-600 dark:text-emerald-400\",\n danger: \"text-red-600 dark:text-red-400\",\n warning: \"text-amber-600 dark:text-amber-400\",\n info: \"text-sky-600 dark:text-sky-400\",\n};\n\nexport const TONE_NODE_CLASS: Record<BadgeTone, string> = {\n neutral: \"border-border bg-card text-foreground\",\n success: \"border-emerald-500/50 bg-emerald-500/10 text-foreground\",\n danger: \"border-red-500/50 bg-red-500/10 text-foreground\",\n warning: \"border-amber-500/50 bg-amber-500/10 text-foreground\",\n info: \"border-sky-500/50 bg-sky-500/10 text-foreground\",\n};\n\n/** The node's accessible name: its `ariaLabel`, else a plain-text label, else its id. */\nexport function nodeName(node: GraphDiagramNode): string {\n if (node.ariaLabel !== undefined) return node.ariaLabel;\n return typeof node.label === \"string\" ? node.label : node.id;\n}\n\n/** A selectable edge's accessible name: `<from> to <to>`, then what its pill shows, in words. */\nexport function edgeName(edge: GraphDiagramEdge, names: Record<string, string>): string {\n const ends = `${requireEntry(names, edge.from, \"node name\")} to ${requireEntry(names, edge.to, \"node name\")}`;\n const pill = [edge.iconLabel, edge.label].filter(Boolean).join(\" \");\n return pill === \"\" ? ends : `${ends}: ${pill}`;\n}\n\n// A pill is not measured: its size is estimated from its content, at the mean\n// glyph advance of its 10px medium label plus its padding and border.\nconst LABEL_CHAR_WIDTH = 5.4;\nconst LABEL_CHROME = 14;\n// The pill's icon slot (`text-sm`, so a 1em icon is 14px) and its `gap-1` to the text.\nconst LABEL_ICON_SIZE = 14;\nconst LABEL_ICON_GAP = 4;\n\n/** Estimated px width of an edge's pill. An icon counts as a fixed width, whatever it draws. */\nexport function edgeLabelWidth(edge: GraphDiagramEdge): number {\n const icon = edge.icon == null ? 0 : LABEL_ICON_SIZE + (edge.label ? LABEL_ICON_GAP : 0);\n return (edge.label?.length ?? 0) * LABEL_CHAR_WIDTH + icon + LABEL_CHROME;\n}\n\nexport function requireEntry<T>(entries: Record<string, T>, id: string, what: string): T {\n const entry = entries[id];\n if (entry === undefined) {\n throw new Error(`GraphDiagram: no ${what} computed for \"${id}\"`);\n }\n return entry;\n}\n\n/** Props that report an element as hovered while the pointer is over it or the keyboard focus is inside it. */\nexport function hoverHandlers(id: string, onHover: (id: string | undefined) => void) {\n const enter = () => onHover(id);\n const leave = () => onHover(undefined);\n return { onMouseEnter: enter, onMouseLeave: leave, onFocus: enter, onBlur: leave };\n}\n\n/** Enter and Space activate a focused `role=\"button\"` element, as they do a native button. */\nexport function activateOnKey(event: KeyboardEvent<Element>, activate: () => void): void {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n activate();\n }\n}\n"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"graph-diagram-model.cjs","sources":["../../src/data/graph-diagram-model.ts"],"sourcesContent":["import type { KeyboardEvent, ReactNode } from \"react\";\nimport type { BadgeTone } from \"./Badge\";\nimport type { GraphLayoutPosition } from \"./graph-layout\";\n\nexport interface GraphDiagramNode {\n id: string;\n label: ReactNode;\n /** Drawn before the label, on the same line. Give the icon its own `title` so it names itself. */\n icon?: ReactNode;\n /** Right-aligned content on a record row, such as a column type. */\n aside?: ReactNode;\n /**\n * A small mark at the end of the node's line, such as how it is accessed (see `AccessMark`). It is\n * hidden from assistive technology, so the node's accessible name stays its own: give it a title.\n */\n mark?: ReactNode;\n detail?: ReactNode;\n badge?: ReactNode;\n tone?: BadgeTone;\n shape?: \"pill\" | \"rect\";\n /** Accessible name for the node, when `label` alone isn't enough. */\n ariaLabel?: string;\n /** Full text for the node, shown as its native tooltip. */\n title?: string;\n /** Column of the node. Required by the \"columns\" layout; may be negative. */\n level?: number;\n /** \"columns\" layout: nodes sharing a group are stacked together and boxed. */\n group?: string;\n /**\n * \"columns\" layout: \"compact\" draws the node on one short line at `compactNodeHeight`, its icon and\n * label only, for a member its group box already names the owner of. Defaults to \"regular\".\n */\n size?: \"regular\" | \"compact\";\n /** Dimmed styling, for a node that leads nowhere further: external, unresolved, not expandable. */\n muted?: boolean;\n /** Neighbours not drawn yet. Renders a `+N` control on the node's outer side. */\n expandCount?: number;\n}\n\nexport interface GraphDiagramEdge {\n id: string;\n from: string;\n to: string;\n /** Short text shown in a pill on the edge. */\n label?: string;\n /** Drawn in the pill before the label. An edge with an icon and no label still gets a pill. */\n icon?: ReactNode;\n /** What `icon` says, in words. It precedes the label in the edge's accessible name. */\n iconLabel?: string;\n /** Full text for the edge, shown as its native tooltip. */\n title?: string;\n tone?: BadgeTone;\n dashed?: boolean;\n}\n\nexport interface GraphDiagramGroup {\n id: string;\n label: ReactNode;\n /** A record draws a header and flush compact member rows. Defaults to a box. */\n variant?: \"box\" | \"record\";\n /** Full text for the group, shown as its caption's native tooltip: what a shortened `label` stands for. */\n title?: string;\n /**\n * \"columns\" layout: whether the group is drawn as its header alone. Set (true or false) to make it\n * collapsible: its header then shows a chevron calling `onGroupToggle`. A collapsed group holds one\n * node, the stand-in its edges attach to, which is not drawn: its header stands for it.\n */\n collapsed?: boolean;\n /** Right-aligned content in the group's header, such as what a collapsed group holds. */\n aside?: ReactNode;\n}\n\n/** The group's name in words: its title, else a plain-text label, else its id. */\nexport function groupName(group: GraphDiagramGroup | undefined, id: string): string {\n if (group?.title !== undefined) return group.title;\n return typeof group?.label === \"string\" ? group.label : id;\n}\n\nexport type GraphDiagramLayout = \"ring\" | \"columns\";\n\n/**\n * Whether edges compete for attention or take turns. With focus on, an edge\n * shows its label pill only while it is in focus — selected, hovered, or\n * attached to the selected or hovered node — and while anything is in focus\n * the other edges fade. \"auto\" turns it on past `DENSE_EDGE_COUNT` edges.\n */\nexport type GraphDiagramEdgeFocus = \"auto\" | \"on\" | \"off\";\n\n/** The most edges a diagram draws with every pill showing under `edgeFocus=\"auto\"`. */\nexport const DENSE_EDGE_COUNT = 32;\n\nexport function edgeFocusOn(mode: GraphDiagramEdgeFocus, edgeCount: number): boolean {\n return mode === \"on\" || (mode === \"auto\" && edgeCount > DENSE_EDGE_COUNT);\n}\n\n/** The edges in focus: the named ones, and every edge attached to a named node. */\nexport function focusedEdgeIds(\n edges: readonly { id: string; from: string; to: string }[],\n focus: { nodes: readonly (string | undefined)[]; edges: readonly (string | undefined)[] },\n): Set<string> {\n return new Set(\n edges\n .filter((edge) => focus.edges.includes(edge.id) || focus.nodes.includes(edge.from) || focus.nodes.includes(edge.to))\n .map((edge) => edge.id),\n );\n}\n\n/** Maps a point in layout coordinates to the CSS offsets of the stage it is drawn on. */\nexport type GraphPlacer = (point: GraphLayoutPosition) => { left: number | string; top: number | string };\n\nexport const TONE_TEXT_CLASS: Record<BadgeTone, string> = {\n neutral: \"text-muted-foreground\",\n success: \"text-emerald-600 dark:text-emerald-400\",\n danger: \"text-red-600 dark:text-red-400\",\n warning: \"text-amber-600 dark:text-amber-400\",\n info: \"text-sky-600 dark:text-sky-400\",\n};\n\nexport const TONE_NODE_CLASS: Record<BadgeTone, string> = {\n neutral: \"border-border bg-card text-foreground\",\n success: \"border-emerald-500/50 bg-emerald-500/10 text-foreground\",\n danger: \"border-red-500/50 bg-red-500/10 text-foreground\",\n warning: \"border-amber-500/50 bg-amber-500/10 text-foreground\",\n info: \"border-sky-500/50 bg-sky-500/10 text-foreground\",\n};\n\n/** The node's accessible name: its `ariaLabel`, else a plain-text label, else its id. */\nexport function nodeName(node: GraphDiagramNode): string {\n if (node.ariaLabel !== undefined) return node.ariaLabel;\n return typeof node.label === \"string\" ? node.label : node.id;\n}\n\n/** A selectable edge's accessible name: `<from> to <to>`, then what its pill shows, in words. */\nexport function edgeName(edge: GraphDiagramEdge, names: Record<string, string>): string {\n const ends = `${requireEntry(names, edge.from, \"node name\")} to ${requireEntry(names, edge.to, \"node name\")}`;\n const pill = [edge.iconLabel, edge.label].filter(Boolean).join(\" \");\n return pill === \"\" ? ends : `${ends}: ${pill}`;\n}\n\n// A pill is not measured: its size is estimated from its content, at the mean\n// glyph advance of its 10px medium label plus its padding and border.\nconst LABEL_CHAR_WIDTH = 5.4;\nconst LABEL_CHROME = 14;\n// The pill's icon slot (`text-sm`, so a 1em icon is 14px) and its `gap-1` to the text.\nconst LABEL_ICON_SIZE = 14;\nconst LABEL_ICON_GAP = 4;\n\n/** Estimated px width of an edge's pill. An icon counts as a fixed width, whatever it draws. */\nexport function edgeLabelWidth(edge: GraphDiagramEdge): number {\n const icon = edge.icon == null ? 0 : LABEL_ICON_SIZE + (edge.label ? LABEL_ICON_GAP : 0);\n return (edge.label?.length ?? 0) * LABEL_CHAR_WIDTH + icon + LABEL_CHROME;\n}\n\nexport function requireEntry<T>(entries: Record<string, T>, id: string, what: string): T {\n const entry = entries[id];\n if (entry === undefined) {\n throw new Error(`GraphDiagram: no ${what} computed for \"${id}\"`);\n }\n return entry;\n}\n\n/** Props that report an element as hovered while the pointer is over it or the keyboard focus is inside it. */\nexport function hoverHandlers(id: string, onHover: (id: string | undefined) => void) {\n const enter = () => onHover(id);\n const leave = () => onHover(undefined);\n return { onMouseEnter: enter, onMouseLeave: leave, onFocus: enter, onBlur: leave };\n}\n\n/** Enter and Space activate a focused `role=\"button\"` element, as they do a native button. */\nexport function activateOnKey(event: KeyboardEvent<Element>, activate: () => void): void {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n activate();\n }\n}\n"],"names":[],"mappings":";;AAyEO,SAAS,UAAU,OAAsC,IAAoB;AAClF,OAAI,+BAAO,WAAU,OAAW,QAAO,MAAM;AAC7C,SAAO,QAAO,+BAAO,WAAU,WAAW,MAAM,QAAQ;AAC1D;AAaO,MAAM,mBAAmB;AAEzB,SAAS,YAAY,MAA6B,WAA4B;AACnF,SAAO,SAAS,QAAS,SAAS,UAAU,YAAY;AAC1D;AAGO,SAAS,eACd,OACA,OACa;AACb,SAAO,IAAI;AAAA,IACT,MACG,OAAO,CAAC,SAAS,MAAM,MAAM,SAAS,KAAK,EAAE,KAAK,MAAM,MAAM,SAAS,KAAK,IAAI,KAAK,MAAM,MAAM,SAAS,KAAK,EAAE,CAAC,EAClH,IAAI,CAAC,SAAS,KAAK,EAAE;AAAA,EAAA;AAE5B;AAKO,MAAM,kBAA6C;AAAA,EACxD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AACR;AAEO,MAAM,kBAA6C;AAAA,EACxD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AACR;AAGO,SAAS,SAAS,MAAgC;AACvD,MAAI,KAAK,cAAc,OAAW,QAAO,KAAK;AAC9C,SAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAK;AAC5D;AAGO,SAAS,SAAS,MAAwB,OAAuC;AACtF,QAAM,OAAO,GAAG,aAAa,OAAO,KAAK,MAAM,WAAW,CAAC,OAAO,aAAa,OAAO,KAAK,IAAI,WAAW,CAAC;AAC3G,QAAM,OAAO,CAAC,KAAK,WAAW,KAAK,KAAK,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAClE,SAAO,SAAS,KAAK,OAAO,GAAG,IAAI,KAAK,IAAI;AAC9C;AAIA,MAAM,mBAAmB;AACzB,MAAM,eAAe;AAErB,MAAM,kBAAkB;AACxB,MAAM,iBAAiB;AAGhB,SAAS,eAAe,MAAgC;;AAC7D,QAAM,OAAO,KAAK,QAAQ,OAAO,IAAI,mBAAmB,KAAK,QAAQ,iBAAiB;AACtF,YAAQ,UAAK,UAAL,mBAAY,WAAU,KAAK,mBAAmB,OAAO;AAC/D;AAEO,SAAS,aAAgB,SAA4B,IAAY,MAAiB;AACvF,QAAM,QAAQ,QAAQ,EAAE;AACxB,MAAI,UAAU,QAAW;AACvB,UAAM,IAAI,MAAM,oBAAoB,IAAI,kBAAkB,EAAE,GAAG;AAAA,EACjE;AACA,SAAO;AACT;AAGO,SAAS,cAAc,IAAY,SAA2C;AACnF,QAAM,QAAQ,MAAM,QAAQ,EAAE;AAC9B,QAAM,QAAQ,MAAM,QAAQ,MAAS;AACrC,SAAO,EAAE,cAAc,OAAO,cAAc,OAAO,SAAS,OAAO,QAAQ,MAAA;AAC7E;AAGO,SAAS,cAAc,OAA+B,UAA4B;AACvF,MAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;AAC9C,UAAM,eAAA;AACN,aAAA;AAAA,EACF;AACF;;;;;;;;;;;;;"}
|
|
@@ -6,6 +6,13 @@ export interface GraphDiagramNode {
|
|
|
6
6
|
label: ReactNode;
|
|
7
7
|
/** Drawn before the label, on the same line. Give the icon its own `title` so it names itself. */
|
|
8
8
|
icon?: ReactNode;
|
|
9
|
+
/** Right-aligned content on a record row, such as a column type. */
|
|
10
|
+
aside?: ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* A small mark at the end of the node's line, such as how it is accessed (see `AccessMark`). It is
|
|
13
|
+
* hidden from assistive technology, so the node's accessible name stays its own: give it a title.
|
|
14
|
+
*/
|
|
15
|
+
mark?: ReactNode;
|
|
9
16
|
detail?: ReactNode;
|
|
10
17
|
badge?: ReactNode;
|
|
11
18
|
tone?: BadgeTone;
|
|
@@ -18,6 +25,11 @@ export interface GraphDiagramNode {
|
|
|
18
25
|
level?: number;
|
|
19
26
|
/** "columns" layout: nodes sharing a group are stacked together and boxed. */
|
|
20
27
|
group?: string;
|
|
28
|
+
/**
|
|
29
|
+
* "columns" layout: "compact" draws the node on one short line at `compactNodeHeight`, its icon and
|
|
30
|
+
* label only, for a member its group box already names the owner of. Defaults to "regular".
|
|
31
|
+
*/
|
|
32
|
+
size?: "regular" | "compact";
|
|
21
33
|
/** Dimmed styling, for a node that leads nowhere further: external, unresolved, not expandable. */
|
|
22
34
|
muted?: boolean;
|
|
23
35
|
/** Neighbours not drawn yet. Renders a `+N` control on the node's outer side. */
|
|
@@ -41,9 +53,21 @@ export interface GraphDiagramEdge {
|
|
|
41
53
|
export interface GraphDiagramGroup {
|
|
42
54
|
id: string;
|
|
43
55
|
label: ReactNode;
|
|
56
|
+
/** A record draws a header and flush compact member rows. Defaults to a box. */
|
|
57
|
+
variant?: "box" | "record";
|
|
44
58
|
/** Full text for the group, shown as its caption's native tooltip: what a shortened `label` stands for. */
|
|
45
59
|
title?: string;
|
|
60
|
+
/**
|
|
61
|
+
* "columns" layout: whether the group is drawn as its header alone. Set (true or false) to make it
|
|
62
|
+
* collapsible: its header then shows a chevron calling `onGroupToggle`. A collapsed group holds one
|
|
63
|
+
* node, the stand-in its edges attach to, which is not drawn: its header stands for it.
|
|
64
|
+
*/
|
|
65
|
+
collapsed?: boolean;
|
|
66
|
+
/** Right-aligned content in the group's header, such as what a collapsed group holds. */
|
|
67
|
+
aside?: ReactNode;
|
|
46
68
|
}
|
|
69
|
+
/** The group's name in words: its title, else a plain-text label, else its id. */
|
|
70
|
+
export declare function groupName(group: GraphDiagramGroup | undefined, id: string): string;
|
|
47
71
|
export type GraphDiagramLayout = "ring" | "columns";
|
|
48
72
|
/**
|
|
49
73
|
* Whether edges compete for attention or take turns. With focus on, an edge
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-diagram-model.d.ts","sourceRoot":"","sources":["../../src/data/graph-diagram-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,SAAS,CAAC;IACjB,kGAAkG;IAClG,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mGAAmG;IACnG,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,SAAS,CAAC;IACjB,2GAA2G;IAC3G,KAAK,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"graph-diagram-model.d.ts","sourceRoot":"","sources":["../../src/data/graph-diagram-model.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
function groupName(group, id) {
|
|
2
|
+
if ((group == null ? void 0 : group.title) !== void 0) return group.title;
|
|
3
|
+
return typeof (group == null ? void 0 : group.label) === "string" ? group.label : id;
|
|
4
|
+
}
|
|
1
5
|
const DENSE_EDGE_COUNT = 32;
|
|
2
6
|
function edgeFocusOn(mode, edgeCount) {
|
|
3
7
|
return mode === "on" || mode === "auto" && edgeCount > DENSE_EDGE_COUNT;
|
|
@@ -66,6 +70,7 @@ export {
|
|
|
66
70
|
edgeLabelWidth,
|
|
67
71
|
edgeName,
|
|
68
72
|
focusedEdgeIds,
|
|
73
|
+
groupName,
|
|
69
74
|
hoverHandlers,
|
|
70
75
|
nodeName,
|
|
71
76
|
requireEntry
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-diagram-model.js","sources":["../../src/data/graph-diagram-model.ts"],"sourcesContent":["import type { KeyboardEvent, ReactNode } from \"react\";\nimport type { BadgeTone } from \"./Badge\";\nimport type { GraphLayoutPosition } from \"./graph-layout\";\n\nexport interface GraphDiagramNode {\n id: string;\n label: ReactNode;\n /** Drawn before the label, on the same line. Give the icon its own `title` so it names itself. */\n icon?: ReactNode;\n detail?: ReactNode;\n badge?: ReactNode;\n tone?: BadgeTone;\n shape?: \"pill\" | \"rect\";\n /** Accessible name for the node, when `label` alone isn't enough. */\n ariaLabel?: string;\n /** Full text for the node, shown as its native tooltip. */\n title?: string;\n /** Column of the node. Required by the \"columns\" layout; may be negative. */\n level?: number;\n /** \"columns\" layout: nodes sharing a group are stacked together and boxed. */\n group?: string;\n /** Dimmed styling, for a node that leads nowhere further: external, unresolved, not expandable. */\n muted?: boolean;\n /** Neighbours not drawn yet. Renders a `+N` control on the node's outer side. */\n expandCount?: number;\n}\n\nexport interface GraphDiagramEdge {\n id: string;\n from: string;\n to: string;\n /** Short text shown in a pill on the edge. */\n label?: string;\n /** Drawn in the pill before the label. An edge with an icon and no label still gets a pill. */\n icon?: ReactNode;\n /** What `icon` says, in words. It precedes the label in the edge's accessible name. */\n iconLabel?: string;\n /** Full text for the edge, shown as its native tooltip. */\n title?: string;\n tone?: BadgeTone;\n dashed?: boolean;\n}\n\nexport interface GraphDiagramGroup {\n id: string;\n label: ReactNode;\n /** Full text for the group, shown as its caption's native tooltip: what a shortened `label` stands for. */\n title?: string;\n}\n\nexport type GraphDiagramLayout = \"ring\" | \"columns\";\n\n/**\n * Whether edges compete for attention or take turns. With focus on, an edge\n * shows its label pill only while it is in focus — selected, hovered, or\n * attached to the selected or hovered node — and while anything is in focus\n * the other edges fade. \"auto\" turns it on past `DENSE_EDGE_COUNT` edges.\n */\nexport type GraphDiagramEdgeFocus = \"auto\" | \"on\" | \"off\";\n\n/** The most edges a diagram draws with every pill showing under `edgeFocus=\"auto\"`. */\nexport const DENSE_EDGE_COUNT = 32;\n\nexport function edgeFocusOn(mode: GraphDiagramEdgeFocus, edgeCount: number): boolean {\n return mode === \"on\" || (mode === \"auto\" && edgeCount > DENSE_EDGE_COUNT);\n}\n\n/** The edges in focus: the named ones, and every edge attached to a named node. */\nexport function focusedEdgeIds(\n edges: readonly { id: string; from: string; to: string }[],\n focus: { nodes: readonly (string | undefined)[]; edges: readonly (string | undefined)[] },\n): Set<string> {\n return new Set(\n edges\n .filter((edge) => focus.edges.includes(edge.id) || focus.nodes.includes(edge.from) || focus.nodes.includes(edge.to))\n .map((edge) => edge.id),\n );\n}\n\n/** Maps a point in layout coordinates to the CSS offsets of the stage it is drawn on. */\nexport type GraphPlacer = (point: GraphLayoutPosition) => { left: number | string; top: number | string };\n\nexport const TONE_TEXT_CLASS: Record<BadgeTone, string> = {\n neutral: \"text-muted-foreground\",\n success: \"text-emerald-600 dark:text-emerald-400\",\n danger: \"text-red-600 dark:text-red-400\",\n warning: \"text-amber-600 dark:text-amber-400\",\n info: \"text-sky-600 dark:text-sky-400\",\n};\n\nexport const TONE_NODE_CLASS: Record<BadgeTone, string> = {\n neutral: \"border-border bg-card text-foreground\",\n success: \"border-emerald-500/50 bg-emerald-500/10 text-foreground\",\n danger: \"border-red-500/50 bg-red-500/10 text-foreground\",\n warning: \"border-amber-500/50 bg-amber-500/10 text-foreground\",\n info: \"border-sky-500/50 bg-sky-500/10 text-foreground\",\n};\n\n/** The node's accessible name: its `ariaLabel`, else a plain-text label, else its id. */\nexport function nodeName(node: GraphDiagramNode): string {\n if (node.ariaLabel !== undefined) return node.ariaLabel;\n return typeof node.label === \"string\" ? node.label : node.id;\n}\n\n/** A selectable edge's accessible name: `<from> to <to>`, then what its pill shows, in words. */\nexport function edgeName(edge: GraphDiagramEdge, names: Record<string, string>): string {\n const ends = `${requireEntry(names, edge.from, \"node name\")} to ${requireEntry(names, edge.to, \"node name\")}`;\n const pill = [edge.iconLabel, edge.label].filter(Boolean).join(\" \");\n return pill === \"\" ? ends : `${ends}: ${pill}`;\n}\n\n// A pill is not measured: its size is estimated from its content, at the mean\n// glyph advance of its 10px medium label plus its padding and border.\nconst LABEL_CHAR_WIDTH = 5.4;\nconst LABEL_CHROME = 14;\n// The pill's icon slot (`text-sm`, so a 1em icon is 14px) and its `gap-1` to the text.\nconst LABEL_ICON_SIZE = 14;\nconst LABEL_ICON_GAP = 4;\n\n/** Estimated px width of an edge's pill. An icon counts as a fixed width, whatever it draws. */\nexport function edgeLabelWidth(edge: GraphDiagramEdge): number {\n const icon = edge.icon == null ? 0 : LABEL_ICON_SIZE + (edge.label ? LABEL_ICON_GAP : 0);\n return (edge.label?.length ?? 0) * LABEL_CHAR_WIDTH + icon + LABEL_CHROME;\n}\n\nexport function requireEntry<T>(entries: Record<string, T>, id: string, what: string): T {\n const entry = entries[id];\n if (entry === undefined) {\n throw new Error(`GraphDiagram: no ${what} computed for \"${id}\"`);\n }\n return entry;\n}\n\n/** Props that report an element as hovered while the pointer is over it or the keyboard focus is inside it. */\nexport function hoverHandlers(id: string, onHover: (id: string | undefined) => void) {\n const enter = () => onHover(id);\n const leave = () => onHover(undefined);\n return { onMouseEnter: enter, onMouseLeave: leave, onFocus: enter, onBlur: leave };\n}\n\n/** Enter and Space activate a focused `role=\"button\"` element, as they do a native button. */\nexport function activateOnKey(event: KeyboardEvent<Element>, activate: () => void): void {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n activate();\n }\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"graph-diagram-model.js","sources":["../../src/data/graph-diagram-model.ts"],"sourcesContent":["import type { KeyboardEvent, ReactNode } from \"react\";\nimport type { BadgeTone } from \"./Badge\";\nimport type { GraphLayoutPosition } from \"./graph-layout\";\n\nexport interface GraphDiagramNode {\n id: string;\n label: ReactNode;\n /** Drawn before the label, on the same line. Give the icon its own `title` so it names itself. */\n icon?: ReactNode;\n /** Right-aligned content on a record row, such as a column type. */\n aside?: ReactNode;\n /**\n * A small mark at the end of the node's line, such as how it is accessed (see `AccessMark`). It is\n * hidden from assistive technology, so the node's accessible name stays its own: give it a title.\n */\n mark?: ReactNode;\n detail?: ReactNode;\n badge?: ReactNode;\n tone?: BadgeTone;\n shape?: \"pill\" | \"rect\";\n /** Accessible name for the node, when `label` alone isn't enough. */\n ariaLabel?: string;\n /** Full text for the node, shown as its native tooltip. */\n title?: string;\n /** Column of the node. Required by the \"columns\" layout; may be negative. */\n level?: number;\n /** \"columns\" layout: nodes sharing a group are stacked together and boxed. */\n group?: string;\n /**\n * \"columns\" layout: \"compact\" draws the node on one short line at `compactNodeHeight`, its icon and\n * label only, for a member its group box already names the owner of. Defaults to \"regular\".\n */\n size?: \"regular\" | \"compact\";\n /** Dimmed styling, for a node that leads nowhere further: external, unresolved, not expandable. */\n muted?: boolean;\n /** Neighbours not drawn yet. Renders a `+N` control on the node's outer side. */\n expandCount?: number;\n}\n\nexport interface GraphDiagramEdge {\n id: string;\n from: string;\n to: string;\n /** Short text shown in a pill on the edge. */\n label?: string;\n /** Drawn in the pill before the label. An edge with an icon and no label still gets a pill. */\n icon?: ReactNode;\n /** What `icon` says, in words. It precedes the label in the edge's accessible name. */\n iconLabel?: string;\n /** Full text for the edge, shown as its native tooltip. */\n title?: string;\n tone?: BadgeTone;\n dashed?: boolean;\n}\n\nexport interface GraphDiagramGroup {\n id: string;\n label: ReactNode;\n /** A record draws a header and flush compact member rows. Defaults to a box. */\n variant?: \"box\" | \"record\";\n /** Full text for the group, shown as its caption's native tooltip: what a shortened `label` stands for. */\n title?: string;\n /**\n * \"columns\" layout: whether the group is drawn as its header alone. Set (true or false) to make it\n * collapsible: its header then shows a chevron calling `onGroupToggle`. A collapsed group holds one\n * node, the stand-in its edges attach to, which is not drawn: its header stands for it.\n */\n collapsed?: boolean;\n /** Right-aligned content in the group's header, such as what a collapsed group holds. */\n aside?: ReactNode;\n}\n\n/** The group's name in words: its title, else a plain-text label, else its id. */\nexport function groupName(group: GraphDiagramGroup | undefined, id: string): string {\n if (group?.title !== undefined) return group.title;\n return typeof group?.label === \"string\" ? group.label : id;\n}\n\nexport type GraphDiagramLayout = \"ring\" | \"columns\";\n\n/**\n * Whether edges compete for attention or take turns. With focus on, an edge\n * shows its label pill only while it is in focus — selected, hovered, or\n * attached to the selected or hovered node — and while anything is in focus\n * the other edges fade. \"auto\" turns it on past `DENSE_EDGE_COUNT` edges.\n */\nexport type GraphDiagramEdgeFocus = \"auto\" | \"on\" | \"off\";\n\n/** The most edges a diagram draws with every pill showing under `edgeFocus=\"auto\"`. */\nexport const DENSE_EDGE_COUNT = 32;\n\nexport function edgeFocusOn(mode: GraphDiagramEdgeFocus, edgeCount: number): boolean {\n return mode === \"on\" || (mode === \"auto\" && edgeCount > DENSE_EDGE_COUNT);\n}\n\n/** The edges in focus: the named ones, and every edge attached to a named node. */\nexport function focusedEdgeIds(\n edges: readonly { id: string; from: string; to: string }[],\n focus: { nodes: readonly (string | undefined)[]; edges: readonly (string | undefined)[] },\n): Set<string> {\n return new Set(\n edges\n .filter((edge) => focus.edges.includes(edge.id) || focus.nodes.includes(edge.from) || focus.nodes.includes(edge.to))\n .map((edge) => edge.id),\n );\n}\n\n/** Maps a point in layout coordinates to the CSS offsets of the stage it is drawn on. */\nexport type GraphPlacer = (point: GraphLayoutPosition) => { left: number | string; top: number | string };\n\nexport const TONE_TEXT_CLASS: Record<BadgeTone, string> = {\n neutral: \"text-muted-foreground\",\n success: \"text-emerald-600 dark:text-emerald-400\",\n danger: \"text-red-600 dark:text-red-400\",\n warning: \"text-amber-600 dark:text-amber-400\",\n info: \"text-sky-600 dark:text-sky-400\",\n};\n\nexport const TONE_NODE_CLASS: Record<BadgeTone, string> = {\n neutral: \"border-border bg-card text-foreground\",\n success: \"border-emerald-500/50 bg-emerald-500/10 text-foreground\",\n danger: \"border-red-500/50 bg-red-500/10 text-foreground\",\n warning: \"border-amber-500/50 bg-amber-500/10 text-foreground\",\n info: \"border-sky-500/50 bg-sky-500/10 text-foreground\",\n};\n\n/** The node's accessible name: its `ariaLabel`, else a plain-text label, else its id. */\nexport function nodeName(node: GraphDiagramNode): string {\n if (node.ariaLabel !== undefined) return node.ariaLabel;\n return typeof node.label === \"string\" ? node.label : node.id;\n}\n\n/** A selectable edge's accessible name: `<from> to <to>`, then what its pill shows, in words. */\nexport function edgeName(edge: GraphDiagramEdge, names: Record<string, string>): string {\n const ends = `${requireEntry(names, edge.from, \"node name\")} to ${requireEntry(names, edge.to, \"node name\")}`;\n const pill = [edge.iconLabel, edge.label].filter(Boolean).join(\" \");\n return pill === \"\" ? ends : `${ends}: ${pill}`;\n}\n\n// A pill is not measured: its size is estimated from its content, at the mean\n// glyph advance of its 10px medium label plus its padding and border.\nconst LABEL_CHAR_WIDTH = 5.4;\nconst LABEL_CHROME = 14;\n// The pill's icon slot (`text-sm`, so a 1em icon is 14px) and its `gap-1` to the text.\nconst LABEL_ICON_SIZE = 14;\nconst LABEL_ICON_GAP = 4;\n\n/** Estimated px width of an edge's pill. An icon counts as a fixed width, whatever it draws. */\nexport function edgeLabelWidth(edge: GraphDiagramEdge): number {\n const icon = edge.icon == null ? 0 : LABEL_ICON_SIZE + (edge.label ? LABEL_ICON_GAP : 0);\n return (edge.label?.length ?? 0) * LABEL_CHAR_WIDTH + icon + LABEL_CHROME;\n}\n\nexport function requireEntry<T>(entries: Record<string, T>, id: string, what: string): T {\n const entry = entries[id];\n if (entry === undefined) {\n throw new Error(`GraphDiagram: no ${what} computed for \"${id}\"`);\n }\n return entry;\n}\n\n/** Props that report an element as hovered while the pointer is over it or the keyboard focus is inside it. */\nexport function hoverHandlers(id: string, onHover: (id: string | undefined) => void) {\n const enter = () => onHover(id);\n const leave = () => onHover(undefined);\n return { onMouseEnter: enter, onMouseLeave: leave, onFocus: enter, onBlur: leave };\n}\n\n/** Enter and Space activate a focused `role=\"button\"` element, as they do a native button. */\nexport function activateOnKey(event: KeyboardEvent<Element>, activate: () => void): void {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n activate();\n }\n}\n"],"names":[],"mappings":"AAyEO,SAAS,UAAU,OAAsC,IAAoB;AAClF,OAAI,+BAAO,WAAU,OAAW,QAAO,MAAM;AAC7C,SAAO,QAAO,+BAAO,WAAU,WAAW,MAAM,QAAQ;AAC1D;AAaO,MAAM,mBAAmB;AAEzB,SAAS,YAAY,MAA6B,WAA4B;AACnF,SAAO,SAAS,QAAS,SAAS,UAAU,YAAY;AAC1D;AAGO,SAAS,eACd,OACA,OACa;AACb,SAAO,IAAI;AAAA,IACT,MACG,OAAO,CAAC,SAAS,MAAM,MAAM,SAAS,KAAK,EAAE,KAAK,MAAM,MAAM,SAAS,KAAK,IAAI,KAAK,MAAM,MAAM,SAAS,KAAK,EAAE,CAAC,EAClH,IAAI,CAAC,SAAS,KAAK,EAAE;AAAA,EAAA;AAE5B;AAKO,MAAM,kBAA6C;AAAA,EACxD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AACR;AAEO,MAAM,kBAA6C;AAAA,EACxD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AACR;AAGO,SAAS,SAAS,MAAgC;AACvD,MAAI,KAAK,cAAc,OAAW,QAAO,KAAK;AAC9C,SAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAK;AAC5D;AAGO,SAAS,SAAS,MAAwB,OAAuC;AACtF,QAAM,OAAO,GAAG,aAAa,OAAO,KAAK,MAAM,WAAW,CAAC,OAAO,aAAa,OAAO,KAAK,IAAI,WAAW,CAAC;AAC3G,QAAM,OAAO,CAAC,KAAK,WAAW,KAAK,KAAK,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAClE,SAAO,SAAS,KAAK,OAAO,GAAG,IAAI,KAAK,IAAI;AAC9C;AAIA,MAAM,mBAAmB;AACzB,MAAM,eAAe;AAErB,MAAM,kBAAkB;AACxB,MAAM,iBAAiB;AAGhB,SAAS,eAAe,MAAgC;AA3ExD;AA4EL,QAAM,OAAO,KAAK,QAAQ,OAAO,IAAI,mBAAmB,KAAK,QAAQ,iBAAiB;AACtF,YAAQ,UAAK,UAAL,mBAAY,WAAU,KAAK,mBAAmB,OAAO;AAC/D;AAEO,SAAS,aAAgB,SAA4B,IAAY,MAAiB;AACvF,QAAM,QAAQ,QAAQ,EAAE;AACxB,MAAI,UAAU,QAAW;AACvB,UAAM,IAAI,MAAM,oBAAoB,IAAI,kBAAkB,EAAE,GAAG;AAAA,EACjE;AACA,SAAO;AACT;AAGO,SAAS,cAAc,IAAY,SAA2C;AACnF,QAAM,QAAQ,MAAM,QAAQ,EAAE;AAC9B,QAAM,QAAQ,MAAM,QAAQ,MAAS;AACrC,SAAO,EAAE,cAAc,OAAO,cAAc,OAAO,SAAS,OAAO,QAAQ,MAAA;AAC7E;AAGO,SAAS,cAAc,OAA+B,UAA4B;AACvF,MAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;AAC9C,UAAM,eAAA;AACN,aAAA;AAAA,EACF;AACF;"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
function heightOf(dimensions, id) {
|
|
4
|
+
var _a;
|
|
5
|
+
return ((_a = dimensions.nodeHeights) == null ? void 0 : _a[id]) ?? dimensions.nodeHeight;
|
|
6
|
+
}
|
|
3
7
|
const CURVE_OFFSET = 22;
|
|
4
8
|
const MIN_SIDE_REACH = 36;
|
|
5
9
|
const LOOP_BASE_REACH = 24;
|
|
@@ -136,7 +140,7 @@ function planLoops(edges, positions, nodeWidth) {
|
|
|
136
140
|
function loopRoute(from, to, options) {
|
|
137
141
|
const { side, maxReach } = options.plan;
|
|
138
142
|
const halfW = options.dimensions.nodeWidth / 2;
|
|
139
|
-
const spread = options.self ? options.
|
|
143
|
+
const spread = options.self ? options.loopHeight / 4 : 0;
|
|
140
144
|
const startY = from.y - spread;
|
|
141
145
|
const endY = to.y + spread;
|
|
142
146
|
const reach = Math.min(maxReach, Math.max(MIN_SIDE_REACH, LOOP_BASE_REACH + LOOP_SPAN_RATE * Math.abs(endY - startY))) + options.index * CURVE_OFFSET;
|
|
@@ -154,14 +158,16 @@ function loopRoute(from, to, options) {
|
|
|
154
158
|
}
|
|
155
159
|
function routeSideGroup(group, positions, dimensions, loops, routes) {
|
|
156
160
|
const halfW = dimensions.nodeWidth / 2;
|
|
157
|
-
const
|
|
161
|
+
const first = requireEdge(group, 0);
|
|
162
|
+
const shorter = Math.min(heightOf(dimensions, first.from), heightOf(dimensions, first.to));
|
|
163
|
+
const step = group.length > 1 ? Math.min(CURVE_OFFSET, Math.max(0, shorter - PORT_INSET) / (group.length - 1)) : 0;
|
|
158
164
|
group.forEach((edge, index) => {
|
|
159
165
|
const from = requirePosition(positions, edge.from);
|
|
160
166
|
const to = requirePosition(positions, edge.to);
|
|
161
167
|
if (isLoop(from, to, dimensions.nodeWidth)) {
|
|
162
168
|
const plan = loops.get(from.x);
|
|
163
169
|
if (!plan) throw new Error(`routeEdges: no loop plan for the column of "${edge.from}"`);
|
|
164
|
-
routes[edge.id] = loopRoute(from, to, { self: edge.from === edge.to, index, dimensions, plan });
|
|
170
|
+
routes[edge.id] = loopRoute(from, to, { self: edge.from === edge.to, index, dimensions, plan, loopHeight: heightOf(dimensions, edge.from) });
|
|
165
171
|
return;
|
|
166
172
|
}
|
|
167
173
|
const direction = to.x > from.x ? 1 : -1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-edge-routing.cjs","sources":["../../src/data/graph-edge-routing.ts"],"sourcesContent":["// Edge routing and label placement for GraphDiagram: pure math, no React.\n// Split from graph-layout.ts, which owns the position types and `ringLayout`\n// and re-exports this file's API.\n\nimport type { GraphLayoutPosition } from \"./graph-layout\";\n\nexport interface GraphLayoutEdge {\n id: string;\n from: string;\n to: string;\n}\n\n/** Start, two control points and end of a cubic Bézier. */\nexport type CubicCurve = readonly [GraphLayoutPosition, GraphLayoutPosition, GraphLayoutPosition, GraphLayoutPosition];\n\nexport interface EdgeRoute {\n /** SVG path `d` attribute, clipped to the node rectangle boundaries. */\n path: string;\n /** Midpoint of the rendered path, for placing an edge label. */\n labelX: number;\n labelY: number;\n /**\n * Set on a loop, whose midpoint is its outermost point: \"start\" when a label\n * should begin at the point and run right of it, \"end\" when it should end\n * there and run left. A centred label would reach back over the node.\n */\n labelAnchor?: \"start\" | \"end\";\n /** A side-anchored edge between two columns: its curve, for placing a label elsewhere than its midpoint. */\n curve?: CubicCurve;\n}\n\nexport interface RouteEdgesDimensions {\n nodeWidth: number;\n nodeHeight: number;\n}\n\nexport interface RouteEdgesOptions {\n /** Where edges attach: clipped toward the node centre, or at the left/right side middles. Defaults to \"center\". */\n anchor?: \"center\" | \"side\";\n}\n\nconst CURVE_OFFSET = 22;\nconst MIN_SIDE_REACH = 36;\n/** A loop reaches this far plus a share of the height it spans, so a longer loop arcs outside a shorter one. */\nconst LOOP_BASE_REACH = 24;\nconst LOOP_SPAN_RATE = 0.18;\nconst MAX_LOOP_REACH = 240;\n/** The share of the gap to the next column a loop may bulge into. */\nconst LOOP_GAP_SHARE = 0.6;\n/** A cubic whose two control points share an x peaks at 3/4 of their offset. */\nconst CUBIC_PEAK = 0.75;\n/** Vertical margin kept between fanned side anchors and the node's corners. */\nconst PORT_INSET = 12;\n\nfunction unorderedPairKey(a: string, b: string): string {\n return a < b ? `${a}\\0${b}` : `${b}\\0${a}`;\n}\n\nfunction requirePosition(positions: Record<string, GraphLayoutPosition>, id: string): GraphLayoutPosition {\n const position = positions[id];\n if (!position) {\n throw new Error(`routeEdges: no layout position for node \"${id}\"`);\n }\n return position;\n}\n\nfunction requireEdge(group: readonly GraphLayoutEdge[], index: number): GraphLayoutEdge {\n const edge = group[index];\n if (!edge) {\n throw new Error(`routeEdges: expected an edge at index ${index}`);\n }\n return edge;\n}\n\n/** Point on the rectangle (centred at `center`, half-extents `halfW`/`halfH`) where the ray toward `towards` exits. */\nfunction clipToRect(\n center: GraphLayoutPosition,\n towards: GraphLayoutPosition,\n halfW: number,\n halfH: number,\n): GraphLayoutPosition {\n const dx = towards.x - center.x;\n const dy = towards.y - center.y;\n if (dx === 0 && dy === 0) return { x: center.x, y: center.y };\n\n const scaleX = dx !== 0 ? halfW / Math.abs(dx) : Number.POSITIVE_INFINITY;\n const scaleY = dy !== 0 ? halfH / Math.abs(dy) : Number.POSITIVE_INFINITY;\n const scale = Math.min(scaleX, scaleY);\n\n return { x: center.x + dx * scale, y: center.y + dy * scale };\n}\n\n/** Point at t=0.5 on the quadratic Bézier defined by p0 (start), p1 (control), p2 (end). */\nfunction quadraticMidpoint(\n p0: GraphLayoutPosition,\n p1: GraphLayoutPosition,\n p2: GraphLayoutPosition,\n): GraphLayoutPosition {\n return {\n x: 0.25 * p0.x + 0.5 * p1.x + 0.25 * p2.x,\n y: 0.25 * p0.y + 0.5 * p1.y + 0.25 * p2.y,\n };\n}\n\n/** Edges sharing an unordered node pair, in input order; throws on an edge whose node has no position. */\nfunction groupEdgesByPair(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n): GraphLayoutEdge[][] {\n const groups = new Map<string, GraphLayoutEdge[]>();\n for (const edge of edges) {\n if (!(edge.from in positions)) {\n throw new Error(`routeEdges: edge \"${edge.id}\" references unknown node \"${edge.from}\"`);\n }\n if (!(edge.to in positions)) {\n throw new Error(`routeEdges: edge \"${edge.id}\" references unknown node \"${edge.to}\"`);\n }\n const key = unorderedPairKey(edge.from, edge.to);\n const group = groups.get(key);\n if (group) {\n group.push(edge);\n } else {\n groups.set(key, [edge]);\n }\n }\n return [...groups.values()];\n}\n\nfunction routeCenterGroup(\n group: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n routes: Record<string, EdgeRoute>,\n): void {\n const halfW = dimensions.nodeWidth / 2;\n const halfH = dimensions.nodeHeight / 2;\n const first = requireEdge(group, 0);\n const posA = requirePosition(positions, first.from);\n const posB = requirePosition(positions, first.to);\n const dx = posB.x - posA.x;\n const dy = posB.y - posA.y;\n const len = Math.hypot(dx, dy) || 1;\n const perpX = -dy / len;\n const perpY = dx / len;\n const midX = (posA.x + posB.x) / 2;\n const midY = (posA.y + posB.y) / 2;\n\n group.forEach((edge, index) => {\n const isCurved = group.length > 1;\n const sign = index % 2 === 0 ? 1 : -1;\n const magnitude = Math.floor(index / 2) + 1;\n const offset = isCurved ? sign * magnitude * CURVE_OFFSET : 0;\n\n const control = { x: midX + perpX * offset, y: midY + perpY * offset };\n const fromCenter = requirePosition(positions, edge.from);\n const toCenter = requirePosition(positions, edge.to);\n const start = clipToRect(fromCenter, control, halfW, halfH);\n const end = clipToRect(toCenter, control, halfW, halfH);\n\n const path = isCurved\n ? `M ${start.x} ${start.y} Q ${control.x} ${control.y} ${end.x} ${end.y}`\n : `M ${start.x} ${start.y} L ${end.x} ${end.y}`;\n\n const label = isCurved\n ? quadraticMidpoint(start, control, end)\n : { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };\n\n routes[edge.id] = { path, labelX: label.x, labelY: label.y };\n });\n}\n\n/** The point at `t` (0 at the start, 1 at the end) along a cubic Bézier. */\nexport function pointOnCurve([p0, c1, c2, p3]: CubicCurve, t: number): GraphLayoutPosition {\n const u = 1 - t;\n const weights = [u * u * u, 3 * u * u * t, 3 * u * t * t, t * t * t] as const;\n return {\n x: weights[0] * p0.x + weights[1] * c1.x + weights[2] * c2.x + weights[3] * p3.x,\n y: weights[0] * p0.y + weights[1] * c1.y + weights[2] * c2.y + weights[3] * p3.y,\n };\n}\n\nfunction cubicRoute(curve: CubicCurve): EdgeRoute {\n const [p0, c1, c2, p3] = curve;\n const label = pointOnCurve(curve, 0.5);\n return {\n path: `M ${p0.x} ${p0.y} C ${c1.x} ${c1.y} ${c2.x} ${c2.y} ${p3.x} ${p3.y}`,\n labelX: label.x,\n labelY: label.y,\n };\n}\n\ninterface LoopPlan {\n /** 1 when the column's loops bulge right of it, -1 when left. */\n side: 1 | -1;\n /** The furthest a loop's control points may sit from the column, so it stays clear of the next one. */\n maxReach: number;\n}\n\nfunction isLoop(from: GraphLayoutPosition, to: GraphLayoutPosition, nodeWidth: number): boolean {\n return Math.abs(to.x - from.x) < nodeWidth;\n}\n\n/**\n * For each column (each distinct node x), where its loops go: the side with\n * fewer edges to other columns attached, the right on a tie, and no further\n * than a share of the gap to the next column on that side.\n */\nfunction planLoops(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n nodeWidth: number,\n): Map<number, LoopPlan> {\n const attached = new Map<number, { left: number; right: number }>();\n for (const { x } of Object.values(positions)) attached.set(x, { left: 0, right: 0 });\n const attach = (x: number, side: \"left\" | \"right\") => {\n const tally = attached.get(x);\n if (tally) tally[side] += 1;\n };\n for (const edge of edges) {\n const from = requirePosition(positions, edge.from);\n const to = requirePosition(positions, edge.to);\n if (isLoop(from, to, nodeWidth)) continue;\n const forward = to.x > from.x;\n attach(from.x, forward ? \"right\" : \"left\");\n attach(to.x, forward ? \"left\" : \"right\");\n }\n const columns = [...attached.keys()];\n return new Map(\n columns.map((x) => {\n const tally = attached.get(x) ?? { left: 0, right: 0 };\n const side = tally.left < tally.right ? -1 : 1;\n const gaps = columns.filter((other) => (other - x) * side > 0).map((other) => Math.abs(other - x) - nodeWidth);\n const room = gaps.length === 0 ? Number.POSITIVE_INFINITY : (LOOP_GAP_SHARE * Math.min(...gaps)) / CUBIC_PEAK;\n return [x, { side, maxReach: Math.min(MAX_LOOP_REACH, Math.max(0, room)) }];\n }),\n );\n}\n\n/** Out of one side of a column and back into it: an edge inside one column, or a self-edge. */\nfunction loopRoute(\n from: GraphLayoutPosition,\n to: GraphLayoutPosition,\n options: { self: boolean; index: number; dimensions: RouteEdgesDimensions; plan: LoopPlan },\n): EdgeRoute {\n const { side, maxReach } = options.plan;\n const halfW = options.dimensions.nodeWidth / 2;\n const spread = options.self ? options.dimensions.nodeHeight / 4 : 0;\n const startY = from.y - spread;\n const endY = to.y + spread;\n const reach =\n Math.min(maxReach, Math.max(MIN_SIDE_REACH, LOOP_BASE_REACH + LOOP_SPAN_RATE * Math.abs(endY - startY))) +\n options.index * CURVE_OFFSET;\n const outer = side > 0 ? Math.max(from.x, to.x) : Math.min(from.x, to.x);\n const bulge = outer + side * (halfW + reach);\n return {\n ...cubicRoute([\n { x: from.x + side * halfW, y: startY },\n { x: bulge, y: startY },\n { x: bulge, y: endY },\n { x: to.x + side * halfW, y: endY },\n ]),\n labelAnchor: side > 0 ? \"start\" : \"end\",\n };\n}\n\nfunction routeSideGroup(\n group: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n loops: ReadonlyMap<number, LoopPlan>,\n routes: Record<string, EdgeRoute>,\n): void {\n const halfW = dimensions.nodeWidth / 2;\n const step =\n group.length > 1\n ? Math.min(CURVE_OFFSET, (dimensions.nodeHeight - PORT_INSET) / (group.length - 1))\n : 0;\n\n group.forEach((edge, index) => {\n const from = requirePosition(positions, edge.from);\n const to = requirePosition(positions, edge.to);\n if (isLoop(from, to, dimensions.nodeWidth)) {\n const plan = loops.get(from.x);\n if (!plan) throw new Error(`routeEdges: no loop plan for the column of \"${edge.from}\"`);\n routes[edge.id] = loopRoute(from, to, { self: edge.from === edge.to, index, dimensions, plan });\n return;\n }\n const direction = to.x > from.x ? 1 : -1;\n const shift = (index - (group.length - 1) / 2) * step;\n const start = { x: from.x + direction * halfW, y: from.y + shift };\n const end = { x: to.x - direction * halfW, y: to.y + shift };\n const reach = Math.max(MIN_SIDE_REACH, Math.abs(end.x - start.x) / 2);\n const curve: CubicCurve = [\n start,\n { x: start.x + direction * reach, y: start.y },\n { x: end.x - direction * reach, y: end.y },\n end,\n ];\n routes[edge.id] = { ...cubicRoute(curve), curve };\n });\n}\n\n/**\n * Routes each edge as an SVG path. Edges sharing an unordered node pair — a\n * reciprocal A→B + B→A, or literal duplicates — are always fanned apart so\n * they never overlap.\n *\n * `anchor: \"center\"` (the default) clips each edge to the source/target node\n * rectangles: a lone edge is a straight line, a fanned one a quadratic curve\n * offset from the line between the two node centres.\n *\n * `anchor: \"side\"` is for column layouts: an edge leaves the side of its\n * source facing the target and enters the facing side of the target as a\n * cubic curve. An edge between two nodes of one column, and a self-edge, is a\n * loop: it arcs out of the column and back on the side with fewer edges to\n * other columns, the further the more height it spans, so loops nest.\n *\n * Throws when an edge references a node id absent from `positions`, naming\n * the edge id, rather than silently dropping or mis-routing it.\n */\nexport function routeEdges(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n options: RouteEdgesOptions = {},\n): Record<string, EdgeRoute> {\n const routes: Record<string, EdgeRoute> = {};\n const groups = groupEdgesByPair(edges, positions);\n if (options.anchor !== \"side\") {\n for (const group of groups) routeCenterGroup(group, positions, dimensions, routes);\n return routes;\n }\n const loops = planLoops(edges, positions, dimensions.nodeWidth);\n for (const group of groups) routeSideGroup(group, positions, dimensions, loops, routes);\n return routes;\n}\n\n/** How far the side-anchored loops of `edges` reach past the outermost columns, on each side. */\nexport function loopOverhang(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n): { left: number; right: number } {\n const xs = Object.values(positions).map((position) => position.x);\n const loops = Object.values(routeEdges(edges, positions, dimensions, { anchor: \"side\" })).filter(\n (route) => route.labelAnchor !== undefined,\n );\n if (loops.length === 0) return { left: 0, right: 0 };\n const peaks = loops.map((route) => route.labelX);\n const halfW = dimensions.nodeWidth / 2;\n return {\n left: Math.max(0, Math.min(...xs) - halfW - Math.min(...peaks)),\n right: Math.max(0, Math.max(...peaks) - (Math.max(...xs) + halfW)),\n };\n}\n\nexport interface EdgeLabelBox {\n id: string;\n /** The label's anchor: its centre, its left edge when `anchor` is \"start\", its right edge when \"end\". */\n x: number;\n y: number;\n width: number;\n height: number;\n anchor?: \"start\" | \"end\";\n}\n\nfunction labelLeft(box: EdgeLabelBox): number {\n if (box.anchor === \"start\") return box.x;\n return box.anchor === \"end\" ? box.x - box.width : box.x - box.width / 2;\n}\n\n/**\n * Resolves the y of each edge label so no two labels cover each other: taken\n * top to bottom, a label that would overlap one already placed drops just\n * below it. Labels keep their x, and a label that overlaps nothing keeps its y.\n */\nexport function spreadLabels(boxes: readonly EdgeLabelBox[], gap = 2): Record<string, number> {\n const placed: { left: number; right: number; y: number; height: number }[] = [];\n const ys: Record<string, number> = {};\n for (const box of [...boxes].sort((a, b) => a.y - b.y)) {\n const left = labelLeft(box);\n const right = left + box.width;\n let y = box.y;\n for (let moved = true; moved; ) {\n moved = false;\n for (const other of placed) {\n const clearance = (box.height + other.height) / 2 + gap;\n if (left < other.right && other.left < right && Math.abs(y - other.y) < clearance) {\n y = other.y + clearance;\n moved = true;\n }\n }\n }\n placed.push({ left, right, y, height: box.height });\n ys[box.id] = y;\n }\n return ys;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"graph-edge-routing.cjs","sources":["../../src/data/graph-edge-routing.ts"],"sourcesContent":["// Edge routing and label placement for GraphDiagram: pure math, no React.\n// Split from graph-layout.ts, which owns the position types and `ringLayout`\n// and re-exports this file's API.\n\nimport type { GraphLayoutPosition } from \"./graph-layout\";\n\nexport interface GraphLayoutEdge {\n id: string;\n from: string;\n to: string;\n}\n\n/** Start, two control points and end of a cubic Bézier. */\nexport type CubicCurve = readonly [GraphLayoutPosition, GraphLayoutPosition, GraphLayoutPosition, GraphLayoutPosition];\n\nexport interface EdgeRoute {\n /** SVG path `d` attribute, clipped to the node rectangle boundaries. */\n path: string;\n /** Midpoint of the rendered path, for placing an edge label. */\n labelX: number;\n labelY: number;\n /**\n * Set on a loop, whose midpoint is its outermost point: \"start\" when a label\n * should begin at the point and run right of it, \"end\" when it should end\n * there and run left. A centred label would reach back over the node.\n */\n labelAnchor?: \"start\" | \"end\";\n /** A side-anchored edge between two columns: its curve, for placing a label elsewhere than its midpoint. */\n curve?: CubicCurve;\n}\n\nexport interface RouteEdgesDimensions {\n nodeWidth: number;\n nodeHeight: number;\n /** Heights of the nodes not drawn at `nodeHeight`, by id. */\n nodeHeights?: Readonly<Record<string, number>>;\n}\n\nfunction heightOf(dimensions: RouteEdgesDimensions, id: string): number {\n return dimensions.nodeHeights?.[id] ?? dimensions.nodeHeight;\n}\n\nexport interface RouteEdgesOptions {\n /** Where edges attach: clipped toward the node centre, or at the left/right side middles. Defaults to \"center\". */\n anchor?: \"center\" | \"side\";\n}\n\nconst CURVE_OFFSET = 22;\nconst MIN_SIDE_REACH = 36;\n/** A loop reaches this far plus a share of the height it spans, so a longer loop arcs outside a shorter one. */\nconst LOOP_BASE_REACH = 24;\nconst LOOP_SPAN_RATE = 0.18;\nconst MAX_LOOP_REACH = 240;\n/** The share of the gap to the next column a loop may bulge into. */\nconst LOOP_GAP_SHARE = 0.6;\n/** A cubic whose two control points share an x peaks at 3/4 of their offset. */\nconst CUBIC_PEAK = 0.75;\n/** Vertical margin kept between fanned side anchors and the node's corners. */\nconst PORT_INSET = 12;\n\nfunction unorderedPairKey(a: string, b: string): string {\n return a < b ? `${a}\\0${b}` : `${b}\\0${a}`;\n}\n\nfunction requirePosition(positions: Record<string, GraphLayoutPosition>, id: string): GraphLayoutPosition {\n const position = positions[id];\n if (!position) {\n throw new Error(`routeEdges: no layout position for node \"${id}\"`);\n }\n return position;\n}\n\nfunction requireEdge(group: readonly GraphLayoutEdge[], index: number): GraphLayoutEdge {\n const edge = group[index];\n if (!edge) {\n throw new Error(`routeEdges: expected an edge at index ${index}`);\n }\n return edge;\n}\n\n/** Point on the rectangle (centred at `center`, half-extents `halfW`/`halfH`) where the ray toward `towards` exits. */\nfunction clipToRect(\n center: GraphLayoutPosition,\n towards: GraphLayoutPosition,\n halfW: number,\n halfH: number,\n): GraphLayoutPosition {\n const dx = towards.x - center.x;\n const dy = towards.y - center.y;\n if (dx === 0 && dy === 0) return { x: center.x, y: center.y };\n\n const scaleX = dx !== 0 ? halfW / Math.abs(dx) : Number.POSITIVE_INFINITY;\n const scaleY = dy !== 0 ? halfH / Math.abs(dy) : Number.POSITIVE_INFINITY;\n const scale = Math.min(scaleX, scaleY);\n\n return { x: center.x + dx * scale, y: center.y + dy * scale };\n}\n\n/** Point at t=0.5 on the quadratic Bézier defined by p0 (start), p1 (control), p2 (end). */\nfunction quadraticMidpoint(\n p0: GraphLayoutPosition,\n p1: GraphLayoutPosition,\n p2: GraphLayoutPosition,\n): GraphLayoutPosition {\n return {\n x: 0.25 * p0.x + 0.5 * p1.x + 0.25 * p2.x,\n y: 0.25 * p0.y + 0.5 * p1.y + 0.25 * p2.y,\n };\n}\n\n/** Edges sharing an unordered node pair, in input order; throws on an edge whose node has no position. */\nfunction groupEdgesByPair(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n): GraphLayoutEdge[][] {\n const groups = new Map<string, GraphLayoutEdge[]>();\n for (const edge of edges) {\n if (!(edge.from in positions)) {\n throw new Error(`routeEdges: edge \"${edge.id}\" references unknown node \"${edge.from}\"`);\n }\n if (!(edge.to in positions)) {\n throw new Error(`routeEdges: edge \"${edge.id}\" references unknown node \"${edge.to}\"`);\n }\n const key = unorderedPairKey(edge.from, edge.to);\n const group = groups.get(key);\n if (group) {\n group.push(edge);\n } else {\n groups.set(key, [edge]);\n }\n }\n return [...groups.values()];\n}\n\nfunction routeCenterGroup(\n group: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n routes: Record<string, EdgeRoute>,\n): void {\n const halfW = dimensions.nodeWidth / 2;\n const halfH = dimensions.nodeHeight / 2;\n const first = requireEdge(group, 0);\n const posA = requirePosition(positions, first.from);\n const posB = requirePosition(positions, first.to);\n const dx = posB.x - posA.x;\n const dy = posB.y - posA.y;\n const len = Math.hypot(dx, dy) || 1;\n const perpX = -dy / len;\n const perpY = dx / len;\n const midX = (posA.x + posB.x) / 2;\n const midY = (posA.y + posB.y) / 2;\n\n group.forEach((edge, index) => {\n const isCurved = group.length > 1;\n const sign = index % 2 === 0 ? 1 : -1;\n const magnitude = Math.floor(index / 2) + 1;\n const offset = isCurved ? sign * magnitude * CURVE_OFFSET : 0;\n\n const control = { x: midX + perpX * offset, y: midY + perpY * offset };\n const fromCenter = requirePosition(positions, edge.from);\n const toCenter = requirePosition(positions, edge.to);\n const start = clipToRect(fromCenter, control, halfW, halfH);\n const end = clipToRect(toCenter, control, halfW, halfH);\n\n const path = isCurved\n ? `M ${start.x} ${start.y} Q ${control.x} ${control.y} ${end.x} ${end.y}`\n : `M ${start.x} ${start.y} L ${end.x} ${end.y}`;\n\n const label = isCurved\n ? quadraticMidpoint(start, control, end)\n : { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };\n\n routes[edge.id] = { path, labelX: label.x, labelY: label.y };\n });\n}\n\n/** The point at `t` (0 at the start, 1 at the end) along a cubic Bézier. */\nexport function pointOnCurve([p0, c1, c2, p3]: CubicCurve, t: number): GraphLayoutPosition {\n const u = 1 - t;\n const weights = [u * u * u, 3 * u * u * t, 3 * u * t * t, t * t * t] as const;\n return {\n x: weights[0] * p0.x + weights[1] * c1.x + weights[2] * c2.x + weights[3] * p3.x,\n y: weights[0] * p0.y + weights[1] * c1.y + weights[2] * c2.y + weights[3] * p3.y,\n };\n}\n\nfunction cubicRoute(curve: CubicCurve): EdgeRoute {\n const [p0, c1, c2, p3] = curve;\n const label = pointOnCurve(curve, 0.5);\n return {\n path: `M ${p0.x} ${p0.y} C ${c1.x} ${c1.y} ${c2.x} ${c2.y} ${p3.x} ${p3.y}`,\n labelX: label.x,\n labelY: label.y,\n };\n}\n\ninterface LoopPlan {\n /** 1 when the column's loops bulge right of it, -1 when left. */\n side: 1 | -1;\n /** The furthest a loop's control points may sit from the column, so it stays clear of the next one. */\n maxReach: number;\n}\n\nfunction isLoop(from: GraphLayoutPosition, to: GraphLayoutPosition, nodeWidth: number): boolean {\n return Math.abs(to.x - from.x) < nodeWidth;\n}\n\n/**\n * For each column (each distinct node x), where its loops go: the side with\n * fewer edges to other columns attached, the right on a tie, and no further\n * than a share of the gap to the next column on that side.\n */\nfunction planLoops(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n nodeWidth: number,\n): Map<number, LoopPlan> {\n const attached = new Map<number, { left: number; right: number }>();\n for (const { x } of Object.values(positions)) attached.set(x, { left: 0, right: 0 });\n const attach = (x: number, side: \"left\" | \"right\") => {\n const tally = attached.get(x);\n if (tally) tally[side] += 1;\n };\n for (const edge of edges) {\n const from = requirePosition(positions, edge.from);\n const to = requirePosition(positions, edge.to);\n if (isLoop(from, to, nodeWidth)) continue;\n const forward = to.x > from.x;\n attach(from.x, forward ? \"right\" : \"left\");\n attach(to.x, forward ? \"left\" : \"right\");\n }\n const columns = [...attached.keys()];\n return new Map(\n columns.map((x) => {\n const tally = attached.get(x) ?? { left: 0, right: 0 };\n const side = tally.left < tally.right ? -1 : 1;\n const gaps = columns.filter((other) => (other - x) * side > 0).map((other) => Math.abs(other - x) - nodeWidth);\n const room = gaps.length === 0 ? Number.POSITIVE_INFINITY : (LOOP_GAP_SHARE * Math.min(...gaps)) / CUBIC_PEAK;\n return [x, { side, maxReach: Math.min(MAX_LOOP_REACH, Math.max(0, room)) }];\n }),\n );\n}\n\n/** Out of one side of a column and back into it: an edge inside one column, or a self-edge. */\nfunction loopRoute(\n from: GraphLayoutPosition,\n to: GraphLayoutPosition,\n options: { self: boolean; index: number; dimensions: RouteEdgesDimensions; plan: LoopPlan; loopHeight: number },\n): EdgeRoute {\n const { side, maxReach } = options.plan;\n const halfW = options.dimensions.nodeWidth / 2;\n const spread = options.self ? options.loopHeight / 4 : 0;\n const startY = from.y - spread;\n const endY = to.y + spread;\n const reach =\n Math.min(maxReach, Math.max(MIN_SIDE_REACH, LOOP_BASE_REACH + LOOP_SPAN_RATE * Math.abs(endY - startY))) +\n options.index * CURVE_OFFSET;\n const outer = side > 0 ? Math.max(from.x, to.x) : Math.min(from.x, to.x);\n const bulge = outer + side * (halfW + reach);\n return {\n ...cubicRoute([\n { x: from.x + side * halfW, y: startY },\n { x: bulge, y: startY },\n { x: bulge, y: endY },\n { x: to.x + side * halfW, y: endY },\n ]),\n labelAnchor: side > 0 ? \"start\" : \"end\",\n };\n}\n\nfunction routeSideGroup(\n group: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n loops: ReadonlyMap<number, LoopPlan>,\n routes: Record<string, EdgeRoute>,\n): void {\n const halfW = dimensions.nodeWidth / 2;\n const first = requireEdge(group, 0);\n const shorter = Math.min(heightOf(dimensions, first.from), heightOf(dimensions, first.to));\n const step = group.length > 1 ? Math.min(CURVE_OFFSET, Math.max(0, shorter - PORT_INSET) / (group.length - 1)) : 0;\n\n group.forEach((edge, index) => {\n const from = requirePosition(positions, edge.from);\n const to = requirePosition(positions, edge.to);\n if (isLoop(from, to, dimensions.nodeWidth)) {\n const plan = loops.get(from.x);\n if (!plan) throw new Error(`routeEdges: no loop plan for the column of \"${edge.from}\"`);\n routes[edge.id] = loopRoute(from, to, { self: edge.from === edge.to, index, dimensions, plan, loopHeight: heightOf(dimensions, edge.from) });\n return;\n }\n const direction = to.x > from.x ? 1 : -1;\n const shift = (index - (group.length - 1) / 2) * step;\n const start = { x: from.x + direction * halfW, y: from.y + shift };\n const end = { x: to.x - direction * halfW, y: to.y + shift };\n const reach = Math.max(MIN_SIDE_REACH, Math.abs(end.x - start.x) / 2);\n const curve: CubicCurve = [\n start,\n { x: start.x + direction * reach, y: start.y },\n { x: end.x - direction * reach, y: end.y },\n end,\n ];\n routes[edge.id] = { ...cubicRoute(curve), curve };\n });\n}\n\n/**\n * Routes each edge as an SVG path. Edges sharing an unordered node pair — a\n * reciprocal A→B + B→A, or literal duplicates — are always fanned apart so\n * they never overlap.\n *\n * `anchor: \"center\"` (the default) clips each edge to the source/target node\n * rectangles: a lone edge is a straight line, a fanned one a quadratic curve\n * offset from the line between the two node centres.\n *\n * `anchor: \"side\"` is for column layouts: an edge leaves the side of its\n * source facing the target and enters the facing side of the target as a\n * cubic curve. An edge between two nodes of one column, and a self-edge, is a\n * loop: it arcs out of the column and back on the side with fewer edges to\n * other columns, the further the more height it spans, so loops nest.\n *\n * Throws when an edge references a node id absent from `positions`, naming\n * the edge id, rather than silently dropping or mis-routing it.\n */\nexport function routeEdges(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n options: RouteEdgesOptions = {},\n): Record<string, EdgeRoute> {\n const routes: Record<string, EdgeRoute> = {};\n const groups = groupEdgesByPair(edges, positions);\n if (options.anchor !== \"side\") {\n for (const group of groups) routeCenterGroup(group, positions, dimensions, routes);\n return routes;\n }\n const loops = planLoops(edges, positions, dimensions.nodeWidth);\n for (const group of groups) routeSideGroup(group, positions, dimensions, loops, routes);\n return routes;\n}\n\n/** How far the side-anchored loops of `edges` reach past the outermost columns, on each side. */\nexport function loopOverhang(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n): { left: number; right: number } {\n const xs = Object.values(positions).map((position) => position.x);\n const loops = Object.values(routeEdges(edges, positions, dimensions, { anchor: \"side\" })).filter(\n (route) => route.labelAnchor !== undefined,\n );\n if (loops.length === 0) return { left: 0, right: 0 };\n const peaks = loops.map((route) => route.labelX);\n const halfW = dimensions.nodeWidth / 2;\n return {\n left: Math.max(0, Math.min(...xs) - halfW - Math.min(...peaks)),\n right: Math.max(0, Math.max(...peaks) - (Math.max(...xs) + halfW)),\n };\n}\n\nexport interface EdgeLabelBox {\n id: string;\n /** The label's anchor: its centre, its left edge when `anchor` is \"start\", its right edge when \"end\". */\n x: number;\n y: number;\n width: number;\n height: number;\n anchor?: \"start\" | \"end\";\n}\n\nfunction labelLeft(box: EdgeLabelBox): number {\n if (box.anchor === \"start\") return box.x;\n return box.anchor === \"end\" ? box.x - box.width : box.x - box.width / 2;\n}\n\n/**\n * Resolves the y of each edge label so no two labels cover each other: taken\n * top to bottom, a label that would overlap one already placed drops just\n * below it. Labels keep their x, and a label that overlaps nothing keeps its y.\n */\nexport function spreadLabels(boxes: readonly EdgeLabelBox[], gap = 2): Record<string, number> {\n const placed: { left: number; right: number; y: number; height: number }[] = [];\n const ys: Record<string, number> = {};\n for (const box of [...boxes].sort((a, b) => a.y - b.y)) {\n const left = labelLeft(box);\n const right = left + box.width;\n let y = box.y;\n for (let moved = true; moved; ) {\n moved = false;\n for (const other of placed) {\n const clearance = (box.height + other.height) / 2 + gap;\n if (left < other.right && other.left < right && Math.abs(y - other.y) < clearance) {\n y = other.y + clearance;\n moved = true;\n }\n }\n }\n placed.push({ left, right, y, height: box.height });\n ys[box.id] = y;\n }\n return ys;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -24,6 +24,8 @@ export interface EdgeRoute {
|
|
|
24
24
|
export interface RouteEdgesDimensions {
|
|
25
25
|
nodeWidth: number;
|
|
26
26
|
nodeHeight: number;
|
|
27
|
+
/** Heights of the nodes not drawn at `nodeHeight`, by id. */
|
|
28
|
+
nodeHeights?: Readonly<Record<string, number>>;
|
|
27
29
|
}
|
|
28
30
|
export interface RouteEdgesOptions {
|
|
29
31
|
/** Where edges attach: clipped toward the node centre, or at the left/right side middles. Defaults to "center". */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-edge-routing.d.ts","sourceRoot":"","sources":["../../src/data/graph-edge-routing.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG,SAAS,CAAC,mBAAmB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,mBAAmB,CAAC,CAAC;AAEvH,MAAM,WAAW,SAAS;IACxB,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAC9B,4GAA4G;IAC5G,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"graph-edge-routing.d.ts","sourceRoot":"","sources":["../../src/data/graph-edge-routing.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG,SAAS,CAAC,mBAAmB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,mBAAmB,CAAC,CAAC;AAEvH,MAAM,WAAW,SAAS;IACxB,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAC9B,4GAA4G;IAC5G,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAChD;AAMD,MAAM,WAAW,iBAAiB;IAChC,mHAAmH;IACnH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CAC5B;AAoID,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAOzF;AA0HD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,SAAS,eAAe,EAAE,EACjC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC9C,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE,iBAAsB,GAC9B,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAU3B;AAED,iGAAiG;AACjG,wBAAgB,YAAY,CAC1B,KAAK,EAAE,SAAS,eAAe,EAAE,EACjC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC9C,UAAU,EAAE,oBAAoB,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAYjC;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,yGAAyG;IACzG,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;CAC1B;AAOD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,EAAE,GAAG,SAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAqB5F"}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
function heightOf(dimensions, id) {
|
|
2
|
+
var _a;
|
|
3
|
+
return ((_a = dimensions.nodeHeights) == null ? void 0 : _a[id]) ?? dimensions.nodeHeight;
|
|
4
|
+
}
|
|
1
5
|
const CURVE_OFFSET = 22;
|
|
2
6
|
const MIN_SIDE_REACH = 36;
|
|
3
7
|
const LOOP_BASE_REACH = 24;
|
|
@@ -134,7 +138,7 @@ function planLoops(edges, positions, nodeWidth) {
|
|
|
134
138
|
function loopRoute(from, to, options) {
|
|
135
139
|
const { side, maxReach } = options.plan;
|
|
136
140
|
const halfW = options.dimensions.nodeWidth / 2;
|
|
137
|
-
const spread = options.self ? options.
|
|
141
|
+
const spread = options.self ? options.loopHeight / 4 : 0;
|
|
138
142
|
const startY = from.y - spread;
|
|
139
143
|
const endY = to.y + spread;
|
|
140
144
|
const reach = Math.min(maxReach, Math.max(MIN_SIDE_REACH, LOOP_BASE_REACH + LOOP_SPAN_RATE * Math.abs(endY - startY))) + options.index * CURVE_OFFSET;
|
|
@@ -152,14 +156,16 @@ function loopRoute(from, to, options) {
|
|
|
152
156
|
}
|
|
153
157
|
function routeSideGroup(group, positions, dimensions, loops, routes) {
|
|
154
158
|
const halfW = dimensions.nodeWidth / 2;
|
|
155
|
-
const
|
|
159
|
+
const first = requireEdge(group, 0);
|
|
160
|
+
const shorter = Math.min(heightOf(dimensions, first.from), heightOf(dimensions, first.to));
|
|
161
|
+
const step = group.length > 1 ? Math.min(CURVE_OFFSET, Math.max(0, shorter - PORT_INSET) / (group.length - 1)) : 0;
|
|
156
162
|
group.forEach((edge, index) => {
|
|
157
163
|
const from = requirePosition(positions, edge.from);
|
|
158
164
|
const to = requirePosition(positions, edge.to);
|
|
159
165
|
if (isLoop(from, to, dimensions.nodeWidth)) {
|
|
160
166
|
const plan = loops.get(from.x);
|
|
161
167
|
if (!plan) throw new Error(`routeEdges: no loop plan for the column of "${edge.from}"`);
|
|
162
|
-
routes[edge.id] = loopRoute(from, to, { self: edge.from === edge.to, index, dimensions, plan });
|
|
168
|
+
routes[edge.id] = loopRoute(from, to, { self: edge.from === edge.to, index, dimensions, plan, loopHeight: heightOf(dimensions, edge.from) });
|
|
163
169
|
return;
|
|
164
170
|
}
|
|
165
171
|
const direction = to.x > from.x ? 1 : -1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-edge-routing.js","sources":["../../src/data/graph-edge-routing.ts"],"sourcesContent":["// Edge routing and label placement for GraphDiagram: pure math, no React.\n// Split from graph-layout.ts, which owns the position types and `ringLayout`\n// and re-exports this file's API.\n\nimport type { GraphLayoutPosition } from \"./graph-layout\";\n\nexport interface GraphLayoutEdge {\n id: string;\n from: string;\n to: string;\n}\n\n/** Start, two control points and end of a cubic Bézier. */\nexport type CubicCurve = readonly [GraphLayoutPosition, GraphLayoutPosition, GraphLayoutPosition, GraphLayoutPosition];\n\nexport interface EdgeRoute {\n /** SVG path `d` attribute, clipped to the node rectangle boundaries. */\n path: string;\n /** Midpoint of the rendered path, for placing an edge label. */\n labelX: number;\n labelY: number;\n /**\n * Set on a loop, whose midpoint is its outermost point: \"start\" when a label\n * should begin at the point and run right of it, \"end\" when it should end\n * there and run left. A centred label would reach back over the node.\n */\n labelAnchor?: \"start\" | \"end\";\n /** A side-anchored edge between two columns: its curve, for placing a label elsewhere than its midpoint. */\n curve?: CubicCurve;\n}\n\nexport interface RouteEdgesDimensions {\n nodeWidth: number;\n nodeHeight: number;\n}\n\nexport interface RouteEdgesOptions {\n /** Where edges attach: clipped toward the node centre, or at the left/right side middles. Defaults to \"center\". */\n anchor?: \"center\" | \"side\";\n}\n\nconst CURVE_OFFSET = 22;\nconst MIN_SIDE_REACH = 36;\n/** A loop reaches this far plus a share of the height it spans, so a longer loop arcs outside a shorter one. */\nconst LOOP_BASE_REACH = 24;\nconst LOOP_SPAN_RATE = 0.18;\nconst MAX_LOOP_REACH = 240;\n/** The share of the gap to the next column a loop may bulge into. */\nconst LOOP_GAP_SHARE = 0.6;\n/** A cubic whose two control points share an x peaks at 3/4 of their offset. */\nconst CUBIC_PEAK = 0.75;\n/** Vertical margin kept between fanned side anchors and the node's corners. */\nconst PORT_INSET = 12;\n\nfunction unorderedPairKey(a: string, b: string): string {\n return a < b ? `${a}\\0${b}` : `${b}\\0${a}`;\n}\n\nfunction requirePosition(positions: Record<string, GraphLayoutPosition>, id: string): GraphLayoutPosition {\n const position = positions[id];\n if (!position) {\n throw new Error(`routeEdges: no layout position for node \"${id}\"`);\n }\n return position;\n}\n\nfunction requireEdge(group: readonly GraphLayoutEdge[], index: number): GraphLayoutEdge {\n const edge = group[index];\n if (!edge) {\n throw new Error(`routeEdges: expected an edge at index ${index}`);\n }\n return edge;\n}\n\n/** Point on the rectangle (centred at `center`, half-extents `halfW`/`halfH`) where the ray toward `towards` exits. */\nfunction clipToRect(\n center: GraphLayoutPosition,\n towards: GraphLayoutPosition,\n halfW: number,\n halfH: number,\n): GraphLayoutPosition {\n const dx = towards.x - center.x;\n const dy = towards.y - center.y;\n if (dx === 0 && dy === 0) return { x: center.x, y: center.y };\n\n const scaleX = dx !== 0 ? halfW / Math.abs(dx) : Number.POSITIVE_INFINITY;\n const scaleY = dy !== 0 ? halfH / Math.abs(dy) : Number.POSITIVE_INFINITY;\n const scale = Math.min(scaleX, scaleY);\n\n return { x: center.x + dx * scale, y: center.y + dy * scale };\n}\n\n/** Point at t=0.5 on the quadratic Bézier defined by p0 (start), p1 (control), p2 (end). */\nfunction quadraticMidpoint(\n p0: GraphLayoutPosition,\n p1: GraphLayoutPosition,\n p2: GraphLayoutPosition,\n): GraphLayoutPosition {\n return {\n x: 0.25 * p0.x + 0.5 * p1.x + 0.25 * p2.x,\n y: 0.25 * p0.y + 0.5 * p1.y + 0.25 * p2.y,\n };\n}\n\n/** Edges sharing an unordered node pair, in input order; throws on an edge whose node has no position. */\nfunction groupEdgesByPair(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n): GraphLayoutEdge[][] {\n const groups = new Map<string, GraphLayoutEdge[]>();\n for (const edge of edges) {\n if (!(edge.from in positions)) {\n throw new Error(`routeEdges: edge \"${edge.id}\" references unknown node \"${edge.from}\"`);\n }\n if (!(edge.to in positions)) {\n throw new Error(`routeEdges: edge \"${edge.id}\" references unknown node \"${edge.to}\"`);\n }\n const key = unorderedPairKey(edge.from, edge.to);\n const group = groups.get(key);\n if (group) {\n group.push(edge);\n } else {\n groups.set(key, [edge]);\n }\n }\n return [...groups.values()];\n}\n\nfunction routeCenterGroup(\n group: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n routes: Record<string, EdgeRoute>,\n): void {\n const halfW = dimensions.nodeWidth / 2;\n const halfH = dimensions.nodeHeight / 2;\n const first = requireEdge(group, 0);\n const posA = requirePosition(positions, first.from);\n const posB = requirePosition(positions, first.to);\n const dx = posB.x - posA.x;\n const dy = posB.y - posA.y;\n const len = Math.hypot(dx, dy) || 1;\n const perpX = -dy / len;\n const perpY = dx / len;\n const midX = (posA.x + posB.x) / 2;\n const midY = (posA.y + posB.y) / 2;\n\n group.forEach((edge, index) => {\n const isCurved = group.length > 1;\n const sign = index % 2 === 0 ? 1 : -1;\n const magnitude = Math.floor(index / 2) + 1;\n const offset = isCurved ? sign * magnitude * CURVE_OFFSET : 0;\n\n const control = { x: midX + perpX * offset, y: midY + perpY * offset };\n const fromCenter = requirePosition(positions, edge.from);\n const toCenter = requirePosition(positions, edge.to);\n const start = clipToRect(fromCenter, control, halfW, halfH);\n const end = clipToRect(toCenter, control, halfW, halfH);\n\n const path = isCurved\n ? `M ${start.x} ${start.y} Q ${control.x} ${control.y} ${end.x} ${end.y}`\n : `M ${start.x} ${start.y} L ${end.x} ${end.y}`;\n\n const label = isCurved\n ? quadraticMidpoint(start, control, end)\n : { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };\n\n routes[edge.id] = { path, labelX: label.x, labelY: label.y };\n });\n}\n\n/** The point at `t` (0 at the start, 1 at the end) along a cubic Bézier. */\nexport function pointOnCurve([p0, c1, c2, p3]: CubicCurve, t: number): GraphLayoutPosition {\n const u = 1 - t;\n const weights = [u * u * u, 3 * u * u * t, 3 * u * t * t, t * t * t] as const;\n return {\n x: weights[0] * p0.x + weights[1] * c1.x + weights[2] * c2.x + weights[3] * p3.x,\n y: weights[0] * p0.y + weights[1] * c1.y + weights[2] * c2.y + weights[3] * p3.y,\n };\n}\n\nfunction cubicRoute(curve: CubicCurve): EdgeRoute {\n const [p0, c1, c2, p3] = curve;\n const label = pointOnCurve(curve, 0.5);\n return {\n path: `M ${p0.x} ${p0.y} C ${c1.x} ${c1.y} ${c2.x} ${c2.y} ${p3.x} ${p3.y}`,\n labelX: label.x,\n labelY: label.y,\n };\n}\n\ninterface LoopPlan {\n /** 1 when the column's loops bulge right of it, -1 when left. */\n side: 1 | -1;\n /** The furthest a loop's control points may sit from the column, so it stays clear of the next one. */\n maxReach: number;\n}\n\nfunction isLoop(from: GraphLayoutPosition, to: GraphLayoutPosition, nodeWidth: number): boolean {\n return Math.abs(to.x - from.x) < nodeWidth;\n}\n\n/**\n * For each column (each distinct node x), where its loops go: the side with\n * fewer edges to other columns attached, the right on a tie, and no further\n * than a share of the gap to the next column on that side.\n */\nfunction planLoops(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n nodeWidth: number,\n): Map<number, LoopPlan> {\n const attached = new Map<number, { left: number; right: number }>();\n for (const { x } of Object.values(positions)) attached.set(x, { left: 0, right: 0 });\n const attach = (x: number, side: \"left\" | \"right\") => {\n const tally = attached.get(x);\n if (tally) tally[side] += 1;\n };\n for (const edge of edges) {\n const from = requirePosition(positions, edge.from);\n const to = requirePosition(positions, edge.to);\n if (isLoop(from, to, nodeWidth)) continue;\n const forward = to.x > from.x;\n attach(from.x, forward ? \"right\" : \"left\");\n attach(to.x, forward ? \"left\" : \"right\");\n }\n const columns = [...attached.keys()];\n return new Map(\n columns.map((x) => {\n const tally = attached.get(x) ?? { left: 0, right: 0 };\n const side = tally.left < tally.right ? -1 : 1;\n const gaps = columns.filter((other) => (other - x) * side > 0).map((other) => Math.abs(other - x) - nodeWidth);\n const room = gaps.length === 0 ? Number.POSITIVE_INFINITY : (LOOP_GAP_SHARE * Math.min(...gaps)) / CUBIC_PEAK;\n return [x, { side, maxReach: Math.min(MAX_LOOP_REACH, Math.max(0, room)) }];\n }),\n );\n}\n\n/** Out of one side of a column and back into it: an edge inside one column, or a self-edge. */\nfunction loopRoute(\n from: GraphLayoutPosition,\n to: GraphLayoutPosition,\n options: { self: boolean; index: number; dimensions: RouteEdgesDimensions; plan: LoopPlan },\n): EdgeRoute {\n const { side, maxReach } = options.plan;\n const halfW = options.dimensions.nodeWidth / 2;\n const spread = options.self ? options.dimensions.nodeHeight / 4 : 0;\n const startY = from.y - spread;\n const endY = to.y + spread;\n const reach =\n Math.min(maxReach, Math.max(MIN_SIDE_REACH, LOOP_BASE_REACH + LOOP_SPAN_RATE * Math.abs(endY - startY))) +\n options.index * CURVE_OFFSET;\n const outer = side > 0 ? Math.max(from.x, to.x) : Math.min(from.x, to.x);\n const bulge = outer + side * (halfW + reach);\n return {\n ...cubicRoute([\n { x: from.x + side * halfW, y: startY },\n { x: bulge, y: startY },\n { x: bulge, y: endY },\n { x: to.x + side * halfW, y: endY },\n ]),\n labelAnchor: side > 0 ? \"start\" : \"end\",\n };\n}\n\nfunction routeSideGroup(\n group: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n loops: ReadonlyMap<number, LoopPlan>,\n routes: Record<string, EdgeRoute>,\n): void {\n const halfW = dimensions.nodeWidth / 2;\n const step =\n group.length > 1\n ? Math.min(CURVE_OFFSET, (dimensions.nodeHeight - PORT_INSET) / (group.length - 1))\n : 0;\n\n group.forEach((edge, index) => {\n const from = requirePosition(positions, edge.from);\n const to = requirePosition(positions, edge.to);\n if (isLoop(from, to, dimensions.nodeWidth)) {\n const plan = loops.get(from.x);\n if (!plan) throw new Error(`routeEdges: no loop plan for the column of \"${edge.from}\"`);\n routes[edge.id] = loopRoute(from, to, { self: edge.from === edge.to, index, dimensions, plan });\n return;\n }\n const direction = to.x > from.x ? 1 : -1;\n const shift = (index - (group.length - 1) / 2) * step;\n const start = { x: from.x + direction * halfW, y: from.y + shift };\n const end = { x: to.x - direction * halfW, y: to.y + shift };\n const reach = Math.max(MIN_SIDE_REACH, Math.abs(end.x - start.x) / 2);\n const curve: CubicCurve = [\n start,\n { x: start.x + direction * reach, y: start.y },\n { x: end.x - direction * reach, y: end.y },\n end,\n ];\n routes[edge.id] = { ...cubicRoute(curve), curve };\n });\n}\n\n/**\n * Routes each edge as an SVG path. Edges sharing an unordered node pair — a\n * reciprocal A→B + B→A, or literal duplicates — are always fanned apart so\n * they never overlap.\n *\n * `anchor: \"center\"` (the default) clips each edge to the source/target node\n * rectangles: a lone edge is a straight line, a fanned one a quadratic curve\n * offset from the line between the two node centres.\n *\n * `anchor: \"side\"` is for column layouts: an edge leaves the side of its\n * source facing the target and enters the facing side of the target as a\n * cubic curve. An edge between two nodes of one column, and a self-edge, is a\n * loop: it arcs out of the column and back on the side with fewer edges to\n * other columns, the further the more height it spans, so loops nest.\n *\n * Throws when an edge references a node id absent from `positions`, naming\n * the edge id, rather than silently dropping or mis-routing it.\n */\nexport function routeEdges(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n options: RouteEdgesOptions = {},\n): Record<string, EdgeRoute> {\n const routes: Record<string, EdgeRoute> = {};\n const groups = groupEdgesByPair(edges, positions);\n if (options.anchor !== \"side\") {\n for (const group of groups) routeCenterGroup(group, positions, dimensions, routes);\n return routes;\n }\n const loops = planLoops(edges, positions, dimensions.nodeWidth);\n for (const group of groups) routeSideGroup(group, positions, dimensions, loops, routes);\n return routes;\n}\n\n/** How far the side-anchored loops of `edges` reach past the outermost columns, on each side. */\nexport function loopOverhang(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n): { left: number; right: number } {\n const xs = Object.values(positions).map((position) => position.x);\n const loops = Object.values(routeEdges(edges, positions, dimensions, { anchor: \"side\" })).filter(\n (route) => route.labelAnchor !== undefined,\n );\n if (loops.length === 0) return { left: 0, right: 0 };\n const peaks = loops.map((route) => route.labelX);\n const halfW = dimensions.nodeWidth / 2;\n return {\n left: Math.max(0, Math.min(...xs) - halfW - Math.min(...peaks)),\n right: Math.max(0, Math.max(...peaks) - (Math.max(...xs) + halfW)),\n };\n}\n\nexport interface EdgeLabelBox {\n id: string;\n /** The label's anchor: its centre, its left edge when `anchor` is \"start\", its right edge when \"end\". */\n x: number;\n y: number;\n width: number;\n height: number;\n anchor?: \"start\" | \"end\";\n}\n\nfunction labelLeft(box: EdgeLabelBox): number {\n if (box.anchor === \"start\") return box.x;\n return box.anchor === \"end\" ? box.x - box.width : box.x - box.width / 2;\n}\n\n/**\n * Resolves the y of each edge label so no two labels cover each other: taken\n * top to bottom, a label that would overlap one already placed drops just\n * below it. Labels keep their x, and a label that overlaps nothing keeps its y.\n */\nexport function spreadLabels(boxes: readonly EdgeLabelBox[], gap = 2): Record<string, number> {\n const placed: { left: number; right: number; y: number; height: number }[] = [];\n const ys: Record<string, number> = {};\n for (const box of [...boxes].sort((a, b) => a.y - b.y)) {\n const left = labelLeft(box);\n const right = left + box.width;\n let y = box.y;\n for (let moved = true; moved; ) {\n moved = false;\n for (const other of placed) {\n const clearance = (box.height + other.height) / 2 + gap;\n if (left < other.right && other.left < right && Math.abs(y - other.y) < clearance) {\n y = other.y + clearance;\n moved = true;\n }\n }\n }\n placed.push({ left, right, y, height: box.height });\n ys[box.id] = y;\n }\n return ys;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"graph-edge-routing.js","sources":["../../src/data/graph-edge-routing.ts"],"sourcesContent":["// Edge routing and label placement for GraphDiagram: pure math, no React.\n// Split from graph-layout.ts, which owns the position types and `ringLayout`\n// and re-exports this file's API.\n\nimport type { GraphLayoutPosition } from \"./graph-layout\";\n\nexport interface GraphLayoutEdge {\n id: string;\n from: string;\n to: string;\n}\n\n/** Start, two control points and end of a cubic Bézier. */\nexport type CubicCurve = readonly [GraphLayoutPosition, GraphLayoutPosition, GraphLayoutPosition, GraphLayoutPosition];\n\nexport interface EdgeRoute {\n /** SVG path `d` attribute, clipped to the node rectangle boundaries. */\n path: string;\n /** Midpoint of the rendered path, for placing an edge label. */\n labelX: number;\n labelY: number;\n /**\n * Set on a loop, whose midpoint is its outermost point: \"start\" when a label\n * should begin at the point and run right of it, \"end\" when it should end\n * there and run left. A centred label would reach back over the node.\n */\n labelAnchor?: \"start\" | \"end\";\n /** A side-anchored edge between two columns: its curve, for placing a label elsewhere than its midpoint. */\n curve?: CubicCurve;\n}\n\nexport interface RouteEdgesDimensions {\n nodeWidth: number;\n nodeHeight: number;\n /** Heights of the nodes not drawn at `nodeHeight`, by id. */\n nodeHeights?: Readonly<Record<string, number>>;\n}\n\nfunction heightOf(dimensions: RouteEdgesDimensions, id: string): number {\n return dimensions.nodeHeights?.[id] ?? dimensions.nodeHeight;\n}\n\nexport interface RouteEdgesOptions {\n /** Where edges attach: clipped toward the node centre, or at the left/right side middles. Defaults to \"center\". */\n anchor?: \"center\" | \"side\";\n}\n\nconst CURVE_OFFSET = 22;\nconst MIN_SIDE_REACH = 36;\n/** A loop reaches this far plus a share of the height it spans, so a longer loop arcs outside a shorter one. */\nconst LOOP_BASE_REACH = 24;\nconst LOOP_SPAN_RATE = 0.18;\nconst MAX_LOOP_REACH = 240;\n/** The share of the gap to the next column a loop may bulge into. */\nconst LOOP_GAP_SHARE = 0.6;\n/** A cubic whose two control points share an x peaks at 3/4 of their offset. */\nconst CUBIC_PEAK = 0.75;\n/** Vertical margin kept between fanned side anchors and the node's corners. */\nconst PORT_INSET = 12;\n\nfunction unorderedPairKey(a: string, b: string): string {\n return a < b ? `${a}\\0${b}` : `${b}\\0${a}`;\n}\n\nfunction requirePosition(positions: Record<string, GraphLayoutPosition>, id: string): GraphLayoutPosition {\n const position = positions[id];\n if (!position) {\n throw new Error(`routeEdges: no layout position for node \"${id}\"`);\n }\n return position;\n}\n\nfunction requireEdge(group: readonly GraphLayoutEdge[], index: number): GraphLayoutEdge {\n const edge = group[index];\n if (!edge) {\n throw new Error(`routeEdges: expected an edge at index ${index}`);\n }\n return edge;\n}\n\n/** Point on the rectangle (centred at `center`, half-extents `halfW`/`halfH`) where the ray toward `towards` exits. */\nfunction clipToRect(\n center: GraphLayoutPosition,\n towards: GraphLayoutPosition,\n halfW: number,\n halfH: number,\n): GraphLayoutPosition {\n const dx = towards.x - center.x;\n const dy = towards.y - center.y;\n if (dx === 0 && dy === 0) return { x: center.x, y: center.y };\n\n const scaleX = dx !== 0 ? halfW / Math.abs(dx) : Number.POSITIVE_INFINITY;\n const scaleY = dy !== 0 ? halfH / Math.abs(dy) : Number.POSITIVE_INFINITY;\n const scale = Math.min(scaleX, scaleY);\n\n return { x: center.x + dx * scale, y: center.y + dy * scale };\n}\n\n/** Point at t=0.5 on the quadratic Bézier defined by p0 (start), p1 (control), p2 (end). */\nfunction quadraticMidpoint(\n p0: GraphLayoutPosition,\n p1: GraphLayoutPosition,\n p2: GraphLayoutPosition,\n): GraphLayoutPosition {\n return {\n x: 0.25 * p0.x + 0.5 * p1.x + 0.25 * p2.x,\n y: 0.25 * p0.y + 0.5 * p1.y + 0.25 * p2.y,\n };\n}\n\n/** Edges sharing an unordered node pair, in input order; throws on an edge whose node has no position. */\nfunction groupEdgesByPair(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n): GraphLayoutEdge[][] {\n const groups = new Map<string, GraphLayoutEdge[]>();\n for (const edge of edges) {\n if (!(edge.from in positions)) {\n throw new Error(`routeEdges: edge \"${edge.id}\" references unknown node \"${edge.from}\"`);\n }\n if (!(edge.to in positions)) {\n throw new Error(`routeEdges: edge \"${edge.id}\" references unknown node \"${edge.to}\"`);\n }\n const key = unorderedPairKey(edge.from, edge.to);\n const group = groups.get(key);\n if (group) {\n group.push(edge);\n } else {\n groups.set(key, [edge]);\n }\n }\n return [...groups.values()];\n}\n\nfunction routeCenterGroup(\n group: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n routes: Record<string, EdgeRoute>,\n): void {\n const halfW = dimensions.nodeWidth / 2;\n const halfH = dimensions.nodeHeight / 2;\n const first = requireEdge(group, 0);\n const posA = requirePosition(positions, first.from);\n const posB = requirePosition(positions, first.to);\n const dx = posB.x - posA.x;\n const dy = posB.y - posA.y;\n const len = Math.hypot(dx, dy) || 1;\n const perpX = -dy / len;\n const perpY = dx / len;\n const midX = (posA.x + posB.x) / 2;\n const midY = (posA.y + posB.y) / 2;\n\n group.forEach((edge, index) => {\n const isCurved = group.length > 1;\n const sign = index % 2 === 0 ? 1 : -1;\n const magnitude = Math.floor(index / 2) + 1;\n const offset = isCurved ? sign * magnitude * CURVE_OFFSET : 0;\n\n const control = { x: midX + perpX * offset, y: midY + perpY * offset };\n const fromCenter = requirePosition(positions, edge.from);\n const toCenter = requirePosition(positions, edge.to);\n const start = clipToRect(fromCenter, control, halfW, halfH);\n const end = clipToRect(toCenter, control, halfW, halfH);\n\n const path = isCurved\n ? `M ${start.x} ${start.y} Q ${control.x} ${control.y} ${end.x} ${end.y}`\n : `M ${start.x} ${start.y} L ${end.x} ${end.y}`;\n\n const label = isCurved\n ? quadraticMidpoint(start, control, end)\n : { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };\n\n routes[edge.id] = { path, labelX: label.x, labelY: label.y };\n });\n}\n\n/** The point at `t` (0 at the start, 1 at the end) along a cubic Bézier. */\nexport function pointOnCurve([p0, c1, c2, p3]: CubicCurve, t: number): GraphLayoutPosition {\n const u = 1 - t;\n const weights = [u * u * u, 3 * u * u * t, 3 * u * t * t, t * t * t] as const;\n return {\n x: weights[0] * p0.x + weights[1] * c1.x + weights[2] * c2.x + weights[3] * p3.x,\n y: weights[0] * p0.y + weights[1] * c1.y + weights[2] * c2.y + weights[3] * p3.y,\n };\n}\n\nfunction cubicRoute(curve: CubicCurve): EdgeRoute {\n const [p0, c1, c2, p3] = curve;\n const label = pointOnCurve(curve, 0.5);\n return {\n path: `M ${p0.x} ${p0.y} C ${c1.x} ${c1.y} ${c2.x} ${c2.y} ${p3.x} ${p3.y}`,\n labelX: label.x,\n labelY: label.y,\n };\n}\n\ninterface LoopPlan {\n /** 1 when the column's loops bulge right of it, -1 when left. */\n side: 1 | -1;\n /** The furthest a loop's control points may sit from the column, so it stays clear of the next one. */\n maxReach: number;\n}\n\nfunction isLoop(from: GraphLayoutPosition, to: GraphLayoutPosition, nodeWidth: number): boolean {\n return Math.abs(to.x - from.x) < nodeWidth;\n}\n\n/**\n * For each column (each distinct node x), where its loops go: the side with\n * fewer edges to other columns attached, the right on a tie, and no further\n * than a share of the gap to the next column on that side.\n */\nfunction planLoops(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n nodeWidth: number,\n): Map<number, LoopPlan> {\n const attached = new Map<number, { left: number; right: number }>();\n for (const { x } of Object.values(positions)) attached.set(x, { left: 0, right: 0 });\n const attach = (x: number, side: \"left\" | \"right\") => {\n const tally = attached.get(x);\n if (tally) tally[side] += 1;\n };\n for (const edge of edges) {\n const from = requirePosition(positions, edge.from);\n const to = requirePosition(positions, edge.to);\n if (isLoop(from, to, nodeWidth)) continue;\n const forward = to.x > from.x;\n attach(from.x, forward ? \"right\" : \"left\");\n attach(to.x, forward ? \"left\" : \"right\");\n }\n const columns = [...attached.keys()];\n return new Map(\n columns.map((x) => {\n const tally = attached.get(x) ?? { left: 0, right: 0 };\n const side = tally.left < tally.right ? -1 : 1;\n const gaps = columns.filter((other) => (other - x) * side > 0).map((other) => Math.abs(other - x) - nodeWidth);\n const room = gaps.length === 0 ? Number.POSITIVE_INFINITY : (LOOP_GAP_SHARE * Math.min(...gaps)) / CUBIC_PEAK;\n return [x, { side, maxReach: Math.min(MAX_LOOP_REACH, Math.max(0, room)) }];\n }),\n );\n}\n\n/** Out of one side of a column and back into it: an edge inside one column, or a self-edge. */\nfunction loopRoute(\n from: GraphLayoutPosition,\n to: GraphLayoutPosition,\n options: { self: boolean; index: number; dimensions: RouteEdgesDimensions; plan: LoopPlan; loopHeight: number },\n): EdgeRoute {\n const { side, maxReach } = options.plan;\n const halfW = options.dimensions.nodeWidth / 2;\n const spread = options.self ? options.loopHeight / 4 : 0;\n const startY = from.y - spread;\n const endY = to.y + spread;\n const reach =\n Math.min(maxReach, Math.max(MIN_SIDE_REACH, LOOP_BASE_REACH + LOOP_SPAN_RATE * Math.abs(endY - startY))) +\n options.index * CURVE_OFFSET;\n const outer = side > 0 ? Math.max(from.x, to.x) : Math.min(from.x, to.x);\n const bulge = outer + side * (halfW + reach);\n return {\n ...cubicRoute([\n { x: from.x + side * halfW, y: startY },\n { x: bulge, y: startY },\n { x: bulge, y: endY },\n { x: to.x + side * halfW, y: endY },\n ]),\n labelAnchor: side > 0 ? \"start\" : \"end\",\n };\n}\n\nfunction routeSideGroup(\n group: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n loops: ReadonlyMap<number, LoopPlan>,\n routes: Record<string, EdgeRoute>,\n): void {\n const halfW = dimensions.nodeWidth / 2;\n const first = requireEdge(group, 0);\n const shorter = Math.min(heightOf(dimensions, first.from), heightOf(dimensions, first.to));\n const step = group.length > 1 ? Math.min(CURVE_OFFSET, Math.max(0, shorter - PORT_INSET) / (group.length - 1)) : 0;\n\n group.forEach((edge, index) => {\n const from = requirePosition(positions, edge.from);\n const to = requirePosition(positions, edge.to);\n if (isLoop(from, to, dimensions.nodeWidth)) {\n const plan = loops.get(from.x);\n if (!plan) throw new Error(`routeEdges: no loop plan for the column of \"${edge.from}\"`);\n routes[edge.id] = loopRoute(from, to, { self: edge.from === edge.to, index, dimensions, plan, loopHeight: heightOf(dimensions, edge.from) });\n return;\n }\n const direction = to.x > from.x ? 1 : -1;\n const shift = (index - (group.length - 1) / 2) * step;\n const start = { x: from.x + direction * halfW, y: from.y + shift };\n const end = { x: to.x - direction * halfW, y: to.y + shift };\n const reach = Math.max(MIN_SIDE_REACH, Math.abs(end.x - start.x) / 2);\n const curve: CubicCurve = [\n start,\n { x: start.x + direction * reach, y: start.y },\n { x: end.x - direction * reach, y: end.y },\n end,\n ];\n routes[edge.id] = { ...cubicRoute(curve), curve };\n });\n}\n\n/**\n * Routes each edge as an SVG path. Edges sharing an unordered node pair — a\n * reciprocal A→B + B→A, or literal duplicates — are always fanned apart so\n * they never overlap.\n *\n * `anchor: \"center\"` (the default) clips each edge to the source/target node\n * rectangles: a lone edge is a straight line, a fanned one a quadratic curve\n * offset from the line between the two node centres.\n *\n * `anchor: \"side\"` is for column layouts: an edge leaves the side of its\n * source facing the target and enters the facing side of the target as a\n * cubic curve. An edge between two nodes of one column, and a self-edge, is a\n * loop: it arcs out of the column and back on the side with fewer edges to\n * other columns, the further the more height it spans, so loops nest.\n *\n * Throws when an edge references a node id absent from `positions`, naming\n * the edge id, rather than silently dropping or mis-routing it.\n */\nexport function routeEdges(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n options: RouteEdgesOptions = {},\n): Record<string, EdgeRoute> {\n const routes: Record<string, EdgeRoute> = {};\n const groups = groupEdgesByPair(edges, positions);\n if (options.anchor !== \"side\") {\n for (const group of groups) routeCenterGroup(group, positions, dimensions, routes);\n return routes;\n }\n const loops = planLoops(edges, positions, dimensions.nodeWidth);\n for (const group of groups) routeSideGroup(group, positions, dimensions, loops, routes);\n return routes;\n}\n\n/** How far the side-anchored loops of `edges` reach past the outermost columns, on each side. */\nexport function loopOverhang(\n edges: readonly GraphLayoutEdge[],\n positions: Record<string, GraphLayoutPosition>,\n dimensions: RouteEdgesDimensions,\n): { left: number; right: number } {\n const xs = Object.values(positions).map((position) => position.x);\n const loops = Object.values(routeEdges(edges, positions, dimensions, { anchor: \"side\" })).filter(\n (route) => route.labelAnchor !== undefined,\n );\n if (loops.length === 0) return { left: 0, right: 0 };\n const peaks = loops.map((route) => route.labelX);\n const halfW = dimensions.nodeWidth / 2;\n return {\n left: Math.max(0, Math.min(...xs) - halfW - Math.min(...peaks)),\n right: Math.max(0, Math.max(...peaks) - (Math.max(...xs) + halfW)),\n };\n}\n\nexport interface EdgeLabelBox {\n id: string;\n /** The label's anchor: its centre, its left edge when `anchor` is \"start\", its right edge when \"end\". */\n x: number;\n y: number;\n width: number;\n height: number;\n anchor?: \"start\" | \"end\";\n}\n\nfunction labelLeft(box: EdgeLabelBox): number {\n if (box.anchor === \"start\") return box.x;\n return box.anchor === \"end\" ? box.x - box.width : box.x - box.width / 2;\n}\n\n/**\n * Resolves the y of each edge label so no two labels cover each other: taken\n * top to bottom, a label that would overlap one already placed drops just\n * below it. Labels keep their x, and a label that overlaps nothing keeps its y.\n */\nexport function spreadLabels(boxes: readonly EdgeLabelBox[], gap = 2): Record<string, number> {\n const placed: { left: number; right: number; y: number; height: number }[] = [];\n const ys: Record<string, number> = {};\n for (const box of [...boxes].sort((a, b) => a.y - b.y)) {\n const left = labelLeft(box);\n const right = left + box.width;\n let y = box.y;\n for (let moved = true; moved; ) {\n moved = false;\n for (const other of placed) {\n const clearance = (box.height + other.height) / 2 + gap;\n if (left < other.right && other.left < right && Math.abs(y - other.y) < clearance) {\n y = other.y + clearance;\n moved = true;\n }\n }\n }\n placed.push({ left, right, y, height: box.height });\n ys[box.id] = y;\n }\n return ys;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
function clampWindowStart(memberWindow, start) {
|
|
4
|
+
return Math.min(Math.max(0, memberWindow.members.length - memberWindow.rows), Math.max(0, Math.round(start)));
|
|
5
|
+
}
|
|
6
|
+
function revealWindowStart(memberWindow, start, id) {
|
|
7
|
+
const index = memberWindow.members.indexOf(id);
|
|
8
|
+
const current = clampWindowStart(memberWindow, start);
|
|
9
|
+
if (index < 0) return current;
|
|
10
|
+
if (index < current) return index;
|
|
11
|
+
return index >= current + memberWindow.rows ? clampWindowStart(memberWindow, index - memberWindow.rows + 1) : current;
|
|
12
|
+
}
|
|
13
|
+
function revealMember(boxes, starts, id) {
|
|
14
|
+
if (id === void 0) return starts;
|
|
15
|
+
const box = boxes.find((candidate) => {
|
|
16
|
+
var _a;
|
|
17
|
+
return (_a = candidate.window) == null ? void 0 : _a.members.includes(id);
|
|
18
|
+
});
|
|
19
|
+
if (!(box == null ? void 0 : box.window)) return starts;
|
|
20
|
+
const current = starts[box.id] ?? 0;
|
|
21
|
+
const next = revealWindowStart(box.window, current, id);
|
|
22
|
+
return next === clampWindowStart(box.window, current) ? starts : { ...starts, [box.id]: next };
|
|
23
|
+
}
|
|
24
|
+
function windowedPositions(positions, boxes, starts) {
|
|
25
|
+
const moved = { ...positions };
|
|
26
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
27
|
+
for (const box of boxes) {
|
|
28
|
+
const memberWindow = box.window;
|
|
29
|
+
if (!memberWindow) continue;
|
|
30
|
+
const start = clampWindowStart(memberWindow, starts[box.id] ?? 0);
|
|
31
|
+
const shift = memberWindow.tops[start] ?? 0;
|
|
32
|
+
memberWindow.members.forEach((id, index) => {
|
|
33
|
+
const at = positions[id];
|
|
34
|
+
if (!at) throw new Error(`GraphDiagram: windowed member "${id}" of "${box.id}" has no layout position`);
|
|
35
|
+
if (index < start || index >= start + memberWindow.rows) {
|
|
36
|
+
hidden.add(id);
|
|
37
|
+
moved[id] = { x: at.x, y: index < start ? memberWindow.top : memberWindow.top + memberWindow.height };
|
|
38
|
+
} else {
|
|
39
|
+
moved[id] = { x: at.x, y: at.y - shift };
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return { positions: moved, hidden };
|
|
44
|
+
}
|
|
45
|
+
function windowRows(memberWindow, start) {
|
|
46
|
+
const first = clampWindowStart(memberWindow, start);
|
|
47
|
+
return { first: first + 1, last: Math.min(memberWindow.members.length, first + memberWindow.rows), total: memberWindow.members.length };
|
|
48
|
+
}
|
|
49
|
+
function wheelRows(carry, deltaPx, rowPx) {
|
|
50
|
+
const total = carry + deltaPx;
|
|
51
|
+
const rows = Math.trunc(total / rowPx);
|
|
52
|
+
return { rows, carry: total - rows * rowPx };
|
|
53
|
+
}
|
|
54
|
+
exports.clampWindowStart = clampWindowStart;
|
|
55
|
+
exports.revealMember = revealMember;
|
|
56
|
+
exports.revealWindowStart = revealWindowStart;
|
|
57
|
+
exports.wheelRows = wheelRows;
|
|
58
|
+
exports.windowRows = windowRows;
|
|
59
|
+
exports.windowedPositions = windowedPositions;
|
|
60
|
+
//# sourceMappingURL=graph-group-window.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-group-window.cjs","sources":["../../src/data/graph-group-window.ts"],"sourcesContent":["// Scrolling the members of a windowed group (see `GroupMemberWindow`): which rows show, where shown\n// members are drawn, and where the edges of hidden ones attach. Pure math, no React.\n\nimport type { GraphLayoutGroupBox, GroupMemberWindow } from \"./graph-columns-layout\";\nimport type { GraphLayoutPosition } from \"./graph-layout\";\n\n/** Window starts, in rows, by group box id. A box without one starts at its first member. */\nexport type WindowStarts = Readonly<Record<string, number>>;\n\n/** The start kept between the first row and the last full window. */\nexport function clampWindowStart(memberWindow: GroupMemberWindow, start: number): number {\n return Math.min(Math.max(0, memberWindow.members.length - memberWindow.rows), Math.max(0, Math.round(start)));\n}\n\n/** The start that brings member `id` into view, moving no further than needed; unchanged for a shown member or a stranger. */\nexport function revealWindowStart(memberWindow: GroupMemberWindow, start: number, id: string): number {\n const index = memberWindow.members.indexOf(id);\n const current = clampWindowStart(memberWindow, start);\n if (index < 0) return current;\n if (index < current) return index;\n return index >= current + memberWindow.rows ? clampWindowStart(memberWindow, index - memberWindow.rows + 1) : current;\n}\n\n/** The starts after revealing `id` in the windowed box holding it: the same object when nothing moves. */\nexport function revealMember(boxes: readonly GraphLayoutGroupBox[], starts: WindowStarts, id: string | undefined): WindowStarts {\n if (id === undefined) return starts;\n const box = boxes.find((candidate) => candidate.window?.members.includes(id));\n if (!box?.window) return starts;\n const current = starts[box.id] ?? 0;\n const next = revealWindowStart(box.window, current, id);\n return next === clampWindowStart(box.window, current) ? starts : { ...starts, [box.id]: next };\n}\n\nexport interface WindowedPositions {\n positions: Record<string, GraphLayoutPosition>;\n /** Members scrolled out of view: not drawn, their edges attached at the window's edge. */\n hidden: Set<string>;\n}\n\n/**\n * Where each node is drawn once windows scroll: a shown member moves up by the rows scrolled past, a\n * member above the window sits on its top edge and one below on its bottom edge, so its edges still\n * reach the group. Every other node keeps its layout position.\n */\nexport function windowedPositions(\n positions: Readonly<Record<string, GraphLayoutPosition>>,\n boxes: readonly GraphLayoutGroupBox[],\n starts: WindowStarts,\n): WindowedPositions {\n const moved: Record<string, GraphLayoutPosition> = { ...positions };\n const hidden = new Set<string>();\n for (const box of boxes) {\n const memberWindow = box.window;\n if (!memberWindow) continue;\n const start = clampWindowStart(memberWindow, starts[box.id] ?? 0);\n const shift = memberWindow.tops[start] ?? 0;\n memberWindow.members.forEach((id, index) => {\n const at = positions[id];\n if (!at) throw new Error(`GraphDiagram: windowed member \"${id}\" of \"${box.id}\" has no layout position`);\n if (index < start || index >= start + memberWindow.rows) {\n hidden.add(id);\n moved[id] = { x: at.x, y: index < start ? memberWindow.top : memberWindow.top + memberWindow.height };\n } else {\n moved[id] = { x: at.x, y: at.y - shift };\n }\n });\n }\n return { positions: moved, hidden };\n}\n\n/** The rows shown, counted from one, as the footer says them. */\nexport function windowRows(memberWindow: GroupMemberWindow, start: number): { first: number; last: number; total: number } {\n const first = clampWindowStart(memberWindow, start);\n return { first: first + 1, last: Math.min(memberWindow.members.length, first + memberWindow.rows), total: memberWindow.members.length };\n}\n\n/** Whole rows of wheel travel, `rowPx` each, the remainder carried to the next event so small deltas add up. */\nexport function wheelRows(carry: number, deltaPx: number, rowPx: number): { rows: number; carry: number } {\n const total = carry + deltaPx;\n const rows = Math.trunc(total / rowPx);\n return { rows, carry: total - rows * rowPx };\n}\n"],"names":[],"mappings":";;AAUO,SAAS,iBAAiB,cAAiC,OAAuB;AACvF,SAAO,KAAK,IAAI,KAAK,IAAI,GAAG,aAAa,QAAQ,SAAS,aAAa,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC,CAAC;AAC9G;AAGO,SAAS,kBAAkB,cAAiC,OAAe,IAAoB;AACpG,QAAM,QAAQ,aAAa,QAAQ,QAAQ,EAAE;AAC7C,QAAM,UAAU,iBAAiB,cAAc,KAAK;AACpD,MAAI,QAAQ,EAAG,QAAO;AACtB,MAAI,QAAQ,QAAS,QAAO;AAC5B,SAAO,SAAS,UAAU,aAAa,OAAO,iBAAiB,cAAc,QAAQ,aAAa,OAAO,CAAC,IAAI;AAChH;AAGO,SAAS,aAAa,OAAuC,QAAsB,IAAsC;AAC9H,MAAI,OAAO,OAAW,QAAO;AAC7B,QAAM,MAAM,MAAM,KAAK,CAAC,cAAA;;AAAc,2BAAU,WAAV,mBAAkB,QAAQ,SAAS;AAAA,GAAG;AAC5E,MAAI,EAAC,2BAAK,QAAQ,QAAO;AACzB,QAAM,UAAU,OAAO,IAAI,EAAE,KAAK;AAClC,QAAM,OAAO,kBAAkB,IAAI,QAAQ,SAAS,EAAE;AACtD,SAAO,SAAS,iBAAiB,IAAI,QAAQ,OAAO,IAAI,SAAS,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,KAAA;AAC1F;AAaO,SAAS,kBACd,WACA,OACA,QACmB;AACnB,QAAM,QAA6C,EAAE,GAAG,UAAA;AACxD,QAAM,6BAAa,IAAA;AACnB,aAAW,OAAO,OAAO;AACvB,UAAM,eAAe,IAAI;AACzB,QAAI,CAAC,aAAc;AACnB,UAAM,QAAQ,iBAAiB,cAAc,OAAO,IAAI,EAAE,KAAK,CAAC;AAChE,UAAM,QAAQ,aAAa,KAAK,KAAK,KAAK;AAC1C,iBAAa,QAAQ,QAAQ,CAAC,IAAI,UAAU;AAC1C,YAAM,KAAK,UAAU,EAAE;AACvB,UAAI,CAAC,GAAI,OAAM,IAAI,MAAM,kCAAkC,EAAE,SAAS,IAAI,EAAE,0BAA0B;AACtG,UAAI,QAAQ,SAAS,SAAS,QAAQ,aAAa,MAAM;AACvD,eAAO,IAAI,EAAE;AACb,cAAM,EAAE,IAAI,EAAE,GAAG,GAAG,GAAG,GAAG,QAAQ,QAAQ,aAAa,MAAM,aAAa,MAAM,aAAa,OAAA;AAAA,MAC/F,OAAO;AACL,cAAM,EAAE,IAAI,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,MAAA;AAAA,MACnC;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO,EAAE,WAAW,OAAO,OAAA;AAC7B;AAGO,SAAS,WAAW,cAAiC,OAA+D;AACzH,QAAM,QAAQ,iBAAiB,cAAc,KAAK;AAClD,SAAO,EAAE,OAAO,QAAQ,GAAG,MAAM,KAAK,IAAI,aAAa,QAAQ,QAAQ,QAAQ,aAAa,IAAI,GAAG,OAAO,aAAa,QAAQ,OAAA;AACjI;AAGO,SAAS,UAAU,OAAe,SAAiB,OAAgD;AACxG,QAAM,QAAQ,QAAQ;AACtB,QAAM,OAAO,KAAK,MAAM,QAAQ,KAAK;AACrC,SAAO,EAAE,MAAM,OAAO,QAAQ,OAAO,MAAA;AACvC;;;;;;;"}
|