@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
@@ -0,0 +1,150 @@
1
+ import type {
2
+ UiKitLocale,
3
+ UiKitMessageKey,
4
+ UiKitMessageParameters,
5
+ UiKitMessages,
6
+ UiKitMessagesOverride,
7
+ UiKitParameterizedMessageKey,
8
+ UiKitStaticMessageKey,
9
+ UiKitTranslate,
10
+ } from './locale'
11
+ import { UI_KIT_MESSAGE_SCHEMA, UI_KIT_MESSAGE_VALUES } from './messages.generated'
12
+ import { hasOwn } from '../internal/es2020'
13
+
14
+ type MessageRecord = Readonly<Record<string, unknown>>
15
+
16
+ function isRecord(value: unknown): value is MessageRecord {
17
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
18
+ }
19
+
20
+ function deepFreeze<T>(value: T): Readonly<T> {
21
+ if (!isRecord(value)) return value
22
+ for (const child of Object.values(value)) deepFreeze(child)
23
+ return Object.freeze(value)
24
+ }
25
+
26
+ function buildMessages(locale: UiKitLocale, values: readonly string[]): Readonly<UiKitMessages> {
27
+ const bundle: Record<string, Record<string, string>> = {}
28
+ let index = 0
29
+ for (const [groupName, messageNames] of UI_KIT_MESSAGE_SCHEMA) {
30
+ const group: Record<string, string> = {}
31
+ for (const messageName of messageNames) {
32
+ const message = values[index]
33
+ if (typeof message !== 'string' || message.trim().length === 0) {
34
+ throw new TypeError(`UI-kit locale ${locale}: message ${groupName}.${messageName} отсутствует.`)
35
+ }
36
+ group[messageName] = message
37
+ index += 1
38
+ }
39
+ bundle[groupName] = group
40
+ }
41
+ if (index !== values.length) {
42
+ throw new TypeError(`UI-kit locale ${locale}: compact catalog содержит лишние значения.`)
43
+ }
44
+ return deepFreeze(bundle) as Readonly<UiKitMessages>
45
+ }
46
+
47
+ function messageAtPath(messages: unknown, key: string): string {
48
+ let current: unknown = messages
49
+ for (const segment of key.split('.')) {
50
+ if (!isRecord(current) || !hasOwn(current, segment)) {
51
+ throw new TypeError(`UI-kit locale: неизвестный message key ${JSON.stringify(key)}.`)
52
+ }
53
+ current = current[segment]
54
+ }
55
+ if (typeof current !== 'string') {
56
+ throw new TypeError(`UI-kit locale: message key ${JSON.stringify(key)} не является строкой.`)
57
+ }
58
+ return current
59
+ }
60
+
61
+ function placeholders(message: string): readonly string[] {
62
+ return [...message.matchAll(/\{([A-Za-z][A-Za-z0-9]*)\}/gu)]
63
+ .map((match) => match[1] ?? '')
64
+ .filter((value, index, values) => value.length > 0 && values.indexOf(value) === index)
65
+ .sort()
66
+ }
67
+
68
+ const schemaKeys = Object.freeze(UI_KIT_MESSAGE_SCHEMA.flatMap(([groupName, messageNames]) => (
69
+ messageNames.map((messageName) => `${groupName}.${messageName}` as UiKitMessageKey)
70
+ )))
71
+ const schemaKeySet = new Set<string>(schemaKeys)
72
+
73
+ /** Complete immutable message bundles for every supported locale. */
74
+ export const UI_KIT_MESSAGES: Readonly<Record<UiKitLocale, Readonly<UiKitMessages>>> = deepFreeze({
75
+ en: buildMessages('en', UI_KIT_MESSAGE_VALUES.en),
76
+ ru: buildMessages('ru', UI_KIT_MESSAGE_VALUES.ru),
77
+ 'zh-CN': buildMessages('zh-CN', UI_KIT_MESSAGE_VALUES['zh-CN']),
78
+ 'zh-TW': buildMessages('zh-TW', UI_KIT_MESSAGE_VALUES['zh-TW']),
79
+ })
80
+
81
+ function mergeRecord(base: MessageRecord, override: unknown, coordinate: string): MessageRecord {
82
+ if (!isRecord(override)) {
83
+ throw new TypeError(`${coordinate} должен быть object.`)
84
+ }
85
+ const unexpected = Object.keys(override).filter((key) => !hasOwn(base, key))
86
+ if (unexpected.length > 0) {
87
+ throw new TypeError(`${coordinate}: неизвестные поля ${unexpected.join(', ')}.`)
88
+ }
89
+ const merged: Record<string, unknown> = { ...base }
90
+ for (const [key, value] of Object.entries(override)) {
91
+ const path = `${coordinate}.${key}`
92
+ const baseValue = base[key]
93
+ if (typeof baseValue === 'string') {
94
+ if (typeof value !== 'string' || value.trim().length === 0) {
95
+ throw new TypeError(`${path} должен быть непустой строкой.`)
96
+ }
97
+ if (placeholders(value).join('\n') !== placeholders(baseValue).join('\n')) {
98
+ throw new TypeError(`${path} должен сохранять placeholders ${placeholders(baseValue).join(', ')}.`)
99
+ }
100
+ merged[key] = value
101
+ } else if (isRecord(baseValue)) {
102
+ merged[key] = mergeRecord(baseValue, value, path)
103
+ } else {
104
+ throw new TypeError(`${path}: base schema повреждена.`)
105
+ }
106
+ }
107
+ return merged
108
+ }
109
+
110
+ /** Validates and immutably applies a provider-level exact deep override. */
111
+ export function mergeUiKitMessages(
112
+ base: Readonly<UiKitMessages>,
113
+ override: UiKitMessagesOverride | undefined,
114
+ ): Readonly<UiKitMessages> {
115
+ if (override === undefined) return base
116
+ return deepFreeze(mergeRecord(base, override, 'SUiKitLocaleProvider.messages')) as Readonly<UiKitMessages>
117
+ }
118
+
119
+ function translate(
120
+ messages: Readonly<UiKitMessages>,
121
+ key: UiKitMessageKey,
122
+ parameters?: Readonly<Record<string, string | number>>,
123
+ ): string {
124
+ if (!schemaKeySet.has(key)) throw new TypeError(`UI-kit locale: неизвестный message key ${JSON.stringify(key)}.`)
125
+ const template = messageAtPath(messages, key)
126
+ const required = placeholders(template)
127
+ const received = parameters === undefined ? [] : Object.keys(parameters).sort()
128
+ if (required.join('\n') !== received.join('\n')) {
129
+ throw new TypeError(
130
+ `UI-kit locale: ${key} ожидает параметры [${required.join(', ')}], получено [${received.join(', ')}].`,
131
+ )
132
+ }
133
+ return template.replace(/\{([A-Za-z][A-Za-z0-9]*)\}/gu, (_match, name: string) => String(parameters?.[name]))
134
+ }
135
+
136
+ /** Creates a strict translator bound to a reactive message accessor. */
137
+ export function createUiKitTranslate(messages: () => Readonly<UiKitMessages>): UiKitTranslate {
138
+ function t<Key extends UiKitStaticMessageKey>(key: Key): string
139
+ function t<Key extends UiKitParameterizedMessageKey>(key: Key, parameters: UiKitMessageParameters[Key]): string
140
+ function t(
141
+ key: UiKitMessageKey,
142
+ parameters?: Readonly<Record<string, string | number>>,
143
+ ): string {
144
+ return translate(messages(), key, parameters)
145
+ }
146
+ return t
147
+ }
148
+
149
+ /** Stable complete key inventory used by package and locale parity proofs. */
150
+ export const UI_KIT_MESSAGE_KEYS: readonly UiKitMessageKey[] = schemaKeys
@@ -1,4 +1,5 @@
1
1
  import { defineStore } from 'pinia'
2
+ import { onScopeDispose, ref } from 'vue'
2
3
  import { v4 as uuidv4 } from 'uuid'
3
4
  import type { FeedbackSeverity } from '../internal/feedbackPresentation'
4
5
 
@@ -16,54 +17,70 @@ export interface Notification {
16
17
 
17
18
  export type NotificationInput = Omit<Notification, 'id' | 'createdAt'>
18
19
 
19
- interface NotifierState {
20
- notifications: Notification[]
21
- history: Notification[]
22
- }
23
-
24
20
  const DEFAULT_NOTIFICATION_DURATION_MS = 2800
25
- const removalTimers = new Map<string, ReturnType<typeof setTimeout>>()
21
+ /** Maximum number of records retained by the autonomous notification history. */
22
+ export const NOTIFIER_HISTORY_CAPACITY = 100
26
23
 
27
- function stopRemovalTimer(id: string): void {
28
- const timer = removalTimers.get(id)
29
- if (!timer) {
30
- return
24
+ export const useNotifierStore = defineStore('notifier', () => {
25
+ const notifications = ref<Notification[]>([])
26
+ const history = ref<Notification[]>([])
27
+ const removalTimers = new Map<string, ReturnType<typeof setTimeout>>()
28
+
29
+ function stopRemovalTimer(id: string): void {
30
+ const timer = removalTimers.get(id)
31
+ if (!timer) return
32
+ clearTimeout(timer)
33
+ removalTimers.delete(id)
34
+ }
35
+
36
+ function clearRemovalTimers(): void {
37
+ for (const timer of removalTimers.values()) clearTimeout(timer)
38
+ removalTimers.clear()
39
+ }
40
+
41
+ function remove(id: string): void {
42
+ stopRemovalTimer(id)
43
+ notifications.value = notifications.value.filter((notification) => notification.id !== id)
31
44
  }
32
- clearTimeout(timer)
33
- removalTimers.delete(id)
34
- }
35
45
 
36
- export const useNotifierStore = defineStore('notifier', {
37
- state: (): NotifierState => ({
38
- notifications: [],
39
- history: [],
40
- }),
41
- actions: {
42
- add(notification: NotificationInput) {
43
- const id = uuidv4()
44
- const newNotification: Notification = {
45
- id,
46
- createdAt: new Date().toISOString(),
47
- duration: DEFAULT_NOTIFICATION_DURATION_MS,
48
- ...notification,
49
- }
46
+ function add(notification: NotificationInput): void {
47
+ const id = uuidv4()
48
+ const newNotification: Notification = {
49
+ id,
50
+ createdAt: new Date().toISOString(),
51
+ duration: DEFAULT_NOTIFICATION_DURATION_MS,
52
+ ...notification,
53
+ }
50
54
 
51
- this.history.unshift(newNotification)
52
- this.notifications.push(newNotification)
55
+ history.value.unshift(newNotification)
56
+ if (history.value.length > NOTIFIER_HISTORY_CAPACITY) {
57
+ history.value.splice(NOTIFIER_HISTORY_CAPACITY)
58
+ }
59
+ notifications.value.push(newNotification)
53
60
 
54
- if (newNotification.duration) {
55
- const removalTimer = setTimeout(() => {
56
- this.remove(id)
57
- }, newNotification.duration)
58
- removalTimers.set(id, removalTimer)
59
- }
60
- },
61
- remove(id: string) {
62
- stopRemovalTimer(id)
63
- this.notifications = this.notifications.filter((n) => n.id !== id)
64
- },
65
- clearHistory() {
66
- this.history = []
67
- },
68
- },
61
+ if (newNotification.duration) {
62
+ removalTimers.set(id, setTimeout(() => remove(id), newNotification.duration))
63
+ }
64
+ }
65
+
66
+ function clearHistory(): void {
67
+ history.value = []
68
+ }
69
+
70
+ function $reset(): void {
71
+ clearRemovalTimers()
72
+ notifications.value = []
73
+ history.value = []
74
+ }
75
+
76
+ onScopeDispose(clearRemovalTimers)
77
+
78
+ return {
79
+ notifications,
80
+ history,
81
+ add,
82
+ remove,
83
+ clearHistory,
84
+ $reset,
85
+ }
69
86
  })
@@ -111,11 +111,15 @@
111
111
  --safe-area-left: env(safe-area-inset-left, 0px);
112
112
 
113
113
  /* Semantic interaction geometry */
114
- --s-control-hitbox-xs: 1.5rem;
115
- --s-control-hitbox-sm: 1.75rem;
114
+ --s-control-hitbox-2xs: 1.5rem;
115
+ --s-control-hitbox-xs: 2rem;
116
+ --s-control-hitbox-sm: 2.25rem;
116
117
  --s-control-hitbox-md: 2.5rem;
117
118
  --s-control-hitbox-lg: 3rem;
118
119
  --s-control-indicator-size: 1.25rem;
120
+ --s-resize-handle-hitbox-compact: 0.375rem;
121
+ --s-resize-handle-hitbox-standard: 0.75rem;
122
+ --s-resize-handle-hitbox-spacious: 2rem;
119
123
  --s-icon-size-2xs: 0.75rem;
120
124
  --s-icon-size-xs: 1rem;
121
125
  --s-icon-size-sm: 1.25rem;
@@ -264,9 +268,26 @@
264
268
  --s-code-editor-viewport-compact: 12rem;
265
269
  --s-code-editor-viewport-standard: 20rem;
266
270
  --s-code-editor-viewport-expanded: 32rem;
271
+ --s-diff-row-block-size: 2rem;
272
+ --s-diff-surface: var(--color-surface);
273
+ --s-diff-gutter-surface: color-mix(in srgb, var(--color-surface-muted) 78%, transparent);
274
+ --s-diff-added-surface: color-mix(in srgb, var(--color-success-500) 13%, transparent);
275
+ --s-diff-added-accent: var(--color-success-600);
276
+ --s-diff-removed-surface: color-mix(in srgb, var(--color-danger-500) 12%, transparent);
277
+ --s-diff-removed-accent: var(--color-danger-600);
278
+ --s-diff-context-surface: color-mix(in srgb, var(--color-info-500) 7%, var(--color-surface));
279
+ --s-code-token-comment: var(--color-text-muted);
280
+ --s-code-token-keyword: var(--color-danger-600);
281
+ --s-code-token-title: var(--color-primary-600);
282
+ --s-code-token-literal: var(--color-warn-700);
283
+ --s-code-token-string: var(--color-success-700);
284
+ --s-code-token-meta: var(--color-info-700);
267
285
  --s-code-editor-search-match-background: rgb(99 102 241 / 22%);
268
286
  --s-code-editor-search-match-selected-background: rgb(16 185 129 / 35%);
269
287
  --s-code-editor-search-match-selected-outline: rgb(16 185 129 / 55%);
288
+ --s-code-editor-diagnostic-error: var(--color-danger-500);
289
+ --s-code-editor-diagnostic-warning: var(--color-warn-500);
290
+ --s-code-editor-diagnostic-info: var(--color-info-500);
270
291
 
271
292
  /* Semantic chart palette and geometry surfaces */
272
293
  --s-chart-series-1: var(--color-primary-600);
package/src/theme.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import type { UiKitTranslate } from './locales/locale';
2
+ import { createUiKitTranslate, UI_KIT_MESSAGES } from './locales/messages';
3
+
1
4
  export const themeFamilies = ['classic', 'pink', 'green'] as const;
2
5
  export type ThemeFamily = (typeof themeFamilies)[number];
3
6
 
@@ -28,6 +31,37 @@ export interface ThemeVariant {
28
31
 
29
32
  export const defaultThemeId: ThemeId = 'classic-light';
30
33
 
34
+ const defaultThemeTranslate = createUiKitTranslate(() => UI_KIT_MESSAGES.ru);
35
+
36
+ function themeFamilyMessageKey(family: ThemeFamily): 'theme.classic' | 'theme.pink' | 'theme.green' {
37
+ if (family === 'classic') return 'theme.classic';
38
+ if (family === 'pink') return 'theme.pink';
39
+ if (family === 'green') return 'theme.green';
40
+ throw new Error(`Неизвестное семейство темы: ${String(family)}`);
41
+ }
42
+
43
+ function themeModeMessageKey(mode: ThemeMode): 'theme.light' | 'theme.dark' {
44
+ if (mode === 'light') return 'theme.light';
45
+ if (mode === 'dark') return 'theme.dark';
46
+ throw new Error(`Неизвестный режим темы: ${String(mode)}`);
47
+ }
48
+
49
+ function localizeThemeVariant(
50
+ family: ThemeFamily,
51
+ mode: ThemeMode,
52
+ translate: UiKitTranslate,
53
+ ): Pick<ThemeVariant, 'label' | 'familyLabel' | 'modeLabel'> {
54
+ const familyLabel = translate(themeFamilyMessageKey(family));
55
+ const modeLabel = translate(themeModeMessageKey(mode));
56
+ return {
57
+ familyLabel,
58
+ modeLabel,
59
+ label: translate(mode === 'light' ? 'theme.variantLight' : 'theme.variantDark', {
60
+ family: familyLabel,
61
+ }),
62
+ };
63
+ }
64
+
31
65
  export const themeVariants = [
32
66
  {
33
67
  id: 'classic-light',
@@ -36,9 +70,7 @@ export const themeVariants = [
36
70
  dataTheme: 'light',
37
71
  browserThemeColor: '#f8fafc',
38
72
  statusBarStyle: 'default',
39
- label: 'Классическая светлая',
40
- familyLabel: 'Классическая',
41
- modeLabel: 'Светлая',
73
+ ...localizeThemeVariant('classic', 'light', defaultThemeTranslate),
42
74
  },
43
75
  {
44
76
  id: 'classic-dark',
@@ -47,9 +79,7 @@ export const themeVariants = [
47
79
  dataTheme: 'dark',
48
80
  browserThemeColor: '#020617',
49
81
  statusBarStyle: 'black-translucent',
50
- label: 'Классическая тёмная',
51
- familyLabel: 'Классическая',
52
- modeLabel: 'Тёмная',
82
+ ...localizeThemeVariant('classic', 'dark', defaultThemeTranslate),
53
83
  },
54
84
  {
55
85
  id: 'pink-light',
@@ -58,9 +88,7 @@ export const themeVariants = [
58
88
  dataTheme: 'pink-light',
59
89
  browserThemeColor: '#fff6fb',
60
90
  statusBarStyle: 'default',
61
- label: 'Розовая светлая',
62
- familyLabel: 'Розовая',
63
- modeLabel: 'Светлая',
91
+ ...localizeThemeVariant('pink', 'light', defaultThemeTranslate),
64
92
  },
65
93
  {
66
94
  id: 'pink-dark',
@@ -69,9 +97,7 @@ export const themeVariants = [
69
97
  dataTheme: 'pink-dark',
70
98
  browserThemeColor: '#020617',
71
99
  statusBarStyle: 'black-translucent',
72
- label: 'Розовая тёмная',
73
- familyLabel: 'Розовая',
74
- modeLabel: 'Тёмная',
100
+ ...localizeThemeVariant('pink', 'dark', defaultThemeTranslate),
75
101
  },
76
102
  {
77
103
  id: 'green-light',
@@ -80,9 +106,7 @@ export const themeVariants = [
80
106
  dataTheme: 'green-light',
81
107
  browserThemeColor: '#f5fbf6',
82
108
  statusBarStyle: 'default',
83
- label: 'Зелёная светлая',
84
- familyLabel: 'Зелёная',
85
- modeLabel: 'Светлая',
109
+ ...localizeThemeVariant('green', 'light', defaultThemeTranslate),
86
110
  },
87
111
  {
88
112
  id: 'green-dark',
@@ -91,9 +115,7 @@ export const themeVariants = [
91
115
  dataTheme: 'green-dark',
92
116
  browserThemeColor: '#04120b',
93
117
  statusBarStyle: 'black-translucent',
94
- label: 'Зелёная тёмная',
95
- familyLabel: 'Зелёная',
96
- modeLabel: 'Тёмная',
118
+ ...localizeThemeVariant('green', 'dark', defaultThemeTranslate),
97
119
  },
98
120
  ] as const satisfies readonly ThemeVariant[];
99
121
 
@@ -417,22 +439,27 @@ export function resolveThemePreference(themeId: ThemeId): ThemePreference {
417
439
  return getThemeVariant(themeId).dataTheme;
418
440
  }
419
441
 
420
- export function getThemeFamilyLabel(family: ThemeFamily): string {
421
- const variant = themeVariants.find((item) => item.family === family);
422
- if (variant === undefined) {
423
- throw new Error(`Неизвестное семейство темы: ${family}`);
424
- }
425
- return variant.familyLabel;
442
+ export function getThemeFamilyLabel(
443
+ family: ThemeFamily,
444
+ translate: UiKitTranslate = defaultThemeTranslate,
445
+ ): string {
446
+ return translate(themeFamilyMessageKey(family));
426
447
  }
427
448
 
428
- export function getThemeModeLabel(mode: ThemeMode): string {
429
- if (mode === 'light') {
430
- return 'Светлая';
431
- }
432
- if (mode === 'dark') {
433
- return 'Тёмная';
434
- }
435
- throw new Error(`Неизвестный режим темы: ${String(mode)}`);
449
+ export function getThemeModeLabel(
450
+ mode: ThemeMode,
451
+ translate: UiKitTranslate = defaultThemeTranslate,
452
+ ): string {
453
+ return translate(themeModeMessageKey(mode));
454
+ }
455
+
456
+ /** Returns a locale-scoped display label without coupling theme state to one application locale. */
457
+ export function getThemeVariantLabel(
458
+ themeId: ThemeId,
459
+ translate: UiKitTranslate = defaultThemeTranslate,
460
+ ): string {
461
+ const variant = getThemeVariant(themeId);
462
+ return localizeThemeVariant(variant.family, variant.mode, translate).label;
436
463
  }
437
464
 
438
465
  export function resolveLegacyThemeId(value: unknown): ThemeId | null {
@@ -0,0 +1,73 @@
1
+ import { computeDiffModel, DiffEngineCancelledError } from '../internal/diffEngine'
2
+ import {
3
+ S_DIFF_WORKER_PROTOCOL_VERSION,
4
+ validateDiffWorkerRequest,
5
+ type SDiffWorkerResponse,
6
+ } from './diffProtocol'
7
+
8
+ interface DiffWorkerScope {
9
+ addEventListener(type: 'message', listener: (event: MessageEvent<unknown>) => void): void
10
+ postMessage(message: SDiffWorkerResponse): void
11
+ }
12
+
13
+ const workerScope = self as unknown as DiffWorkerScope
14
+ const cancelledRequestIds = new Set<string>()
15
+
16
+ function errorResponse(requestId: string, cause: unknown): SDiffWorkerResponse {
17
+ const error = cause instanceof Error ? cause : new Error(String(cause))
18
+ return {
19
+ version: S_DIFF_WORKER_PROTOCOL_VERSION,
20
+ type: 'error',
21
+ requestId,
22
+ error: { name: error.name, message: error.message },
23
+ }
24
+ }
25
+
26
+ workerScope.addEventListener('message', (event) => {
27
+ let requestId = 'invalid-request'
28
+ try {
29
+ const request = validateDiffWorkerRequest(event.data)
30
+ requestId = request.requestId
31
+ if (request.type === 'cancel') {
32
+ cancelledRequestIds.add(request.requestId)
33
+ workerScope.postMessage({
34
+ version: S_DIFF_WORKER_PROTOCOL_VERSION,
35
+ type: 'cancelled',
36
+ requestId: request.requestId,
37
+ })
38
+ return
39
+ }
40
+ const result = computeDiffModel(
41
+ request.before,
42
+ request.after,
43
+ request.options,
44
+ () => cancelledRequestIds.has(request.requestId),
45
+ )
46
+ if (cancelledRequestIds.delete(request.requestId)) {
47
+ workerScope.postMessage({
48
+ version: S_DIFF_WORKER_PROTOCOL_VERSION,
49
+ type: 'cancelled',
50
+ requestId: request.requestId,
51
+ })
52
+ return
53
+ }
54
+ workerScope.postMessage({
55
+ version: S_DIFF_WORKER_PROTOCOL_VERSION,
56
+ type: 'result',
57
+ requestId: request.requestId,
58
+ result,
59
+ })
60
+ } catch (cause) {
61
+ if (cause instanceof DiffEngineCancelledError) {
62
+ workerScope.postMessage({
63
+ version: S_DIFF_WORKER_PROTOCOL_VERSION,
64
+ type: 'cancelled',
65
+ requestId,
66
+ })
67
+ return
68
+ }
69
+ workerScope.postMessage(errorResponse(requestId, cause))
70
+ } finally {
71
+ cancelledRequestIds.delete(requestId)
72
+ }
73
+ })