@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,360 @@
1
+ <template>
2
+ <div
3
+ class="s-collapsible-panel"
4
+ ref="rootEl"
5
+ :data-density="density"
6
+ :data-surface="surface"
7
+ :data-content-padding="contentPadding"
8
+ v-bind="ownedAttrs.bindings()"
9
+ >
10
+ <div class="s-collapsible-panel-header">
11
+ <div class="s-collapsible-panel-trigger">
12
+ <SButton
13
+ :id="triggerId"
14
+ :appearance="headerVariant === 'minimal' ? 'ghost' : 'soft'"
15
+ severity="secondary"
16
+ shape="row"
17
+ :size="buttonSize"
18
+ width="full"
19
+ content-align="between"
20
+ :disabled="disabled"
21
+ :disclosure="{ expanded: isOpen, controls: panelId }"
22
+ @click="toggle"
23
+ >
24
+ <SDisclosureSummary
25
+ :title="summary"
26
+ :typography="summaryTypography"
27
+ :casing="summaryCase"
28
+ :state="summaryState"
29
+ :facts="summaryFacts"
30
+ :badges="summaryBadges"
31
+ :status="summaryStatus"
32
+ />
33
+ </SButton>
34
+ </div>
35
+ <div v-if="hasActions" class="s-collapsible-panel-actions">
36
+ <slot name="actions" />
37
+ </div>
38
+ </div>
39
+ <div
40
+ :id="panelId"
41
+ :aria-labelledby="triggerId"
42
+ role="region"
43
+ ref="contentWrapEl"
44
+ class="s-collapsible-panel-content w-full min-w-0"
45
+ :class="[{ 'min-h-0 flex flex-col grow overflow-y-auto overflow-x-hidden': fill }, { 'no-transition': noAnimation }]"
46
+ :data-open="isOpen"
47
+ v-show="shouldShowContent"
48
+ :style="{
49
+ ...(noAnimation ? { transition: 'none' } : {})
50
+ }"
51
+ >
52
+ <div ref="contentInnerEl" class="s-collapsible-panel-inner">
53
+ <slot v-if="hasBeenOpened" />
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import { computed, ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
61
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
62
+ import SDisclosureSummary from '../_internal/SDisclosureSummary.vue'
63
+ import SButton from '../controls/SButton.vue'
64
+ import { useDisclosure } from '../../../composables/useDisclosure'
65
+ import type {
66
+ SDisclosureSummaryBadge,
67
+ SDisclosureSummaryCase,
68
+ SDisclosureSummaryFact,
69
+ SDisclosureSummaryState,
70
+ SDisclosureSummaryStatus,
71
+ SDisclosureSummaryTypography,
72
+ } from './disclosureSummary'
73
+
74
+ defineOptions({
75
+ inheritAttrs: false,
76
+ })
77
+
78
+ export interface Props {
79
+ summary: string
80
+ summaryTypography?: SDisclosureSummaryTypography
81
+ summaryCase?: SDisclosureSummaryCase
82
+ summaryState?: SDisclosureSummaryState
83
+ summaryFacts?: readonly SDisclosureSummaryFact[]
84
+ summaryBadges?: readonly SDisclosureSummaryBadge[]
85
+ summaryStatus?: SDisclosureSummaryStatus
86
+ defaultOpen?: boolean
87
+ // Support v-model:open
88
+ open?: boolean
89
+ fill?: boolean
90
+ density?: 'comfortable' | 'compact' | 'dense'
91
+ contentPadding?: 'density' | 'none'
92
+ headerVariant?: 'default' | 'minimal'
93
+ lazy?: boolean
94
+ noAnimation?: boolean
95
+ surface?: 'card' | 'plain'
96
+ id?: string
97
+ disabled?: boolean
98
+ }
99
+
100
+ const props = withDefaults(defineProps<Props>(), {
101
+ summaryTypography: 'default',
102
+ summaryCase: 'sentence',
103
+ summaryState: undefined,
104
+ summaryFacts: () => [],
105
+ summaryBadges: () => [],
106
+ summaryStatus: undefined,
107
+ defaultOpen: false,
108
+ open: undefined,
109
+ fill: false,
110
+ density: 'comfortable',
111
+ contentPadding: 'density',
112
+ headerVariant: 'default',
113
+ lazy: false,
114
+ noAnimation: false,
115
+ surface: 'card',
116
+ id: undefined,
117
+ disabled: false,
118
+ })
119
+
120
+ const slots = defineSlots<{
121
+ default(): unknown
122
+ actions?(): unknown
123
+ }>()
124
+
125
+ const emit = defineEmits<{
126
+ 'update:open': [value: boolean]
127
+ }>()
128
+ const hasActions = computed(() => Boolean(slots.actions))
129
+ const ownedAttrs = useOwnedAttrs({ component: 'SCollapsiblePanel', owner: 'collapsible panel root' })
130
+ const buttonSize = computed(() => props.density === 'comfortable' ? 'md' : 'sm')
131
+
132
+ const controlledOpen = computed(() => props.open ?? false)
133
+ const disclosure = useDisclosure({
134
+ id: props.id,
135
+ open: props.open !== undefined ? controlledOpen : undefined,
136
+ defaultOpen: props.open === undefined ? props.defaultOpen : undefined,
137
+ disabled: () => props.disabled,
138
+ onUpdate: (value) => emit('update:open', value),
139
+ })
140
+ const { isOpen, triggerId, panelId } = disclosure
141
+
142
+ // Состояние видимости контента (display: block/none)
143
+ // Показываем, если открыто ИЛИ идет анимация.
144
+ // Это убирает элементы из Layout Tree, когда они закрыты.
145
+ const isAnimating = ref(false)
146
+ const shouldShowContent = computed(() => isOpen.value || isAnimating.value)
147
+
148
+ const hasBeenOpened = ref(!props.lazy || props.defaultOpen || (props.open === true))
149
+
150
+ watch(() => props.open, (val) => {
151
+ if (val) hasBeenOpened.value = true
152
+ })
153
+
154
+ function toggle() {
155
+ if (props.disabled) return
156
+ if (props.noAnimation) {
157
+ disclosure.toggle()
158
+ return
159
+ }
160
+
161
+ const root = rootEl.value
162
+ const wrap = contentWrapEl.value
163
+
164
+ isAnimating.value = true
165
+
166
+ root?.classList.add('is-animating')
167
+ root?.classList.remove('is-opened')
168
+ wrap?.classList.add('is-animating')
169
+ wrap?.classList.remove('is-opened')
170
+
171
+ disclosure.toggle()
172
+ }
173
+
174
+ const contentWrapEl = ref<HTMLElement | null>(null)
175
+ const contentInnerEl = ref<HTMLElement | null>(null)
176
+ const rootEl = ref<HTMLElement | null>(null)
177
+ let resizeObserver: ResizeObserver | null = null
178
+ let pendingHeightSyncFrame: number | null = null
179
+
180
+ function cancelPendingHeightSync(): void {
181
+ if (pendingHeightSyncFrame !== null) {
182
+ window.cancelAnimationFrame(pendingHeightSyncFrame)
183
+ pendingHeightSyncFrame = null
184
+ }
185
+ }
186
+
187
+ function syncAnimatedMaxHeight(): void {
188
+ const wrap = contentWrapEl.value
189
+ const inner = contentInnerEl.value
190
+ if (!wrap || !inner || !isOpen.value || !isAnimating.value) return
191
+
192
+ const nextMaxHeight = `${inner.scrollHeight}px`
193
+ if (wrap.style.maxHeight !== nextMaxHeight) {
194
+ wrap.style.maxHeight = nextMaxHeight
195
+ }
196
+ }
197
+
198
+ function scheduleAnimatedMaxHeightSync(): void {
199
+ cancelPendingHeightSync()
200
+ pendingHeightSyncFrame = window.requestAnimationFrame(() => {
201
+ pendingHeightSyncFrame = null
202
+ syncAnimatedMaxHeight()
203
+ })
204
+ }
205
+
206
+ function setOpenStyles() {
207
+ const el = contentWrapEl.value
208
+ if (!el) return
209
+ el.classList.remove('is-animating')
210
+ el.classList.add('is-opened')
211
+ el.style.overflow = ''
212
+ el.style.maxHeight = ''
213
+ const root = rootEl.value
214
+ if (root) {
215
+ root.classList.remove('is-animating')
216
+ root.classList.add('is-opened')
217
+ }
218
+ }
219
+
220
+ function setClosedStyles() {
221
+ const el = contentWrapEl.value
222
+ if (!el) return
223
+ el.classList.remove('is-animating')
224
+ el.classList.remove('is-opened')
225
+ el.style.maxHeight = '0px'
226
+ el.style.overflow = 'hidden'
227
+ const root = rootEl.value
228
+ if (root) {
229
+ root.classList.remove('is-animating')
230
+ root.classList.remove('is-opened')
231
+ }
232
+ }
233
+
234
+ async function applyImmediateState(open: boolean) {
235
+ if (open) {
236
+ await nextTick()
237
+ hasBeenOpened.value = true
238
+ setOpenStyles()
239
+ } else {
240
+ setClosedStyles()
241
+ }
242
+ isAnimating.value = false
243
+ }
244
+
245
+ async function animateOpen() {
246
+ const el = contentWrapEl.value
247
+ if (!el) return
248
+ const root = rootEl.value
249
+
250
+ // Ждем nextTick, чтобы v-show="true" применился и элемент появился в DOM
251
+ await nextTick()
252
+
253
+ el.classList.remove('is-opened')
254
+ el.classList.add('is-animating')
255
+ if (root) {
256
+ root.classList.add('is-animating')
257
+ root.classList.remove('is-opened')
258
+ }
259
+
260
+ el.style.overflow = 'hidden'
261
+ el.style.maxHeight = '0px'
262
+ // Force layout calc
263
+ void el.offsetHeight
264
+
265
+ el.style.maxHeight = el.scrollHeight + 'px'
266
+
267
+ const onEnd = () => {
268
+ el.removeEventListener('transitionend', onEnd)
269
+ if (!isOpen.value) return // прервано закрытием
270
+ cancelPendingHeightSync()
271
+ el.style.maxHeight = ''
272
+ el.style.overflow = ''
273
+ el.classList.remove('is-animating')
274
+ el.classList.add('is-opened')
275
+ if (root) {
276
+ root.classList.remove('is-animating')
277
+ root.classList.add('is-opened')
278
+ }
279
+ isAnimating.value = false
280
+ }
281
+ el.addEventListener('transitionend', onEnd)
282
+ }
283
+
284
+ function animateClose() {
285
+ const el = contentWrapEl.value
286
+ if (!el) return
287
+ const root = rootEl.value
288
+
289
+ isAnimating.value = true // ensure visible during anim
290
+
291
+ el.classList.add('is-animating')
292
+ el.classList.remove('is-opened')
293
+ if (root) {
294
+ root.classList.add('is-animating')
295
+ root.classList.remove('is-opened')
296
+ }
297
+
298
+ if (el.style.maxHeight === '' || el.style.maxHeight === 'none') {
299
+ el.style.maxHeight = el.scrollHeight + 'px'
300
+ void el.offsetHeight
301
+ }
302
+
303
+ el.style.overflow = 'hidden'
304
+ el.style.maxHeight = '0px'
305
+
306
+ const onEnd = () => {
307
+ el.removeEventListener('transitionend', onEnd)
308
+ if (isOpen.value) return // прервано открытием
309
+ cancelPendingHeightSync()
310
+ el.classList.remove('is-animating')
311
+ if (root) {
312
+ root.classList.remove('is-animating')
313
+ }
314
+ isAnimating.value = false // trigger v-show=false
315
+ }
316
+ el.addEventListener('transitionend', onEnd)
317
+ }
318
+
319
+ onMounted(() => {
320
+ const el = contentWrapEl.value
321
+ if (!el) return
322
+ el.style.willChange = 'max-height'
323
+
324
+ if (props.noAnimation) {
325
+ void applyImmediateState(isOpen.value)
326
+ } else if (isOpen.value) {
327
+ setOpenStyles()
328
+ } else {
329
+ // Closed initially
330
+ setClosedStyles()
331
+ }
332
+
333
+ const target = contentInnerEl.value
334
+ if (!props.noAnimation && 'ResizeObserver' in window && target) {
335
+ resizeObserver = new ResizeObserver(() => {
336
+ scheduleAnimatedMaxHeightSync()
337
+ })
338
+ resizeObserver.observe(target)
339
+ }
340
+ })
341
+
342
+ onBeforeUnmount(() => {
343
+ cancelPendingHeightSync()
344
+ if (resizeObserver) {
345
+ resizeObserver.disconnect()
346
+ resizeObserver = null
347
+ }
348
+ })
349
+
350
+ watch(isOpen, (open) => {
351
+ if (props.noAnimation) {
352
+ void applyImmediateState(open)
353
+ return
354
+ }
355
+ if (open) animateOpen()
356
+ else animateClose()
357
+ })
358
+ </script>
359
+
360
+ <style lang="postcss" src="./SCollapsiblePanel.css" scoped></style>
@@ -0,0 +1,60 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-drawer-backdrop {
4
+ @apply fixed inset-0;
5
+ background: var(--s-overlay-backdrop-surface);
6
+ -webkit-backdrop-filter: blur(var(--s-overlay-backdrop-blur));
7
+ backdrop-filter: blur(var(--s-overlay-backdrop-blur));
8
+ }
9
+
10
+ .s-drawer-shell {
11
+ @apply fixed inset-0;
12
+ }
13
+
14
+ .s-drawer-positioner {
15
+ @apply absolute inset-0 flex items-stretch justify-end;
16
+ }
17
+
18
+ .s-drawer-backdrop-enter-active,
19
+ .s-drawer-backdrop-leave-active {
20
+ transition: opacity var(--s-motion-standard) ease;
21
+ }
22
+
23
+ .s-drawer-backdrop-enter-from,
24
+ .s-drawer-backdrop-leave-to {
25
+ opacity: 0;
26
+ }
27
+
28
+ .s-drawer-panel-enter-active,
29
+ .s-drawer-panel-leave-active {
30
+ transition:
31
+ opacity var(--s-motion-standard) ease,
32
+ transform var(--s-motion-standard) ease;
33
+ }
34
+
35
+ .s-drawer-panel-enter-from,
36
+ .s-drawer-panel-leave-to {
37
+ opacity: 0;
38
+ transform: translateX(1.5rem);
39
+ }
40
+
41
+ @media (max-width: 767px) {
42
+ .s-drawer-panel-enter-from,
43
+ .s-drawer-panel-leave-to {
44
+ transform: translateY(1.5rem);
45
+ }
46
+ }
47
+
48
+ @media (prefers-reduced-motion: reduce) {
49
+ .s-drawer-backdrop-enter-active,
50
+ .s-drawer-backdrop-leave-active,
51
+ .s-drawer-panel-enter-active,
52
+ .s-drawer-panel-leave-active {
53
+ transition: none;
54
+ }
55
+
56
+ .s-drawer-panel-enter-from,
57
+ .s-drawer-panel-leave-to {
58
+ transform: none;
59
+ }
60
+ }
@@ -0,0 +1,86 @@
1
+ <template>
2
+ <teleport to="body">
3
+ <transition name="s-drawer-backdrop">
4
+ <div v-if="open" class="s-drawer-backdrop" :style="backdropStyle" aria-hidden="true" />
5
+ </transition>
6
+
7
+ <transition name="s-drawer-panel">
8
+ <div v-if="open" class="s-drawer-shell" :style="surfaceStyle">
9
+ <div class="s-drawer-positioner">
10
+ <SDialogSurface
11
+ v-bind="ownedAttrs.bindings()"
12
+ :presentation="dialogPresentation"
13
+ :title="title"
14
+ :accessible-name="ariaLabel"
15
+ :closable="closable"
16
+ :set-surface-element="setPanelElement"
17
+ @request-close="closeDrawer"
18
+ >
19
+ <template v-if="$slots.header" #header>
20
+ <slot name="header" />
21
+ </template>
22
+ <slot />
23
+ <template v-if="$slots.footer" #footer="{ close }">
24
+ <slot name="footer" :close="close" />
25
+ </template>
26
+ </SDialogSurface>
27
+ </div>
28
+ </div>
29
+ </transition>
30
+ </teleport>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import { computed, toRef } from 'vue'
35
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
36
+ import { useDialogOverlay } from '../../../composables/useDialogOverlay'
37
+ import SDialogSurface from '../_internal/SDialogSurface.vue'
38
+ import type {
39
+ DialogSurfaceDrawerSize,
40
+ DrawerDialogSurfacePresentation,
41
+ } from '../_internal/dialogSurface'
42
+
43
+ defineOptions({ inheritAttrs: false })
44
+
45
+ export type DrawerSize = DialogSurfaceDrawerSize
46
+
47
+ export interface Props {
48
+ open: boolean
49
+ title?: string
50
+ closable?: boolean
51
+ size?: DrawerSize
52
+ mobileFullScreen?: boolean
53
+ ariaLabel?: string
54
+ }
55
+
56
+ const props = withDefaults(defineProps<Props>(), {
57
+ title: undefined,
58
+ closable: true,
59
+ size: 'md',
60
+ mobileFullScreen: true,
61
+ ariaLabel: 'Боковая панель',
62
+ })
63
+
64
+ const emit = defineEmits<{
65
+ 'update:open': [value: boolean]
66
+ }>()
67
+
68
+ const ownedAttrs = useOwnedAttrs({ component: 'SDrawer', owner: 'drawer dialog surface' })
69
+ const dialogPresentation = computed<DrawerDialogSurfacePresentation>(() => ({
70
+ kind: 'drawer',
71
+ size: props.size,
72
+ mobileFullScreen: props.mobileFullScreen,
73
+ }))
74
+
75
+ function closeDrawer() {
76
+ emit('update:open', false)
77
+ }
78
+
79
+ const { setPanelElement, backdropStyle, surfaceStyle } = useDialogOverlay({
80
+ open: toRef(props, 'open'),
81
+ closable: toRef(props, 'closable'),
82
+ requestClose: closeDrawer,
83
+ })
84
+ </script>
85
+
86
+ <style lang="postcss" src="./SDrawer.css" scoped></style>
@@ -0,0 +1,26 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-form-grid {
4
+ @apply grid min-w-0 grid-cols-1 gap-4;
5
+ }
6
+
7
+ .s-form-grid[data-density='compact'] {
8
+ @apply gap-x-4 gap-y-3;
9
+ }
10
+
11
+ .s-form-grid[data-responsive='fixed'][data-columns='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
12
+ .s-form-grid[data-responsive='fixed'][data-columns='3'] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
13
+ .s-form-grid[data-responsive='fixed'][data-columns='4'] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
14
+
15
+ @media (min-width: 768px) {
16
+ .s-form-grid[data-responsive='collapse'][data-columns='2'],
17
+ .s-form-grid[data-responsive='collapse'][data-columns='3'],
18
+ .s-form-grid[data-responsive='collapse'][data-columns='4'] {
19
+ grid-template-columns: repeat(2, minmax(0, 1fr));
20
+ }
21
+ }
22
+
23
+ @media (min-width: 1024px) {
24
+ .s-form-grid[data-responsive='collapse'][data-columns='3'] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
25
+ .s-form-grid[data-responsive='collapse'][data-columns='4'] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
26
+ }
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <div
3
+ v-bind="ownedAttrs.bindings()"
4
+ class="s-form-grid"
5
+ :data-columns="columns"
6
+ :data-density="density"
7
+ :data-responsive="responsive"
8
+ >
9
+ <slot />
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import type { VNodeChild } from 'vue'
15
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
16
+
17
+ defineOptions({ inheritAttrs: false })
18
+
19
+ /**
20
+ * Адаптивный owner сетки полей без consumer grid- и gap-переопределений.
21
+ * Responsive form-grid owner without consumer grid and gap overrides.
22
+ */
23
+ export interface Props {
24
+ /** Максимальное число колонок на широком viewport. / Maximum column count on a wide viewport. */
25
+ columns?: 1 | 2 | 3 | 4
26
+ /** Плотность межколоночных и межстрочных интервалов. / Column and row spacing density. */
27
+ density?: 'comfortable' | 'compact'
28
+ /** Collapse складывает поля в одну колонку на узком viewport. / Collapse stacks fields on narrow viewports. */
29
+ responsive?: 'collapse' | 'fixed'
30
+ }
31
+
32
+ withDefaults(defineProps<Props>(), {
33
+ columns: 2,
34
+ density: 'comfortable',
35
+ responsive: 'collapse',
36
+ })
37
+
38
+ const ownedAttrs = useOwnedAttrs({ component: 'SFormGrid', owner: 'form grid root' })
39
+
40
+ defineSlots<{
41
+ /** Поля или SFormGridItem. / Fields or SFormGridItem participants. */
42
+ default?: () => VNodeChild
43
+ }>()
44
+ </script>
45
+
46
+ <style lang="postcss" src="./SFormGrid.css" scoped></style>
@@ -0,0 +1,18 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-form-grid-item {
4
+ @apply min-w-0;
5
+ }
6
+
7
+ .s-form-grid-item[data-span='full'] {
8
+ grid-column: 1 / -1;
9
+ }
10
+
11
+ @media (min-width: 768px) {
12
+ .s-form-grid-item[data-span='2'] { grid-column: span 2 / span 2; }
13
+ }
14
+
15
+ @media (min-width: 1024px) {
16
+ .s-form-grid-item[data-span='3'] { grid-column: span 3 / span 3; }
17
+ .s-form-grid-item[data-span='4'] { grid-column: span 4 / span 4; }
18
+ }
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <div v-bind="ownedAttrs.bindings()" class="s-form-grid-item" :data-span="span">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import type { VNodeChild } from 'vue'
9
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
10
+
11
+ defineOptions({ inheritAttrs: false })
12
+
13
+ /** Типизированный участник SFormGrid с адаптивным span. / Typed SFormGrid participant with responsive span. */
14
+ export interface Props {
15
+ /** Число занятых колонок или вся текущая строка. / Occupied column count or the full current row. */
16
+ span?: 1 | 2 | 3 | 4 | 'full'
17
+ }
18
+
19
+ withDefaults(defineProps<Props>(), { span: 1 })
20
+
21
+ const ownedAttrs = useOwnedAttrs({ component: 'SFormGridItem', owner: 'form grid item root' })
22
+
23
+ defineSlots<{
24
+ /** Поле или связанная композиция полей. / Field or related field composition. */
25
+ default?: () => VNodeChild
26
+ }>()
27
+ </script>
28
+
29
+ <style lang="postcss" src="./SFormGridItem.css" scoped></style>
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <div v-bind="ownedAttrs.bindings()" class="s-left-sidebar">
3
+ <SPanel
4
+ :title="title"
5
+ :icon="icon"
6
+ :state="state"
7
+ :no-content-padding="true"
8
+ :grow="true"
9
+ :layout="layout"
10
+ :scroll-behavior="scrollBehavior"
11
+ surface-radius="none"
12
+ surface-tone="transparent"
13
+ surface-border="none"
14
+ >
15
+ <template #actions>
16
+ <slot name="actions" />
17
+ </template>
18
+
19
+ <SSidebarGroup
20
+ v-if="state.status === 'ready'"
21
+ :sections-state="sectionsState"
22
+ @update-sections="emit('update-sections', $event)"
23
+ @toggle-section="emit('toggle-section', $event)"
24
+ >
25
+ <slot />
26
+ </SSidebarGroup>
27
+ </SPanel>
28
+ </div>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { type FunctionalComponent, type HTMLAttributes, type VNodeProps } from 'vue';
33
+ import SPanel from './SPanel.vue';
34
+ import SSidebarGroup from './SSidebarGroup.vue';
35
+ import type { SAsyncStateContract } from '../feedback/SAsyncState.vue';
36
+ import type { SidebarGroupEmits, SidebarSectionsState } from './sidebar';
37
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs';
38
+
39
+ defineOptions({ inheritAttrs: false });
40
+
41
+ export interface Props {
42
+ title?: string;
43
+ icon?: FunctionalComponent<HTMLAttributes & VNodeProps>;
44
+ /** Единственное состояние содержимого sidebar. / The sole sidebar-content state. */
45
+ state?: SAsyncStateContract;
46
+ sectionsState: SidebarSectionsState;
47
+ layout?: 'desktop' | 'mobile';
48
+ scrollBehavior?: 'auto' | 'body' | 'self';
49
+ }
50
+
51
+ withDefaults(defineProps<Props>(), {
52
+ title: undefined,
53
+ icon: undefined,
54
+ state: () => ({ status: 'ready' }),
55
+ layout: 'desktop',
56
+ scrollBehavior: 'auto',
57
+ });
58
+
59
+ const emit = defineEmits<SidebarGroupEmits>();
60
+ const ownedAttrs = useOwnedAttrs({ component: 'SLeftSidebar', owner: 'left-sidebar root' });
61
+ </script>
62
+
63
+ <style scoped>
64
+ @reference "../../../styles/reference.css";
65
+
66
+ .s-left-sidebar {
67
+ /*
68
+ Компонент должен быть flex-контейнером, чтобы правильно управлять высотой
69
+ дочернего SPanel и занимать все доступное пространство, которое ему
70
+ выделяет родительский layout.
71
+ */
72
+ @apply flex flex-col h-full w-full;
73
+ }
74
+ </style>