@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,314 @@
1
+ <template>
2
+ <div
3
+ v-bind="ownedAttrs.bindings()"
4
+ class="s-expandable-text"
5
+ :data-expanded="isExpanded ? 'true' : 'false'"
6
+ :data-expanded-max-height="validatedExpandedMaxHeight"
7
+ :data-typography="typography"
8
+ :data-appearance="appearance"
9
+ >
10
+ <div
11
+ :id="contentId"
12
+ ref="contentEl"
13
+ class="s-expandable-text-content"
14
+ :class="{
15
+ 's-expandable-text-content--mono': monospace,
16
+ 's-expandable-text-content--collapsed': canToggle && !isExpanded,
17
+ 's-expandable-text-content--expanded': isExpanded,
18
+ }"
19
+ :style="contentStyle"
20
+ >
21
+ {{ text }}
22
+ </div>
23
+ <div v-if="canToggle || copyable" class="s-expandable-text-actions">
24
+ <SButton
25
+ v-if="canToggle"
26
+ appearance="text"
27
+ severity="secondary"
28
+ size="sm"
29
+ :label="isExpanded ? 'Свернуть' : 'Развернуть'"
30
+ :disclosure="{ expanded: isExpanded, controls: contentId, iconPlacement: 'leading' }"
31
+ @click="toggleExpanded"
32
+ />
33
+ <SButton
34
+ v-if="copyable"
35
+ appearance="text"
36
+ severity="secondary"
37
+ size="sm"
38
+ shape="circle"
39
+ icon-only
40
+ :leading-icon="Copy"
41
+ title="Скопировать текст"
42
+ aria-label="Скопировать текст"
43
+ :disabled="!text"
44
+ @click="copyText"
45
+ />
46
+ </div>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch } from 'vue'
52
+ import { Copy } from '../../icons/sputnigUiIcons'
53
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
54
+ import SButton from '../controls/SButton.vue'
55
+ import { useNotifier } from '../../../composables/useNotifier'
56
+ import { useClipboard } from '../../../composables/useClipboard'
57
+ import {
58
+ validateExpandedContentMaxBlockSize,
59
+ type ExpandedContentMaxBlockSize,
60
+ } from '../../../internal/semanticSizing'
61
+ import { validateBoundedInteger } from '../../../internal/runtimeContract'
62
+
63
+ defineOptions({ inheritAttrs: false })
64
+
65
+ /** Семантический max block-size раскрытого текста. / Semantic expanded-text max block size. */
66
+ export type ExpandedTextMaxHeight = ExpandedContentMaxBlockSize
67
+ /** Точное число строк в collapsed preview. / Exact line count in the collapsed preview. */
68
+ export type ExpandedTextCollapsedLineCount = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
69
+ /** Семантическая типографическая роль. / Semantic typography role. */
70
+ export type ExpandedTextTypography = 'body' | 'title' | 'label' | 'meta'
71
+ /** Владеемая компонентом surface presentation. / Component-owned surface presentation. */
72
+ export type ExpandedTextAppearance = 'plain' | 'preview' | 'surface'
73
+
74
+ const COLLAPSED_LINE_COUNT_DOMAIN = { minimum: 1, maximum: 12 } as const
75
+ const THRESHOLD_CHARACTER_COUNT_DOMAIN = { minimum: 1, maximum: 100_000 } as const
76
+
77
+ const props = withDefaults(defineProps<{
78
+ text: string
79
+ /** Число видимых строк без clamp/fallback. / Visible line count without clamping or fallback. */
80
+ collapsedLines?: ExpandedTextCollapsedLineCount
81
+ /** Эвристический порог до DOM measurement, целое 1…100000. / Pre-measure heuristic, integer 1…100000. */
82
+ thresholdChars?: number
83
+ expanded?: boolean
84
+ defaultExpanded?: boolean
85
+ monospace?: boolean
86
+ copyable?: boolean
87
+ expandedMaxHeight?: ExpandedTextMaxHeight
88
+ /** Семантическая роль текста, не произвольный набор font classes. / Semantic text role, not arbitrary font classes. */
89
+ typography?: ExpandedTextTypography
90
+ /** Повторяемая owned surface для preview/state blocks. / Reusable owned surface for preview/state blocks. */
91
+ appearance?: ExpandedTextAppearance
92
+ }>(), {
93
+ collapsedLines: 3,
94
+ thresholdChars: 220,
95
+ expanded: undefined,
96
+ defaultExpanded: false,
97
+ monospace: false,
98
+ copyable: true,
99
+ expandedMaxHeight: 'md',
100
+ typography: 'body',
101
+ appearance: 'plain',
102
+ })
103
+
104
+ const emit = defineEmits<{
105
+ 'update:expanded': [value: boolean]
106
+ }>()
107
+
108
+ const notifier = useNotifier()
109
+ const { copyText: copyToClipboard } = useClipboard()
110
+ const contentEl = ref<HTMLElement | null>(null)
111
+ const ownedAttrs = useOwnedAttrs({ component: 'SExpandableText', owner: 'expandable text root' })
112
+ const measuredOverflow = ref(false)
113
+ const internalExpanded = ref(props.defaultExpanded)
114
+ const contentId = `s-expandable-text-${useId()}`
115
+ let resizeObserver: ResizeObserver | null = null
116
+ let measureFrame: number | null = null
117
+
118
+ const validatedCollapsedLines = computed<ExpandedTextCollapsedLineCount>(() => {
119
+ const value = validateBoundedInteger(
120
+ 'SExpandableText',
121
+ 'collapsedLines',
122
+ props.collapsedLines,
123
+ COLLAPSED_LINE_COUNT_DOMAIN,
124
+ )
125
+ switch (value) {
126
+ case 1:
127
+ case 2:
128
+ case 3:
129
+ case 4:
130
+ case 5:
131
+ case 6:
132
+ case 7:
133
+ case 8:
134
+ case 9:
135
+ case 10:
136
+ case 11:
137
+ case 12:
138
+ return value
139
+ }
140
+ throw new TypeError('SExpandableText: unreachable collapsedLines contract state.')
141
+ })
142
+ const validatedThresholdChars = computed(() => validateBoundedInteger(
143
+ 'SExpandableText',
144
+ 'thresholdChars',
145
+ props.thresholdChars,
146
+ THRESHOLD_CHARACTER_COUNT_DOMAIN,
147
+ ))
148
+ const lineCount = computed(() => (props.text.match(/\r\n|[\r\n]/gu)?.length ?? 0) + 1)
149
+ const likelyOverflow = computed(() =>
150
+ props.text.length > validatedThresholdChars.value || lineCount.value > validatedCollapsedLines.value,
151
+ )
152
+ const canToggle = computed(() => likelyOverflow.value || measuredOverflow.value)
153
+ const isControlled = computed(() => props.expanded !== undefined)
154
+ const isExpanded = computed({
155
+ get: () => (isControlled.value ? Boolean(props.expanded) : internalExpanded.value),
156
+ set: (value: boolean) => {
157
+ if (isControlled.value) {
158
+ emit('update:expanded', value)
159
+ return
160
+ }
161
+ internalExpanded.value = value
162
+ emit('update:expanded', value)
163
+ },
164
+ })
165
+ const contentStyle = computed<Record<string, string>>(() => ({
166
+ '--s-expandable-text-lines': String(validatedCollapsedLines.value),
167
+ }))
168
+ const validatedExpandedMaxHeight = computed(() => validateExpandedContentMaxBlockSize(
169
+ 'SExpandableText',
170
+ 'expandedMaxHeight',
171
+ props.expandedMaxHeight,
172
+ ))
173
+
174
+ function cancelMeasure(): void {
175
+ if (measureFrame !== null) {
176
+ window.cancelAnimationFrame(measureFrame)
177
+ measureFrame = null
178
+ }
179
+ }
180
+
181
+ function measureOverflow(): void {
182
+ const element = contentEl.value
183
+ if (!element || isExpanded.value) {
184
+ return
185
+ }
186
+ measuredOverflow.value = element.scrollHeight > element.clientHeight + 1
187
+ }
188
+
189
+ function scheduleMeasure(): void {
190
+ cancelMeasure()
191
+ measureFrame = window.requestAnimationFrame(() => {
192
+ measureFrame = null
193
+ measureOverflow()
194
+ })
195
+ }
196
+
197
+ function toggleExpanded(): void {
198
+ isExpanded.value = !isExpanded.value
199
+ if (!isExpanded.value) {
200
+ void nextTick(scheduleMeasure)
201
+ }
202
+ }
203
+
204
+ async function copyText(): Promise<void> {
205
+ if (!props.text) {
206
+ return
207
+ }
208
+ const result = await copyToClipboard(props.text)
209
+ if (result.status === 'copied') {
210
+ notifier.success({ message: 'Текст скопирован' })
211
+ return
212
+ }
213
+
214
+ notifier.danger({
215
+ title: 'Ошибка копирования',
216
+ message: result.status === 'unavailable'
217
+ ? 'Буфер обмена недоступен в этом браузере.'
218
+ : result.status === 'empty'
219
+ ? 'Нет текста для копирования.'
220
+ : `Не удалось скопировать текст. ${result.error.message}`,
221
+ })
222
+ }
223
+
224
+ watch(() => props.text, () => {
225
+ measuredOverflow.value = false
226
+ void nextTick(scheduleMeasure)
227
+ })
228
+
229
+ onMounted(() => {
230
+ void nextTick(scheduleMeasure)
231
+ if (typeof ResizeObserver !== 'undefined' && contentEl.value) {
232
+ resizeObserver = new ResizeObserver(scheduleMeasure)
233
+ resizeObserver.observe(contentEl.value)
234
+ }
235
+ })
236
+
237
+ onBeforeUnmount(() => {
238
+ cancelMeasure()
239
+ resizeObserver?.disconnect()
240
+ resizeObserver = null
241
+ })
242
+ </script>
243
+
244
+ <style lang="postcss" scoped>
245
+ @reference "../../../styles/reference.css";
246
+
247
+ .s-expandable-text {
248
+ @apply min-w-0;
249
+ }
250
+
251
+ .s-expandable-text[data-appearance='preview'] {
252
+ @apply rounded-xl bg-surface-0 px-3 py-3 dark:bg-surface-950/60;
253
+ }
254
+
255
+ .s-expandable-text[data-appearance='surface'] {
256
+ @apply rounded-2xl border border-surface-200 bg-surface-50/70 px-4 py-3 dark:border-surface-700 dark:bg-surface-900/40;
257
+ }
258
+
259
+ .s-expandable-text-content {
260
+ @apply min-w-0 text-sm leading-6 text-surface-700 dark:text-surface-200;
261
+ white-space: pre-wrap;
262
+ overflow-wrap: anywhere;
263
+ word-break: break-word;
264
+ }
265
+
266
+ .s-expandable-text[data-typography='title'] .s-expandable-text-content {
267
+ @apply text-sm font-semibold leading-6 text-surface-800 dark:text-surface-100;
268
+ }
269
+
270
+ .s-expandable-text[data-typography='label'] .s-expandable-text-content {
271
+ @apply text-sm font-medium leading-6 text-surface-800 dark:text-surface-100;
272
+ }
273
+
274
+ .s-expandable-text[data-typography='meta'] .s-expandable-text-content {
275
+ @apply text-xs leading-5 text-surface-500 dark:text-surface-400;
276
+ }
277
+
278
+ .s-expandable-text[data-appearance='preview'] .s-expandable-text-content,
279
+ .s-expandable-text[data-appearance='surface'] .s-expandable-text-content {
280
+ @apply text-surface-600 dark:text-surface-300;
281
+ }
282
+
283
+ .s-expandable-text-content--mono {
284
+ @apply font-mono text-xs leading-5;
285
+ }
286
+
287
+ .s-expandable-text-content--collapsed {
288
+ display: -webkit-box;
289
+ -webkit-box-orient: vertical;
290
+ -webkit-line-clamp: var(--s-expandable-text-lines);
291
+ overflow: hidden;
292
+ }
293
+
294
+ .s-expandable-text-content--expanded {
295
+ overflow: auto;
296
+ }
297
+
298
+ .s-expandable-text[data-expanded-max-height='sm'] .s-expandable-text-content--expanded {
299
+ max-block-size: var(--s-expanded-content-max-block-size-sm);
300
+ }
301
+
302
+ .s-expandable-text[data-expanded-max-height='md'] .s-expandable-text-content--expanded {
303
+ max-block-size: var(--s-expanded-content-max-block-size-md);
304
+ }
305
+
306
+ .s-expandable-text[data-expanded-max-height='lg'] .s-expandable-text-content--expanded {
307
+ max-block-size: var(--s-expanded-content-max-block-size-lg);
308
+ }
309
+
310
+ .s-expandable-text-actions {
311
+ @apply mt-2 flex min-w-0 flex-wrap items-center gap-2;
312
+ }
313
+
314
+ </style>
@@ -0,0 +1,6 @@
1
+ @reference "../../../styles/reference.css";
2
+ .s-json-tree { @apply overflow-auto rounded p-3 font-mono text-xs; background: var(--s-json-tree-surface); }
3
+ .s-json-tree__entry { @apply min-w-0; }
4
+ .s-json-tree__key { @apply mr-1 font-mono; color: var(--s-json-tree-key-foreground); }
5
+ .s-json-tree__value { @apply font-mono; color: var(--s-json-tree-value-foreground); font-variant-ligatures: none; }
6
+ .s-json-tree__value[data-kind='array'], .s-json-tree__value[data-kind='object'] { @apply opacity-70; }
@@ -0,0 +1,111 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
4
+ import STree from '../complex/STree.vue'
5
+ import type { TreeModel, TreeSelection } from '../complex/types'
6
+ import {
7
+ appendJsonPointer,
8
+ assertJsonValue,
9
+ type JsonValue,
10
+ } from './json'
11
+
12
+ defineOptions({ inheritAttrs: false })
13
+
14
+ export interface Props { value: JsonValue; initiallyCollapsed?: boolean }
15
+ interface JsonTreeNode {
16
+ id: string
17
+ pointer: string
18
+ keyLabel?: string
19
+ summary: string
20
+ kind: 'array' | 'object' | 'scalar'
21
+ children: JsonTreeNode[]
22
+ }
23
+
24
+ const props = withDefaults(defineProps<Props>(), { initiallyCollapsed: false })
25
+ const expanded = ref<Set<string>>(new Set())
26
+ const ownedAttrs = useOwnedAttrs({ component: 'SJsonTree', owner: 'JSON tree root' })
27
+ const selection: TreeSelection<string> = { mode: 'none' }
28
+ const treeModel: TreeModel<JsonTreeNode, string> = {
29
+ key: (node) => node.id,
30
+ label: (node) => node.keyLabel === undefined
31
+ ? node.summary
32
+ : node.keyLabel.length === 0 ? 'Пустой ключ' : node.keyLabel,
33
+ children: (node) => node.children,
34
+ disabled: () => false,
35
+ icon: () => undefined,
36
+ multipleSelection: () => 'disabled',
37
+ }
38
+
39
+ function scalarSummary(value: JsonValue): string {
40
+ if (typeof value === 'string') return JSON.stringify(value)
41
+ if (value === null) return 'null'
42
+ return String(value)
43
+ }
44
+
45
+ function treeKey(pointer: string): string {
46
+ return `json-pointer:${pointer}`
47
+ }
48
+
49
+ function toNode(value: JsonValue, pointer = '', keyLabel?: string): JsonTreeNode {
50
+ if (Array.isArray(value)) {
51
+ return {
52
+ id: treeKey(pointer),
53
+ pointer,
54
+ keyLabel,
55
+ summary: `Array [${value.length}]`,
56
+ kind: 'array',
57
+ children: value.map((child, index) => toNode(child, appendJsonPointer(pointer, String(index)), `[${index}]`)),
58
+ }
59
+ }
60
+ if (value !== null && typeof value === 'object') {
61
+ const entries = Object.entries(value)
62
+ return {
63
+ id: treeKey(pointer),
64
+ pointer,
65
+ keyLabel,
66
+ summary: `Object {${entries.length}}`,
67
+ kind: 'object',
68
+ children: entries.map(([key, child]) => toNode(child, appendJsonPointer(pointer, key), key)),
69
+ }
70
+ }
71
+ return {
72
+ id: treeKey(pointer),
73
+ pointer,
74
+ keyLabel,
75
+ summary: scalarSummary(value),
76
+ kind: 'scalar',
77
+ children: [],
78
+ }
79
+ }
80
+
81
+ const root = computed(() => {
82
+ assertJsonValue(props.value, 'SJsonTree.value')
83
+ return toNode(props.value)
84
+ })
85
+ function collectContainerIds(node: JsonTreeNode, target: Set<string>) {
86
+ if (node.children.length === 0) return
87
+ target.add(node.id)
88
+ node.children.forEach((child) => collectContainerIds(child, target))
89
+ }
90
+ function expandAll() { const next = new Set<string>(); collectContainerIds(root.value, next); expanded.value = next }
91
+ function collapseAll() { expanded.value = new Set() }
92
+
93
+ watch(root, () => props.initiallyCollapsed ? collapseAll() : expandAll(), { immediate: true })
94
+ defineExpose({ expandAll, collapseAll })
95
+ </script>
96
+
97
+ <template>
98
+ <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)">
100
+ <template #col-left />
101
+ <template #col-center="{ node }">
102
+ <span class="s-json-tree__entry" :data-json-pointer="node.pointer">
103
+ <span v-if="node.keyLabel !== undefined" class="s-json-tree__key">{{ JSON.stringify(node.keyLabel) }}:</span>
104
+ <span class="s-json-tree__value" :data-kind="node.kind">{{ node.summary }}</span>
105
+ </span>
106
+ </template>
107
+ </STree>
108
+ </div>
109
+ </template>
110
+
111
+ <style lang="postcss" src="./SJsonTree.css" scoped></style>
@@ -0,0 +1,37 @@
1
+ @reference "../../../styles/reference.css";
2
+ .s-key-value-grid { container-type: inline-size; @apply grid gap-3; }
3
+ .s-key-value-grid[data-columns='2'] { @apply sm:grid-cols-2; }
4
+ .s-key-value-grid[data-columns='3'] { @apply sm:grid-cols-2 xl:grid-cols-3; }
5
+ .s-key-value-grid__item { @apply min-w-0; }
6
+ .s-key-value-grid[data-variant='cards'] .s-key-value-grid__item { @apply rounded-md border border-surface-200 bg-surface-0/80 p-4 dark:border-surface-700 dark:bg-surface-900/60; }
7
+ .s-key-value-grid[data-variant='inline'] .s-key-value-grid__item,
8
+ .s-key-value-grid[data-variant='plain'] .s-key-value-grid__item { @apply grid items-start gap-x-3 gap-y-1; }
9
+ .s-key-value-grid[data-variant='inline'] .s-key-value-grid__item { @apply border-b border-surface-200 py-2 last:border-b-0 dark:border-surface-700; }
10
+ .s-key-value-grid[data-label-width='narrow'] .s-key-value-grid__item { grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); }
11
+ .s-key-value-grid[data-label-width='balanced'] .s-key-value-grid__item { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
12
+ .s-key-value-grid[data-label-width='wide'] .s-key-value-grid__item { grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); }
13
+ .s-key-value-grid[data-density='compact'] { @apply gap-1; }
14
+ .s-key-value-grid__label {
15
+ @apply whitespace-normal text-xs uppercase tracking-[0.2em] text-surface-500 dark:text-surface-300;
16
+ overflow-wrap: anywhere;
17
+ }
18
+ .s-key-value-grid__value {
19
+ @apply whitespace-normal text-sm font-medium text-surface-900 dark:text-surface-100;
20
+ overflow-wrap: anywhere;
21
+ }
22
+ .s-key-value-grid[data-variant='cards'] .s-key-value-grid__value { @apply mt-2; }
23
+ .s-key-value-grid[data-density='compact'] .s-key-value-grid__label,
24
+ .s-key-value-grid[data-density='compact'] .s-key-value-grid__value { @apply text-[0.82rem] font-normal normal-case tracking-normal; }
25
+ .s-key-value-grid[data-value-align='end'] .s-key-value-grid__value { @apply text-right; }
26
+ .s-key-value-grid__value[data-monospace='true'] { @apply font-mono; }
27
+
28
+ @container (max-width: 24rem) {
29
+ .s-key-value-grid[data-variant='inline'] .s-key-value-grid__item,
30
+ .s-key-value-grid[data-variant='plain'] .s-key-value-grid__item {
31
+ grid-template-columns: minmax(0, 1fr);
32
+ }
33
+
34
+ .s-key-value-grid[data-value-align='end'] .s-key-value-grid__value {
35
+ text-align: start;
36
+ }
37
+ }