@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,277 @@
1
+ <script lang="ts">
2
+ /** Устойчивый доменный ключ строки definition list. / Stable domain key of a definition-list row. */
3
+ export type SKeyValueGridKey = string | number
4
+
5
+ /** Значение, которое компонент умеет представить без formatter. / Value the component can present without a formatter. */
6
+ export type SKeyValueGridPrimitiveValue = string | number | boolean | null | undefined
7
+
8
+ /** Typed presentation-модель одной definition-list строки. / Typed presentation model of one definition-list row. */
9
+ export interface SKeyValueGridItem<Value = SKeyValueGridPrimitiveValue> {
10
+ label: string
11
+ value: Value
12
+ valueMonospace?: boolean
13
+ }
14
+
15
+ /**
16
+ * Generic API definition-list: identity задаёт обязательный resolver, форматирование не меняет raw value.
17
+ * Generic definition-list API: identity comes from the required resolver; formatting preserves the raw value.
18
+ */
19
+ export interface SKeyValueGridProps<
20
+ Item extends SKeyValueGridItem = SKeyValueGridItem,
21
+ ItemKey extends SKeyValueGridKey = SKeyValueGridKey,
22
+ > {
23
+ items: readonly Item[]
24
+ getItemKey: (item: Item) => ItemKey
25
+ formatValue?: (value: Item['value'], item: Item) => string
26
+ emptyValueLabel?: string
27
+ variant?: 'cards' | 'inline' | 'plain'
28
+ columns?: 1 | 2 | 3
29
+ density?: 'comfortable' | 'compact'
30
+ valueAlign?: 'start' | 'end'
31
+ labelWidth?: 'narrow' | 'balanced' | 'wide'
32
+ }
33
+ </script>
34
+
35
+ <script setup lang="ts" generic="Item extends SKeyValueGridItem, ItemKey extends SKeyValueGridKey">
36
+ import { computed } from 'vue'
37
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
38
+ import {
39
+ validateBoolean,
40
+ validateBoundedInteger,
41
+ validateExactString,
42
+ validateNonEmptyString,
43
+ } from '../../../internal/runtimeContract'
44
+
45
+ defineOptions({ inheritAttrs: false })
46
+
47
+ const props = withDefaults(defineProps<SKeyValueGridProps<Item, ItemKey>>(), {
48
+ formatValue: undefined,
49
+ emptyValueLabel: '—',
50
+ variant: 'cards',
51
+ columns: 3,
52
+ density: 'comfortable',
53
+ valueAlign: 'start',
54
+ labelWidth: 'narrow',
55
+ })
56
+
57
+ defineSlots<{
58
+ label?(props: { item: Item; itemKey: ItemKey; index: number }): unknown
59
+ value?(props: {
60
+ item: Item
61
+ itemKey: ItemKey
62
+ index: number
63
+ value: Item['value']
64
+ formattedValue: SKeyValueGridPrimitiveValue
65
+ }): unknown
66
+ }>()
67
+
68
+ const VARIANTS = ['cards', 'inline', 'plain'] as const
69
+ const DENSITIES = ['comfortable', 'compact'] as const
70
+ const VALUE_ALIGNMENTS = ['start', 'end'] as const
71
+ const LABEL_WIDTHS = ['narrow', 'balanced', 'wide'] as const
72
+
73
+ interface ResolvedItem {
74
+ item: Item
75
+ itemKey: ItemKey
76
+ index: number
77
+ label: string
78
+ formattedValue: SKeyValueGridPrimitiveValue
79
+ valueMonospace: boolean
80
+ }
81
+
82
+ const ownedAttrs = useOwnedAttrs({ component: 'SKeyValueGrid', owner: 'key-value definition list' })
83
+ const validatedVariant = computed(() => validateExactString(
84
+ 'SKeyValueGrid',
85
+ 'variant',
86
+ props.variant,
87
+ VARIANTS,
88
+ ))
89
+ const validatedColumns = computed(() => validateBoundedInteger(
90
+ 'SKeyValueGrid',
91
+ 'columns',
92
+ props.columns,
93
+ { minimum: 1, maximum: 3 },
94
+ ))
95
+ const validatedDensity = computed(() => validateExactString(
96
+ 'SKeyValueGrid',
97
+ 'density',
98
+ props.density,
99
+ DENSITIES,
100
+ ))
101
+ const validatedValueAlign = computed(() => validateExactString(
102
+ 'SKeyValueGrid',
103
+ 'valueAlign',
104
+ props.valueAlign,
105
+ VALUE_ALIGNMENTS,
106
+ ))
107
+ const validatedLabelWidth = computed(() => validateExactString(
108
+ 'SKeyValueGrid',
109
+ 'labelWidth',
110
+ props.labelWidth,
111
+ LABEL_WIDTHS,
112
+ ))
113
+
114
+ function describeKey(key: SKeyValueGridKey): string {
115
+ return typeof key === 'string' ? JSON.stringify(key) : String(key)
116
+ }
117
+
118
+ function validateItemKey(candidate: unknown, coordinate: string): asserts candidate is ItemKey {
119
+ if (typeof candidate === 'string') {
120
+ if (candidate.trim().length > 0) return
121
+ throw new TypeError(
122
+ `SKeyValueGrid: getItemKey вернул пустую строку для ${coordinate}. `
123
+ + `/ SKeyValueGrid: getItemKey returned an empty string for ${coordinate}.`,
124
+ )
125
+ }
126
+ if (typeof candidate === 'number') {
127
+ if (Number.isFinite(candidate)) return
128
+ throw new TypeError(
129
+ `SKeyValueGrid: getItemKey вернул неконечное число для ${coordinate}. `
130
+ + `/ SKeyValueGrid: getItemKey returned a non-finite number for ${coordinate}.`,
131
+ )
132
+ }
133
+ throw new TypeError(
134
+ `SKeyValueGrid: getItemKey вернул неподдерживаемый key ${typeof candidate} для ${coordinate}. `
135
+ + `/ SKeyValueGrid: getItemKey returned unsupported ${typeof candidate} key for ${coordinate}.`,
136
+ )
137
+ }
138
+
139
+ function validateUnformattedValue(
140
+ value: unknown,
141
+ coordinate: string,
142
+ ): asserts value is SKeyValueGridPrimitiveValue {
143
+ if (
144
+ value === null
145
+ || value === undefined
146
+ || typeof value === 'string'
147
+ || typeof value === 'boolean'
148
+ || (typeof value === 'number' && Number.isFinite(value))
149
+ ) return
150
+
151
+ throw new TypeError(
152
+ `SKeyValueGrid: ${coordinate} должен быть string | finite number | boolean | null | undefined `
153
+ + `или иметь formatValue. / SKeyValueGrid: ${coordinate} must be string | finite number | boolean `
154
+ + '| null | undefined or provide formatValue.',
155
+ )
156
+ }
157
+
158
+ function resolveFormattedValue(item: Item, index: number): SKeyValueGridPrimitiveValue {
159
+ const coordinate = `items[${index}]`
160
+ if (props.formatValue !== undefined) {
161
+ if (typeof props.formatValue !== 'function') {
162
+ throw new TypeError(
163
+ 'SKeyValueGrid: formatValue должен быть function. '
164
+ + '/ SKeyValueGrid: formatValue must be a function.',
165
+ )
166
+ }
167
+ const result: unknown = props.formatValue(item.value, item)
168
+ if (typeof result !== 'string') {
169
+ throw new TypeError(
170
+ `SKeyValueGrid: formatValue должен вернуть string для ${coordinate}. `
171
+ + `/ SKeyValueGrid: formatValue must return a string for ${coordinate}.`,
172
+ )
173
+ }
174
+ return result
175
+ }
176
+
177
+ validateUnformattedValue(item.value, `${coordinate}.value`)
178
+ return item.value ?? validateNonEmptyString('SKeyValueGrid', 'emptyValueLabel', props.emptyValueLabel)
179
+ }
180
+
181
+ const resolvedItems = computed<readonly ResolvedItem[]>(() => {
182
+ if (!Array.isArray(props.items)) {
183
+ throw new TypeError(
184
+ 'SKeyValueGrid: items должен быть readonly array. '
185
+ + '/ SKeyValueGrid: items must be a readonly array.',
186
+ )
187
+ }
188
+ if (typeof props.getItemKey !== 'function') {
189
+ throw new TypeError(
190
+ 'SKeyValueGrid: getItemKey должен быть обязательным function resolver. '
191
+ + '/ SKeyValueGrid: getItemKey must be a required function resolver.',
192
+ )
193
+ }
194
+
195
+ const coordinatesByKey = new Map<SKeyValueGridKey, string>()
196
+ return props.items.map((candidate, index) => {
197
+ const coordinate = `items[${index}]`
198
+ if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) {
199
+ throw new TypeError(
200
+ `SKeyValueGrid: ${coordinate} должен быть object. `
201
+ + `/ SKeyValueGrid: ${coordinate} must be an object.`,
202
+ )
203
+ }
204
+ const item = candidate as Item
205
+ const label = validateNonEmptyString('SKeyValueGrid', `${coordinate}.label`, item.label)
206
+ const rawKey: unknown = props.getItemKey(item)
207
+ validateItemKey(rawKey, coordinate)
208
+ const previousCoordinate = coordinatesByKey.get(rawKey)
209
+ if (previousCoordinate !== undefined) {
210
+ throw new TypeError(
211
+ `SKeyValueGrid: getItemKey вернул duplicate key ${describeKey(rawKey)} для ${coordinate}; `
212
+ + `впервые объявлен в ${previousCoordinate}. / SKeyValueGrid: getItemKey returned duplicate key `
213
+ + `${describeKey(rawKey)} for ${coordinate}; first declared at ${previousCoordinate}.`,
214
+ )
215
+ }
216
+ coordinatesByKey.set(rawKey, coordinate)
217
+
218
+ const valueMonospace = item.valueMonospace === undefined
219
+ ? false
220
+ : validateBoolean('SKeyValueGrid', `${coordinate}.valueMonospace`, item.valueMonospace)
221
+ return {
222
+ item,
223
+ itemKey: rawKey,
224
+ index,
225
+ label,
226
+ formattedValue: resolveFormattedValue(item, index),
227
+ valueMonospace,
228
+ }
229
+ })
230
+ })
231
+ </script>
232
+
233
+ <template>
234
+ <dl
235
+ v-bind="ownedAttrs.bindings()"
236
+ class="s-key-value-grid"
237
+ :data-variant="validatedVariant"
238
+ :data-columns="validatedColumns"
239
+ :data-density="validatedDensity"
240
+ :data-value-align="validatedValueAlign"
241
+ :data-label-width="validatedLabelWidth"
242
+ >
243
+ <div
244
+ v-for="entry in resolvedItems"
245
+ :key="entry.itemKey"
246
+ class="s-key-value-grid__item"
247
+ >
248
+ <dt class="s-key-value-grid__label">
249
+ <slot
250
+ name="label"
251
+ :item="entry.item"
252
+ :item-key="entry.itemKey"
253
+ :index="entry.index"
254
+ >
255
+ {{ entry.label }}
256
+ </slot>
257
+ </dt>
258
+ <dd
259
+ class="s-key-value-grid__value"
260
+ :data-monospace="entry.valueMonospace ? 'true' : undefined"
261
+ >
262
+ <slot
263
+ name="value"
264
+ :item="entry.item"
265
+ :item-key="entry.itemKey"
266
+ :index="entry.index"
267
+ :value="entry.item.value"
268
+ :formatted-value="entry.formattedValue"
269
+ >
270
+ {{ entry.formattedValue }}
271
+ </slot>
272
+ </dd>
273
+ </div>
274
+ </dl>
275
+ </template>
276
+
277
+ <style lang="postcss" src="./SKeyValueGrid.css" scoped></style>
@@ -0,0 +1,270 @@
1
+ <template>
2
+ <div
3
+ v-if="validatedPresentation === 'chips'"
4
+ v-bind="ownedAttrs.bindings()"
5
+ class="s-linked-systems-list"
6
+ data-presentation="chips"
7
+ role="list"
8
+ :aria-label="validatedAccessibleLabel"
9
+ >
10
+ <span
11
+ v-for="item in validatedItems"
12
+ :key="item.id"
13
+ class="s-linked-systems-list__chip"
14
+ role="listitem"
15
+ :title="`${item.providerName}: ${item.externalId}`"
16
+ >
17
+ <SStatus
18
+ :label="item.syncStatus.label"
19
+ :tone="item.syncStatus.tone"
20
+ :title="item.syncStatus.title"
21
+ :icon="item.syncStatus.icon"
22
+ label-visibility="sr-only"
23
+ size="xs"
24
+ />
25
+ <SLink
26
+ v-if="item.externalUrl"
27
+ :href="item.externalUrl"
28
+ target="_blank"
29
+ external
30
+ variant="subtle"
31
+ underline="hover"
32
+ wrap="anywhere"
33
+ content-kind="identifier"
34
+ >
35
+ {{ item.externalId }}
36
+ </SLink>
37
+ <span v-else class="s-linked-systems-list__external-id">{{ item.externalId }}</span>
38
+ </span>
39
+ </div>
40
+ <SActionList v-else v-bind="ownedAttrs.bindings()" :accessible-label="validatedAccessibleLabel">
41
+ <SActionListItem
42
+ v-for="item in validatedItems"
43
+ :key="item.id"
44
+ :title="item.providerName"
45
+ :surface="{ mode: 'static' }"
46
+ :action-disabled="validatedActionsDisabled"
47
+ trailing-layout="responsive"
48
+ >
49
+ <template #description>
50
+ <SLink
51
+ v-if="item.externalUrl"
52
+ :href="item.externalUrl"
53
+ target="_blank"
54
+ external
55
+ variant="subtle"
56
+ >
57
+ {{ item.externalId }}
58
+ </SLink>
59
+ <span v-else>
60
+ {{ item.externalId }}
61
+ </span>
62
+ </template>
63
+ <template #status>
64
+ <SStatus
65
+ :label="item.syncStatus.label"
66
+ :tone="item.syncStatus.tone"
67
+ :title="item.syncStatus.title"
68
+ :icon="item.syncStatus.icon"
69
+ size="sm"
70
+ />
71
+ </template>
72
+ <template v-if="$slots.actions" #actions="{ disabled }">
73
+ <slot name="actions" :item="item" :disabled="disabled" />
74
+ </template>
75
+ </SActionListItem>
76
+ </SActionList>
77
+ </template>
78
+
79
+ <script lang="ts">
80
+ import type { Component, VNodeChild } from 'vue'
81
+ import type { StatusTone } from './SStatus.vue'
82
+
83
+ export type LinkedSystemPresentation = 'list' | 'chips'
84
+
85
+ export interface LinkedSystemSyncStatus {
86
+ readonly label: string
87
+ readonly tone: StatusTone
88
+ readonly title?: string
89
+ readonly icon?: Component | null
90
+ }
91
+
92
+ export interface LinkedSystemListItem {
93
+ readonly id: string
94
+ readonly providerName: string
95
+ readonly externalId: string
96
+ readonly externalUrl: string | null
97
+ readonly syncStatus: LinkedSystemSyncStatus
98
+ }
99
+
100
+ export interface Props<T extends LinkedSystemListItem = LinkedSystemListItem> {
101
+ readonly items: readonly T[]
102
+ readonly accessibleLabel?: string
103
+ /** List подходит для inspector, chips — для компактных строк сущностей. / List suits inspectors; chips suit compact entity rows. */
104
+ readonly presentation?: LinkedSystemPresentation
105
+ /** Единое disabled-состояние для слота действий. / Shared disabled state for the actions slot. */
106
+ readonly actionsDisabled?: boolean
107
+ }
108
+
109
+ export interface Slots<T extends LinkedSystemListItem = LinkedSystemListItem> {
110
+ actions?: (props: { readonly item: T, readonly disabled: boolean }) => VNodeChild
111
+ }
112
+ </script>
113
+
114
+ <script setup lang="ts" generic="T extends LinkedSystemListItem = LinkedSystemListItem">
115
+ import { computed, watchEffect } from 'vue'
116
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
117
+ import {
118
+ validateBoolean,
119
+ validateExactString,
120
+ validateNonEmptyString,
121
+ } from '../../../internal/runtimeContract'
122
+ import SLink from '../controls/SLink.vue'
123
+ import SActionList from './SActionList.vue'
124
+ import SActionListItem from './SActionListItem.vue'
125
+ import SStatus from './SStatus.vue'
126
+
127
+ defineOptions({ inheritAttrs: false })
128
+
129
+ const props = withDefaults(defineProps<Props<T>>(), {
130
+ accessibleLabel: 'Связанные системы',
131
+ presentation: 'list',
132
+ actionsDisabled: false,
133
+ })
134
+
135
+ const slots = defineSlots<Slots<T>>()
136
+ const ownedAttrs = useOwnedAttrs({ component: 'SLinkedSystemsList', owner: 'active linked-systems list root' })
137
+ const validatedAccessibleLabel = computed(() => validateNonEmptyString(
138
+ 'SLinkedSystemsList',
139
+ 'accessibleLabel',
140
+ props.accessibleLabel,
141
+ ))
142
+ const validatedPresentation = computed(() => validateExactString(
143
+ 'SLinkedSystemsList',
144
+ 'presentation',
145
+ props.presentation,
146
+ ['list', 'chips'] as const,
147
+ ))
148
+ const validatedActionsDisabled = computed(() => validateBoolean(
149
+ 'SLinkedSystemsList',
150
+ 'actionsDisabled',
151
+ props.actionsDisabled,
152
+ ))
153
+
154
+ function requireRecord(value: unknown, coordinate: string): Record<string, unknown> {
155
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
156
+ return value as Record<string, unknown>
157
+ }
158
+ throw new TypeError(
159
+ `SLinkedSystemsList: ${coordinate} должен быть объектом. `
160
+ + `/ SLinkedSystemsList: ${coordinate} must be an object.`,
161
+ )
162
+ }
163
+
164
+ function validateExternalUrl(value: unknown, index: number): void {
165
+ const coordinate = `items[${index}].externalUrl`
166
+ if (value === null) return
167
+ const externalUrl = validateNonEmptyString('SLinkedSystemsList', coordinate, value)
168
+ let parsedUrl: URL
169
+ try {
170
+ parsedUrl = new URL(externalUrl)
171
+ } catch (error: unknown) {
172
+ throw new TypeError(
173
+ `SLinkedSystemsList: ${coordinate} должен быть абсолютным HTTP(S) URL или null; получено ${JSON.stringify(externalUrl)}. `
174
+ + `/ SLinkedSystemsList: ${coordinate} must be an absolute HTTP(S) URL or null; received ${JSON.stringify(externalUrl)}.`,
175
+ { cause: error },
176
+ )
177
+ }
178
+ if (
179
+ (parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:')
180
+ && parsedUrl.hostname.length > 0
181
+ && externalUrl.trim() === externalUrl
182
+ ) return
183
+ throw new TypeError(
184
+ `SLinkedSystemsList: ${coordinate} должен быть абсолютным HTTP(S) URL без окружающих пробелов или null; получено ${JSON.stringify(externalUrl)}. `
185
+ + `/ SLinkedSystemsList: ${coordinate} must be an absolute HTTP(S) URL without surrounding whitespace or null; received ${JSON.stringify(externalUrl)}.`,
186
+ )
187
+ }
188
+
189
+ function validateSyncStatus(value: unknown, index: number): void {
190
+ const coordinate = `items[${index}].syncStatus`
191
+ const status = requireRecord(value, coordinate)
192
+ validateNonEmptyString('SLinkedSystemsList', `${coordinate}.label`, status.label)
193
+ validateExactString(
194
+ 'SLinkedSystemsList',
195
+ `${coordinate}.tone`,
196
+ status.tone,
197
+ ['neutral', 'info', 'success', 'warn', 'danger'] as const,
198
+ )
199
+ if (status.title !== undefined) {
200
+ validateNonEmptyString('SLinkedSystemsList', `${coordinate}.title`, status.title)
201
+ }
202
+ if (
203
+ status.icon !== undefined
204
+ && status.icon !== null
205
+ && (
206
+ Array.isArray(status.icon)
207
+ || (typeof status.icon !== 'object' && typeof status.icon !== 'function')
208
+ )
209
+ ) {
210
+ throw new TypeError(
211
+ `SLinkedSystemsList: ${coordinate}.icon должен быть Vue Component, null или undefined. `
212
+ + `/ SLinkedSystemsList: ${coordinate}.icon must be a Vue Component, null, or undefined.`,
213
+ )
214
+ }
215
+ }
216
+
217
+ const validatedItems = computed<readonly T[]>(() => {
218
+ const candidate: unknown = props.items
219
+ if (!Array.isArray(candidate)) {
220
+ throw new TypeError(
221
+ 'SLinkedSystemsList: items должен быть readonly-массивом linked-system объектов. '
222
+ + '/ SLinkedSystemsList: items must be a readonly array of linked-system objects.',
223
+ )
224
+ }
225
+ const identities = new Set<string>()
226
+ for (const [index, item] of candidate.entries()) {
227
+ const record = requireRecord(item, `items[${index}]`)
228
+ const identity = validateNonEmptyString('SLinkedSystemsList', `items[${index}].id`, record.id)
229
+ if (identities.has(identity)) {
230
+ throw new TypeError(
231
+ `SLinkedSystemsList: items должен содержать уникальные stable id; дублируется ${JSON.stringify(identity)}. `
232
+ + `/ SLinkedSystemsList: items must contain unique stable ids; duplicate ${JSON.stringify(identity)}.`,
233
+ )
234
+ }
235
+ identities.add(identity)
236
+ validateNonEmptyString('SLinkedSystemsList', `items[${index}].providerName`, record.providerName)
237
+ validateNonEmptyString('SLinkedSystemsList', `items[${index}].externalId`, record.externalId)
238
+ validateExternalUrl(record.externalUrl, index)
239
+ validateSyncStatus(record.syncStatus, index)
240
+ }
241
+ return props.items
242
+ })
243
+
244
+ watchEffect(() => {
245
+ void validatedItems.value
246
+ void validatedAccessibleLabel.value
247
+ void validatedActionsDisabled.value
248
+ if (validatedPresentation.value === 'chips' && slots.actions) {
249
+ throw new Error('SLinkedSystemsList: presentation="chips" не принимает actions slot. / Chips presentation does not accept the actions slot.')
250
+ }
251
+ })
252
+ </script>
253
+
254
+ <style lang="postcss" scoped>
255
+ @reference "../../../styles/reference.css";
256
+
257
+ .s-linked-systems-list[data-presentation='chips'] {
258
+ @apply flex min-w-0 flex-wrap items-center gap-1.5;
259
+ }
260
+
261
+ .s-linked-systems-list__chip {
262
+ @apply inline-flex min-w-0 items-center gap-1.5 rounded-full border border-surface-200 bg-surface-100 px-1.5 py-0.5 text-xs dark:border-surface-700 dark:bg-surface-700/50;
263
+ }
264
+
265
+ .s-linked-systems-list__external-id {
266
+ @apply min-w-0 font-mono;
267
+ overflow-wrap: anywhere;
268
+ word-break: break-word;
269
+ }
270
+ </style>
@@ -0,0 +1,88 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-message {
4
+ @apply flex min-w-0 items-start p-4 rounded-lg border;
5
+ }
6
+
7
+ .s-message[data-size='sm'] {
8
+ @apply p-2;
9
+ }
10
+
11
+ .s-message-icon {
12
+ @apply shrink-0 mr-3;
13
+ }
14
+
15
+ .s-message[data-size='sm'] .s-message-icon {
16
+ @apply mr-2;
17
+ }
18
+
19
+ .s-message-icon-graphic {
20
+ width: var(--s-icon-size-sm);
21
+ height: var(--s-icon-size-sm);
22
+ }
23
+
24
+ .s-message[data-size='sm'] .s-message-icon-graphic {
25
+ width: var(--s-icon-size-xs);
26
+ height: var(--s-icon-size-xs);
27
+ }
28
+
29
+ .s-message-content {
30
+ @apply min-w-0 flex-grow text-sm;
31
+ }
32
+
33
+ .s-message[data-size='sm'] .s-message-content {
34
+ @apply text-xs;
35
+ }
36
+
37
+ .s-message-title {
38
+ @apply block font-bold mb-1;
39
+ overflow-wrap: anywhere;
40
+ word-break: break-word;
41
+ }
42
+
43
+ .s-message-body {
44
+ @apply min-w-0 opacity-90;
45
+ white-space: pre-wrap;
46
+ overflow-wrap: anywhere;
47
+ word-break: break-word;
48
+ }
49
+
50
+ .s-message-close {
51
+ @apply shrink-0 ml-auto -my-1 -mr-1;
52
+ }
53
+
54
+ /* Severity: info */
55
+ .s-message[data-severity='info'] {
56
+ @apply bg-info-50 border-info-200 text-info-800;
57
+ @apply dark:bg-info-950 dark:border-info-800 dark:text-info-200;
58
+ }
59
+ .s-message[data-severity='info'] .s-message-icon {
60
+ @apply text-info-500 dark:text-info-400;
61
+ }
62
+
63
+ /* Severity: success */
64
+ .s-message[data-severity='success'] {
65
+ @apply bg-success-50 border-success-200 text-success-800;
66
+ @apply dark:bg-success-950 dark:border-success-800 dark:text-success-200;
67
+ }
68
+ .s-message[data-severity='success'] .s-message-icon {
69
+ @apply text-success-500 dark:text-success-400;
70
+ }
71
+
72
+ /* Severity: warn */
73
+ .s-message[data-severity='warn'] {
74
+ @apply bg-warn-50 border-warn-200 text-warn-800;
75
+ @apply dark:bg-warn-950 dark:border-warn-800 dark:text-warn-200;
76
+ }
77
+ .s-message[data-severity='warn'] .s-message-icon {
78
+ @apply text-warn-500 dark:text-warn-400;
79
+ }
80
+
81
+ /* Severity: danger */
82
+ .s-message[data-severity='danger'] {
83
+ @apply bg-danger-50 border-danger-200 text-danger-800;
84
+ @apply dark:bg-danger-950 dark:border-danger-800 dark:text-danger-200;
85
+ }
86
+ .s-message[data-severity='danger'] .s-message-icon {
87
+ @apply text-danger-500 dark:text-danger-400;
88
+ }