@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,186 @@
1
+ <template>
2
+ <div
3
+ v-bind="ownedAttrs.bindings()"
4
+ class="s-context-menu-boundary"
5
+ :data-layout="boundaryLayout"
6
+ @click="handleBoundaryClick"
7
+ @contextmenu="handleBoundaryContextMenu"
8
+ >
9
+ <slot :trigger-props="triggerProps" />
10
+ <teleport to="body">
11
+ <transition
12
+ enter-active-class="transition ease-out duration-[var(--s-motion-fast)]"
13
+ enter-from-class="transform opacity-0 scale-95"
14
+ enter-to-class="transform opacity-100 scale-100"
15
+ leave-active-class="transition ease-in duration-[var(--s-motion-instant)]"
16
+ leave-from-class="transform opacity-100 scale-100"
17
+ leave-to-class="transform opacity-0 scale-95"
18
+ >
19
+ <SMenuLayer
20
+ v-if="isOpen"
21
+ ref="layerRef"
22
+ kind="context"
23
+ landmark-label="Контекстные действия"
24
+ :position="position"
25
+ :z-index="zIndex"
26
+ >
27
+ <SMenuSurface
28
+ :id="menuId"
29
+ label="Контекстные действия"
30
+ autofocus
31
+ :initial-focus="initialMenuFocus"
32
+ :items="items"
33
+ @dismiss-tree="closeMenu('outside')"
34
+ @escape="closeMenu('escape')"
35
+ @focus-exit="closeMenu('outside')"
36
+ @select="handleSelect"
37
+ @tab="closeMenuForTab"
38
+ />
39
+ </SMenuLayer>
40
+ </transition>
41
+ </teleport>
42
+ </div>
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ import { computed, ref, useId, watch, watchEffect } from 'vue'
47
+ import type { VNodeChild } from 'vue'
48
+ import { findFocusableOwner } from '../../../internal/focusNavigation'
49
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
50
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
51
+ import { useMenuLayer } from '../../../internal/useMenuLayer'
52
+ import SMenuLayer from '../_internal/SMenuLayer.vue'
53
+ import type { SMenuLayerApi } from '../_internal/SMenuLayer.vue'
54
+ import SMenuSurface from './SMenuSurface.vue'
55
+ import { validateSMenuEntries, type SMenuActivationEvent, type SMenuEntry, type SMenuSelectableEntry } from './menu'
56
+
57
+ defineOptions({ inheritAttrs: false })
58
+
59
+ export interface Props {
60
+ items: readonly SMenuEntry[];
61
+ /** False оставляет boundary полностью inert: не открывает menu и не перехватывает native events. / False keeps the boundary fully inert: it neither opens the menu nor intercepts native events. */
62
+ enabled?: boolean;
63
+ trigger?: 'contextmenu' | 'click' | 'both';
64
+ boundaryLayout?: 'block' | 'inline' | 'contents';
65
+ }
66
+
67
+ /**
68
+ * ARIA-связь canonical trigger с принадлежащим ему context-menu layer.
69
+ * ARIA relationship between the canonical trigger and its owned context-menu layer.
70
+ */
71
+ export interface SContextMenuTriggerProps {
72
+ readonly 'aria-haspopup'?: 'menu'
73
+ readonly 'aria-expanded'?: boolean
74
+ readonly 'aria-controls'?: string
75
+ readonly onKeydown?: (event: KeyboardEvent) => void
76
+ }
77
+
78
+ const props = withDefaults(defineProps<Props>(), {
79
+ enabled: true,
80
+ trigger: 'contextmenu',
81
+ boundaryLayout: 'block',
82
+ })
83
+ const emit = defineEmits<{
84
+ select: [entry: SMenuSelectableEntry, event: SMenuActivationEvent]
85
+ }>()
86
+ defineSlots<{
87
+ /** Canonical interactive owner получает готовые ARIA props. / The canonical interactive owner receives complete ARIA props. */
88
+ default?: (props: { triggerProps: SContextMenuTriggerProps }) => VNodeChild
89
+ }>()
90
+ const ownedAttrs = useOwnedAttrs({ component: 'SContextMenu', owner: 'interaction boundary root' })
91
+ useInteractiveLeafRegistration({ owner: 'SContextMenu', active: () => props.enabled })
92
+
93
+ const generatedMenuId = useId()
94
+ const menuId = `s-context-menu-${generatedMenuId}`
95
+ const initialMenuFocus = ref<'first' | 'last'>('first')
96
+ const layerRef = ref<SMenuLayerApi | null>(null)
97
+ const layerElement = computed(() => layerRef.value?.getElement() ?? null)
98
+ const {
99
+ isOpen,
100
+ position,
101
+ zIndex,
102
+ open,
103
+ close: closeMenu,
104
+ closeForTab: closeMenuForTab,
105
+ } = useMenuLayer(layerElement)
106
+ const triggerProps = computed<SContextMenuTriggerProps>(() => (
107
+ props.enabled
108
+ ? {
109
+ 'aria-haspopup': 'menu',
110
+ 'aria-expanded': isOpen.value,
111
+ 'aria-controls': menuId,
112
+ onKeydown: handleTriggerKeydown,
113
+ }
114
+ : {}
115
+ ))
116
+
117
+ const openMenu = async (event: MouseEvent): Promise<void> => {
118
+ if (!props.enabled) return
119
+ initialMenuFocus.value = 'first'
120
+ const boundary = event.currentTarget instanceof Element ? event.currentTarget : null
121
+ const focusTarget = findFocusableOwner(event.target, boundary)
122
+ ?? findFocusableOwner(document.activeElement)
123
+ const isKeyboardActivation = event.clientX === 0 && event.clientY === 0 && event.detail === 0
124
+ await open(
125
+ isKeyboardActivation && focusTarget
126
+ ? { type: 'element', element: focusTarget, align: 'left' }
127
+ : { type: 'point', x: event.clientX, y: event.clientY },
128
+ focusTarget,
129
+ )
130
+ }
131
+
132
+ const handleTriggerKeydown = (event: KeyboardEvent): void => {
133
+ if (
134
+ !props.enabled
135
+ || (props.trigger !== 'click' && props.trigger !== 'both')
136
+ || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
137
+ ) return
138
+ const trigger = event.currentTarget
139
+ if (!(trigger instanceof HTMLElement)) {
140
+ throw new TypeError('SContextMenu: trigger keydown currentTarget must be an HTMLElement')
141
+ }
142
+ event.preventDefault()
143
+ event.stopPropagation()
144
+ initialMenuFocus.value = event.key === 'ArrowUp' ? 'last' : 'first'
145
+ void open({ type: 'element', element: trigger, align: 'left' }, trigger)
146
+ }
147
+
148
+ const activateBoundary = (event: MouseEvent): void => {
149
+ if (!props.enabled) return
150
+ event.preventDefault()
151
+ event.stopPropagation()
152
+ void openMenu(event)
153
+ }
154
+
155
+ const handleBoundaryClick = (event: MouseEvent): void => {
156
+ if (props.trigger === 'click' || props.trigger === 'both') activateBoundary(event)
157
+ }
158
+
159
+ const handleBoundaryContextMenu = (event: MouseEvent): void => {
160
+ if (props.trigger === 'contextmenu' || props.trigger === 'both') activateBoundary(event)
161
+ }
162
+
163
+ watch(() => props.enabled, (enabled) => {
164
+ if (!enabled) closeMenu('programmatic')
165
+ })
166
+
167
+ const handleSelect = (item: SMenuSelectableEntry, event: SMenuActivationEvent): void => {
168
+ if (item.disabled) return
169
+ closeMenu('selection')
170
+ emit('select', item, event)
171
+ }
172
+
173
+ watchEffect(() => {
174
+ validateSMenuEntries(props.items)
175
+ if (typeof props.enabled !== 'boolean') throw new TypeError('SContextMenu: enabled must be a boolean')
176
+ if (!['contextmenu', 'click', 'both'].includes(props.trigger)) {
177
+ throw new TypeError("SContextMenu: trigger must be 'contextmenu', 'click', or 'both'")
178
+ }
179
+ if (!['block', 'inline', 'contents'].includes(props.boundaryLayout)) {
180
+ throw new TypeError("SContextMenu: boundaryLayout must be 'block', 'inline', or 'contents'")
181
+ }
182
+ })
183
+
184
+ </script>
185
+
186
+ <style lang="postcss" src="./SContextMenu.css" scoped></style>
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <SButton
3
+ v-bind="ownedAttrs.bindings()"
4
+ appearance="text"
5
+ icon-only
6
+ shape="circle"
7
+ :size="size"
8
+ :severity="partial ? 'warn' : inContext ? 'primary' : 'secondary'"
9
+ :title="accessibleTitle"
10
+ :aria-label="accessibleTitle"
11
+ :pressed="pressedState"
12
+ :leading-icon="stateIcon"
13
+ :loading="loading"
14
+ @click="emit('click')"
15
+ />
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { computed, type Component } from 'vue'
20
+ import { Minus, MinusCircle, PlusCircle } from '../../icons/sputnigUiIcons'
21
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
22
+ import { validateNonEmptyString } from '../../../internal/runtimeContract'
23
+ import SButton from './SButton.vue'
24
+
25
+ defineOptions({ inheritAttrs: false })
26
+
27
+ /** Доменный toggle добавления сущности в контекст. / Domain toggle for adding an entity to context. */
28
+ export interface Props {
29
+ inContext: boolean
30
+ partial?: boolean
31
+ addTitle?: string
32
+ removeTitle?: string
33
+ size?: 'sm' | 'md' | 'lg'
34
+ loading?: boolean
35
+ }
36
+
37
+ const props = withDefaults(defineProps<Props>(), {
38
+ partial: false,
39
+ addTitle: 'Добавить в контекст',
40
+ removeTitle: 'Убрать из контекста',
41
+ size: 'sm',
42
+ loading: false,
43
+ })
44
+
45
+ const emit = defineEmits<{
46
+ (e: 'click'): void
47
+ }>()
48
+ const ownedAttrs = useOwnedAttrs({ component: 'SContextToggleButton', owner: 'toggle button' })
49
+
50
+ const accessibleLabels = computed(() => ({
51
+ add: validateNonEmptyString('SContextToggleButton', 'addTitle', props.addTitle),
52
+ remove: validateNonEmptyString('SContextToggleButton', 'removeTitle', props.removeTitle),
53
+ }))
54
+ const accessibleTitle = computed(() => (
55
+ props.inContext ? accessibleLabels.value.remove : accessibleLabels.value.add
56
+ ))
57
+ const pressedState = computed<boolean | 'mixed'>(() => (
58
+ props.partial ? 'mixed' : props.inContext
59
+ ))
60
+ const stateIcon = computed<Component>(() => {
61
+ if (!props.inContext) return PlusCircle
62
+ return props.partial ? Minus : MinusCircle
63
+ })
64
+ </script>
@@ -0,0 +1,100 @@
1
+ <template>
2
+ <SButton
3
+ v-bind="ownedAttrs.bindings()"
4
+ size="sm"
5
+ severity="secondary"
6
+ appearance="ghost"
7
+ icon-only
8
+ :leading-icon="DragIcon"
9
+ :disabled="disabled"
10
+ :pressed="keyboardActive"
11
+ :title="accessibleLabel"
12
+ :aria-label="accessibleLabel"
13
+ :aria-keyshortcuts="keyboardShortcuts"
14
+ data-s-drag-handle
15
+ :data-state="keyboardActive ? 'keyboard-active' : 'idle'"
16
+ @click="handleClick"
17
+ @pointerdown="handlePointerDown"
18
+ @keydown="handleKeydown"
19
+ />
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import { computed, ref, watchEffect } from 'vue'
24
+ import { getSputnigUiIconComponent } from '../../icons/sputnigUiIcons'
25
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
26
+ import { validateNonEmptyString } from '../../../internal/runtimeContract'
27
+ import SButton from './SButton.vue'
28
+
29
+ defineOptions({ inheritAttrs: false })
30
+
31
+ const props = withDefaults(defineProps<{
32
+ label: string
33
+ orientation?: 'vertical' | 'horizontal'
34
+ canMovePrevious?: boolean
35
+ canMoveNext?: boolean
36
+ disabled?: boolean
37
+ }>(), {
38
+ orientation: 'vertical',
39
+ canMovePrevious: false,
40
+ canMoveNext: false,
41
+ disabled: false,
42
+ })
43
+
44
+ const emit = defineEmits<{
45
+ (event: 'drag-start', pointerEvent: PointerEvent): void
46
+ (event: 'reorder', direction: -1 | 1): void
47
+ (event: 'keyboard-active-change', active: boolean): void
48
+ }>()
49
+
50
+ const DragIcon = getSputnigUiIconComponent('drag')
51
+ const ownedAttrs = useOwnedAttrs({ component: 'SDragHandle', owner: 'drag handle command' })
52
+ const keyboardActive = ref(false)
53
+ const validatedLabel = computed(() => validateNonEmptyString('SDragHandle', 'label', props.label))
54
+ const keyboardShortcuts = computed(() => props.orientation === 'vertical'
55
+ ? 'Enter Space ArrowUp ArrowDown Escape'
56
+ : 'Enter Space ArrowLeft ArrowRight Escape')
57
+ const accessibleLabel = computed(() => keyboardActive.value
58
+ ? `Перемещение: ${validatedLabel.value}. Используйте стрелки, Enter — завершить, Escape — отменить`
59
+ : `Переместить: ${validatedLabel.value}. Нажмите Enter и используйте стрелки`)
60
+
61
+ function setKeyboardActive(active: boolean): void {
62
+ if (keyboardActive.value === active) return
63
+ keyboardActive.value = active
64
+ emit('keyboard-active-change', active)
65
+ }
66
+
67
+ function handleClick(event: MouseEvent): void {
68
+ if (props.disabled || event.detail > 0) return
69
+ setKeyboardActive(!keyboardActive.value)
70
+ }
71
+
72
+ function handlePointerDown(event: PointerEvent): void {
73
+ if (props.disabled) return
74
+ setKeyboardActive(false)
75
+ emit('drag-start', event)
76
+ }
77
+
78
+ function handleKeydown(event: KeyboardEvent): void {
79
+ if (props.disabled || !keyboardActive.value) return
80
+ if (event.key === 'Escape') {
81
+ event.preventDefault()
82
+ setKeyboardActive(false)
83
+ return
84
+ }
85
+
86
+ const previousKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
87
+ const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'
88
+ if (event.key !== previousKey && event.key !== nextKey) return
89
+
90
+ event.preventDefault()
91
+ const direction = event.key === previousKey ? -1 : 1
92
+ if (direction === -1 && !props.canMovePrevious) return
93
+ if (direction === 1 && !props.canMoveNext) return
94
+ emit('reorder', direction)
95
+ }
96
+
97
+ watchEffect(() => {
98
+ if (props.disabled) setKeyboardActive(false)
99
+ })
100
+ </script>
@@ -0,0 +1,5 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-dropdown-menu {
4
+ @apply relative inline-block self-start text-left;
5
+ }
@@ -0,0 +1,188 @@
1
+ <template>
2
+ <div class="s-dropdown-menu">
3
+ <slot v-if="hasCustomTrigger" :trigger-attrs="triggerAttrs()" />
4
+ <SButton
5
+ v-else
6
+ v-bind="triggerAttrs()"
7
+ label="Опции"
8
+ :disclosure="{ expanded: isOpen, controls: menuId }"
9
+ severity="secondary"
10
+ />
11
+ <teleport to="body">
12
+ <SMenuLayer
13
+ v-if="isOpen"
14
+ ref="layerRef"
15
+ kind="dropdown"
16
+ :landmark-label="menuLabel"
17
+ :position="position"
18
+ :z-index="zIndex"
19
+ >
20
+ <SMenuSurface
21
+ ref="menuSurfaceRef"
22
+ :id="menuId"
23
+ :label="menuLabel"
24
+ density="compact"
25
+ autofocus
26
+ :initial-focus="initialFocus"
27
+ :items="items"
28
+ @dismiss-tree="close('outside')"
29
+ @escape="close('escape')"
30
+ @focus-exit="close('outside')"
31
+ @select="select"
32
+ @tab="closeForTab"
33
+ />
34
+ </SMenuLayer>
35
+ </teleport>
36
+ </div>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import {
41
+ computed,
42
+ nextTick,
43
+ onMounted,
44
+ ref,
45
+ useId,
46
+ watchEffect,
47
+ type VNodeChild,
48
+ type VNodeRef,
49
+ } from 'vue'
50
+ import { useOwnedAttrs, type OwnedAttributeBindings } from '../../../internal/ownedAttrs'
51
+ import { useMenuLayer } from '../../../internal/useMenuLayer'
52
+ import SMenuLayer from '../_internal/SMenuLayer.vue'
53
+ import type { SMenuLayerApi } from '../_internal/SMenuLayer.vue'
54
+ import SButton from './SButton.vue'
55
+ import SMenuSurface, { type SMenuSurfaceApi } from './SMenuSurface.vue'
56
+ import { validateSMenuEntries, type SMenuActivationEvent, type SMenuEntry, type SMenuSelectableEntry } from './menu'
57
+
58
+ defineOptions({ inheritAttrs: false })
59
+
60
+ const props = withDefaults(defineProps<{
61
+ items: readonly SMenuEntry[]
62
+ align?: 'left' | 'right'
63
+ menuLabel?: string
64
+ }>(), {
65
+ align: 'right',
66
+ menuLabel: 'Дополнительные действия',
67
+ })
68
+ const emit = defineEmits<{
69
+ select: [entry: SMenuSelectableEntry, event: SMenuActivationEvent]
70
+ }>()
71
+ export type SDropdownMenuTriggerAttrs = OwnedAttributeBindings<'SDropdownMenu'> & {
72
+ readonly ref: VNodeRef
73
+ readonly 'aria-haspopup': 'menu'
74
+ readonly 'aria-expanded': boolean
75
+ readonly 'aria-controls': string
76
+ readonly onClick: (event?: Event) => void
77
+ readonly onKeydown: (event: KeyboardEvent) => void
78
+ }
79
+ const slots = defineSlots<{
80
+ /** Единственный custom-trigger contract: attrs должны принадлежать реальному control. / The sole custom-trigger contract: attrs must be bound to the actual control. */
81
+ default?: (props: { triggerAttrs: SDropdownMenuTriggerAttrs }) => VNodeChild
82
+ }>()
83
+
84
+ const ownedAttrs = useOwnedAttrs({ component: 'SDropdownMenu', owner: 'menu trigger' })
85
+ const layerRef = ref<SMenuLayerApi | null>(null)
86
+ const menuSurfaceRef = ref<SMenuSurfaceApi | null>(null)
87
+ let registeredTriggerTarget: Element | object | null = null
88
+ const layerElement = computed(() => layerRef.value?.getElement() ?? null)
89
+ const {
90
+ isOpen,
91
+ position,
92
+ zIndex,
93
+ close,
94
+ closeForTab,
95
+ open,
96
+ toggle: toggleLayer,
97
+ } = useMenuLayer(layerElement)
98
+ const menuId = `s-dropdown-menu-${useId()}`
99
+ const initialFocus = ref<'first' | 'last'>('first')
100
+ const hasCustomTrigger = Boolean(slots.default)
101
+
102
+ function resolveTriggerElement(target: Element | object | null): HTMLElement | null {
103
+ if (target === null) return null
104
+ if (target instanceof HTMLElement) return target
105
+ const getElement = Reflect.get(target, 'getElement')
106
+ if (typeof getElement !== 'function') {
107
+ throw new Error('SDropdownMenu: trigger ref must resolve to an HTMLElement or UI-kit leaf API')
108
+ }
109
+ const element: unknown = Reflect.apply(getElement, target, [])
110
+ if (!(element instanceof HTMLElement)) {
111
+ throw new Error('SDropdownMenu: trigger leaf getElement() must return an HTMLElement')
112
+ }
113
+ return element
114
+ }
115
+
116
+ const bindTriggerElement: VNodeRef = (target) => {
117
+ if (target !== null && registeredTriggerTarget !== null && target !== registeredTriggerTarget) {
118
+ throw new Error('SDropdownMenu: trigger binding must belong to exactly one UI-kit leaf')
119
+ }
120
+ registeredTriggerTarget = target
121
+ }
122
+
123
+ const triggerAttrs = (): SDropdownMenuTriggerAttrs => {
124
+ const attrs = ownedAttrs.bindings()
125
+ if (Object.prototype.hasOwnProperty.call(attrs, 'onClick')) {
126
+ throw new Error('SDropdownMenu: @click belongs to the typed triggerAttrs activation contract')
127
+ }
128
+ return {
129
+ ...attrs,
130
+ ref: bindTriggerElement,
131
+ 'aria-haspopup': 'menu' as const,
132
+ 'aria-expanded': isOpen.value,
133
+ 'aria-controls': menuId,
134
+ onClick: toggle,
135
+ onKeydown: handleTriggerKeydown,
136
+ }
137
+ }
138
+
139
+ function triggerElement(event?: Event): HTMLElement {
140
+ const trigger = resolveTriggerElement(registeredTriggerTarget)
141
+ if (!trigger) throw new Error('SDropdownMenu: expected exactly one trigger, received 0')
142
+ if (event?.currentTarget instanceof HTMLElement && event.currentTarget !== trigger) {
143
+ throw new Error('SDropdownMenu: activation target does not match the registered trigger')
144
+ }
145
+ return trigger
146
+ }
147
+
148
+ onMounted(() => {
149
+ const trigger = triggerElement()
150
+ if (trigger.getAttribute('aria-haspopup') !== 'menu' || trigger.getAttribute('aria-controls') !== menuId) {
151
+ throw new Error('SDropdownMenu: expected exactly one trigger, received 0')
152
+ }
153
+ })
154
+
155
+ function toggle(event?: Event): void {
156
+ const trigger = triggerElement(event)
157
+ initialFocus.value = 'first'
158
+ toggleLayer({ type: 'element', element: trigger, align: props.align }, trigger)
159
+ }
160
+
161
+ async function handleTriggerKeydown(event: KeyboardEvent): Promise<void> {
162
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
163
+ event.preventDefault()
164
+ event.stopPropagation()
165
+ const trigger = triggerElement(event)
166
+ initialFocus.value = event.key === 'ArrowUp' ? 'last' : 'first'
167
+ if (!isOpen.value) {
168
+ await open({ type: 'element', element: trigger, align: props.align }, trigger)
169
+ }
170
+ await nextTick()
171
+ if (initialFocus.value === 'last') menuSurfaceRef.value?.focusLast()
172
+ else menuSurfaceRef.value?.focusFirst()
173
+ }
174
+
175
+ function select(item: SMenuSelectableEntry, event: SMenuActivationEvent): void {
176
+ if (item.disabled) return
177
+ close('selection')
178
+ emit('select', item, event)
179
+ }
180
+
181
+ watchEffect(() => {
182
+ validateSMenuEntries(props.items)
183
+ if (!props.menuLabel.trim()) throw new TypeError('SDropdownMenu: menuLabel must be a non-empty string')
184
+ if (props.align !== 'left' && props.align !== 'right') throw new TypeError("SDropdownMenu: align must be 'left' or 'right'")
185
+ })
186
+ </script>
187
+
188
+ <style lang="postcss" src="./SDropdownMenu.css" scoped></style>
@@ -0,0 +1,79 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-field {
4
+ @apply grid w-full min-w-0;
5
+ grid-template-areas:
6
+ "label"
7
+ "control"
8
+ "help"
9
+ "error";
10
+ }
11
+
12
+ .s-field[data-fill='container'] {
13
+ @apply h-full;
14
+ }
15
+
16
+ .s-field--stacked[data-fill='container'] {
17
+ grid-template-rows: auto minmax(0, 1fr) auto auto;
18
+ }
19
+
20
+ .s-field--inline[data-fill='container'] {
21
+ grid-template-rows: minmax(0, 1fr) auto auto;
22
+ }
23
+
24
+ .s-field[data-fill='container'] .s-field__control {
25
+ @apply min-h-0;
26
+ }
27
+
28
+ .s-field--inline {
29
+ @apply items-start gap-x-2;
30
+ grid-template-columns: auto minmax(0, 1fr);
31
+ grid-template-areas:
32
+ "control label"
33
+ ". help"
34
+ ". error";
35
+ }
36
+
37
+ .s-field__label {
38
+ grid-area: label;
39
+ @apply mb-1 block min-w-0 text-sm font-medium text-surface-700 dark:text-surface-300;
40
+ }
41
+
42
+ .s-field--inline .s-field__label {
43
+ @apply mb-0 cursor-pointer whitespace-normal;
44
+ overflow-wrap: anywhere;
45
+ }
46
+
47
+ .s-field__control {
48
+ grid-area: control;
49
+ @apply min-w-0;
50
+ }
51
+
52
+ .s-field--inline .s-field__control {
53
+ @apply flex items-center;
54
+ }
55
+
56
+ .s-field__required {
57
+ @apply ml-0.5 text-danger-600 dark:text-danger-400;
58
+ }
59
+
60
+ .s-field__help,
61
+ .s-field__error {
62
+ @apply mt-1 min-w-0 whitespace-normal text-xs;
63
+ overflow-wrap: anywhere;
64
+ }
65
+
66
+ .s-field__help {
67
+ grid-area: help;
68
+ @apply text-surface-500 dark:text-surface-400;
69
+ }
70
+
71
+ .s-field__error {
72
+ grid-area: error;
73
+ @apply text-danger-600 dark:text-danger-400;
74
+ }
75
+
76
+ .s-field[data-disabled='true'] .s-field__label,
77
+ .s-field[data-readonly='true'] .s-field__label {
78
+ color: var(--s-field-disabled-foreground);
79
+ }