@pgcorp/ui-kit 0.8.1 → 0.10.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 (113) hide show
  1. package/README.md +332 -9
  2. package/docs/getting-started.md +135 -0
  3. package/docs/public-api.md +53 -4
  4. package/docs/rich-document.md +216 -0
  5. package/docs/scroll-viewport.md +167 -0
  6. package/docs/theming.md +17 -0
  7. package/package.json +108 -50
  8. package/src/components/layout/SAppShell.vue +3 -1
  9. package/src/components/layout/SDockRegion.vue +5 -3
  10. package/src/components/layout/SResizeHandle.vue +45 -5
  11. package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
  12. package/src/components/shared/_internal/SDialogSurface.css +23 -0
  13. package/src/components/shared/_internal/SDialogSurface.vue +9 -2
  14. package/src/components/shared/_internal/SToastItem.css +4 -0
  15. package/src/components/shared/_internal/SToastItem.vue +44 -8
  16. package/src/components/shared/_internal/STreeNode.vue +24 -11
  17. package/src/components/shared/_internal/dialogSurface.ts +7 -1
  18. package/src/components/shared/complex/SKanbanLane.vue +4 -2
  19. package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
  20. package/src/components/shared/containers/SDrawer.css +11 -2
  21. package/src/components/shared/containers/SDrawer.vue +48 -9
  22. package/src/components/shared/containers/SModal.vue +5 -2
  23. package/src/components/shared/containers/SPanel.vue +4 -2
  24. package/src/components/shared/containers/SPopover.vue +7 -3
  25. package/src/components/shared/containers/SUiKitLocaleProvider.vue +57 -0
  26. package/src/components/shared/controls/SButton.css +14 -10
  27. package/src/components/shared/controls/SCombobox.vue +8 -6
  28. package/src/components/shared/controls/SComboboxTrigger.css +7 -4
  29. package/src/components/shared/controls/SComboboxTrigger.vue +5 -2
  30. package/src/components/shared/controls/SContextToggleButton.vue +6 -4
  31. package/src/components/shared/controls/SDragHandle.vue +8 -4
  32. package/src/components/shared/controls/SDropdownMenu.vue +7 -4
  33. package/src/components/shared/controls/SInputText.css +5 -4
  34. package/src/components/shared/controls/SListbox.vue +4 -2
  35. package/src/components/shared/controls/SSelect.vue +4 -2
  36. package/src/components/shared/controls/STextarea.ts +13 -0
  37. package/src/components/shared/controls/STextarea.vue +391 -37
  38. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +4 -2
  39. package/src/components/shared/data-display/SChart.vue +7 -5
  40. package/src/components/shared/data-display/SChip.vue +3 -1
  41. package/src/components/shared/data-display/SCodeBlock.css +4 -0
  42. package/src/components/shared/data-display/SCodeBlock.vue +46 -22
  43. package/src/components/shared/data-display/SCodeEditor.css +17 -0
  44. package/src/components/shared/data-display/SCodeEditor.vue +173 -8
  45. package/src/components/shared/data-display/SCodeSearchPanel.vue +48 -33
  46. package/src/components/shared/data-display/SCodeWorkspaceEditor.css +13 -0
  47. package/src/components/shared/data-display/SCodeWorkspaceEditor.vue +355 -0
  48. package/src/components/shared/data-display/SControlledToastViewport.css +89 -0
  49. package/src/components/shared/data-display/SControlledToastViewport.vue +205 -0
  50. package/src/components/shared/data-display/SCopyField.vue +12 -10
  51. package/src/components/shared/data-display/SDiffViewer.css +122 -0
  52. package/src/components/shared/data-display/SDiffViewer.vue +499 -0
  53. package/src/components/shared/data-display/SDocBlock.vue +99 -71
  54. package/src/components/shared/data-display/SExpandableText.vue +10 -8
  55. package/src/components/shared/data-display/SJsonTree.vue +4 -2
  56. package/src/components/shared/data-display/SLinkedSystemsList.vue +4 -2
  57. package/src/components/shared/data-display/SMessage.vue +6 -6
  58. package/src/components/shared/data-display/SRichDocument.css +8 -0
  59. package/src/components/shared/data-display/SRichDocument.vue +65 -0
  60. package/src/components/shared/data-display/SScrollViewport.css +53 -0
  61. package/src/components/shared/data-display/SScrollViewport.vue +470 -0
  62. package/src/components/shared/data-display/STable.vue +38 -19
  63. package/src/components/shared/data-display/SToastContainer.vue +22 -17
  64. package/src/components/shared/data-display/SVirtualList.ts +33 -0
  65. package/src/components/shared/data-display/SVirtualList.vue +321 -36
  66. package/src/components/shared/data-display/_internal/SCodeTokenLine.css +72 -0
  67. package/src/components/shared/data-display/_internal/SCodeTokenLine.vue +79 -0
  68. package/src/components/shared/data-display/_internal/SRichBlockNode.css +85 -0
  69. package/src/components/shared/data-display/_internal/SRichBlockNode.vue +282 -0
  70. package/src/components/shared/data-display/_internal/SRichInlineNode.css +6 -0
  71. package/src/components/shared/data-display/_internal/SRichInlineNode.vue +76 -0
  72. package/src/components/shared/data-display/_internal/SRichMath.css +42 -0
  73. package/src/components/shared/data-display/_internal/SRichMath.vue +102 -0
  74. package/src/components/shared/data-display/codeEditor.ts +200 -0
  75. package/src/components/shared/data-display/codeWorkspace.ts +233 -0
  76. package/src/components/shared/data-display/diff.ts +541 -0
  77. package/src/components/shared/data-display/richDocument.ts +876 -0
  78. package/src/components/shared/data-display/scrollViewport.ts +47 -0
  79. package/src/components/shared/data-display/toast.ts +194 -0
  80. package/src/components/shared/database/SDataGrid.vue +7 -4
  81. package/src/components/shared/database/SSqlEditor.vue +39 -39
  82. package/src/components/shared/feedback/SAsyncState.vue +3 -1
  83. package/src/components/shared/graph/SGraphViewport.vue +11 -9
  84. package/src/components/shared/navigation/SBreadcrumbs.vue +8 -5
  85. package/src/components/shared/navigation/SCatalogNavigator.vue +26 -20
  86. package/src/components/shared/navigation/STabList.vue +10 -6
  87. package/src/components/shared/navigation/STabs.vue +15 -11
  88. package/src/components/shared/navigation/SWizardSteps.vue +4 -2
  89. package/src/components/shared/persona/SPersonaProjectPicker.vue +31 -27
  90. package/src/components/shared/persona/SPersonaRunWorkbench.vue +39 -33
  91. package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +8 -6
  92. package/src/components/shared/persona/_internal/labels.ts +66 -57
  93. package/src/components/shared/persona/presentation.ts +140 -110
  94. package/src/composables/useFloatingPosition.ts +39 -2
  95. package/src/composables/useUiKitLocale.ts +17 -0
  96. package/src/internal/codeHighlight.ts +110 -0
  97. package/src/internal/diffEngine.ts +464 -0
  98. package/src/internal/diffRuntime.ts +189 -0
  99. package/src/internal/feedbackPresentation.ts +33 -14
  100. package/src/internal/localeContext.ts +22 -0
  101. package/src/internal/ownedAttrs.ts +8 -0
  102. package/src/internal/runtimeContract.ts +11 -9
  103. package/src/internal/semanticSizing.ts +13 -0
  104. package/src/locales/catalog.ts +634 -0
  105. package/src/locales/locale.ts +472 -0
  106. package/src/locales/messages.generated.ts +1826 -0
  107. package/src/locales/messages.ts +150 -0
  108. package/src/stores/useNotifierStore.ts +61 -44
  109. package/src/styles/tokens.css +23 -2
  110. package/src/theme.ts +59 -32
  111. package/src/workers/diff.worker.ts +73 -0
  112. package/src/workers/diffProtocol.ts +155 -0
  113. package/src/components/shared/data-display/SToastContainer.css +0 -53
@@ -1,33 +1,33 @@
1
1
  <template>
2
- <section v-bind="ownedAttrs.bindings()" class="s-catalog-navigator" :aria-label="ariaLabel">
2
+ <section v-bind="ownedAttrs.bindings()" class="s-catalog-navigator" :aria-label="resolvedAriaLabel">
3
3
  <div class="s-catalog-navigator__toolbar">
4
4
  <SInputText
5
5
  v-model="query"
6
6
  size="sm"
7
- :placeholder="searchPlaceholder"
8
- :aria-label="searchPlaceholder"
7
+ :placeholder="resolvedSearchPlaceholder"
8
+ :aria-label="resolvedSearchPlaceholder"
9
9
  />
10
10
  <SSelect
11
11
  v-model="selectedGroup"
12
12
  size="sm"
13
13
  :options="groupOptions"
14
- :label="groupFilterLabel"
14
+ :label="resolvedGroupFilterLabel"
15
15
  />
16
16
  <SSelect
17
17
  v-model="sortMode"
18
18
  size="sm"
19
19
  :options="sortOptions"
20
- label="Сортировка"
20
+ :label="t('navigation.sort')"
21
21
  />
22
22
  </div>
23
23
 
24
24
  <div class="s-catalog-navigator__summary" aria-live="polite">
25
- Найдено: {{ filteredItemCount }} из {{ items.length }}
25
+ {{ t('navigation.found') }} {{ filteredItemCount }} {{ t('navigation.of') }} {{ items.length }}
26
26
  </div>
27
27
 
28
- <div class="s-catalog-navigator__viewport" role="region" :aria-label="`${ariaLabel}: элементы`">
28
+ <div class="s-catalog-navigator__viewport" role="region" :aria-label="t('navigation.catalogItems', { catalog: resolvedAriaLabel })">
29
29
  <div v-if="groupedItems.length === 0" class="s-catalog-navigator__empty">
30
- {{ emptyText }}
30
+ {{ resolvedEmptyText }}
31
31
  </div>
32
32
  <template v-else>
33
33
  <SCatalogNavigatorGroup
@@ -81,6 +81,7 @@ import SButton from '../controls/SButton.vue';
81
81
  import SBadge from '../data-display/SBadge.vue';
82
82
  import SCatalogNavigatorGroup from './_internal/SCatalogNavigatorGroup.vue';
83
83
  import { useOwnedAttrs } from '../../../internal/ownedAttrs';
84
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale';
84
85
 
85
86
  defineOptions({ inheritAttrs: false });
86
87
 
@@ -104,11 +105,16 @@ const props = withDefaults(defineProps<{
104
105
  emptyText?: string;
105
106
  }>(), {
106
107
  selectedId: null,
107
- ariaLabel: 'Каталог',
108
- searchPlaceholder: 'Поиск',
109
- groupFilterLabel: 'Группа',
110
- emptyText: 'Ничего не найдено.',
108
+ ariaLabel: undefined,
109
+ searchPlaceholder: undefined,
110
+ groupFilterLabel: undefined,
111
+ emptyText: undefined,
111
112
  });
113
+ const { locale, t } = useUiKitLocale();
114
+ const resolvedAriaLabel = computed(() => props.ariaLabel ?? t('navigation.catalog'));
115
+ const resolvedSearchPlaceholder = computed(() => props.searchPlaceholder ?? t('actions.search'));
116
+ const resolvedGroupFilterLabel = computed(() => props.groupFilterLabel ?? t('navigation.group'));
117
+ const resolvedEmptyText = computed(() => props.emptyText ?? t('navigation.noResults'));
112
118
 
113
119
  const emit = defineEmits<{
114
120
  (event: 'select', id: string): void;
@@ -240,19 +246,19 @@ function groupDisclosureId(groupKey: string): string {
240
246
  }
241
247
 
242
248
  const groupOptions = computed(() => [
243
- { value: ALL_GROUP_FILTER, label: 'Все группы' },
249
+ { value: ALL_GROUP_FILTER, label: t('navigation.allGroups') },
244
250
  ...[...catalogInventory.value.groups]
245
- .sort((left, right) => left.label.localeCompare(right.label, 'ru'))
251
+ .sort((left, right) => left.label.localeCompare(right.label, locale.value))
246
252
  .map((group) => ({ value: group.filterValue, label: group.label })),
247
253
  ]);
248
254
 
249
- const sortOptions = [
250
- { value: 'title-asc', label: 'По названию' },
251
- { value: 'title-desc', label: 'По названию, обратно' },
252
- { value: 'id-asc', label: 'По ID' },
253
- ];
255
+ const sortOptions = computed(() => [
256
+ { value: 'title-asc', label: t('navigation.sortNameAscending') },
257
+ { value: 'title-desc', label: t('navigation.sortNameDescending') },
258
+ { value: 'id-asc', label: t('navigation.sortId') },
259
+ ]);
254
260
 
255
- const normalizedQuery = computed(() => query.value.trim().toLocaleLowerCase('ru'));
261
+ const normalizedQuery = computed(() => query.value.trim().toLocaleLowerCase(locale.value));
256
262
 
257
263
  const selectedGroupKey = computed(() => {
258
264
  if (selectedGroup.value === ALL_GROUP_FILTER) return null;
@@ -35,7 +35,7 @@
35
35
  class="s-tab-list__tabs"
36
36
  :id="tabListId"
37
37
  role="tablist"
38
- :aria-label="ariaLabel"
38
+ :aria-label="resolvedAriaLabel"
39
39
  aria-orientation="horizontal"
40
40
  >
41
41
  <slot />
@@ -48,7 +48,7 @@
48
48
  ref="controlsRef"
49
49
  class="s-tabs-header__controls"
50
50
  role="group"
51
- :aria-label="controlsAriaLabel"
51
+ :aria-label="resolvedControlsAriaLabel"
52
52
  >
53
53
  <slot name="controls" />
54
54
  </div>
@@ -81,7 +81,7 @@
81
81
  class="s-tab-list__tabs"
82
82
  :id="tabListId"
83
83
  role="tablist"
84
- :aria-label="ariaLabel"
84
+ :aria-label="resolvedAriaLabel"
85
85
  aria-orientation="horizontal"
86
86
  >
87
87
  <slot />
@@ -92,7 +92,7 @@
92
92
  ref="controlsRef"
93
93
  class="s-tabs-header__controls"
94
94
  role="group"
95
- :aria-label="controlsAriaLabel"
95
+ :aria-label="resolvedControlsAriaLabel"
96
96
  >
97
97
  <slot name="controls" />
98
98
  </div>
@@ -116,6 +116,7 @@ import {
116
116
  type STabsContext,
117
117
  } from '../_internal/tabsContext'
118
118
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
119
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
119
120
  import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
120
121
 
121
122
  defineOptions({
@@ -150,11 +151,14 @@ const props = withDefaults(defineProps<Props>(), {
150
151
  divider: true,
151
152
  inset: 'none',
152
153
  headerPosition: 'sticky',
153
- ariaLabel: 'Вкладки',
154
+ ariaLabel: undefined,
154
155
  tabListId: undefined,
155
156
  leadingAriaLabel: undefined,
156
- controlsAriaLabel: 'Действия вкладок',
157
+ controlsAriaLabel: undefined,
157
158
  })
159
+ const { t } = useUiKitLocale()
160
+ const resolvedAriaLabel = computed(() => props.ariaLabel ?? t('navigation.tabs'))
161
+ const resolvedControlsAriaLabel = computed(() => props.controlsAriaLabel ?? t('navigation.tabActions'))
158
162
 
159
163
  watch(
160
164
  () => props.tabListId,
@@ -21,8 +21,8 @@
21
21
  <STabList
22
22
  v-if="variant !== 'bottom'"
23
23
  placement="top"
24
- :aria-label="ariaLabel"
25
- :controls-aria-label="controlsAriaLabel"
24
+ :aria-label="resolvedAriaLabel"
25
+ :controls-aria-label="resolvedControlsAriaLabel"
26
26
  :header-position="headerPosition"
27
27
  :inset="tabListInset"
28
28
  >
@@ -33,8 +33,8 @@
33
33
  <STabList
34
34
  v-if="variant === 'bottom'"
35
35
  placement="bottom"
36
- :aria-label="ariaLabel"
37
- :controls-aria-label="controlsAriaLabel"
36
+ :aria-label="resolvedAriaLabel"
37
+ :controls-aria-label="resolvedControlsAriaLabel"
38
38
  >
39
39
  <slot name="list" />
40
40
  <template v-if="$slots.controls" #controls><slot name="controls" /></template>
@@ -58,6 +58,7 @@ import {
58
58
  type VNodeChild,
59
59
  } from 'vue'
60
60
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
61
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
61
62
  import { resolveSemanticToneBinding } from '../../../internal/semanticTone'
62
63
  import { lastItem } from '../../../internal/es2020'
63
64
  import {
@@ -153,8 +154,8 @@ const props = withDefaults(defineProps<Props>(), {
153
154
  defaultValue: undefined,
154
155
  activationMode: 'automatic',
155
156
  tone: 'primary',
156
- ariaLabel: 'Вкладки',
157
- controlsAriaLabel: 'Действия вкладок',
157
+ ariaLabel: undefined,
158
+ controlsAriaLabel: undefined,
158
159
  headerPosition: 'sticky',
159
160
  tabListInset: 'none',
160
161
  grow: true,
@@ -166,6 +167,9 @@ const props = withDefaults(defineProps<Props>(), {
166
167
  surfaceRadius: 'default',
167
168
  surface: 'default',
168
169
  })
170
+ const { t } = useUiKitLocale()
171
+ const resolvedAriaLabel = computed(() => props.ariaLabel ?? t('navigation.tabs'))
172
+ const resolvedControlsAriaLabel = computed(() => props.controlsAriaLabel ?? t('navigation.tabActions'))
169
173
 
170
174
  const emit = defineEmits<{
171
175
  'update:modelValue': [key: string]
@@ -213,8 +217,8 @@ function assertPropsContract(): void {
213
217
  if (props.modelValue !== undefined && props.defaultValue !== undefined) {
214
218
  throw new Error('STabs: modelValue and defaultValue are mutually exclusive')
215
219
  }
216
- if (!props.ariaLabel.trim()) throw new TypeError('STabs: ariaLabel must be non-empty')
217
- if (!props.controlsAriaLabel.trim()) throw new TypeError('STabs: controlsAriaLabel must be non-empty')
220
+ if (!resolvedAriaLabel.value.trim()) throw new TypeError('STabs: ariaLabel must be non-empty')
221
+ if (!resolvedControlsAriaLabel.value.trim()) throw new TypeError('STabs: controlsAriaLabel must be non-empty')
218
222
  if (props.testId !== undefined && !props.testId.trim()) throw new TypeError('STabs: testId must be non-empty')
219
223
  }
220
224
 
@@ -223,8 +227,8 @@ watch(() => [
223
227
  props.composition,
224
228
  props.modelValue,
225
229
  props.defaultValue,
226
- props.ariaLabel,
227
- props.controlsAriaLabel,
230
+ resolvedAriaLabel.value,
231
+ resolvedControlsAriaLabel.value,
228
232
  props.testId,
229
233
  ] as const, assertPropsContract)
230
234
 
@@ -538,7 +542,7 @@ defineExpose({
538
542
  --s-tab-padding-inline: var(--s-tab-base-padding-inline);
539
543
  --s-tab-padding-block: 0;
540
544
  --s-tab-content-offset-y: 0;
541
- --s-tab-trigger-min-height: var(--s-control-hitbox-sm);
545
+ --s-tab-trigger-min-height: var(--s-tab-row-height);
542
546
  --s-tab-indicator-width: 2px;
543
547
  --s-tab-radius: 0.375rem 0.375rem 0 0;
544
548
  --s-tab-active-translate-y: 1px;
@@ -63,6 +63,7 @@ export interface Props<T extends PublicSelectionKey = string | number> {
63
63
 
64
64
  <script setup lang="ts" generic="T extends SelectionKey = string | number">
65
65
  import { computed } from 'vue'
66
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
66
67
  import { validateBoolean, validateExactString, validateNonEmptyString } from '../../../internal/runtimeContract'
67
68
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
68
69
  import SButton from '../controls/SButton.vue'
@@ -86,8 +87,9 @@ interface WizardStepRecord<TStep extends SelectionKey> {
86
87
  const props = withDefaults(defineProps<Props<T>>(), {
87
88
  columns: 'auto',
88
89
  density: 'default',
89
- ariaLabel: 'Шаги мастера',
90
+ ariaLabel: undefined,
90
91
  })
92
+ const { t } = useUiKitLocale()
91
93
 
92
94
  const emit = defineEmits<{
93
95
  'update:modelValue': [value: T]
@@ -125,7 +127,7 @@ const validatedDensity = computed(() => validateExactString(
125
127
  const validatedAriaLabel = computed(() => validateNonEmptyString(
126
128
  'SWizardSteps',
127
129
  'ariaLabel',
128
- props.ariaLabel,
130
+ props.ariaLabel ?? t('navigation.wizardSteps'),
129
131
  ))
130
132
 
131
133
  const stepRecords = computed<readonly WizardStepRecord<T>[]>(() => {
@@ -25,6 +25,7 @@ import {
25
25
  resolvePersonaProjectPickerAsyncState,
26
26
  } from './_internal/inventory'
27
27
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
28
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
28
29
 
29
30
  defineOptions({ inheritAttrs: false })
30
31
 
@@ -52,6 +53,7 @@ const emit = defineEmits<{
52
53
  'activate-connected': [bindingId: string]
53
54
  }>()
54
55
  const ownedAttrs = useOwnedAttrs({ component: 'SPersonaProjectPicker', owner: 'project-picker modal surface' })
56
+ const { locale, t } = useUiKitLocale()
55
57
 
56
58
  const filter = ref('')
57
59
  const selectedProjectId = ref<string | null>(null)
@@ -122,8 +124,8 @@ const listing = computed(() => {
122
124
  const pickerAsyncState = computed(() => resolvePersonaProjectPickerAsyncState(
123
125
  props.state,
124
126
  {
125
- loadingMessage: 'Загрузка каталогов',
126
- emptyMessage: 'Выберите область поиска',
127
+ loadingMessage: t('persona.loadingCatalogs'),
128
+ emptyMessage: t('persona.chooseSearchArea'),
127
129
  },
128
130
  ))
129
131
  const projectInventory = computed(() => {
@@ -139,10 +141,10 @@ const selectedProject = computed(() => {
139
141
  return project?.isSelectable ? project : null
140
142
  })
141
143
  const visibleDirectories = computed(() => {
142
- const query = filter.value.trim().toLocaleLowerCase('ru-RU')
144
+ const query = filter.value.trim().toLocaleLowerCase(locale.value)
143
145
  if (!query) return listing.value?.directories ?? []
144
146
  return (listing.value?.directories ?? []).filter((directory) =>
145
- `${directory.name} ${directory.displayPath}`.toLocaleLowerCase('ru-RU').includes(query),
147
+ `${directory.name} ${directory.displayPath}`.toLocaleLowerCase(locale.value).includes(query),
146
148
  )
147
149
  })
148
150
  const breadcrumbs = computed(() => {
@@ -165,7 +167,9 @@ const breadcrumbs = computed(() => {
165
167
  }
166
168
  return result
167
169
  })
168
- const emptyLabel = computed(() => filter.value.trim() ? 'Совпадений не найдено' : 'В каталоге нет вложенных директорий')
170
+ const emptyLabel = computed(() => filter.value.trim()
171
+ ? t('persona.noMatches')
172
+ : t('persona.noNestedDirectories'))
169
173
 
170
174
  watch(
171
175
  () => props.open,
@@ -205,8 +209,8 @@ function connectedProject(project: PersonaProjectPickerEntry): PersonaProjectPic
205
209
  function projectActionLabel(project: PersonaProjectPickerEntry): string | undefined {
206
210
  if (!project.isSelectable) return undefined
207
211
  const connected = connectedProject(project)
208
- if (connected) return connected.active ? 'Активен' : 'Открыть'
209
- return selectedProject.value?.id === project.id ? 'Выбран' : 'Выбрать'
212
+ if (connected) return connected.active ? t('persona.active') : t('persona.open')
213
+ return selectedProject.value?.id === project.id ? t('persona.selected') : t('persona.select')
210
214
  }
211
215
 
212
216
  function projectAction(project: PersonaProjectPickerEntry): void {
@@ -227,14 +231,14 @@ function confirmSelection(): void {
227
231
  <SModal
228
232
  v-bind="ownedAttrs.bindings()"
229
233
  :open="open"
230
- title="Выбор Persona-проекта"
234
+ :title="t('persona.pickerTitle')"
231
235
  size="wide"
232
236
  @update:open="emit('update:open', $event)"
233
237
  >
234
238
  <div class="s-persona-project-picker">
235
239
  <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">
240
+ <SPanel :title="t('persona.connected')" :icon="FolderTree">
241
+ <SActionList :accessible-label="t('persona.connectedProjects')" max-height="md">
238
242
  <SActionListItem
239
243
  v-for="project in validatedConnectedProjects"
240
244
  :key="project.id"
@@ -242,9 +246,9 @@ function confirmSelection(): void {
242
246
  :description="project.path"
243
247
  :icon="FolderTree"
244
248
  :selected="project.active"
245
- :status-label="project.active ? 'Активен' : undefined"
249
+ :status-label="project.active ? t('persona.active') : undefined"
246
250
  status-severity="success"
247
- :surface="{ mode: 'action', accessibleLabel: `Открыть проект ${project.title}` }"
251
+ :surface="{ mode: 'action', accessibleLabel: t('persona.openProject', { title: project.title }) }"
248
252
  @activate="emit('activate-connected', project.id)"
249
253
  />
250
254
  </SActionList>
@@ -257,8 +261,8 @@ function confirmSelection(): void {
257
261
  <SSelect
258
262
  :model-value="resolvedSelectedRootId"
259
263
  :options="rootOptions"
260
- placeholder="Выберите область"
261
- aria-label="Область поиска проектов"
264
+ :placeholder="t('persona.selectArea')"
265
+ :aria-label="t('persona.projectSearchArea')"
262
266
  @update:model-value="selectRoot"
263
267
  />
264
268
  </div>
@@ -266,8 +270,8 @@ function confirmSelection(): void {
266
270
  ref="searchInput"
267
271
  v-model="filter"
268
272
  size="sm"
269
- placeholder="Найти в текущем каталоге"
270
- aria-label="Поиск каталога"
273
+ :placeholder="t('persona.searchCurrentCatalog')"
274
+ :aria-label="t('persona.catalogSearch')"
271
275
  />
272
276
  <SButton
273
277
  label=""
@@ -275,8 +279,8 @@ function confirmSelection(): void {
275
279
  severity="secondary"
276
280
  :leading-icon="RefreshCw"
277
281
  icon-only
278
- title="Обновить каталог"
279
- aria-label="Обновить каталог"
282
+ :title="t('persona.refreshCatalog')"
283
+ :aria-label="t('persona.refreshCatalog')"
280
284
  @click="emit('refresh')"
281
285
  />
282
286
  </div>
@@ -284,13 +288,13 @@ function confirmSelection(): void {
284
288
  <SBreadcrumbs
285
289
  v-if="listing"
286
290
  :items="breadcrumbs"
287
- aria-label="Путь к каталогу"
291
+ :aria-label="t('persona.catalogPath')"
288
292
  @action="navigateBreadcrumb($event)"
289
293
  />
290
294
 
291
295
  <SAsyncState :state="pickerAsyncState">
292
296
  <div v-if="listing" class="s-persona-project-picker__content">
293
- <SActionList accessible-label="Директории и Persona-проекты" max-height="lg">
297
+ <SActionList :accessible-label="t('persona.directoriesAndProjects')" max-height="lg">
294
298
  <SActionListItem
295
299
  v-if="listing.current.isSelectable"
296
300
  :title="listing.current.projectName || listing.current.name"
@@ -302,7 +306,7 @@ function confirmSelection(): void {
302
306
  :action-icon="connectedProject(listing.current)?.active ? Check : undefined"
303
307
  :action-disabled="connectedProject(listing.current)?.active"
304
308
  action-severity="success"
305
- status-label="Persona-проект"
309
+ :status-label="t('persona.personaProject')"
306
310
  status-severity="success"
307
311
  :surface="{ mode: 'static' }"
308
312
  @action="projectAction(listing.current)"
@@ -319,10 +323,10 @@ function confirmSelection(): void {
319
323
  :action-icon="connectedProject(directory)?.active || selectedProject?.id === directory.id ? Check : undefined"
320
324
  :action-disabled="connectedProject(directory)?.active"
321
325
  :action-severity="connectedProject(directory)?.active || selectedProject?.id === directory.id ? 'success' : 'secondary'"
322
- :status-label="directory.readiness === 'unreadable' ? 'Нет доступа' : directory.readiness === 'invalid' ? 'Ошибка проекта' : undefined"
326
+ :status-label="directory.readiness === 'unreadable' ? t('persona.noAccess') : directory.readiness === 'invalid' ? t('persona.projectError') : undefined"
323
327
  status-severity="danger"
324
328
  :trailing-icon="directory.isSelectable || directory.readiness === 'unreadable' || directory.readiness === 'invalid' ? undefined : ChevronRight"
325
- :surface="{ mode: 'action', accessibleLabel: `Открыть ${directory.name}` }"
329
+ :surface="{ mode: 'action', accessibleLabel: t('persona.openDirectory', { name: directory.name }) }"
326
330
  @activate="emit('navigate', directory.relativePath)"
327
331
  @action="projectAction(directory)"
328
332
  />
@@ -331,7 +335,7 @@ function confirmSelection(): void {
331
335
  <SEmptyState
332
336
  v-if="visibleDirectories.length === 0"
333
337
  :title="emptyLabel"
334
- :description="filter.trim() ? 'Измените запрос или перейдите в другой каталог.' : 'Вернитесь на уровень выше или выберите другую область.'"
338
+ :description="filter.trim() ? t('persona.adjustSearch') : t('persona.chooseAnotherArea')"
335
339
  :icon="FolderSearch"
336
340
  size="sm"
337
341
  />
@@ -341,7 +345,7 @@ function confirmSelection(): void {
341
345
  :loading="state.pagination === 'loading-more'"
342
346
  @click="emit('load-more')"
343
347
  >
344
- Загрузить ещё
348
+ {{ t('persona.loadMore') }}
345
349
  </SButton>
346
350
  </div>
347
351
  </SAsyncState>
@@ -352,8 +356,8 @@ function confirmSelection(): void {
352
356
  <SBadge v-if="selectedProject" severity="info">{{ selectedProject.displayPath }}</SBadge>
353
357
  </template>
354
358
  <template #footer-end>
355
- <SButton severity="secondary" @click="emit('update:open', false)">Отмена</SButton>
356
- <SButton :disabled="!selectedProject" @click="confirmSelection">Подключить проект</SButton>
359
+ <SButton severity="secondary" @click="emit('update:open', false)">{{ t('persona.cancel') }}</SButton>
360
+ <SButton :disabled="!selectedProject" @click="confirmSelection">{{ t('persona.connectProject') }}</SButton>
357
361
  </template>
358
362
  </SModal>
359
363
  </template>