@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,362 @@
1
+ /** Поддерживаемые семантические тона графа. / Supported semantic graph tones. */
2
+ export const S_GRAPH_TONES = [
3
+ 'neutral',
4
+ 'primary',
5
+ 'info',
6
+ 'success',
7
+ 'warning',
8
+ 'danger',
9
+ 'accent',
10
+ ] as const
11
+
12
+ /** Семантический тон, разрешаемый UI-kit в theme tokens. / Theme-token-resolved semantic tone. */
13
+ export type SGraphTone = (typeof S_GRAPH_TONES)[number]
14
+
15
+ /** Семантическая роль узла в раскладке. / Semantic node role in the layout. */
16
+ export type SGraphNodeRole = 'item' | 'group' | 'coverage' | 'relation'
17
+
18
+ /** Точная позиция узла в координатах графа. / Exact position in graph coordinates. */
19
+ export interface SGraphPosition {
20
+ x: number
21
+ y: number
22
+ }
23
+
24
+ /** Публичные данные узла без presentation colors. / Public node data without presentation colors. */
25
+ export interface SGraphNode {
26
+ id: string
27
+ label: string
28
+ role: SGraphNodeRole
29
+ kind: string
30
+ group?: string | null
31
+ tone: SGraphTone
32
+ size: number
33
+ x: number
34
+ y: number
35
+ count?: number | null
36
+ forceLabel?: boolean
37
+ dragGroupIds?: readonly string[]
38
+ payload?: Record<string, unknown> | null
39
+ }
40
+
41
+ /** Публичные данные ребра без presentation colors. / Public edge data without presentation colors. */
42
+ export interface SGraphEdge {
43
+ id: string
44
+ source: string
45
+ target: string
46
+ relationType: string
47
+ tone: SGraphTone
48
+ size?: number
49
+ opacity?: number
50
+ hidden?: boolean
51
+ showOnInteractionOnly?: boolean
52
+ payload?: Record<string, unknown> | null
53
+ }
54
+
55
+ /** Полный immutable input графа. / Complete immutable graph input. */
56
+ export interface SGraphViewportGraph {
57
+ nodes: readonly SGraphNode[]
58
+ edges: readonly SGraphEdge[]
59
+ }
60
+
61
+ /** Controlled отсутствие или один выбранный узел. / Controlled none-or-single node selection. */
62
+ export type SGraphSelection =
63
+ | { mode: 'none' }
64
+ | { mode: 'single'; nodeId: string }
65
+
66
+ /** Readonly или editable позиционирование с точным keyboard step. / Readonly or editable positioning with an exact keyboard step. */
67
+ export type SGraphPositioning =
68
+ | { mode: 'readonly' }
69
+ | { mode: 'editable'; keyboardStep: number }
70
+
71
+ /** Единый controlled interaction-контракт viewport. / Unified controlled viewport interaction contract. */
72
+ export interface SGraphViewportInteraction {
73
+ selection: SGraphSelection
74
+ positioning: SGraphPositioning
75
+ }
76
+
77
+ /** Ошибка точного runtime-контракта графа. / Exact graph runtime contract error. */
78
+ export class SGraphContractError extends TypeError {
79
+ override readonly name = 'SGraphContractError'
80
+ }
81
+
82
+ const graphNodeRoles = new Set<SGraphNodeRole>(['item', 'group', 'coverage', 'relation'])
83
+ const graphTones = new Set<SGraphTone>(S_GRAPH_TONES)
84
+
85
+ function graphContractError(path: string, message: string): SGraphContractError {
86
+ return new SGraphContractError(`SGraphViewport: ${path} ${message}`)
87
+ }
88
+
89
+ function assertRecord(value: unknown, path: string): asserts value is Record<string, unknown> {
90
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
91
+ throw graphContractError(path, 'должен быть object.')
92
+ }
93
+ }
94
+
95
+ function assertExactKeys(value: Record<string, unknown>, allowed: readonly string[], path: string): void {
96
+ const unexpected = Object.keys(value).filter((key) => !allowed.includes(key))
97
+ if (unexpected.length > 0) {
98
+ throw graphContractError(path, `содержит неизвестные поля: ${unexpected.join(', ')}.`)
99
+ }
100
+ }
101
+
102
+ function assertNonEmptyString(value: unknown, path: string): asserts value is string {
103
+ if (typeof value !== 'string' || value.trim().length === 0) {
104
+ throw graphContractError(path, 'должен быть непустой строкой.')
105
+ }
106
+ }
107
+
108
+ function assertFiniteNumber(value: unknown, path: string): asserts value is number {
109
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
110
+ throw graphContractError(path, 'должен быть конечным числом.')
111
+ }
112
+ }
113
+
114
+ function assertTone(value: unknown, path: string): asserts value is SGraphTone {
115
+ if (!graphTones.has(value as SGraphTone)) {
116
+ throw graphContractError(path, `имеет неизвестное значение "${String(value)}".`)
117
+ }
118
+ }
119
+
120
+ const nodeKeys = [
121
+ 'id',
122
+ 'label',
123
+ 'role',
124
+ 'kind',
125
+ 'group',
126
+ 'tone',
127
+ 'size',
128
+ 'x',
129
+ 'y',
130
+ 'count',
131
+ 'forceLabel',
132
+ 'dragGroupIds',
133
+ 'payload',
134
+ ] as const
135
+
136
+ const edgeKeys = [
137
+ 'id',
138
+ 'source',
139
+ 'target',
140
+ 'relationType',
141
+ 'tone',
142
+ 'size',
143
+ 'opacity',
144
+ 'hidden',
145
+ 'showOnInteractionOnly',
146
+ 'payload',
147
+ ] as const
148
+
149
+ function assertGraphNode(value: unknown, index: number): asserts value is SGraphNode {
150
+ const path = `graph.nodes[${index}]`
151
+ assertRecord(value, path)
152
+ assertExactKeys(value, nodeKeys, path)
153
+ assertNonEmptyString(value.id, `${path}.id`)
154
+ assertNonEmptyString(value.label, `${path}.label`)
155
+ if (!graphNodeRoles.has(value.role as SGraphNodeRole)) {
156
+ throw graphContractError(`${path}.role`, `имеет неизвестное значение "${String(value.role)}".`)
157
+ }
158
+ assertNonEmptyString(value.kind, `${path}.kind`)
159
+ assertTone(value.tone, `${path}.tone`)
160
+ assertFiniteNumber(value.size, `${path}.size`)
161
+ if (value.size <= 0) throw graphContractError(`${path}.size`, 'должен быть больше 0.')
162
+ assertFiniteNumber(value.x, `${path}.x`)
163
+ assertFiniteNumber(value.y, `${path}.y`)
164
+ if (value.group !== undefined && value.group !== null) {
165
+ assertNonEmptyString(value.group, `${path}.group`)
166
+ }
167
+ if (value.count !== undefined && value.count !== null) {
168
+ assertFiniteNumber(value.count, `${path}.count`)
169
+ if (!Number.isInteger(value.count) || value.count < 0) {
170
+ throw graphContractError(`${path}.count`, 'должен быть неотрицательным целым числом.')
171
+ }
172
+ }
173
+ if (value.forceLabel !== undefined && typeof value.forceLabel !== 'boolean') {
174
+ throw graphContractError(`${path}.forceLabel`, 'должен быть boolean.')
175
+ }
176
+ if (value.dragGroupIds !== undefined && !Array.isArray(value.dragGroupIds)) {
177
+ throw graphContractError(`${path}.dragGroupIds`, 'должен быть массивом node id.')
178
+ }
179
+ if (value.payload !== undefined && value.payload !== null) {
180
+ assertRecord(value.payload, `${path}.payload`)
181
+ }
182
+ }
183
+
184
+ function assertGraphEdge(value: unknown, index: number): asserts value is SGraphEdge {
185
+ const path = `graph.edges[${index}]`
186
+ assertRecord(value, path)
187
+ assertExactKeys(value, edgeKeys, path)
188
+ assertNonEmptyString(value.id, `${path}.id`)
189
+ assertNonEmptyString(value.source, `${path}.source`)
190
+ assertNonEmptyString(value.target, `${path}.target`)
191
+ assertNonEmptyString(value.relationType, `${path}.relationType`)
192
+ assertTone(value.tone, `${path}.tone`)
193
+ if (value.size !== undefined) {
194
+ assertFiniteNumber(value.size, `${path}.size`)
195
+ if (value.size <= 0) throw graphContractError(`${path}.size`, 'должен быть больше 0.')
196
+ }
197
+ if (value.opacity !== undefined) {
198
+ assertFiniteNumber(value.opacity, `${path}.opacity`)
199
+ if (value.opacity < 0 || value.opacity > 1) {
200
+ throw graphContractError(`${path}.opacity`, 'должен находиться в диапазоне 0..1.')
201
+ }
202
+ }
203
+ for (const key of ['hidden', 'showOnInteractionOnly'] as const) {
204
+ if (value[key] !== undefined && typeof value[key] !== 'boolean') {
205
+ throw graphContractError(`${path}.${key}`, 'должен быть boolean.')
206
+ }
207
+ }
208
+ if (value.payload !== undefined && value.payload !== null) {
209
+ assertRecord(value.payload, `${path}.payload`)
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Проверяет точный граф до передачи в Graphology/Sigma без silent skip, clamp или merge.
215
+ * Validates the exact graph before Graphology/Sigma without silent skips, clamps, or merges.
216
+ */
217
+ export function assertSGraphViewportGraph(value: unknown): asserts value is SGraphViewportGraph {
218
+ assertRecord(value, 'graph')
219
+ assertExactKeys(value, ['nodes', 'edges'], 'graph')
220
+ if (!Array.isArray(value.nodes)) throw graphContractError('graph.nodes', 'должен быть массивом.')
221
+ if (!Array.isArray(value.edges)) throw graphContractError('graph.edges', 'должен быть массивом.')
222
+
223
+ const nodeIds = new Set<string>()
224
+ value.nodes.forEach((node, index) => {
225
+ assertGraphNode(node, index)
226
+ if (nodeIds.has(node.id)) {
227
+ throw graphContractError(`graph.nodes[${index}].id`, `дублирует node id "${node.id}".`)
228
+ }
229
+ nodeIds.add(node.id)
230
+ })
231
+
232
+ value.nodes.forEach((node, nodeIndex) => {
233
+ if (!node.dragGroupIds) return
234
+ const groupIds = new Set<string>()
235
+ node.dragGroupIds.forEach((groupId: string, groupIndex: number) => {
236
+ assertNonEmptyString(groupId, `graph.nodes[${nodeIndex}].dragGroupIds[${groupIndex}]`)
237
+ if (groupId === node.id) {
238
+ throw graphContractError(`graph.nodes[${nodeIndex}].dragGroupIds[${groupIndex}]`, 'не может ссылаться на сам узел.')
239
+ }
240
+ if (!nodeIds.has(groupId)) {
241
+ throw graphContractError(
242
+ `graph.nodes[${nodeIndex}].dragGroupIds[${groupIndex}]`,
243
+ `ссылается на отсутствующий node "${groupId}".`,
244
+ )
245
+ }
246
+ if (groupIds.has(groupId)) {
247
+ throw graphContractError(
248
+ `graph.nodes[${nodeIndex}].dragGroupIds[${groupIndex}]`,
249
+ `дублирует node id "${groupId}".`,
250
+ )
251
+ }
252
+ groupIds.add(groupId)
253
+ })
254
+ })
255
+
256
+ const edgeIds = new Set<string>()
257
+ value.edges.forEach((edge, index) => {
258
+ assertGraphEdge(edge, index)
259
+ if (edgeIds.has(edge.id)) {
260
+ throw graphContractError(`graph.edges[${index}].id`, `дублирует edge id "${edge.id}".`)
261
+ }
262
+ edgeIds.add(edge.id)
263
+ if (!nodeIds.has(edge.source)) {
264
+ throw graphContractError(`graph.edges[${index}].source`, `ссылается на отсутствующий node "${edge.source}".`)
265
+ }
266
+ if (!nodeIds.has(edge.target)) {
267
+ throw graphContractError(`graph.edges[${index}].target`, `ссылается на отсутствующий node "${edge.target}".`)
268
+ }
269
+ })
270
+ }
271
+
272
+ /**
273
+ * Проверяет controlled selection и positioning как один exact interaction-контракт.
274
+ * Validates controlled selection and positioning as one exact interaction contract.
275
+ */
276
+ export function assertSGraphViewportInteraction(
277
+ value: unknown,
278
+ graph: SGraphViewportGraph,
279
+ ): asserts value is SGraphViewportInteraction {
280
+ assertRecord(value, 'interaction')
281
+ assertExactKeys(value, ['selection', 'positioning'], 'interaction')
282
+
283
+ assertRecord(value.selection, 'interaction.selection')
284
+ if (value.selection.mode === 'none') {
285
+ assertExactKeys(value.selection, ['mode'], 'interaction.selection')
286
+ } else if (value.selection.mode === 'single') {
287
+ assertExactKeys(value.selection, ['mode', 'nodeId'], 'interaction.selection')
288
+ assertNonEmptyString(value.selection.nodeId, 'interaction.selection.nodeId')
289
+ const selectedNodeId = value.selection.nodeId
290
+ if (!graph.nodes.some((node) => node.id === selectedNodeId)) {
291
+ throw graphContractError(
292
+ 'interaction.selection.nodeId',
293
+ `ссылается на отсутствующий node "${selectedNodeId}".`,
294
+ )
295
+ }
296
+ } else {
297
+ throw graphContractError(
298
+ 'interaction.selection.mode',
299
+ `имеет неизвестное значение "${String(value.selection.mode)}".`,
300
+ )
301
+ }
302
+
303
+ assertRecord(value.positioning, 'interaction.positioning')
304
+ if (value.positioning.mode === 'readonly') {
305
+ assertExactKeys(value.positioning, ['mode'], 'interaction.positioning')
306
+ } else if (value.positioning.mode === 'editable') {
307
+ assertExactKeys(value.positioning, ['mode', 'keyboardStep'], 'interaction.positioning')
308
+ assertFiniteNumber(value.positioning.keyboardStep, 'interaction.positioning.keyboardStep')
309
+ if (value.positioning.keyboardStep <= 0) {
310
+ throw graphContractError('interaction.positioning.keyboardStep', 'должен быть больше 0.')
311
+ }
312
+ } else {
313
+ throw graphContractError(
314
+ 'interaction.positioning.mode',
315
+ `имеет неизвестное значение "${String(value.positioning.mode)}".`,
316
+ )
317
+ }
318
+ }
319
+
320
+ /** Режим масштабирования размеров узлов Sigma. / Sigma node-size scaling mode. */
321
+ export type SGraphNodeSizeMode = 'screen' | 'graph'
322
+
323
+ /** Измеряемая фаза renderer lifecycle. / Measured renderer lifecycle phase. */
324
+ export type SGraphRenderMetricPhase = 'sigma_mount' | 'graph_replace'
325
+
326
+ /** Метрика завершённой renderer-фазы. / Completed renderer-phase metric. */
327
+ export interface SGraphRenderMetric {
328
+ phase: SGraphRenderMetricPhase
329
+ durationMs: number
330
+ nodes: number
331
+ edges: number
332
+ renderer: 'sigma'
333
+ }
334
+
335
+ /** Машиночитаемая причина отказа renderer. / Machine-readable renderer failure reason. */
336
+ export type SGraphRenderErrorReason = 'webgl_unavailable' | 'invalid_graph' | 'sigma_mount_failed'
337
+
338
+ /** Диагностируемый отказ renderer с размером входа. / Diagnosable renderer failure with input size. */
339
+ export interface SGraphRenderError {
340
+ reason: SGraphRenderErrorReason
341
+ message: string
342
+ nodes: number
343
+ edges: number
344
+ }
345
+
346
+ /** Координаты pointer относительно viewport. / Pointer coordinates relative to the viewport. */
347
+ export interface SGraphNodePointer {
348
+ x: number
349
+ y: number
350
+ }
351
+
352
+ /** Hover-событие узла с pointer coordinates. / Node hover event with pointer coordinates. */
353
+ export interface SGraphNodePointerEvent {
354
+ nodeId: string
355
+ pointer: SGraphNodePointer
356
+ }
357
+
358
+ /** Controlled изменение позиции одного узла. / Controlled position change for one node. */
359
+ export interface SGraphPositionChange {
360
+ nodeId: string
361
+ position: SGraphPosition
362
+ }
@@ -0,0 +1,169 @@
1
+ import DOMPurify from 'dompurify'
2
+ import { marked } from 'marked'
3
+
4
+ /** Закрытый tag contract очищенного document content. / Closed tag contract for sanitized document content. */
5
+ export const SANITIZED_DOCUMENT_TAGS = [
6
+ 'h1',
7
+ 'h2',
8
+ 'h3',
9
+ 'h4',
10
+ 'h5',
11
+ 'h6',
12
+ 'p',
13
+ 'br',
14
+ 'hr',
15
+ 'strong',
16
+ 'em',
17
+ 'b',
18
+ 'i',
19
+ 'u',
20
+ 's',
21
+ 'span',
22
+ 'ul',
23
+ 'ol',
24
+ 'li',
25
+ 'blockquote',
26
+ 'code',
27
+ 'pre',
28
+ 'a',
29
+ 'img',
30
+ 'table',
31
+ 'thead',
32
+ 'tbody',
33
+ 'tr',
34
+ 'th',
35
+ 'td',
36
+ 'div',
37
+ 'section',
38
+ 'article',
39
+ 'header',
40
+ 'footer',
41
+ 'sup',
42
+ 'sub',
43
+ 'small',
44
+ 'mark',
45
+ 'del',
46
+ 'ins',
47
+ ] as const
48
+
49
+ /** Закрытый attribute contract очищенного document content. / Closed attribute contract for sanitized document content. */
50
+ export const SANITIZED_DOCUMENT_ATTRIBUTES = [
51
+ 'href',
52
+ 'title',
53
+ 'src',
54
+ 'alt',
55
+ 'class',
56
+ 'id',
57
+ 'colspan',
58
+ 'rowspan',
59
+ ] as const
60
+
61
+ /** Атрибуты, доступные typed renderer после presentation mapping. / Attributes available to the typed renderer after presentation mapping. */
62
+ export const SANITIZED_DOCUMENT_RENDER_ATTRIBUTES = [
63
+ ...SANITIZED_DOCUMENT_ATTRIBUTES.filter((attribute) => attribute !== 'id'),
64
+ 'data-s-doc-anchor',
65
+ 'data-s-doc-align',
66
+ ] as const
67
+
68
+ const domPurifyConfig = {
69
+ ALLOWED_TAGS: [...SANITIZED_DOCUMENT_TAGS],
70
+ ALLOWED_ATTR: [...SANITIZED_DOCUMENT_ATTRIBUTES],
71
+ FORBID_TAGS: [
72
+ 'script',
73
+ 'style',
74
+ 'link',
75
+ 'meta',
76
+ 'iframe',
77
+ 'object',
78
+ 'embed',
79
+ 'form',
80
+ 'input',
81
+ 'button',
82
+ 'select',
83
+ 'textarea',
84
+ ],
85
+ FORBID_ATTR: [
86
+ 'style',
87
+ 'width',
88
+ 'height',
89
+ 'target',
90
+ 'rel',
91
+ 'onerror',
92
+ 'onload',
93
+ 'onclick',
94
+ 'onmouseover',
95
+ 'onmouseout',
96
+ 'onfocus',
97
+ 'onblur',
98
+ 'onchange',
99
+ 'onsubmit',
100
+ ],
101
+ ALLOW_DATA_ATTR: false,
102
+ USE_PROFILES: { html: true },
103
+ }
104
+
105
+ const generatedCodeClass = /^(?:hljs|hljs-[a-z0-9_-]+|language-[a-z0-9-]+)$/
106
+
107
+ function mapSanitizedDocumentPresentation(html: string): string {
108
+ const template = document.createElement('template')
109
+ template.innerHTML = html
110
+
111
+ for (const cell of template.content.querySelectorAll<HTMLTableCellElement>('th[align], td[align]')) {
112
+ const alignment = cell.getAttribute('align')?.trim().toLowerCase()
113
+ cell.removeAttribute('align')
114
+ if (alignment === 'left' || alignment === 'center' || alignment === 'right') {
115
+ cell.dataset.sDocAlign = alignment
116
+ }
117
+ }
118
+
119
+ for (const element of template.content.querySelectorAll<HTMLElement>('[align]')) {
120
+ element.removeAttribute('align')
121
+ }
122
+
123
+ for (const element of template.content.querySelectorAll<HTMLElement>('[id]')) {
124
+ const anchor = element.getAttribute('id')?.trim()
125
+ element.removeAttribute('id')
126
+ if (anchor) element.dataset.sDocAnchor = anchor
127
+ }
128
+
129
+ for (const element of template.content.querySelectorAll<HTMLElement>('[class]')) {
130
+ const allowsGeneratedClass = element.matches('code, code span')
131
+ const classes = allowsGeneratedClass
132
+ ? [...element.classList].filter((className) => generatedCodeClass.test(className))
133
+ : []
134
+ if (classes.length === 0) element.removeAttribute('class')
135
+ else element.className = classes.join(' ')
136
+ }
137
+
138
+ for (const anchor of template.content.querySelectorAll<HTMLAnchorElement>('a:not([href])')) {
139
+ anchor.replaceWith(...anchor.childNodes)
140
+ }
141
+
142
+ return template.innerHTML
143
+ }
144
+
145
+ /**
146
+ * Очищает document HTML и отображает разрешённые presentation hints в закрытый контракт.
147
+ * Sanitizes document HTML and maps allowed presentation hints into a closed contract.
148
+ */
149
+ export function sanitizeDocumentHtml(content: string): string {
150
+ if (typeof content !== 'string') {
151
+ throw new TypeError('sanitizeDocumentHtml: content must be a string')
152
+ }
153
+ return mapSanitizedDocumentPresentation(DOMPurify.sanitize(content, domPurifyConfig))
154
+ }
155
+
156
+ export function renderMarkdownToSafeHtml(content: string): string {
157
+ if (typeof content !== 'string') {
158
+ throw new TypeError(
159
+ 'renderMarkdownToSafeHtml: content должен быть строкой. '
160
+ + '/ renderMarkdownToSafeHtml: content must be a string',
161
+ )
162
+ }
163
+ const rendered = marked.parse(content, {
164
+ gfm: true,
165
+ breaks: true,
166
+ }) as string
167
+
168
+ return sanitizeDocumentHtml(rendered)
169
+ }