@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,105 @@
1
+ export type DialogSurfaceModalSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'wide' | 'full'
2
+ export type DialogSurfaceDrawerSize = 'sm' | 'md' | 'lg'
3
+ export type DialogSurfaceTone = 'default' | 'subtle'
4
+ export type DialogSurfaceScrollBehavior = 'page' | 'body'
5
+
6
+ export interface ModalDialogSurfacePresentation {
7
+ kind: 'modal'
8
+ size: DialogSurfaceModalSize
9
+ fullscreen: boolean
10
+ surface: DialogSurfaceTone
11
+ scrollBehavior: DialogSurfaceScrollBehavior
12
+ }
13
+
14
+ export interface DrawerDialogSurfacePresentation {
15
+ kind: 'drawer'
16
+ size: DialogSurfaceDrawerSize
17
+ mobileFullScreen: boolean
18
+ }
19
+
20
+ export type DialogSurfacePresentation =
21
+ | ModalDialogSurfacePresentation
22
+ | DrawerDialogSurfacePresentation
23
+
24
+ export type DialogSurfaceContractErrorCode =
25
+ | 'invalid-presentation'
26
+ | 'invalid-size'
27
+ | 'invalid-surface'
28
+ | 'invalid-scroll-behavior'
29
+
30
+ export class DialogSurfaceContractError extends Error {
31
+ readonly code: DialogSurfaceContractErrorCode
32
+
33
+ constructor(code: DialogSurfaceContractErrorCode, message: string) {
34
+ super(message)
35
+ this.name = 'DialogSurfaceContractError'
36
+ this.code = code
37
+ }
38
+ }
39
+
40
+ const modalKeys = new Set(['kind', 'size', 'fullscreen', 'surface', 'scrollBehavior'])
41
+ const drawerKeys = new Set(['kind', 'size', 'mobileFullScreen'])
42
+ const modalSizes = new Set<DialogSurfaceModalSize>(['sm', 'md', 'lg', 'xl', '2xl', '3xl', 'wide', 'full'])
43
+ const drawerSizes = new Set<DialogSurfaceDrawerSize>(['sm', 'md', 'lg'])
44
+ const surfaceTones = new Set<DialogSurfaceTone>(['default', 'subtle'])
45
+ const scrollBehaviors = new Set<DialogSurfaceScrollBehavior>(['page', 'body'])
46
+
47
+ const assertExactKeys = (
48
+ presentation: Record<string, unknown>,
49
+ kind: DialogSurfacePresentation['kind'],
50
+ allowedKeys: ReadonlySet<string>,
51
+ ): void => {
52
+ const unexpectedKey = Object.keys(presentation).find((key) => !allowedKeys.has(key))
53
+ if (unexpectedKey) {
54
+ throw new DialogSurfaceContractError(
55
+ 'invalid-presentation',
56
+ `SDialogSurface presentation kind="${kind}" содержит недопустимое поле ${unexpectedKey}`,
57
+ )
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Проверяет закрытый discriminated contract приватной dialog surface без fallback-веток.
63
+ * Validates the private dialog surface discriminated contract without fallback branches.
64
+ */
65
+ export function assertDialogSurfacePresentation(
66
+ presentation: DialogSurfacePresentation,
67
+ ): asserts presentation is DialogSurfacePresentation {
68
+ const candidate = presentation as unknown as Record<string, unknown>
69
+
70
+ if (candidate.kind === 'modal') {
71
+ assertExactKeys(candidate, 'modal', modalKeys)
72
+ if (!modalSizes.has(candidate.size as DialogSurfaceModalSize)) {
73
+ throw new DialogSurfaceContractError('invalid-size', `Недопустимый размер SModal: ${String(candidate.size)}`)
74
+ }
75
+ if (typeof candidate.fullscreen !== 'boolean') {
76
+ throw new DialogSurfaceContractError('invalid-presentation', 'SModal presentation требует boolean fullscreen')
77
+ }
78
+ if (!surfaceTones.has(candidate.surface as DialogSurfaceTone)) {
79
+ throw new DialogSurfaceContractError('invalid-surface', `Недопустимая поверхность SModal: ${String(candidate.surface)}`)
80
+ }
81
+ if (!scrollBehaviors.has(candidate.scrollBehavior as DialogSurfaceScrollBehavior)) {
82
+ throw new DialogSurfaceContractError(
83
+ 'invalid-scroll-behavior',
84
+ `Недопустимый scroll behavior SModal: ${String(candidate.scrollBehavior)}`,
85
+ )
86
+ }
87
+ return
88
+ }
89
+
90
+ if (candidate.kind === 'drawer') {
91
+ assertExactKeys(candidate, 'drawer', drawerKeys)
92
+ if (!drawerSizes.has(candidate.size as DialogSurfaceDrawerSize)) {
93
+ throw new DialogSurfaceContractError('invalid-size', `Недопустимый размер SDrawer: ${String(candidate.size)}`)
94
+ }
95
+ if (typeof candidate.mobileFullScreen !== 'boolean') {
96
+ throw new DialogSurfaceContractError('invalid-presentation', 'SDrawer presentation требует boolean mobileFullScreen')
97
+ }
98
+ return
99
+ }
100
+
101
+ throw new DialogSurfaceContractError(
102
+ 'invalid-presentation',
103
+ `Неизвестный kind SDialogSurface: ${String(candidate.kind)}`,
104
+ )
105
+ }
@@ -0,0 +1,40 @@
1
+ import type { Ref } from 'vue'
2
+
3
+ import { getFocusableElements } from '../../../internal/focusNavigation'
4
+
5
+ export function trapFocusInsidePanel(event: KeyboardEvent, panelRef: Ref<HTMLElement | null>, isOpen: boolean): void {
6
+ if (event.key !== 'Tab' || !isOpen) {
7
+ return
8
+ }
9
+
10
+ const panel = panelRef.value
11
+ if (!panel) {
12
+ return
13
+ }
14
+
15
+ const visibleFocusable = getFocusableElements(panel, { requireLayout: true })
16
+
17
+ if (visibleFocusable.length === 0) {
18
+ event.preventDefault()
19
+ panel.focus()
20
+ return
21
+ }
22
+
23
+ const firstElement = visibleFocusable[0]
24
+ const lastElement = visibleFocusable[visibleFocusable.length - 1]
25
+ const activeElement = document.activeElement as HTMLElement | null
26
+ const focusIsOutsideSequence = activeElement === panel || !panel.contains(activeElement)
27
+
28
+ if (event.shiftKey) {
29
+ if (activeElement === firstElement || focusIsOutsideSequence) {
30
+ event.preventDefault()
31
+ lastElement?.focus()
32
+ }
33
+ return
34
+ }
35
+
36
+ if (activeElement === lastElement || focusIsOutsideSequence) {
37
+ event.preventDefault()
38
+ firstElement?.focus()
39
+ }
40
+ }
@@ -0,0 +1,16 @@
1
+ import type { InjectionKey } from 'vue'
2
+
3
+ export interface ListboxOptionRegistration {
4
+ token: string
5
+ element: HTMLElement
6
+ activate: () => void
7
+ disabled: () => boolean
8
+ label: () => string
9
+ selected: () => boolean
10
+ }
11
+
12
+ export interface ListboxContext {
13
+ register: (registration: ListboxOptionRegistration) => () => void
14
+ }
15
+
16
+ export const listboxContextKey: InjectionKey<ListboxContext> = Symbol('s-listbox-context')
@@ -0,0 +1,10 @@
1
+ export type SMarkerTone = 'surface' | 'primary' | 'success' | 'blue' | 'sky' | 'indigo' | 'emerald' | 'green' | 'teal' | 'amber' | 'orange' | 'rose' | 'pink' | 'fuchsia' | 'cyan' | 'lime'
2
+ export type SMarker = { tone: SMarkerTone } | { tone: 'custom'; color: `#${string}` }
3
+ export type SCustomMarker = Extract<SMarker, { tone: 'custom' }>
4
+
5
+ export function createCustomMarker(color: string): SCustomMarker {
6
+ if (!/^#[\da-f]{6}$/iu.test(color)) {
7
+ throw new Error('SMarker: custom color must be a six-digit hex value')
8
+ }
9
+ return { tone: 'custom', color: color as `#${string}` }
10
+ }
@@ -0,0 +1,23 @@
1
+ import type { InjectionKey, Ref } from 'vue'
2
+
3
+ export type MenuDensity = 'compact' | 'comfortable'
4
+
5
+ export interface MenuItemRegistration {
6
+ readonly entryId: string
7
+ readonly label: () => string
8
+ readonly hasSubmenu: () => boolean
9
+ readonly element: () => HTMLElement
10
+ readonly setTabStop: (active: boolean) => void
11
+ readonly focus: () => void
12
+ readonly isFocused: () => boolean
13
+ readonly ownsEventTarget: (target: EventTarget | null) => boolean
14
+ readonly activate: (event: KeyboardEvent) => void
15
+ }
16
+
17
+ export interface MenuContext {
18
+ density: Readonly<Ref<MenuDensity>>
19
+ register: (registration: MenuItemRegistration) => () => void
20
+ }
21
+
22
+ export const menuContextKey: InjectionKey<MenuContext> = Symbol('s-menu-context')
23
+ export const menuDepthKey: InjectionKey<number> = Symbol('s-menu-depth')
@@ -0,0 +1,239 @@
1
+ import { validateBoolean, validateNonEmptyString } from '../../../internal/runtimeContract'
2
+ import { createCustomMarker, type SMarker } from './markerContract'
3
+ import {
4
+ selectionKeyToken,
5
+ validateSelectionIdentities,
6
+ type SelectionKey,
7
+ } from './useSelectionRoving'
8
+
9
+ /** Одна типизированная option общего selection owner. / One typed option of the shared selection owner. */
10
+ export interface SelectionOption<TKey extends SelectionKey = SelectionKey> {
11
+ value: TKey
12
+ label: string
13
+ title?: string
14
+ marker?: SMarker
15
+ disabled?: boolean
16
+ }
17
+
18
+ /** Группа options с собственной стабильной identity. / An option group with its own stable identity. */
19
+ export interface SelectionOptionGroup<TKey extends SelectionKey = SelectionKey> {
20
+ groupId: string
21
+ label: string
22
+ options: readonly SelectionOption<TKey>[]
23
+ }
24
+
25
+ export type SelectionEntry<TKey extends SelectionKey = SelectionKey> =
26
+ | SelectionOption<TKey>
27
+ | SelectionOptionGroup<TKey>
28
+ export type SelectionMode = 'single' | 'multiple'
29
+ export type SelectionModelValue<
30
+ TKey extends SelectionKey,
31
+ TMode extends SelectionMode,
32
+ > = TMode extends 'multiple' ? readonly TKey[] : TKey | null
33
+
34
+ export interface ResolvedSelectionOption<TKey extends SelectionKey> {
35
+ option: SelectionOption<TKey>
36
+ token: string
37
+ coordinate: string
38
+ }
39
+
40
+ export type ResolvedSelectionEntry<TKey extends SelectionKey> =
41
+ | { kind: 'option'; record: ResolvedSelectionOption<TKey>; token: string }
42
+ | {
43
+ kind: 'group'
44
+ groupId: string
45
+ label: string
46
+ records: readonly ResolvedSelectionOption<TKey>[]
47
+ token: string
48
+ }
49
+
50
+ export interface ResolvedSelectionInventory<TKey extends SelectionKey> {
51
+ entries: readonly ResolvedSelectionEntry<TKey>[]
52
+ options: readonly ResolvedSelectionOption<TKey>[]
53
+ optionsByToken: ReadonlyMap<string, ResolvedSelectionOption<TKey>>
54
+ }
55
+
56
+ const OPTION_KEYS = new Set(['value', 'label', 'title', 'marker', 'disabled'])
57
+ const GROUP_KEYS = new Set(['groupId', 'label', 'options'])
58
+ const MARKER_TONES = [
59
+ 'surface', 'primary', 'success', 'blue', 'sky', 'indigo', 'emerald', 'green', 'teal',
60
+ 'amber', 'orange', 'rose', 'pink', 'fuchsia', 'cyan', 'lime', 'custom',
61
+ ] as const
62
+
63
+ function assertRecord(owner: string, coordinate: string, value: unknown): asserts value is Record<string, unknown> {
64
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
65
+ throw new TypeError(`${owner}: ${coordinate} must be an option object or an option group`)
66
+ }
67
+ }
68
+
69
+ function assertExactKeys(
70
+ owner: string,
71
+ coordinate: string,
72
+ value: Record<string, unknown>,
73
+ keys: ReadonlySet<string>,
74
+ ): void {
75
+ const unsupported = Object.keys(value).find((key) => !keys.has(key))
76
+ if (unsupported !== undefined) {
77
+ throw new TypeError(`${owner}: ${coordinate}.${unsupported} is not part of the selection contract`)
78
+ }
79
+ }
80
+
81
+ /** Проверяет optional selection text без coercion. / Validates optional selection text without coercion. */
82
+ export function validateOptionalSelectionString(
83
+ owner: string,
84
+ coordinate: string,
85
+ value: unknown,
86
+ ): string | undefined {
87
+ if (value === undefined) return undefined
88
+ if (typeof value !== 'string') {
89
+ throw new TypeError(`${owner}: ${coordinate} must be a string when provided`)
90
+ }
91
+ return value
92
+ }
93
+
94
+ /** Проверяет exact marker payload на selection boundary. / Validates an exact marker payload at a selection boundary. */
95
+ export function validateSelectionMarker(
96
+ owner: string,
97
+ coordinate: string,
98
+ marker: SMarker | undefined,
99
+ ): SMarker | undefined {
100
+ if (marker === undefined) return undefined
101
+ assertRecord(owner, coordinate, marker)
102
+ const tone = marker.tone
103
+ if (typeof tone !== 'string' || !MARKER_TONES.some((candidate) => candidate === tone)) {
104
+ throw new TypeError(`${owner}: ${coordinate}.tone is not a supported marker tone`)
105
+ }
106
+ const expectedKeys = tone === 'custom' ? new Set(['tone', 'color']) : new Set(['tone'])
107
+ assertExactKeys(owner, coordinate, marker, expectedKeys)
108
+ if (tone === 'custom') createCustomMarker(validateNonEmptyString(owner, `${coordinate}.color`, marker.color))
109
+ return marker
110
+ }
111
+
112
+ function resolveOption<TKey extends SelectionKey>(
113
+ owner: string,
114
+ value: SelectionOption<TKey>,
115
+ coordinate: string,
116
+ ): ResolvedSelectionOption<TKey> {
117
+ assertRecord(owner, coordinate, value)
118
+ assertExactKeys(owner, coordinate, value, OPTION_KEYS)
119
+ const key = value.value
120
+ const token = selectionKeyToken(owner, key, `${coordinate}.value`)
121
+ const label = validateNonEmptyString(owner, `${coordinate}.label`, value.label)
122
+ const title = validateOptionalSelectionString(owner, `${coordinate}.title`, value.title)
123
+ if (value.disabled !== undefined) validateBoolean(owner, `${coordinate}.disabled`, value.disabled)
124
+ const marker = validateSelectionMarker(owner, `${coordinate}.marker`, value.marker)
125
+ const option: SelectionOption<TKey> = {
126
+ value: key,
127
+ label,
128
+ ...(title === undefined ? {} : { title }),
129
+ ...(marker === undefined ? {} : { marker }),
130
+ ...(value.disabled === undefined ? {} : { disabled: value.disabled }),
131
+ }
132
+ return {
133
+ option,
134
+ token,
135
+ coordinate,
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Проверяет весь inventory и возвращает новый типизированный контейнер без непроверенных полей.
141
+ * Validates the complete inventory and returns a new typed container without unchecked fields.
142
+ */
143
+ export function resolveSelectionInventory<TKey extends SelectionKey>(
144
+ owner: string,
145
+ value: readonly SelectionEntry<TKey>[],
146
+ ): ResolvedSelectionInventory<TKey> {
147
+ if (!Array.isArray(value)) throw new TypeError(`${owner}: options must be an array`)
148
+ const typedEntries: readonly SelectionEntry<TKey>[] = value
149
+ const entries: ResolvedSelectionEntry<TKey>[] = []
150
+ const options: ResolvedSelectionOption<TKey>[] = []
151
+ const optionIdentities: Array<{ key: TKey; coordinate: string }> = []
152
+ const groupIdentities: Array<{ key: string; coordinate: string }> = []
153
+
154
+ for (const [entryIndex, entryValue] of typedEntries.entries()) {
155
+ const coordinate = `options[${entryIndex}]`
156
+ assertRecord(owner, coordinate, entryValue)
157
+ if ('groupId' in entryValue) {
158
+ assertExactKeys(owner, coordinate, entryValue, GROUP_KEYS)
159
+ const groupId = validateNonEmptyString(owner, `${coordinate}.groupId`, entryValue.groupId)
160
+ const label = validateNonEmptyString(owner, `${coordinate}.label`, entryValue.label)
161
+ if (!Array.isArray(entryValue.options)) {
162
+ throw new TypeError(`${owner}: ${coordinate}.options must be an array`)
163
+ }
164
+ if (entryValue.options.length === 0) {
165
+ throw new Error(`${owner}: ${coordinate}.options must contain at least one option`)
166
+ }
167
+ groupIdentities.push({ key: groupId, coordinate: `${coordinate}.groupId` })
168
+ const records = entryValue.options.map((option, optionIndex) => {
169
+ const optionCoordinate = `${coordinate}.options[${optionIndex}]`
170
+ const record = resolveOption<TKey>(owner, option, optionCoordinate)
171
+ optionIdentities.push({ key: record.option.value, coordinate: `${optionCoordinate}.value` })
172
+ options.push(record)
173
+ return record
174
+ })
175
+ entries.push({
176
+ kind: 'group',
177
+ groupId,
178
+ label,
179
+ records,
180
+ token: `group:${selectionKeyToken(owner, groupId, `${coordinate}.groupId`)}`,
181
+ })
182
+ continue
183
+ }
184
+
185
+ const record = resolveOption<TKey>(owner, entryValue, coordinate)
186
+ optionIdentities.push({ key: record.option.value, coordinate: `${coordinate}.value` })
187
+ options.push(record)
188
+ entries.push({ kind: 'option', record, token: `option:${record.token}` })
189
+ }
190
+
191
+ validateSelectionIdentities(`${owner} groups`, groupIdentities)
192
+ validateSelectionIdentities(owner, optionIdentities)
193
+ return {
194
+ entries,
195
+ options,
196
+ optionsByToken: new Map(options.map((record) => [record.token, record])),
197
+ }
198
+ }
199
+
200
+ /** Проверяет controlled model относительно точного inventory. / Validates the controlled model against the exact inventory. */
201
+ export function validateSelectionModel<TKey extends SelectionKey>(
202
+ owner: string,
203
+ mode: SelectionMode,
204
+ modelValue: TKey | null | readonly TKey[] | undefined,
205
+ inventory: ResolvedSelectionInventory<TKey>,
206
+ ): ReadonlySet<string> {
207
+ if (modelValue === undefined) throw new TypeError(`${owner}: modelValue is required and cannot be undefined`)
208
+ if (mode === 'multiple') {
209
+ if (!Array.isArray(modelValue)) {
210
+ throw new TypeError(`${owner}: multiple selection requires modelValue to be an array`)
211
+ }
212
+ const tokens = validateSelectionIdentities(`${owner} modelValue`, modelValue.map((key, index) => ({
213
+ key,
214
+ coordinate: `modelValue[${index}]`,
215
+ })))
216
+ for (const token of tokens.keys()) {
217
+ if (!inventory.optionsByToken.has(token)) {
218
+ throw new Error(`${owner}: modelValue does not match every option`)
219
+ }
220
+ }
221
+ return new Set(tokens.keys())
222
+ }
223
+ if (isSelectionModelList(modelValue)) {
224
+ throw new TypeError(`${owner}: single selection requires modelValue to be a selection key or null`)
225
+ }
226
+ if (modelValue === null) return new Set()
227
+ const token = selectionKeyToken(owner, modelValue, 'modelValue')
228
+ if (!inventory.optionsByToken.has(token)) {
229
+ throw new Error(`${owner}: modelValue does not match any option`)
230
+ }
231
+ return new Set([token])
232
+ }
233
+
234
+ /** Narrowing helper for the exact multiple-selection model branch. */
235
+ export function isSelectionModelList<TKey extends SelectionKey>(
236
+ value: TKey | null | readonly TKey[],
237
+ ): value is readonly TKey[] {
238
+ return Array.isArray(value)
239
+ }
@@ -0,0 +1,31 @@
1
+ import type { ComputedRef, InjectionKey } from 'vue';
2
+
3
+ import type { SectionState, SidebarSectionsState } from '../containers/sidebar';
4
+
5
+ /** Stable mounted descriptor registered by one SSidebarSection. */
6
+ export interface SidebarSectionRegistration {
7
+ readonly key: string;
8
+ readonly element: HTMLElement;
9
+ }
10
+
11
+ /** DOM-ordered registered section with its exact controlled state. */
12
+ export interface RegisteredSidebarSection {
13
+ readonly key: string;
14
+ readonly element: HTMLElement;
15
+ readonly state: Readonly<SectionState>;
16
+ }
17
+
18
+ export type SidebarSectionStyle = Readonly<Record<string, string | number>>;
19
+
20
+ export interface SidebarGroupContext {
21
+ readonly sectionsState: ComputedRef<SidebarSectionsState>;
22
+ readonly orderedSections: ComputedRef<readonly RegisteredSidebarSection[]>;
23
+ registerSection(registration: SidebarSectionRegistration): () => void;
24
+ toggleSection(key: string): void;
25
+ getSectionStyle(key: string): SidebarSectionStyle;
26
+ beginResize(topKey: string, bottomKey: string): void;
27
+ resizeSections(topKey: string, bottomKey: string, requestedTopSize: number): void;
28
+ finishResize(topKey: string, bottomKey: string): void;
29
+ }
30
+
31
+ export const sidebarGroupContextKey: InjectionKey<SidebarGroupContext> = Symbol('SidebarGroupContext');
@@ -0,0 +1,57 @@
1
+ import type { ComputedRef, InjectionKey } from 'vue'
2
+
3
+ export type STabsActivationMode = 'automatic' | 'manual'
4
+ export type STabRegistrationKey = string
5
+ export type STabPanelRegistrationKey = string
6
+ export type STabListRegistrationKey = symbol
7
+ export type STabPanelsRegistrationKey = symbol
8
+
9
+ export interface STabRegistration {
10
+ readonly tabKey: string
11
+ readonly disabled: () => boolean
12
+ readonly element: () => HTMLElement | null
13
+ }
14
+
15
+ export interface STabPanelRegistration {
16
+ readonly tabKey: string
17
+ readonly element: () => HTMLElement | null
18
+ }
19
+
20
+ export interface STabState {
21
+ readonly active: boolean
22
+ readonly disabled: boolean
23
+ readonly tabIndex: 0 | -1
24
+ readonly tabId: string
25
+ readonly panelId: string | undefined
26
+ }
27
+
28
+ export interface STabPanelState {
29
+ readonly active: boolean
30
+ readonly panelId: string
31
+ readonly tabId: string | undefined
32
+ }
33
+
34
+ export interface STabsContext {
35
+ readonly activeKey: ComputedRef<string | null>
36
+ readonly activationMode: ComputedRef<STabsActivationMode>
37
+ readonly closeButtonMode: ComputedRef<'reserve' | 'overlay'>
38
+ readonly tabDensity: ComputedRef<'compact' | 'default' | 'comfortable'>
39
+ readonly registerTab: (registration: STabRegistration) => STabRegistrationKey
40
+ readonly unregisterTab: (key: STabRegistrationKey) => void
41
+ readonly notifyTabOrderChanged: () => void
42
+ readonly getTabState: (key: STabRegistrationKey) => STabState
43
+ readonly getActiveTabElement: () => HTMLElement | null
44
+ readonly activateTab: (key: STabRegistrationKey) => void
45
+ readonly focusTab: (key: STabRegistrationKey) => void
46
+ readonly registerPanel: (registration: STabPanelRegistration) => STabPanelRegistrationKey
47
+ readonly unregisterPanel: (key: STabPanelRegistrationKey) => void
48
+ readonly notifyPanelOrderChanged: () => void
49
+ readonly getPanelState: (key: STabPanelRegistrationKey) => STabPanelState
50
+ readonly handleTabKeydown: (key: STabRegistrationKey, event: KeyboardEvent) => void
51
+ readonly registerTabList: (scrollActiveIntoCenter: () => void) => STabListRegistrationKey
52
+ readonly unregisterTabList: (key: STabListRegistrationKey) => void
53
+ readonly registerPanels: () => STabPanelsRegistrationKey
54
+ readonly unregisterPanels: (key: STabPanelsRegistrationKey) => void
55
+ }
56
+
57
+ export const tabsContextKey: InjectionKey<STabsContext> = Symbol('STabsContext')