@pgcorp/ui-kit 0.9.0 → 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 (105) hide show
  1. package/README.md +188 -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/package.json +103 -50
  7. package/src/components/layout/SAppShell.vue +3 -1
  8. package/src/components/layout/SDockRegion.vue +5 -3
  9. package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
  10. package/src/components/shared/_internal/SDialogSurface.css +5 -0
  11. package/src/components/shared/_internal/SDialogSurface.vue +3 -1
  12. package/src/components/shared/_internal/SToastItem.css +4 -0
  13. package/src/components/shared/_internal/SToastItem.vue +44 -8
  14. package/src/components/shared/_internal/STreeNode.vue +24 -11
  15. package/src/components/shared/complex/SKanbanLane.vue +4 -2
  16. package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
  17. package/src/components/shared/containers/SDrawer.vue +5 -2
  18. package/src/components/shared/containers/SModal.vue +5 -2
  19. package/src/components/shared/containers/SPanel.vue +4 -2
  20. package/src/components/shared/containers/SPopover.vue +7 -3
  21. package/src/components/shared/containers/SUiKitLocaleProvider.vue +57 -0
  22. package/src/components/shared/controls/SButton.css +14 -10
  23. package/src/components/shared/controls/SCombobox.vue +8 -6
  24. package/src/components/shared/controls/SComboboxTrigger.css +7 -4
  25. package/src/components/shared/controls/SComboboxTrigger.vue +5 -2
  26. package/src/components/shared/controls/SContextToggleButton.vue +6 -4
  27. package/src/components/shared/controls/SDragHandle.vue +8 -4
  28. package/src/components/shared/controls/SDropdownMenu.vue +7 -4
  29. package/src/components/shared/controls/SInputText.css +5 -4
  30. package/src/components/shared/controls/SListbox.vue +4 -2
  31. package/src/components/shared/controls/SSelect.vue +4 -2
  32. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +4 -2
  33. package/src/components/shared/data-display/SChart.vue +7 -5
  34. package/src/components/shared/data-display/SChip.vue +3 -1
  35. package/src/components/shared/data-display/SCodeBlock.css +4 -0
  36. package/src/components/shared/data-display/SCodeBlock.vue +42 -20
  37. package/src/components/shared/data-display/SCodeEditor.css +17 -0
  38. package/src/components/shared/data-display/SCodeEditor.vue +173 -8
  39. package/src/components/shared/data-display/SCodeSearchPanel.vue +48 -33
  40. package/src/components/shared/data-display/SCodeWorkspaceEditor.css +13 -0
  41. package/src/components/shared/data-display/SCodeWorkspaceEditor.vue +355 -0
  42. package/src/components/shared/data-display/SControlledToastViewport.css +89 -0
  43. package/src/components/shared/data-display/SControlledToastViewport.vue +205 -0
  44. package/src/components/shared/data-display/SCopyField.vue +12 -10
  45. package/src/components/shared/data-display/SDiffViewer.css +122 -0
  46. package/src/components/shared/data-display/SDiffViewer.vue +499 -0
  47. package/src/components/shared/data-display/SDocBlock.vue +99 -71
  48. package/src/components/shared/data-display/SExpandableText.vue +10 -8
  49. package/src/components/shared/data-display/SJsonTree.vue +4 -2
  50. package/src/components/shared/data-display/SLinkedSystemsList.vue +4 -2
  51. package/src/components/shared/data-display/SMessage.vue +6 -6
  52. package/src/components/shared/data-display/SRichDocument.css +8 -0
  53. package/src/components/shared/data-display/SRichDocument.vue +65 -0
  54. package/src/components/shared/data-display/SScrollViewport.css +53 -0
  55. package/src/components/shared/data-display/SScrollViewport.vue +470 -0
  56. package/src/components/shared/data-display/STable.vue +38 -19
  57. package/src/components/shared/data-display/SToastContainer.vue +22 -17
  58. package/src/components/shared/data-display/_internal/SCodeTokenLine.css +72 -0
  59. package/src/components/shared/data-display/_internal/SCodeTokenLine.vue +79 -0
  60. package/src/components/shared/data-display/_internal/SRichBlockNode.css +85 -0
  61. package/src/components/shared/data-display/_internal/SRichBlockNode.vue +282 -0
  62. package/src/components/shared/data-display/_internal/SRichInlineNode.css +6 -0
  63. package/src/components/shared/data-display/_internal/SRichInlineNode.vue +76 -0
  64. package/src/components/shared/data-display/_internal/SRichMath.css +42 -0
  65. package/src/components/shared/data-display/_internal/SRichMath.vue +102 -0
  66. package/src/components/shared/data-display/codeEditor.ts +200 -0
  67. package/src/components/shared/data-display/codeWorkspace.ts +233 -0
  68. package/src/components/shared/data-display/diff.ts +541 -0
  69. package/src/components/shared/data-display/richDocument.ts +876 -0
  70. package/src/components/shared/data-display/scrollViewport.ts +47 -0
  71. package/src/components/shared/data-display/toast.ts +194 -0
  72. package/src/components/shared/database/SDataGrid.vue +7 -4
  73. package/src/components/shared/database/SSqlEditor.vue +39 -39
  74. package/src/components/shared/feedback/SAsyncState.vue +3 -1
  75. package/src/components/shared/graph/SGraphViewport.vue +11 -9
  76. package/src/components/shared/navigation/SBreadcrumbs.vue +8 -5
  77. package/src/components/shared/navigation/SCatalogNavigator.vue +26 -20
  78. package/src/components/shared/navigation/STabList.vue +10 -6
  79. package/src/components/shared/navigation/STabs.vue +15 -11
  80. package/src/components/shared/navigation/SWizardSteps.vue +4 -2
  81. package/src/components/shared/persona/SPersonaProjectPicker.vue +31 -27
  82. package/src/components/shared/persona/SPersonaRunWorkbench.vue +39 -33
  83. package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +8 -6
  84. package/src/components/shared/persona/_internal/labels.ts +66 -57
  85. package/src/components/shared/persona/presentation.ts +140 -110
  86. package/src/composables/useFloatingPosition.ts +39 -2
  87. package/src/composables/useUiKitLocale.ts +17 -0
  88. package/src/internal/codeHighlight.ts +110 -0
  89. package/src/internal/diffEngine.ts +464 -0
  90. package/src/internal/diffRuntime.ts +189 -0
  91. package/src/internal/feedbackPresentation.ts +33 -14
  92. package/src/internal/localeContext.ts +22 -0
  93. package/src/internal/ownedAttrs.ts +8 -0
  94. package/src/internal/runtimeContract.ts +11 -9
  95. package/src/internal/semanticSizing.ts +13 -0
  96. package/src/locales/catalog.ts +634 -0
  97. package/src/locales/locale.ts +472 -0
  98. package/src/locales/messages.generated.ts +1826 -0
  99. package/src/locales/messages.ts +150 -0
  100. package/src/stores/useNotifierStore.ts +61 -44
  101. package/src/styles/tokens.css +20 -2
  102. package/src/theme.ts +59 -32
  103. package/src/workers/diff.worker.ts +73 -0
  104. package/src/workers/diffProtocol.ts +155 -0
  105. package/src/components/shared/data-display/SToastContainer.css +0 -53
@@ -13,7 +13,7 @@
13
13
  <span v-if="renderMode" class="s-doc-block-language">{{ headerLabel }}</span>
14
14
  </div>
15
15
  <div class="s-doc-block-actions" v-if="contract.showActions">
16
- <SButton appearance="text" severity="secondary" size="xs" icon-only :leading-icon="Copy" @click="copyContent" title="Копировать" aria-label="Копировать" />
16
+ <SButton appearance="text" severity="secondary" size="xs" icon-only :leading-icon="Copy" @click="copyContent" :title="t('clipboard.copyDocument')" :aria-label="t('clipboard.copyDocument')" />
17
17
  </div>
18
18
  </div>
19
19
 
@@ -38,7 +38,18 @@
38
38
  </template>
39
39
 
40
40
  <script setup lang="ts">
41
- import { computed, defineComponent, h, ref, type Component, type PropType, type VNodeChild } from 'vue';
41
+ import {
42
+ computed,
43
+ defineComponent,
44
+ h,
45
+ onBeforeUnmount,
46
+ ref,
47
+ shallowRef,
48
+ watch,
49
+ type Component,
50
+ type PropType,
51
+ type VNodeChild,
52
+ } from 'vue';
42
53
  import { useOwnedAttrs } from '../../../internal/ownedAttrs';
43
54
  import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract';
44
55
  import { validateBoolean, validateExactString, validateNonEmptyString } from '../../../internal/runtimeContract';
@@ -48,21 +59,9 @@ import SAsyncState from '../feedback/SAsyncState.vue';
48
59
  import { Copy } from '../../icons/sputnigUiIcons';
49
60
  import { useNotifier } from '../../../composables/useNotifier';
50
61
  import { useClipboard } from '../../../composables/useClipboard';
62
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale';
51
63
 
52
- // Markdown rendering with highlight.js similar to MessageBubble
53
64
  import { marked } from 'marked';
54
- import hljs from 'highlight.js/lib/core';
55
- import bash from 'highlight.js/lib/languages/bash';
56
- import dockerfile from 'highlight.js/lib/languages/dockerfile';
57
- import javascript from 'highlight.js/lib/languages/javascript';
58
- import makefile from 'highlight.js/lib/languages/makefile';
59
- import nginx from 'highlight.js/lib/languages/nginx';
60
- import powershell from 'highlight.js/lib/languages/powershell';
61
- import python from 'highlight.js/lib/languages/python';
62
- import typescript from 'highlight.js/lib/languages/typescript';
63
- import xml from 'highlight.js/lib/languages/xml';
64
- import yaml from 'highlight.js/lib/languages/yaml';
65
- import { resolveHighlightLanguageKey } from '../codeLanguages';
66
65
  import {
67
66
  SANITIZED_DOCUMENT_RENDER_ATTRIBUTES,
68
67
  SANITIZED_DOCUMENT_TAGS,
@@ -71,30 +70,10 @@ import {
71
70
 
72
71
  defineOptions({ inheritAttrs: false });
73
72
 
74
- hljs.registerLanguage('bash', bash);
75
- hljs.registerLanguage('dockerfile', dockerfile);
76
- hljs.registerLanguage('javascript', javascript);
77
- hljs.registerLanguage('makefile', makefile);
78
- hljs.registerLanguage('nginx', nginx);
79
- hljs.registerLanguage('powershell', powershell);
80
- hljs.registerLanguage('python', python);
81
- hljs.registerLanguage('typescript', typescript);
82
- hljs.registerLanguage('xml', xml);
83
- hljs.registerLanguage('yaml', yaml);
84
-
85
73
  const ownedAttrs = useOwnedAttrs({ component: 'SDocBlock', owner: 'document block root' });
86
74
 
87
75
  useInteractiveLeafRegistration({ owner: 'SDocBlock' });
88
76
 
89
- function escapeHighlightedCode(code: string): string {
90
- return code
91
- .replace(/&/gu, '&amp;')
92
- .replace(/</gu, '&lt;')
93
- .replace(/>/gu, '&gt;')
94
- .replace(/"/gu, '&quot;')
95
- .replace(/'/gu, '&#39;');
96
- }
97
-
98
77
  function escapeHtmlAttribute(value: string): string {
99
78
  return value
100
79
  .replace(/&/gu, '&amp;')
@@ -104,23 +83,38 @@ function escapeHtmlAttribute(value: string): string {
104
83
  .replace(/'/gu, '&#39;');
105
84
  }
106
85
 
107
- const markdownRenderer = new marked.Renderer();
108
- markdownRenderer.code = ({ text, lang }) => {
109
- const rawLanguage = (lang || '').match(/\S+/)?.[0] ?? '';
110
- const normalizedLanguage = rawLanguage
111
- ? resolveHighlightLanguageKey({ mode: 'explicit', id: rawLanguage })
112
- : null;
113
- const languageClass = rawLanguage ? `language-${escapeHtmlAttribute(rawLanguage.toLowerCase())}` : '';
114
- const isSupportedLanguage = normalizedLanguage !== null && Boolean(hljs.getLanguage(normalizedLanguage));
115
-
116
- const highlightedCode = isSupportedLanguage
117
- ? hljs.highlight(text, { language: normalizedLanguage, ignoreIllegals: true }).value
118
- : escapeHighlightedCode(text);
119
- const classNames = [languageClass, isSupportedLanguage ? 'hljs' : ''].filter(Boolean).join(' ');
120
- const classAttribute = classNames ? ` class="${classNames}"` : '';
121
-
122
- return `<pre><code${classAttribute}>${highlightedCode}</code></pre>\n`;
123
- };
86
+ type CodeHighlightRuntime = typeof import('../../../internal/codeHighlight');
87
+
88
+ interface MarkdownRenderResult {
89
+ html: string;
90
+ requiresHighlightRuntime: boolean;
91
+ }
92
+
93
+ function renderMarkdown(content: string, highlighter: CodeHighlightRuntime | null): MarkdownRenderResult {
94
+ let requiresHighlightRuntime = false;
95
+ const renderer = new marked.Renderer();
96
+ renderer.code = ({ text, lang }) => {
97
+ const rawLanguage = (lang || '').match(/\S+/)?.[0] ?? '';
98
+ const language = rawLanguage ? { mode: 'explicit' as const, id: rawLanguage } : null;
99
+ if (language !== null && highlighter === null) requiresHighlightRuntime = true;
100
+ const languageClass = rawLanguage ? `language-${escapeHtmlAttribute(rawLanguage.toLowerCase())}` : '';
101
+ const isSupportedLanguage = language !== null
102
+ && highlighter !== null
103
+ && highlighter.isCodeHighlightLanguageSupported(language);
104
+ const highlightedCode = language !== null && highlighter !== null
105
+ ? highlighter.highlightCodeToHtml(text, language)
106
+ : escapeHtmlAttribute(text);
107
+ const classNames = [languageClass, isSupportedLanguage ? 'hljs' : ''].filter(Boolean).join(' ');
108
+ const classAttribute = classNames ? ` class="${classNames}"` : '';
109
+
110
+ return `<pre><code${classAttribute}>${highlightedCode}</code></pre>\n`;
111
+ };
112
+
113
+ return {
114
+ html: marked.parse(content, { gfm: true, breaks: true, renderer }) as string,
115
+ requiresHighlightRuntime,
116
+ };
117
+ }
124
118
 
125
119
  interface Props {
126
120
  content: string;
@@ -150,6 +144,7 @@ const emit = defineEmits<{
150
144
  }>();
151
145
 
152
146
  const notifier = useNotifier();
147
+ const { t } = useUiKitLocale();
153
148
  const { copyText } = useClipboard();
154
149
  const rootRef = ref<HTMLElement | null>(null);
155
150
 
@@ -186,28 +181,61 @@ const headerLabel = computed(() => {
186
181
  switch (renderMode.value) {
187
182
  case 'markdown': return 'Markdown';
188
183
  case 'html': return 'HTML';
189
- default: return 'Текст';
184
+ default: return t('editor.plainText');
190
185
  }
191
186
  });
192
187
 
193
- const renderedMarkdown = computed(() => marked.parse(contract.value.content, {
194
- gfm: true,
195
- breaks: true,
196
- renderer: markdownRenderer,
197
- }) as string);
188
+ const renderedDocumentHtml = shallowRef('');
189
+ const documentRenderError = shallowRef<Error | null>(null);
190
+ let documentRenderGeneration = 0;
198
191
 
199
- const sanitizedMarkdown = computed(() => {
200
- const html = renderedMarkdown.value;
201
- return sanitizeDocumentHtml(html);
202
- });
192
+ function documentRenderingError(error: unknown): Error {
193
+ if (error instanceof Error) return error;
194
+ return new Error(`SDocBlock: document rendering failed with ${String(error)}.`);
195
+ }
196
+
197
+ async function enhanceMarkdownHighlighting(
198
+ content: string,
199
+ generation: number,
200
+ ): Promise<void> {
201
+ try {
202
+ const highlighter = await import('../../../internal/codeHighlight');
203
+ const highlighted = renderMarkdown(content, highlighter);
204
+ if (generation !== documentRenderGeneration) return;
205
+ renderedDocumentHtml.value = sanitizeDocumentHtml(highlighted.html);
206
+ } catch (error) {
207
+ if (generation !== documentRenderGeneration) return;
208
+ documentRenderError.value = documentRenderingError(error);
209
+ }
210
+ }
211
+
212
+ watch(
213
+ () => [contract.value.content, renderMode.value] as const,
214
+ ([content, mode]) => {
215
+ const generation = documentRenderGeneration += 1;
216
+ documentRenderError.value = null;
217
+ if (mode === 'html') {
218
+ renderedDocumentHtml.value = sanitizeDocumentHtml(content);
219
+ return;
220
+ }
221
+ if (mode === 'plaintext') {
222
+ renderedDocumentHtml.value = '';
223
+ return;
224
+ }
225
+ const initial = renderMarkdown(content, null);
226
+ renderedDocumentHtml.value = sanitizeDocumentHtml(initial.html);
227
+ if (initial.requiresHighlightRuntime) void enhanceMarkdownHighlighting(content, generation);
228
+ },
229
+ { immediate: true },
230
+ );
203
231
 
204
- const sanitizedHtml = computed(() => {
205
- return sanitizeDocumentHtml(contract.value.content);
232
+ onBeforeUnmount(() => {
233
+ documentRenderGeneration += 1;
206
234
  });
207
235
 
208
236
  const sanitizedDocumentHtml = computed(() => {
209
- if (renderMode.value === 'markdown') return sanitizedMarkdown.value;
210
- if (renderMode.value === 'html') return sanitizedHtml.value;
237
+ if (documentRenderError.value !== null) throw documentRenderError.value;
238
+ if (renderMode.value === 'markdown' || renderMode.value === 'html') return renderedDocumentHtml.value;
211
239
  throw new Error('SDocBlock: document renderer доступен только для markdown/html mode. / SDocBlock: the document renderer is available only in markdown/html mode.');
212
240
  });
213
241
 
@@ -298,15 +326,15 @@ const copyContent = async () => {
298
326
  if (!text) return;
299
327
  const result = await copyText(text);
300
328
  if (result.status === 'copied') {
301
- notifier.success({ message: 'Содержимое скопировано' });
329
+ notifier.success({ message: t('clipboard.documentCopied') });
302
330
  return;
303
331
  }
304
332
  notifier.danger({
305
333
  message: result.status === 'unavailable'
306
- ? 'Буфер обмена недоступен в этом браузере.'
334
+ ? t('clipboard.clipboardUnavailable')
307
335
  : result.status === 'empty'
308
- ? 'Нет содержимого для копирования.'
309
- : `Не удалось скопировать. ${result.error.message}`,
336
+ ? t('clipboard.emptyDocument')
337
+ : `${t('clipboard.documentCopyFailed')} ${result.error.message}`,
310
338
  });
311
339
  };
312
340
 
@@ -350,7 +378,7 @@ const handleDocumentLinkClick = (event: MouseEvent, payload: DocumentLinkPayload
350
378
  try {
351
379
  openExternalHref(new URL(payload.href, window.location.href).toString());
352
380
  } catch {
353
- notifier.danger({ message: `Не удалось открыть ссылку: ${payload.href}` });
381
+ notifier.danger({ message: t('feedback.openLinkFailed', { href: payload.href }) });
354
382
  }
355
383
  };
356
384
  </script>
@@ -26,7 +26,7 @@
26
26
  appearance="text"
27
27
  severity="secondary"
28
28
  size="sm"
29
- :label="isExpanded ? 'Свернуть' : 'Развернуть'"
29
+ :label="isExpanded ? t('actions.collapse') : t('actions.expand')"
30
30
  :disclosure="{ expanded: isExpanded, controls: contentId, iconPlacement: 'leading' }"
31
31
  @click="toggleExpanded"
32
32
  />
@@ -38,8 +38,8 @@
38
38
  shape="circle"
39
39
  icon-only
40
40
  :leading-icon="Copy"
41
- title="Скопировать текст"
42
- aria-label="Скопировать текст"
41
+ :title="t('clipboard.copyText')"
42
+ :aria-label="t('clipboard.copyText')"
43
43
  :disabled="!text"
44
44
  @click="copyText"
45
45
  />
@@ -54,6 +54,7 @@ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
54
54
  import SButton from '../controls/SButton.vue'
55
55
  import { useNotifier } from '../../../composables/useNotifier'
56
56
  import { useClipboard } from '../../../composables/useClipboard'
57
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
57
58
  import {
58
59
  validateExpandedContentMaxBlockSize,
59
60
  type ExpandedContentMaxBlockSize,
@@ -106,6 +107,7 @@ const emit = defineEmits<{
106
107
  }>()
107
108
 
108
109
  const notifier = useNotifier()
110
+ const { t } = useUiKitLocale()
109
111
  const { copyText: copyToClipboard } = useClipboard()
110
112
  const contentEl = ref<HTMLElement | null>(null)
111
113
  const ownedAttrs = useOwnedAttrs({ component: 'SExpandableText', owner: 'expandable text root' })
@@ -207,17 +209,17 @@ async function copyText(): Promise<void> {
207
209
  }
208
210
  const result = await copyToClipboard(props.text)
209
211
  if (result.status === 'copied') {
210
- notifier.success({ message: 'Текст скопирован' })
212
+ notifier.success({ message: t('clipboard.textCopied') })
211
213
  return
212
214
  }
213
215
 
214
216
  notifier.danger({
215
- title: 'Ошибка копирования',
217
+ title: t('clipboard.copyErrorTitle'),
216
218
  message: result.status === 'unavailable'
217
- ? 'Буфер обмена недоступен в этом браузере.'
219
+ ? t('clipboard.clipboardUnavailable')
218
220
  : result.status === 'empty'
219
- ? 'Нет текста для копирования.'
220
- : `Не удалось скопировать текст. ${result.error.message}`,
221
+ ? t('clipboard.emptyText')
222
+ : `${t('clipboard.textCopyFailed')} ${result.error.message}`,
221
223
  })
222
224
  }
223
225
 
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref, watch } from 'vue'
3
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
3
4
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
4
5
  import STree from '../complex/STree.vue'
5
6
  import type { TreeModel, TreeSelection } from '../complex/types'
@@ -22,6 +23,7 @@ interface JsonTreeNode {
22
23
  }
23
24
 
24
25
  const props = withDefaults(defineProps<Props>(), { initiallyCollapsed: false })
26
+ const { t } = useUiKitLocale()
25
27
  const expanded = ref<Set<string>>(new Set())
26
28
  const ownedAttrs = useOwnedAttrs({ component: 'SJsonTree', owner: 'JSON tree root' })
27
29
  const selection: TreeSelection<string> = { mode: 'none' }
@@ -29,7 +31,7 @@ const treeModel: TreeModel<JsonTreeNode, string> = {
29
31
  key: (node) => node.id,
30
32
  label: (node) => node.keyLabel === undefined
31
33
  ? node.summary
32
- : node.keyLabel.length === 0 ? 'Пустой ключ' : node.keyLabel,
34
+ : node.keyLabel.length === 0 ? t('data.emptyKey') : node.keyLabel,
33
35
  children: (node) => node.children,
34
36
  disabled: () => false,
35
37
  icon: () => undefined,
@@ -96,7 +98,7 @@ defineExpose({ expandAll, collapseAll })
96
98
 
97
99
  <template>
98
100
  <div v-bind="ownedAttrs.bindings()" class="s-json-tree">
99
- <STree label="Структура JSON" :nodes="[root]" :model="treeModel" :selection="selection" :expanded="expanded" density="compact" actions-layout="overlay" @update:expanded="expanded = new Set($event)">
101
+ <STree :label="t('data.jsonStructure')" :nodes="[root]" :model="treeModel" :selection="selection" :expanded="expanded" density="compact" actions-layout="overlay" @update:expanded="expanded = new Set($event)">
100
102
  <template #col-left />
101
103
  <template #col-center="{ node }">
102
104
  <span class="s-json-tree__entry" :data-json-pointer="node.pointer">
@@ -113,6 +113,7 @@ export interface Slots<T extends LinkedSystemListItem = LinkedSystemListItem> {
113
113
 
114
114
  <script setup lang="ts" generic="T extends LinkedSystemListItem = LinkedSystemListItem">
115
115
  import { computed, watchEffect } from 'vue'
116
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
116
117
  import { withErrorCause } from '../../../internal/es2020'
117
118
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
118
119
  import {
@@ -128,17 +129,18 @@ import SStatus from './SStatus.vue'
128
129
  defineOptions({ inheritAttrs: false })
129
130
 
130
131
  const props = withDefaults(defineProps<Props<T>>(), {
131
- accessibleLabel: 'Связанные системы',
132
+ accessibleLabel: undefined,
132
133
  presentation: 'list',
133
134
  actionsDisabled: false,
134
135
  })
136
+ const { t } = useUiKitLocale()
135
137
 
136
138
  const slots = defineSlots<Slots<T>>()
137
139
  const ownedAttrs = useOwnedAttrs({ component: 'SLinkedSystemsList', owner: 'active linked-systems list root' })
138
140
  const validatedAccessibleLabel = computed(() => validateNonEmptyString(
139
141
  'SLinkedSystemsList',
140
142
  'accessibleLabel',
141
- props.accessibleLabel,
143
+ props.accessibleLabel ?? t('data.linkedSystems'),
142
144
  ))
143
145
  const validatedPresentation = computed(() => validateExactString(
144
146
  'SLinkedSystemsList',
@@ -41,6 +41,7 @@
41
41
 
42
42
  <script setup lang="ts">
43
43
  import { computed } from 'vue'
44
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
44
45
  import { X } from '../../icons/sputnigUiIcons'
45
46
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
46
47
  import { resolveSemanticToneBinding } from '../../../internal/semanticTone'
@@ -79,8 +80,6 @@ export interface SMessageDismissEvent {
79
80
  const MESSAGE_SEVERITIES = ['info', 'success', 'warn', 'danger'] as const satisfies readonly Severity[]
80
81
  const MESSAGE_LIVE_MODES = ['off', 'polite', 'assertive', 'auto'] as const satisfies readonly FeedbackLiveMode[]
81
82
  const MESSAGE_SIZES = ['sm', 'md'] as const satisfies readonly SMessageSize[]
82
- const DEFAULT_DISMISS_LABEL = 'Закрыть'
83
-
84
83
  export interface Props {
85
84
  severity?: Severity
86
85
  title?: string
@@ -96,11 +95,12 @@ const props = withDefaults(defineProps<Props>(), {
96
95
  severity: 'info',
97
96
  title: undefined,
98
97
  closable: false,
99
- dismissLabel: DEFAULT_DISMISS_LABEL,
98
+ dismissLabel: undefined,
100
99
  dismissDisabled: false,
101
100
  live: 'off',
102
101
  size: 'md',
103
102
  })
103
+ const { t } = useUiKitLocale()
104
104
 
105
105
  const emit = defineEmits<{
106
106
  dismiss: [event: SMessageDismissEvent]
@@ -134,9 +134,9 @@ const validatedSize = computed<SMessageSize>(() => validateExactString(
134
134
  const dismissAction = computed<SMessageDismissAction | undefined>(() => {
135
135
  const closable = validateBoolean('SMessage', 'closable', props.closable)
136
136
  const disabled = validateBoolean('SMessage', 'dismissDisabled', props.dismissDisabled)
137
- const label = validateNonEmptyString('SMessage', 'dismissLabel', props.dismissLabel)
137
+ const label = validateNonEmptyString('SMessage', 'dismissLabel', props.dismissLabel ?? t('feedback.dismissMessage'))
138
138
  if (!closable) {
139
- if (disabled || label !== DEFAULT_DISMISS_LABEL) {
139
+ if (disabled || props.dismissLabel !== undefined) {
140
140
  throw new TypeError(
141
141
  'SMessage: dismissLabel/dismissDisabled применяются только при closable=true. '
142
142
  + '/ SMessage: dismissLabel/dismissDisabled apply only when closable=true.',
@@ -147,7 +147,7 @@ const dismissAction = computed<SMessageDismissAction | undefined>(() => {
147
147
  return { label, disabled }
148
148
  })
149
149
 
150
- const presentation = computed(() => resolveFeedbackPresentation(validatedSeverity.value))
150
+ const presentation = computed(() => resolveFeedbackPresentation(validatedSeverity.value, t))
151
151
  const iconComponent = computed(() => presentation.value.icon)
152
152
  const liveAttributes = computed(() => resolveFeedbackLive(validatedSeverity.value, validatedLive.value))
153
153
 
@@ -0,0 +1,8 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-rich-document {
4
+ @apply min-w-0 text-base leading-7 text-surface-800 dark:text-surface-200;
5
+ display: grid;
6
+ gap: var(--s-space-section-gap);
7
+ overflow-wrap: anywhere;
8
+ }
@@ -0,0 +1,65 @@
1
+ <template>
2
+ <article
3
+ v-bind="ownedAttrs.bindings()"
4
+ class="s-rich-document"
5
+ :aria-label="validatedAccessibleLabel"
6
+ :data-document-id="model.document.id"
7
+ >
8
+ <SRichBlockNode
9
+ v-for="(node, index) in model.document.nodes"
10
+ :key="index"
11
+ :node="node"
12
+ :resources="model.resources"
13
+ :document-id="model.document.id"
14
+ :path="`document.nodes[${index}]`"
15
+ @link-activate="emit('link-activate', $event)"
16
+ @resource-activate="emit('resource-activate', $event)"
17
+ @action="emit('action', $event)"
18
+ />
19
+ </article>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import { computed } from 'vue'
24
+
25
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
26
+ import { validateNonEmptyString } from '../../../internal/runtimeContract'
27
+ import SRichBlockNode from './_internal/SRichBlockNode.vue'
28
+ import {
29
+ validateRichDocumentModel,
30
+ type SRichDocumentActionIntent,
31
+ type SRichDocumentLinkActivate,
32
+ type SRichDocumentResource,
33
+ type SRichDocumentResourceActivate,
34
+ type SRichDocumentV1,
35
+ } from './richDocument'
36
+
37
+ defineOptions({ inheritAttrs: false })
38
+
39
+ /** Safe renderer input. The document and resources are validated before any node reaches the DOM. */
40
+ export interface Props {
41
+ document: SRichDocumentV1
42
+ resources?: readonly SRichDocumentResource[]
43
+ accessibleLabel: string
44
+ }
45
+
46
+ const props = withDefaults(defineProps<Props>(), {
47
+ resources: () => [],
48
+ })
49
+
50
+ const emit = defineEmits<{
51
+ 'link-activate': [payload: SRichDocumentLinkActivate]
52
+ 'resource-activate': [payload: SRichDocumentResourceActivate]
53
+ action: [payload: SRichDocumentActionIntent]
54
+ }>()
55
+
56
+ const ownedAttrs = useOwnedAttrs({ component: 'SRichDocument', owner: 'safe rich document root' })
57
+ const validatedAccessibleLabel = computed(() => validateNonEmptyString(
58
+ 'SRichDocument',
59
+ 'accessibleLabel',
60
+ props.accessibleLabel,
61
+ ))
62
+ const model = computed(() => validateRichDocumentModel(props.document, props.resources))
63
+ </script>
64
+
65
+ <style lang="postcss" src="./SRichDocument.css" scoped></style>
@@ -0,0 +1,53 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-scroll-viewport {
4
+ @apply relative min-h-0 min-w-0;
5
+ }
6
+
7
+ .s-scroll-viewport[data-axis='x'] {
8
+ overflow-x: auto;
9
+ overflow-y: hidden;
10
+ }
11
+
12
+ .s-scroll-viewport[data-axis='y'] {
13
+ overflow-x: hidden;
14
+ overflow-y: auto;
15
+ }
16
+
17
+ .s-scroll-viewport[data-axis='both'] {
18
+ overflow: auto;
19
+ }
20
+
21
+ .s-scroll-viewport[data-block-size='sm'] {
22
+ block-size: var(--s-scroll-viewport-max-block-size-sm);
23
+ }
24
+
25
+ .s-scroll-viewport[data-block-size='md'] {
26
+ block-size: var(--s-scroll-viewport-max-block-size-md);
27
+ }
28
+
29
+ .s-scroll-viewport[data-block-size='lg'] {
30
+ block-size: var(--s-scroll-viewport-max-block-size-lg);
31
+ }
32
+
33
+ .s-scroll-viewport[data-block-size='fill'] {
34
+ block-size: 100%;
35
+ }
36
+
37
+ .s-scroll-viewport[data-overscroll-behavior='contain'] {
38
+ overscroll-behavior: contain;
39
+ }
40
+
41
+ .s-scroll-viewport[data-overscroll-behavior='none'] {
42
+ overscroll-behavior: none;
43
+ }
44
+
45
+ .s-scroll-viewport[data-focusable='true']:focus-visible {
46
+ outline: var(--s-focus-ring-width) solid var(--s-focus-ring-color);
47
+ outline-offset: var(--s-focus-ring-inset-offset);
48
+ }
49
+
50
+ .s-scroll-viewport__content {
51
+ min-block-size: 100%;
52
+ min-inline-size: 100%;
53
+ }