@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.
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/docs/accessibility.md +27 -0
- package/docs/getting-started.md +80 -0
- package/docs/licensing.md +30 -0
- package/docs/public-api.md +39 -0
- package/docs/security.md +21 -0
- package/docs/theming.md +35 -0
- package/docs/versioning.md +23 -0
- package/index.ts +3 -0
- package/package.json +592 -0
- package/src/components/icons/_internal/generatedSputnigUiIconMarkup.ts +971 -0
- package/src/components/icons/sputnigUiIcons.ts +530 -0
- package/src/components/layout/SAppShell.vue +232 -0
- package/src/components/layout/SDockRegion.vue +200 -0
- package/src/components/layout/SResizeHandle.vue +251 -0
- package/src/components/layout/SSideMenuButton.vue +104 -0
- package/src/components/layout/SSideMenuRail.vue +47 -0
- package/src/components/layout/SStack.css +25 -0
- package/src/components/layout/SStack.vue +57 -0
- package/src/components/layout/SStatusBarShell.css +15 -0
- package/src/components/layout/SStatusBarShell.vue +26 -0
- package/src/components/layout/SWorkbenchLayout.vue +53 -0
- package/src/components/layout/SWorkbenchPanelShell.css +161 -0
- package/src/components/layout/SWorkbenchPanelShell.vue +286 -0
- package/src/components/layout/_internal/SAppBrand.css +45 -0
- package/src/components/layout/_internal/SAppBrand.vue +37 -0
- package/src/components/layout/_internal/SAppBrandContent.vue +30 -0
- package/src/components/layout/types.ts +28 -0
- package/src/components/shared/_internal/SDialogSurface.css +127 -0
- package/src/components/shared/_internal/SDialogSurface.vue +117 -0
- package/src/components/shared/_internal/SDisclosureSummary.css +92 -0
- package/src/components/shared/_internal/SDisclosureSummary.vue +121 -0
- package/src/components/shared/_internal/SListboxGroup.css +18 -0
- package/src/components/shared/_internal/SListboxGroup.vue +24 -0
- package/src/components/shared/_internal/SMenuItem.css +112 -0
- package/src/components/shared/_internal/SMenuItem.vue +213 -0
- package/src/components/shared/_internal/SMenuLayer.css +21 -0
- package/src/components/shared/_internal/SMenuLayer.vue +48 -0
- package/src/components/shared/_internal/SMenuSeparator.css +5 -0
- package/src/components/shared/_internal/SMenuSeparator.vue +13 -0
- package/src/components/shared/_internal/SToastItem.css +65 -0
- package/src/components/shared/_internal/SToastItem.vue +58 -0
- package/src/components/shared/_internal/STreeNode.vue +761 -0
- package/src/components/shared/_internal/dialogSurface.ts +105 -0
- package/src/components/shared/_internal/focusTrap.ts +40 -0
- package/src/components/shared/_internal/listboxContext.ts +16 -0
- package/src/components/shared/_internal/markerContract.ts +10 -0
- package/src/components/shared/_internal/menuContext.ts +23 -0
- package/src/components/shared/_internal/selectionContract.ts +239 -0
- package/src/components/shared/_internal/sidebarGroupContext.ts +31 -0
- package/src/components/shared/_internal/tabsContext.ts +57 -0
- package/src/components/shared/_internal/treeRuntime.ts +203 -0
- package/src/components/shared/_internal/useSelectionRoving.ts +162 -0
- package/src/components/shared/codeLanguages.ts +168 -0
- package/src/components/shared/complex/SKanbanCard.css +8 -0
- package/src/components/shared/complex/SKanbanCard.vue +76 -0
- package/src/components/shared/complex/SKanbanLane.vue +82 -0
- package/src/components/shared/complex/STree.css +24 -0
- package/src/components/shared/complex/STree.vue +288 -0
- package/src/components/shared/complex/treeAdapter.ts +210 -0
- package/src/components/shared/complex/types.ts +102 -0
- package/src/components/shared/containers/SCloudChrome.css +103 -0
- package/src/components/shared/containers/SCloudChrome.vue +87 -0
- package/src/components/shared/containers/SCloudChromeEmpty.css +10 -0
- package/src/components/shared/containers/SCloudChromeEmpty.vue +18 -0
- package/src/components/shared/containers/SCloudChromeRow.css +78 -0
- package/src/components/shared/containers/SCloudChromeRow.vue +108 -0
- package/src/components/shared/containers/SCollapsiblePanel.css +57 -0
- package/src/components/shared/containers/SCollapsiblePanel.vue +360 -0
- package/src/components/shared/containers/SDrawer.css +60 -0
- package/src/components/shared/containers/SDrawer.vue +86 -0
- package/src/components/shared/containers/SFormGrid.css +26 -0
- package/src/components/shared/containers/SFormGrid.vue +46 -0
- package/src/components/shared/containers/SFormGridItem.css +18 -0
- package/src/components/shared/containers/SFormGridItem.vue +29 -0
- package/src/components/shared/containers/SLeftSidebar.vue +74 -0
- package/src/components/shared/containers/SModal.css +27 -0
- package/src/components/shared/containers/SModal.vue +105 -0
- package/src/components/shared/containers/SPageHeader.css +66 -0
- package/src/components/shared/containers/SPageHeader.vue +45 -0
- package/src/components/shared/containers/SPanel.css +134 -0
- package/src/components/shared/containers/SPanel.vue +169 -0
- package/src/components/shared/containers/SPopover.css +35 -0
- package/src/components/shared/containers/SPopover.vue +551 -0
- package/src/components/shared/containers/SSidebarGroup.css +12 -0
- package/src/components/shared/containers/SSidebarGroup.vue +443 -0
- package/src/components/shared/containers/SSidebarSection.css +41 -0
- package/src/components/shared/containers/SSidebarSection.vue +268 -0
- package/src/components/shared/containers/disclosureSummary.ts +48 -0
- package/src/components/shared/containers/popover.ts +72 -0
- package/src/components/shared/containers/sidebar.ts +206 -0
- package/src/components/shared/controls/SButton.css +274 -0
- package/src/components/shared/controls/SButton.vue +211 -0
- package/src/components/shared/controls/SCheckbox.css +65 -0
- package/src/components/shared/controls/SCheckbox.vue +130 -0
- package/src/components/shared/controls/SCheckboxGroup.vue +134 -0
- package/src/components/shared/controls/SChoiceCards.css +29 -0
- package/src/components/shared/controls/SChoiceCards.vue +145 -0
- package/src/components/shared/controls/SColorSelect.vue +146 -0
- package/src/components/shared/controls/SComboboxTrigger.css +35 -0
- package/src/components/shared/controls/SComboboxTrigger.vue +122 -0
- package/src/components/shared/controls/SContextMenu.css +11 -0
- package/src/components/shared/controls/SContextMenu.vue +186 -0
- package/src/components/shared/controls/SContextToggleButton.vue +64 -0
- package/src/components/shared/controls/SDragHandle.vue +100 -0
- package/src/components/shared/controls/SDropdownMenu.css +5 -0
- package/src/components/shared/controls/SDropdownMenu.vue +188 -0
- package/src/components/shared/controls/SField.css +79 -0
- package/src/components/shared/controls/SField.vue +89 -0
- package/src/components/shared/controls/SFieldGroup.css +58 -0
- package/src/components/shared/controls/SFieldGroup.vue +117 -0
- package/src/components/shared/controls/SFieldLabel.css +12 -0
- package/src/components/shared/controls/SFieldLabel.vue +51 -0
- package/src/components/shared/controls/SInlineTokenEditor.css +61 -0
- package/src/components/shared/controls/SInlineTokenEditor.vue +1081 -0
- package/src/components/shared/controls/SInputText.css +66 -0
- package/src/components/shared/controls/SInputText.vue +321 -0
- package/src/components/shared/controls/SInteractiveSurface.css +72 -0
- package/src/components/shared/controls/SInteractiveSurface.vue +326 -0
- package/src/components/shared/controls/SLink.css +114 -0
- package/src/components/shared/controls/SLink.ts +85 -0
- package/src/components/shared/controls/SLink.vue +114 -0
- package/src/components/shared/controls/SListbox.css +31 -0
- package/src/components/shared/controls/SListbox.vue +388 -0
- package/src/components/shared/controls/SListboxOption.css +63 -0
- package/src/components/shared/controls/SListboxOption.vue +85 -0
- package/src/components/shared/controls/SMarker.css +19 -0
- package/src/components/shared/controls/SMarker.vue +33 -0
- package/src/components/shared/controls/SMenuSurface.css +20 -0
- package/src/components/shared/controls/SMenuSurface.vue +333 -0
- package/src/components/shared/controls/SSegmentedControl.css +38 -0
- package/src/components/shared/controls/SSegmentedControl.vue +134 -0
- package/src/components/shared/controls/SSelect.css +12 -0
- package/src/components/shared/controls/SSelect.vue +338 -0
- package/src/components/shared/controls/STextarea.css +31 -0
- package/src/components/shared/controls/STextarea.vue +316 -0
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +224 -0
- package/src/components/shared/controls/menu.ts +178 -0
- package/src/components/shared/data-display/SActionCard.css +6 -0
- package/src/components/shared/data-display/SActionCard.vue +49 -0
- package/src/components/shared/data-display/SActionList.css +15 -0
- package/src/components/shared/data-display/SActionList.vue +34 -0
- package/src/components/shared/data-display/SActionListItem.css +114 -0
- package/src/components/shared/data-display/SActionListItem.vue +230 -0
- package/src/components/shared/data-display/SBadge.css +73 -0
- package/src/components/shared/data-display/SBadge.vue +52 -0
- package/src/components/shared/data-display/SCodeBlock.css +56 -0
- package/src/components/shared/data-display/SCodeBlock.vue +188 -0
- package/src/components/shared/data-display/SCodeEditor.css +178 -0
- package/src/components/shared/data-display/SCodeEditor.vue +274 -0
- package/src/components/shared/data-display/SCodeSearchPanel.vue +327 -0
- package/src/components/shared/data-display/SCopyField.vue +77 -0
- package/src/components/shared/data-display/SDocBlock.css +136 -0
- package/src/components/shared/data-display/SDocBlock.vue +358 -0
- package/src/components/shared/data-display/SEmptyState.css +42 -0
- package/src/components/shared/data-display/SEmptyState.vue +79 -0
- package/src/components/shared/data-display/SExpandableText.vue +314 -0
- package/src/components/shared/data-display/SJsonTree.css +6 -0
- package/src/components/shared/data-display/SJsonTree.vue +111 -0
- package/src/components/shared/data-display/SKeyValueGrid.css +37 -0
- package/src/components/shared/data-display/SKeyValueGrid.vue +277 -0
- package/src/components/shared/data-display/SLinkedSystemsList.vue +270 -0
- package/src/components/shared/data-display/SMessage.css +88 -0
- package/src/components/shared/data-display/SMessage.vue +152 -0
- package/src/components/shared/data-display/SMetricCard.css +32 -0
- package/src/components/shared/data-display/SMetricCard.vue +117 -0
- package/src/components/shared/data-display/SProgressBar.css +84 -0
- package/src/components/shared/data-display/SProgressBar.vue +114 -0
- package/src/components/shared/data-display/SProgressIndicator.css +59 -0
- package/src/components/shared/data-display/SProgressIndicator.vue +104 -0
- package/src/components/shared/data-display/SSectionHeader.css +84 -0
- package/src/components/shared/data-display/SSectionHeader.vue +58 -0
- package/src/components/shared/data-display/SStatus.css +90 -0
- package/src/components/shared/data-display/SStatus.vue +79 -0
- package/src/components/shared/data-display/STable.css +83 -0
- package/src/components/shared/data-display/STable.vue +339 -0
- package/src/components/shared/data-display/SToastContainer.css +53 -0
- package/src/components/shared/data-display/SToastContainer.vue +31 -0
- package/src/components/shared/data-display/STooltip.css +114 -0
- package/src/components/shared/data-display/STooltip.vue +550 -0
- package/src/components/shared/data-display/STooltipAnchor.css +21 -0
- package/src/components/shared/data-display/STooltipAnchor.vue +119 -0
- package/src/components/shared/data-display/SVirtualList.css +24 -0
- package/src/components/shared/data-display/SVirtualList.ts +19 -0
- package/src/components/shared/data-display/SVirtualList.vue +361 -0
- package/src/components/shared/data-display/json.ts +136 -0
- package/src/components/shared/data-display/table.ts +76 -0
- package/src/components/shared/database/SDataGrid.css +3 -0
- package/src/components/shared/database/SDataGrid.vue +464 -0
- package/src/components/shared/database/SSqlEditor.vue +363 -0
- package/src/components/shared/database/dataGrid.ts +61 -0
- package/src/components/shared/feedback/SAsyncState.css +26 -0
- package/src/components/shared/feedback/SAsyncState.vue +190 -0
- package/src/components/shared/graph/SGraphViewport.ts +1 -0
- package/src/components/shared/graph/SGraphViewport.vue +858 -0
- package/src/components/shared/graph/_internal/graphViewportContract.ts +362 -0
- package/src/components/shared/markdown.ts +169 -0
- package/src/components/shared/navigation/SBottomNav.vue +692 -0
- package/src/components/shared/navigation/SBreadcrumbs.css +26 -0
- package/src/components/shared/navigation/SBreadcrumbs.vue +155 -0
- package/src/components/shared/navigation/SCatalogNavigator.css +100 -0
- package/src/components/shared/navigation/SCatalogNavigator.vue +308 -0
- package/src/components/shared/navigation/STab.vue +363 -0
- package/src/components/shared/navigation/STabList.vue +442 -0
- package/src/components/shared/navigation/STabPanel.vue +165 -0
- package/src/components/shared/navigation/STabPanels.vue +33 -0
- package/src/components/shared/navigation/STabs.vue +617 -0
- package/src/components/shared/navigation/SWizardSteps.css +69 -0
- package/src/components/shared/navigation/SWizardSteps.vue +171 -0
- package/src/components/shared/navigation/_internal/SCatalogNavigatorGroup.vue +63 -0
- package/src/components/shared/persona/SPersonaProjectPicker.css +36 -0
- package/src/components/shared/persona/SPersonaProjectPicker.vue +361 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.css +109 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.vue +549 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.css +40 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +298 -0
- package/src/components/shared/persona/_internal/inventory.ts +110 -0
- package/src/components/shared/persona/_internal/labels.ts +79 -0
- package/src/components/shared/persona/presentation.ts +371 -0
- package/src/components/shared/persona/types.ts +256 -0
- package/src/composables/uiPreferencesReset.ts +52 -0
- package/src/composables/useClipboard.ts +37 -0
- package/src/composables/useDialogOverlay.ts +85 -0
- package/src/composables/useDisclosure.ts +40 -0
- package/src/composables/useFloatingPosition.ts +245 -0
- package/src/composables/useNotifier.ts +26 -0
- package/src/composables/useSidebarPanelState.ts +610 -0
- package/src/internal/codeEditorContract.ts +110 -0
- package/src/internal/codeEditorExtensions.ts +27 -0
- package/src/internal/feedbackPresentation.ts +136 -0
- package/src/internal/fieldSurface.ts +59 -0
- package/src/internal/focusNavigation.ts +69 -0
- package/src/internal/inlineTokenEditorContract.ts +136 -0
- package/src/internal/interactiveElement.ts +33 -0
- package/src/internal/layerStack.ts +202 -0
- package/src/internal/linkTarget.ts +256 -0
- package/src/internal/ownedAttrs.ts +464 -0
- package/src/internal/passiveContentContract.ts +129 -0
- package/src/internal/pointerInteractionLease.ts +81 -0
- package/src/internal/runtimeContract.ts +194 -0
- package/src/internal/runtimeTheme.ts +159 -0
- package/src/internal/semanticSizing.ts +60 -0
- package/src/internal/useMenuLayer.ts +129 -0
- package/src/stores/useNotifierStore.ts +69 -0
- package/src/styles/reference.css +4 -0
- package/src/styles/style.css +151 -0
- package/src/styles/tokens.css +836 -0
- package/src/theme.ts +449 -0
- package/src/types/highlight-js-lib.d.ts +76 -0
- 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>
|