@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,171 @@
1
+ <template>
2
+ <nav
3
+ v-bind="ownedAttrs.bindings()"
4
+ class="s-wizard-steps"
5
+ :aria-label="validatedAriaLabel"
6
+ :data-columns="validatedColumns"
7
+ :data-density="validatedDensity"
8
+ >
9
+ <ol class="s-wizard-steps__list">
10
+ <li
11
+ v-for="record in stepRecords"
12
+ :key="record.token"
13
+ class="s-wizard-steps__item"
14
+ :data-state="record.state"
15
+ >
16
+ <SButton
17
+ :severity="record.state === 'completed' ? 'success' : record.state === 'current' ? 'primary' : 'secondary'"
18
+ :appearance="record.state === 'current' ? 'soft' : 'ghost'"
19
+ :size="validatedDensity === 'compact' ? 'sm' : 'md'"
20
+ shape="row"
21
+ width="full"
22
+ content-align="start"
23
+ :disabled="record.step.disabled"
24
+ :aria-current="record.state === 'current' ? 'step' : undefined"
25
+ :data-state="record.state"
26
+ @click="selectStep(record.step)"
27
+ >
28
+ <span class="s-wizard-steps__marker" aria-hidden="true">{{ record.index + 1 }}</span>
29
+ <span class="s-wizard-steps__title">{{ record.step.title }}</span>
30
+ </SButton>
31
+ </li>
32
+ </ol>
33
+ </nav>
34
+ </template>
35
+
36
+ <script lang="ts">
37
+ import type { SelectionKey as PublicSelectionKey } from '../_internal/useSelectionRoving'
38
+
39
+ /** Один типизированный шаг мастера. / One typed wizard step. */
40
+ export interface WizardStepOption<T extends PublicSelectionKey = string | number> {
41
+ /** Стабильная identity шага. / Stable step identity. */
42
+ id: T
43
+ /** Пользовательская метка шага. / User-facing step label. */
44
+ title: string
45
+ /** Запрещает активацию шага и исключает его из последовательности Tab. / Prevents step activation and removes it from the Tab sequence. */
46
+ disabled?: boolean
47
+ }
48
+
49
+ /** Публичный controlled contract ordered step navigation. / Public controlled contract for ordered step navigation. */
50
+ export interface Props<T extends PublicSelectionKey = string | number> {
51
+ /** Текущий шаг. / Current step. */
52
+ modelValue: T
53
+ /** Полный ordered набор шагов. / Complete ordered step collection. */
54
+ steps: readonly WizardStepOption<T>[]
55
+ /** Ширина колонок; auto сохраняет одну scrollable строку. / Column-width policy; auto keeps one scrollable row. */
56
+ columns?: 'auto' | '2' | '3' | '4'
57
+ /** Плотность hitbox canonical action owner. / Canonical action-owner hitbox density. */
58
+ density?: 'default' | 'compact'
59
+ /** Accessible name step navigation landmark. / Accessible name of the step-navigation landmark. */
60
+ ariaLabel?: string
61
+ }
62
+ </script>
63
+
64
+ <script setup lang="ts" generic="T extends SelectionKey = string | number">
65
+ import { computed } from 'vue'
66
+ import { validateBoolean, validateExactString, validateNonEmptyString } from '../../../internal/runtimeContract'
67
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
68
+ import SButton from '../controls/SButton.vue'
69
+ import {
70
+ selectionKeyToken,
71
+ validateSelectionIdentities,
72
+ } from '../_internal/useSelectionRoving'
73
+ import type { SelectionKey } from '../_internal/useSelectionRoving'
74
+
75
+ defineOptions({ inheritAttrs: false })
76
+
77
+ type WizardStepState = 'current' | 'completed' | 'upcoming'
78
+
79
+ interface WizardStepRecord<TStep extends SelectionKey> {
80
+ readonly index: number
81
+ readonly state: WizardStepState
82
+ readonly step: WizardStepOption<TStep>
83
+ readonly token: string
84
+ }
85
+
86
+ const props = withDefaults(defineProps<Props<T>>(), {
87
+ columns: 'auto',
88
+ density: 'default',
89
+ ariaLabel: 'Шаги мастера',
90
+ })
91
+
92
+ const emit = defineEmits<{
93
+ 'update:modelValue': [value: T]
94
+ }>()
95
+ const ownedAttrs = useOwnedAttrs({ component: 'SWizardSteps', owner: 'ordered wizard navigation' })
96
+ const STEP_KEYS = new Set(['id', 'title', 'disabled'])
97
+
98
+ function assertStepRecord(value: unknown, coordinate: string): asserts value is WizardStepOption<T> {
99
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
100
+ throw new TypeError(`SWizardSteps: ${coordinate} must be a step object`)
101
+ }
102
+ for (const key of Reflect.ownKeys(value)) {
103
+ if (typeof key !== 'string' || !STEP_KEYS.has(key)) {
104
+ throw new TypeError(`SWizardSteps: ${coordinate} has unsupported field '${String(key)}'`)
105
+ }
106
+ const descriptor = Object.getOwnPropertyDescriptor(value, key)
107
+ if (!descriptor?.enumerable || !('value' in descriptor)) {
108
+ throw new TypeError(`SWizardSteps: ${coordinate}.${key} must be an enumerable data property`)
109
+ }
110
+ }
111
+ }
112
+
113
+ const validatedColumns = computed(() => validateExactString(
114
+ 'SWizardSteps',
115
+ 'columns',
116
+ props.columns,
117
+ ['auto', '2', '3', '4'] as const,
118
+ ))
119
+ const validatedDensity = computed(() => validateExactString(
120
+ 'SWizardSteps',
121
+ 'density',
122
+ props.density,
123
+ ['default', 'compact'] as const,
124
+ ))
125
+ const validatedAriaLabel = computed(() => validateNonEmptyString(
126
+ 'SWizardSteps',
127
+ 'ariaLabel',
128
+ props.ariaLabel,
129
+ ))
130
+
131
+ const stepRecords = computed<readonly WizardStepRecord<T>[]>(() => {
132
+ void validatedColumns.value
133
+ void validatedDensity.value
134
+ void validatedAriaLabel.value
135
+ if (!Array.isArray(props.steps) || props.steps.length === 0) {
136
+ throw new TypeError('SWizardSteps: steps must be a non-empty array')
137
+ }
138
+
139
+ const tokens: string[] = []
140
+ for (const [index, step] of props.steps.entries()) {
141
+ const coordinate = `steps[${index}]`
142
+ assertStepRecord(step, coordinate)
143
+ validateNonEmptyString('SWizardSteps', `${coordinate}.title`, step.title)
144
+ if (step.disabled !== undefined) {
145
+ validateBoolean('SWizardSteps', `${coordinate}.disabled`, step.disabled)
146
+ }
147
+ tokens.push(selectionKeyToken('SWizardSteps', step.id, `${coordinate}.id`))
148
+ }
149
+ validateSelectionIdentities('SWizardSteps', props.steps.map((step, index) => ({
150
+ key: step.id,
151
+ coordinate: `steps[${index}].id`,
152
+ })))
153
+
154
+ const modelToken = selectionKeyToken('SWizardSteps', props.modelValue, 'modelValue')
155
+ const currentIndex = tokens.indexOf(modelToken)
156
+ if (currentIndex < 0) throw new Error('SWizardSteps: modelValue does not match any step')
157
+
158
+ return props.steps.map((step, index) => ({
159
+ index,
160
+ state: index === currentIndex ? 'current' : index < currentIndex ? 'completed' : 'upcoming',
161
+ step,
162
+ token: tokens[index]!,
163
+ }))
164
+ })
165
+
166
+ function selectStep(step: WizardStepOption<T>): void {
167
+ if (!step.disabled) emit('update:modelValue', step.id)
168
+ }
169
+ </script>
170
+
171
+ <style lang="postcss" src="./SWizardSteps.css" scoped></style>
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <section class="s-catalog-navigator__group">
3
+ <header class="s-catalog-navigator__group-header">
4
+ <SButton
5
+ :id="triggerId"
6
+ :label="`${label} (${count})`"
7
+ size="sm"
8
+ severity="secondary"
9
+ appearance="ghost"
10
+ shape="row"
11
+ width="full"
12
+ content-align="start"
13
+ data-testid="catalog-group-toggle"
14
+ :disabled="disabled"
15
+ :disclosure="{ expanded: isOpen, controls: panelId, iconPlacement: 'leading' }"
16
+ @click="toggle"
17
+ />
18
+ </header>
19
+ <div
20
+ v-if="isOpen"
21
+ :id="panelId"
22
+ role="list"
23
+ :aria-labelledby="triggerId"
24
+ >
25
+ <slot />
26
+ </div>
27
+ </section>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import SButton from '../../controls/SButton.vue'
32
+ import { useDisclosure } from '../../../../composables/useDisclosure'
33
+
34
+ const props = withDefaults(defineProps<{
35
+ id: string
36
+ label: string
37
+ count: number
38
+ defaultOpen?: boolean
39
+ disabled?: boolean
40
+ }>(), { defaultOpen: true, disabled: false })
41
+
42
+ const { isOpen, triggerId, panelId, toggle } = useDisclosure({
43
+ id: props.id,
44
+ defaultOpen: props.defaultOpen,
45
+ disabled: () => props.disabled,
46
+ })
47
+ </script>
48
+
49
+ <style lang="postcss" scoped>
50
+ @reference "../../../../styles/reference.css";
51
+
52
+ .s-catalog-navigator__group-header {
53
+ position: sticky;
54
+ z-index: var(--s-layer-content);
55
+ top: 0;
56
+ padding: 0;
57
+ background: var(--color-surface);
58
+ color: var(--color-text-muted);
59
+ font-size: 0.75rem;
60
+ font-weight: 700;
61
+ }
62
+
63
+ </style>
@@ -0,0 +1,36 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-persona-project-picker {
4
+ @apply grid min-h-[32rem] min-w-0 gap-4 lg:grid-cols-[minmax(14rem,0.32fr)_minmax(0,1fr)];
5
+ }
6
+
7
+ .s-persona-project-picker__connected {
8
+ @apply min-w-0 self-start;
9
+ }
10
+
11
+ .s-persona-project-picker__browser {
12
+ @apply grid min-h-0 min-w-0 content-start gap-3;
13
+ }
14
+
15
+ .s-persona-project-picker__toolbar {
16
+ @apply grid min-w-0 gap-2 sm:grid-cols-[minmax(12rem,0.42fr)_minmax(14rem,1fr)_auto];
17
+ }
18
+
19
+ .s-persona-project-picker__content {
20
+ @apply grid min-h-0 gap-3;
21
+ }
22
+
23
+ @media (max-width: 639px) {
24
+ .s-persona-project-picker {
25
+ @apply min-h-0;
26
+ }
27
+
28
+ .s-persona-project-picker__toolbar {
29
+ @apply grid-cols-[minmax(0,1fr)_auto];
30
+ }
31
+
32
+ .s-persona-project-picker__root-selector {
33
+ @apply col-span-2;
34
+ }
35
+
36
+ }
@@ -0,0 +1,361 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, ref, watch } from 'vue'
3
+
4
+ import { Check, ChevronRight, Folder, FolderSearch, FolderTree, RefreshCw } from '../../icons/sputnigUiIcons'
5
+ import SModal from '../containers/SModal.vue'
6
+ import SPanel from '../containers/SPanel.vue'
7
+ import SButton from '../controls/SButton.vue'
8
+ import SInputText from '../controls/SInputText.vue'
9
+ import SSelect from '../controls/SSelect.vue'
10
+ import SBreadcrumbs from '../navigation/SBreadcrumbs.vue'
11
+ import SActionList from '../data-display/SActionList.vue'
12
+ import SActionListItem from '../data-display/SActionListItem.vue'
13
+ import SBadge from '../data-display/SBadge.vue'
14
+ import SEmptyState from '../data-display/SEmptyState.vue'
15
+ import SAsyncState from '../feedback/SAsyncState.vue'
16
+ import type {
17
+ PersonaProjectPickerConnected,
18
+ PersonaProjectPickerEntry,
19
+ PersonaProjectPickerRoot,
20
+ PersonaProjectPickerState,
21
+ } from './types'
22
+ import {
23
+ assertPersonaInventory,
24
+ findExactPersonaInventoryItem,
25
+ resolvePersonaProjectPickerAsyncState,
26
+ } from './_internal/inventory'
27
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
28
+
29
+ defineOptions({ inheritAttrs: false })
30
+
31
+ const props = withDefaults(
32
+ defineProps<{
33
+ open: boolean
34
+ roots: PersonaProjectPickerRoot[]
35
+ state: PersonaProjectPickerState
36
+ selectedRootId?: string | null
37
+ connectedProjects?: PersonaProjectPickerConnected[]
38
+ }>(),
39
+ {
40
+ selectedRootId: null,
41
+ connectedProjects: () => [],
42
+ },
43
+ )
44
+
45
+ const emit = defineEmits<{
46
+ 'update:open': [open: boolean]
47
+ 'select-root': [rootId: string]
48
+ navigate: [relativePath: string]
49
+ refresh: []
50
+ 'load-more': []
51
+ confirm: [project: PersonaProjectPickerEntry]
52
+ 'activate-connected': [bindingId: string]
53
+ }>()
54
+ const ownedAttrs = useOwnedAttrs({ component: 'SPersonaProjectPicker', owner: 'project-picker modal surface' })
55
+
56
+ const filter = ref('')
57
+ const selectedProjectId = ref<string | null>(null)
58
+ const searchInput = ref<InstanceType<typeof SInputText> | null>(null)
59
+ const rootBreadcrumbId = 'persona-project-picker:root'
60
+
61
+ const validatedRoots = computed(() => assertPersonaInventory('SPersonaProjectPicker.roots', props.roots))
62
+ const validatedConnectedProjects = computed(() => {
63
+ const projects = assertPersonaInventory(
64
+ 'SPersonaProjectPicker.connectedProjects',
65
+ props.connectedProjects,
66
+ )
67
+ const projectIds = new Set<string>()
68
+ for (const [index, project] of projects.entries()) {
69
+ if (typeof project.projectId !== 'string' || project.projectId.length === 0) {
70
+ throw new TypeError(
71
+ `SPersonaProjectPicker.connectedProjects[${index}].projectId должен быть непустой строкой. `
72
+ + `/ SPersonaProjectPicker.connectedProjects[${index}].projectId must be a non-empty string.`,
73
+ )
74
+ }
75
+ if (projectIds.has(project.projectId)) {
76
+ throw new TypeError(
77
+ `SPersonaProjectPicker.connectedProjects содержит duplicate projectId ${JSON.stringify(project.projectId)}. `
78
+ + `/ SPersonaProjectPicker.connectedProjects contains duplicate projectId ${JSON.stringify(project.projectId)}.`,
79
+ )
80
+ }
81
+ projectIds.add(project.projectId)
82
+ }
83
+ return projects
84
+ })
85
+ const rootOptions = computed(() => validatedRoots.value.map((root) => ({ label: root.label, value: root.id, title: root.path })))
86
+ const resolvedSelectedRootId = computed(() => {
87
+ const selectedRoot = findExactPersonaInventoryItem(
88
+ 'SPersonaProjectPicker.roots',
89
+ validatedRoots.value,
90
+ props.selectedRootId,
91
+ )
92
+ return selectedRoot === null ? null : selectedRoot.id
93
+ })
94
+ const listing = computed(() => {
95
+ if (props.state.status !== 'ready' || props.state.listing === null) return null
96
+ const candidate = props.state.listing
97
+ if (!validatedRoots.value.some((root) => root.id === candidate.root.id)) {
98
+ throw new TypeError(
99
+ `SPersonaProjectPicker.state: listing.root.id ${JSON.stringify(candidate.root.id)} отсутствует в roots inventory. `
100
+ + `/ SPersonaProjectPicker.state: listing.root.id ${JSON.stringify(candidate.root.id)} is absent from the roots inventory.`,
101
+ )
102
+ }
103
+ if (candidate.root.id !== resolvedSelectedRootId.value) {
104
+ throw new TypeError(
105
+ `SPersonaProjectPicker.state: listing.root.id ${JSON.stringify(candidate.root.id)} не совпадает с selectedRootId ${JSON.stringify(resolvedSelectedRootId.value)}. `
106
+ + `/ SPersonaProjectPicker.state: listing.root.id ${JSON.stringify(candidate.root.id)} does not match selectedRootId ${JSON.stringify(resolvedSelectedRootId.value)}.`,
107
+ )
108
+ }
109
+ const entries = assertPersonaInventory(
110
+ 'SPersonaProjectPicker.state.listing',
111
+ [candidate.current, ...candidate.directories],
112
+ )
113
+ const invalidRootEntry = entries.find((entry) => entry.rootId !== candidate.root.id)
114
+ if (invalidRootEntry) {
115
+ throw new TypeError(
116
+ `SPersonaProjectPicker.state: entry ${JSON.stringify(invalidRootEntry.id)} ссылается на rootId ${JSON.stringify(invalidRootEntry.rootId)} вместо ${JSON.stringify(candidate.root.id)}. `
117
+ + `/ SPersonaProjectPicker.state: entry ${JSON.stringify(invalidRootEntry.id)} references rootId ${JSON.stringify(invalidRootEntry.rootId)} instead of ${JSON.stringify(candidate.root.id)}.`,
118
+ )
119
+ }
120
+ return candidate
121
+ })
122
+ const pickerAsyncState = computed(() => resolvePersonaProjectPickerAsyncState(
123
+ props.state,
124
+ {
125
+ loadingMessage: 'Загрузка каталогов',
126
+ emptyMessage: 'Выберите область поиска',
127
+ },
128
+ ))
129
+ const projectInventory = computed(() => {
130
+ const currentListing = listing.value
131
+ return currentListing ? [currentListing.current, ...currentListing.directories] : []
132
+ })
133
+ const selectedProject = computed(() => {
134
+ const project = findExactPersonaInventoryItem(
135
+ 'SPersonaProjectPicker.state.listing',
136
+ projectInventory.value,
137
+ selectedProjectId.value,
138
+ )
139
+ return project?.isSelectable ? project : null
140
+ })
141
+ const visibleDirectories = computed(() => {
142
+ const query = filter.value.trim().toLocaleLowerCase('ru-RU')
143
+ if (!query) return listing.value?.directories ?? []
144
+ return (listing.value?.directories ?? []).filter((directory) =>
145
+ `${directory.name} ${directory.displayPath}`.toLocaleLowerCase('ru-RU').includes(query),
146
+ )
147
+ })
148
+ const breadcrumbs = computed(() => {
149
+ const currentListing = listing.value
150
+ if (!currentListing) return []
151
+ const parts = currentListing.current.relativePath.split('/').filter(Boolean)
152
+ const result = [{
153
+ id: rootBreadcrumbId,
154
+ label: currentListing.root.label,
155
+ kind: parts.length === 0 ? 'current' as const : 'action' as const,
156
+ }]
157
+ let relativePath = ''
158
+ for (const [index, part] of parts.entries()) {
159
+ relativePath = relativePath ? `${relativePath}/${part}` : part
160
+ result.push({
161
+ label: part,
162
+ id: relativePath,
163
+ kind: index === parts.length - 1 ? 'current' : 'action',
164
+ })
165
+ }
166
+ return result
167
+ })
168
+ const emptyLabel = computed(() => filter.value.trim() ? 'Совпадений не найдено' : 'В каталоге нет вложенных директорий')
169
+
170
+ watch(
171
+ () => props.open,
172
+ async (open) => {
173
+ if (!open) return
174
+ filter.value = ''
175
+ selectedProjectId.value = null
176
+ await nextTick()
177
+ searchInput.value?.focus()
178
+ },
179
+ )
180
+
181
+ watch(
182
+ () => listing.value?.root.id,
183
+ () => {
184
+ selectedProjectId.value = null
185
+ filter.value = ''
186
+ },
187
+ )
188
+
189
+ function selectRoot(value: unknown): void {
190
+ if (typeof value === 'string' && value) emit('select-root', value)
191
+ }
192
+
193
+ function navigateBreadcrumb(id: string): void {
194
+ emit('navigate', id === rootBreadcrumbId ? '' : id)
195
+ }
196
+
197
+ function selectProject(project: PersonaProjectPickerEntry): void {
198
+ if (project.isSelectable) selectedProjectId.value = project.id
199
+ }
200
+
201
+ function connectedProject(project: PersonaProjectPickerEntry): PersonaProjectPickerConnected | undefined {
202
+ return validatedConnectedProjects.value.find((connected) => connected.projectId === project.id)
203
+ }
204
+
205
+ function projectActionLabel(project: PersonaProjectPickerEntry): string | undefined {
206
+ if (!project.isSelectable) return undefined
207
+ const connected = connectedProject(project)
208
+ if (connected) return connected.active ? 'Активен' : 'Открыть'
209
+ return selectedProject.value?.id === project.id ? 'Выбран' : 'Выбрать'
210
+ }
211
+
212
+ function projectAction(project: PersonaProjectPickerEntry): void {
213
+ const connected = connectedProject(project)
214
+ if (connected) {
215
+ if (!connected.active) emit('activate-connected', connected.id)
216
+ return
217
+ }
218
+ selectProject(project)
219
+ }
220
+
221
+ function confirmSelection(): void {
222
+ if (selectedProject.value) emit('confirm', selectedProject.value)
223
+ }
224
+ </script>
225
+
226
+ <template>
227
+ <SModal
228
+ v-bind="ownedAttrs.bindings()"
229
+ :open="open"
230
+ title="Выбор Persona-проекта"
231
+ size="wide"
232
+ @update:open="emit('update:open', $event)"
233
+ >
234
+ <div class="s-persona-project-picker">
235
+ <div v-if="validatedConnectedProjects.length > 0" class="s-persona-project-picker__connected">
236
+ <SPanel title="Подключённые" :icon="FolderTree">
237
+ <SActionList accessible-label="Подключённые Persona-проекты" max-height="md">
238
+ <SActionListItem
239
+ v-for="project in validatedConnectedProjects"
240
+ :key="project.id"
241
+ :title="project.title"
242
+ :description="project.path"
243
+ :icon="FolderTree"
244
+ :selected="project.active"
245
+ :status-label="project.active ? 'Активен' : undefined"
246
+ status-severity="success"
247
+ :surface="{ mode: 'action', accessibleLabel: `Открыть проект ${project.title}` }"
248
+ @activate="emit('activate-connected', project.id)"
249
+ />
250
+ </SActionList>
251
+ </SPanel>
252
+ </div>
253
+
254
+ <section class="s-persona-project-picker__browser">
255
+ <div class="s-persona-project-picker__toolbar">
256
+ <div class="s-persona-project-picker__root-selector">
257
+ <SSelect
258
+ :model-value="resolvedSelectedRootId"
259
+ :options="rootOptions"
260
+ placeholder="Выберите область"
261
+ aria-label="Область поиска проектов"
262
+ @update:model-value="selectRoot"
263
+ />
264
+ </div>
265
+ <SInputText
266
+ ref="searchInput"
267
+ v-model="filter"
268
+ size="sm"
269
+ placeholder="Найти в текущем каталоге"
270
+ aria-label="Поиск каталога"
271
+ />
272
+ <SButton
273
+ label=""
274
+ size="sm"
275
+ severity="secondary"
276
+ :leading-icon="RefreshCw"
277
+ icon-only
278
+ title="Обновить каталог"
279
+ aria-label="Обновить каталог"
280
+ @click="emit('refresh')"
281
+ />
282
+ </div>
283
+
284
+ <SBreadcrumbs
285
+ v-if="listing"
286
+ :items="breadcrumbs"
287
+ aria-label="Путь к каталогу"
288
+ @action="navigateBreadcrumb($event)"
289
+ />
290
+
291
+ <SAsyncState :state="pickerAsyncState">
292
+ <div v-if="listing" class="s-persona-project-picker__content">
293
+ <SActionList accessible-label="Директории и Persona-проекты" max-height="lg">
294
+ <SActionListItem
295
+ v-if="listing.current.isSelectable"
296
+ :title="listing.current.projectName || listing.current.name"
297
+ :description="listing.current.displayPath"
298
+ :icon="FolderTree"
299
+ tone="success"
300
+ :selected="selectedProject?.id === listing.current.id"
301
+ :action-label="projectActionLabel(listing.current)"
302
+ :action-icon="connectedProject(listing.current)?.active ? Check : undefined"
303
+ :action-disabled="connectedProject(listing.current)?.active"
304
+ action-severity="success"
305
+ status-label="Persona-проект"
306
+ status-severity="success"
307
+ :surface="{ mode: 'static' }"
308
+ @action="projectAction(listing.current)"
309
+ />
310
+ <SActionListItem
311
+ v-for="directory in visibleDirectories"
312
+ :key="directory.id"
313
+ :title="directory.projectName || directory.name"
314
+ :description="directory.diagnosticMessage || directory.displayPath"
315
+ :icon="directory.isSelectable ? FolderTree : Folder"
316
+ :selected="selectedProject?.id === directory.id"
317
+ :tone="directory.readiness === 'invalid' || directory.readiness === 'unreadable' ? 'danger' : directory.isSelectable ? 'success' : 'default'"
318
+ :action-label="projectActionLabel(directory)"
319
+ :action-icon="connectedProject(directory)?.active || selectedProject?.id === directory.id ? Check : undefined"
320
+ :action-disabled="connectedProject(directory)?.active"
321
+ :action-severity="connectedProject(directory)?.active || selectedProject?.id === directory.id ? 'success' : 'secondary'"
322
+ :status-label="directory.readiness === 'unreadable' ? 'Нет доступа' : directory.readiness === 'invalid' ? 'Ошибка проекта' : undefined"
323
+ status-severity="danger"
324
+ :trailing-icon="directory.isSelectable || directory.readiness === 'unreadable' || directory.readiness === 'invalid' ? undefined : ChevronRight"
325
+ :surface="{ mode: 'action', accessibleLabel: `Открыть ${directory.name}` }"
326
+ @activate="emit('navigate', directory.relativePath)"
327
+ @action="projectAction(directory)"
328
+ />
329
+ </SActionList>
330
+
331
+ <SEmptyState
332
+ v-if="visibleDirectories.length === 0"
333
+ :title="emptyLabel"
334
+ :description="filter.trim() ? 'Измените запрос или перейдите в другой каталог.' : 'Вернитесь на уровень выше или выберите другую область.'"
335
+ :icon="FolderSearch"
336
+ size="sm"
337
+ />
338
+ <SButton
339
+ v-if="state.status === 'ready' && state.pagination !== 'complete'"
340
+ severity="secondary"
341
+ :loading="state.pagination === 'loading-more'"
342
+ @click="emit('load-more')"
343
+ >
344
+ Загрузить ещё
345
+ </SButton>
346
+ </div>
347
+ </SAsyncState>
348
+ </section>
349
+ </div>
350
+
351
+ <template #footer-start>
352
+ <SBadge v-if="selectedProject" severity="info">{{ selectedProject.displayPath }}</SBadge>
353
+ </template>
354
+ <template #footer-end>
355
+ <SButton severity="secondary" @click="emit('update:open', false)">Отмена</SButton>
356
+ <SButton :disabled="!selectedProject" @click="confirmSelection">Подключить проект</SButton>
357
+ </template>
358
+ </SModal>
359
+ </template>
360
+
361
+ <style lang="postcss" src="./SPersonaProjectPicker.css" scoped></style>