@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,286 @@
1
+ <template>
2
+ <div v-bind="ownedAttrs.bindings()" class="workbench-panel" data-testid="workbench-panel-shell">
3
+ <slot name="dialogs" />
4
+
5
+ <div class="workbench-header">
6
+ <slot name="header-navigation" />
7
+ <div class="title-container">
8
+ <slot name="title" />
9
+ </div>
10
+ <slot name="header-actions" />
11
+ </div>
12
+
13
+ <slot name="status" />
14
+
15
+ <slot v-if="renderState.mode === 'creating'" name="creating" />
16
+
17
+ <SAsyncState v-else :state="renderState.state">
18
+ <template v-if="$slots.loading" #loading="slotProps">
19
+ <slot name="loading" v-bind="slotProps" />
20
+ </template>
21
+ <template v-if="$slots.error" #error="slotProps">
22
+ <slot name="error" v-bind="slotProps" />
23
+ </template>
24
+
25
+ <div
26
+ ref="shellRef"
27
+ class="workbench-shell"
28
+ :class="shellClasses"
29
+ :style="shellStyle"
30
+ >
31
+ <div class="workbench-shell-main" data-testid="workbench-shell-main">
32
+ <div class="workbench-shell-main-content">
33
+ <slot name="main" />
34
+ </div>
35
+ </div>
36
+
37
+ <div
38
+ v-if="showSecondary"
39
+ class="workbench-shell-secondary-wrap"
40
+ :class="`workbench-shell-secondary-wrap--${secondaryPlacement}`"
41
+ >
42
+ <SDockRegion
43
+ :placement="dockPlacement"
44
+ :visible="showSecondary"
45
+ :size="secondarySizePx"
46
+ :min-size="secondaryBounds.min"
47
+ :max-size="secondaryBounds.max"
48
+ :resize-step="16"
49
+ :resize-label="secondaryResizeLabel"
50
+ :resize-test-id="secondaryResizeTestId"
51
+ :hide-resize-handle-on-narrow="secondaryResponsiveStack"
52
+ :responsive-size="secondaryResponsiveStack ? 'fill-on-narrow' : 'fixed'"
53
+ handle-variant="overlay"
54
+ surface="transparent"
55
+ @resize-start="refreshSecondaryBounds"
56
+ @resize="resizeSecondary"
57
+ >
58
+ <div
59
+ class="workbench-shell-secondary flex flex-col"
60
+ :class="`workbench-shell-secondary--${secondaryPlacement}`"
61
+ data-testid="workbench-shell-secondary"
62
+ >
63
+ <div
64
+ v-if="$slots['secondary-header'] || $slots['secondary-actions']"
65
+ class="workbench-shell-secondary-header"
66
+ :class="{ 'workbench-shell-secondary-header--sticky': secondaryStickyHeader }"
67
+ data-testid="workbench-shell-secondary-header"
68
+ >
69
+ <div class="workbench-shell-secondary-header-main">
70
+ <slot name="secondary-header" />
71
+ </div>
72
+ <div class="workbench-shell-secondary-header-actions">
73
+ <slot name="secondary-actions" />
74
+ </div>
75
+ </div>
76
+
77
+ <div
78
+ class="workbench-shell-secondary-content"
79
+ :class="{ 'flex min-h-0 flex-col': isBottomDock }"
80
+ data-testid="workbench-shell-secondary-content"
81
+ >
82
+ <slot name="secondary" />
83
+ </div>
84
+ </div>
85
+ </SDockRegion>
86
+ </div>
87
+ </div>
88
+ </SAsyncState>
89
+ </div>
90
+ </template>
91
+
92
+ <script setup lang="ts">
93
+ import { computed, onBeforeUnmount, onMounted, ref, useSlots, watch } from 'vue'
94
+ import SAsyncState from '../shared/feedback/SAsyncState.vue'
95
+ import { useOwnedAttrs } from '../../internal/ownedAttrs'
96
+ import {
97
+ validateBoolean,
98
+ validateExactString,
99
+ } from '../../internal/runtimeContract'
100
+ import SDockRegion from './SDockRegion.vue'
101
+ import type { WorkbenchPanelState } from './types'
102
+
103
+ defineOptions({ inheritAttrs: false })
104
+
105
+ type SecondaryPlacement = 'none' | 'right-drawer' | 'bottom-dock'
106
+ type SecondarySize = 'sm' | 'md' | 'lg'
107
+ const RIGHT_DRAWER_WIDTHS: Record<SecondarySize, number> = {
108
+ sm: 320,
109
+ md: 384,
110
+ lg: 480,
111
+ }
112
+
113
+ const BOTTOM_DOCK_HEIGHTS: Record<SecondarySize, number> = {
114
+ sm: 240,
115
+ md: 304,
116
+ lg: 384,
117
+ }
118
+
119
+ const props = withDefaults(defineProps<{
120
+ state: WorkbenchPanelState
121
+ secondaryPlacement?: SecondaryPlacement
122
+ secondaryOpen?: boolean
123
+ secondarySize?: SecondarySize
124
+ secondaryStickyHeader?: boolean
125
+ secondaryResponsiveStack?: boolean
126
+ }>(), {
127
+ secondaryPlacement: 'none',
128
+ secondaryOpen: true,
129
+ secondarySize: 'md',
130
+ secondaryStickyHeader: false,
131
+ secondaryResponsiveStack: false,
132
+ })
133
+
134
+ const slots = useSlots()
135
+ const ownedAttrs = useOwnedAttrs({ component: 'SWorkbenchPanelShell', owner: 'workbench panel root' })
136
+ const shellRef = ref<HTMLElement | null>(null)
137
+ const secondarySizePx = ref(resolveSecondarySizePx(props.secondaryPlacement, props.secondarySize))
138
+ const secondaryBounds = ref(resolveSecondaryBounds())
139
+ let shellResizeObserver: ResizeObserver | null = null
140
+ const renderState = computed(() => {
141
+ assertShellOptions()
142
+ const candidate: unknown = props.state
143
+ if (
144
+ typeof candidate !== 'object'
145
+ || candidate === null
146
+ || Array.isArray(candidate)
147
+ || Reflect.get(candidate, 'status') !== 'creating'
148
+ ) {
149
+ return {
150
+ mode: 'async' as const,
151
+ state: candidate as Exclude<WorkbenchPanelState, { status: 'creating' }>,
152
+ }
153
+ }
154
+ if (Object.keys(candidate).length !== 1) {
155
+ throw new TypeError('SWorkbenchPanelShell: state со status=creating должен содержать только поле status')
156
+ }
157
+ return { mode: 'creating' as const }
158
+ })
159
+
160
+ const hasSecondary = computed(() =>
161
+ Boolean(slots.secondary || slots['secondary-header'] || slots['secondary-actions'])
162
+ )
163
+
164
+ const showSecondary = computed(() =>
165
+ props.secondaryPlacement !== 'none'
166
+ && props.secondaryOpen
167
+ && hasSecondary.value
168
+ )
169
+
170
+ const isBottomDock = computed(() => props.secondaryPlacement === 'bottom-dock')
171
+ const dockPlacement = computed<'right' | 'bottom'>(() => isBottomDock.value ? 'bottom' : 'right')
172
+ const secondaryResizeLabel = computed(() => isBottomDock.value
173
+ ? 'Изменить высоту нижней панели'
174
+ : 'Изменить ширину боковой панели')
175
+ const secondaryResizeTestId = computed(() => isBottomDock.value
176
+ ? 'workbench-shell-resize-handle-bottom'
177
+ : 'workbench-shell-resize-handle-right')
178
+
179
+ const shellClasses = computed(() => [
180
+ `workbench-shell--${props.secondaryPlacement}`,
181
+ `workbench-shell--secondary-${props.secondarySize}`,
182
+ {
183
+ 'workbench-shell--secondary-open': showSecondary.value,
184
+ 'workbench-shell--secondary-responsive-stack': props.secondaryResponsiveStack,
185
+ },
186
+ ])
187
+
188
+ const shellStyle = computed<Record<string, string>>(() => {
189
+ return {
190
+ '--workbench-secondary-size': `${secondarySizePx.value}px`,
191
+ }
192
+ })
193
+
194
+ watch(
195
+ () => [props.secondaryPlacement, props.secondarySize] as const,
196
+ ([secondaryPlacement, secondarySize]) => {
197
+ secondarySizePx.value = resolveSecondarySizePx(secondaryPlacement, secondarySize)
198
+ secondaryBounds.value = resolveSecondaryBounds()
199
+ },
200
+ { immediate: true },
201
+ )
202
+
203
+ function resolveSecondarySizePx(
204
+ secondaryPlacement: SecondaryPlacement,
205
+ secondarySize: SecondarySize,
206
+ ): number {
207
+ if (secondaryPlacement === 'bottom-dock') {
208
+ return BOTTOM_DOCK_HEIGHTS[secondarySize]
209
+ }
210
+
211
+ return RIGHT_DRAWER_WIDTHS[secondarySize]
212
+ }
213
+
214
+ function resolveSecondaryBounds(): { min: number, max: number } {
215
+ const shell = shellRef.value
216
+ if (!shell) {
217
+ return props.secondaryPlacement === 'bottom-dock'
218
+ ? { min: 192, max: 640 }
219
+ : { min: 248, max: 768 }
220
+ }
221
+
222
+ if (props.secondaryPlacement === 'right-drawer') {
223
+ if (shell.clientWidth <= 0) return { min: 248, max: 768 }
224
+ const shellWidth = Math.max(shell.clientWidth, 320)
225
+ const minWidth = Math.min(320, Math.max(248, Math.floor(shellWidth * 0.34)))
226
+ const maxWidth = Math.max(minWidth, Math.floor(shellWidth * 0.62))
227
+ return { min: minWidth, max: maxWidth }
228
+ }
229
+
230
+ if (props.secondaryPlacement === 'bottom-dock') {
231
+ if (shell.clientHeight <= 0) return { min: 192, max: 640 }
232
+ const shellHeight = Math.max(shell.clientHeight, 320)
233
+ const minHeight = Math.min(320, Math.max(192, Math.floor(shellHeight * 0.24)))
234
+ const maxHeight = Math.max(minHeight, Math.floor(shellHeight * 0.68))
235
+ return { min: minHeight, max: maxHeight }
236
+ }
237
+
238
+ return { min: 0, max: 0 }
239
+ }
240
+
241
+ function refreshSecondaryBounds(): void {
242
+ const bounds = resolveSecondaryBounds()
243
+ secondaryBounds.value = bounds
244
+ secondarySizePx.value = Math.min(bounds.max, Math.max(bounds.min, secondarySizePx.value))
245
+ }
246
+
247
+ function resizeSecondary(nextSize: number): void {
248
+ refreshSecondaryBounds()
249
+ secondarySizePx.value = Math.min(
250
+ secondaryBounds.value.max,
251
+ Math.max(secondaryBounds.value.min, nextSize),
252
+ )
253
+ }
254
+
255
+ function assertShellOptions(): void {
256
+ validateExactString(
257
+ 'SWorkbenchPanelShell',
258
+ 'secondaryPlacement',
259
+ props.secondaryPlacement,
260
+ ['none', 'right-drawer', 'bottom-dock'] as const,
261
+ )
262
+ validateExactString(
263
+ 'SWorkbenchPanelShell',
264
+ 'secondarySize',
265
+ props.secondarySize,
266
+ ['sm', 'md', 'lg'] as const,
267
+ )
268
+ validateBoolean('SWorkbenchPanelShell', 'secondaryOpen', props.secondaryOpen)
269
+ validateBoolean('SWorkbenchPanelShell', 'secondaryStickyHeader', props.secondaryStickyHeader)
270
+ validateBoolean('SWorkbenchPanelShell', 'secondaryResponsiveStack', props.secondaryResponsiveStack)
271
+ }
272
+
273
+ onMounted(() => {
274
+ refreshSecondaryBounds()
275
+ if (typeof ResizeObserver === 'undefined' || !shellRef.value) return
276
+ shellResizeObserver = new ResizeObserver(refreshSecondaryBounds)
277
+ shellResizeObserver.observe(shellRef.value)
278
+ })
279
+
280
+ onBeforeUnmount(() => {
281
+ shellResizeObserver?.disconnect()
282
+ shellResizeObserver = null
283
+ })
284
+ </script>
285
+
286
+ <style lang="postcss" src="./SWorkbenchPanelShell.css" scoped></style>
@@ -0,0 +1,45 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-app-brand__text {
4
+ min-width: 0;
5
+ }
6
+
7
+ .s-app-brand__content {
8
+ @apply flex min-w-0 items-center gap-3;
9
+ }
10
+
11
+ .s-app-brand__icon {
12
+ @apply flex shrink-0 items-center justify-center bg-primary-500 text-primary-contrast shadow-md shadow-primary-500/20;
13
+ width: var(--s-app-brand-icon-size);
14
+ height: var(--s-app-brand-icon-size);
15
+ border-radius: var(--s-app-brand-icon-radius);
16
+ }
17
+
18
+ .s-app-brand__text {
19
+ @apply flex flex-col;
20
+ }
21
+
22
+ .s-app-brand__title,
23
+ .s-app-brand__subtitle {
24
+ @apply truncate;
25
+ }
26
+
27
+ .s-app-brand__title {
28
+ @apply text-base font-semibold tracking-wide;
29
+ }
30
+
31
+ .s-app-brand__content[data-placement='header'] .s-app-brand__title {
32
+ @apply text-surface-950 dark:text-surface-0;
33
+ }
34
+
35
+ .s-app-brand__subtitle {
36
+ @apply text-surface-500;
37
+ }
38
+
39
+ .s-app-brand__content[data-placement='sidebar'] .s-app-brand__subtitle {
40
+ @apply text-xs;
41
+ }
42
+
43
+ .s-app-brand__content[data-placement='header'] .s-app-brand__subtitle {
44
+ @apply text-sm;
45
+ }
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ import { computed, useSlots } from 'vue'
3
+ import type { RouteLocationRaw } from 'vue-router'
4
+ import SLink from '../../shared/controls/SLink.vue'
5
+ import SAppBrandContent from './SAppBrandContent.vue'
6
+
7
+ withDefaults(defineProps<{
8
+ title: string
9
+ subtitle?: string
10
+ to?: RouteLocationRaw | null
11
+ placement: 'sidebar' | 'header'
12
+ }>(), {
13
+ subtitle: '',
14
+ to: null,
15
+ })
16
+
17
+ const slots = useSlots()
18
+ const hasLogo = computed(() => Boolean(slots.logo))
19
+ const hasIcon = computed(() => Boolean(slots.icon))
20
+ </script>
21
+
22
+ <template>
23
+ <div class="s-app-brand" :data-placement="placement">
24
+ <SLink v-if="to" :to="to" variant="brand" :aria-label="hasLogo ? title : undefined">
25
+ <SAppBrandContent :title="title" :subtitle="subtitle" :placement="placement" :has-logo="hasLogo" :has-icon="hasIcon">
26
+ <template v-if="hasLogo" #logo><slot name="logo" /></template>
27
+ <template v-if="hasIcon" #icon><slot name="icon" /></template>
28
+ </SAppBrandContent>
29
+ </SLink>
30
+ <div v-else data-s-app-brand-passive>
31
+ <SAppBrandContent :title="title" :subtitle="subtitle" :placement="placement" :has-logo="hasLogo" :has-icon="hasIcon">
32
+ <template v-if="hasLogo" #logo><slot name="logo" /></template>
33
+ <template v-if="hasIcon" #icon><slot name="icon" /></template>
34
+ </SAppBrandContent>
35
+ </div>
36
+ </div>
37
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ withDefaults(defineProps<{
3
+ title: string
4
+ subtitle?: string
5
+ placement: 'sidebar' | 'header'
6
+ hasLogo?: boolean
7
+ hasIcon?: boolean
8
+ }>(), {
9
+ subtitle: '',
10
+ hasLogo: false,
11
+ hasIcon: false,
12
+ })
13
+ </script>
14
+
15
+ <template>
16
+ <span class="s-app-brand__content" :data-placement="placement">
17
+ <slot v-if="hasLogo" name="logo" />
18
+ <template v-else>
19
+ <span v-if="hasIcon" class="s-app-brand__icon" aria-hidden="true">
20
+ <slot name="icon" />
21
+ </span>
22
+ <span class="s-app-brand__text">
23
+ <span class="s-app-brand__title">{{ title }}</span>
24
+ <span v-if="subtitle" class="s-app-brand__subtitle">{{ subtitle }}</span>
25
+ </span>
26
+ </template>
27
+ </span>
28
+ </template>
29
+
30
+ <style lang="postcss" src="./SAppBrand.css" scoped></style>
@@ -0,0 +1,28 @@
1
+ import type { Component } from 'vue'
2
+ import type { RouteLocationRaw } from 'vue-router'
3
+ import type { BadgeSeverity } from '../shared/data-display/SBadge.vue'
4
+ import type { SAsyncStateContract } from '../shared/feedback/SAsyncState.vue'
5
+
6
+ export type ShellBadgeSeverity = BadgeSeverity
7
+
8
+ /**
9
+ * Точное взаимоисключающее состояние рабочей панели.
10
+ * Exact mutually exclusive workbench panel state.
11
+ */
12
+ type WorkbenchPanelAsyncState = Exclude<SAsyncStateContract, { status: 'empty' }> extends infer State
13
+ ? State extends { status: 'loading' }
14
+ ? State & { message: string }
15
+ : State
16
+ : never
17
+
18
+ export type WorkbenchPanelState = { status: 'creating' } | WorkbenchPanelAsyncState
19
+
20
+ export interface AppShellNavigationItem {
21
+ key: string
22
+ label: string
23
+ to: RouteLocationRaw
24
+ icon?: Component
25
+ badgeLabel?: string
26
+ badgeSeverity?: ShellBadgeSeverity
27
+ active?: boolean
28
+ }
@@ -0,0 +1,127 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-dialog-surface {
4
+ box-shadow: var(--s-elevation-overlay);
5
+ }
6
+
7
+ .s-dialog-surface[data-presentation='modal'] {
8
+ @apply w-full transform overflow-hidden rounded-2xl p-6 text-left align-middle transition-all;
9
+ @apply bg-surface-0 dark:bg-surface-800;
10
+ }
11
+
12
+ .s-dialog-surface[data-presentation='modal'][data-surface='subtle'] {
13
+ @apply bg-surface-50 dark:bg-surface-900;
14
+ }
15
+
16
+ .s-dialog-surface[data-presentation='modal'][data-scroll='body'] {
17
+ @apply flex max-h-[calc(100dvh-2rem)] flex-col;
18
+ }
19
+
20
+ .s-dialog-surface[data-presentation='modal'][data-scroll='body'] .s-dialog-surface__body {
21
+ @apply min-h-0 flex-1 overflow-y-auto;
22
+ }
23
+
24
+ .s-dialog-surface[data-presentation='modal'][data-layout='fullscreen'] {
25
+ @apply min-h-screen rounded-none;
26
+ max-width: 100%;
27
+ height: 100%;
28
+ }
29
+
30
+ .s-dialog-surface[data-presentation='modal'][data-size='sm'] {
31
+ max-width: 24rem;
32
+ }
33
+
34
+ .s-dialog-surface[data-presentation='modal'][data-size='md'] {
35
+ max-width: 28rem;
36
+ }
37
+
38
+ .s-dialog-surface[data-presentation='modal'][data-size='lg'] {
39
+ max-width: 32rem;
40
+ }
41
+
42
+ .s-dialog-surface[data-presentation='modal'][data-size='xl'] {
43
+ max-width: 36rem;
44
+ }
45
+
46
+ .s-dialog-surface[data-presentation='modal'][data-size='2xl'] {
47
+ max-width: 42rem;
48
+ }
49
+
50
+ .s-dialog-surface[data-presentation='modal'][data-size='3xl'] {
51
+ max-width: 48rem;
52
+ }
53
+
54
+ .s-dialog-surface[data-presentation='modal'][data-size='wide'] {
55
+ max-width: 72rem;
56
+ }
57
+
58
+ .s-dialog-surface[data-presentation='modal'] .s-dialog-surface__header {
59
+ @apply flex items-start justify-between border-b border-surface-200 pb-4 dark:border-surface-700;
60
+ }
61
+
62
+ .s-dialog-surface[data-presentation='modal'] .s-dialog-surface__title {
63
+ @apply text-lg font-medium leading-6 text-surface-900 dark:text-surface-100;
64
+ }
65
+
66
+ .s-dialog-surface[data-presentation='modal'] .s-dialog-surface__body {
67
+ @apply mt-4 text-sm text-surface-600 dark:text-surface-300;
68
+ }
69
+
70
+ .s-dialog-surface[data-presentation='modal'] .s-dialog-surface__footer {
71
+ @apply mt-6 flex justify-end gap-3 border-t border-surface-200 pt-4 dark:border-surface-700;
72
+ }
73
+
74
+ .s-dialog-surface[data-presentation='modal'] .s-dialog-surface__footer[data-layout='split'] {
75
+ @apply flex-wrap items-center justify-start gap-x-3 gap-y-2;
76
+ }
77
+
78
+ .s-dialog-surface__footer-start,
79
+ .s-dialog-surface__footer-end {
80
+ @apply flex min-w-0 flex-wrap items-center gap-2;
81
+ }
82
+
83
+ .s-dialog-surface__footer-start {
84
+ @apply mr-auto;
85
+ }
86
+
87
+ .s-dialog-surface__footer-end {
88
+ @apply ml-auto justify-end;
89
+ }
90
+
91
+ .s-dialog-surface[data-presentation='drawer'] {
92
+ @apply relative flex h-full w-full flex-col overflow-hidden border-l border-surface-200 bg-surface-0 text-surface-900 dark:border-surface-700 dark:bg-surface-900 dark:text-surface-0;
93
+ }
94
+
95
+ .s-dialog-surface[data-presentation='drawer'][data-size='sm'] {
96
+ max-width: 30rem;
97
+ }
98
+
99
+ .s-dialog-surface[data-presentation='drawer'][data-size='md'] {
100
+ max-width: 36rem;
101
+ }
102
+
103
+ .s-dialog-surface[data-presentation='drawer'][data-size='lg'] {
104
+ max-width: 44rem;
105
+ }
106
+
107
+ .s-dialog-surface[data-presentation='drawer'] .s-dialog-surface__header {
108
+ @apply flex items-start gap-3 border-b border-surface-200 px-5 py-4 dark:border-surface-700 md:px-6;
109
+ }
110
+
111
+ .s-dialog-surface[data-presentation='drawer'] .s-dialog-surface__title {
112
+ @apply min-w-0 flex-1 text-lg font-semibold leading-6 text-surface-950 dark:text-surface-0;
113
+ }
114
+
115
+ .s-dialog-surface[data-presentation='drawer'] .s-dialog-surface__body {
116
+ @apply flex-1 overflow-y-auto p-5 md:p-6;
117
+ }
118
+
119
+ .s-dialog-surface[data-presentation='drawer'] .s-dialog-surface__footer {
120
+ @apply flex flex-wrap justify-end gap-3 border-t border-surface-200 px-5 py-4 dark:border-surface-700 md:px-6;
121
+ }
122
+
123
+ @media (max-width: 767px) {
124
+ .s-dialog-surface[data-presentation='drawer'][data-layout='mobile-fullscreen'] {
125
+ @apply max-w-full border-l-0;
126
+ }
127
+ }
@@ -0,0 +1,117 @@
1
+ <template>
2
+ <component
3
+ :is="rootTag"
4
+ :ref="setSurfaceElement"
5
+ v-bind="$attrs"
6
+ class="s-dialog-surface"
7
+ :data-presentation="presentation.kind"
8
+ :data-layout="surfaceLayout"
9
+ :data-size="presentation.size"
10
+ :data-surface="modalPresentation?.surface"
11
+ :data-scroll="modalPresentation?.scrollBehavior"
12
+ tabindex="-1"
13
+ role="dialog"
14
+ aria-modal="true"
15
+ :aria-labelledby="hasHeader ? titleId : undefined"
16
+ :aria-label="hasHeader ? undefined : accessibleName"
17
+ >
18
+ <component :is="headerTag" v-if="hasHeader" class="s-dialog-surface__header">
19
+ <component :is="headingTag" :id="titleId" class="s-dialog-surface__title">
20
+ <slot name="header">{{ title }}</slot>
21
+ </component>
22
+ <SButton
23
+ v-if="closable"
24
+ :leading-icon="X"
25
+ icon-only
26
+ appearance="text"
27
+ size="sm"
28
+ aria-label="Закрыть"
29
+ @click="requestClose"
30
+ />
31
+ </component>
32
+
33
+ <div class="s-dialog-surface__body">
34
+ <slot />
35
+ </div>
36
+
37
+ <div
38
+ v-if="hasFooter"
39
+ class="s-dialog-surface__footer"
40
+ :data-layout="hasSplitFooter ? 'split' : 'end'"
41
+ >
42
+ <template v-if="hasSplitFooter">
43
+ <div v-if="$slots['footer-start']" class="s-dialog-surface__footer-start">
44
+ <slot name="footer-start" :close="requestClose" />
45
+ </div>
46
+ <div v-if="$slots['footer-end']" class="s-dialog-surface__footer-end">
47
+ <slot name="footer-end" :close="requestClose" />
48
+ </div>
49
+ </template>
50
+ <slot v-else name="footer" :close="requestClose" />
51
+ </div>
52
+ </component>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import { computed, useId, useSlots, type ComponentPublicInstance } from 'vue'
57
+
58
+ import { X } from '../../icons/sputnigUiIcons'
59
+ import SButton from '../controls/SButton.vue'
60
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
61
+ import {
62
+ assertDialogSurfacePresentation,
63
+ type DialogSurfacePresentation,
64
+ type ModalDialogSurfacePresentation,
65
+ } from './dialogSurface'
66
+
67
+ defineOptions({ inheritAttrs: false })
68
+
69
+ const props = defineProps<{
70
+ presentation: DialogSurfacePresentation
71
+ title?: string
72
+ accessibleName: string
73
+ closable: boolean
74
+ setSurfaceElement: (element: Element | ComponentPublicInstance | null) => void
75
+ }>()
76
+
77
+ const emit = defineEmits<{
78
+ 'request-close': []
79
+ }>()
80
+
81
+ defineSlots<{
82
+ header?: () => unknown
83
+ default?: () => unknown
84
+ footer?: (properties: { close: () => void }) => unknown
85
+ 'footer-start'?: (properties: { close: () => void }) => unknown
86
+ 'footer-end'?: (properties: { close: () => void }) => unknown
87
+ }>()
88
+
89
+ assertDialogSurfacePresentation(props.presentation)
90
+ useInteractiveLeafRegistration({ owner: 'SDialogSurface' })
91
+
92
+ const slots = useSlots()
93
+ const titleId = `s-dialog-title-${useId()}`
94
+ const isModal = computed(() => props.presentation.kind === 'modal')
95
+ const modalPresentation = computed<ModalDialogSurfacePresentation | null>(() => (
96
+ props.presentation.kind === 'modal' ? props.presentation : null
97
+ ))
98
+ const hasHeader = computed(() => Boolean(props.title || slots.header))
99
+ const hasSplitFooter = computed(() => isModal.value && Boolean(slots['footer-start'] || slots['footer-end']))
100
+ const hasFooter = computed(() => hasSplitFooter.value || Boolean(slots.footer))
101
+ const rootTag = 'div'
102
+ const headerTag = computed(() => (isModal.value ? 'div' : 'header'))
103
+ const headingTag = computed(() => (isModal.value ? 'h3' : 'h2'))
104
+ type DialogSurfaceLayout = 'contained' | 'fullscreen' | 'side-panel' | 'mobile-fullscreen'
105
+
106
+ const surfaceLayout = computed<DialogSurfaceLayout>(() => {
107
+ const presentation = props.presentation
108
+ if (presentation.kind === 'drawer') {
109
+ return presentation.mobileFullScreen ? 'mobile-fullscreen' : 'side-panel'
110
+ }
111
+ return presentation.fullscreen || presentation.size === 'full' ? 'fullscreen' : 'contained'
112
+ })
113
+
114
+ const requestClose = (): void => emit('request-close')
115
+ </script>
116
+
117
+ <style lang="postcss" src="./SDialogSurface.css" scoped></style>