@pgcorp/ui-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/docs/accessibility.md +27 -0
  4. package/docs/getting-started.md +80 -0
  5. package/docs/licensing.md +30 -0
  6. package/docs/public-api.md +39 -0
  7. package/docs/security.md +21 -0
  8. package/docs/theming.md +35 -0
  9. package/docs/versioning.md +23 -0
  10. package/index.ts +3 -0
  11. package/package.json +592 -0
  12. package/src/components/icons/_internal/generatedSputnigUiIconMarkup.ts +971 -0
  13. package/src/components/icons/sputnigUiIcons.ts +530 -0
  14. package/src/components/layout/SAppShell.vue +232 -0
  15. package/src/components/layout/SDockRegion.vue +200 -0
  16. package/src/components/layout/SResizeHandle.vue +251 -0
  17. package/src/components/layout/SSideMenuButton.vue +104 -0
  18. package/src/components/layout/SSideMenuRail.vue +47 -0
  19. package/src/components/layout/SStack.css +25 -0
  20. package/src/components/layout/SStack.vue +57 -0
  21. package/src/components/layout/SStatusBarShell.css +15 -0
  22. package/src/components/layout/SStatusBarShell.vue +26 -0
  23. package/src/components/layout/SWorkbenchLayout.vue +53 -0
  24. package/src/components/layout/SWorkbenchPanelShell.css +161 -0
  25. package/src/components/layout/SWorkbenchPanelShell.vue +286 -0
  26. package/src/components/layout/_internal/SAppBrand.css +45 -0
  27. package/src/components/layout/_internal/SAppBrand.vue +37 -0
  28. package/src/components/layout/_internal/SAppBrandContent.vue +30 -0
  29. package/src/components/layout/types.ts +28 -0
  30. package/src/components/shared/_internal/SDialogSurface.css +127 -0
  31. package/src/components/shared/_internal/SDialogSurface.vue +117 -0
  32. package/src/components/shared/_internal/SDisclosureSummary.css +92 -0
  33. package/src/components/shared/_internal/SDisclosureSummary.vue +121 -0
  34. package/src/components/shared/_internal/SListboxGroup.css +18 -0
  35. package/src/components/shared/_internal/SListboxGroup.vue +24 -0
  36. package/src/components/shared/_internal/SMenuItem.css +112 -0
  37. package/src/components/shared/_internal/SMenuItem.vue +213 -0
  38. package/src/components/shared/_internal/SMenuLayer.css +21 -0
  39. package/src/components/shared/_internal/SMenuLayer.vue +48 -0
  40. package/src/components/shared/_internal/SMenuSeparator.css +5 -0
  41. package/src/components/shared/_internal/SMenuSeparator.vue +13 -0
  42. package/src/components/shared/_internal/SToastItem.css +65 -0
  43. package/src/components/shared/_internal/SToastItem.vue +58 -0
  44. package/src/components/shared/_internal/STreeNode.vue +761 -0
  45. package/src/components/shared/_internal/dialogSurface.ts +105 -0
  46. package/src/components/shared/_internal/focusTrap.ts +40 -0
  47. package/src/components/shared/_internal/listboxContext.ts +16 -0
  48. package/src/components/shared/_internal/markerContract.ts +10 -0
  49. package/src/components/shared/_internal/menuContext.ts +23 -0
  50. package/src/components/shared/_internal/selectionContract.ts +239 -0
  51. package/src/components/shared/_internal/sidebarGroupContext.ts +31 -0
  52. package/src/components/shared/_internal/tabsContext.ts +57 -0
  53. package/src/components/shared/_internal/treeRuntime.ts +203 -0
  54. package/src/components/shared/_internal/useSelectionRoving.ts +162 -0
  55. package/src/components/shared/codeLanguages.ts +168 -0
  56. package/src/components/shared/complex/SKanbanCard.css +8 -0
  57. package/src/components/shared/complex/SKanbanCard.vue +76 -0
  58. package/src/components/shared/complex/SKanbanLane.vue +82 -0
  59. package/src/components/shared/complex/STree.css +24 -0
  60. package/src/components/shared/complex/STree.vue +288 -0
  61. package/src/components/shared/complex/treeAdapter.ts +210 -0
  62. package/src/components/shared/complex/types.ts +102 -0
  63. package/src/components/shared/containers/SCloudChrome.css +103 -0
  64. package/src/components/shared/containers/SCloudChrome.vue +87 -0
  65. package/src/components/shared/containers/SCloudChromeEmpty.css +10 -0
  66. package/src/components/shared/containers/SCloudChromeEmpty.vue +18 -0
  67. package/src/components/shared/containers/SCloudChromeRow.css +78 -0
  68. package/src/components/shared/containers/SCloudChromeRow.vue +108 -0
  69. package/src/components/shared/containers/SCollapsiblePanel.css +57 -0
  70. package/src/components/shared/containers/SCollapsiblePanel.vue +360 -0
  71. package/src/components/shared/containers/SDrawer.css +60 -0
  72. package/src/components/shared/containers/SDrawer.vue +86 -0
  73. package/src/components/shared/containers/SFormGrid.css +26 -0
  74. package/src/components/shared/containers/SFormGrid.vue +46 -0
  75. package/src/components/shared/containers/SFormGridItem.css +18 -0
  76. package/src/components/shared/containers/SFormGridItem.vue +29 -0
  77. package/src/components/shared/containers/SLeftSidebar.vue +74 -0
  78. package/src/components/shared/containers/SModal.css +27 -0
  79. package/src/components/shared/containers/SModal.vue +105 -0
  80. package/src/components/shared/containers/SPageHeader.css +66 -0
  81. package/src/components/shared/containers/SPageHeader.vue +45 -0
  82. package/src/components/shared/containers/SPanel.css +134 -0
  83. package/src/components/shared/containers/SPanel.vue +169 -0
  84. package/src/components/shared/containers/SPopover.css +35 -0
  85. package/src/components/shared/containers/SPopover.vue +551 -0
  86. package/src/components/shared/containers/SSidebarGroup.css +12 -0
  87. package/src/components/shared/containers/SSidebarGroup.vue +443 -0
  88. package/src/components/shared/containers/SSidebarSection.css +41 -0
  89. package/src/components/shared/containers/SSidebarSection.vue +268 -0
  90. package/src/components/shared/containers/disclosureSummary.ts +48 -0
  91. package/src/components/shared/containers/popover.ts +72 -0
  92. package/src/components/shared/containers/sidebar.ts +206 -0
  93. package/src/components/shared/controls/SButton.css +274 -0
  94. package/src/components/shared/controls/SButton.vue +211 -0
  95. package/src/components/shared/controls/SCheckbox.css +65 -0
  96. package/src/components/shared/controls/SCheckbox.vue +130 -0
  97. package/src/components/shared/controls/SCheckboxGroup.vue +134 -0
  98. package/src/components/shared/controls/SChoiceCards.css +29 -0
  99. package/src/components/shared/controls/SChoiceCards.vue +145 -0
  100. package/src/components/shared/controls/SColorSelect.vue +146 -0
  101. package/src/components/shared/controls/SComboboxTrigger.css +35 -0
  102. package/src/components/shared/controls/SComboboxTrigger.vue +122 -0
  103. package/src/components/shared/controls/SContextMenu.css +11 -0
  104. package/src/components/shared/controls/SContextMenu.vue +186 -0
  105. package/src/components/shared/controls/SContextToggleButton.vue +64 -0
  106. package/src/components/shared/controls/SDragHandle.vue +100 -0
  107. package/src/components/shared/controls/SDropdownMenu.css +5 -0
  108. package/src/components/shared/controls/SDropdownMenu.vue +188 -0
  109. package/src/components/shared/controls/SField.css +79 -0
  110. package/src/components/shared/controls/SField.vue +89 -0
  111. package/src/components/shared/controls/SFieldGroup.css +58 -0
  112. package/src/components/shared/controls/SFieldGroup.vue +117 -0
  113. package/src/components/shared/controls/SFieldLabel.css +12 -0
  114. package/src/components/shared/controls/SFieldLabel.vue +51 -0
  115. package/src/components/shared/controls/SInlineTokenEditor.css +61 -0
  116. package/src/components/shared/controls/SInlineTokenEditor.vue +1081 -0
  117. package/src/components/shared/controls/SInputText.css +66 -0
  118. package/src/components/shared/controls/SInputText.vue +321 -0
  119. package/src/components/shared/controls/SInteractiveSurface.css +72 -0
  120. package/src/components/shared/controls/SInteractiveSurface.vue +326 -0
  121. package/src/components/shared/controls/SLink.css +114 -0
  122. package/src/components/shared/controls/SLink.ts +85 -0
  123. package/src/components/shared/controls/SLink.vue +114 -0
  124. package/src/components/shared/controls/SListbox.css +31 -0
  125. package/src/components/shared/controls/SListbox.vue +388 -0
  126. package/src/components/shared/controls/SListboxOption.css +63 -0
  127. package/src/components/shared/controls/SListboxOption.vue +85 -0
  128. package/src/components/shared/controls/SMarker.css +19 -0
  129. package/src/components/shared/controls/SMarker.vue +33 -0
  130. package/src/components/shared/controls/SMenuSurface.css +20 -0
  131. package/src/components/shared/controls/SMenuSurface.vue +333 -0
  132. package/src/components/shared/controls/SSegmentedControl.css +38 -0
  133. package/src/components/shared/controls/SSegmentedControl.vue +134 -0
  134. package/src/components/shared/controls/SSelect.css +12 -0
  135. package/src/components/shared/controls/SSelect.vue +338 -0
  136. package/src/components/shared/controls/STextarea.css +31 -0
  137. package/src/components/shared/controls/STextarea.vue +316 -0
  138. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +224 -0
  139. package/src/components/shared/controls/menu.ts +178 -0
  140. package/src/components/shared/data-display/SActionCard.css +6 -0
  141. package/src/components/shared/data-display/SActionCard.vue +49 -0
  142. package/src/components/shared/data-display/SActionList.css +15 -0
  143. package/src/components/shared/data-display/SActionList.vue +34 -0
  144. package/src/components/shared/data-display/SActionListItem.css +114 -0
  145. package/src/components/shared/data-display/SActionListItem.vue +230 -0
  146. package/src/components/shared/data-display/SBadge.css +73 -0
  147. package/src/components/shared/data-display/SBadge.vue +52 -0
  148. package/src/components/shared/data-display/SCodeBlock.css +56 -0
  149. package/src/components/shared/data-display/SCodeBlock.vue +188 -0
  150. package/src/components/shared/data-display/SCodeEditor.css +178 -0
  151. package/src/components/shared/data-display/SCodeEditor.vue +274 -0
  152. package/src/components/shared/data-display/SCodeSearchPanel.vue +327 -0
  153. package/src/components/shared/data-display/SCopyField.vue +77 -0
  154. package/src/components/shared/data-display/SDocBlock.css +136 -0
  155. package/src/components/shared/data-display/SDocBlock.vue +358 -0
  156. package/src/components/shared/data-display/SEmptyState.css +42 -0
  157. package/src/components/shared/data-display/SEmptyState.vue +79 -0
  158. package/src/components/shared/data-display/SExpandableText.vue +314 -0
  159. package/src/components/shared/data-display/SJsonTree.css +6 -0
  160. package/src/components/shared/data-display/SJsonTree.vue +111 -0
  161. package/src/components/shared/data-display/SKeyValueGrid.css +37 -0
  162. package/src/components/shared/data-display/SKeyValueGrid.vue +277 -0
  163. package/src/components/shared/data-display/SLinkedSystemsList.vue +270 -0
  164. package/src/components/shared/data-display/SMessage.css +88 -0
  165. package/src/components/shared/data-display/SMessage.vue +152 -0
  166. package/src/components/shared/data-display/SMetricCard.css +32 -0
  167. package/src/components/shared/data-display/SMetricCard.vue +117 -0
  168. package/src/components/shared/data-display/SProgressBar.css +84 -0
  169. package/src/components/shared/data-display/SProgressBar.vue +114 -0
  170. package/src/components/shared/data-display/SProgressIndicator.css +59 -0
  171. package/src/components/shared/data-display/SProgressIndicator.vue +104 -0
  172. package/src/components/shared/data-display/SSectionHeader.css +84 -0
  173. package/src/components/shared/data-display/SSectionHeader.vue +58 -0
  174. package/src/components/shared/data-display/SStatus.css +90 -0
  175. package/src/components/shared/data-display/SStatus.vue +79 -0
  176. package/src/components/shared/data-display/STable.css +83 -0
  177. package/src/components/shared/data-display/STable.vue +339 -0
  178. package/src/components/shared/data-display/SToastContainer.css +53 -0
  179. package/src/components/shared/data-display/SToastContainer.vue +31 -0
  180. package/src/components/shared/data-display/STooltip.css +114 -0
  181. package/src/components/shared/data-display/STooltip.vue +550 -0
  182. package/src/components/shared/data-display/STooltipAnchor.css +21 -0
  183. package/src/components/shared/data-display/STooltipAnchor.vue +119 -0
  184. package/src/components/shared/data-display/SVirtualList.css +24 -0
  185. package/src/components/shared/data-display/SVirtualList.ts +19 -0
  186. package/src/components/shared/data-display/SVirtualList.vue +361 -0
  187. package/src/components/shared/data-display/json.ts +136 -0
  188. package/src/components/shared/data-display/table.ts +76 -0
  189. package/src/components/shared/database/SDataGrid.css +3 -0
  190. package/src/components/shared/database/SDataGrid.vue +464 -0
  191. package/src/components/shared/database/SSqlEditor.vue +363 -0
  192. package/src/components/shared/database/dataGrid.ts +61 -0
  193. package/src/components/shared/feedback/SAsyncState.css +26 -0
  194. package/src/components/shared/feedback/SAsyncState.vue +190 -0
  195. package/src/components/shared/graph/SGraphViewport.ts +1 -0
  196. package/src/components/shared/graph/SGraphViewport.vue +858 -0
  197. package/src/components/shared/graph/_internal/graphViewportContract.ts +362 -0
  198. package/src/components/shared/markdown.ts +169 -0
  199. package/src/components/shared/navigation/SBottomNav.vue +692 -0
  200. package/src/components/shared/navigation/SBreadcrumbs.css +26 -0
  201. package/src/components/shared/navigation/SBreadcrumbs.vue +155 -0
  202. package/src/components/shared/navigation/SCatalogNavigator.css +100 -0
  203. package/src/components/shared/navigation/SCatalogNavigator.vue +308 -0
  204. package/src/components/shared/navigation/STab.vue +363 -0
  205. package/src/components/shared/navigation/STabList.vue +442 -0
  206. package/src/components/shared/navigation/STabPanel.vue +165 -0
  207. package/src/components/shared/navigation/STabPanels.vue +33 -0
  208. package/src/components/shared/navigation/STabs.vue +617 -0
  209. package/src/components/shared/navigation/SWizardSteps.css +69 -0
  210. package/src/components/shared/navigation/SWizardSteps.vue +171 -0
  211. package/src/components/shared/navigation/_internal/SCatalogNavigatorGroup.vue +63 -0
  212. package/src/components/shared/persona/SPersonaProjectPicker.css +36 -0
  213. package/src/components/shared/persona/SPersonaProjectPicker.vue +361 -0
  214. package/src/components/shared/persona/SPersonaRunWorkbench.css +109 -0
  215. package/src/components/shared/persona/SPersonaRunWorkbench.vue +549 -0
  216. package/src/components/shared/persona/_internal/SPersonaRunConsole.css +40 -0
  217. package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +298 -0
  218. package/src/components/shared/persona/_internal/inventory.ts +110 -0
  219. package/src/components/shared/persona/_internal/labels.ts +79 -0
  220. package/src/components/shared/persona/presentation.ts +371 -0
  221. package/src/components/shared/persona/types.ts +256 -0
  222. package/src/composables/uiPreferencesReset.ts +52 -0
  223. package/src/composables/useClipboard.ts +37 -0
  224. package/src/composables/useDialogOverlay.ts +85 -0
  225. package/src/composables/useDisclosure.ts +40 -0
  226. package/src/composables/useFloatingPosition.ts +245 -0
  227. package/src/composables/useNotifier.ts +26 -0
  228. package/src/composables/useSidebarPanelState.ts +610 -0
  229. package/src/internal/codeEditorContract.ts +110 -0
  230. package/src/internal/codeEditorExtensions.ts +27 -0
  231. package/src/internal/feedbackPresentation.ts +136 -0
  232. package/src/internal/fieldSurface.ts +59 -0
  233. package/src/internal/focusNavigation.ts +69 -0
  234. package/src/internal/inlineTokenEditorContract.ts +136 -0
  235. package/src/internal/interactiveElement.ts +33 -0
  236. package/src/internal/layerStack.ts +202 -0
  237. package/src/internal/linkTarget.ts +256 -0
  238. package/src/internal/ownedAttrs.ts +464 -0
  239. package/src/internal/passiveContentContract.ts +129 -0
  240. package/src/internal/pointerInteractionLease.ts +81 -0
  241. package/src/internal/runtimeContract.ts +194 -0
  242. package/src/internal/runtimeTheme.ts +159 -0
  243. package/src/internal/semanticSizing.ts +60 -0
  244. package/src/internal/useMenuLayer.ts +129 -0
  245. package/src/stores/useNotifierStore.ts +69 -0
  246. package/src/styles/reference.css +4 -0
  247. package/src/styles/style.css +151 -0
  248. package/src/styles/tokens.css +836 -0
  249. package/src/theme.ts +449 -0
  250. package/src/types/highlight-js-lib.d.ts +76 -0
  251. package/src/variants.ts +15 -0
@@ -0,0 +1,858 @@
1
+ <template>
2
+ <div
3
+ v-bind="ownedAttrs.bindings()"
4
+ class="s-graph-viewport"
5
+ :data-render-state="renderState"
6
+ :data-positioning="resolvedPositioningMode"
7
+ >
8
+ <div ref="rendererRef" class="s-graph-viewport__renderer" aria-hidden="true" />
9
+ <div v-if="renderError" class="s-graph-viewport__error">
10
+ <SAsyncState
11
+ :state="{ status: 'error', message: renderError.message }"
12
+ presentation="block"
13
+ />
14
+ </div>
15
+ <div
16
+ class="s-graph-viewport__node-inventory"
17
+ :class="{ 's-graph-viewport__node-inventory--fallback': renderError !== null }"
18
+ >
19
+ <SListbox
20
+ :label="resolvedNodeListLabel"
21
+ :model-value="selectedNodeId"
22
+ :options="nodeListOptions"
23
+ :empty-label="resolvedNodeListEmptyLabel"
24
+ @update:model-value="handleInventorySelection"
25
+ @option-keydown="handleInventoryOptionKeydown"
26
+ />
27
+ </div>
28
+ </div>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
33
+ import Graph from 'graphology'
34
+ import Sigma from 'sigma'
35
+ import type { Settings } from 'sigma/settings'
36
+ import type {
37
+ SGraphEdge,
38
+ SGraphNode,
39
+ SGraphNodeSizeMode,
40
+ SGraphNodePointerEvent,
41
+ SGraphPosition,
42
+ SGraphPositionChange,
43
+ SGraphRenderError,
44
+ SGraphRenderErrorReason,
45
+ SGraphRenderMetric,
46
+ SGraphSelection,
47
+ SGraphTone,
48
+ SGraphViewportGraph,
49
+ SGraphViewportInteraction,
50
+ } from './_internal/graphViewportContract'
51
+ import {
52
+ assertSGraphViewportGraph,
53
+ assertSGraphViewportInteraction,
54
+ SGraphContractError,
55
+ } from './_internal/graphViewportContract'
56
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
57
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
58
+ import { useRuntimeTheme } from '../../../internal/runtimeTheme'
59
+ import SListbox, { type SListboxOptionKeydownPayload } from '../controls/SListbox.vue'
60
+ import SAsyncState from '../feedback/SAsyncState.vue'
61
+
62
+ defineOptions({ inheritAttrs: false })
63
+
64
+ type SGraphAttributes = Record<string, unknown>
65
+
66
+ type GraphNodeAttributes = SGraphAttributes & SGraphNode & {
67
+ color: string
68
+ borderColor: string
69
+ labelColor: string
70
+ highlighted: boolean
71
+ hidden: boolean
72
+ }
73
+
74
+ type GraphEdgeAttributes = SGraphAttributes & SGraphEdge & {
75
+ color: string
76
+ hidden: boolean
77
+ size: number
78
+ opacity: number
79
+ }
80
+
81
+ const props = withDefaults(defineProps<{
82
+ graph: SGraphViewportGraph
83
+ interaction: SGraphViewportInteraction
84
+ nodeListLabel: string
85
+ nodeListEmptyLabel: string
86
+ showEdgeLabels?: boolean
87
+ labelZoomThreshold?: number
88
+ nodeSizeMode?: SGraphNodeSizeMode
89
+ }>(), {
90
+ showEdgeLabels: false,
91
+ labelZoomThreshold: 0.76,
92
+ nodeSizeMode: 'screen',
93
+ })
94
+
95
+ const emit = defineEmits<{
96
+ 'hover-node': [event: SGraphNodePointerEvent]
97
+ 'leave-node': []
98
+ 'selection-change': [selection: SGraphSelection]
99
+ 'activate-node': [nodeId: string]
100
+ 'positions-change': [changes: SGraphPositionChange[]]
101
+ 'render-metric': [metric: SGraphRenderMetric]
102
+ 'render-error': [error: SGraphRenderError]
103
+ }>()
104
+
105
+ const ownedAttrs = useOwnedAttrs({ component: 'SGraphViewport', owner: 'graph viewport root' })
106
+ useInteractiveLeafRegistration({ owner: 'SGraphViewport' })
107
+ const rendererRef = ref<HTMLElement | null>(null)
108
+ const runtimeTheme = useRuntimeTheme()
109
+ const renderState = ref<'idle' | 'painted' | 'failed'>('idle')
110
+ const renderError = ref<SGraphRenderError | null>(null)
111
+ let sigma: Sigma<GraphNodeAttributes, GraphEdgeAttributes> | null = null
112
+ let renderedGraph: Graph<GraphNodeAttributes, GraphEdgeAttributes> | null = null
113
+ let resizeObserver: ResizeObserver | null = null
114
+ let draggedNodeId: string | null = null
115
+ let draggedNodeStartPosition: SGraphPosition | null = null
116
+ let draggedGroupStartPositions = new Map<string, SGraphPosition>()
117
+ let didDrag = false
118
+
119
+ const SIGMA_INITIAL_CAMERA_RATIO = 1
120
+
121
+ function assertViewportConfiguration(): void {
122
+ assertSGraphViewportGraph(props.graph)
123
+ assertSGraphViewportInteraction(props.interaction, props.graph)
124
+ if (typeof props.nodeListLabel !== 'string' || props.nodeListLabel.trim().length === 0) {
125
+ throw new SGraphContractError('SGraphViewport: nodeListLabel должен быть непустой строкой.')
126
+ }
127
+ if (typeof props.nodeListEmptyLabel !== 'string' || props.nodeListEmptyLabel.trim().length === 0) {
128
+ throw new SGraphContractError('SGraphViewport: nodeListEmptyLabel должен быть непустой строкой.')
129
+ }
130
+ if (!Number.isFinite(props.labelZoomThreshold) || props.labelZoomThreshold <= 0) {
131
+ throw new SGraphContractError('SGraphViewport: labelZoomThreshold должен быть конечным числом больше 0.')
132
+ }
133
+ if (!['screen', 'graph'].includes(props.nodeSizeMode)) {
134
+ throw new SGraphContractError(`SGraphViewport: неизвестный nodeSizeMode "${String(props.nodeSizeMode)}".`)
135
+ }
136
+ if (typeof props.showEdgeLabels !== 'boolean') {
137
+ throw new SGraphContractError('SGraphViewport: showEdgeLabels должен быть boolean.')
138
+ }
139
+ }
140
+
141
+ const interactionContract = computed(() => {
142
+ assertViewportConfiguration()
143
+ return props.interaction
144
+ })
145
+ const resolvedNodeListLabel = computed(() => {
146
+ assertViewportConfiguration()
147
+ return props.nodeListLabel
148
+ })
149
+ const resolvedNodeListEmptyLabel = computed(() => {
150
+ assertViewportConfiguration()
151
+ return props.nodeListEmptyLabel
152
+ })
153
+ const resolvedPositioningMode = computed(() => interactionContract.value.positioning.mode)
154
+ const selectedNodeId = computed(() => (
155
+ interactionContract.value.selection.mode === 'single'
156
+ ? interactionContract.value.selection.nodeId
157
+ : null
158
+ ))
159
+ const selectedNodeSet = computed(() => new Set(selectedNodeId.value === null ? [] : [selectedNodeId.value]))
160
+ const nodeListOptions = computed(() => {
161
+ assertViewportConfiguration()
162
+ return props.graph.nodes.map((node) => ({ value: node.id, label: node.label }))
163
+ })
164
+
165
+ function requiredRuntimeToken(name: string): string {
166
+ const element = rendererRef.value
167
+ if (!element) {
168
+ throw new Error(`SGraphViewport: невозможно прочитать semantic token ${name} до монтирования viewport.`)
169
+ }
170
+ const value = getComputedStyle(element).getPropertyValue(name).trim()
171
+ if (!value) {
172
+ throw new Error(`SGraphViewport: обязательный semantic token ${name} отсутствует в computed style.`)
173
+ }
174
+ return value
175
+ }
176
+
177
+ function requiredRuntimeNumber(name: string): number {
178
+ const raw = requiredRuntimeToken(name)
179
+ const value = Number(raw)
180
+ if (!Number.isFinite(value)) {
181
+ throw new Error(`SGraphViewport: semantic token ${name} должен быть конечным unitless number, получено '${raw}'.`)
182
+ }
183
+ return value
184
+ }
185
+
186
+ function requiredRuntimePixels(name: string): number {
187
+ const raw = requiredRuntimeToken(name)
188
+ const match = raw.match(/^(-?(?:\d+(?:\.\d+)?|\.\d+))px$/u)
189
+ if (!match) {
190
+ throw new Error(`SGraphViewport: semantic token ${name} должен быть вычислен в px, получено '${raw}'.`)
191
+ }
192
+ return Number(match[1])
193
+ }
194
+
195
+ function requiredRuntimeDuration(name: string): number {
196
+ const raw = requiredRuntimeToken(name)
197
+ const match = raw.match(/^((?:\d+(?:\.\d+)?|\.\d+))(ms|s)$/u)
198
+ if (!match) {
199
+ throw new Error(`SGraphViewport: semantic token ${name} должен быть вычислен в ms или s, получено '${raw}'.`)
200
+ }
201
+ const value = Number(match[1])
202
+ return match[2] === 's' ? value * 1000 : value
203
+ }
204
+
205
+ function requiredRuntimeHexColor(name: string): string {
206
+ const value = requiredRuntimeToken(name)
207
+ if (!/^#[\da-f]{6}$/iu.test(value)) {
208
+ throw new Error(`SGraphViewport: semantic token ${name} должен быть вычислен в #RRGGBB, получено '${value}'.`)
209
+ }
210
+ return value
211
+ }
212
+
213
+ function resolveNodeTone(tone: SGraphTone): Pick<GraphNodeAttributes, 'color' | 'borderColor' | 'labelColor'> {
214
+ return {
215
+ color: requiredRuntimeHexColor(`--s-graph-node-tone-${tone}-fill`),
216
+ borderColor: requiredRuntimeHexColor(`--s-graph-node-tone-${tone}-border`),
217
+ labelColor: requiredRuntimeHexColor(`--s-graph-node-tone-${tone}-label`),
218
+ }
219
+ }
220
+
221
+ function resolveEdgeTone(tone: SGraphTone): string {
222
+ return requiredRuntimeHexColor(`--s-graph-edge-tone-${tone}`)
223
+ }
224
+
225
+ function colorWithOpacity(color: string, opacity: number | undefined): string {
226
+ if (opacity === undefined || opacity === 1) return color
227
+ const red = Number.parseInt(color.slice(1, 3), 16)
228
+ const green = Number.parseInt(color.slice(3, 5), 16)
229
+ const blue = Number.parseInt(color.slice(5, 7), 16)
230
+ return `rgba(${red}, ${green}, ${blue}, ${opacity})`
231
+ }
232
+
233
+ function buildGraph(source: SGraphViewportGraph): Graph<GraphNodeAttributes, GraphEdgeAttributes> {
234
+ assertSGraphViewportGraph(source)
235
+ const result = new Graph<GraphNodeAttributes, GraphEdgeAttributes>({ multi: true, type: 'mixed' })
236
+ for (const node of source.nodes) {
237
+ result.addNode(node.id, {
238
+ ...node,
239
+ ...resolveNodeTone(node.tone),
240
+ highlighted: false,
241
+ hidden: false,
242
+ })
243
+ }
244
+ for (const edge of source.edges) {
245
+ result.addEdgeWithKey(edge.id, edge.source, edge.target, {
246
+ ...edge,
247
+ color: resolveEdgeTone(edge.tone),
248
+ hidden: edge.hidden ?? false,
249
+ size: edge.size ?? 1,
250
+ opacity: edge.opacity ?? 1,
251
+ })
252
+ }
253
+ return result
254
+ }
255
+
256
+ function refreshGraphToneAttributes(): void {
257
+ if (!renderedGraph) return
258
+ for (const node of props.graph.nodes) {
259
+ if (!renderedGraph.hasNode(node.id)) {
260
+ throw new Error(`SGraphViewport: node "${node.id}" отсутствует при обновлении runtime theme.`)
261
+ }
262
+ const tone = resolveNodeTone(node.tone)
263
+ renderedGraph.setNodeAttribute(node.id, 'color', tone.color)
264
+ renderedGraph.setNodeAttribute(node.id, 'borderColor', tone.borderColor)
265
+ renderedGraph.setNodeAttribute(node.id, 'labelColor', tone.labelColor)
266
+ }
267
+ for (const edge of props.graph.edges) {
268
+ if (!renderedGraph.hasEdge(edge.id)) {
269
+ throw new Error(`SGraphViewport: edge "${edge.id}" отсутствует при обновлении runtime theme.`)
270
+ }
271
+ renderedGraph.setEdgeAttribute(edge.id, 'color', resolveEdgeTone(edge.tone))
272
+ }
273
+ }
274
+
275
+ function graphSettings(): Partial<Settings<GraphNodeAttributes, GraphEdgeAttributes>> {
276
+ return {
277
+ renderLabels: true,
278
+ renderEdgeLabels: props.showEdgeLabels,
279
+ hideEdgesOnMove: true,
280
+ hideLabelsOnMove: true,
281
+ enableEdgeEvents: false,
282
+ defaultNodeColor: requiredRuntimeHexColor('--s-graph-node-default-color'),
283
+ defaultEdgeColor: requiredRuntimeHexColor('--s-graph-edge-default-color'),
284
+ labelColor: { attribute: 'labelColor', color: requiredRuntimeHexColor('--s-graph-label-color') },
285
+ labelFont: requiredRuntimeToken('--s-graph-label-font-family'),
286
+ labelSize: requiredRuntimePixels('--s-graph-label-font-size'),
287
+ labelWeight: requiredRuntimeToken('--s-graph-label-font-weight'),
288
+ labelDensity: requiredRuntimeNumber('--s-graph-label-density'),
289
+ labelGridCellSize: requiredRuntimePixels('--s-graph-label-grid-cell-size'),
290
+ labelRenderedSizeThreshold: requiredRuntimePixels('--s-graph-label-rendered-size-threshold'),
291
+ minEdgeThickness: requiredRuntimePixels('--s-graph-min-edge-thickness'),
292
+ stagePadding: requiredRuntimePixels('--s-graph-stage-padding'),
293
+ zIndex: true,
294
+ ...(props.nodeSizeMode === 'screen'
295
+ ? {
296
+ zoomToSizeRatioFunction: (ratio: number) =>
297
+ Math.max(0.08, Math.min(0.72, Math.sqrt(Math.max(0.0001, ratio)) * 0.78)),
298
+ }
299
+ : {}),
300
+ nodeReducer: (nodeId, data) => {
301
+ const isSelected = selectedNodeSet.value.has(nodeId)
302
+ const forceLabel = Boolean(data.forceLabel) || isSelected || data.highlighted
303
+ // Sigma синхронно вызывает reducer внутри конструктора до присваивания
304
+ // готового instance. Камера Sigma стартует с ratio 1; следующие проходы
305
+ // reducer читают состояние уже смонтированной камеры.
306
+ const zoom = sigma?.getCamera().getState().ratio ?? SIGMA_INITIAL_CAMERA_RATIO
307
+ const label = forceLabel || zoom < props.labelZoomThreshold ? data.label : ''
308
+ return {
309
+ x: data.x,
310
+ y: data.y,
311
+ label,
312
+ color: isSelected ? data.borderColor : data.color,
313
+ size: isSelected ? Math.min(data.size * 1.16, data.size + 1.4) : data.size,
314
+ forceLabel,
315
+ highlighted: data.highlighted || isSelected,
316
+ zIndex: data.role === 'group' || data.role === 'coverage' ? 4 : isSelected ? 5 : 1,
317
+ }
318
+ },
319
+ edgeReducer: (_edgeId, data) => {
320
+ if (!renderedGraph) throw new Error('SGraphViewport: edge reducer вызван без rendered graph.')
321
+ if (!renderedGraph.hasNode(data.source) || !renderedGraph.hasNode(data.target)) {
322
+ throw new Error(
323
+ `SGraphViewport: edge "${data.id}" ссылается на отсутствующий rendered node при reducer.`,
324
+ )
325
+ }
326
+ const selected = selectedNodeId.value
327
+ const relatedToSelection = selected ? data.source === selected || data.target === selected : false
328
+ const sourceHighlighted = Boolean(renderedGraph.getNodeAttribute(data.source, 'highlighted'))
329
+ const targetHighlighted = Boolean(renderedGraph.getNodeAttribute(data.target, 'highlighted'))
330
+ const relatedToInteraction = relatedToSelection || sourceHighlighted || targetHighlighted
331
+ return {
332
+ color: colorWithOpacity(data.color, relatedToInteraction ? Math.max(data.opacity, 0.55) : data.opacity),
333
+ size: relatedToInteraction ? Math.max(data.size, 1.4) : data.size,
334
+ hidden: data.hidden
335
+ || Boolean(selected && !relatedToSelection)
336
+ || Boolean(data.showOnInteractionOnly && !relatedToInteraction),
337
+ forceLabel: false,
338
+ }
339
+ },
340
+ }
341
+ }
342
+
343
+ function emitRenderMetric(phase: SGraphRenderMetric['phase'], startedAt: number): void {
344
+ emit('render-metric', {
345
+ phase,
346
+ durationMs: performance.now() - startedAt,
347
+ nodes: props.graph.nodes.length,
348
+ edges: props.graph.edges.length,
349
+ renderer: 'sigma',
350
+ })
351
+ }
352
+
353
+ function failRender(reason: SGraphRenderErrorReason, message: string): void {
354
+ sigma?.kill()
355
+ sigma = null
356
+ renderedGraph = null
357
+ renderState.value = 'failed'
358
+ renderError.value = {
359
+ reason,
360
+ message,
361
+ nodes: props.graph.nodes.length,
362
+ edges: props.graph.edges.length,
363
+ }
364
+ emit('render-error', renderError.value)
365
+ }
366
+
367
+ function clearRenderError(): void {
368
+ renderError.value = null
369
+ if (renderState.value === 'failed') renderState.value = 'idle'
370
+ }
371
+
372
+ function hasRenderableContainer(): boolean {
373
+ const container = rendererRef.value
374
+ if (!container) return false
375
+ return container.clientWidth > 0 && container.clientHeight > 0
376
+ }
377
+
378
+ function hasWebgl2Support(): boolean {
379
+ const canvas = document.createElement('canvas')
380
+ return canvas.getContext('webgl2') !== null
381
+ }
382
+
383
+ function tryMountSigma(): void {
384
+ if (sigma || !hasRenderableContainer()) return
385
+ mountSigma()
386
+ }
387
+
388
+ function refreshRuntimeTheme(): void {
389
+ if (!sigma) return
390
+ try {
391
+ refreshGraphToneAttributes()
392
+ sigma.setSettings(graphSettings())
393
+ sigma.refresh()
394
+ } catch (error) {
395
+ failRender(
396
+ 'sigma_mount_failed',
397
+ error instanceof Error ? error.message : 'SGraphViewport: не удалось применить semantic runtime theme.',
398
+ )
399
+ }
400
+ }
401
+
402
+ function mountSigma(): void {
403
+ const container = rendererRef.value
404
+ if (!container || !hasRenderableContainer()) return
405
+ let nextGraph: Graph<GraphNodeAttributes, GraphEdgeAttributes>
406
+ try {
407
+ assertViewportConfiguration()
408
+ nextGraph = buildGraph(props.graph)
409
+ } catch (error) {
410
+ failRender(
411
+ error instanceof SGraphContractError ? 'invalid_graph' : 'sigma_mount_failed',
412
+ error instanceof Error ? error.message : 'Граф содержит некорректные данные.',
413
+ )
414
+ return
415
+ }
416
+ if (!hasWebgl2Support()) {
417
+ failRender('webgl_unavailable', 'Графический движок требует WebGL2.')
418
+ return
419
+ }
420
+ const startedAt = performance.now()
421
+ try {
422
+ renderedGraph = nextGraph
423
+ sigma = new Sigma<GraphNodeAttributes, GraphEdgeAttributes>(renderedGraph, container, graphSettings())
424
+ clearRenderError()
425
+ renderState.value = 'painted'
426
+ bindSigmaEvents()
427
+ emitRenderMetric('sigma_mount', startedAt)
428
+ } catch (error) {
429
+ failRender(
430
+ error instanceof SGraphContractError ? 'invalid_graph' : 'sigma_mount_failed',
431
+ error instanceof Error ? error.message : 'Графический движок не смог построить карту.',
432
+ )
433
+ }
434
+ }
435
+
436
+ function assertRenderedNode(nodeId: string, operation: string): void {
437
+ if (!renderedGraph?.hasNode(nodeId)) {
438
+ throw new Error(`SGraphViewport: node "${nodeId}" отсутствует при ${operation}.`)
439
+ }
440
+ }
441
+
442
+ function requestSelection(selection: SGraphSelection): void {
443
+ if (selection.mode === 'single' && !props.graph.nodes.some((node) => node.id === selection.nodeId)) {
444
+ throw new SGraphContractError(
445
+ `SGraphViewport: selection-change ссылается на отсутствующий node "${selection.nodeId}".`,
446
+ )
447
+ }
448
+ emit('selection-change', selection)
449
+ }
450
+
451
+ function requestActivation(nodeId: string): void {
452
+ if (!props.graph.nodes.some((node) => node.id === nodeId)) {
453
+ throw new SGraphContractError(`SGraphViewport: activate-node ссылается на отсутствующий node "${nodeId}".`)
454
+ }
455
+ emit('activate-node', nodeId)
456
+ }
457
+
458
+ function bindSigmaEvents(): void {
459
+ if (!sigma || !renderedGraph) {
460
+ throw new Error('SGraphViewport: Sigma event binding требует mounted renderer и graph.')
461
+ }
462
+ sigma.on('enterNode', ({ node, event }) => {
463
+ assertRenderedNode(node, 'hover')
464
+ renderedGraph?.setNodeAttribute(node, 'highlighted', true)
465
+ sigma?.refresh()
466
+ emit('hover-node', { nodeId: node, pointer: { x: event.x, y: event.y } })
467
+ })
468
+ sigma.on('leaveNode', ({ node }) => {
469
+ assertRenderedNode(node, 'leave')
470
+ renderedGraph?.setNodeAttribute(node, 'highlighted', false)
471
+ sigma?.refresh()
472
+ emit('leave-node')
473
+ })
474
+ sigma.on('clickNode', ({ node }) => {
475
+ if (!didDrag) requestSelection({ mode: 'single', nodeId: node })
476
+ })
477
+ sigma.on('clickStage', () => {
478
+ if (!didDrag) requestSelection({ mode: 'none' })
479
+ })
480
+ sigma.on('doubleClickNode', ({ node }) => requestActivation(node))
481
+ sigma.on('downNode', ({ node }) => startNodeDrag(node))
482
+ sigma.getMouseCaptor().on('mousemovebody', updateNodeDrag)
483
+ sigma.getMouseCaptor().on('mouseup', finishNodeDrag)
484
+ sigma.getMouseCaptor().on('mousedown', () => {
485
+ if (!sigma?.getCustomBBox()) sigma?.setCustomBBox(sigma.getBBox())
486
+ })
487
+ }
488
+
489
+ function replaceGraph(): void {
490
+ const startedAt = performance.now()
491
+ let nextGraph: Graph<GraphNodeAttributes, GraphEdgeAttributes>
492
+ try {
493
+ assertViewportConfiguration()
494
+ nextGraph = buildGraph(props.graph)
495
+ } catch (error) {
496
+ failRender('invalid_graph', error instanceof Error ? error.message : 'Граф содержит некорректные данные.')
497
+ return
498
+ }
499
+ try {
500
+ renderedGraph = nextGraph
501
+ if (sigma) {
502
+ sigma.setGraph(nextGraph)
503
+ sigma.setSettings(graphSettings())
504
+ sigma.refresh()
505
+ clearRenderError()
506
+ renderState.value = 'painted'
507
+ emitRenderMetric('graph_replace', startedAt)
508
+ return
509
+ }
510
+ tryMountSigma()
511
+ } catch (error) {
512
+ failRender(
513
+ error instanceof SGraphContractError ? 'invalid_graph' : 'sigma_mount_failed',
514
+ error instanceof Error ? error.message : 'SGraphViewport: не удалось заменить граф.',
515
+ )
516
+ }
517
+ }
518
+
519
+ function refreshValidatedInteraction(): void {
520
+ try {
521
+ assertViewportConfiguration()
522
+ } catch (error) {
523
+ failRender('invalid_graph', error instanceof Error ? error.message : 'SGraphViewport: interaction некорректен.')
524
+ return
525
+ }
526
+ if (resolvedPositioningMode.value === 'readonly') cancelNodeDrag()
527
+ if (!sigma) {
528
+ tryMountSigma()
529
+ return
530
+ }
531
+ sigma.refresh()
532
+ }
533
+
534
+ function refreshValidatedSettings(): void {
535
+ try {
536
+ assertViewportConfiguration()
537
+ } catch (error) {
538
+ failRender('invalid_graph', error instanceof Error ? error.message : 'SGraphViewport: renderer settings некорректны.')
539
+ return
540
+ }
541
+ if (!sigma) {
542
+ tryMountSigma()
543
+ return
544
+ }
545
+ try {
546
+ sigma.setSettings(graphSettings())
547
+ sigma.refresh()
548
+ } catch (error) {
549
+ failRender(
550
+ 'sigma_mount_failed',
551
+ error instanceof Error ? error.message : 'SGraphViewport: не удалось обновить renderer settings.',
552
+ )
553
+ }
554
+ }
555
+
556
+ function isEditablePositioning(): boolean {
557
+ return interactionContract.value.positioning.mode === 'editable'
558
+ }
559
+
560
+ function startNodeDrag(nodeId: string): void {
561
+ if (!isEditablePositioning()) return
562
+ if (!sigma || !renderedGraph) {
563
+ throw new Error('SGraphViewport: pointer positioning требует mounted renderer и graph.')
564
+ }
565
+ assertRenderedNode(nodeId, 'начале drag')
566
+ draggedNodeId = nodeId
567
+ draggedNodeStartPosition = {
568
+ x: Number(renderedGraph.getNodeAttribute(nodeId, 'x')),
569
+ y: Number(renderedGraph.getNodeAttribute(nodeId, 'y')),
570
+ }
571
+ const dragGroupIds = renderedGraph.getNodeAttribute(nodeId, 'dragGroupIds') ?? []
572
+ draggedGroupStartPositions = new Map()
573
+ for (const groupNodeId of [nodeId, ...dragGroupIds]) {
574
+ assertRenderedNode(groupNodeId, 'начале group drag')
575
+ draggedGroupStartPositions.set(groupNodeId, {
576
+ x: Number(renderedGraph.getNodeAttribute(groupNodeId, 'x')),
577
+ y: Number(renderedGraph.getNodeAttribute(groupNodeId, 'y')),
578
+ })
579
+ }
580
+ renderedGraph.setNodeAttribute(nodeId, 'highlighted', true)
581
+ didDrag = false
582
+ }
583
+
584
+ function updateNodeDrag(event: { x: number; y: number; preventSigmaDefault: () => void; original: Event }): void {
585
+ if (!draggedNodeId) return
586
+ if (!isEditablePositioning()) {
587
+ cancelNodeDrag()
588
+ return
589
+ }
590
+ if (!sigma || !renderedGraph || !draggedNodeStartPosition) {
591
+ throw new Error('SGraphViewport: active drag потерял renderer, graph или start position.')
592
+ }
593
+ const position = sigma.viewportToGraph(event)
594
+ const delta = {
595
+ x: position.x - draggedNodeStartPosition.x,
596
+ y: position.y - draggedNodeStartPosition.y,
597
+ }
598
+ if (Math.abs(delta.x) + Math.abs(delta.y) > 0.5) didDrag = true
599
+ for (const [nodeId, startPosition] of draggedGroupStartPositions.entries()) {
600
+ assertRenderedNode(nodeId, 'обновлении drag')
601
+ renderedGraph.setNodeAttribute(nodeId, 'x', startPosition.x + delta.x)
602
+ renderedGraph.setNodeAttribute(nodeId, 'y', startPosition.y + delta.y)
603
+ }
604
+ event.preventSigmaDefault()
605
+ event.original.preventDefault()
606
+ }
607
+
608
+ function finishNodeDrag(): void {
609
+ if (!draggedNodeId) {
610
+ didDrag = false
611
+ return
612
+ }
613
+ if (!isEditablePositioning()) {
614
+ cancelNodeDrag()
615
+ return
616
+ }
617
+ if (!renderedGraph) throw new Error('SGraphViewport: active drag потерял rendered graph при завершении.')
618
+ assertRenderedNode(draggedNodeId, 'завершении drag')
619
+ renderedGraph.setNodeAttribute(draggedNodeId, 'highlighted', false)
620
+ const changes: SGraphPositionChange[] = []
621
+ for (const nodeId of draggedGroupStartPositions.keys()) {
622
+ assertRenderedNode(nodeId, 'завершении group drag')
623
+ changes.push({
624
+ nodeId,
625
+ position: {
626
+ x: Number(renderedGraph.getNodeAttribute(nodeId, 'x')),
627
+ y: Number(renderedGraph.getNodeAttribute(nodeId, 'y')),
628
+ },
629
+ })
630
+ }
631
+ if (didDrag) emit('positions-change', changes)
632
+ draggedNodeId = null
633
+ draggedNodeStartPosition = null
634
+ draggedGroupStartPositions = new Map()
635
+ window.setTimeout(() => { didDrag = false }, 0)
636
+ }
637
+
638
+ function cancelNodeDrag(): void {
639
+ if (renderedGraph && draggedNodeId && renderedGraph.hasNode(draggedNodeId)) {
640
+ renderedGraph.setNodeAttribute(draggedNodeId, 'highlighted', false)
641
+ }
642
+ draggedNodeId = null
643
+ draggedNodeStartPosition = null
644
+ draggedGroupStartPositions = new Map()
645
+ didDrag = false
646
+ }
647
+
648
+ function handleInventorySelection(value: unknown): void {
649
+ if (typeof value !== 'string' || value.trim().length === 0) {
650
+ throw new SGraphContractError('SGraphViewport: SListbox вернул недопустимый node id.')
651
+ }
652
+ requestSelection({ mode: 'single', nodeId: value })
653
+ }
654
+
655
+ function sourcePosition(nodeId: string): SGraphPosition {
656
+ if (renderedGraph?.hasNode(nodeId)) {
657
+ return {
658
+ x: Number(renderedGraph.getNodeAttribute(nodeId, 'x')),
659
+ y: Number(renderedGraph.getNodeAttribute(nodeId, 'y')),
660
+ }
661
+ }
662
+ const node = props.graph.nodes.find((candidate) => candidate.id === nodeId)
663
+ if (!node) throw new Error(`SGraphViewport: node "${nodeId}" отсутствует при keyboard positioning.`)
664
+ return { x: node.x, y: node.y }
665
+ }
666
+
667
+ function nudgeNode(nodeId: string, delta: SGraphPosition): void {
668
+ const sourceNode = props.graph.nodes.find((node) => node.id === nodeId)
669
+ if (!sourceNode) throw new Error(`SGraphViewport: node "${nodeId}" отсутствует при keyboard positioning.`)
670
+ const ids = [sourceNode.id, ...(sourceNode.dragGroupIds ?? [])]
671
+ const changes = ids.map((id): SGraphPositionChange => {
672
+ const position = sourcePosition(id)
673
+ const next = { x: position.x + delta.x, y: position.y + delta.y }
674
+ if (renderedGraph?.hasNode(id)) {
675
+ renderedGraph.setNodeAttribute(id, 'x', next.x)
676
+ renderedGraph.setNodeAttribute(id, 'y', next.y)
677
+ }
678
+ return { nodeId: id, position: next }
679
+ })
680
+ sigma?.refresh()
681
+ requestSelection({ mode: 'single', nodeId })
682
+ emit('positions-change', changes)
683
+ }
684
+
685
+ function handleInventoryOptionKeydown(payload: SListboxOptionKeydownPayload<string>): void {
686
+ const { event, value: nodeId } = payload
687
+ if (nodeId === null) {
688
+ throw new SGraphContractError('SGraphViewport: node inventory не поддерживает clear option.')
689
+ }
690
+ if (event.key === 'Enter' && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
691
+ requestActivation(nodeId)
692
+ return
693
+ }
694
+ const positioning = interactionContract.value.positioning
695
+ if (positioning.mode !== 'editable' || !event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
696
+ const step = positioning.keyboardStep
697
+ const deltas: Partial<Record<string, SGraphPosition>> = {
698
+ ArrowLeft: { x: -step, y: 0 },
699
+ ArrowRight: { x: step, y: 0 },
700
+ ArrowUp: { x: 0, y: -step },
701
+ ArrowDown: { x: 0, y: step },
702
+ }
703
+ const delta = deltas[event.key]
704
+ if (!delta) return
705
+ event.preventDefault()
706
+ event.stopPropagation()
707
+ nudgeNode(nodeId, delta)
708
+ }
709
+
710
+ onMounted(() => {
711
+ assertViewportConfiguration()
712
+ const renderer = rendererRef.value
713
+ if (typeof ResizeObserver !== 'undefined' && renderer) {
714
+ resizeObserver = new ResizeObserver(() => {
715
+ if (!sigma) {
716
+ tryMountSigma()
717
+ return
718
+ }
719
+ sigma.resize()
720
+ sigma.refresh()
721
+ })
722
+ resizeObserver.observe(renderer)
723
+ }
724
+ void nextTick(tryMountSigma)
725
+ })
726
+
727
+ onBeforeUnmount(() => {
728
+ resizeObserver?.disconnect()
729
+ resizeObserver = null
730
+ sigma?.kill()
731
+ sigma = null
732
+ renderedGraph = null
733
+ renderError.value = null
734
+ renderState.value = 'idle'
735
+ cancelNodeDrag()
736
+ })
737
+
738
+ watch(() => props.graph, replaceGraph, { deep: true })
739
+ watch(runtimeTheme, refreshRuntimeTheme)
740
+ watch(() => props.interaction, refreshValidatedInteraction, { deep: true })
741
+ watch(() => props.showEdgeLabels, refreshValidatedSettings)
742
+ watch(() => props.labelZoomThreshold, refreshValidatedSettings)
743
+ watch(() => props.nodeSizeMode, refreshValidatedSettings)
744
+
745
+ function zoomIn(): void {
746
+ const camera = sigma?.getCamera()
747
+ if (!camera) return
748
+ const state = camera.getState()
749
+ camera.animate(
750
+ { ratio: Math.max(state.ratio * 0.82, 0.02) },
751
+ { duration: requiredRuntimeDuration('--s-graph-camera-duration') },
752
+ )
753
+ }
754
+
755
+ function zoomOut(): void {
756
+ const camera = sigma?.getCamera()
757
+ if (!camera) return
758
+ const state = camera.getState()
759
+ camera.animate(
760
+ { ratio: Math.min(state.ratio * 1.18, 12) },
761
+ { duration: requiredRuntimeDuration('--s-graph-camera-duration') },
762
+ )
763
+ }
764
+
765
+ function fit(): void {
766
+ sigma?.setCustomBBox(null)
767
+ sigma?.getCamera().animatedReset({ duration: requiredRuntimeDuration('--s-graph-camera-reset-duration') })
768
+ }
769
+
770
+ defineExpose({ zoomIn, zoomOut, fit })
771
+ </script>
772
+
773
+ <style scoped>
774
+ .s-graph-viewport {
775
+ position: absolute;
776
+ inset: 0;
777
+ min-width: 0;
778
+ min-height: 0;
779
+ overflow: hidden;
780
+ border: var(--s-graph-surface-border-width) solid var(--s-graph-surface-border);
781
+ border-radius: var(--s-graph-surface-radius);
782
+ background: var(--s-graph-surface-background);
783
+ }
784
+
785
+ .s-graph-viewport::before {
786
+ position: absolute;
787
+ z-index: var(--s-graph-grid-layer);
788
+ inset: 0;
789
+ background-image: var(--s-graph-grid-background-image);
790
+ background-position: var(--s-graph-grid-background-position);
791
+ background-size: var(--s-graph-grid-background-size);
792
+ content: "";
793
+ opacity: var(--s-graph-grid-opacity);
794
+ pointer-events: none;
795
+ }
796
+
797
+ .s-graph-viewport__renderer {
798
+ position: absolute;
799
+ z-index: var(--s-graph-renderer-layer);
800
+ inset: 0;
801
+ min-width: 0;
802
+ min-height: 0;
803
+ }
804
+
805
+ .s-graph-viewport__renderer :deep(canvas) {
806
+ border-radius: var(--s-graph-surface-radius);
807
+ }
808
+
809
+ .s-graph-viewport__error {
810
+ position: absolute;
811
+ z-index: var(--s-graph-error-layer);
812
+ inset: var(--s-graph-error-inset);
813
+ display: grid;
814
+ place-items: center;
815
+ border: var(--s-graph-error-border-width) solid var(--s-graph-error-border);
816
+ border-radius: var(--s-graph-error-radius);
817
+ background: var(--s-graph-error-background);
818
+ color: var(--s-graph-error-color);
819
+ font-family: var(--s-graph-label-font-family);
820
+ font-size: var(--s-graph-error-font-size);
821
+ font-weight: var(--s-graph-error-font-weight);
822
+ line-height: var(--s-graph-error-line-height);
823
+ text-align: center;
824
+ padding: var(--s-graph-error-padding);
825
+ }
826
+
827
+ .s-graph-viewport__node-inventory {
828
+ position: absolute;
829
+ z-index: var(--s-graph-inventory-layer);
830
+ inset-block-start: var(--s-graph-inventory-offset);
831
+ inset-inline-start: var(--s-graph-inventory-offset);
832
+ inline-size: min(var(--s-graph-inventory-inline-size), calc(100% - 2 * var(--s-graph-inventory-offset)));
833
+ max-block-size: calc(100% - 2 * var(--s-graph-inventory-offset));
834
+ overflow: auto;
835
+ border: var(--s-graph-inventory-border-width) solid transparent;
836
+ border-radius: var(--s-graph-inventory-radius);
837
+ opacity: 0;
838
+ clip-path: inset(0 0 100% 0);
839
+ pointer-events: none;
840
+ transform: translateY(calc(-1 * var(--s-graph-inventory-reveal-distance)));
841
+ transition:
842
+ opacity var(--s-motion-fast) ease,
843
+ transform var(--s-motion-fast) ease,
844
+ clip-path var(--s-motion-fast) ease;
845
+ }
846
+
847
+ .s-graph-viewport__node-inventory:focus-within,
848
+ .s-graph-viewport__node-inventory--fallback {
849
+ border-color: var(--s-graph-inventory-border);
850
+ background: var(--s-graph-inventory-background);
851
+ box-shadow: var(--s-graph-inventory-shadow);
852
+ opacity: 1;
853
+ clip-path: inset(0);
854
+ padding: var(--s-graph-inventory-padding);
855
+ pointer-events: auto;
856
+ transform: translateY(0);
857
+ }
858
+ </style>