@pgcorp/ui-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/docs/accessibility.md +27 -0
  4. package/docs/getting-started.md +80 -0
  5. package/docs/licensing.md +30 -0
  6. package/docs/public-api.md +39 -0
  7. package/docs/security.md +21 -0
  8. package/docs/theming.md +35 -0
  9. package/docs/versioning.md +23 -0
  10. package/index.ts +3 -0
  11. package/package.json +592 -0
  12. package/src/components/icons/_internal/generatedSputnigUiIconMarkup.ts +971 -0
  13. package/src/components/icons/sputnigUiIcons.ts +530 -0
  14. package/src/components/layout/SAppShell.vue +232 -0
  15. package/src/components/layout/SDockRegion.vue +200 -0
  16. package/src/components/layout/SResizeHandle.vue +251 -0
  17. package/src/components/layout/SSideMenuButton.vue +104 -0
  18. package/src/components/layout/SSideMenuRail.vue +47 -0
  19. package/src/components/layout/SStack.css +25 -0
  20. package/src/components/layout/SStack.vue +57 -0
  21. package/src/components/layout/SStatusBarShell.css +15 -0
  22. package/src/components/layout/SStatusBarShell.vue +26 -0
  23. package/src/components/layout/SWorkbenchLayout.vue +53 -0
  24. package/src/components/layout/SWorkbenchPanelShell.css +161 -0
  25. package/src/components/layout/SWorkbenchPanelShell.vue +286 -0
  26. package/src/components/layout/_internal/SAppBrand.css +45 -0
  27. package/src/components/layout/_internal/SAppBrand.vue +37 -0
  28. package/src/components/layout/_internal/SAppBrandContent.vue +30 -0
  29. package/src/components/layout/types.ts +28 -0
  30. package/src/components/shared/_internal/SDialogSurface.css +127 -0
  31. package/src/components/shared/_internal/SDialogSurface.vue +117 -0
  32. package/src/components/shared/_internal/SDisclosureSummary.css +92 -0
  33. package/src/components/shared/_internal/SDisclosureSummary.vue +121 -0
  34. package/src/components/shared/_internal/SListboxGroup.css +18 -0
  35. package/src/components/shared/_internal/SListboxGroup.vue +24 -0
  36. package/src/components/shared/_internal/SMenuItem.css +112 -0
  37. package/src/components/shared/_internal/SMenuItem.vue +213 -0
  38. package/src/components/shared/_internal/SMenuLayer.css +21 -0
  39. package/src/components/shared/_internal/SMenuLayer.vue +48 -0
  40. package/src/components/shared/_internal/SMenuSeparator.css +5 -0
  41. package/src/components/shared/_internal/SMenuSeparator.vue +13 -0
  42. package/src/components/shared/_internal/SToastItem.css +65 -0
  43. package/src/components/shared/_internal/SToastItem.vue +58 -0
  44. package/src/components/shared/_internal/STreeNode.vue +761 -0
  45. package/src/components/shared/_internal/dialogSurface.ts +105 -0
  46. package/src/components/shared/_internal/focusTrap.ts +40 -0
  47. package/src/components/shared/_internal/listboxContext.ts +16 -0
  48. package/src/components/shared/_internal/markerContract.ts +10 -0
  49. package/src/components/shared/_internal/menuContext.ts +23 -0
  50. package/src/components/shared/_internal/selectionContract.ts +239 -0
  51. package/src/components/shared/_internal/sidebarGroupContext.ts +31 -0
  52. package/src/components/shared/_internal/tabsContext.ts +57 -0
  53. package/src/components/shared/_internal/treeRuntime.ts +203 -0
  54. package/src/components/shared/_internal/useSelectionRoving.ts +162 -0
  55. package/src/components/shared/codeLanguages.ts +168 -0
  56. package/src/components/shared/complex/SKanbanCard.css +8 -0
  57. package/src/components/shared/complex/SKanbanCard.vue +76 -0
  58. package/src/components/shared/complex/SKanbanLane.vue +82 -0
  59. package/src/components/shared/complex/STree.css +24 -0
  60. package/src/components/shared/complex/STree.vue +288 -0
  61. package/src/components/shared/complex/treeAdapter.ts +210 -0
  62. package/src/components/shared/complex/types.ts +102 -0
  63. package/src/components/shared/containers/SCloudChrome.css +103 -0
  64. package/src/components/shared/containers/SCloudChrome.vue +87 -0
  65. package/src/components/shared/containers/SCloudChromeEmpty.css +10 -0
  66. package/src/components/shared/containers/SCloudChromeEmpty.vue +18 -0
  67. package/src/components/shared/containers/SCloudChromeRow.css +78 -0
  68. package/src/components/shared/containers/SCloudChromeRow.vue +108 -0
  69. package/src/components/shared/containers/SCollapsiblePanel.css +57 -0
  70. package/src/components/shared/containers/SCollapsiblePanel.vue +360 -0
  71. package/src/components/shared/containers/SDrawer.css +60 -0
  72. package/src/components/shared/containers/SDrawer.vue +86 -0
  73. package/src/components/shared/containers/SFormGrid.css +26 -0
  74. package/src/components/shared/containers/SFormGrid.vue +46 -0
  75. package/src/components/shared/containers/SFormGridItem.css +18 -0
  76. package/src/components/shared/containers/SFormGridItem.vue +29 -0
  77. package/src/components/shared/containers/SLeftSidebar.vue +74 -0
  78. package/src/components/shared/containers/SModal.css +27 -0
  79. package/src/components/shared/containers/SModal.vue +105 -0
  80. package/src/components/shared/containers/SPageHeader.css +66 -0
  81. package/src/components/shared/containers/SPageHeader.vue +45 -0
  82. package/src/components/shared/containers/SPanel.css +134 -0
  83. package/src/components/shared/containers/SPanel.vue +169 -0
  84. package/src/components/shared/containers/SPopover.css +35 -0
  85. package/src/components/shared/containers/SPopover.vue +551 -0
  86. package/src/components/shared/containers/SSidebarGroup.css +12 -0
  87. package/src/components/shared/containers/SSidebarGroup.vue +443 -0
  88. package/src/components/shared/containers/SSidebarSection.css +41 -0
  89. package/src/components/shared/containers/SSidebarSection.vue +268 -0
  90. package/src/components/shared/containers/disclosureSummary.ts +48 -0
  91. package/src/components/shared/containers/popover.ts +72 -0
  92. package/src/components/shared/containers/sidebar.ts +206 -0
  93. package/src/components/shared/controls/SButton.css +274 -0
  94. package/src/components/shared/controls/SButton.vue +211 -0
  95. package/src/components/shared/controls/SCheckbox.css +65 -0
  96. package/src/components/shared/controls/SCheckbox.vue +130 -0
  97. package/src/components/shared/controls/SCheckboxGroup.vue +134 -0
  98. package/src/components/shared/controls/SChoiceCards.css +29 -0
  99. package/src/components/shared/controls/SChoiceCards.vue +145 -0
  100. package/src/components/shared/controls/SColorSelect.vue +146 -0
  101. package/src/components/shared/controls/SComboboxTrigger.css +35 -0
  102. package/src/components/shared/controls/SComboboxTrigger.vue +122 -0
  103. package/src/components/shared/controls/SContextMenu.css +11 -0
  104. package/src/components/shared/controls/SContextMenu.vue +186 -0
  105. package/src/components/shared/controls/SContextToggleButton.vue +64 -0
  106. package/src/components/shared/controls/SDragHandle.vue +100 -0
  107. package/src/components/shared/controls/SDropdownMenu.css +5 -0
  108. package/src/components/shared/controls/SDropdownMenu.vue +188 -0
  109. package/src/components/shared/controls/SField.css +79 -0
  110. package/src/components/shared/controls/SField.vue +89 -0
  111. package/src/components/shared/controls/SFieldGroup.css +58 -0
  112. package/src/components/shared/controls/SFieldGroup.vue +117 -0
  113. package/src/components/shared/controls/SFieldLabel.css +12 -0
  114. package/src/components/shared/controls/SFieldLabel.vue +51 -0
  115. package/src/components/shared/controls/SInlineTokenEditor.css +61 -0
  116. package/src/components/shared/controls/SInlineTokenEditor.vue +1081 -0
  117. package/src/components/shared/controls/SInputText.css +66 -0
  118. package/src/components/shared/controls/SInputText.vue +321 -0
  119. package/src/components/shared/controls/SInteractiveSurface.css +72 -0
  120. package/src/components/shared/controls/SInteractiveSurface.vue +326 -0
  121. package/src/components/shared/controls/SLink.css +114 -0
  122. package/src/components/shared/controls/SLink.ts +85 -0
  123. package/src/components/shared/controls/SLink.vue +114 -0
  124. package/src/components/shared/controls/SListbox.css +31 -0
  125. package/src/components/shared/controls/SListbox.vue +388 -0
  126. package/src/components/shared/controls/SListboxOption.css +63 -0
  127. package/src/components/shared/controls/SListboxOption.vue +85 -0
  128. package/src/components/shared/controls/SMarker.css +19 -0
  129. package/src/components/shared/controls/SMarker.vue +33 -0
  130. package/src/components/shared/controls/SMenuSurface.css +20 -0
  131. package/src/components/shared/controls/SMenuSurface.vue +333 -0
  132. package/src/components/shared/controls/SSegmentedControl.css +38 -0
  133. package/src/components/shared/controls/SSegmentedControl.vue +134 -0
  134. package/src/components/shared/controls/SSelect.css +12 -0
  135. package/src/components/shared/controls/SSelect.vue +338 -0
  136. package/src/components/shared/controls/STextarea.css +31 -0
  137. package/src/components/shared/controls/STextarea.vue +316 -0
  138. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +224 -0
  139. package/src/components/shared/controls/menu.ts +178 -0
  140. package/src/components/shared/data-display/SActionCard.css +6 -0
  141. package/src/components/shared/data-display/SActionCard.vue +49 -0
  142. package/src/components/shared/data-display/SActionList.css +15 -0
  143. package/src/components/shared/data-display/SActionList.vue +34 -0
  144. package/src/components/shared/data-display/SActionListItem.css +114 -0
  145. package/src/components/shared/data-display/SActionListItem.vue +230 -0
  146. package/src/components/shared/data-display/SBadge.css +73 -0
  147. package/src/components/shared/data-display/SBadge.vue +52 -0
  148. package/src/components/shared/data-display/SCodeBlock.css +56 -0
  149. package/src/components/shared/data-display/SCodeBlock.vue +188 -0
  150. package/src/components/shared/data-display/SCodeEditor.css +178 -0
  151. package/src/components/shared/data-display/SCodeEditor.vue +274 -0
  152. package/src/components/shared/data-display/SCodeSearchPanel.vue +327 -0
  153. package/src/components/shared/data-display/SCopyField.vue +77 -0
  154. package/src/components/shared/data-display/SDocBlock.css +136 -0
  155. package/src/components/shared/data-display/SDocBlock.vue +358 -0
  156. package/src/components/shared/data-display/SEmptyState.css +42 -0
  157. package/src/components/shared/data-display/SEmptyState.vue +79 -0
  158. package/src/components/shared/data-display/SExpandableText.vue +314 -0
  159. package/src/components/shared/data-display/SJsonTree.css +6 -0
  160. package/src/components/shared/data-display/SJsonTree.vue +111 -0
  161. package/src/components/shared/data-display/SKeyValueGrid.css +37 -0
  162. package/src/components/shared/data-display/SKeyValueGrid.vue +277 -0
  163. package/src/components/shared/data-display/SLinkedSystemsList.vue +270 -0
  164. package/src/components/shared/data-display/SMessage.css +88 -0
  165. package/src/components/shared/data-display/SMessage.vue +152 -0
  166. package/src/components/shared/data-display/SMetricCard.css +32 -0
  167. package/src/components/shared/data-display/SMetricCard.vue +117 -0
  168. package/src/components/shared/data-display/SProgressBar.css +84 -0
  169. package/src/components/shared/data-display/SProgressBar.vue +114 -0
  170. package/src/components/shared/data-display/SProgressIndicator.css +59 -0
  171. package/src/components/shared/data-display/SProgressIndicator.vue +104 -0
  172. package/src/components/shared/data-display/SSectionHeader.css +84 -0
  173. package/src/components/shared/data-display/SSectionHeader.vue +58 -0
  174. package/src/components/shared/data-display/SStatus.css +90 -0
  175. package/src/components/shared/data-display/SStatus.vue +79 -0
  176. package/src/components/shared/data-display/STable.css +83 -0
  177. package/src/components/shared/data-display/STable.vue +339 -0
  178. package/src/components/shared/data-display/SToastContainer.css +53 -0
  179. package/src/components/shared/data-display/SToastContainer.vue +31 -0
  180. package/src/components/shared/data-display/STooltip.css +114 -0
  181. package/src/components/shared/data-display/STooltip.vue +550 -0
  182. package/src/components/shared/data-display/STooltipAnchor.css +21 -0
  183. package/src/components/shared/data-display/STooltipAnchor.vue +119 -0
  184. package/src/components/shared/data-display/SVirtualList.css +24 -0
  185. package/src/components/shared/data-display/SVirtualList.ts +19 -0
  186. package/src/components/shared/data-display/SVirtualList.vue +361 -0
  187. package/src/components/shared/data-display/json.ts +136 -0
  188. package/src/components/shared/data-display/table.ts +76 -0
  189. package/src/components/shared/database/SDataGrid.css +3 -0
  190. package/src/components/shared/database/SDataGrid.vue +464 -0
  191. package/src/components/shared/database/SSqlEditor.vue +363 -0
  192. package/src/components/shared/database/dataGrid.ts +61 -0
  193. package/src/components/shared/feedback/SAsyncState.css +26 -0
  194. package/src/components/shared/feedback/SAsyncState.vue +190 -0
  195. package/src/components/shared/graph/SGraphViewport.ts +1 -0
  196. package/src/components/shared/graph/SGraphViewport.vue +858 -0
  197. package/src/components/shared/graph/_internal/graphViewportContract.ts +362 -0
  198. package/src/components/shared/markdown.ts +169 -0
  199. package/src/components/shared/navigation/SBottomNav.vue +692 -0
  200. package/src/components/shared/navigation/SBreadcrumbs.css +26 -0
  201. package/src/components/shared/navigation/SBreadcrumbs.vue +155 -0
  202. package/src/components/shared/navigation/SCatalogNavigator.css +100 -0
  203. package/src/components/shared/navigation/SCatalogNavigator.vue +308 -0
  204. package/src/components/shared/navigation/STab.vue +363 -0
  205. package/src/components/shared/navigation/STabList.vue +442 -0
  206. package/src/components/shared/navigation/STabPanel.vue +165 -0
  207. package/src/components/shared/navigation/STabPanels.vue +33 -0
  208. package/src/components/shared/navigation/STabs.vue +617 -0
  209. package/src/components/shared/navigation/SWizardSteps.css +69 -0
  210. package/src/components/shared/navigation/SWizardSteps.vue +171 -0
  211. package/src/components/shared/navigation/_internal/SCatalogNavigatorGroup.vue +63 -0
  212. package/src/components/shared/persona/SPersonaProjectPicker.css +36 -0
  213. package/src/components/shared/persona/SPersonaProjectPicker.vue +361 -0
  214. package/src/components/shared/persona/SPersonaRunWorkbench.css +109 -0
  215. package/src/components/shared/persona/SPersonaRunWorkbench.vue +549 -0
  216. package/src/components/shared/persona/_internal/SPersonaRunConsole.css +40 -0
  217. package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +298 -0
  218. package/src/components/shared/persona/_internal/inventory.ts +110 -0
  219. package/src/components/shared/persona/_internal/labels.ts +79 -0
  220. package/src/components/shared/persona/presentation.ts +371 -0
  221. package/src/components/shared/persona/types.ts +256 -0
  222. package/src/composables/uiPreferencesReset.ts +52 -0
  223. package/src/composables/useClipboard.ts +37 -0
  224. package/src/composables/useDialogOverlay.ts +85 -0
  225. package/src/composables/useDisclosure.ts +40 -0
  226. package/src/composables/useFloatingPosition.ts +245 -0
  227. package/src/composables/useNotifier.ts +26 -0
  228. package/src/composables/useSidebarPanelState.ts +610 -0
  229. package/src/internal/codeEditorContract.ts +110 -0
  230. package/src/internal/codeEditorExtensions.ts +27 -0
  231. package/src/internal/feedbackPresentation.ts +136 -0
  232. package/src/internal/fieldSurface.ts +59 -0
  233. package/src/internal/focusNavigation.ts +69 -0
  234. package/src/internal/inlineTokenEditorContract.ts +136 -0
  235. package/src/internal/interactiveElement.ts +33 -0
  236. package/src/internal/layerStack.ts +202 -0
  237. package/src/internal/linkTarget.ts +256 -0
  238. package/src/internal/ownedAttrs.ts +464 -0
  239. package/src/internal/passiveContentContract.ts +129 -0
  240. package/src/internal/pointerInteractionLease.ts +81 -0
  241. package/src/internal/runtimeContract.ts +194 -0
  242. package/src/internal/runtimeTheme.ts +159 -0
  243. package/src/internal/semanticSizing.ts +60 -0
  244. package/src/internal/useMenuLayer.ts +129 -0
  245. package/src/stores/useNotifierStore.ts +69 -0
  246. package/src/styles/reference.css +4 -0
  247. package/src/styles/style.css +151 -0
  248. package/src/styles/tokens.css +836 -0
  249. package/src/theme.ts +449 -0
  250. package/src/types/highlight-js-lib.d.ts +76 -0
  251. package/src/variants.ts +15 -0
@@ -0,0 +1,358 @@
1
+ <template>
2
+ <div
3
+ v-bind="ownedAttrs.bindings()"
4
+ ref="rootRef"
5
+ class="s-doc-block"
6
+ :class="{
7
+ 's-doc-block--radius-none': contract.surfaceRadius === 'none',
8
+ }"
9
+ >
10
+ <div v-if="contract.showHeader && (filename || renderMode)" class="s-doc-block-header">
11
+ <div class="s-doc-block-info">
12
+ <span v-if="filename" class="s-doc-block-filename">{{ filename }}</span>
13
+ <span v-if="renderMode" class="s-doc-block-language">{{ headerLabel }}</span>
14
+ </div>
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="Копировать" />
17
+ </div>
18
+ </div>
19
+
20
+ <SAsyncState
21
+ v-if="contract.errorMessage !== undefined"
22
+ :state="{ status: 'error', message: contract.errorMessage }"
23
+ presentation="block"
24
+ />
25
+
26
+ <div
27
+ v-else-if="renderMode === 'markdown' || renderMode === 'html'"
28
+ class="s-doc-block__content prose dark:prose-invert max-w-none"
29
+ >
30
+ <SDocumentContent
31
+ :html="sanitizedDocumentHtml"
32
+ @link-click="handleDocumentLinkClick"
33
+ />
34
+ </div>
35
+
36
+ <pre v-else-if="renderMode === 'plaintext'" class="fallback-pre">{{ contract.content }}</pre>
37
+ </div>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import { computed, defineComponent, h, ref, type Component, type PropType, type VNodeChild } from 'vue';
42
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs';
43
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract';
44
+ import { validateBoolean, validateExactString, validateNonEmptyString } from '../../../internal/runtimeContract';
45
+ import SButton from '../controls/SButton.vue';
46
+ import SLink from '../controls/SLink.vue';
47
+ import SAsyncState from '../feedback/SAsyncState.vue';
48
+ import { Copy } from '../../icons/sputnigUiIcons';
49
+ import { useNotifier } from '../../../composables/useNotifier';
50
+ import { useClipboard } from '../../../composables/useClipboard';
51
+
52
+ // Markdown rendering with highlight.js similar to MessageBubble
53
+ 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
+ import {
67
+ SANITIZED_DOCUMENT_RENDER_ATTRIBUTES,
68
+ SANITIZED_DOCUMENT_TAGS,
69
+ sanitizeDocumentHtml,
70
+ } from '../markdown';
71
+
72
+ defineOptions({ inheritAttrs: false });
73
+
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
+ const ownedAttrs = useOwnedAttrs({ component: 'SDocBlock', owner: 'document block root' });
86
+
87
+ useInteractiveLeafRegistration({ owner: 'SDocBlock' });
88
+
89
+ function escapeHighlightedCode(code: string): string {
90
+ return code
91
+ .replaceAll('&', '&amp;')
92
+ .replaceAll('<', '&lt;')
93
+ .replaceAll('>', '&gt;')
94
+ .replaceAll('"', '&quot;')
95
+ .replaceAll("'", '&#39;');
96
+ }
97
+
98
+ function escapeHtmlAttribute(value: string): string {
99
+ return value
100
+ .replaceAll('&', '&amp;')
101
+ .replaceAll('<', '&lt;')
102
+ .replaceAll('>', '&gt;')
103
+ .replaceAll('"', '&quot;')
104
+ .replaceAll("'", '&#39;');
105
+ }
106
+
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
+ };
124
+
125
+ interface Props {
126
+ content: string;
127
+ mode: 'markdown' | 'html' | 'plaintext';
128
+ linkPolicy?: 'open' | 'emit';
129
+ filename?: string;
130
+ showHeader?: boolean;
131
+ showActions?: boolean;
132
+ surfaceRadius?: 'default' | 'none';
133
+ errorMessage?: string;
134
+ }
135
+ const props = withDefaults(defineProps<Props>(), {
136
+ linkPolicy: 'open',
137
+ filename: undefined,
138
+ showHeader: true,
139
+ showActions: true,
140
+ surfaceRadius: 'default',
141
+ errorMessage: undefined,
142
+ });
143
+ const emit = defineEmits<{
144
+ 'link-click': [{
145
+ href: string;
146
+ text: string;
147
+ title: string | null;
148
+ isExternal: boolean;
149
+ }];
150
+ }>();
151
+
152
+ const notifier = useNotifier();
153
+ const { copyText } = useClipboard();
154
+ const rootRef = ref<HTMLElement | null>(null);
155
+
156
+ const contract = computed(() => {
157
+ if (typeof props.content !== 'string') {
158
+ throw new TypeError(
159
+ 'SDocBlock: content должен быть строкой. / SDocBlock: content must be a string',
160
+ );
161
+ }
162
+ return {
163
+ content: props.content,
164
+ mode: validateExactString(
165
+ 'SDocBlock', 'mode', props.mode, ['markdown', 'html', 'plaintext'] as const,
166
+ ),
167
+ linkPolicy: validateExactString(
168
+ 'SDocBlock', 'linkPolicy', props.linkPolicy, ['open', 'emit'] as const,
169
+ ),
170
+ showHeader: validateBoolean('SDocBlock', 'showHeader', props.showHeader),
171
+ showActions: validateBoolean('SDocBlock', 'showActions', props.showActions),
172
+ surfaceRadius: validateExactString(
173
+ 'SDocBlock', 'surfaceRadius', props.surfaceRadius, ['default', 'none'] as const,
174
+ ),
175
+ errorMessage: props.errorMessage === undefined
176
+ ? undefined
177
+ : validateNonEmptyString('SDocBlock', 'errorMessage', props.errorMessage),
178
+ };
179
+ });
180
+
181
+ const renderMode = computed<'markdown' | 'html' | 'plaintext'>(() => {
182
+ return contract.value.mode;
183
+ });
184
+
185
+ const headerLabel = computed(() => {
186
+ switch (renderMode.value) {
187
+ case 'markdown': return 'Markdown';
188
+ case 'html': return 'HTML';
189
+ default: return 'Текст';
190
+ }
191
+ });
192
+
193
+ const renderedMarkdown = computed(() => marked.parse(contract.value.content, {
194
+ gfm: true,
195
+ breaks: true,
196
+ renderer: markdownRenderer,
197
+ }) as string);
198
+
199
+ const sanitizedMarkdown = computed(() => {
200
+ const html = renderedMarkdown.value;
201
+ return sanitizeDocumentHtml(html);
202
+ });
203
+
204
+ const sanitizedHtml = computed(() => {
205
+ return sanitizeDocumentHtml(contract.value.content);
206
+ });
207
+
208
+ const sanitizedDocumentHtml = computed(() => {
209
+ if (renderMode.value === 'markdown') return sanitizedMarkdown.value;
210
+ if (renderMode.value === 'html') return sanitizedHtml.value;
211
+ throw new Error('SDocBlock: document renderer доступен только для markdown/html mode. / SDocBlock: the document renderer is available only in markdown/html mode.');
212
+ });
213
+
214
+ interface DocumentLinkPayload {
215
+ href: string;
216
+ text: string;
217
+ title: string | null;
218
+ isExternal: boolean;
219
+ }
220
+
221
+ type DocumentLinkHandler = (event: MouseEvent, payload: DocumentLinkPayload) => void;
222
+ const sanitizedTagSet = new Set<string>(SANITIZED_DOCUMENT_TAGS);
223
+ const sanitizedAttributeSet = new Set<string>(SANITIZED_DOCUMENT_RENDER_ATTRIBUTES);
224
+ const documentLinkComponent: Component = SLink;
225
+
226
+ function renderSanitizedNode(node: Node, handleLink: DocumentLinkHandler): VNodeChild {
227
+ if (node.nodeType === Node.TEXT_NODE) return node.textContent ?? '';
228
+ if (node.nodeType === Node.COMMENT_NODE) return null;
229
+ if (!(node instanceof HTMLElement)) {
230
+ throw new TypeError(`SDocBlock: unsupported sanitized node type ${node.nodeType}.`);
231
+ }
232
+
233
+ const tag = node.tagName.toLowerCase();
234
+ if (!sanitizedTagSet.has(tag)) {
235
+ throw new TypeError(`SDocBlock: sanitized renderer получил недопустимый tag <${tag}>. / SDocBlock: sanitized renderer received unsupported tag <${tag}>.`);
236
+ }
237
+ const attributes: Record<string, string> = {};
238
+ for (const attribute of node.attributes) {
239
+ if (!sanitizedAttributeSet.has(attribute.name)) {
240
+ throw new TypeError(`SDocBlock: sanitized renderer получил недопустимый attribute ${attribute.name}. / SDocBlock: sanitized renderer received unsupported attribute ${attribute.name}.`);
241
+ }
242
+ attributes[attribute.name] = attribute.value;
243
+ }
244
+ const children = [...node.childNodes].map((child) => renderSanitizedNode(child, handleLink));
245
+
246
+ if (tag === 'a') {
247
+ const href = attributes.href?.trim();
248
+ if (!href) {
249
+ throw new TypeError('SDocBlock: sanitized <a> требует href. / SDocBlock: sanitized <a> requires href.');
250
+ }
251
+ if (!href.startsWith('#') && !URL.canParse(href, window.location.href)) {
252
+ throw new TypeError(`SDocBlock: href ${JSON.stringify(href)} не является допустимым URL. / SDocBlock: href ${JSON.stringify(href)} is not a valid URL.`);
253
+ }
254
+ const payload: DocumentLinkPayload = {
255
+ href,
256
+ text: node.textContent?.trim() || href,
257
+ title: attributes.title ?? null,
258
+ isExternal: /^(https?:\/\/|mailto:|tel:|file:\/\/)/i.test(href),
259
+ };
260
+ return h(documentLinkComponent, {
261
+ href,
262
+ title: payload.title ?? undefined,
263
+ variant: 'document',
264
+ underline: 'always',
265
+ wrap: 'anywhere',
266
+ external: payload.isExternal,
267
+ onClick: (event: MouseEvent) => handleLink(event, payload),
268
+ }, { default: () => children });
269
+ }
270
+
271
+ return h(tag, attributes, children);
272
+ }
273
+
274
+ const SDocumentContent = defineComponent({
275
+ name: 'SDocumentContent',
276
+ props: {
277
+ html: { type: String as PropType<string>, required: true },
278
+ },
279
+ emits: {
280
+ 'link-click': (event: MouseEvent, payload: DocumentLinkPayload) => (
281
+ event instanceof MouseEvent && typeof payload.href === 'string'
282
+ ),
283
+ },
284
+ setup(rendererProps, { emit: emitDocument }) {
285
+ return () => {
286
+ const template = document.createElement('template');
287
+ template.innerHTML = rendererProps.html;
288
+ return [...template.content.childNodes].map((node) => renderSanitizedNode(
289
+ node,
290
+ (event, payload) => emitDocument('link-click', event, payload),
291
+ ));
292
+ };
293
+ },
294
+ });
295
+
296
+ const copyContent = async () => {
297
+ const text = contract.value.content;
298
+ if (!text) return;
299
+ const result = await copyText(text);
300
+ if (result.status === 'copied') {
301
+ notifier.success({ message: 'Содержимое скопировано' });
302
+ return;
303
+ }
304
+ notifier.danger({
305
+ message: result.status === 'unavailable'
306
+ ? 'Буфер обмена недоступен в этом браузере.'
307
+ : result.status === 'empty'
308
+ ? 'Нет содержимого для копирования.'
309
+ : `Не удалось скопировать. ${result.error.message}`,
310
+ });
311
+ };
312
+
313
+ const openExternalHref = (href: string) => {
314
+ if (/^(mailto:|tel:)/i.test(href)) {
315
+ window.location.href = href;
316
+ return;
317
+ }
318
+
319
+ window.open(href, '_blank', 'noopener,noreferrer');
320
+ };
321
+
322
+ const scrollToAnchor = (href: string) => {
323
+ const anchorId = href.slice(1);
324
+ if (!anchorId) return;
325
+ const target = [...(rootRef.value?.querySelectorAll<HTMLElement>('[data-s-doc-anchor]') ?? [])]
326
+ .find((element) => element.dataset.sDocAnchor === anchorId);
327
+ target?.scrollIntoView({ block: 'start', behavior: 'smooth' });
328
+ };
329
+
330
+ const handleDocumentLinkClick = (event: MouseEvent, payload: DocumentLinkPayload) => {
331
+ event.preventDefault();
332
+ event.stopPropagation();
333
+
334
+ if (payload.href.startsWith('#')) {
335
+ scrollToAnchor(payload.href);
336
+ return;
337
+ }
338
+
339
+ emit('link-click', payload);
340
+
341
+ if (contract.value.linkPolicy === 'emit') {
342
+ return;
343
+ }
344
+
345
+ if (payload.isExternal) {
346
+ openExternalHref(payload.href);
347
+ return;
348
+ }
349
+
350
+ try {
351
+ openExternalHref(new URL(payload.href, window.location.href).toString());
352
+ } catch {
353
+ notifier.danger({ message: `Не удалось открыть ссылку: ${payload.href}` });
354
+ }
355
+ };
356
+ </script>
357
+
358
+ <style lang="postcss" src="./SDocBlock.css" scoped></style>
@@ -0,0 +1,42 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-empty-state {
4
+ @apply grid min-w-0 place-items-center content-center gap-2;
5
+ }
6
+
7
+ .s-empty-state[data-presentation='panel'] {
8
+ @apply rounded-md border border-dashed border-surface-300 p-6 text-center;
9
+ @apply dark:border-surface-700;
10
+ }
11
+
12
+ .s-empty-state[data-presentation='panel'][data-size='md'] {
13
+ @apply min-h-40;
14
+ }
15
+
16
+ .s-empty-state[data-presentation='panel'][data-size='sm'] {
17
+ @apply min-h-24;
18
+ }
19
+
20
+ .s-empty-state[data-presentation='inline'] {
21
+ @apply flex w-full items-center justify-start text-left;
22
+ }
23
+
24
+ .s-empty-state[data-presentation='inline'] .s-empty-state__title {
25
+ @apply text-xs font-normal text-surface-500 dark:text-surface-400;
26
+ }
27
+
28
+ .s-empty-state__icon {
29
+ @apply h-8 w-8 text-surface-400;
30
+ }
31
+
32
+ .s-empty-state__title {
33
+ @apply text-sm font-semibold text-surface-800 dark:text-surface-200;
34
+ }
35
+
36
+ .s-empty-state__description {
37
+ @apply max-w-lg text-xs text-surface-500 dark:text-surface-400;
38
+ }
39
+
40
+ .s-empty-state__actions {
41
+ @apply mt-2 flex flex-wrap items-center justify-center gap-2;
42
+ }
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+ import { computed, watchEffect, type Component, type VNodeChild } from 'vue'
3
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
4
+ import { PassiveContentBoundary, type PassiveContentContract } from '../../../internal/passiveContentContract'
5
+ import { validateExactString, validateNonEmptyString } from '../../../internal/runtimeContract'
6
+
7
+ defineOptions({ inheritAttrs: false })
8
+
9
+ const props = withDefaults(defineProps<{
10
+ title?: string
11
+ description?: string
12
+ icon?: Component
13
+ size?: 'sm' | 'md'
14
+ presentation?: 'panel' | 'inline'
15
+ }>(), {
16
+ title: undefined,
17
+ description: undefined,
18
+ icon: undefined,
19
+ size: 'md',
20
+ presentation: 'panel',
21
+ })
22
+ const slots = defineSlots<{
23
+ title?: () => VNodeChild
24
+ actions?: () => VNodeChild
25
+ }>()
26
+
27
+ const ownedAttrs = useOwnedAttrs({ component: 'SEmptyState', owner: 'empty state root' })
28
+ const validatedSize = computed(() => validateExactString(
29
+ 'SEmptyState', 'size', props.size, ['sm', 'md'] as const,
30
+ ))
31
+ const validatedPresentation = computed(() => validateExactString(
32
+ 'SEmptyState', 'presentation', props.presentation, ['panel', 'inline'] as const,
33
+ ))
34
+ const validatedTitle = computed(() => props.title === undefined
35
+ ? undefined
36
+ : validateNonEmptyString('SEmptyState', 'title', props.title))
37
+ const validatedDescription = computed(() => props.description === undefined
38
+ ? undefined
39
+ : validateNonEmptyString('SEmptyState', 'description', props.description))
40
+ const passiveTitleContract: PassiveContentContract = {
41
+ owner: 'SEmptyState',
42
+ region: 'empty-state title',
43
+ guidance: 'Передайте действия через actions slot. / Pass actions through the actions slot.',
44
+ }
45
+
46
+ watchEffect(() => {
47
+ const hasTitleProp = validatedTitle.value !== undefined
48
+ const hasTitleSlot = Boolean(slots.title)
49
+ if (hasTitleProp === hasTitleSlot) {
50
+ throw new Error(
51
+ 'SEmptyState: требуется ровно один источник title — prop или title slot. '
52
+ + '/ SEmptyState: exactly one title source is required: the prop or the title slot.',
53
+ )
54
+ }
55
+ })
56
+ </script>
57
+
58
+ <template>
59
+ <div
60
+ v-bind="ownedAttrs.bindings()"
61
+ class="s-empty-state"
62
+ :data-size="validatedSize"
63
+ :data-presentation="validatedPresentation"
64
+ role="status"
65
+ >
66
+ <component :is="icon" v-if="icon" class="s-empty-state__icon" aria-hidden="true" />
67
+ <div class="s-empty-state__title">
68
+ <PassiveContentBoundary :contract="passiveTitleContract">
69
+ <slot name="title">{{ validatedTitle }}</slot>
70
+ </PassiveContentBoundary>
71
+ </div>
72
+ <span v-if="validatedDescription" class="s-empty-state__description">{{ validatedDescription }}</span>
73
+ <div v-if="$slots.actions" class="s-empty-state__actions">
74
+ <slot name="actions" />
75
+ </div>
76
+ </div>
77
+ </template>
78
+
79
+ <style lang="postcss" src="./SEmptyState.css" scoped></style>