@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.
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/docs/accessibility.md +27 -0
- package/docs/getting-started.md +80 -0
- package/docs/licensing.md +30 -0
- package/docs/public-api.md +39 -0
- package/docs/security.md +21 -0
- package/docs/theming.md +35 -0
- package/docs/versioning.md +23 -0
- package/index.ts +3 -0
- package/package.json +592 -0
- package/src/components/icons/_internal/generatedSputnigUiIconMarkup.ts +971 -0
- package/src/components/icons/sputnigUiIcons.ts +530 -0
- package/src/components/layout/SAppShell.vue +232 -0
- package/src/components/layout/SDockRegion.vue +200 -0
- package/src/components/layout/SResizeHandle.vue +251 -0
- package/src/components/layout/SSideMenuButton.vue +104 -0
- package/src/components/layout/SSideMenuRail.vue +47 -0
- package/src/components/layout/SStack.css +25 -0
- package/src/components/layout/SStack.vue +57 -0
- package/src/components/layout/SStatusBarShell.css +15 -0
- package/src/components/layout/SStatusBarShell.vue +26 -0
- package/src/components/layout/SWorkbenchLayout.vue +53 -0
- package/src/components/layout/SWorkbenchPanelShell.css +161 -0
- package/src/components/layout/SWorkbenchPanelShell.vue +286 -0
- package/src/components/layout/_internal/SAppBrand.css +45 -0
- package/src/components/layout/_internal/SAppBrand.vue +37 -0
- package/src/components/layout/_internal/SAppBrandContent.vue +30 -0
- package/src/components/layout/types.ts +28 -0
- package/src/components/shared/_internal/SDialogSurface.css +127 -0
- package/src/components/shared/_internal/SDialogSurface.vue +117 -0
- package/src/components/shared/_internal/SDisclosureSummary.css +92 -0
- package/src/components/shared/_internal/SDisclosureSummary.vue +121 -0
- package/src/components/shared/_internal/SListboxGroup.css +18 -0
- package/src/components/shared/_internal/SListboxGroup.vue +24 -0
- package/src/components/shared/_internal/SMenuItem.css +112 -0
- package/src/components/shared/_internal/SMenuItem.vue +213 -0
- package/src/components/shared/_internal/SMenuLayer.css +21 -0
- package/src/components/shared/_internal/SMenuLayer.vue +48 -0
- package/src/components/shared/_internal/SMenuSeparator.css +5 -0
- package/src/components/shared/_internal/SMenuSeparator.vue +13 -0
- package/src/components/shared/_internal/SToastItem.css +65 -0
- package/src/components/shared/_internal/SToastItem.vue +58 -0
- package/src/components/shared/_internal/STreeNode.vue +761 -0
- package/src/components/shared/_internal/dialogSurface.ts +105 -0
- package/src/components/shared/_internal/focusTrap.ts +40 -0
- package/src/components/shared/_internal/listboxContext.ts +16 -0
- package/src/components/shared/_internal/markerContract.ts +10 -0
- package/src/components/shared/_internal/menuContext.ts +23 -0
- package/src/components/shared/_internal/selectionContract.ts +239 -0
- package/src/components/shared/_internal/sidebarGroupContext.ts +31 -0
- package/src/components/shared/_internal/tabsContext.ts +57 -0
- package/src/components/shared/_internal/treeRuntime.ts +203 -0
- package/src/components/shared/_internal/useSelectionRoving.ts +162 -0
- package/src/components/shared/codeLanguages.ts +168 -0
- package/src/components/shared/complex/SKanbanCard.css +8 -0
- package/src/components/shared/complex/SKanbanCard.vue +76 -0
- package/src/components/shared/complex/SKanbanLane.vue +82 -0
- package/src/components/shared/complex/STree.css +24 -0
- package/src/components/shared/complex/STree.vue +288 -0
- package/src/components/shared/complex/treeAdapter.ts +210 -0
- package/src/components/shared/complex/types.ts +102 -0
- package/src/components/shared/containers/SCloudChrome.css +103 -0
- package/src/components/shared/containers/SCloudChrome.vue +87 -0
- package/src/components/shared/containers/SCloudChromeEmpty.css +10 -0
- package/src/components/shared/containers/SCloudChromeEmpty.vue +18 -0
- package/src/components/shared/containers/SCloudChromeRow.css +78 -0
- package/src/components/shared/containers/SCloudChromeRow.vue +108 -0
- package/src/components/shared/containers/SCollapsiblePanel.css +57 -0
- package/src/components/shared/containers/SCollapsiblePanel.vue +360 -0
- package/src/components/shared/containers/SDrawer.css +60 -0
- package/src/components/shared/containers/SDrawer.vue +86 -0
- package/src/components/shared/containers/SFormGrid.css +26 -0
- package/src/components/shared/containers/SFormGrid.vue +46 -0
- package/src/components/shared/containers/SFormGridItem.css +18 -0
- package/src/components/shared/containers/SFormGridItem.vue +29 -0
- package/src/components/shared/containers/SLeftSidebar.vue +74 -0
- package/src/components/shared/containers/SModal.css +27 -0
- package/src/components/shared/containers/SModal.vue +105 -0
- package/src/components/shared/containers/SPageHeader.css +66 -0
- package/src/components/shared/containers/SPageHeader.vue +45 -0
- package/src/components/shared/containers/SPanel.css +134 -0
- package/src/components/shared/containers/SPanel.vue +169 -0
- package/src/components/shared/containers/SPopover.css +35 -0
- package/src/components/shared/containers/SPopover.vue +551 -0
- package/src/components/shared/containers/SSidebarGroup.css +12 -0
- package/src/components/shared/containers/SSidebarGroup.vue +443 -0
- package/src/components/shared/containers/SSidebarSection.css +41 -0
- package/src/components/shared/containers/SSidebarSection.vue +268 -0
- package/src/components/shared/containers/disclosureSummary.ts +48 -0
- package/src/components/shared/containers/popover.ts +72 -0
- package/src/components/shared/containers/sidebar.ts +206 -0
- package/src/components/shared/controls/SButton.css +274 -0
- package/src/components/shared/controls/SButton.vue +211 -0
- package/src/components/shared/controls/SCheckbox.css +65 -0
- package/src/components/shared/controls/SCheckbox.vue +130 -0
- package/src/components/shared/controls/SCheckboxGroup.vue +134 -0
- package/src/components/shared/controls/SChoiceCards.css +29 -0
- package/src/components/shared/controls/SChoiceCards.vue +145 -0
- package/src/components/shared/controls/SColorSelect.vue +146 -0
- package/src/components/shared/controls/SComboboxTrigger.css +35 -0
- package/src/components/shared/controls/SComboboxTrigger.vue +122 -0
- package/src/components/shared/controls/SContextMenu.css +11 -0
- package/src/components/shared/controls/SContextMenu.vue +186 -0
- package/src/components/shared/controls/SContextToggleButton.vue +64 -0
- package/src/components/shared/controls/SDragHandle.vue +100 -0
- package/src/components/shared/controls/SDropdownMenu.css +5 -0
- package/src/components/shared/controls/SDropdownMenu.vue +188 -0
- package/src/components/shared/controls/SField.css +79 -0
- package/src/components/shared/controls/SField.vue +89 -0
- package/src/components/shared/controls/SFieldGroup.css +58 -0
- package/src/components/shared/controls/SFieldGroup.vue +117 -0
- package/src/components/shared/controls/SFieldLabel.css +12 -0
- package/src/components/shared/controls/SFieldLabel.vue +51 -0
- package/src/components/shared/controls/SInlineTokenEditor.css +61 -0
- package/src/components/shared/controls/SInlineTokenEditor.vue +1081 -0
- package/src/components/shared/controls/SInputText.css +66 -0
- package/src/components/shared/controls/SInputText.vue +321 -0
- package/src/components/shared/controls/SInteractiveSurface.css +72 -0
- package/src/components/shared/controls/SInteractiveSurface.vue +326 -0
- package/src/components/shared/controls/SLink.css +114 -0
- package/src/components/shared/controls/SLink.ts +85 -0
- package/src/components/shared/controls/SLink.vue +114 -0
- package/src/components/shared/controls/SListbox.css +31 -0
- package/src/components/shared/controls/SListbox.vue +388 -0
- package/src/components/shared/controls/SListboxOption.css +63 -0
- package/src/components/shared/controls/SListboxOption.vue +85 -0
- package/src/components/shared/controls/SMarker.css +19 -0
- package/src/components/shared/controls/SMarker.vue +33 -0
- package/src/components/shared/controls/SMenuSurface.css +20 -0
- package/src/components/shared/controls/SMenuSurface.vue +333 -0
- package/src/components/shared/controls/SSegmentedControl.css +38 -0
- package/src/components/shared/controls/SSegmentedControl.vue +134 -0
- package/src/components/shared/controls/SSelect.css +12 -0
- package/src/components/shared/controls/SSelect.vue +338 -0
- package/src/components/shared/controls/STextarea.css +31 -0
- package/src/components/shared/controls/STextarea.vue +316 -0
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +224 -0
- package/src/components/shared/controls/menu.ts +178 -0
- package/src/components/shared/data-display/SActionCard.css +6 -0
- package/src/components/shared/data-display/SActionCard.vue +49 -0
- package/src/components/shared/data-display/SActionList.css +15 -0
- package/src/components/shared/data-display/SActionList.vue +34 -0
- package/src/components/shared/data-display/SActionListItem.css +114 -0
- package/src/components/shared/data-display/SActionListItem.vue +230 -0
- package/src/components/shared/data-display/SBadge.css +73 -0
- package/src/components/shared/data-display/SBadge.vue +52 -0
- package/src/components/shared/data-display/SCodeBlock.css +56 -0
- package/src/components/shared/data-display/SCodeBlock.vue +188 -0
- package/src/components/shared/data-display/SCodeEditor.css +178 -0
- package/src/components/shared/data-display/SCodeEditor.vue +274 -0
- package/src/components/shared/data-display/SCodeSearchPanel.vue +327 -0
- package/src/components/shared/data-display/SCopyField.vue +77 -0
- package/src/components/shared/data-display/SDocBlock.css +136 -0
- package/src/components/shared/data-display/SDocBlock.vue +358 -0
- package/src/components/shared/data-display/SEmptyState.css +42 -0
- package/src/components/shared/data-display/SEmptyState.vue +79 -0
- package/src/components/shared/data-display/SExpandableText.vue +314 -0
- package/src/components/shared/data-display/SJsonTree.css +6 -0
- package/src/components/shared/data-display/SJsonTree.vue +111 -0
- package/src/components/shared/data-display/SKeyValueGrid.css +37 -0
- package/src/components/shared/data-display/SKeyValueGrid.vue +277 -0
- package/src/components/shared/data-display/SLinkedSystemsList.vue +270 -0
- package/src/components/shared/data-display/SMessage.css +88 -0
- package/src/components/shared/data-display/SMessage.vue +152 -0
- package/src/components/shared/data-display/SMetricCard.css +32 -0
- package/src/components/shared/data-display/SMetricCard.vue +117 -0
- package/src/components/shared/data-display/SProgressBar.css +84 -0
- package/src/components/shared/data-display/SProgressBar.vue +114 -0
- package/src/components/shared/data-display/SProgressIndicator.css +59 -0
- package/src/components/shared/data-display/SProgressIndicator.vue +104 -0
- package/src/components/shared/data-display/SSectionHeader.css +84 -0
- package/src/components/shared/data-display/SSectionHeader.vue +58 -0
- package/src/components/shared/data-display/SStatus.css +90 -0
- package/src/components/shared/data-display/SStatus.vue +79 -0
- package/src/components/shared/data-display/STable.css +83 -0
- package/src/components/shared/data-display/STable.vue +339 -0
- package/src/components/shared/data-display/SToastContainer.css +53 -0
- package/src/components/shared/data-display/SToastContainer.vue +31 -0
- package/src/components/shared/data-display/STooltip.css +114 -0
- package/src/components/shared/data-display/STooltip.vue +550 -0
- package/src/components/shared/data-display/STooltipAnchor.css +21 -0
- package/src/components/shared/data-display/STooltipAnchor.vue +119 -0
- package/src/components/shared/data-display/SVirtualList.css +24 -0
- package/src/components/shared/data-display/SVirtualList.ts +19 -0
- package/src/components/shared/data-display/SVirtualList.vue +361 -0
- package/src/components/shared/data-display/json.ts +136 -0
- package/src/components/shared/data-display/table.ts +76 -0
- package/src/components/shared/database/SDataGrid.css +3 -0
- package/src/components/shared/database/SDataGrid.vue +464 -0
- package/src/components/shared/database/SSqlEditor.vue +363 -0
- package/src/components/shared/database/dataGrid.ts +61 -0
- package/src/components/shared/feedback/SAsyncState.css +26 -0
- package/src/components/shared/feedback/SAsyncState.vue +190 -0
- package/src/components/shared/graph/SGraphViewport.ts +1 -0
- package/src/components/shared/graph/SGraphViewport.vue +858 -0
- package/src/components/shared/graph/_internal/graphViewportContract.ts +362 -0
- package/src/components/shared/markdown.ts +169 -0
- package/src/components/shared/navigation/SBottomNav.vue +692 -0
- package/src/components/shared/navigation/SBreadcrumbs.css +26 -0
- package/src/components/shared/navigation/SBreadcrumbs.vue +155 -0
- package/src/components/shared/navigation/SCatalogNavigator.css +100 -0
- package/src/components/shared/navigation/SCatalogNavigator.vue +308 -0
- package/src/components/shared/navigation/STab.vue +363 -0
- package/src/components/shared/navigation/STabList.vue +442 -0
- package/src/components/shared/navigation/STabPanel.vue +165 -0
- package/src/components/shared/navigation/STabPanels.vue +33 -0
- package/src/components/shared/navigation/STabs.vue +617 -0
- package/src/components/shared/navigation/SWizardSteps.css +69 -0
- package/src/components/shared/navigation/SWizardSteps.vue +171 -0
- package/src/components/shared/navigation/_internal/SCatalogNavigatorGroup.vue +63 -0
- package/src/components/shared/persona/SPersonaProjectPicker.css +36 -0
- package/src/components/shared/persona/SPersonaProjectPicker.vue +361 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.css +109 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.vue +549 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.css +40 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +298 -0
- package/src/components/shared/persona/_internal/inventory.ts +110 -0
- package/src/components/shared/persona/_internal/labels.ts +79 -0
- package/src/components/shared/persona/presentation.ts +371 -0
- package/src/components/shared/persona/types.ts +256 -0
- package/src/composables/uiPreferencesReset.ts +52 -0
- package/src/composables/useClipboard.ts +37 -0
- package/src/composables/useDialogOverlay.ts +85 -0
- package/src/composables/useDisclosure.ts +40 -0
- package/src/composables/useFloatingPosition.ts +245 -0
- package/src/composables/useNotifier.ts +26 -0
- package/src/composables/useSidebarPanelState.ts +610 -0
- package/src/internal/codeEditorContract.ts +110 -0
- package/src/internal/codeEditorExtensions.ts +27 -0
- package/src/internal/feedbackPresentation.ts +136 -0
- package/src/internal/fieldSurface.ts +59 -0
- package/src/internal/focusNavigation.ts +69 -0
- package/src/internal/inlineTokenEditorContract.ts +136 -0
- package/src/internal/interactiveElement.ts +33 -0
- package/src/internal/layerStack.ts +202 -0
- package/src/internal/linkTarget.ts +256 -0
- package/src/internal/ownedAttrs.ts +464 -0
- package/src/internal/passiveContentContract.ts +129 -0
- package/src/internal/pointerInteractionLease.ts +81 -0
- package/src/internal/runtimeContract.ts +194 -0
- package/src/internal/runtimeTheme.ts +159 -0
- package/src/internal/semanticSizing.ts +60 -0
- package/src/internal/useMenuLayer.ts +129 -0
- package/src/stores/useNotifierStore.ts +69 -0
- package/src/styles/reference.css +4 -0
- package/src/styles/style.css +151 -0
- package/src/styles/tokens.css +836 -0
- package/src/theme.ts +449 -0
- package/src/types/highlight-js-lib.d.ts +76 -0
- 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
|
+
}
|