@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,4 +1,9 @@
1
1
  import { validateExactString } from '../../../internal/runtimeContract'
2
+ import type { UiKitLocale, UiKitStaticMessageKey, UiKitTranslate } from '../../../locales/locale'
3
+ import { createUiKitTranslate, UI_KIT_MESSAGES } from '../../../locales/messages'
4
+
5
+ const defaultTranslate = createUiKitTranslate(() => UI_KIT_MESSAGES.ru)
6
+ type PersonaStaticMessageKey = Extract<UiKitStaticMessageKey, `persona.${string}`>
2
7
 
3
8
  export type PersonaStatusSeverity = 'secondary' | 'success' | 'info' | 'warn' | 'danger'
4
9
 
@@ -31,18 +36,23 @@ type PersonaDiagnosticOverviewItem = {
31
36
 
32
37
  export function personaDiagnosticPresentation(
33
38
  input: PersonaDiagnosticPresentationInput,
39
+ t: UiKitTranslate = defaultTranslate,
34
40
  ): PersonaDiagnosticPresentation {
35
41
  const isError = input.severity === 'error' || input.severity === 'blocking'
36
- const statusLabel = isError ? 'ошибка' : input.severity === 'warning' ? 'предупреждение' : 'информация'
42
+ const statusLabel = isError
43
+ ? t('persona.diagnosticError')
44
+ : input.severity === 'warning'
45
+ ? t('persona.diagnosticWarning')
46
+ : t('persona.diagnosticInformation')
37
47
  const statusSeverity: PersonaStatusSeverity = isError ? 'danger' : input.severity === 'warning' ? 'warn' : 'info'
38
48
 
39
49
  if (input.context === 'native_scenarios') {
40
50
  return {
41
51
  category: 'scenario_import',
42
- title: 'Сценарий не импортирован',
43
- ownerLabel: 'Исправить в тестовом проекте',
44
- impactLabel: 'Сценарий недоступен для запуска',
45
- remediation: 'Восстановить отсутствующий модуль или исправить импорт в сценарии.',
52
+ title: t('persona.scenarioNotImported'),
53
+ ownerLabel: t('persona.fixInTestProject'),
54
+ impactLabel: t('persona.scenarioUnavailable'),
55
+ remediation: t('persona.restoreScenarioImport'),
46
56
  statusLabel,
47
57
  statusSeverity,
48
58
  }
@@ -50,23 +60,23 @@ export function personaDiagnosticPresentation(
50
60
  if (input.context === 'components') {
51
61
  return {
52
62
  category: 'component_analysis',
53
- title: 'Компонент не проанализирован',
54
- ownerLabel: 'Исправить в тестовом проекте',
55
- impactLabel: 'Структура связанных сценариев может быть неполной',
56
- remediation: 'Исправить импорт компонента и его зависимостей.',
63
+ title: t('persona.componentNotAnalyzed'),
64
+ ownerLabel: t('persona.fixInTestProject'),
65
+ impactLabel: t('persona.relatedStructureIncomplete'),
66
+ remediation: t('persona.restoreComponentImport'),
57
67
  statusLabel,
58
68
  statusSeverity,
59
69
  }
60
70
  }
61
71
  if (input.context === 'support_quality') {
62
72
  const remediation = input.reason === 'page_support_import_non_maintained'
63
- ? 'Перенести используемый PageObject из пробной области в поддерживаемую область проекта.'
64
- : 'Дать элементу или компоненту устойчивое предметное имя в коде проекта.'
73
+ ? t('persona.movePageObject')
74
+ : t('persona.giveStableName')
65
75
  return {
66
76
  category: 'support_quality',
67
- title: 'Качество PageObject',
68
- ownerLabel: 'Исправить в тестовом проекте',
69
- impactLabel: 'Не блокирует доступные сценарии',
77
+ title: t('persona.pageObjectQuality'),
78
+ ownerLabel: t('persona.fixInTestProject'),
79
+ impactLabel: t('persona.doesNotBlockScenarios'),
70
80
  remediation,
71
81
  statusLabel,
72
82
  statusSeverity,
@@ -74,10 +84,10 @@ export function personaDiagnosticPresentation(
74
84
  }
75
85
  return {
76
86
  category: 'unclassified',
77
- title: 'Неклассифицированное сообщение Persona',
78
- ownerLabel: 'Требуется классификация Persona',
79
- impactLabel: 'Влияние не указано',
80
- remediation: 'Persona должна передать машинно-читаемые владельца, влияние и способ исправления.',
87
+ title: t('persona.unclassifiedPersonaMessage'),
88
+ ownerLabel: t('persona.personaClassificationRequired'),
89
+ impactLabel: t('persona.impactUnspecified'),
90
+ remediation: t('persona.personaMustClassify'),
81
91
  statusLabel,
82
92
  statusSeverity,
83
93
  }
@@ -87,6 +97,7 @@ export function personaDiagnosticOverviewMessage(
87
97
  availableTests: number,
88
98
  unavailableTests: number,
89
99
  diagnostics: readonly PersonaDiagnosticOverviewItem[],
100
+ t: UiKitTranslate = defaultTranslate,
90
101
  ): string {
91
102
  const count = (category: PersonaDiagnosticOverviewItem['category']) => (
92
103
  diagnostics.filter((diagnostic) => diagnostic.category === category).length
@@ -96,12 +107,12 @@ export function personaDiagnosticOverviewMessage(
96
107
  const componentAnalysis = count('component_analysis')
97
108
  const supportQuality = count('support_quality')
98
109
  const parts = [
99
- `Доступно ${availableTests} из ${totalTests} сценариев.`,
100
- scenarioImports > 0 ? `${scenarioImports} сценариев не импортируются из-за ошибок в зависимостях тестового проекта.` : null,
110
+ t('persona.overviewAvailable', { available: availableTests, total: totalTests }),
111
+ scenarioImports > 0 ? t('persona.overviewScenarioImports', { count: scenarioImports }) : null,
101
112
  componentAnalysis > 0
102
- ? `${componentAnalysis} сообщений относятся к анализу компонентов, остановленному отсутствующими модулями тестового проекта.`
113
+ ? t('persona.overviewComponentAnalysis', { count: componentAnalysis })
103
114
  : null,
104
- supportQuality > 0 ? `${supportQuality} замечаний качества PageObject не блокируют доступные сценарии.` : null,
115
+ supportQuality > 0 ? t('persona.overviewSupportQuality', { count: supportQuality }) : null,
105
116
  ]
106
117
  return parts.filter((part): part is string => Boolean(part)).join(' ')
107
118
  }
@@ -126,24 +137,24 @@ const PERSONA_RUN_STATUSES = [
126
137
  ] as const
127
138
  type PersonaRunStatus = typeof PERSONA_RUN_STATUSES[number]
128
139
  const PERSONA_RUN_STATUS_PRESENTATIONS = {
129
- cancelled: { label: 'отменён', severity: 'secondary' },
130
- cancelling: { label: 'отменяется', severity: 'warn' },
131
- completed: { label: 'завершён', severity: 'success' },
132
- degraded: { label: 'требует внимания', severity: 'warn' },
133
- error: { label: 'ошибка', severity: 'danger' },
134
- failed: { label: 'ошибка', severity: 'danger' },
135
- passed: { label: 'пройден', severity: 'success' },
136
- paused: { label: 'приостановлен', severity: 'info' },
137
- queued: { label: 'в очереди', severity: 'warn' },
138
- running: { label: 'выполняется', severity: 'info' },
139
- skipped: { label: 'пропущен', severity: 'warn' },
140
- timed_out: { label: 'превышено время', severity: 'danger' },
141
- timeout: { label: 'превышено время', severity: 'danger' },
142
- info: { label: 'информация', severity: 'info' },
143
- warn: { label: 'предупреждение', severity: 'warn' },
144
- warning: { label: 'предупреждение', severity: 'warn' },
140
+ cancelled: { labelKey: 'persona.statusCancelled', severity: 'secondary' },
141
+ cancelling: { labelKey: 'persona.statusCancelling', severity: 'warn' },
142
+ completed: { labelKey: 'persona.statusCompleted', severity: 'success' },
143
+ degraded: { labelKey: 'persona.statusDegraded', severity: 'warn' },
144
+ error: { labelKey: 'persona.statusError', severity: 'danger' },
145
+ failed: { labelKey: 'persona.statusError', severity: 'danger' },
146
+ passed: { labelKey: 'persona.statusPassed', severity: 'success' },
147
+ paused: { labelKey: 'persona.statusPaused', severity: 'info' },
148
+ queued: { labelKey: 'persona.statusQueued', severity: 'warn' },
149
+ running: { labelKey: 'persona.statusRunning', severity: 'info' },
150
+ skipped: { labelKey: 'persona.statusSkipped', severity: 'warn' },
151
+ timed_out: { labelKey: 'persona.statusTimedOut', severity: 'danger' },
152
+ timeout: { labelKey: 'persona.statusTimedOut', severity: 'danger' },
153
+ info: { labelKey: 'persona.diagnosticInformation', severity: 'info' },
154
+ warn: { labelKey: 'persona.diagnosticWarning', severity: 'warn' },
155
+ warning: { labelKey: 'persona.diagnosticWarning', severity: 'warn' },
145
156
  } as const satisfies Record<PersonaRunStatus, {
146
- label: string
157
+ labelKey: PersonaStaticMessageKey
147
158
  severity: PersonaStatusSeverity
148
159
  }>
149
160
 
@@ -151,8 +162,8 @@ function resolvePersonaRunStatus(status: unknown): PersonaRunStatus {
151
162
  return validateExactString('Persona presentation', 'run status', status, PERSONA_RUN_STATUSES)
152
163
  }
153
164
 
154
- export function personaRunStatusLabel(status: unknown): string {
155
- return PERSONA_RUN_STATUS_PRESENTATIONS[resolvePersonaRunStatus(status)].label
165
+ export function personaRunStatusLabel(status: unknown, t: UiKitTranslate = defaultTranslate): string {
166
+ return t(PERSONA_RUN_STATUS_PRESENTATIONS[resolvePersonaRunStatus(status)].labelKey)
156
167
  }
157
168
 
158
169
  export function personaStatusSeverity(status: unknown): PersonaStatusSeverity {
@@ -180,26 +191,26 @@ const PERSONA_SEMANTIC_KINDS = [
180
191
  ] as const
181
192
  type PersonaSemanticKind = typeof PERSONA_SEMANTIC_KINDS[number]
182
193
  const PERSONA_SEMANTIC_KIND_LABELS = {
183
- Action: 'Действие',
184
- CombinedStep: 'Составной шаг',
185
- Expectation: 'Проверка',
186
- Fact: 'Данные',
187
- Goal: 'Цель',
188
- Ops: 'Служебная операция',
189
- Step: 'Шаг',
190
- action: 'Действие',
191
- check: 'Проверка',
192
- combined_step: 'Составной шаг',
193
- expectation: 'Проверка',
194
- fact: 'Данные',
195
- get: 'Получение данных',
196
- goal: 'Цель',
197
- make: 'Действие',
198
- persona_call: 'Вызов Persona',
199
- step: 'Шаг',
200
- } as const satisfies Record<PersonaSemanticKind, string>
194
+ Action: 'persona.semanticAction',
195
+ CombinedStep: 'persona.semanticCombinedStep',
196
+ Expectation: 'persona.semanticExpectation',
197
+ Fact: 'persona.semanticFact',
198
+ Goal: 'persona.semanticGoal',
199
+ Ops: 'persona.semanticOps',
200
+ Step: 'persona.semanticStep',
201
+ action: 'persona.semanticAction',
202
+ check: 'persona.semanticExpectation',
203
+ combined_step: 'persona.semanticCombinedStep',
204
+ expectation: 'persona.semanticExpectation',
205
+ fact: 'persona.semanticFact',
206
+ get: 'persona.semanticGet',
207
+ goal: 'persona.semanticGoal',
208
+ make: 'persona.semanticAction',
209
+ persona_call: 'persona.semanticPersonaCall',
210
+ step: 'persona.semanticStep',
211
+ } as const satisfies Record<PersonaSemanticKind, PersonaStaticMessageKey>
201
212
 
202
- export function personaSemanticKindLabel(kind: unknown): string {
213
+ export function personaSemanticKindLabel(kind: unknown, t: UiKitTranslate = defaultTranslate): string {
203
214
  if (kind === null || kind === undefined) return ''
204
215
  const resolvedKind = validateExactString(
205
216
  'Persona presentation',
@@ -207,30 +218,33 @@ export function personaSemanticKindLabel(kind: unknown): string {
207
218
  kind,
208
219
  PERSONA_SEMANTIC_KINDS,
209
220
  )
210
- return PERSONA_SEMANTIC_KIND_LABELS[resolvedKind]
221
+ return t(PERSONA_SEMANTIC_KIND_LABELS[resolvedKind])
211
222
  }
212
223
 
213
- export function personaParameterSummary(parameters: readonly PersonaParameterPresentation[]): string {
224
+ export function personaParameterSummary(
225
+ parameters: readonly PersonaParameterPresentation[],
226
+ t: UiKitTranslate = defaultTranslate,
227
+ ): string {
214
228
  return parameters.map((parameter) => {
215
229
  const type = parameter.type ? `: ${parameter.type}` : ''
216
230
  const hasPrimitiveDefault = typeof parameter.default === 'string'
217
231
  || typeof parameter.default === 'number'
218
232
  || typeof parameter.default === 'boolean'
219
233
  const defaultValue = hasPrimitiveDefault ? ` = ${String(parameter.default)}` : ''
220
- return `${parameter.name}${type}${defaultValue}${parameter.required ? ' (обязательный)' : ''}`
234
+ return `${parameter.name}${type}${defaultValue}${parameter.required ? ` (${t('persona.requiredParameter')})` : ''}`
221
235
  }).join(', ')
222
236
  }
223
237
 
224
238
  const PERSONA_SEVERITIES = ['blocker', 'critical', 'minor', 'normal', 'trivial'] as const
225
239
  type PersonaSeverity = typeof PERSONA_SEVERITIES[number]
226
240
  const PERSONA_SEVERITY_PRESENTATIONS = {
227
- blocker: { label: 'блокирующий', severity: 'danger' },
228
- critical: { label: 'критический', severity: 'danger' },
229
- minor: { label: 'низкая критичность', severity: 'secondary' },
230
- normal: { label: 'обычная критичность', severity: 'info' },
231
- trivial: { label: 'минимальная критичность', severity: 'secondary' },
241
+ blocker: { labelKey: 'persona.severityBlocker', severity: 'danger' },
242
+ critical: { labelKey: 'persona.severityCritical', severity: 'danger' },
243
+ minor: { labelKey: 'persona.severityMinor', severity: 'secondary' },
244
+ normal: { labelKey: 'persona.severityNormal', severity: 'info' },
245
+ trivial: { labelKey: 'persona.severityTrivial', severity: 'secondary' },
232
246
  } as const satisfies Record<PersonaSeverity, {
233
- label: string
247
+ labelKey: PersonaStaticMessageKey
234
248
  severity: PersonaStatusSeverity
235
249
  }>
236
250
 
@@ -238,8 +252,8 @@ function resolvePersonaSeverity(value: unknown): PersonaSeverity {
238
252
  return validateExactString('Persona presentation', 'severity', value, PERSONA_SEVERITIES)
239
253
  }
240
254
 
241
- export function personaSeverityLabel(value: unknown): string {
242
- return PERSONA_SEVERITY_PRESENTATIONS[resolvePersonaSeverity(value)].label
255
+ export function personaSeverityLabel(value: unknown, t: UiKitTranslate = defaultTranslate): string {
256
+ return t(PERSONA_SEVERITY_PRESENTATIONS[resolvePersonaSeverity(value)].labelKey)
243
257
  }
244
258
 
245
259
  export function personaSeverityBadge(value: unknown): PersonaStatusSeverity {
@@ -249,20 +263,20 @@ export function personaSeverityBadge(value: unknown): PersonaStatusSeverity {
249
263
  const PERSONA_REPORT_STATUSES = ['broken', 'failed', 'passed', 'skipped'] as const
250
264
  type PersonaReportStatus = typeof PERSONA_REPORT_STATUSES[number]
251
265
  const PERSONA_REPORT_STATUS_LABELS = {
252
- broken: 'сломан',
253
- failed: 'ошибка',
254
- passed: 'пройден',
255
- skipped: 'пропущен',
256
- } as const satisfies Record<PersonaReportStatus, string>
266
+ broken: 'persona.reportBroken',
267
+ failed: 'persona.statusError',
268
+ passed: 'persona.statusPassed',
269
+ skipped: 'persona.statusSkipped',
270
+ } as const satisfies Record<PersonaReportStatus, PersonaStaticMessageKey>
257
271
 
258
- export function personaReportStatusLabel(status: unknown): string {
272
+ export function personaReportStatusLabel(status: unknown, t: UiKitTranslate = defaultTranslate): string {
259
273
  const resolvedStatus = validateExactString(
260
274
  'Persona presentation',
261
275
  'report status',
262
276
  status,
263
277
  PERSONA_REPORT_STATUSES,
264
278
  )
265
- return PERSONA_REPORT_STATUS_LABELS[resolvedStatus]
279
+ return t(PERSONA_REPORT_STATUS_LABELS[resolvedStatus])
266
280
  }
267
281
 
268
282
  const PERSONA_LIFECYCLE_STAGES = [
@@ -273,20 +287,20 @@ const PERSONA_LIFECYCLE_STAGES = [
273
287
  ] as const
274
288
  type PersonaLifecycleStage = typeof PERSONA_LIFECYCLE_STAGES[number]
275
289
  const PERSONA_LIFECYCLE_STAGE_LABELS = {
276
- run_after_scenario: 'после сценария',
277
- run_after_test: 'после теста',
278
- run_before_scenario: 'перед сценарием',
279
- run_before_test: 'перед тестом',
280
- } as const satisfies Record<PersonaLifecycleStage, string>
290
+ run_after_scenario: 'persona.lifecycleAfterScenario',
291
+ run_after_test: 'persona.lifecycleAfterTest',
292
+ run_before_scenario: 'persona.lifecycleBeforeScenario',
293
+ run_before_test: 'persona.lifecycleBeforeTest',
294
+ } as const satisfies Record<PersonaLifecycleStage, PersonaStaticMessageKey>
281
295
 
282
- export function personaLifecycleStageLabel(stage: unknown): string {
296
+ export function personaLifecycleStageLabel(stage: unknown, t: UiKitTranslate = defaultTranslate): string {
283
297
  const resolvedStage = validateExactString(
284
298
  'Persona presentation',
285
299
  'lifecycle stage',
286
300
  stage,
287
301
  PERSONA_LIFECYCLE_STAGES,
288
302
  )
289
- return PERSONA_LIFECYCLE_STAGE_LABELS[resolvedStage]
303
+ return t(PERSONA_LIFECYCLE_STAGE_LABELS[resolvedStage])
290
304
  }
291
305
 
292
306
  const PERSONA_EVENT_TYPES = [
@@ -308,31 +322,31 @@ const PERSONA_EVENT_TYPES = [
308
322
  ] as const
309
323
  type PersonaEventType = typeof PERSONA_EVENT_TYPES[number]
310
324
  const PERSONA_EVENT_TYPE_LABELS = {
311
- cancelled: 'Запуск отменён',
312
- completed: 'Запуск завершён',
313
- failed: 'Запуск завершён с ошибкой',
314
- log: 'Сообщение выполнения',
315
- queued: 'Запуск поставлен в очередь',
316
- running: 'Запуск начат',
317
- started: 'Запуск начат',
318
- timeout: 'Время запуска превышено',
319
- 'report.runtime_log': 'Сообщение выполнения',
320
- 'scenario.accepted': 'Тест поставлен в очередь',
321
- 'scenario.cancelled': 'Тест отменён',
322
- 'scenario.finished': 'Тест завершён',
323
- 'scenario.started': 'Тест запущен',
324
- 'worker.started': 'Исполнитель подключён',
325
- 'worker.stopped': 'Исполнитель остановлен',
326
- } as const satisfies Record<PersonaEventType, string>
325
+ cancelled: 'persona.eventRunCancelled',
326
+ completed: 'persona.eventRunCompleted',
327
+ failed: 'persona.eventRunFailed',
328
+ log: 'persona.eventRuntimeMessage',
329
+ queued: 'persona.eventRunQueued',
330
+ running: 'persona.eventRunStarted',
331
+ started: 'persona.eventRunStarted',
332
+ timeout: 'persona.eventRunTimedOut',
333
+ 'report.runtime_log': 'persona.eventRuntimeMessage',
334
+ 'scenario.accepted': 'persona.eventTestQueued',
335
+ 'scenario.cancelled': 'persona.eventTestCancelled',
336
+ 'scenario.finished': 'persona.eventTestFinished',
337
+ 'scenario.started': 'persona.eventTestStarted',
338
+ 'worker.started': 'persona.eventWorkerStarted',
339
+ 'worker.stopped': 'persona.eventWorkerStopped',
340
+ } as const satisfies Record<PersonaEventType, PersonaStaticMessageKey>
327
341
 
328
- export function personaEventTypeLabel(eventType: unknown): string {
342
+ export function personaEventTypeLabel(eventType: unknown, t: UiKitTranslate = defaultTranslate): string {
329
343
  const resolvedEventType = validateExactString(
330
344
  'Persona presentation',
331
345
  'event type',
332
346
  eventType,
333
347
  PERSONA_EVENT_TYPES,
334
348
  )
335
- return PERSONA_EVENT_TYPE_LABELS[resolvedEventType]
349
+ return t(PERSONA_EVENT_TYPE_LABELS[resolvedEventType])
336
350
  }
337
351
 
338
352
  export function personaPayloadMessage(payload: Record<string, unknown> | null): string {
@@ -344,20 +358,36 @@ export function personaPayloadMessage(payload: Record<string, unknown> | null):
344
358
  return ''
345
359
  }
346
360
 
347
- export function personaDistinctMessage(title: string, message: string | null | undefined): string | null {
361
+ export function personaDistinctMessage(
362
+ title: string,
363
+ message: string | null | undefined,
364
+ locale: UiKitLocale = 'ru',
365
+ ): string | null {
348
366
  const normalizedMessage = message?.trim()
349
367
  if (!normalizedMessage) return null
350
- const comparable = (value: string) => value.toLocaleLowerCase('ru-RU').replace(/[.!?]+$/u, '').trim()
368
+ const comparable = (value: string) => value.toLocaleLowerCase(locale).replace(/[.!?]+$/u, '').trim()
351
369
  return comparable(title) === comparable(normalizedMessage) ? null : normalizedMessage
352
370
  }
353
371
 
354
- export function personaDurationLabel(durationMs: number | null | undefined): string {
355
- if (durationMs === null || durationMs === undefined) return 'длительность не определена'
356
- if (durationMs < 1000) return `${durationMs} мс`
357
- return `${(durationMs / 1000).toLocaleString('ru-RU', { maximumFractionDigits: 2 })} с`
372
+ export function personaDurationLabel(
373
+ durationMs: number | null | undefined,
374
+ t: UiKitTranslate = defaultTranslate,
375
+ locale: UiKitLocale = 'ru',
376
+ ): string {
377
+ if (durationMs === null || durationMs === undefined) return t('persona.durationUnknown')
378
+ if (durationMs < 1000) return t('persona.durationMilliseconds', { value: durationMs })
379
+ return t('persona.durationSeconds', {
380
+ value: (durationMs / 1000).toLocaleString(locale, { maximumFractionDigits: 2 }),
381
+ })
358
382
  }
359
383
 
360
- export function personaCountLabel(count: number, forms: [string, string, string]): string {
384
+ export function personaCountLabel(
385
+ count: number,
386
+ forms: [string, string, string],
387
+ locale: UiKitLocale = 'ru',
388
+ ): string {
389
+ if (locale === 'en') return `${count} ${count === 1 ? forms[0] : forms[2]}`
390
+ if (locale === 'zh-CN' || locale === 'zh-TW') return `${count} ${forms[2]}`
361
391
  const remainder100 = count % 100
362
392
  const remainder10 = count % 10
363
393
  const form = remainder100 >= 11 && remainder100 <= 14
@@ -15,6 +15,7 @@ export interface FloatingPositionOptions {
15
15
  boundaryPadding?: () => number
16
16
  flip?: () => boolean
17
17
  shift?: () => boolean
18
+ constrainMainAxis?: () => boolean
18
19
  widthMatch?: () => FloatingWidthMatch
19
20
  zIndex?: () => string
20
21
  scrollStrategy?: 'reposition' | 'close'
@@ -141,9 +142,44 @@ export function useFloatingPosition(options: FloatingPositionOptions): FloatingP
141
142
  }
142
143
  }
143
144
 
145
+ let positionedPanelWidth = panelWidth
146
+ let positionedPanelHeight = panelHeight
147
+ const constrainedStyle: Record<string, string> = {}
148
+ if (options.constrainMainAxis?.() ?? false) {
149
+ if (side === 'top' || side === 'bottom') {
150
+ const availableHeight = side === 'top'
151
+ ? anchor.top - offset - padding
152
+ : viewportHeight - padding - anchor.bottom - offset
153
+ positionedPanelHeight = Math.min(panelHeight, Math.max(0, Math.floor(availableHeight)))
154
+ constrainedStyle.maxHeight = `${positionedPanelHeight}px`
155
+ constrainedStyle.overflowY = 'auto'
156
+ } else {
157
+ const availableWidth = side === 'left'
158
+ ? anchor.left - offset - padding
159
+ : viewportWidth - padding - anchor.right - offset
160
+ positionedPanelWidth = Math.min(panelWidth, Math.max(0, Math.floor(availableWidth)))
161
+ constrainedStyle.maxWidth = `${positionedPanelWidth}px`
162
+ constrainedStyle.overflowX = 'auto'
163
+ }
164
+ coordinates = coordinatesFor(
165
+ side,
166
+ alignment,
167
+ anchor,
168
+ positionedPanelWidth,
169
+ positionedPanelHeight,
170
+ offset,
171
+ )
172
+ }
173
+
144
174
  if (options.shift?.() ?? true) {
145
- coordinates.left = Math.max(padding, Math.min(coordinates.left, viewportWidth - panelWidth - padding))
146
- coordinates.top = Math.max(padding, Math.min(coordinates.top, viewportHeight - panelHeight - padding))
175
+ coordinates.left = Math.max(
176
+ padding,
177
+ Math.min(coordinates.left, viewportWidth - positionedPanelWidth - padding),
178
+ )
179
+ coordinates.top = Math.max(
180
+ padding,
181
+ Math.min(coordinates.top, viewportHeight - positionedPanelHeight - padding),
182
+ )
147
183
  }
148
184
 
149
185
  const placement = `${side}-${alignment}` as FloatingPlacement
@@ -158,6 +194,7 @@ export function useFloatingPosition(options: FloatingPositionOptions): FloatingP
158
194
  left: `${resolvedCoordinates.value.left}px`,
159
195
  top: `${resolvedCoordinates.value.top}px`,
160
196
  zIndex: options.zIndex?.() ?? 'var(--s-layer-root-floating)',
197
+ ...constrainedStyle,
161
198
  ...(widthMatch === 'min' ? { minWidth: `${Math.round(anchor.width)}px` } : {}),
162
199
  ...(widthMatch === 'exact' ? { width: `${Math.round(anchor.width)}px` } : {}),
163
200
  }
@@ -0,0 +1,17 @@
1
+ import { resolveUiKitLocaleContext, type UiKitLocaleContext } from '../internal/localeContext'
2
+
3
+ /** Returns the immutable locale contract scoped to the current Vue application subtree. */
4
+ export function useUiKitLocale(): UiKitLocaleContext {
5
+ return resolveUiKitLocaleContext()
6
+ }
7
+
8
+ export type {
9
+ UiKitLocale,
10
+ UiKitMessageKey,
11
+ UiKitMessageParameters,
12
+ UiKitMessages,
13
+ UiKitMessagesOverride,
14
+ UiKitTranslate,
15
+ } from '../locales/locale'
16
+ export { UI_KIT_LOCALES } from '../locales/locale'
17
+ export { UI_KIT_MESSAGES, UI_KIT_MESSAGE_KEYS } from '../locales/messages'
@@ -0,0 +1,110 @@
1
+ import hljs from 'highlight.js/lib/core'
2
+ import bash from 'highlight.js/lib/languages/bash'
3
+ import dockerfile from 'highlight.js/lib/languages/dockerfile'
4
+ import javascript from 'highlight.js/lib/languages/javascript'
5
+ import makefile from 'highlight.js/lib/languages/makefile'
6
+ import nginx from 'highlight.js/lib/languages/nginx'
7
+ import powershell from 'highlight.js/lib/languages/powershell'
8
+ import python from 'highlight.js/lib/languages/python'
9
+ import typescript from 'highlight.js/lib/languages/typescript'
10
+ import xml from 'highlight.js/lib/languages/xml'
11
+ import yaml from 'highlight.js/lib/languages/yaml'
12
+
13
+ import type { CodeEditorLanguageContract } from './codeEditorContract'
14
+ import { resolveCodeLanguageKey } from './codeLanguageIdentity'
15
+
16
+ export type CodeHighlightToken = string | {
17
+ readonly scope: string | null
18
+ readonly children: readonly CodeHighlightToken[]
19
+ }
20
+
21
+ const HIGHLIGHT_LANGUAGE_ALIASES: Readonly<Record<string, string>> = {
22
+ shell: 'bash',
23
+ text: 'plaintext',
24
+ }
25
+
26
+ const SAFE_SCOPE_PATTERN = /^[a-z][a-z0-9_.-]*$/u
27
+
28
+ hljs.registerLanguage('bash', bash)
29
+ hljs.registerLanguage('dockerfile', dockerfile)
30
+ hljs.registerLanguage('javascript', javascript)
31
+ hljs.registerLanguage('makefile', makefile)
32
+ hljs.registerLanguage('nginx', nginx)
33
+ hljs.registerLanguage('powershell', powershell)
34
+ hljs.registerLanguage('python', python)
35
+ hljs.registerLanguage('typescript', typescript)
36
+ hljs.registerLanguage('xml', xml)
37
+ hljs.registerLanguage('yaml', yaml)
38
+
39
+ function escapeHighlightedCode(code: string): string {
40
+ return code
41
+ .replace(/&/gu, '&amp;')
42
+ .replace(/</gu, '&lt;')
43
+ .replace(/>/gu, '&gt;')
44
+ .replace(/"/gu, '&quot;')
45
+ .replace(/'/gu, '&#39;')
46
+ }
47
+
48
+ function resolveHighlightLanguage(language: CodeEditorLanguageContract): string {
49
+ const languageKey = resolveCodeLanguageKey(language)
50
+ return HIGHLIGHT_LANGUAGE_ALIASES[languageKey] ?? languageKey
51
+ }
52
+
53
+ function isRecord(value: unknown): value is Record<string, unknown> {
54
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
55
+ }
56
+
57
+ function readTokenNode(value: unknown, coordinate: string): CodeHighlightToken {
58
+ if (typeof value === 'string') return value
59
+ if (!isRecord(value)) throw new TypeError(`codeHighlight: ${coordinate} must be a token node or string.`)
60
+ const unexpected = Object.keys(value).filter((key) => !['scope', 'children', 'language'].includes(key))
61
+ if (unexpected.length > 0) {
62
+ throw new TypeError(`codeHighlight: ${coordinate} contains unknown fields ${unexpected.join(', ')}.`)
63
+ }
64
+ const rawScope = value.scope
65
+ if (rawScope !== undefined && (typeof rawScope !== 'string' || !SAFE_SCOPE_PATTERN.test(rawScope))) {
66
+ throw new TypeError(`codeHighlight: ${coordinate}.scope is invalid.`)
67
+ }
68
+ if (!Array.isArray(value.children)) {
69
+ throw new TypeError(`codeHighlight: ${coordinate}.children must be an array.`)
70
+ }
71
+ return Object.freeze({
72
+ scope: rawScope ?? null,
73
+ children: Object.freeze(value.children.map((child, index) => readTokenNode(child, `${coordinate}.children[${index}]`))),
74
+ })
75
+ }
76
+
77
+ function highlightResult(code: string, language: CodeEditorLanguageContract) {
78
+ const resolvedLanguage = resolveHighlightLanguage(language)
79
+ if (!hljs.getLanguage(resolvedLanguage)) return null
80
+ return hljs.highlight(code, { language: resolvedLanguage, ignoreIllegals: true })
81
+ }
82
+
83
+ export function isCodeHighlightLanguageSupported(language: CodeEditorLanguageContract): boolean {
84
+ return Boolean(hljs.getLanguage(resolveHighlightLanguage(language)))
85
+ }
86
+
87
+ /** Returns trusted library markup only for the legacy sanitized-document pipeline. */
88
+ export function highlightCodeToHtml(code: string, language: CodeEditorLanguageContract): string {
89
+ return highlightResult(code, language)?.value ?? escapeHighlightedCode(code)
90
+ }
91
+
92
+ /** Converts the Highlight.js emitter boundary into a closed, validated token tree without HTML rendering. */
93
+ export function highlightCodeToTokens(
94
+ code: string,
95
+ language: CodeEditorLanguageContract,
96
+ ): readonly CodeHighlightToken[] {
97
+ const result = highlightResult(code, language)
98
+ if (result === null) return Object.freeze([code])
99
+ const root = Reflect.get(result._emitter, 'root')
100
+ const validated = readTokenNode(root, 'root')
101
+ if (typeof validated === 'string') {
102
+ throw new TypeError('codeHighlight: emitter root must be a token node.')
103
+ }
104
+ return validated.children
105
+ }
106
+
107
+ export function highlightScopeClass(scope: string): string {
108
+ if (!SAFE_SCOPE_PATTERN.test(scope)) throw new TypeError(`codeHighlight: invalid scope ${JSON.stringify(scope)}.`)
109
+ return `hljs-${scope.replace(/\./gu, '_')}`
110
+ }