@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,37 @@
1
+ export type ClipboardCopyResult =
2
+ | { status: 'copied'; method: 'clipboard-api' }
3
+ | { status: 'empty' }
4
+ | { status: 'unavailable'; reason: 'clipboard-api-missing' }
5
+ | { status: 'failed'; error: Error | DOMException }
6
+
7
+ export interface ClipboardService {
8
+ copyText: (value: string) => Promise<ClipboardCopyResult>
9
+ }
10
+
11
+ /**
12
+ * Предоставляет единый типизированный контракт копирования текста.
13
+ * Provides the canonical typed text-copying contract.
14
+ */
15
+ export function useClipboard(): ClipboardService {
16
+ const copyText = async (value: string): Promise<ClipboardCopyResult> => {
17
+ if (!value) return { status: 'empty' }
18
+
19
+ const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard
20
+ if (!clipboard?.writeText) {
21
+ return { status: 'unavailable', reason: 'clipboard-api-missing' }
22
+ }
23
+
24
+ try {
25
+ await clipboard.writeText(value)
26
+ return { status: 'copied', method: 'clipboard-api' }
27
+ } catch (cause) {
28
+ const isDomException = typeof DOMException !== 'undefined' && cause instanceof DOMException
29
+ const error = cause instanceof Error || isDomException
30
+ ? cause
31
+ : new Error('Clipboard API rejected writeText without an Error object', { cause })
32
+ return { status: 'failed', error }
33
+ }
34
+ }
35
+
36
+ return { copyText }
37
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ nextTick,
3
+ onBeforeUnmount,
4
+ ref,
5
+ watch,
6
+ type ComponentPublicInstance,
7
+ type Ref,
8
+ } from 'vue'
9
+ import { trapFocusInsidePanel } from '../components/shared/_internal/focusTrap'
10
+ import { registerLayer, type LayerRegistration } from '../internal/layerStack'
11
+
12
+ export interface DialogOverlayOptions {
13
+ open: Readonly<Ref<boolean>>
14
+ closable: Readonly<Ref<boolean>>
15
+ requestClose: () => void
16
+ }
17
+
18
+ export interface DialogOverlayController {
19
+ panelRef: Ref<HTMLElement | null>
20
+ setPanelElement: (element: Element | ComponentPublicInstance | null) => void
21
+ backdropStyle: Readonly<Ref<Record<string, string>>>
22
+ surfaceStyle: Readonly<Ref<Record<string, string>>>
23
+ }
24
+
25
+ /**
26
+ * Владеет modal-регистрацией, focus trap, body lock и восстановлением фокуса.
27
+ * Owns modal registration, focus trapping, body lock, and focus restoration.
28
+ */
29
+ export function useDialogOverlay(options: DialogOverlayOptions): DialogOverlayController {
30
+ const panelRef = ref<HTMLElement | null>(null)
31
+ const backdropStyle = ref<Record<string, string>>({})
32
+ const surfaceStyle = ref<Record<string, string>>({})
33
+ let registration: LayerRegistration | null = null
34
+
35
+ const setPanelElement = (element: Element | ComponentPublicInstance | null): void => {
36
+ if (element === null || element instanceof HTMLElement) {
37
+ panelRef.value = element
38
+ return
39
+ }
40
+ throw new TypeError('Dialog overlay panel ref обязан указывать на HTMLElement')
41
+ }
42
+
43
+ const registerOpen = (): void => {
44
+ if (registration) return
45
+ const restoreTarget = typeof document !== 'undefined' && document.activeElement instanceof HTMLElement
46
+ ? document.activeElement
47
+ : null
48
+ registration = registerLayer({
49
+ kind: 'dialog',
50
+ root: () => panelRef.value,
51
+ modal: true,
52
+ restoreTarget,
53
+ dismissOnEscape: () => options.closable.value,
54
+ dismissOnOutsidePointer: () => options.closable.value,
55
+ trapFocus: (event) => trapFocusInsidePanel(event, panelRef, true),
56
+ requestDismiss: () => options.requestClose(),
57
+ })
58
+ backdropStyle.value = { zIndex: registration.zIndex }
59
+ surfaceStyle.value = {
60
+ zIndex: `calc(${registration.zIndex} + var(--s-layer-overlay-surface-offset))`,
61
+ }
62
+ }
63
+
64
+ const unregisterOpen = (restoreFocus = true): void => {
65
+ registration?.unregister({ restoreFocus })
66
+ registration = null
67
+ backdropStyle.value = {}
68
+ surfaceStyle.value = {}
69
+ }
70
+
71
+ watch(options.open, async (open, wasOpen) => {
72
+ if (open) {
73
+ registerOpen()
74
+ await nextTick()
75
+ if (options.open.value) panelRef.value?.focus()
76
+ return
77
+ }
78
+
79
+ unregisterOpen(Boolean(wasOpen))
80
+ }, { immediate: true })
81
+
82
+ onBeforeUnmount(() => unregisterOpen(false))
83
+
84
+ return { panelRef, setPanelElement, backdropStyle, surfaceStyle }
85
+ }
@@ -0,0 +1,40 @@
1
+ import { computed, ref, toValue, useId, type MaybeRefOrGetter, type Ref } from 'vue'
2
+
3
+ export interface UseDisclosureOptions {
4
+ id?: string
5
+ open?: Readonly<Ref<boolean>>
6
+ defaultOpen?: boolean
7
+ disabled?: MaybeRefOrGetter<boolean>
8
+ onUpdate?: (open: boolean) => void
9
+ }
10
+
11
+ /** Canonical controlled/uncontrolled state and ID contract for button disclosures. */
12
+ export function useDisclosure(options: UseDisclosureOptions = {}) {
13
+ if (options.id !== undefined && options.id.trim().length === 0) {
14
+ throw new Error('useDisclosure: id must be a non-empty string')
15
+ }
16
+ if (options.open && !options.onUpdate) {
17
+ throw new Error('useDisclosure: controlled open requires onUpdate')
18
+ }
19
+ if (options.open && options.defaultOpen !== undefined) {
20
+ throw new Error('useDisclosure: controlled open cannot be combined with defaultOpen')
21
+ }
22
+
23
+ const generatedId = useId()
24
+ const baseId = options.id ?? `s-disclosure-${generatedId}`
25
+ const internalOpen = ref(options.defaultOpen ?? false)
26
+ const isControlled = options.open !== undefined
27
+ const isDisabled = computed(() => toValue(options.disabled ?? false))
28
+ const isOpen = computed(() => isControlled ? options.open!.value : internalOpen.value)
29
+ const triggerId = computed(() => `${baseId}-trigger`)
30
+ const panelId = computed(() => `${baseId}-panel`)
31
+
32
+ const setOpen = (nextOpen: boolean): void => {
33
+ if (isDisabled.value || nextOpen === isOpen.value) return
34
+ if (isControlled) options.onUpdate!(nextOpen)
35
+ else internalOpen.value = nextOpen
36
+ }
37
+ const toggle = (): void => setOpen(!isOpen.value)
38
+
39
+ return { isOpen, isDisabled, triggerId, panelId, setOpen, toggle }
40
+ }
@@ -0,0 +1,245 @@
1
+ import { nextTick, onBeforeUnmount, onMounted, ref, watch, type Ref } from 'vue'
2
+
3
+ export type FloatingSide = 'top' | 'bottom' | 'left' | 'right'
4
+ export type FloatingAlignment = 'start' | 'center' | 'end'
5
+ export type FloatingPlacement = `${FloatingSide}-${FloatingAlignment}`
6
+ export type FloatingWidthMatch = 'none' | 'min' | 'exact'
7
+
8
+ export interface FloatingPositionOptions {
9
+ active: Readonly<Ref<boolean>>
10
+ placement: () => FloatingPlacement
11
+ anchorElement?: () => HTMLElement | null
12
+ anchorRect?: () => DOMRect | null
13
+ panelRef?: Ref<HTMLElement | null>
14
+ offset?: () => number
15
+ boundaryPadding?: () => number
16
+ flip?: () => boolean
17
+ shift?: () => boolean
18
+ widthMatch?: () => FloatingWidthMatch
19
+ zIndex?: () => string
20
+ scrollStrategy?: 'reposition' | 'close'
21
+ requestClose?: () => void
22
+ }
23
+
24
+ export interface FloatingPositionController {
25
+ anchorRef: Ref<HTMLElement | null>
26
+ panelRef: Ref<HTMLElement | null>
27
+ floatingStyle: Ref<Record<string, string>>
28
+ arrowStyle: Ref<Record<string, string>>
29
+ resolvedPlacement: Ref<FloatingPlacement>
30
+ coordinates: Ref<FloatingCoordinates>
31
+ updatePosition: () => void
32
+ }
33
+
34
+ /** Координаты floating-поверхности во viewport. / Floating-surface viewport coordinates. */
35
+ export interface FloatingCoordinates {
36
+ left: number
37
+ top: number
38
+ }
39
+
40
+ const oppositeSide: Record<FloatingSide, FloatingSide> = {
41
+ top: 'bottom',
42
+ bottom: 'top',
43
+ left: 'right',
44
+ right: 'left',
45
+ }
46
+
47
+ function splitPlacement(placement: FloatingPlacement): [FloatingSide, FloatingAlignment] {
48
+ return placement.split('-') as [FloatingSide, FloatingAlignment]
49
+ }
50
+
51
+ function alignedOffset(start: number, anchorSize: number, panelSize: number, alignment: FloatingAlignment): number {
52
+ if (alignment === 'start') return start
53
+ if (alignment === 'end') return start + anchorSize - panelSize
54
+ return start + (anchorSize - panelSize) / 2
55
+ }
56
+
57
+ function coordinatesFor(
58
+ side: FloatingSide,
59
+ alignment: FloatingAlignment,
60
+ anchor: DOMRect,
61
+ panelWidth: number,
62
+ panelHeight: number,
63
+ offset: number,
64
+ ): FloatingCoordinates {
65
+ if (side === 'top' || side === 'bottom') {
66
+ return {
67
+ left: alignedOffset(anchor.left, anchor.width, panelWidth, alignment),
68
+ top: side === 'top' ? anchor.top - panelHeight - offset : anchor.bottom + offset,
69
+ }
70
+ }
71
+ return {
72
+ left: side === 'left' ? anchor.left - panelWidth - offset : anchor.right + offset,
73
+ top: alignedOffset(anchor.top, anchor.height, panelHeight, alignment),
74
+ }
75
+ }
76
+
77
+ function mainAxisOverflow(
78
+ side: FloatingSide,
79
+ coordinates: FloatingCoordinates,
80
+ panelWidth: number,
81
+ panelHeight: number,
82
+ viewportWidth: number,
83
+ viewportHeight: number,
84
+ padding: number,
85
+ ): number {
86
+ if (side === 'top') return Math.max(0, padding - coordinates.top)
87
+ if (side === 'bottom') return Math.max(0, coordinates.top + panelHeight - (viewportHeight - padding))
88
+ if (side === 'left') return Math.max(0, padding - coordinates.left)
89
+ return Math.max(0, coordinates.left + panelWidth - (viewportWidth - padding))
90
+ }
91
+
92
+ function clampArrowCoordinate(coordinate: number, panelSize: number): number {
93
+ const inset = Math.min(10, panelSize / 2)
94
+ return Math.max(inset, Math.min(coordinate, panelSize - inset))
95
+ }
96
+
97
+ /**
98
+ * Единый владелец geometry/collision/observer-контракта floating surfaces.
99
+ * Canonical owner of floating-surface geometry, collision and observers.
100
+ */
101
+ export function useFloatingPosition(options: FloatingPositionOptions): FloatingPositionController {
102
+ const anchorRef = ref<HTMLElement | null>(null)
103
+ const panelRef = options.panelRef ?? ref<HTMLElement | null>(null)
104
+ const floatingStyle = ref<Record<string, string>>({})
105
+ const arrowStyle = ref<Record<string, string>>({})
106
+ const resolvedPlacement = ref<FloatingPlacement>(options.placement())
107
+ const resolvedCoordinates = ref<FloatingCoordinates>({ left: -10000, top: -10000 })
108
+ let resizeObserver: ResizeObserver | null = null
109
+ let listenersAttached = false
110
+
111
+ const updatePosition = (): void => {
112
+ if (!options.active.value || typeof window === 'undefined') return
113
+ const anchorElement = options.anchorElement?.() ?? anchorRef.value
114
+ const panelElement = panelRef.value
115
+ const anchor = options.anchorRect?.() ?? anchorElement?.getBoundingClientRect() ?? null
116
+ if (!anchor || !panelElement) return
117
+
118
+ const panel = panelElement.getBoundingClientRect()
119
+ const panelWidth = panel.width || panelElement.offsetWidth
120
+ const panelHeight = panel.height || panelElement.offsetHeight
121
+ const offset = options.offset?.() ?? 6
122
+ const padding = options.boundaryPadding?.() ?? 8
123
+ const viewportWidth = window.innerWidth
124
+ const viewportHeight = window.innerHeight
125
+ const [preferredSide, alignment] = splitPlacement(options.placement())
126
+ let side = preferredSide
127
+ let coordinates = coordinatesFor(side, alignment, anchor, panelWidth, panelHeight, offset)
128
+
129
+ if (options.flip?.() ?? true) {
130
+ const alternateSide = oppositeSide[side]
131
+ const alternateCoordinates = coordinatesFor(alternateSide, alignment, anchor, panelWidth, panelHeight, offset)
132
+ const preferredOverflow = mainAxisOverflow(
133
+ side, coordinates, panelWidth, panelHeight, viewportWidth, viewportHeight, padding,
134
+ )
135
+ const alternateOverflow = mainAxisOverflow(
136
+ alternateSide, alternateCoordinates, panelWidth, panelHeight, viewportWidth, viewportHeight, padding,
137
+ )
138
+ if (alternateOverflow < preferredOverflow) {
139
+ side = alternateSide
140
+ coordinates = alternateCoordinates
141
+ }
142
+ }
143
+
144
+ if (options.shift?.() ?? true) {
145
+ coordinates.left = Math.max(padding, Math.min(coordinates.left, viewportWidth - panelWidth - padding))
146
+ coordinates.top = Math.max(padding, Math.min(coordinates.top, viewportHeight - panelHeight - padding))
147
+ }
148
+
149
+ const placement = `${side}-${alignment}` as FloatingPlacement
150
+ resolvedPlacement.value = placement
151
+ resolvedCoordinates.value = {
152
+ left: Math.round(coordinates.left),
153
+ top: Math.round(coordinates.top),
154
+ }
155
+ const widthMatch = options.widthMatch?.() ?? 'none'
156
+ floatingStyle.value = {
157
+ position: 'fixed',
158
+ left: `${resolvedCoordinates.value.left}px`,
159
+ top: `${resolvedCoordinates.value.top}px`,
160
+ zIndex: options.zIndex?.() ?? 'var(--s-layer-floating)',
161
+ ...(widthMatch === 'min' ? { minWidth: `${Math.round(anchor.width)}px` } : {}),
162
+ ...(widthMatch === 'exact' ? { width: `${Math.round(anchor.width)}px` } : {}),
163
+ }
164
+
165
+ if (side === 'top' || side === 'bottom') {
166
+ const anchorCenter = anchor.left + anchor.width / 2 - coordinates.left
167
+ arrowStyle.value = {
168
+ left: `${Math.round(clampArrowCoordinate(anchorCenter, panelWidth))}px`,
169
+ }
170
+ } else {
171
+ const anchorCenter = anchor.top + anchor.height / 2 - coordinates.top
172
+ arrowStyle.value = {
173
+ top: `${Math.round(clampArrowCoordinate(anchorCenter, panelHeight))}px`,
174
+ }
175
+ }
176
+ }
177
+
178
+ const handleScroll = (): void => {
179
+ if (!options.active.value) return
180
+ if (options.scrollStrategy === 'close') {
181
+ options.requestClose?.()
182
+ return
183
+ }
184
+ updatePosition()
185
+ }
186
+
187
+ const attachListeners = (): void => {
188
+ if (listenersAttached || typeof window === 'undefined') return
189
+ listenersAttached = true
190
+ window.addEventListener('resize', updatePosition, { passive: true })
191
+ window.addEventListener('scroll', handleScroll, { passive: true, capture: true })
192
+ if (typeof ResizeObserver !== 'undefined') {
193
+ resizeObserver = new ResizeObserver(updatePosition)
194
+ const observedAnchor = options.anchorElement?.() ?? anchorRef.value
195
+ if (observedAnchor) resizeObserver.observe(observedAnchor)
196
+ if (panelRef.value) resizeObserver.observe(panelRef.value)
197
+ }
198
+ }
199
+
200
+ const detachListeners = (): void => {
201
+ if (!listenersAttached || typeof window === 'undefined') return
202
+ listenersAttached = false
203
+ window.removeEventListener('resize', updatePosition)
204
+ window.removeEventListener('scroll', handleScroll, true)
205
+ resizeObserver?.disconnect()
206
+ resizeObserver = null
207
+ }
208
+
209
+ watch(options.active, async (active) => {
210
+ if (!active) {
211
+ detachListeners()
212
+ floatingStyle.value = {}
213
+ arrowStyle.value = {}
214
+ resolvedCoordinates.value = { left: -10000, top: -10000 }
215
+ return
216
+ }
217
+ floatingStyle.value = {
218
+ position: 'fixed',
219
+ left: '-10000px',
220
+ top: '-10000px',
221
+ zIndex: options.zIndex?.() ?? 'var(--s-layer-floating)',
222
+ }
223
+ await nextTick()
224
+ attachListeners()
225
+ updatePosition()
226
+ }, { immediate: true })
227
+
228
+ onMounted(() => {
229
+ if (options.active.value) {
230
+ attachListeners()
231
+ updatePosition()
232
+ }
233
+ })
234
+ onBeforeUnmount(detachListeners)
235
+
236
+ return {
237
+ anchorRef,
238
+ panelRef,
239
+ floatingStyle,
240
+ arrowStyle,
241
+ resolvedPlacement,
242
+ coordinates: resolvedCoordinates,
243
+ updatePosition,
244
+ }
245
+ }
@@ -0,0 +1,26 @@
1
+ import { useNotifierStore } from '../stores/useNotifierStore'
2
+ import type { NotificationInput, Severity } from '../stores/useNotifierStore'
3
+
4
+ export type NotifyOptions = Pick<NotificationInput, 'title' | 'message' | 'duration'>
5
+
6
+ type NotifyArg = NotifyOptions | string
7
+
8
+ // This composable provides a clean API to trigger notifications from any component.
9
+ export function useNotifier() {
10
+ const store = useNotifierStore()
11
+
12
+ const notify = (severity: Severity, options: NotifyArg) => {
13
+ const payload: NotifyOptions = typeof options === 'string' ? { message: options } : options
14
+ store.add({
15
+ severity,
16
+ ...payload,
17
+ })
18
+ }
19
+
20
+ return {
21
+ info: (options: NotifyArg) => notify('info', options),
22
+ success: (options: NotifyArg) => notify('success', options),
23
+ warn: (options: NotifyArg) => notify('warn', options),
24
+ danger: (options: NotifyArg) => notify('danger', options),
25
+ }
26
+ }