@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,268 @@
1
+ <template>
2
+ <div
3
+ v-bind="ownedAttrs.bindings()"
4
+ ref="sectionEl"
5
+ class="s-sidebar-section-wrapper"
6
+ :class="{ 's-sidebar-section-wrapper--resizable-before': isResizableBefore }"
7
+ :style="style"
8
+ >
9
+ <!-- Разделитель должен быть ПЕРЕД секцией, а не внутри нее. -->
10
+ <SResizeHandle
11
+ v-if="isResizableBefore"
12
+ orientation="horizontal"
13
+ label="Изменить высоту секций"
14
+ :value="resizeContract.value"
15
+ :min="resizeContract.min"
16
+ :max="resizeContract.max"
17
+ :step="10"
18
+ :data-test-id="resizeContract.testId"
19
+ @resize-start="handleResizeStart"
20
+ @resize="handleResize"
21
+ @resize-end="handleResizeEnd"
22
+ />
23
+ <div class="s-sidebar-section">
24
+ <div class="s-sidebar-section-header" :class="{ 's-sidebar-section-header--mobile': layout === 'mobile' }">
25
+ <div class="s-sidebar-section-trigger">
26
+ <SButton
27
+ :id="triggerId"
28
+ :size="layout === 'mobile' ? 'xs' : 'sm'"
29
+ severity="secondary"
30
+ appearance="ghost"
31
+ shape="row"
32
+ width="full"
33
+ content-align="start"
34
+ data-sidebar-section-toggle
35
+ :disabled="disabled"
36
+ :disclosure="{ expanded: isOpen, controls: panelId, iconPlacement: 'leading' }"
37
+ @click="disclosure.toggle"
38
+ >
39
+ <SDisclosureSummary
40
+ :title="summary"
41
+ :typography="summaryTypography"
42
+ :casing="summaryCase"
43
+ :state="summaryState"
44
+ :facts="summaryFacts"
45
+ :badges="summaryBadges"
46
+ :status="summaryStatus"
47
+ />
48
+ </SButton>
49
+ </div>
50
+ <div v-if="hasActions" class="s-sidebar-section-actions">
51
+ <slot name="actions" />
52
+ </div>
53
+ </div>
54
+ <div
55
+ v-if="isOpen"
56
+ :id="panelId"
57
+ :aria-labelledby="triggerId"
58
+ role="region"
59
+ tabindex="0"
60
+ class="s-sidebar-section-content"
61
+ >
62
+ <slot />
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { computed, inject, onBeforeUnmount, onMounted, ref, useId, watch } from 'vue';
70
+
71
+ import { useDisclosure } from '../../../composables/useDisclosure';
72
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs';
73
+ import SResizeHandle from '../../layout/SResizeHandle.vue';
74
+ import {
75
+ sidebarGroupContextKey,
76
+ type RegisteredSidebarSection,
77
+ } from '../_internal/sidebarGroupContext';
78
+ import SDisclosureSummary from '../_internal/SDisclosureSummary.vue';
79
+ import SButton from '../controls/SButton.vue';
80
+ import type {
81
+ SDisclosureSummaryBadge,
82
+ SDisclosureSummaryCase,
83
+ SDisclosureSummaryFact,
84
+ SDisclosureSummaryState,
85
+ SDisclosureSummaryStatus,
86
+ SDisclosureSummaryTypography,
87
+ } from './disclosureSummary';
88
+
89
+ defineOptions({
90
+ name: 'SSidebarSection',
91
+ inheritAttrs: false,
92
+ });
93
+
94
+ const props = withDefaults(defineProps<{
95
+ summary: string;
96
+ summaryTypography?: SDisclosureSummaryTypography;
97
+ summaryCase?: SDisclosureSummaryCase;
98
+ summaryState?: SDisclosureSummaryState;
99
+ summaryFacts?: readonly SDisclosureSummaryFact[];
100
+ summaryBadges?: readonly SDisclosureSummaryBadge[];
101
+ summaryStatus?: SDisclosureSummaryStatus;
102
+ stateKey: string;
103
+ layout?: 'desktop' | 'mobile';
104
+ disabled?: boolean;
105
+ }>(), {
106
+ summaryTypography: 'strong',
107
+ summaryCase: 'uppercase',
108
+ summaryState: undefined,
109
+ summaryFacts: () => [],
110
+ summaryBadges: () => [],
111
+ summaryStatus: undefined,
112
+ layout: 'desktop',
113
+ disabled: false,
114
+ });
115
+
116
+ const slots = defineSlots<{
117
+ default(): unknown;
118
+ actions?(): unknown;
119
+ }>();
120
+
121
+ const sectionEl = ref<HTMLDivElement | null>(null);
122
+ const ownedAttrs = useOwnedAttrs({ component: 'SSidebarSection', owner: 'sidebar section wrapper' });
123
+ const hasActions = computed(() => Boolean(slots.actions));
124
+ const injectedGroupContext = inject(sidebarGroupContextKey);
125
+ if (!injectedGroupContext) {
126
+ throw new Error('SSidebarSection: component must be rendered inside SSidebarGroup');
127
+ }
128
+ const groupContext = injectedGroupContext;
129
+ if (props.stateKey.trim().length === 0) {
130
+ throw new TypeError('SSidebarSection: stateKey must be a non-empty string');
131
+ }
132
+ const registeredStateKey = props.stateKey;
133
+ const disclosureBaseId = `s-sidebar-section-${useId()}-${props.stateKey.replace(/[^a-zA-Z0-9_-]/g, '-')}`;
134
+ let unregisterSection: (() => void) | null = null;
135
+ const isRegistered = ref(false);
136
+
137
+ onMounted(() => {
138
+ const element = sectionEl.value;
139
+ if (!element) {
140
+ throw new Error('SSidebarSection: section wrapper is unavailable during mount');
141
+ }
142
+ unregisterSection = groupContext.registerSection({ key: registeredStateKey, element });
143
+ isRegistered.value = true;
144
+ });
145
+
146
+ onBeforeUnmount(() => {
147
+ isRegistered.value = false;
148
+ unregisterSection?.();
149
+ unregisterSection = null;
150
+ });
151
+
152
+ watch(() => props.stateKey, (stateKey) => {
153
+ if (stateKey !== registeredStateKey) {
154
+ throw new Error(
155
+ `SSidebarSection: stateKey is immutable after setup; received '${registeredStateKey}' -> '${stateKey}'`,
156
+ );
157
+ }
158
+ });
159
+
160
+ const sectionIndex = computed(() => {
161
+ const index = groupContext.orderedSections.value.findIndex(
162
+ (section) => section.key === registeredStateKey,
163
+ );
164
+ if (isRegistered.value && index < 0) {
165
+ throw new Error(
166
+ `SSidebarSection: registered section '${registeredStateKey}' is absent from group order`,
167
+ );
168
+ }
169
+ return index;
170
+ });
171
+
172
+ const isOpen = computed(() => {
173
+ const state = groupContext.sectionsState.value[registeredStateKey];
174
+ if (!state) {
175
+ throw new Error(
176
+ `SSidebarSection: sectionsState has no entry for stateKey '${registeredStateKey}'`,
177
+ );
178
+ }
179
+ return state.isOpen;
180
+ });
181
+ const disclosure = useDisclosure({
182
+ id: disclosureBaseId,
183
+ open: isOpen,
184
+ disabled: () => props.disabled ?? false,
185
+ onUpdate: () => groupContext.toggleSection(registeredStateKey),
186
+ });
187
+ const { triggerId, panelId } = disclosure;
188
+
189
+ const style = computed(() => {
190
+ return groupContext.getSectionStyle(registeredStateKey);
191
+ });
192
+
193
+ const isResizableBefore = computed(() => {
194
+ if (sectionIndex.value <= 0 || !isOpen.value) {
195
+ return false;
196
+ }
197
+ const sections = groupContext.orderedSections.value;
198
+ for (let i = sectionIndex.value - 1; i >= 0; i--) {
199
+ if (sections[i]?.state.isOpen) return true;
200
+ }
201
+ return false;
202
+ });
203
+
204
+ const topSectionIndex = computed(() => {
205
+ for (let index = sectionIndex.value - 1; index >= 0; index--) {
206
+ if (groupContext.orderedSections.value[index]?.state.isOpen) return index;
207
+ }
208
+ return -1;
209
+ });
210
+
211
+ function measuredHeight(section: RegisteredSidebarSection): number {
212
+ const height = section.element.offsetHeight;
213
+ if (!Number.isFinite(height) || height < 0) {
214
+ throw new RangeError(
215
+ `SSidebarSection: sections.${section.key}.offsetHeight must be a non-negative finite number`,
216
+ );
217
+ }
218
+ return height;
219
+ }
220
+
221
+ function resizePair(): readonly [RegisteredSidebarSection, RegisteredSidebarSection] {
222
+ if (topSectionIndex.value < 0 || sectionIndex.value < 0) {
223
+ throw new Error(`SSidebarSection: resize pair is unavailable for section '${registeredStateKey}'`);
224
+ }
225
+ const sections = groupContext.orderedSections.value;
226
+ const top = sections[topSectionIndex.value];
227
+ const bottom = sections[sectionIndex.value];
228
+ if (!top || !bottom) {
229
+ throw new Error(
230
+ `SSidebarSection: resize pair indices are outside mounted group order for section '${registeredStateKey}'`,
231
+ );
232
+ }
233
+ return [top, bottom];
234
+ }
235
+
236
+ const resizeContract = computed(() => {
237
+ const [top, bottom] = resizePair();
238
+ const value = Math.max(top.state.minSize, top.state.size ?? measuredHeight(top));
239
+ const bottomSize = Math.max(
240
+ bottom.state.minSize,
241
+ bottom.state.size ?? measuredHeight(bottom),
242
+ );
243
+ const pairSize = value + bottomSize;
244
+ return {
245
+ value,
246
+ min: top.state.minSize,
247
+ max: Math.max(top.state.minSize, pairSize - bottom.state.minSize),
248
+ testId: `sidebar-resize-${top.key}-${bottom.key}`,
249
+ };
250
+ });
251
+
252
+ function handleResizeStart(): void {
253
+ const [top, bottom] = resizePair();
254
+ groupContext.beginResize(top.key, bottom.key);
255
+ }
256
+
257
+ function handleResize(value: number): void {
258
+ const [top, bottom] = resizePair();
259
+ groupContext.resizeSections(top.key, bottom.key, value);
260
+ }
261
+
262
+ function handleResizeEnd(): void {
263
+ const [top, bottom] = resizePair();
264
+ groupContext.finishResize(top.key, bottom.key);
265
+ }
266
+ </script>
267
+
268
+ <style lang="postcss" src="./SSidebarSection.css" scoped></style>
@@ -0,0 +1,48 @@
1
+ import type { BadgeSeverity } from '../data-display/SBadge.vue'
2
+ import type { StatusTone } from '../data-display/SStatus.vue'
3
+
4
+ /** Типографика заголовка disclosure. / Disclosure summary typography. */
5
+ export type SDisclosureSummaryTypography = 'default' | 'strong' | 'mono'
6
+
7
+ /** Регистр заголовка disclosure. / Disclosure summary letter case. */
8
+ export type SDisclosureSummaryCase = 'sentence' | 'uppercase'
9
+
10
+ /** Семантическое состояние с системной иконкой. / Semantic state with a system-owned icon. */
11
+ export type SDisclosureSummaryState = 'neutral' | 'progress' | 'success' | 'warn' | 'danger'
12
+
13
+ /** Семантический тон факта. / Semantic fact tone. */
14
+ export type SDisclosureSummaryTone = 'neutral' | 'info' | 'success' | 'warn' | 'danger'
15
+
16
+ /** Короткая пара label/value в заголовке. / Compact label/value pair in a summary. */
17
+ export interface SDisclosureSummaryFact {
18
+ /** Стабильная identity факта, не зависящая от отображаемого значения. / Stable fact identity independent of its displayed value. */
19
+ id: string
20
+ /** Необязательная подпись значения. / Optional value label. */
21
+ label?: string
22
+ /** Отображаемое непустое значение. / Non-empty displayed value. */
23
+ value: string | number
24
+ /** Семантический тон факта. / Semantic tone of the fact. */
25
+ tone?: SDisclosureSummaryTone
26
+ }
27
+
28
+ /** Неинтерактивный badge в заголовке. / Non-interactive summary badge. */
29
+ export interface SDisclosureSummaryBadge {
30
+ /** Стабильная identity badge, не зависящая от отображаемой подписи. / Stable badge identity independent of its displayed label. */
31
+ id: string
32
+ /** Непустая подпись badge. / Non-empty badge label. */
33
+ label: string | number
34
+ /** Семантическая важность badge. / Semantic badge severity. */
35
+ severity?: BadgeSeverity
36
+ /** Дополнительная нативная подсказка. / Additional native tooltip. */
37
+ title?: string
38
+ }
39
+
40
+ /** Текстовый статус в конце заголовка. / Trailing text status in a summary. */
41
+ export interface SDisclosureSummaryStatus {
42
+ /** Непустая подпись статуса. / Non-empty status label. */
43
+ label: string
44
+ /** Семантический тон статуса. / Semantic status tone. */
45
+ tone?: StatusTone
46
+ /** Дополнительная нативная подсказка. / Additional native tooltip. */
47
+ title?: string
48
+ }
@@ -0,0 +1,72 @@
1
+ import type { InteractiveElementRefTarget } from '../../../internal/interactiveElement'
2
+ import type { SButtonDisclosure } from '../controls/SButton.vue'
3
+
4
+ /**
5
+ * Режим реакции custom trigger на единый click-контракт.
6
+ * Custom-trigger activation mode for the unified click contract.
7
+ */
8
+ export type SPopoverTriggerActivation = 'toggle' | 'manual'
9
+
10
+ /**
11
+ * Допустимая цель callback-ref custom trigger.
12
+ * Supported callback-ref target for a custom trigger.
13
+ */
14
+ export type SPopoverTriggerRefTarget = InteractiveElementRefTarget
15
+
16
+ /**
17
+ * Полный контракт для `v-bind` непосредственно на button-root custom trigger.
18
+ * Complete contract for `v-bind` directly on the custom trigger button root.
19
+ */
20
+ export interface SPopoverTriggerBinding {
21
+ readonly ref: (target: SPopoverTriggerRefTarget) => void
22
+ readonly disclosure: SButtonDisclosure
23
+ readonly 'aria-expanded': boolean
24
+ readonly 'aria-haspopup': 'dialog'
25
+ readonly 'aria-controls': string
26
+ readonly onClick: (event: MouseEvent) => void
27
+ }
28
+
29
+ /**
30
+ * Binder создаёт один контракт для каждого уникального trigger id.
31
+ * The binder creates one contract for every unique trigger id.
32
+ */
33
+ export type SPopoverTriggerBinder = (
34
+ triggerId: string,
35
+ activation?: SPopoverTriggerActivation,
36
+ ) => SPopoverTriggerBinding
37
+
38
+ /**
39
+ * Публичный scoped-контракт custom trigger.
40
+ * Public scoped contract for custom triggers.
41
+ */
42
+ export interface SPopoverTriggerSlotProps {
43
+ readonly bindTrigger: SPopoverTriggerBinder
44
+ readonly open: (triggerId?: string) => void
45
+ readonly close: () => void
46
+ readonly isOpen: boolean
47
+ }
48
+
49
+ /**
50
+ * Содержимое built-in trigger получает только наблюдаемое состояние.
51
+ * Built-in trigger content receives observable state only.
52
+ */
53
+ export interface SPopoverDefaultSlotProps {
54
+ readonly isOpen: boolean
55
+ }
56
+
57
+ /**
58
+ * Панель может запросить явное закрытие с возвратом focus к trigger.
59
+ * The panel may explicitly close and restore focus to its trigger.
60
+ */
61
+ export interface SPopoverPanelSlotProps {
62
+ readonly close: () => void
63
+ }
64
+
65
+ /**
66
+ * Публичный programmatic API использует зарегистрированный anchor.
67
+ * The public programmatic API uses a registered anchor.
68
+ */
69
+ export interface SPopoverPublicApi {
70
+ open: (triggerId?: string) => void
71
+ close: () => void
72
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Состояние размера и раскрытия одной sidebar-секции.
3
+ * Size and disclosure state of one sidebar section.
4
+ */
5
+ export interface SectionState {
6
+ readonly isOpen: boolean;
7
+ readonly size: number | null;
8
+ readonly minSize: number;
9
+ }
10
+
11
+ /**
12
+ * Точная controlled-state карта под управлением consumer.
13
+ * Exact controlled state map owned by the consumer.
14
+ */
15
+ export type SidebarSectionsState = Readonly<Record<string, Readonly<SectionState>>>;
16
+
17
+ /** Exact payload map общего владельца sidebar-секций. / Exact payload map of the shared sidebar-section owner. */
18
+ export interface SidebarGroupEmits {
19
+ 'update-sections': [state: SidebarSectionsState];
20
+ 'toggle-section': [key: string];
21
+ }
22
+
23
+ /**
24
+ * Одна точная immutable-замена секции в controlled transition.
25
+ * One exact immutable section replacement in a controlled transition.
26
+ */
27
+ export interface SidebarSectionReplacement {
28
+ readonly key: string;
29
+ readonly state: Readonly<SectionState>;
30
+ }
31
+
32
+ const SECTION_STATE_FIELDS = Object.freeze(['isOpen', 'minSize', 'size'] as const);
33
+
34
+ function assertPlainDataRecord(
35
+ owner: string,
36
+ coordinate: string,
37
+ value: unknown,
38
+ ): asserts value is Record<string, unknown> {
39
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
40
+ throw new TypeError(`${owner}: ${coordinate} must be a plain data record`);
41
+ }
42
+ const prototype = Object.getPrototypeOf(value);
43
+ if (prototype !== Object.prototype && prototype !== null) {
44
+ throw new TypeError(`${owner}: ${coordinate} must be a plain data record`);
45
+ }
46
+ if (Object.getOwnPropertySymbols(value).length > 0) {
47
+ throw new TypeError(`${owner}: ${coordinate} must not contain symbol properties`);
48
+ }
49
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
50
+ if (!descriptor.enumerable || !('value' in descriptor)) {
51
+ throw new TypeError(`${owner}: ${coordinate}.${key} must be an enumerable data property`);
52
+ }
53
+ }
54
+ }
55
+
56
+ function assertExactSectionState(
57
+ owner: string,
58
+ coordinate: string,
59
+ value: unknown,
60
+ ): asserts value is SectionState {
61
+ assertPlainDataRecord(owner, coordinate, value);
62
+ const keys = Object.keys(value).sort();
63
+ if (
64
+ keys.length !== SECTION_STATE_FIELDS.length
65
+ || keys.some((key, index) => key !== SECTION_STATE_FIELDS[index])
66
+ ) {
67
+ throw new TypeError(
68
+ `${owner}: ${coordinate} must contain exactly [${SECTION_STATE_FIELDS.join(', ')}]`,
69
+ );
70
+ }
71
+ if (typeof value.isOpen !== 'boolean') {
72
+ throw new TypeError(`${owner}: ${coordinate}.isOpen must be a boolean`);
73
+ }
74
+ if (typeof value.minSize !== 'number' || !Number.isFinite(value.minSize) || value.minSize < 0) {
75
+ throw new TypeError(`${owner}: ${coordinate}.minSize must be a non-negative finite number`);
76
+ }
77
+ if (
78
+ value.size !== null
79
+ && (
80
+ typeof value.size !== 'number'
81
+ || !Number.isFinite(value.size)
82
+ || value.size < value.minSize
83
+ )
84
+ ) {
85
+ throw new TypeError(
86
+ `${owner}: ${coordinate}.size must be null or a finite number not below minSize`,
87
+ );
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Проверяет полный точный controlled-контракт геометрии sidebar.
93
+ * Validates the complete exact controlled sidebar geometry contract.
94
+ */
95
+ export function assertSidebarSectionsState(
96
+ state: unknown,
97
+ owner = 'SSidebarGroup',
98
+ ): asserts state is SidebarSectionsState {
99
+ assertPlainDataRecord(owner, 'sectionsState', state);
100
+ const openSections: SectionState[] = [];
101
+ for (const [key, section] of Object.entries(state)) {
102
+ if (key.trim().length === 0) {
103
+ throw new TypeError(`${owner}: section keys must be non-empty strings`);
104
+ }
105
+ assertExactSectionState(owner, `sectionsState.${key}`, section);
106
+ if (section.isOpen) openSections.push(section);
107
+ }
108
+ const flexibleOpenCount = openSections.filter((section) => section.size === null).length;
109
+ if (openSections.length > 0 && flexibleOpenCount !== 1) {
110
+ throw new TypeError(
111
+ `${owner}: exactly one open section must have size=null; received ${flexibleOpenCount}`,
112
+ );
113
+ }
114
+ }
115
+
116
+ function immutableSectionState(state: Readonly<SectionState>): Readonly<SectionState> {
117
+ return Object.freeze({
118
+ isOpen: state.isOpen,
119
+ size: state.size,
120
+ minSize: state.minSize,
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Проверяет, копирует и глубоко замораживает полное controlled-state значение.
126
+ * Validates, copies, and deeply freezes one complete controlled state value.
127
+ */
128
+ export function createSidebarSectionsState(
129
+ state: unknown,
130
+ owner = 'SSidebarGroup',
131
+ ): SidebarSectionsState {
132
+ assertSidebarSectionsState(state, owner);
133
+ return Object.freeze(Object.fromEntries(
134
+ Object.entries(state).map(([key, section]) => [key, immutableSectionState(section)]),
135
+ ));
136
+ }
137
+
138
+ /**
139
+ * Создаёт deeply immutable controlled transition без мутации или сериализации входа.
140
+ * Creates one deeply immutable controlled transition without mutating or serializing its input.
141
+ */
142
+ export function createSidebarSectionsTransition(
143
+ current: SidebarSectionsState,
144
+ replacements: readonly SidebarSectionReplacement[],
145
+ ): SidebarSectionsState {
146
+ assertSidebarSectionsState(current);
147
+ if (!Array.isArray(replacements) || replacements.length === 0) {
148
+ throw new TypeError('SSidebarGroup: a transition requires at least one section replacement');
149
+ }
150
+ const replacementsByKey = new Map<string, Readonly<SectionState>>();
151
+ for (const [index, replacement] of replacements.entries()) {
152
+ assertPlainDataRecord('SSidebarGroup', `replacements[${index}]`, replacement);
153
+ const replacementKeys = Object.keys(replacement).sort();
154
+ if (
155
+ replacementKeys.length !== 2
156
+ || replacementKeys[0] !== 'key'
157
+ || replacementKeys[1] !== 'state'
158
+ ) {
159
+ throw new TypeError(
160
+ `SSidebarGroup: replacements[${index}] must contain exactly [key, state]`,
161
+ );
162
+ }
163
+ if (typeof replacement.key !== 'string' || replacement.key.trim().length === 0) {
164
+ throw new TypeError(`SSidebarGroup: replacements[${index}].key must be a non-empty string`);
165
+ }
166
+ if (!Object.hasOwn(current, replacement.key)) {
167
+ throw new Error(`SSidebarGroup: cannot replace unknown section '${replacement.key}'`);
168
+ }
169
+ if (replacementsByKey.has(replacement.key)) {
170
+ throw new Error(`SSidebarGroup: duplicate replacement for section '${replacement.key}'`);
171
+ }
172
+ assertExactSectionState(
173
+ 'SSidebarGroup',
174
+ `replacements[${index}].state`,
175
+ replacement.state,
176
+ );
177
+ replacementsByKey.set(replacement.key, replacement.state);
178
+ }
179
+
180
+ const next = Object.fromEntries(Object.entries(current).map(([key, state]) => [
181
+ key,
182
+ immutableSectionState(replacementsByKey.get(key) ?? state),
183
+ ]));
184
+ return createSidebarSectionsState(next);
185
+ }
186
+
187
+ /**
188
+ * Сравнивает точные public state fields без coercion.
189
+ * Compares the exact public state fields without coercion.
190
+ */
191
+ export function sidebarSectionsStateEqual(
192
+ left: SidebarSectionsState,
193
+ right: SidebarSectionsState,
194
+ ): boolean {
195
+ const leftKeys = Object.keys(left);
196
+ const rightKeys = Object.keys(right);
197
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => {
198
+ const leftSection = left[key];
199
+ const rightSection = right[key];
200
+ return leftSection !== undefined
201
+ && rightSection !== undefined
202
+ && leftSection.isOpen === rightSection.isOpen
203
+ && leftSection.size === rightSection.size
204
+ && leftSection.minSize === rightSection.minSize;
205
+ });
206
+ }