@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,232 @@
1
+ <script setup lang="ts">
2
+ import { computed, getCurrentInstance, useSlots } from 'vue'
3
+ import type { RouteLocationRaw } from 'vue-router'
4
+ import { useOwnedAttrs } from '../../internal/ownedAttrs'
5
+ import { validateNonEmptyString } from '../../internal/runtimeContract'
6
+ import SBadge from '../shared/data-display/SBadge.vue'
7
+ import SPanel from '../shared/containers/SPanel.vue'
8
+ import SLink from '../shared/controls/SLink.vue'
9
+ import type { AppShellNavigationItem, ShellBadgeSeverity } from './types'
10
+ import SAppBrand from './_internal/SAppBrand.vue'
11
+
12
+ defineOptions({ inheritAttrs: false })
13
+
14
+ interface SharedProps {
15
+ brandTitle: string
16
+ brandTo?: RouteLocationRaw | null
17
+ brandSubtitle?: string
18
+ }
19
+
20
+ export interface SAppShellAppProps extends SharedProps {
21
+ mode?: 'app'
22
+ pageEyebrow?: string
23
+ pageTitle: string
24
+ pageBadgeLabel?: string
25
+ pageBadgeSeverity?: ShellBadgeSeverity
26
+ userLabel?: string
27
+ navigation?: readonly AppShellNavigationItem[]
28
+ navigationLabel?: string
29
+ showSidebar?: boolean
30
+ }
31
+
32
+ export interface SAppShellPublicProps extends SharedProps {
33
+ mode: 'public'
34
+ pageEyebrow?: never
35
+ pageTitle?: never
36
+ pageBadgeLabel?: never
37
+ pageBadgeSeverity?: never
38
+ userLabel?: never
39
+ navigation?: never
40
+ navigationLabel?: never
41
+ showSidebar?: never
42
+ }
43
+
44
+ export type Props = SAppShellAppProps | SAppShellPublicProps
45
+
46
+ interface RuntimeProps extends SharedProps {
47
+ mode?: 'app' | 'public'
48
+ pageEyebrow?: string
49
+ pageTitle?: string
50
+ pageBadgeLabel?: string
51
+ pageBadgeSeverity?: ShellBadgeSeverity
52
+ userLabel?: string
53
+ navigation?: readonly AppShellNavigationItem[]
54
+ navigationLabel?: string
55
+ showSidebar?: boolean
56
+ }
57
+
58
+ const props = defineProps<RuntimeProps>()
59
+ function requireComponentInstance() {
60
+ const instance = getCurrentInstance()
61
+ if (!instance) throw new Error('SAppShell: component instance unavailable during setup')
62
+ return instance
63
+ }
64
+ const componentInstance = requireComponentInstance()
65
+ const slots = useSlots()
66
+ const hasBrandIcon = computed(() => Boolean(slots['brand-icon']))
67
+ const hasBrandLogo = computed(() => Boolean(slots['brand-logo']))
68
+
69
+ type ResolvedShell =
70
+ | { mode: 'public' }
71
+ | {
72
+ mode: 'app'
73
+ navigation: readonly AppShellNavigationItem[]
74
+ navigationLabel: string
75
+ pageBadgeLabel: string
76
+ pageBadgeSeverity: ShellBadgeSeverity
77
+ pageEyebrow: string
78
+ pageTitle: string
79
+ showSidebar: boolean
80
+ userLabel: string
81
+ }
82
+
83
+ const appOnlyPropNames = [
84
+ 'pageEyebrow',
85
+ 'pageTitle',
86
+ 'pageBadgeLabel',
87
+ 'pageBadgeSeverity',
88
+ 'userLabel',
89
+ 'navigation',
90
+ 'navigationLabel',
91
+ 'showSidebar',
92
+ ] as const
93
+
94
+ function toKebabCase(value: string): string {
95
+ return value.replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`)
96
+ }
97
+
98
+ function hasExplicitProp(propName: string): boolean {
99
+ const vnodeProps = componentInstance.vnode.props
100
+ return vnodeProps !== null && (
101
+ Object.hasOwn(vnodeProps, propName)
102
+ || Object.hasOwn(vnodeProps, toKebabCase(propName))
103
+ )
104
+ }
105
+
106
+ const shell = computed<ResolvedShell>(() => {
107
+ validateNonEmptyString('SAppShell', 'brandTitle', props.brandTitle)
108
+ if (props.mode === 'public') {
109
+ for (const propName of appOnlyPropNames) {
110
+ if (hasExplicitProp(propName)) {
111
+ throw new TypeError(`SAppShell: app-only prop '${propName}' недоступен в public mode / is unavailable in public mode`)
112
+ }
113
+ }
114
+ return { mode: 'public' }
115
+ }
116
+
117
+ return {
118
+ mode: 'app',
119
+ navigation: props.navigation ?? [],
120
+ navigationLabel: validateNonEmptyString(
121
+ 'SAppShell',
122
+ 'navigationLabel',
123
+ props.navigationLabel ?? 'Основная навигация',
124
+ ),
125
+ pageBadgeLabel: props.pageBadgeLabel ?? '',
126
+ pageBadgeSeverity: props.pageBadgeSeverity ?? 'info',
127
+ pageEyebrow: props.pageEyebrow ?? '',
128
+ pageTitle: validateNonEmptyString('SAppShell', 'pageTitle', props.pageTitle),
129
+ showSidebar: hasExplicitProp('showSidebar') ? props.showSidebar === true : true,
130
+ userLabel: props.userLabel ?? '',
131
+ }
132
+ })
133
+
134
+ const shouldShowSidebar = computed(() => shell.value.mode === 'app' && shell.value.showSidebar)
135
+ const ownedAttrs = useOwnedAttrs({ component: 'SAppShell', owner: 'application shell root' })
136
+ </script>
137
+
138
+ <template>
139
+ <div v-bind="ownedAttrs.bindings()" class="min-h-screen">
140
+ <div class="mx-auto flex min-h-screen max-w-[1680px] gap-6 p-4 lg:p-6">
141
+ <aside
142
+ v-if="shouldShowSidebar"
143
+ class="hidden w-72 shrink-0 self-start lg:flex lg:flex-col"
144
+ >
145
+ <div class="sticky top-0">
146
+ <SPanel surface-overflow="clip">
147
+ <template #header>
148
+ <SAppBrand
149
+ :title="brandTitle"
150
+ :subtitle="brandSubtitle"
151
+ :to="brandTo"
152
+ placement="sidebar"
153
+ >
154
+ <template v-if="hasBrandLogo" #logo><slot name="brand-logo" /></template>
155
+ <template v-if="hasBrandIcon" #icon><slot name="brand-icon" /></template>
156
+ </SAppBrand>
157
+ </template>
158
+ <nav class="space-y-2 px-2 pb-2" :aria-label="shell.mode === 'app' ? shell.navigationLabel : undefined">
159
+ <SLink
160
+ v-for="item in shell.mode === 'app' ? shell.navigation : []"
161
+ :key="item.key"
162
+ :to="item.to"
163
+ variant="navigation"
164
+ width="full"
165
+ :active="item.active"
166
+ :leading-icon="item.icon"
167
+ >
168
+ {{ item.label }}
169
+ <template v-if="item.badgeLabel" #trailing>
170
+ <SBadge :severity="item.badgeSeverity ?? 'secondary'">
171
+ {{ item.badgeLabel }}
172
+ </SBadge>
173
+ </template>
174
+ </SLink>
175
+ </nav>
176
+ <template #actions>
177
+ <slot name="sidebar-actions" />
178
+ </template>
179
+ </SPanel>
180
+ </div>
181
+ </aside>
182
+
183
+ <div class="min-w-0 flex-1">
184
+ <header
185
+ class="mb-6 px-1"
186
+ :class="shell.mode === 'app'
187
+ ? 'rounded-[1.4rem] border border-surface-200/80 bg-surface-0/88 px-4 py-3 shadow-sm backdrop-blur dark:border-surface-700 dark:bg-surface-900/74'
188
+ : 'rounded-[var(--s-app-shell-floating-radius)] border border-surface-200/80 bg-surface-0/82 px-4 py-3 shadow-[var(--s-app-shell-floating-shadow)] backdrop-blur dark:border-surface-700 dark:bg-surface-900/72'"
189
+ >
190
+ <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
191
+ <div v-if="shell.mode === 'app'" class="min-w-0">
192
+ <div v-if="shell.pageEyebrow" class="text-xs uppercase tracking-[0.3em] text-surface-500">{{ shell.pageEyebrow }}</div>
193
+ <div class="flex flex-wrap items-center gap-2">
194
+ <h1 class="text-lg font-semibold">{{ shell.pageTitle }}</h1>
195
+ <SBadge v-if="shell.pageBadgeLabel" :severity="shell.pageBadgeSeverity">
196
+ {{ shell.pageBadgeLabel }}
197
+ </SBadge>
198
+ </div>
199
+ <div v-if="shell.userLabel" class="text-sm text-surface-500">
200
+ {{ shell.userLabel }}
201
+ </div>
202
+ </div>
203
+
204
+ <SAppBrand
205
+ v-else
206
+ :title="brandTitle"
207
+ :subtitle="brandSubtitle"
208
+ :to="brandTo"
209
+ placement="header"
210
+ >
211
+ <template v-if="hasBrandLogo" #logo>
212
+ <slot name="brand-logo" />
213
+ </template>
214
+ <template v-if="hasBrandIcon" #icon>
215
+ <slot name="brand-icon" />
216
+ </template>
217
+ </SAppBrand>
218
+
219
+ <div class="flex flex-wrap items-center gap-2">
220
+ <slot name="header-actions" />
221
+ </div>
222
+ </div>
223
+ </header>
224
+
225
+ <slot name="banner" />
226
+ <main class="pb-8 pt-1">
227
+ <slot />
228
+ </main>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </template>
@@ -0,0 +1,200 @@
1
+ <template>
2
+ <div
3
+ v-if="contract.visible"
4
+ v-bind="ownedAttrs.bindings()"
5
+ :class="containerClasses"
6
+ :data-dock-placement="contract.placement"
7
+ :data-responsive-size="contract.responsiveSize"
8
+ >
9
+ <template v-for="part in orderedParts" :key="part">
10
+ <SResizeHandle
11
+ v-if="part === 'handle'"
12
+ :orientation="handleOrientation"
13
+ :value-direction="handleValueDirection"
14
+ :variant="contract.handleVariant"
15
+ :label="resolvedResizeLabel"
16
+ :data-test-id="contract.resizeTestId ?? `${contract.placement}-dock-resizer`"
17
+ :hidden-on-narrow="contract.hideResizeHandleOnNarrow"
18
+ :resizing="effectiveResizing"
19
+ :value="size"
20
+ :min="minSize"
21
+ :max="maxSize"
22
+ :step="resizeStep"
23
+ @resize-start="handleResizeStart"
24
+ @resize="emit('resize', $event)"
25
+ @resize-end="handleResizeEnd"
26
+ />
27
+
28
+ <div
29
+ v-else
30
+ :data-testid="`${placement}-dock-panel`"
31
+ :class="panelClasses"
32
+ :style="panelStyle"
33
+ >
34
+ <slot />
35
+ </div>
36
+ </template>
37
+ </div>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import { computed, ref, type CSSProperties } from 'vue'
42
+ import { useOwnedAttrs } from '../../internal/ownedAttrs'
43
+ import {
44
+ validateBoolean,
45
+ validateExactString,
46
+ validateNonEmptyString,
47
+ } from '../../internal/runtimeContract'
48
+ import SResizeHandle from './SResizeHandle.vue'
49
+
50
+ defineOptions({ inheritAttrs: false })
51
+
52
+ type DockPlacement = 'left' | 'right' | 'bottom'
53
+
54
+ const props = withDefaults(defineProps<{
55
+ placement: DockPlacement
56
+ visible: boolean
57
+ size: number
58
+ minSize?: number
59
+ maxSize?: number
60
+ resizeStep?: number
61
+ resizing?: boolean
62
+ surface?: 'base' | 'subtle' | 'transparent'
63
+ elevated?: boolean
64
+ resizeLabel?: string
65
+ resizeTestId?: string
66
+ handleVariant?: 'inline' | 'overlay'
67
+ hideResizeHandleOnNarrow?: boolean
68
+ responsiveSize?: 'fixed' | 'fill-on-narrow'
69
+ }>(), {
70
+ minSize: 0,
71
+ maxSize: 4096,
72
+ resizeStep: 10,
73
+ resizing: false,
74
+ surface: 'base',
75
+ elevated: false,
76
+ resizeLabel: undefined,
77
+ resizeTestId: undefined,
78
+ handleVariant: 'inline',
79
+ hideResizeHandleOnNarrow: false,
80
+ responsiveSize: 'fixed',
81
+ })
82
+
83
+ const emit = defineEmits<{
84
+ (event: 'resize-start', pointerEvent: PointerEvent): void
85
+ (event: 'resize', size: number): void
86
+ (event: 'resize-end', size: number): void
87
+ }>()
88
+
89
+ const pointerResizing = ref(false)
90
+ const ownedAttrs = useOwnedAttrs({ component: 'SDockRegion', owner: 'dock region root' })
91
+ const contract = computed(() => ({
92
+ placement: validateExactString(
93
+ 'SDockRegion', 'placement', props.placement, ['left', 'right', 'bottom'] as const,
94
+ ),
95
+ visible: validateBoolean('SDockRegion', 'visible', props.visible),
96
+ surface: validateExactString(
97
+ 'SDockRegion', 'surface', props.surface, ['base', 'subtle', 'transparent'] as const,
98
+ ),
99
+ elevated: validateBoolean('SDockRegion', 'elevated', props.elevated),
100
+ handleVariant: validateExactString(
101
+ 'SDockRegion', 'handleVariant', props.handleVariant, ['inline', 'overlay'] as const,
102
+ ),
103
+ hideResizeHandleOnNarrow: validateBoolean(
104
+ 'SDockRegion', 'hideResizeHandleOnNarrow', props.hideResizeHandleOnNarrow,
105
+ ),
106
+ responsiveSize: validateExactString(
107
+ 'SDockRegion', 'responsiveSize', props.responsiveSize, ['fixed', 'fill-on-narrow'] as const,
108
+ ),
109
+ resizeTestId: props.resizeTestId === undefined
110
+ ? undefined
111
+ : validateNonEmptyString('SDockRegion', 'resizeTestId', props.resizeTestId),
112
+ }))
113
+ const effectiveResizing = computed(() => props.resizing || pointerResizing.value)
114
+
115
+ const handleOrientation = computed(() => contract.value.placement === 'bottom' ? 'horizontal' : 'vertical')
116
+ const handleValueDirection = computed(() => contract.value.placement === 'left' ? 'forward' : 'reverse')
117
+ const orderedParts = computed<readonly ('panel' | 'handle')[]>(() => contract.value.placement === 'left'
118
+ ? ['panel', 'handle']
119
+ : ['handle', 'panel'])
120
+
121
+ const resolvedResizeLabel = computed(() => {
122
+ if (props.resizeLabel !== undefined) {
123
+ return validateNonEmptyString('SDockRegion', 'resizeLabel', props.resizeLabel)
124
+ }
125
+ if (contract.value.placement === 'bottom') return 'Изменить высоту нижней панели'
126
+ if (contract.value.placement === 'left') return 'Изменить ширину левой панели'
127
+ return 'Изменить ширину правой панели'
128
+ })
129
+
130
+ function handleResizeStart(event: PointerEvent): void {
131
+ pointerResizing.value = true
132
+ emit('resize-start', event)
133
+ }
134
+
135
+ function handleResizeEnd(size: number): void {
136
+ pointerResizing.value = false
137
+ emit('resize-end', size)
138
+ }
139
+
140
+ const containerClasses = computed(() => contract.value.placement === 'bottom'
141
+ ? 'contents'
142
+ : ['flex min-h-0 flex-shrink-0', { relative: contract.value.handleVariant === 'overlay' }])
143
+
144
+ const panelClasses = computed(() => [
145
+ 's-dock-region__panel relative flex-shrink-0',
146
+ contract.value.placement === 'bottom' ? 'flex min-h-0 flex-col' : 'h-full min-h-0',
147
+ contract.value.surface === 'base'
148
+ ? 'bg-surface-0 dark:bg-surface-900'
149
+ : contract.value.surface === 'subtle'
150
+ ? 'bg-surface-50 dark:bg-surface-850'
151
+ : 'bg-transparent',
152
+ {
153
+ 'transition-none': effectiveResizing.value,
154
+ 'z-[var(--s-layer-navigation)] overflow-visible': contract.value.elevated,
155
+ 'z-[var(--s-layer-affordance)] overflow-hidden': contract.value.placement === 'bottom' && !contract.value.elevated,
156
+ },
157
+ ])
158
+
159
+ const panelStyle = computed<CSSProperties>(() => {
160
+ if (![props.size, props.minSize, props.maxSize, props.resizeStep].every(Number.isFinite)) {
161
+ throw new RangeError('SDockRegion: size, minSize, maxSize и resizeStep должны быть конечными числами')
162
+ }
163
+ if (props.minSize > props.maxSize) {
164
+ throw new RangeError(`SDockRegion: minSize ${props.minSize} не может превышать maxSize ${props.maxSize}`)
165
+ }
166
+ if (props.size < props.minSize || props.size > props.maxSize) {
167
+ throw new RangeError(
168
+ `SDockRegion: size ${props.size} должен находиться в диапазоне ${props.minSize}..${props.maxSize}`,
169
+ )
170
+ }
171
+ if (props.resizeStep <= 0) {
172
+ throw new RangeError('SDockRegion: resizeStep должен быть больше нуля')
173
+ }
174
+
175
+ return { '--s-dock-region-size': `${props.size}px` }
176
+ })
177
+ </script>
178
+
179
+ <style scoped>
180
+ .s-dock-region__panel {
181
+ width: var(--s-dock-region-size);
182
+ }
183
+
184
+ [data-dock-placement='bottom'] > .s-dock-region__panel {
185
+ width: 100%;
186
+ height: var(--s-dock-region-size);
187
+ }
188
+
189
+ @media (max-width: 1023px) {
190
+ [data-responsive-size='fill-on-narrow'] {
191
+ width: 100%;
192
+ height: 100%;
193
+ }
194
+
195
+ [data-responsive-size='fill-on-narrow'] > .s-dock-region__panel {
196
+ width: 100%;
197
+ height: 100%;
198
+ }
199
+ }
200
+ </style>
@@ -0,0 +1,251 @@
1
+ <template>
2
+ <button
3
+ v-bind="ownedAttrs.bindings()"
4
+ type="button"
5
+ role="separator"
6
+ :aria-orientation="orientation"
7
+ :aria-label="validatedLabel"
8
+ :aria-valuenow="validatedRange.value"
9
+ :aria-valuemin="validatedRange.min"
10
+ :aria-valuemax="validatedRange.max"
11
+ :aria-disabled="disabled || undefined"
12
+ :disabled="disabled"
13
+ :title="validatedLabel"
14
+ :data-testid="dataTestId"
15
+ :data-resizing="effectiveResizing || undefined"
16
+ :data-hidden-on-narrow="hiddenOnNarrow || undefined"
17
+ :class="handleClasses"
18
+ @pointerdown="handlePointerDown"
19
+ @keydown="handleKeydown"
20
+ >
21
+ <span aria-hidden="true" :class="lineClasses"></span>
22
+ </button>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import { computed, onBeforeUnmount, ref } from 'vue'
27
+ import { useOwnedAttrs } from '../../internal/ownedAttrs'
28
+ import { useInteractiveLeafRegistration } from '../../internal/passiveContentContract'
29
+ import {
30
+ acquirePointerInteractionLease,
31
+ type PointerInteractionLease,
32
+ } from '../../internal/pointerInteractionLease'
33
+ import { validateNonEmptyString } from '../../internal/runtimeContract'
34
+
35
+ defineOptions({ inheritAttrs: false })
36
+
37
+ const props = withDefaults(defineProps<{
38
+ orientation: 'vertical' | 'horizontal'
39
+ variant?: 'inline' | 'overlay'
40
+ valueDirection?: 'forward' | 'reverse'
41
+ label: string
42
+ dataTestId?: string
43
+ resizing?: boolean
44
+ hiddenOnNarrow?: boolean
45
+ disabled?: boolean
46
+ value?: number
47
+ min?: number
48
+ max?: number
49
+ step?: number
50
+ pointerScale?: number
51
+ }>(), {
52
+ orientation: 'vertical',
53
+ variant: 'inline',
54
+ valueDirection: 'forward',
55
+ dataTestId: undefined,
56
+ resizing: false,
57
+ hiddenOnNarrow: false,
58
+ disabled: false,
59
+ value: 50,
60
+ min: 0,
61
+ max: 100,
62
+ step: 10,
63
+ pointerScale: 1,
64
+ })
65
+
66
+ const emit = defineEmits<{
67
+ (event: 'resize-start', pointerEvent: PointerEvent): void
68
+ (event: 'resize', value: number): void
69
+ (event: 'resize-end', value: number): void
70
+ }>()
71
+
72
+ type PointerResizeState = {
73
+ pointerId: number
74
+ startCoordinate: number
75
+ startValue: number
76
+ latestValue: number
77
+ target: HTMLButtonElement
78
+ interactionLease: PointerInteractionLease
79
+ }
80
+
81
+ const pointerResizeState = ref<PointerResizeState | null>(null)
82
+ const ownedAttrs = useOwnedAttrs({ component: 'SResizeHandle', owner: 'native separator button' })
83
+ useInteractiveLeafRegistration({ owner: 'SResizeHandle' })
84
+ const effectiveResizing = computed(() => props.resizing || pointerResizeState.value !== null)
85
+ const validatedLabel = computed(() => validateNonEmptyString('SResizeHandle', 'label', props.label))
86
+ const validatedRange = computed(() => {
87
+ assertRangeContract()
88
+ return { value: props.value, min: props.min, max: props.max }
89
+ })
90
+
91
+ function assertRangeContract(): void {
92
+ if (![props.value, props.min, props.max, props.step, props.pointerScale].every(Number.isFinite)) {
93
+ throw new RangeError('SResizeHandle: value, min, max, step и pointerScale должны быть конечными числами')
94
+ }
95
+ if (props.min > props.max) {
96
+ throw new RangeError(`SResizeHandle: min ${props.min} не может превышать max ${props.max}`)
97
+ }
98
+ if (props.value < props.min || props.value > props.max) {
99
+ throw new RangeError(
100
+ `SResizeHandle: value ${props.value} должен находиться в диапазоне ${props.min}..${props.max}`,
101
+ )
102
+ }
103
+ if (props.step <= 0) {
104
+ throw new RangeError('SResizeHandle: step должен быть больше нуля')
105
+ }
106
+ if (props.pointerScale <= 0) {
107
+ throw new RangeError('SResizeHandle: pointerScale должен быть больше нуля')
108
+ }
109
+ }
110
+
111
+ function clampValue(value: number): number {
112
+ return Math.min(props.max, Math.max(props.min, value))
113
+ }
114
+
115
+ function pointerCoordinate(event: PointerEvent): number {
116
+ return props.orientation === 'vertical' ? event.clientX : event.clientY
117
+ }
118
+
119
+ function handlePointerMove(event: PointerEvent): void {
120
+ const state = pointerResizeState.value
121
+ if (!state || state.pointerId !== event.pointerId) return
122
+
123
+ const direction = props.valueDirection === 'forward' ? 1 : -1
124
+ const delta = (pointerCoordinate(event) - state.startCoordinate) * direction * props.pointerScale
125
+ const nextValue = clampValue(state.startValue + delta)
126
+ if (nextValue === state.latestValue) return
127
+
128
+ state.latestValue = nextValue
129
+ emit('resize', nextValue)
130
+ }
131
+
132
+ function finishPointerResize(event: PointerEvent): void {
133
+ const state = pointerResizeState.value
134
+ if (!state || state.pointerId !== event.pointerId) return
135
+ cleanupPointerResize(true)
136
+ }
137
+
138
+ function handleLostPointerCapture(event: PointerEvent): void {
139
+ const state = pointerResizeState.value
140
+ if (!state || state.pointerId !== event.pointerId) return
141
+ cleanupPointerResize(true)
142
+ }
143
+
144
+ function cleanupPointerResize(emitEnd: boolean): void {
145
+ const state = pointerResizeState.value
146
+ if (!state) return
147
+
148
+ window.removeEventListener('pointermove', handlePointerMove)
149
+ window.removeEventListener('pointerup', finishPointerResize)
150
+ window.removeEventListener('pointercancel', finishPointerResize)
151
+ state.target.removeEventListener('lostpointercapture', handleLostPointerCapture)
152
+ try {
153
+ if (state.target.hasPointerCapture?.(state.pointerId)) {
154
+ state.target.releasePointerCapture(state.pointerId)
155
+ }
156
+ } finally {
157
+ state.interactionLease.release()
158
+ pointerResizeState.value = null
159
+ }
160
+
161
+ if (emitEnd) emit('resize-end', state.latestValue)
162
+ }
163
+
164
+ function handlePointerDown(event: PointerEvent): void {
165
+ if (props.disabled || (event.pointerType !== 'touch' && event.button !== 0)) return
166
+ assertRangeContract()
167
+ cleanupPointerResize(false)
168
+
169
+ const target = event.currentTarget as HTMLButtonElement
170
+ const interactionLease = acquirePointerInteractionLease({
171
+ cursor: props.orientation === 'vertical' ? 'col-resize' : 'row-resize',
172
+ })
173
+ try {
174
+ target.focus({ preventScroll: true })
175
+ target.setPointerCapture?.(event.pointerId)
176
+ event.preventDefault()
177
+ } catch (error: unknown) {
178
+ interactionLease.release()
179
+ throw error
180
+ }
181
+
182
+ pointerResizeState.value = {
183
+ pointerId: event.pointerId,
184
+ startCoordinate: pointerCoordinate(event),
185
+ startValue: props.value,
186
+ latestValue: props.value,
187
+ target,
188
+ interactionLease,
189
+ }
190
+ window.addEventListener('pointermove', handlePointerMove)
191
+ window.addEventListener('pointerup', finishPointerResize)
192
+ window.addEventListener('pointercancel', finishPointerResize)
193
+ target.addEventListener('lostpointercapture', handleLostPointerCapture)
194
+ emit('resize-start', event)
195
+ }
196
+
197
+ function handleKeydown(event: KeyboardEvent): void {
198
+ if (props.disabled) return
199
+ assertRangeContract()
200
+ const backwardKey = props.orientation === 'vertical' ? 'ArrowLeft' : 'ArrowUp'
201
+ const forwardKey = props.orientation === 'vertical' ? 'ArrowRight' : 'ArrowDown'
202
+ const decreaseKey = props.valueDirection === 'forward' ? backwardKey : forwardKey
203
+ const increaseKey = props.valueDirection === 'forward' ? forwardKey : backwardKey
204
+ let nextValue: number | undefined
205
+
206
+ if (event.key === decreaseKey) nextValue = props.value - props.step
207
+ if (event.key === increaseKey) nextValue = props.value + props.step
208
+ if (event.key === 'Home') nextValue = props.min
209
+ if (event.key === 'End') nextValue = props.max
210
+ if (nextValue === undefined) return
211
+
212
+ event.preventDefault()
213
+ emit('resize', clampValue(nextValue))
214
+ }
215
+
216
+ onBeforeUnmount(() => {
217
+ cleanupPointerResize(false)
218
+ })
219
+
220
+ const handleClasses = computed(() => [
221
+ 's-resize-handle group flex touch-none border-0 bg-transparent p-0',
222
+ { 'max-lg:hidden': props.hiddenOnNarrow },
223
+ props.variant === 'inline'
224
+ ? 'relative flex-shrink-0'
225
+ : 'pointer-events-auto absolute z-[var(--s-layer-floating)] items-center justify-center',
226
+ props.orientation === 'vertical'
227
+ ? props.variant === 'inline'
228
+ ? 'w-1.5 self-stretch cursor-col-resize items-stretch justify-center'
229
+ : 'inset-y-3 left-2 w-8 -translate-x-1/2 cursor-col-resize'
230
+ : props.variant === 'inline'
231
+ ? 'h-1.5 w-full cursor-row-resize items-center justify-stretch'
232
+ : 'inset-x-5 top-0 h-8 -translate-y-1/2 cursor-row-resize',
233
+ ])
234
+
235
+ const lineClasses = computed(() =>
236
+ props.orientation === 'vertical'
237
+ ? props.variant === 'inline'
238
+ ? 'block h-full w-px bg-surface-200 transition-colors group-hover:bg-primary-500/45 dark:bg-surface-700 dark:group-hover:bg-primary-500/55'
239
+ : 'block h-24 w-1.5 rounded-full border border-surface-200/80 bg-surface-0/95 shadow-sm transition-colors group-hover:border-primary-300 group-hover:bg-primary-50/95 group-data-[resizing=true]:border-primary-300 group-data-[resizing=true]:bg-primary-50/95 dark:border-surface-700/80 dark:bg-surface-850/95 dark:group-hover:border-primary-500/70 dark:group-hover:bg-surface-800/95 dark:group-data-[resizing=true]:border-primary-500/70 dark:group-data-[resizing=true]:bg-surface-800/95'
240
+ : props.variant === 'inline'
241
+ ? 'block h-px w-full bg-surface-200 transition-colors group-hover:bg-primary-500/45 dark:bg-surface-700 dark:group-hover:bg-primary-500/55'
242
+ : 'block h-1.5 w-24 rounded-full border border-surface-200/80 bg-surface-0/95 shadow-sm transition-colors group-hover:border-primary-300 group-hover:bg-primary-50/95 group-data-[resizing=true]:border-primary-300 group-data-[resizing=true]:bg-primary-50/95 dark:border-surface-700/80 dark:bg-surface-850/95 dark:group-hover:border-primary-500/70 dark:group-hover:bg-surface-800/95 dark:group-data-[resizing=true]:border-primary-500/70 dark:group-data-[resizing=true]:bg-surface-800/95',
243
+ )
244
+ </script>
245
+
246
+ <style scoped>
247
+ .s-resize-handle:focus-visible {
248
+ outline: var(--s-focus-ring-width) solid var(--s-focus-ring-color);
249
+ outline-offset: var(--s-focus-ring-offset);
250
+ }
251
+ </style>