@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,551 @@
1
+ <template>
2
+ <div
3
+ v-bind="rootAttrs.bindings()"
4
+ class="s-popover-wrapper"
5
+ >
6
+ <div class="s-popover relative block w-full">
7
+ <slot
8
+ v-if="$slots.trigger"
9
+ name="trigger"
10
+ :bindTrigger="bindTrigger"
11
+ :open="openPopover"
12
+ :close="closeExplicitly"
13
+ :isOpen="isOpen"
14
+ />
15
+ <SButton
16
+ v-else
17
+ v-bind="builtInTriggerBinding"
18
+ data-s-popover-trigger=""
19
+ :label="triggerLabel"
20
+ :leading-icon="triggerLeadingIcon"
21
+ :trailing-icon="triggerTrailingIcon"
22
+ :icon-only="triggerIconOnly"
23
+ :appearance="triggerAppearance"
24
+ :size="triggerSize"
25
+ :width="triggerWidth"
26
+ :content-align="triggerContentAlign"
27
+ :shape="triggerShape"
28
+ :severity="triggerSeverity"
29
+ :badge="triggerBadge"
30
+ :disabled="disabled"
31
+ :title="triggerTitle"
32
+ :aria-label="resolvedTriggerAriaLabel"
33
+ :data-testid="triggerTestId"
34
+ >
35
+ <slot :isOpen="isOpen" />
36
+ </SButton>
37
+
38
+ <transition
39
+ enter-active-class="transition duration-[var(--s-motion-standard)] ease-out"
40
+ enter-from-class="translate-y-1 opacity-0"
41
+ enter-to-class="translate-y-0 opacity-100"
42
+ leave-active-class="pointer-events-none transition duration-[var(--s-motion-fast)] ease-in"
43
+ leave-from-class="translate-y-0 opacity-100"
44
+ leave-to-class="translate-y-1 opacity-0"
45
+ @enter="focusEnteredPanel"
46
+ >
47
+ <Teleport to="body">
48
+ <div
49
+ v-if="isOpen"
50
+ ref="panelRef"
51
+ class="s-popover-panel"
52
+ :style="floatingStyle"
53
+ :data-placement="resolvedPlacement"
54
+ :data-surface="surface"
55
+ :data-width="width"
56
+ tabindex="-1"
57
+ role="dialog"
58
+ :id="panelId"
59
+ :aria-label="ariaLabel"
60
+ @keydown.tab="handlePanelTab"
61
+ @focusout="handlePanelFocusout"
62
+ >
63
+ <div class="s-popover-content">
64
+ <slot name="panel" :close="closeExplicitly" />
65
+ </div>
66
+ </div>
67
+ </Teleport>
68
+ </transition>
69
+ </div>
70
+ </div>
71
+ </template>
72
+
73
+ <script setup lang="ts">
74
+ import {
75
+ computed,
76
+ ref,
77
+ onMounted,
78
+ onUpdated,
79
+ onBeforeUnmount,
80
+ nextTick,
81
+ useId,
82
+ useSlots,
83
+ watch,
84
+ type Component,
85
+ } from 'vue';
86
+ import SButton, {
87
+ type SButtonAppearance,
88
+ type SButtonContentAlign,
89
+ type SButtonSeverity,
90
+ type SButtonShape,
91
+ type SButtonSize,
92
+ type SButtonWidth,
93
+ } from '../controls/SButton.vue';
94
+ import { useFloatingPosition, type FloatingPlacement } from '../../../composables/useFloatingPosition';
95
+ import { findRelativeTabTarget, getFocusableElements } from '../../../internal/focusNavigation';
96
+ import { resolveInteractiveElement } from '../../../internal/interactiveElement';
97
+ import { registerLayer, type LayerRegistration } from '../../../internal/layerStack';
98
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs';
99
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract';
100
+ import type {
101
+ SPopoverDefaultSlotProps,
102
+ SPopoverPanelSlotProps,
103
+ SPopoverPublicApi,
104
+ SPopoverTriggerActivation,
105
+ SPopoverTriggerBinding,
106
+ SPopoverTriggerRefTarget,
107
+ SPopoverTriggerSlotProps,
108
+ } from './popover';
109
+
110
+ export interface Props {
111
+ align?: 'left' | 'right' | 'center';
112
+ surface?: 'default' | 'transparent';
113
+ width?: 'trigger' | 'content' | 'sm' | 'md' | 'lg';
114
+ ariaLabel?: string;
115
+ triggerAppearance?: SButtonAppearance;
116
+ triggerSize?: SButtonSize;
117
+ triggerWidth?: SButtonWidth;
118
+ triggerContentAlign?: SButtonContentAlign;
119
+ triggerShape?: SButtonShape;
120
+ triggerLabel?: string;
121
+ triggerLeadingIcon?: Component;
122
+ triggerTrailingIcon?: Component;
123
+ triggerIconOnly?: boolean;
124
+ triggerSeverity?: SButtonSeverity;
125
+ triggerBadge?: string | number;
126
+ triggerTitle?: string;
127
+ triggerAriaLabel?: string;
128
+ triggerTestId?: string;
129
+ disabled?: boolean;
130
+ /**
131
+ * Управляемая видимость; без prop owner хранит toggle-state сам.
132
+ * Controlled visibility; omit to use the owner's internal toggle state.
133
+ */
134
+ open?: boolean;
135
+ }
136
+
137
+ defineOptions({ inheritAttrs: false });
138
+
139
+ const props = withDefaults(defineProps<Props>(), {
140
+ align: 'right',
141
+ surface: 'default',
142
+ width: 'trigger',
143
+ ariaLabel: 'Дополнительные действия',
144
+ triggerAppearance: 'ghost',
145
+ triggerSize: 'md',
146
+ triggerWidth: 'full',
147
+ triggerContentAlign: 'center',
148
+ triggerShape: 'default',
149
+ triggerLabel: undefined,
150
+ triggerLeadingIcon: undefined,
151
+ triggerTrailingIcon: undefined,
152
+ triggerIconOnly: false,
153
+ triggerSeverity: 'secondary',
154
+ triggerBadge: undefined,
155
+ triggerTitle: undefined,
156
+ triggerAriaLabel: undefined,
157
+ triggerTestId: undefined,
158
+ disabled: false,
159
+ open: undefined,
160
+ });
161
+ const emit = defineEmits<{
162
+ (e: 'update:open', value: boolean): void;
163
+ }>();
164
+ defineSlots<{
165
+ trigger?: (props: SPopoverTriggerSlotProps) => unknown;
166
+ default?: (props: SPopoverDefaultSlotProps) => unknown;
167
+ panel: (props: SPopoverPanelSlotProps) => unknown;
168
+ }>();
169
+
170
+ type CloseReason = 'explicit' | 'escape' | 'outside' | 'tab' | 'trigger';
171
+
172
+ const BUILT_IN_TRIGGER_ID = 'built-in';
173
+ const slots = useSlots();
174
+ const rootAttrs = useOwnedAttrs({ component: 'SPopover', owner: 'root wrapper' });
175
+ if (slots.trigger && slots.default) {
176
+ throw new Error(
177
+ 'SPopover: default slot is reserved for built-in SButton content and cannot be combined with #trigger; move popover content to #panel',
178
+ );
179
+ }
180
+ if (!slots.panel) {
181
+ throw new Error('SPopover: #panel slot is required');
182
+ }
183
+
184
+ const controlledMode = props.open !== undefined;
185
+ const uncontrolledOpen = ref(false);
186
+ const isOpen = computed(() => props.open ?? uncontrolledOpen.value);
187
+ useInteractiveLeafRegistration({ owner: 'SPopover', active: () => isOpen.value });
188
+ const panelId = `s-popover-panel-${useId()}`;
189
+ const activeTriggerId = ref<string | null>(null);
190
+ const layerZIndex = ref('var(--s-layer-floating)');
191
+ const resolvedTriggerAriaLabel = computed(() =>
192
+ props.triggerAriaLabel ?? (
193
+ props.triggerIconOnly || (!props.triggerLabel && !slots.default)
194
+ ? props.ariaLabel
195
+ : undefined
196
+ ),
197
+ );
198
+ let layerRegistration: LayerRegistration | null = null;
199
+ let pendingCloseReason: CloseReason | null = null;
200
+ const placement = computed<FloatingPlacement>(() => {
201
+ if (props.align === 'left') return 'bottom-start';
202
+ if (props.align === 'center') return 'bottom-center';
203
+ return 'bottom-end';
204
+ });
205
+ const {
206
+ anchorRef: triggerRef,
207
+ panelRef,
208
+ floatingStyle,
209
+ resolvedPlacement,
210
+ updatePosition,
211
+ } = useFloatingPosition({
212
+ active: isOpen,
213
+ placement: () => placement.value,
214
+ offset: () => 6,
215
+ boundaryPadding: () => 8,
216
+ flip: () => true,
217
+ shift: () => true,
218
+ widthMatch: () => props.width === 'trigger' ? 'min' : 'none',
219
+ zIndex: () => layerZIndex.value,
220
+ scrollStrategy: 'reposition',
221
+ });
222
+
223
+ class TriggerRecord {
224
+ target: SPopoverTriggerRefTarget = null;
225
+ readonly binding: SPopoverTriggerBinding;
226
+
227
+ constructor(
228
+ readonly id: string,
229
+ readonly activation: SPopoverTriggerActivation,
230
+ ) {
231
+ this.binding = {
232
+ ref: (target) => assignTriggerElement(this, target),
233
+ get ['aria-expanded']() {
234
+ return isOpen.value && activeTriggerId.value === id;
235
+ },
236
+ get disclosure(): SPopoverTriggerBinding['disclosure'] {
237
+ return {
238
+ expanded: isOpen.value && activeTriggerId.value === id,
239
+ controls: panelId,
240
+ iconPlacement: 'none',
241
+ };
242
+ },
243
+ 'aria-haspopup': 'dialog',
244
+ 'aria-controls': panelId,
245
+ onClick: (event) => activateBoundTrigger(this, event),
246
+ };
247
+ }
248
+
249
+ element(): HTMLElement | null {
250
+ return resolveTriggerElement(this.target, this.id);
251
+ }
252
+ }
253
+
254
+ const triggerRecords = new Map<string, TriggerRecord>();
255
+
256
+ function requireTriggerId(triggerId: string): void {
257
+ if (triggerId.length === 0 || triggerId.trim() !== triggerId) {
258
+ throw new Error('SPopover: trigger id must be a non-empty string without surrounding whitespace');
259
+ }
260
+ }
261
+
262
+ function resolveTriggerElement(target: SPopoverTriggerRefTarget, triggerId: string): HTMLElement | null {
263
+ return resolveInteractiveElement(`SPopover: trigger "${triggerId}"`, target);
264
+ }
265
+
266
+ function assignTriggerElement(record: TriggerRecord, target: SPopoverTriggerRefTarget): void {
267
+ if (target === null) {
268
+ record.target = null;
269
+ return;
270
+ }
271
+ if (record.target && record.target !== target) {
272
+ throw new Error(`SPopover: trigger id "${record.id}" is bound to multiple elements`);
273
+ }
274
+ for (const existing of triggerRecords.values()) {
275
+ if (existing !== record && existing.target === target) {
276
+ throw new Error(
277
+ `SPopover: one trigger element cannot use both "${existing.id}" and "${record.id}" ids`,
278
+ );
279
+ }
280
+ }
281
+ record.target = target;
282
+ }
283
+
284
+ function bindTrigger(
285
+ triggerId: string,
286
+ activation: SPopoverTriggerActivation = 'toggle',
287
+ ): SPopoverTriggerBinding {
288
+ requireTriggerId(triggerId);
289
+ const existing = triggerRecords.get(triggerId);
290
+ if (existing) {
291
+ if (existing.activation !== activation) {
292
+ throw new Error(
293
+ `SPopover: trigger "${triggerId}" activation cannot change from "${existing.activation}" to "${activation}"`,
294
+ );
295
+ }
296
+ return existing.binding;
297
+ }
298
+ const record = new TriggerRecord(triggerId, activation);
299
+ triggerRecords.set(triggerId, record);
300
+ return record.binding;
301
+ }
302
+
303
+ const builtInTriggerBinding = slots.trigger
304
+ ? undefined
305
+ : bindTrigger(BUILT_IN_TRIGGER_ID);
306
+
307
+ function completeBindingError(triggerId: string): Error {
308
+ return new Error(
309
+ `SPopover: custom trigger "${triggerId}" must apply the complete object returned by bindTrigger()`,
310
+ );
311
+ }
312
+
313
+ function assertTriggerBinding(record: TriggerRecord): HTMLElement {
314
+ const element = record.element();
315
+ if (!element) {
316
+ throw completeBindingError(record.id);
317
+ }
318
+ if (slots.trigger && !(element instanceof HTMLButtonElement)) {
319
+ throw new Error(
320
+ `SPopover: custom trigger "${record.id}" must bind directly to a button element, not to a wrapper`,
321
+ );
322
+ }
323
+ if (
324
+ element.getAttribute('aria-haspopup') !== 'dialog'
325
+ || element.getAttribute('aria-controls') !== panelId
326
+ || element.getAttribute('aria-expanded') !== String(isOpen.value && activeTriggerId.value === record.id)
327
+ ) {
328
+ throw completeBindingError(record.id);
329
+ }
330
+ return element;
331
+ }
332
+
333
+ function validateMountedTriggerContract(): void {
334
+ if (triggerRecords.size === 0) {
335
+ throw new Error('SPopover: #trigger must apply a binding returned by bindTrigger()');
336
+ }
337
+ for (const record of triggerRecords.values()) {
338
+ assertTriggerBinding(record);
339
+ }
340
+ }
341
+
342
+ function registeredTriggerIds(): string[] {
343
+ return [...triggerRecords.values()]
344
+ .filter((record) => record.element()?.isConnected)
345
+ .map((record) => record.id)
346
+ .sort((left, right) => left.localeCompare(right));
347
+ }
348
+
349
+ function resolveOpenTrigger(triggerId?: string): TriggerRecord {
350
+ if (triggerId !== undefined) {
351
+ requireTriggerId(triggerId);
352
+ const record = triggerRecords.get(triggerId);
353
+ if (!record?.element()?.isConnected) {
354
+ throw new Error(`SPopover: programmatic open requires a registered trigger with id "${triggerId}"`);
355
+ }
356
+ assertTriggerBinding(record);
357
+ return record;
358
+ }
359
+
360
+ const ids = registeredTriggerIds();
361
+ if (ids.length === 0) {
362
+ throw new Error('SPopover: programmatic open requires one registered trigger anchor');
363
+ }
364
+ if (ids.length > 1) {
365
+ throw new Error(
366
+ `SPopover: programmatic open is ambiguous; pass one of the registered trigger ids: ${ids.map((id) => `"${id}"`).join(', ')}`,
367
+ );
368
+ }
369
+ const [onlyId] = ids;
370
+ if (!onlyId) {
371
+ throw new Error('SPopover: registered trigger inventory is unexpectedly empty');
372
+ }
373
+ const record = triggerRecords.get(onlyId);
374
+ if (!record) {
375
+ throw new Error('SPopover: registered trigger invariant failed');
376
+ }
377
+ assertTriggerBinding(record);
378
+ return record;
379
+ }
380
+
381
+ function selectAnchor(record: TriggerRecord): HTMLElement {
382
+ const element = assertTriggerBinding(record);
383
+ activeTriggerId.value = record.id;
384
+ triggerRef.value = element;
385
+ return element;
386
+ }
387
+
388
+ function registerOpenLayer(restoreTarget: HTMLElement): void {
389
+ layerRegistration?.unregister({ restoreFocus: false });
390
+ layerRegistration = registerLayer({
391
+ kind: 'popover',
392
+ root: () => panelRef.value,
393
+ anchor: () => triggerRef.value,
394
+ branches: () => [...triggerRecords.values()]
395
+ .map((record) => record.element())
396
+ .filter((element): element is HTMLElement => Boolean(element?.isConnected)),
397
+ restoreTarget,
398
+ requestDismiss: (reason) => closePopover(reason === 'escape' ? 'escape' : 'outside'),
399
+ });
400
+ layerZIndex.value = layerRegistration.zIndex;
401
+ }
402
+
403
+ function resolveCurrentOpenTrigger(): TriggerRecord {
404
+ if (activeTriggerId.value !== null) {
405
+ const activeRecord = triggerRecords.get(activeTriggerId.value);
406
+ if (activeRecord?.element()?.isConnected) return activeRecord;
407
+ }
408
+ return resolveOpenTrigger();
409
+ }
410
+
411
+ function activateOpenState(record: TriggerRecord = resolveCurrentOpenTrigger()): void {
412
+ const anchor = selectAnchor(record);
413
+ registerOpenLayer(anchor);
414
+ nextTick(updatePosition);
415
+ }
416
+
417
+ function focusEnteredPanel(element: Element): void {
418
+ if (!(element instanceof HTMLElement)) {
419
+ throw new Error('SPopover: transition enter target must be an HTMLElement');
420
+ }
421
+ if (isOpen.value) element.focus();
422
+ }
423
+
424
+ function deactivateOpenState(reason: CloseReason): void {
425
+ const shouldRestoreFocus = reason === 'explicit' || reason === 'escape' || reason === 'trigger';
426
+ layerRegistration?.unregister({ restoreFocus: shouldRestoreFocus });
427
+ layerRegistration = null;
428
+ layerZIndex.value = 'var(--s-layer-floating)';
429
+ }
430
+
431
+ function assertControlMode(): void {
432
+ if ((props.open !== undefined) !== controlledMode) {
433
+ throw new Error('SPopover: open control mode cannot change after mount');
434
+ }
435
+ }
436
+
437
+ function activateBoundTrigger(record: TriggerRecord, _event: MouseEvent): void {
438
+ if (record.activation === 'manual') {
439
+ selectAnchor(record);
440
+ if (isOpen.value) {
441
+ nextTick(updatePosition);
442
+ }
443
+ return;
444
+ }
445
+ if (isOpen.value && activeTriggerId.value === record.id) {
446
+ closePopover('trigger');
447
+ return;
448
+ }
449
+ if (isOpen.value) {
450
+ const anchor = selectAnchor(record);
451
+ registerOpenLayer(anchor);
452
+ nextTick(updatePosition);
453
+ return;
454
+ }
455
+ openPopover(record.id);
456
+ }
457
+
458
+ function openPopover(triggerId?: string): void {
459
+ if (!isOpen.value && !props.disabled) {
460
+ const record = resolveOpenTrigger(triggerId);
461
+ selectAnchor(record);
462
+ emit('update:open', true);
463
+ if (controlledMode) return;
464
+ activateOpenState(record);
465
+ uncontrolledOpen.value = true;
466
+ return;
467
+ }
468
+ if (isOpen.value && triggerId !== undefined && triggerId !== activeTriggerId.value) {
469
+ const record = resolveOpenTrigger(triggerId);
470
+ const anchor = selectAnchor(record);
471
+ registerOpenLayer(anchor);
472
+ nextTick(updatePosition);
473
+ }
474
+ }
475
+ function closePopover(reason: CloseReason = 'explicit'): void {
476
+ const wasOpen = isOpen.value;
477
+ if (wasOpen) {
478
+ pendingCloseReason = reason;
479
+ emit('update:open', false);
480
+ if (controlledMode) return;
481
+ uncontrolledOpen.value = false;
482
+ deactivateOpenState(reason);
483
+ pendingCloseReason = null;
484
+ }
485
+ }
486
+
487
+ function closeExplicitly(): void {
488
+ closePopover('explicit');
489
+ }
490
+
491
+ function handlePanelTab(event: KeyboardEvent): void {
492
+ const panel = panelRef.value;
493
+ const anchor = triggerRef.value;
494
+ if (!panel || !anchor) return;
495
+ const focusable = getFocusableElements(panel);
496
+ const active = document.activeElement;
497
+ const first = focusable[0];
498
+ const last = focusable.at(-1);
499
+ const leavesPanel = focusable.length === 0
500
+ || (event.shiftKey ? active === panel || active === first : active === last);
501
+ if (!leavesPanel) return;
502
+ const scope = layerRegistration?.focusScope() ?? document;
503
+ const target = findRelativeTabTarget(anchor, scope, panel, event.shiftKey);
504
+ event.preventDefault();
505
+ closePopover('tab');
506
+ if (target) void nextTick(() => target.focus());
507
+ }
508
+
509
+ function handlePanelFocusout(event: FocusEvent): void {
510
+ if (!isOpen.value) return;
511
+ const nextTarget = event.relatedTarget;
512
+ if (nextTarget instanceof Node) {
513
+ if (panelRef.value?.contains(nextTarget)) return;
514
+ const isTrigger = [...triggerRecords.values()].some((record) => record.element()?.contains(nextTarget));
515
+ if (isTrigger) return;
516
+ }
517
+ closePopover('outside');
518
+ }
519
+
520
+ onMounted(() => {
521
+ validateMountedTriggerContract();
522
+ if (isOpen.value) activateOpenState();
523
+ });
524
+ onUpdated(() => {
525
+ assertControlMode();
526
+ });
527
+ watch(
528
+ () => props.open,
529
+ (open) => {
530
+ assertControlMode();
531
+ if (open) {
532
+ activateOpenState();
533
+ return;
534
+ }
535
+ deactivateOpenState(pendingCloseReason ?? 'explicit');
536
+ pendingCloseReason = null;
537
+ },
538
+ { flush: 'post' },
539
+ );
540
+ onBeforeUnmount(() => {
541
+ layerRegistration?.unregister({ restoreFocus: false });
542
+ layerRegistration = null;
543
+ });
544
+
545
+ defineExpose<SPopoverPublicApi>({
546
+ open: openPopover,
547
+ close: closeExplicitly,
548
+ });
549
+ </script>
550
+
551
+ <style lang="postcss" src="./SPopover.css" scoped></style>
@@ -0,0 +1,12 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-sidebar-group {
4
+ @apply w-full;
5
+ @apply flex flex-col;
6
+ @apply min-h-0;
7
+ @apply flex-grow;
8
+ @apply overflow-hidden;
9
+ }
10
+
11
+ /* Этот файл определяет базовый flex-контейнер для группы секций. */
12
+