@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,82 @@
1
+ <template>
2
+ <SCollapsiblePanel
3
+ v-bind="ownedAttrs.bindings()"
4
+ :summary="validatedTitle"
5
+ summary-typography="strong"
6
+ :summary-badges="[{ id: 'item-count', label: validatedCount, severity: 'secondary' }]"
7
+ :open="open"
8
+ :default-open="defaultOpen"
9
+ :density="density"
10
+ header-variant="minimal"
11
+ surface="plain"
12
+ @update:open="$emit('update:open', $event)"
13
+ data-testid="s-kanban-lane"
14
+ >
15
+ <SStack v-if="validatedCount > 0" gap="control">
16
+ <slot />
17
+ </SStack>
18
+ <SEmptyState
19
+ v-else
20
+ :title="validatedEmptyText"
21
+ size="sm"
22
+ />
23
+ </SCollapsiblePanel>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { computed, type VNodeChild } from 'vue'
28
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
29
+ import { validateBoundedInteger, validateNonEmptyString } from '../../../internal/runtimeContract'
30
+ import SStack from '../../layout/SStack.vue'
31
+ import SCollapsiblePanel from '../containers/SCollapsiblePanel.vue'
32
+ import SEmptyState from '../data-display/SEmptyState.vue'
33
+
34
+ defineOptions({ inheritAttrs: false })
35
+
36
+ /** Каноническая вертикальная lane-композиция поверх disclosure, stack и empty-state owners. / Canonical vertical lane composition over disclosure, stack, and empty-state owners. */
37
+ export interface Props {
38
+ /** Пользовательская метка lane. / User-facing lane label. */
39
+ title: string
40
+ /** Точное число элементов в lane. / Exact number of items in the lane. */
41
+ count: number
42
+ /** Сообщение пустого состояния. / Empty-state message. */
43
+ emptyText?: string
44
+ /** Начальное состояние uncontrolled disclosure. / Initial uncontrolled disclosure state. */
45
+ defaultOpen?: boolean
46
+ /** Controlled disclosure state. / Controlled disclosure state. */
47
+ open?: boolean
48
+ /** Семантическая плотность disclosure. / Semantic disclosure density. */
49
+ density?: 'comfortable' | 'compact' | 'dense'
50
+ }
51
+
52
+ const props = withDefaults(defineProps<Props>(), {
53
+ emptyText: 'Нет элементов',
54
+ defaultOpen: true,
55
+ open: undefined,
56
+ density: 'compact',
57
+ })
58
+
59
+ defineEmits<{
60
+ (e: 'update:open', value: boolean): void
61
+ }>()
62
+
63
+ defineSlots<{
64
+ /** Элементы lane; их presentation принадлежит переданным UI-kit owners. / Lane items whose presentation belongs to supplied UI-kit owners. */
65
+ default?: () => VNodeChild
66
+ }>()
67
+
68
+ const validatedTitle = computed(() => validateNonEmptyString('SKanbanLane', 'title', props.title))
69
+ const validatedCount = computed(() => validateBoundedInteger(
70
+ 'SKanbanLane',
71
+ 'count',
72
+ props.count,
73
+ { minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
74
+ ))
75
+ const validatedEmptyText = computed(() => validateNonEmptyString(
76
+ 'SKanbanLane',
77
+ 'emptyText',
78
+ props.emptyText,
79
+ ))
80
+
81
+ const ownedAttrs = useOwnedAttrs({ component: 'SKanbanLane', owner: 'collapsible lane composition' })
82
+ </script>
@@ -0,0 +1,24 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-tree {
4
+ @apply h-full w-full text-sm;
5
+ margin: 0;
6
+ padding: 0;
7
+ list-style: none;
8
+
9
+ --s-tree-branch-color: color-mix(in srgb, var(--color-surface-300) 68%, transparent);
10
+ --s-tree-branch-active-color: color-mix(in srgb, var(--color-primary-500) 34%, var(--color-surface-300));
11
+ --s-tree-row-border-bottom: 0 solid transparent;
12
+ --s-tree-row-background: transparent;
13
+ --s-tree-row-selected-bg: linear-gradient(
14
+ 90deg,
15
+ color-mix(in srgb, var(--color-success-50) 82%, transparent),
16
+ color-mix(in srgb, var(--color-success-50) 28%, transparent) 72%,
17
+ transparent
18
+ );
19
+ }
20
+
21
+ .s-tree--row-table {
22
+ --s-tree-row-border-bottom: 1px solid color-mix(in srgb, var(--color-surface-200) 74%, transparent);
23
+ --s-tree-row-background: var(--color-surface-0);
24
+ }
@@ -0,0 +1,288 @@
1
+ <template>
2
+ <ul
3
+ v-bind="ownedAttrs.bindings()"
4
+ ref="treeRef"
5
+ class="s-tree"
6
+ role="tree"
7
+ :aria-label="props.label"
8
+ :aria-multiselectable="props.selection.mode === 'multiple' ? true : undefined"
9
+ :class="[
10
+ `s-tree--density-${props.density}`,
11
+ `s-tree--actions-${props.actionsLayout}`,
12
+ `s-tree--expand-${props.expandControl}`,
13
+ `s-tree--selection-rail-${props.selectionRail}`,
14
+ `s-tree--row-${props.rowVariant}`,
15
+ ]"
16
+ >
17
+ <!-- @vue-generic {T, K} -->
18
+ <STreeNode
19
+ v-for="(node, index) in validatedNodes"
20
+ :key="props.model.key(node)"
21
+ :node="node"
22
+ :level="0"
23
+ :position="index + 1"
24
+ :set-size="validatedNodes.length"
25
+ >
26
+ <template v-if="$slots['col-left']" #col-left="scope"><slot name="col-left" v-bind="scope" /></template>
27
+ <template v-if="$slots['col-center']" #col-center="scope"><slot name="col-center" v-bind="scope" /></template>
28
+ <template v-if="$slots.meta" #meta="scope"><slot name="meta" v-bind="scope" /></template>
29
+ <template v-if="$slots['persistent-actions']" #persistent-actions="scope"><slot name="persistent-actions" v-bind="scope" /></template>
30
+ <template v-if="$slots.actions" #actions="scope"><slot name="actions" v-bind="scope" /></template>
31
+ </STreeNode>
32
+ </ul>
33
+ </template>
34
+
35
+ <script setup lang="ts" generic="T extends object, K extends SelectionKey">
36
+ import { computed, nextTick, provide, ref, watch } from 'vue'
37
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
38
+ import { assertSelectionKey, selectionKeyToken } from '../_internal/useSelectionRoving'
39
+ import STreeNode from '../_internal/STreeNode.vue'
40
+ import {
41
+ TREE_RUNTIME_KEY,
42
+ type RegisteredTreeItem,
43
+ TreeItemRegistrationStore,
44
+ type TreeRuntime,
45
+ } from '../_internal/treeRuntime'
46
+ import { validateTreeModel } from './treeAdapter'
47
+ import type {
48
+ STreeNodeContentScope,
49
+ STreeProps,
50
+ SelectionKey,
51
+ TreeActivationOrigin,
52
+ TreeNodeActivationEvent,
53
+ TreeNodeEvent,
54
+ TreeSelection,
55
+ } from './types'
56
+
57
+ defineOptions({ inheritAttrs: false })
58
+
59
+ const props = withDefaults(defineProps<STreeProps<T, K>>(), {
60
+ expanded: () => new Set<K>(),
61
+ density: 'regular',
62
+ actionsLayout: 'reserved',
63
+ expandControl: 'button',
64
+ selectionRail: 'hidden',
65
+ rowVariant: 'default',
66
+ })
67
+
68
+ const emit = defineEmits<{
69
+ 'update:selection': [selection: TreeSelection<K>]
70
+ 'update:expanded': [expanded: Set<K>]
71
+ 'node-activate': [event: TreeNodeActivationEvent<T, K>]
72
+ 'node-select': [event: TreeNodeEvent<T, K>]
73
+ 'node-expand': [event: TreeNodeEvent<T, K>]
74
+ 'node-collapse': [event: TreeNodeEvent<T, K>]
75
+ }>()
76
+
77
+ defineSlots<{
78
+ 'col-left': (props: STreeNodeContentScope<T, K>) => unknown
79
+ 'col-center': (props: STreeNodeContentScope<T, K>) => unknown
80
+ 'meta': (props: STreeNodeContentScope<T, K>) => unknown
81
+ 'persistent-actions': (props: STreeNodeContentScope<T, K>) => unknown
82
+ 'actions': (props: STreeNodeContentScope<T, K>) => unknown
83
+ }>()
84
+
85
+ const ownedAttrs = useOwnedAttrs({ component: 'STree', owner: 'role=tree root' })
86
+ const treeRef = ref<HTMLElement | null>(null)
87
+ const activeKey = ref<K>()
88
+ const registeredItems = new TreeItemRegistrationStore<T, K>(() => treeRef.value)
89
+ let typeahead = ''
90
+ let typeaheadTimer: ReturnType<typeof setTimeout> | undefined
91
+
92
+ function assertControlledKeyMembership(
93
+ key: K,
94
+ coordinate: 'selection.key' | 'selection.keys' | 'expanded',
95
+ inventory: ReadonlySet<K>,
96
+ ): void {
97
+ assertSelectionKey('STree', key, coordinate)
98
+ if (!inventory.has(key)) {
99
+ const keyDescription = selectionKeyToken('STree', key, coordinate)
100
+ throw new Error(
101
+ `STree: key ${keyDescription} из ${coordinate} отсутствует в текущем inventory модели. `
102
+ + `/ STree: ${coordinate} key ${keyDescription} is absent from the current model inventory.`,
103
+ )
104
+ }
105
+ }
106
+
107
+ function validateControlledState(inventory: ReadonlySet<K>): void {
108
+ const selection = props.selection
109
+ if (selection.mode === 'single' && selection.key !== null) {
110
+ assertControlledKeyMembership(selection.key, 'selection.key', inventory)
111
+ }
112
+ if (selection.mode === 'multiple') {
113
+ if (!(selection.keys instanceof Set)) {
114
+ throw new TypeError('STree: selection.keys must be a Set')
115
+ }
116
+ selection.keys.forEach((key) => assertControlledKeyMembership(key, 'selection.keys', inventory))
117
+ }
118
+ if (!(props.expanded instanceof Set)) {
119
+ throw new TypeError('STree: expanded must be a Set')
120
+ }
121
+ props.expanded.forEach((key) => assertControlledKeyMembership(key, 'expanded', inventory))
122
+ }
123
+
124
+ const validatedNodes = computed<readonly T[]>(() => {
125
+ const inventory = validateTreeModel(props.nodes, props.model)
126
+ validateControlledState(inventory)
127
+ return props.nodes
128
+ })
129
+
130
+ function registerTreeItem(item: RegisteredTreeItem<T, K>): void {
131
+ registeredItems.register(item)
132
+ if (activeKey.value === undefined && !item.disabled()) activeKey.value = item.key
133
+ }
134
+
135
+ function unregisterTreeItem(key: K): void {
136
+ registeredItems.unregister(key)
137
+ if (activeKey.value !== key) return
138
+ activeKey.value = undefined
139
+ void nextTick(() => {
140
+ activeKey.value = registeredItems.ordered().find((item) => !item.disabled())?.key
141
+ })
142
+ }
143
+
144
+ function visibleItems(): RegisteredTreeItem<T, K>[] {
145
+ return registeredItems.ordered()
146
+ }
147
+
148
+ async function focusItem(item: RegisteredTreeItem<T, K> | undefined): Promise<void> {
149
+ if (!item || item.disabled()) return
150
+ activeKey.value = item.key
151
+ await nextTick()
152
+ item.element.focus()
153
+ }
154
+
155
+ function handleTreeItemKeydown(item: RegisteredTreeItem<T, K>, event: KeyboardEvent): void {
156
+ const items = visibleItems().filter((candidate) => !candidate.disabled())
157
+ const index = items.findIndex((candidate) => candidate.key === item.key)
158
+ if (event.key === 'ArrowDown') {
159
+ event.preventDefault()
160
+ void focusItem(items[index + 1])
161
+ return
162
+ }
163
+ if (event.key === 'ArrowUp') {
164
+ event.preventDefault()
165
+ void focusItem(items[index - 1])
166
+ return
167
+ }
168
+ if (event.key === 'Home') {
169
+ event.preventDefault()
170
+ void focusItem(items[0])
171
+ return
172
+ }
173
+ if (event.key === 'End') {
174
+ event.preventDefault()
175
+ void focusItem(items.at(-1))
176
+ return
177
+ }
178
+ if (event.key === 'ArrowRight' && item.hasChildren()) {
179
+ event.preventDefault()
180
+ if (!item.expanded()) item.toggle()
181
+ else void focusItem(items.find((candidate) => candidate.parentKey === item.key))
182
+ return
183
+ }
184
+ if (event.key === 'ArrowLeft') {
185
+ event.preventDefault()
186
+ if (item.hasChildren() && item.expanded()) item.toggle()
187
+ else if (item.parentKey !== undefined) void focusItem(registeredItems.get(item.parentKey))
188
+ return
189
+ }
190
+ if (event.key === 'Enter' || event.key === ' ') {
191
+ event.preventDefault()
192
+ if (event.key === ' ' && props.selection.mode === 'multiple') item.select()
193
+ else item.activate('keyboard')
194
+ return
195
+ }
196
+ if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return
197
+
198
+ typeahead += event.key.toLocaleLowerCase()
199
+ if (typeaheadTimer !== undefined) clearTimeout(typeaheadTimer)
200
+ typeaheadTimer = setTimeout(() => { typeahead = '' }, 500)
201
+ const ordered = [...items.slice(index + 1), ...items.slice(0, index + 1)]
202
+ void focusItem(ordered.find((candidate) =>
203
+ props.model.label(candidate.node).toLocaleLowerCase().startsWith(typeahead),
204
+ ))
205
+ }
206
+
207
+ function collectMultipleKeys(node: T): K[] {
208
+ if (props.model.disabled(node)) return []
209
+ const behavior = props.model.multipleSelection(node)
210
+ if (behavior === 'disabled') return []
211
+ if (behavior === 'self') return [props.model.key(node)]
212
+ const descendantKeys = props.model.children(node).flatMap((child) => collectMultipleKeys(child))
213
+ return behavior === 'self-and-descendants'
214
+ ? [props.model.key(node), ...descendantKeys]
215
+ : descendantKeys
216
+ }
217
+
218
+ function handleNodeSelect(node: T): void {
219
+ if (props.model.disabled(node) || props.selection.mode !== 'single') return
220
+ const key = props.model.key(node)
221
+ const nextKey = props.selection.key === key ? null : key
222
+ emit('update:selection', { mode: 'single', key: nextKey })
223
+ emit('node-select', { node, key })
224
+ }
225
+
226
+ function handleNodeActivate(node: T, origin: TreeActivationOrigin): void {
227
+ if (props.model.disabled(node) || props.model.children(node).length > 0) return
228
+ emit('node-activate', { node, key: props.model.key(node), origin })
229
+ }
230
+
231
+ function handleNodeCheck(node: T): void {
232
+ if (props.selection.mode !== 'multiple') return
233
+ const keys = collectMultipleKeys(node)
234
+ if (keys.length === 0) return
235
+
236
+ const nextKeys = new Set(props.selection.keys)
237
+ const allSelected = keys.every((key) => nextKeys.has(key))
238
+ for (const key of keys) {
239
+ if (allSelected) nextKeys.delete(key)
240
+ else nextKeys.add(key)
241
+ }
242
+ emit('update:selection', { mode: 'multiple', keys: nextKeys })
243
+ }
244
+
245
+ function handleNodeToggle(node: T): void {
246
+ const key = props.model.key(node)
247
+ const nextExpanded = new Set(props.expanded)
248
+ const event = { node, key }
249
+ if (nextExpanded.has(key)) {
250
+ nextExpanded.delete(key)
251
+ emit('node-collapse', event)
252
+ } else {
253
+ nextExpanded.add(key)
254
+ emit('node-expand', event)
255
+ }
256
+ emit('update:expanded', nextExpanded)
257
+ }
258
+
259
+ const runtime: TreeRuntime<T, K> = {
260
+ get model() { return props.model },
261
+ selection: computed(() => props.selection),
262
+ expanded: computed(() => props.expanded),
263
+ density: computed(() => props.density),
264
+ actionsLayout: computed(() => props.actionsLayout),
265
+ expandControl: computed(() => props.expandControl),
266
+ selectionRail: computed(() => props.selectionRail),
267
+ rowVariant: computed(() => props.rowVariant),
268
+ activeKey,
269
+ activateNode: handleNodeActivate,
270
+ selectNode: handleNodeSelect,
271
+ toggleNode: handleNodeToggle,
272
+ checkNode: handleNodeCheck,
273
+ multipleKeys: collectMultipleKeys,
274
+ register: registerTreeItem,
275
+ unregister: unregisterTreeItem,
276
+ handleItemKeydown: handleTreeItemKeydown,
277
+ }
278
+
279
+ provide(TREE_RUNTIME_KEY, runtime)
280
+
281
+ watch(validatedNodes, (nodes) => {
282
+ if (activeKey.value !== undefined) return
283
+ const firstEnabled = nodes.find((node) => !props.model.disabled(node))
284
+ activeKey.value = firstEnabled === undefined ? undefined : props.model.key(firstEnabled)
285
+ }, { immediate: true })
286
+ </script>
287
+
288
+ <style lang="postcss" src="./STree.css"></style>
@@ -0,0 +1,210 @@
1
+ import {
2
+ assertSelectionKey,
3
+ selectionKeyToken,
4
+ validateSelectionIdentities,
5
+ type SelectionIdentity,
6
+ type SelectionKey,
7
+ } from '../_internal/useSelectionRoving'
8
+ import type { TreeModel } from './types'
9
+
10
+ export interface FlatTreeAdapter<T extends object, K extends SelectionKey> {
11
+ key(node: T): K
12
+ parentKey(node: T): K | null
13
+ attachChildren(node: T, children: readonly T[]): T
14
+ }
15
+
16
+ function describeKey(key: SelectionKey): string {
17
+ return selectionKeyToken('tree adapter', key, 'diagnostic key')
18
+ }
19
+
20
+ /**
21
+ * Строит иерархию из плоских доменных данных. / Builds a hierarchy from flat domain data.
22
+ *
23
+ * Adapter не угадывает поля, не клонирует данные spread-оператором и не
24
+ * исправляет orphan/cycle payload. Любое нарушение даёт координатную ошибку.
25
+ */
26
+ export function buildTreeFromFlat<T extends object, K extends SelectionKey>(
27
+ nodes: readonly T[],
28
+ adapter: FlatTreeAdapter<T, K>,
29
+ ): readonly T[] {
30
+ if (!Array.isArray(nodes)) {
31
+ throw new TypeError('buildTreeFromFlat.nodes: expected an array')
32
+ }
33
+
34
+ const entries = nodes.map((node, index) => {
35
+ const coordinate = `buildTreeFromFlat.nodes[${index}]`
36
+ const key = adapter.key(node)
37
+ assertSelectionKey('buildTreeFromFlat', key, `${coordinate}.key`)
38
+ return {
39
+ node,
40
+ key,
41
+ parentKey: adapter.parentKey(node),
42
+ coordinate,
43
+ children: [] as T[],
44
+ }
45
+ })
46
+ const byKey = new Map<K, (typeof entries)[number]>()
47
+
48
+ validateSelectionIdentities(
49
+ 'buildTreeFromFlat',
50
+ entries.map((entry) => ({ key: entry.key, coordinate: `${entry.coordinate}.key` })),
51
+ )
52
+
53
+ for (const entry of entries) {
54
+ byKey.set(entry.key, entry)
55
+ }
56
+
57
+ for (const entry of entries) {
58
+ if (entry.parentKey === null) continue
59
+ assertSelectionKey('buildTreeFromFlat', entry.parentKey, `${entry.coordinate}.parentKey`)
60
+ if (entry.parentKey === entry.key) {
61
+ throw new TypeError(`${entry.coordinate}: node ${describeKey(entry.key)} cannot be its own parent`)
62
+ }
63
+ if (!byKey.has(entry.parentKey)) {
64
+ throw new TypeError(
65
+ `${entry.coordinate}: parent ${describeKey(entry.parentKey)} does not exist in the flat input`,
66
+ )
67
+ }
68
+ }
69
+
70
+ for (const entry of entries) {
71
+ const path = new Set<K>([entry.key])
72
+ let parentKey = entry.parentKey
73
+ while (parentKey !== null) {
74
+ if (path.has(parentKey)) {
75
+ throw new TypeError(
76
+ `${entry.coordinate}: parent chain contains a cycle at ${describeKey(parentKey)}`,
77
+ )
78
+ }
79
+ path.add(parentKey)
80
+ const parent = byKey.get(parentKey)
81
+ if (!parent) {
82
+ throw new TypeError(`${entry.coordinate}: parent chain is incomplete at ${describeKey(parentKey)}`)
83
+ }
84
+ parentKey = parent.parentKey
85
+ }
86
+ }
87
+
88
+ const roots: T[] = []
89
+ for (const entry of entries) {
90
+ if (entry.parentKey === null) {
91
+ roots.push(entry.node)
92
+ continue
93
+ }
94
+ const parent = byKey.get(entry.parentKey)
95
+ if (!parent) {
96
+ throw new TypeError(`${entry.coordinate}: parent ${describeKey(entry.parentKey)} is unavailable`)
97
+ }
98
+ parent.children.push(entry.node)
99
+ }
100
+
101
+ return roots.map((root) => {
102
+ const rootEntry = byKey.get(adapter.key(root))
103
+ if (!rootEntry) {
104
+ throw new TypeError('buildTreeFromFlat: root is absent from the validated key index')
105
+ }
106
+ return attachTreeChildren(rootEntry, byKey, adapter)
107
+ })
108
+ }
109
+
110
+ function attachTreeChildren<T extends object, K extends SelectionKey>(
111
+ entry: { readonly node: T; readonly key: K; readonly children: readonly T[] },
112
+ byKey: ReadonlyMap<K, { readonly node: T; readonly key: K; readonly children: readonly T[] }>,
113
+ adapter: FlatTreeAdapter<T, K>,
114
+ ): T {
115
+ const children = entry.children.map((child) => {
116
+ const childEntry = byKey.get(adapter.key(child))
117
+ if (!childEntry) {
118
+ throw new TypeError('buildTreeFromFlat: child is absent from the validated key index')
119
+ }
120
+ return attachTreeChildren(childEntry, byKey, adapter)
121
+ })
122
+ return adapter.attachChildren(entry.node, children)
123
+ }
124
+
125
+ function readModelValue<R>(
126
+ coordinate: string,
127
+ field: string,
128
+ reader: () => R,
129
+ ): R {
130
+ try {
131
+ return reader()
132
+ } catch (error) {
133
+ throw new TypeError(`STree: model.${field} failed at ${coordinate}`, { cause: error })
134
+ }
135
+ }
136
+
137
+ /** Проверяет точный runtime-контракт дерева. / Validates the exact runtime tree contract. */
138
+ export function validateTreeModel<T extends object, K extends SelectionKey>(
139
+ nodes: readonly T[],
140
+ model: TreeModel<T, K>,
141
+ ): ReadonlySet<K> {
142
+ if (!Array.isArray(nodes)) {
143
+ throw new TypeError('STree: nodes must be an array')
144
+ }
145
+
146
+ const identities: SelectionIdentity<K>[] = []
147
+ const visitedNodes = new Set<T>()
148
+ const activeNodes = new Set<T>()
149
+
150
+ const visit = (node: T, coordinate: string): void => {
151
+ if (typeof node !== 'object' || node === null) {
152
+ throw new TypeError(`STree: node must be an object at ${coordinate}`)
153
+ }
154
+ if (activeNodes.has(node)) {
155
+ throw new TypeError(`STree: cyclic child reference at ${coordinate}`)
156
+ }
157
+ if (visitedNodes.has(node)) {
158
+ throw new TypeError(`STree: the same node object is referenced more than once at ${coordinate}`)
159
+ }
160
+
161
+ activeNodes.add(node)
162
+ visitedNodes.add(node)
163
+
164
+ const key = readModelValue(coordinate, 'key', () => model.key(node))
165
+ assertSelectionKey('STree', key, `${coordinate}.key`)
166
+ identities.push({ key, coordinate: `${coordinate}.key` })
167
+
168
+ const label = readModelValue(coordinate, 'label', () => model.label(node))
169
+ if (typeof label !== 'string' || label.trim().length === 0) {
170
+ throw new TypeError(`STree: model.label must return a non-empty string at ${coordinate}`)
171
+ }
172
+
173
+ const children = readModelValue(coordinate, 'children', () => model.children(node))
174
+ if (!Array.isArray(children)) {
175
+ throw new TypeError(`STree: model.children must return an array at ${coordinate}`)
176
+ }
177
+
178
+ const disabled = readModelValue(coordinate, 'disabled', () => model.disabled(node))
179
+ if (typeof disabled !== 'boolean') {
180
+ throw new TypeError(`STree: model.disabled must return a boolean at ${coordinate}`)
181
+ }
182
+
183
+ const icon = readModelValue(coordinate, 'icon', () => model.icon(node))
184
+ if (
185
+ icon !== undefined
186
+ && typeof icon !== 'function'
187
+ && (typeof icon !== 'object' || icon === null)
188
+ ) {
189
+ throw new TypeError(`STree: model.icon must return a Vue component or undefined at ${coordinate}`)
190
+ }
191
+
192
+ const multipleSelection = readModelValue(
193
+ coordinate,
194
+ 'multipleSelection',
195
+ () => model.multipleSelection(node),
196
+ )
197
+ if (!['self', 'descendants', 'self-and-descendants', 'disabled'].includes(multipleSelection)) {
198
+ throw new TypeError(
199
+ `STree: model.multipleSelection returned an unsupported value at ${coordinate}`,
200
+ )
201
+ }
202
+
203
+ children.forEach((child, index) => visit(child, `${coordinate}.children[${index}]`))
204
+ activeNodes.delete(node)
205
+ }
206
+
207
+ nodes.forEach((node, index) => visit(node, `nodes[${index}]`))
208
+ validateSelectionIdentities('STree', identities)
209
+ return new Set(identities.map((identity) => identity.key))
210
+ }