@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,464 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useAttrs,
|
|
3
|
+
type AnchorHTMLAttributes,
|
|
4
|
+
type HTMLAttributes,
|
|
5
|
+
type InputHTMLAttributes,
|
|
6
|
+
type TextareaHTMLAttributes,
|
|
7
|
+
} from 'vue'
|
|
8
|
+
|
|
9
|
+
type OwnedAttributeValueKind =
|
|
10
|
+
| 'aria'
|
|
11
|
+
| 'aria-boolean'
|
|
12
|
+
| 'aria-current'
|
|
13
|
+
| 'aria-haspopup'
|
|
14
|
+
| 'aria-pressed'
|
|
15
|
+
| 'boolean-attribute'
|
|
16
|
+
| 'data'
|
|
17
|
+
| 'input-mode'
|
|
18
|
+
| 'listener'
|
|
19
|
+
| 'string'
|
|
20
|
+
| 'string-or-boolean'
|
|
21
|
+
| 'string-or-number'
|
|
22
|
+
|
|
23
|
+
type OwnedAttributeSchema = Readonly<Record<string, OwnedAttributeValueKind>>
|
|
24
|
+
|
|
25
|
+
function defineOwnedAttributeSchema<const Schema extends OwnedAttributeSchema>(schema: Schema): Readonly<Schema> {
|
|
26
|
+
return Object.freeze(schema)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Exact runtime allowlist каждого public owner.
|
|
31
|
+
* Exact runtime allowlist for every public owner.
|
|
32
|
+
*
|
|
33
|
+
* Пустая схема означает, что компонент не принимает fallthrough attrs: его API целиком выражен props/emits/slots.
|
|
34
|
+
* An empty schema means that the component accepts no fallthrough attrs: props/emits/slots express its entire API.
|
|
35
|
+
*/
|
|
36
|
+
export const ownedAttributeSchemas = Object.freeze({
|
|
37
|
+
SAppShell: defineOwnedAttributeSchema({}),
|
|
38
|
+
SDockRegion: defineOwnedAttributeSchema({}),
|
|
39
|
+
SResizeHandle: defineOwnedAttributeSchema({
|
|
40
|
+
'data-test-id': 'data',
|
|
41
|
+
}),
|
|
42
|
+
SSideMenuButton: defineOwnedAttributeSchema({
|
|
43
|
+
'data-testid': 'data',
|
|
44
|
+
}),
|
|
45
|
+
SSideMenuRail: defineOwnedAttributeSchema({
|
|
46
|
+
'data-testid': 'data',
|
|
47
|
+
}),
|
|
48
|
+
SStack: defineOwnedAttributeSchema({}),
|
|
49
|
+
SStatusBarShell: defineOwnedAttributeSchema({}),
|
|
50
|
+
SWorkbenchLayout: defineOwnedAttributeSchema({}),
|
|
51
|
+
SWorkbenchPanelShell: defineOwnedAttributeSchema({}),
|
|
52
|
+
SKanbanCard: defineOwnedAttributeSchema({
|
|
53
|
+
'data-testid': 'data',
|
|
54
|
+
}),
|
|
55
|
+
SKanbanLane: defineOwnedAttributeSchema({}),
|
|
56
|
+
STree: defineOwnedAttributeSchema({
|
|
57
|
+
'data-testid': 'data',
|
|
58
|
+
}),
|
|
59
|
+
SCloudChrome: defineOwnedAttributeSchema({
|
|
60
|
+
'data-chat-chrome-drop-zone': 'data',
|
|
61
|
+
'data-testid': 'data',
|
|
62
|
+
}),
|
|
63
|
+
SCloudChromeEmpty: defineOwnedAttributeSchema({}),
|
|
64
|
+
SCloudChromeRow: defineOwnedAttributeSchema({}),
|
|
65
|
+
SCollapsiblePanel: defineOwnedAttributeSchema({
|
|
66
|
+
'data-testid': 'data',
|
|
67
|
+
}),
|
|
68
|
+
SDrawer: defineOwnedAttributeSchema({}),
|
|
69
|
+
SFormGrid: defineOwnedAttributeSchema({}),
|
|
70
|
+
SFormGridItem: defineOwnedAttributeSchema({}),
|
|
71
|
+
SLeftSidebar: defineOwnedAttributeSchema({}),
|
|
72
|
+
SModal: defineOwnedAttributeSchema({
|
|
73
|
+
'data-testid': 'data',
|
|
74
|
+
}),
|
|
75
|
+
SPageHeader: defineOwnedAttributeSchema({}),
|
|
76
|
+
SPanel: defineOwnedAttributeSchema({
|
|
77
|
+
'data-testid': 'data',
|
|
78
|
+
}),
|
|
79
|
+
SPopover: defineOwnedAttributeSchema({
|
|
80
|
+
id: 'string',
|
|
81
|
+
'aria-label': 'string',
|
|
82
|
+
'data-testid': 'data',
|
|
83
|
+
onClick: 'listener',
|
|
84
|
+
}),
|
|
85
|
+
SSidebarGroup: defineOwnedAttributeSchema({}),
|
|
86
|
+
SSidebarSection: defineOwnedAttributeSchema({}),
|
|
87
|
+
SButton: defineOwnedAttributeSchema({
|
|
88
|
+
'aria-controls': 'string',
|
|
89
|
+
'aria-describedby': 'string',
|
|
90
|
+
'aria-current': 'aria-current',
|
|
91
|
+
'aria-expanded': 'aria-boolean',
|
|
92
|
+
'aria-haspopup': 'aria-haspopup',
|
|
93
|
+
'aria-hidden': 'aria-boolean',
|
|
94
|
+
'aria-keyshortcuts': 'string',
|
|
95
|
+
'aria-label': 'string',
|
|
96
|
+
'aria-pressed': 'aria-pressed',
|
|
97
|
+
'aria-selected': 'aria-boolean',
|
|
98
|
+
'data-s-popover-trigger': 'data',
|
|
99
|
+
'data-s-drag-handle': 'data',
|
|
100
|
+
'data-sidebar-section-toggle': 'data',
|
|
101
|
+
'data-state': 'data',
|
|
102
|
+
'data-test': 'data',
|
|
103
|
+
'data-testid': 'data',
|
|
104
|
+
'data-tree-selection-control': 'data',
|
|
105
|
+
autofocus: 'boolean-attribute',
|
|
106
|
+
id: 'string',
|
|
107
|
+
onClick: 'listener',
|
|
108
|
+
onFocusin: 'listener',
|
|
109
|
+
onFocusout: 'listener',
|
|
110
|
+
onKeydown: 'listener',
|
|
111
|
+
onMousedown: 'listener',
|
|
112
|
+
onPointercancel: 'listener',
|
|
113
|
+
onPointerdown: 'listener',
|
|
114
|
+
onPointerenter: 'listener',
|
|
115
|
+
onPointerleave: 'listener',
|
|
116
|
+
onPointerup: 'listener',
|
|
117
|
+
role: 'string',
|
|
118
|
+
tabindex: 'string-or-number',
|
|
119
|
+
title: 'string',
|
|
120
|
+
}),
|
|
121
|
+
SCheckbox: defineOwnedAttributeSchema({
|
|
122
|
+
'aria-label': 'string',
|
|
123
|
+
'aria-describedby': 'string',
|
|
124
|
+
'data-testid': 'data',
|
|
125
|
+
onClick: 'listener',
|
|
126
|
+
onPointerdown: 'listener',
|
|
127
|
+
tabindex: 'string-or-number',
|
|
128
|
+
}),
|
|
129
|
+
SCheckboxGroup: defineOwnedAttributeSchema({
|
|
130
|
+
'aria-describedby': 'string',
|
|
131
|
+
'data-testid': 'data',
|
|
132
|
+
}),
|
|
133
|
+
SChoiceCards: defineOwnedAttributeSchema({
|
|
134
|
+
'aria-describedby': 'string',
|
|
135
|
+
'data-testid': 'data',
|
|
136
|
+
}),
|
|
137
|
+
SColorSelect: defineOwnedAttributeSchema({
|
|
138
|
+
'aria-label': 'string',
|
|
139
|
+
}),
|
|
140
|
+
SDragHandle: defineOwnedAttributeSchema({
|
|
141
|
+
'data-testid': 'data',
|
|
142
|
+
}),
|
|
143
|
+
SContextMenu: defineOwnedAttributeSchema({}),
|
|
144
|
+
SContextToggleButton: defineOwnedAttributeSchema({}),
|
|
145
|
+
SField: defineOwnedAttributeSchema({}),
|
|
146
|
+
SFieldGroup: defineOwnedAttributeSchema({
|
|
147
|
+
'aria-describedby': 'string',
|
|
148
|
+
'data-testid': 'data',
|
|
149
|
+
}),
|
|
150
|
+
SFieldLabel: defineOwnedAttributeSchema({
|
|
151
|
+
'aria-label': 'string',
|
|
152
|
+
'data-testid': 'data',
|
|
153
|
+
}),
|
|
154
|
+
SDropdownMenu: defineOwnedAttributeSchema({
|
|
155
|
+
'aria-label': 'string',
|
|
156
|
+
'data-testid': 'data',
|
|
157
|
+
id: 'string',
|
|
158
|
+
title: 'string',
|
|
159
|
+
}),
|
|
160
|
+
SInputText: defineOwnedAttributeSchema({
|
|
161
|
+
'aria-describedby': 'string',
|
|
162
|
+
'aria-invalid': 'aria-boolean',
|
|
163
|
+
'aria-label': 'string',
|
|
164
|
+
'data-testid': 'data',
|
|
165
|
+
autocomplete: 'string',
|
|
166
|
+
inputmode: 'input-mode',
|
|
167
|
+
list: 'string',
|
|
168
|
+
max: 'string-or-number',
|
|
169
|
+
min: 'string-or-number',
|
|
170
|
+
onBlur: 'listener',
|
|
171
|
+
onFocus: 'listener',
|
|
172
|
+
onKeydown: 'listener',
|
|
173
|
+
onKeyup: 'listener',
|
|
174
|
+
pattern: 'string',
|
|
175
|
+
step: 'string-or-number',
|
|
176
|
+
title: 'string',
|
|
177
|
+
}),
|
|
178
|
+
SInlineTokenEditor: defineOwnedAttributeSchema({
|
|
179
|
+
'aria-describedby': 'string',
|
|
180
|
+
'aria-label': 'string',
|
|
181
|
+
'aria-labelledby': 'string',
|
|
182
|
+
'data-testid': 'data',
|
|
183
|
+
autocomplete: 'string',
|
|
184
|
+
}),
|
|
185
|
+
SInteractiveSurface: defineOwnedAttributeSchema({}),
|
|
186
|
+
SLink: defineOwnedAttributeSchema({
|
|
187
|
+
'aria-label': 'string',
|
|
188
|
+
'data-s-app-shell-brand-passive': 'data',
|
|
189
|
+
'data-testid': 'data',
|
|
190
|
+
download: 'string-or-boolean',
|
|
191
|
+
title: 'string',
|
|
192
|
+
}),
|
|
193
|
+
SListbox: defineOwnedAttributeSchema({
|
|
194
|
+
'aria-describedby': 'string',
|
|
195
|
+
'data-testid': 'data',
|
|
196
|
+
}),
|
|
197
|
+
SMarker: defineOwnedAttributeSchema({}),
|
|
198
|
+
SMenuSurface: defineOwnedAttributeSchema({
|
|
199
|
+
id: 'string',
|
|
200
|
+
}),
|
|
201
|
+
SSegmentedControl: defineOwnedAttributeSchema({
|
|
202
|
+
'aria-describedby': 'string',
|
|
203
|
+
'data-testid': 'data',
|
|
204
|
+
}),
|
|
205
|
+
SSelect: defineOwnedAttributeSchema({
|
|
206
|
+
'aria-describedby': 'string',
|
|
207
|
+
'aria-label': 'string',
|
|
208
|
+
'data-task-mapping-internal-value': 'data',
|
|
209
|
+
'data-task-mapping-kind': 'data',
|
|
210
|
+
'data-test': 'data',
|
|
211
|
+
'data-testid': 'data',
|
|
212
|
+
}),
|
|
213
|
+
STextarea: defineOwnedAttributeSchema({
|
|
214
|
+
'aria-describedby': 'string',
|
|
215
|
+
'aria-label': 'string',
|
|
216
|
+
'data-testid': 'data',
|
|
217
|
+
autocomplete: 'string',
|
|
218
|
+
onBlur: 'listener',
|
|
219
|
+
}),
|
|
220
|
+
SActionList: defineOwnedAttributeSchema({}),
|
|
221
|
+
SActionListItem: defineOwnedAttributeSchema({
|
|
222
|
+
'data-testid': 'data',
|
|
223
|
+
role: 'string',
|
|
224
|
+
}),
|
|
225
|
+
SActionCard: defineOwnedAttributeSchema({
|
|
226
|
+
'data-testid': 'data',
|
|
227
|
+
onFocusout: 'listener',
|
|
228
|
+
}),
|
|
229
|
+
SBadge: defineOwnedAttributeSchema({
|
|
230
|
+
'data-testid': 'data',
|
|
231
|
+
title: 'string',
|
|
232
|
+
}),
|
|
233
|
+
SCodeBlock: defineOwnedAttributeSchema({
|
|
234
|
+
'aria-label': 'string',
|
|
235
|
+
'data-testid': 'data',
|
|
236
|
+
}),
|
|
237
|
+
SCodeEditor: defineOwnedAttributeSchema({
|
|
238
|
+
'aria-label': 'string',
|
|
239
|
+
'aria-labelledby': 'string',
|
|
240
|
+
autofocus: 'boolean-attribute',
|
|
241
|
+
}),
|
|
242
|
+
SCodeSearchPanel: defineOwnedAttributeSchema({}),
|
|
243
|
+
SCopyField: defineOwnedAttributeSchema({}),
|
|
244
|
+
SDocBlock: defineOwnedAttributeSchema({}),
|
|
245
|
+
SEmptyState: defineOwnedAttributeSchema({}),
|
|
246
|
+
SExpandableText: defineOwnedAttributeSchema({
|
|
247
|
+
'data-testid': 'data',
|
|
248
|
+
}),
|
|
249
|
+
SJsonTree: defineOwnedAttributeSchema({}),
|
|
250
|
+
SKeyValueGrid: defineOwnedAttributeSchema({}),
|
|
251
|
+
SLinkedSystemsList: defineOwnedAttributeSchema({}),
|
|
252
|
+
SMessage: defineOwnedAttributeSchema({
|
|
253
|
+
'data-testid': 'data',
|
|
254
|
+
}),
|
|
255
|
+
SMetricCard: defineOwnedAttributeSchema({}),
|
|
256
|
+
SProgressBar: defineOwnedAttributeSchema({}),
|
|
257
|
+
SProgressIndicator: defineOwnedAttributeSchema({}),
|
|
258
|
+
SSectionHeader: defineOwnedAttributeSchema({}),
|
|
259
|
+
SStatus: defineOwnedAttributeSchema({}),
|
|
260
|
+
STable: defineOwnedAttributeSchema({}),
|
|
261
|
+
SToastContainer: defineOwnedAttributeSchema({}),
|
|
262
|
+
STooltip: defineOwnedAttributeSchema({}),
|
|
263
|
+
STooltipAnchor: defineOwnedAttributeSchema({
|
|
264
|
+
'aria-describedby': 'string',
|
|
265
|
+
'data-testid': 'data',
|
|
266
|
+
id: 'string',
|
|
267
|
+
}),
|
|
268
|
+
SVirtualList: defineOwnedAttributeSchema({}),
|
|
269
|
+
SAsyncState: defineOwnedAttributeSchema({}),
|
|
270
|
+
SDataGrid: defineOwnedAttributeSchema({}),
|
|
271
|
+
SSqlEditor: defineOwnedAttributeSchema({}),
|
|
272
|
+
SGraphViewport: defineOwnedAttributeSchema({}),
|
|
273
|
+
SBottomNav: defineOwnedAttributeSchema({}),
|
|
274
|
+
SBreadcrumbs: defineOwnedAttributeSchema({
|
|
275
|
+
'aria-label': 'string',
|
|
276
|
+
}),
|
|
277
|
+
SCatalogNavigator: defineOwnedAttributeSchema({
|
|
278
|
+
'aria-label': 'string',
|
|
279
|
+
}),
|
|
280
|
+
STab: defineOwnedAttributeSchema({
|
|
281
|
+
'aria-label': 'string',
|
|
282
|
+
'data-terminal-id': 'data',
|
|
283
|
+
'data-testid': 'data',
|
|
284
|
+
onDblclick: 'listener',
|
|
285
|
+
onMousedown: 'listener',
|
|
286
|
+
title: 'string',
|
|
287
|
+
}),
|
|
288
|
+
STabList: defineOwnedAttributeSchema({
|
|
289
|
+
'aria-label': 'string',
|
|
290
|
+
}),
|
|
291
|
+
STabPanel: defineOwnedAttributeSchema({
|
|
292
|
+
'data-terminal-id': 'data',
|
|
293
|
+
'data-testid': 'data',
|
|
294
|
+
}),
|
|
295
|
+
STabPanels: defineOwnedAttributeSchema({}),
|
|
296
|
+
STabs: defineOwnedAttributeSchema({
|
|
297
|
+
'aria-label': 'string',
|
|
298
|
+
}),
|
|
299
|
+
SWizardSteps: defineOwnedAttributeSchema({
|
|
300
|
+
'data-wizard-owner': 'data',
|
|
301
|
+
}),
|
|
302
|
+
SPersonaProjectPicker: defineOwnedAttributeSchema({}),
|
|
303
|
+
SPersonaRunWorkbench: defineOwnedAttributeSchema({}),
|
|
304
|
+
} as const)
|
|
305
|
+
|
|
306
|
+
export type OwnedAttributeComponent = keyof typeof ownedAttributeSchemas
|
|
307
|
+
type OwnedAttributeName<Component extends OwnedAttributeComponent> = keyof (typeof ownedAttributeSchemas)[Component] & string
|
|
308
|
+
type OwnedAttributeKind<Component extends OwnedAttributeComponent, Name extends OwnedAttributeName<Component>> =
|
|
309
|
+
(typeof ownedAttributeSchemas)[Component][Name] & OwnedAttributeValueKind
|
|
310
|
+
|
|
311
|
+
type KindAttributeValue<Kind extends OwnedAttributeValueKind> =
|
|
312
|
+
Kind extends 'listener' ? (payload: Event) => void
|
|
313
|
+
: Kind extends 'string' ? string
|
|
314
|
+
: Kind extends 'string-or-boolean' ? string | boolean
|
|
315
|
+
: Kind extends 'string-or-number' ? string | number
|
|
316
|
+
: Kind extends 'data' | 'aria' ? string | number | boolean
|
|
317
|
+
: Kind extends 'aria-boolean' ? boolean | 'true' | 'false'
|
|
318
|
+
: Kind extends 'aria-current' ? boolean | 'page' | 'step' | 'location' | 'date' | 'time'
|
|
319
|
+
: Kind extends 'aria-haspopup' ? boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'
|
|
320
|
+
: Kind extends 'aria-pressed' ? boolean | 'mixed'
|
|
321
|
+
: Kind extends 'input-mode' ? 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'
|
|
322
|
+
: Kind extends 'boolean-attribute' ? string | boolean
|
|
323
|
+
: never
|
|
324
|
+
|
|
325
|
+
type OwnedAttributeValue<Name extends string, Kind extends OwnedAttributeValueKind> =
|
|
326
|
+
Name extends keyof HTMLAttributes ? NonNullable<HTMLAttributes[Name]>
|
|
327
|
+
: Name extends keyof InputHTMLAttributes ? NonNullable<InputHTMLAttributes[Name]>
|
|
328
|
+
: Name extends keyof TextareaHTMLAttributes ? NonNullable<TextareaHTMLAttributes[Name]>
|
|
329
|
+
: Name extends keyof AnchorHTMLAttributes ? NonNullable<AnchorHTMLAttributes[Name]>
|
|
330
|
+
: KindAttributeValue<Kind>
|
|
331
|
+
|
|
332
|
+
export type OwnedAttributeBindings<Component extends OwnedAttributeComponent> = Readonly<Partial<{
|
|
333
|
+
[Name in OwnedAttributeName<Component>]: OwnedAttributeValue<Name, OwnedAttributeKind<Component, Name>>
|
|
334
|
+
}>>
|
|
335
|
+
|
|
336
|
+
export interface OwnedAttrsOptions<Component extends OwnedAttributeComponent> {
|
|
337
|
+
/** Имя public-компонента для диагностики контракта. / Public component name used in contract diagnostics. */
|
|
338
|
+
component: Component
|
|
339
|
+
/** Семантический/native-элемент, владеющий атрибутами. / Semantic/native element that owns the attributes. */
|
|
340
|
+
owner: string
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export interface OwnedAttrsContract<Component extends OwnedAttributeComponent> {
|
|
344
|
+
/**
|
|
345
|
+
* Возвращает проверенные exact non-prop атрибуты для семантического owner.
|
|
346
|
+
* Returns validated exact non-prop attributes for the semantic owner.
|
|
347
|
+
*/
|
|
348
|
+
bindings: () => OwnedAttributeBindings<Component>
|
|
349
|
+
/**
|
|
350
|
+
* Отклоняет любые attrs, когда у состояния нет семантического owner.
|
|
351
|
+
* Rejects every attr when the current state has no semantic owner.
|
|
352
|
+
*/
|
|
353
|
+
reject: (reason: string) => void
|
|
354
|
+
/** Читает разрешённый строковый attr и отклоняет неверный runtime-тип. / Reads an allowed string attr and rejects an invalid runtime type. */
|
|
355
|
+
string: (name: OwnedAttributeName<Component>) => string | undefined
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Объединяет ARIA ID-reference lists без повторов. / Merges ARIA ID-reference lists without duplicates.
|
|
360
|
+
*/
|
|
361
|
+
export function mergeIdReferences(...references: Array<string | undefined>): string | undefined {
|
|
362
|
+
const tokens = references.flatMap((reference) => reference?.trim().split(/\s+/).filter(Boolean) ?? [])
|
|
363
|
+
return tokens.length > 0 ? [...new Set(tokens)].join(' ') : undefined
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isListener(value: unknown): boolean {
|
|
367
|
+
return typeof value === 'function'
|
|
368
|
+
|| (Array.isArray(value) && value.length > 0 && value.every((candidate) => typeof candidate === 'function'))
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function isAllowedAttributeValue(kind: OwnedAttributeValueKind, value: unknown): boolean {
|
|
372
|
+
if (value === undefined || value === null) return true
|
|
373
|
+
if (kind === 'listener') return isListener(value)
|
|
374
|
+
if (kind === 'string') return typeof value === 'string'
|
|
375
|
+
if (kind === 'aria-boolean') return typeof value === 'boolean' || value === 'true' || value === 'false'
|
|
376
|
+
if (kind === 'aria-current') {
|
|
377
|
+
return typeof value === 'boolean' || ['page', 'step', 'location', 'date', 'time'].includes(String(value))
|
|
378
|
+
}
|
|
379
|
+
if (kind === 'aria-haspopup') {
|
|
380
|
+
return typeof value === 'boolean' || ['menu', 'listbox', 'tree', 'grid', 'dialog'].includes(String(value))
|
|
381
|
+
}
|
|
382
|
+
if (kind === 'aria-pressed') return typeof value === 'boolean' || value === 'mixed'
|
|
383
|
+
if (kind === 'input-mode') {
|
|
384
|
+
return ['none', 'text', 'decimal', 'numeric', 'tel', 'search', 'email', 'url'].includes(String(value))
|
|
385
|
+
}
|
|
386
|
+
if (kind === 'string-or-boolean' || kind === 'boolean-attribute') {
|
|
387
|
+
return typeof value === 'string' || typeof value === 'boolean'
|
|
388
|
+
}
|
|
389
|
+
if (kind === 'string-or-number') return typeof value === 'string' || typeof value === 'number'
|
|
390
|
+
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function attributeBoundaryError(component: string, owner: string, message: string): Error {
|
|
394
|
+
return new Error(`${component}: ${message} на границе ${owner}. / ${message} at the ${owner} boundary.`)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Маршрутизирует только exact-audited native/data/ARIA/listener attrs к одному owner.
|
|
399
|
+
* Routes only exact-audited native/data/ARIA/listener attrs to one owner.
|
|
400
|
+
*/
|
|
401
|
+
export function useOwnedAttrs<Component extends OwnedAttributeComponent>(
|
|
402
|
+
options: OwnedAttrsOptions<Component>,
|
|
403
|
+
): OwnedAttrsContract<Component> {
|
|
404
|
+
const attrs = useAttrs()
|
|
405
|
+
const schema: OwnedAttributeSchema = ownedAttributeSchemas[options.component]
|
|
406
|
+
|
|
407
|
+
const validate = (): void => {
|
|
408
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
409
|
+
if (name === 'class' || name === 'style') {
|
|
410
|
+
throw attributeBoundaryError(
|
|
411
|
+
options.component,
|
|
412
|
+
options.owner,
|
|
413
|
+
`raw "${name}" запрещён; атрибут "${name}" не объявлен в exact owned-attrs schema; используйте типизированные props/tokens UI-kit или consumer layout wrapper. Raw "${name}" is forbidden and is not declared by the exact owned-attrs schema; use typed UI-kit props/tokens or a consumer layout wrapper`,
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
const kind = schema[name]
|
|
417
|
+
if (!kind) {
|
|
418
|
+
throw attributeBoundaryError(
|
|
419
|
+
options.component,
|
|
420
|
+
options.owner,
|
|
421
|
+
`атрибут "${name}" не объявлен в exact owned-attrs schema; используйте типизированные props/emits/tokens UI-kit или consumer layout wrapper. Attribute "${name}" is not declared by the exact owned-attrs schema; use typed UI-kit props/emits/tokens or a consumer layout wrapper`,
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
if (!isAllowedAttributeValue(kind, value)) {
|
|
425
|
+
throw attributeBoundaryError(
|
|
426
|
+
options.component,
|
|
427
|
+
options.owner,
|
|
428
|
+
`атрибут "${name}" имеет недопустимый runtime-тип ${typeof value}; ожидается ${kind}. Attribute "${name}" has invalid runtime type ${typeof value}; expected ${kind}`,
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const bindings = (): OwnedAttributeBindings<Component> => {
|
|
435
|
+
validate()
|
|
436
|
+
return Object.fromEntries(Object.entries(attrs)) as OwnedAttributeBindings<Component>
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const string = (name: OwnedAttributeName<Component>): string | undefined => {
|
|
440
|
+
const kind = schema[name]
|
|
441
|
+
if (kind !== 'string') {
|
|
442
|
+
throw attributeBoundaryError(
|
|
443
|
+
options.component,
|
|
444
|
+
options.owner,
|
|
445
|
+
`атрибут "${name}" не объявлен строковым. Attribute "${name}" is not declared as a string`,
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
validate()
|
|
449
|
+
const value = attrs[name]
|
|
450
|
+
if (value === undefined || value === null) return undefined
|
|
451
|
+
return value as string
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const reject = (reason: string): void => {
|
|
455
|
+
const names = Object.keys(attrs)
|
|
456
|
+
if (names.length === 0) return
|
|
457
|
+
throw new Error(
|
|
458
|
+
`${options.component}: attrs ${names.map((name) => `"${name}"`).join(', ')} не имеют владельца (${reason}). `
|
|
459
|
+
+ `Attributes ${names.map((name) => `"${name}"`).join(', ')} have no owner (${reason}).`,
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return { bindings, reject, string }
|
|
464
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineComponent,
|
|
3
|
+
inject,
|
|
4
|
+
onBeforeUnmount,
|
|
5
|
+
provide,
|
|
6
|
+
watchEffect,
|
|
7
|
+
type InjectionKey,
|
|
8
|
+
type PropType,
|
|
9
|
+
} from 'vue'
|
|
10
|
+
|
|
11
|
+
export interface PassiveContentContract {
|
|
12
|
+
/** Имя компонента-владельца для точной runtime-диагностики. / Owner name used in precise runtime diagnostics. */
|
|
13
|
+
owner: string
|
|
14
|
+
/** Имя пассивной области публичного slot contract. / Name of the passive public slot region. */
|
|
15
|
+
region: string
|
|
16
|
+
/** Отключает запрет, когда поверхность семантически не интерактивна. / Disables the guard while the surface is semantically passive. */
|
|
17
|
+
active?: () => boolean
|
|
18
|
+
/** Подсказка о правильном sibling API владельца. / Guidance to the owner's sibling API. */
|
|
19
|
+
guidance?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface InteractiveLeafContract {
|
|
23
|
+
/** Канонический leaf owner интерактивного поведения. / Canonical leaf owner of the interactive behavior. */
|
|
24
|
+
owner: string
|
|
25
|
+
/** Учитывает leaf только когда он действительно интерактивен. / Counts the leaf only while it is actually interactive. */
|
|
26
|
+
active?: () => boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface InteractiveLeafRegistration extends InteractiveLeafContract {
|
|
30
|
+
readonly id: symbol
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PassiveContentBoundary {
|
|
34
|
+
register: (registration: InteractiveLeafRegistration) => () => void
|
|
35
|
+
assert: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const passiveContentBoundaryKey: InjectionKey<PassiveContentBoundary> = Symbol('passiveContentBoundary')
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Ограничивает typed passive-content contract точным slot-поддеревом без дополнительного DOM-узла.
|
|
42
|
+
* Scopes a typed passive-content contract to the exact slot subtree without adding a DOM node.
|
|
43
|
+
*/
|
|
44
|
+
export const PassiveContentBoundary = defineComponent({
|
|
45
|
+
name: 'PassiveContentBoundary',
|
|
46
|
+
props: {
|
|
47
|
+
contract: {
|
|
48
|
+
type: Object as PropType<PassiveContentContract>,
|
|
49
|
+
required: true,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
setup(props, { slots }) {
|
|
53
|
+
usePassiveContentContract(props.contract)
|
|
54
|
+
return () => slots.default?.()
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
function passiveContentError(
|
|
59
|
+
contract: PassiveContentContract,
|
|
60
|
+
registration: InteractiveLeafRegistration,
|
|
61
|
+
): Error {
|
|
62
|
+
const guidance = contract.guidance ? ` ${contract.guidance}` : ''
|
|
63
|
+
return new Error(
|
|
64
|
+
`${contract.owner}: ${contract.region} не допускает интерактивный leaf ${registration.owner}; `
|
|
65
|
+
+ `интерактивное действие должно быть sibling-узлом.`
|
|
66
|
+
+ ` / ${contract.owner}: ${contract.region} does not allow the interactive leaf `
|
|
67
|
+
+ `${registration.owner}; render the interactive action as a sibling.${guidance}`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Создаёт typed registration boundary для пассивного slot-контента без DOM-инспекции.
|
|
73
|
+
* Creates a typed registration boundary for passive slot content without DOM inspection.
|
|
74
|
+
*/
|
|
75
|
+
export function usePassiveContentContract(contract: PassiveContentContract): void {
|
|
76
|
+
const parentBoundary = inject(passiveContentBoundaryKey, null)
|
|
77
|
+
const registrations = new Map<symbol, InteractiveLeafRegistration>()
|
|
78
|
+
|
|
79
|
+
const assert = (): void => {
|
|
80
|
+
if (contract.active?.() === false) return
|
|
81
|
+
const interactive = [...registrations.values()].find((registration) => registration.active?.() !== false)
|
|
82
|
+
if (interactive) throw passiveContentError(contract, interactive)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const boundary: PassiveContentBoundary = {
|
|
86
|
+
register: (registration) => {
|
|
87
|
+
if (registrations.has(registration.id)) {
|
|
88
|
+
throw new Error(`${contract.owner}: duplicate interactive leaf registration from ${registration.owner}`)
|
|
89
|
+
}
|
|
90
|
+
registrations.set(registration.id, registration)
|
|
91
|
+
let unregisterParent: (() => void) | undefined
|
|
92
|
+
try {
|
|
93
|
+
assert()
|
|
94
|
+
unregisterParent = parentBoundary?.register(registration)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
registrations.delete(registration.id)
|
|
97
|
+
unregisterParent?.()
|
|
98
|
+
throw error
|
|
99
|
+
}
|
|
100
|
+
return () => {
|
|
101
|
+
registrations.delete(registration.id)
|
|
102
|
+
unregisterParent?.()
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
assert,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
provide(passiveContentBoundaryKey, boundary)
|
|
109
|
+
watchEffect(assert)
|
|
110
|
+
onBeforeUnmount(() => registrations.clear())
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Регистрирует UI-kit leaf во всех родительских passive boundaries.
|
|
115
|
+
* Registers a UI-kit leaf with every parent passive boundary.
|
|
116
|
+
*/
|
|
117
|
+
export function useInteractiveLeafRegistration(contract: InteractiveLeafContract): void {
|
|
118
|
+
const boundary = inject(passiveContentBoundaryKey, null)
|
|
119
|
+
if (!boundary) return
|
|
120
|
+
const registration: InteractiveLeafRegistration = {
|
|
121
|
+
...contract,
|
|
122
|
+
id: Symbol(contract.owner),
|
|
123
|
+
}
|
|
124
|
+
const unregister = boundary.register(registration)
|
|
125
|
+
watchEffect(() => {
|
|
126
|
+
if (contract.active?.() !== false) boundary.assert()
|
|
127
|
+
})
|
|
128
|
+
onBeforeUnmount(unregister)
|
|
129
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type PointerInteractionCursor = 'col-resize' | 'row-resize'
|
|
2
|
+
|
|
3
|
+
export interface PointerInteractionLeaseOptions {
|
|
4
|
+
cursor: PointerInteractionCursor
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PointerInteractionLease {
|
|
8
|
+
release: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ActivePointerInteraction {
|
|
12
|
+
id: symbol
|
|
13
|
+
cursor: PointerInteractionCursor
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const activeInteractions: ActivePointerInteraction[] = []
|
|
17
|
+
let lockedBody: HTMLElement | null = null
|
|
18
|
+
let preservedCursor = ''
|
|
19
|
+
let preservedUserSelect = ''
|
|
20
|
+
|
|
21
|
+
const applyActiveInteraction = (): void => {
|
|
22
|
+
const interaction = activeInteractions.at(-1)
|
|
23
|
+
if (!lockedBody || !interaction) return
|
|
24
|
+
lockedBody.style.cursor = interaction.cursor
|
|
25
|
+
lockedBody.style.userSelect = 'none'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const restoreBodyContract = (): void => {
|
|
29
|
+
if (!lockedBody) return
|
|
30
|
+
lockedBody.style.cursor = preservedCursor
|
|
31
|
+
lockedBody.style.userSelect = preservedUserSelect
|
|
32
|
+
lockedBody = null
|
|
33
|
+
preservedCursor = ''
|
|
34
|
+
preservedUserSelect = ''
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Захватывает общий cursor/user-select контракт на время pointer-взаимодействия.
|
|
39
|
+
* Acquires the shared cursor/user-select contract for one pointer interaction.
|
|
40
|
+
*/
|
|
41
|
+
export function acquirePointerInteractionLease(
|
|
42
|
+
options: PointerInteractionLeaseOptions,
|
|
43
|
+
): PointerInteractionLease {
|
|
44
|
+
if (typeof document === 'undefined') {
|
|
45
|
+
throw new Error('pointerInteractionLease: document недоступен вне браузерного runtime')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const body = document.body
|
|
49
|
+
if (activeInteractions.length === 0) {
|
|
50
|
+
lockedBody = body
|
|
51
|
+
preservedCursor = body.style.cursor
|
|
52
|
+
preservedUserSelect = body.style.userSelect
|
|
53
|
+
} else if (lockedBody !== body) {
|
|
54
|
+
throw new Error('pointerInteractionLease: document.body изменился во время активного interaction')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const interaction: ActivePointerInteraction = {
|
|
58
|
+
id: Symbol('pointer-interaction'),
|
|
59
|
+
cursor: options.cursor,
|
|
60
|
+
}
|
|
61
|
+
let active = true
|
|
62
|
+
activeInteractions.push(interaction)
|
|
63
|
+
applyActiveInteraction()
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
release: () => {
|
|
67
|
+
if (!active) return
|
|
68
|
+
active = false
|
|
69
|
+
const index = activeInteractions.findIndex(candidate => candidate.id === interaction.id)
|
|
70
|
+
if (index < 0) {
|
|
71
|
+
throw new Error('pointerInteractionLease: активный lease отсутствует в реестре')
|
|
72
|
+
}
|
|
73
|
+
activeInteractions.splice(index, 1)
|
|
74
|
+
if (activeInteractions.length === 0) {
|
|
75
|
+
restoreBodyContract()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
applyActiveInteraction()
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|