@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,152 @@
1
+ <template>
2
+ <div
3
+ v-bind="ownedAttrs.bindings()"
4
+ class="s-message"
5
+ :data-severity="validatedSeverity"
6
+ :data-size="validatedSize"
7
+ :role="liveAttributes.role"
8
+ :aria-live="liveAttributes.ariaLive"
9
+ :aria-atomic="liveAttributes.ariaAtomic"
10
+ >
11
+ <div class="s-message-icon">
12
+ <component
13
+ :is="iconComponent"
14
+ class="s-message-icon-graphic"
15
+ aria-hidden="true"
16
+ />
17
+ </div>
18
+ <div class="s-message-content">
19
+ <span v-if="title" class="s-message-title">{{ title }}</span>
20
+ <div class="s-message-body">
21
+ <slot />
22
+ </div>
23
+ </div>
24
+ <div v-if="dismissAction" class="s-message-close">
25
+ <SButton
26
+ label=""
27
+ size="sm"
28
+ appearance="ghost"
29
+ severity="secondary"
30
+ shape="circle"
31
+ icon-only
32
+ :leading-icon="X"
33
+ :title="dismissAction.label"
34
+ :aria-label="dismissAction.label"
35
+ :disabled="dismissAction.disabled"
36
+ @click="requestDismiss"
37
+ />
38
+ </div>
39
+ </div>
40
+ </template>
41
+
42
+ <script setup lang="ts">
43
+ import { computed } from 'vue'
44
+ import { X } from '../../icons/sputnigUiIcons'
45
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
46
+ import {
47
+ resolveFeedbackLive,
48
+ resolveFeedbackPresentation,
49
+ type FeedbackLiveMode,
50
+ type FeedbackSeverity,
51
+ } from '../../../internal/feedbackPresentation'
52
+ import SButton from '../controls/SButton.vue'
53
+ import {
54
+ validateBoolean,
55
+ validateExactString,
56
+ validateNonEmptyString,
57
+ } from '../../../internal/runtimeContract'
58
+
59
+ defineOptions({ inheritAttrs: false })
60
+
61
+ export type Severity = FeedbackSeverity
62
+
63
+ /** Семантический размер message surface. / Semantic message-surface size. */
64
+ export type SMessageSize = 'sm' | 'md'
65
+
66
+ /** Управляемое dismiss-действие, которое только отправляет intent родителю. / Controlled dismiss action that only emits intent to the parent. */
67
+ export interface SMessageDismissAction {
68
+ label: string
69
+ disabled: boolean
70
+ }
71
+
72
+ /** Typed payload управляемого dismiss intent. / Typed payload of a controlled dismiss intent. */
73
+ export interface SMessageDismissEvent {
74
+ source: 'close-button'
75
+ nativeEvent: MouseEvent
76
+ }
77
+
78
+ const MESSAGE_SEVERITIES = ['info', 'success', 'warn', 'danger'] as const satisfies readonly Severity[]
79
+ const MESSAGE_LIVE_MODES = ['off', 'polite', 'assertive', 'auto'] as const satisfies readonly FeedbackLiveMode[]
80
+ const MESSAGE_SIZES = ['sm', 'md'] as const satisfies readonly SMessageSize[]
81
+ const DEFAULT_DISMISS_LABEL = 'Закрыть'
82
+
83
+ export interface Props {
84
+ severity?: Severity
85
+ title?: string
86
+ closable?: boolean
87
+ dismissLabel?: string
88
+ dismissDisabled?: boolean
89
+ live?: FeedbackLiveMode
90
+ /** Семантический размер и padding сообщения. / Semantic message size and padding. */
91
+ size?: SMessageSize
92
+ }
93
+
94
+ const props = withDefaults(defineProps<Props>(), {
95
+ severity: 'info',
96
+ title: undefined,
97
+ closable: false,
98
+ dismissLabel: DEFAULT_DISMISS_LABEL,
99
+ dismissDisabled: false,
100
+ live: 'off',
101
+ size: 'md',
102
+ })
103
+
104
+ const emit = defineEmits<{
105
+ dismiss: [event: SMessageDismissEvent]
106
+ }>()
107
+ const ownedAttrs = useOwnedAttrs({ component: 'SMessage', owner: 'message root' })
108
+
109
+ const validatedSeverity = computed<Severity>(() => validateExactString(
110
+ 'SMessage',
111
+ 'severity',
112
+ props.severity,
113
+ MESSAGE_SEVERITIES,
114
+ ))
115
+ const validatedLive = computed<FeedbackLiveMode>(() => validateExactString(
116
+ 'SMessage',
117
+ 'live',
118
+ props.live,
119
+ MESSAGE_LIVE_MODES,
120
+ ))
121
+ const validatedSize = computed<SMessageSize>(() => validateExactString(
122
+ 'SMessage',
123
+ 'size',
124
+ props.size,
125
+ MESSAGE_SIZES,
126
+ ))
127
+ const dismissAction = computed<SMessageDismissAction | undefined>(() => {
128
+ const closable = validateBoolean('SMessage', 'closable', props.closable)
129
+ const disabled = validateBoolean('SMessage', 'dismissDisabled', props.dismissDisabled)
130
+ const label = validateNonEmptyString('SMessage', 'dismissLabel', props.dismissLabel)
131
+ if (!closable) {
132
+ if (disabled || label !== DEFAULT_DISMISS_LABEL) {
133
+ throw new TypeError(
134
+ 'SMessage: dismissLabel/dismissDisabled применяются только при closable=true. '
135
+ + '/ SMessage: dismissLabel/dismissDisabled apply only when closable=true.',
136
+ )
137
+ }
138
+ return undefined
139
+ }
140
+ return { label, disabled }
141
+ })
142
+
143
+ const presentation = computed(() => resolveFeedbackPresentation(validatedSeverity.value))
144
+ const iconComponent = computed(() => presentation.value.icon)
145
+ const liveAttributes = computed(() => resolveFeedbackLive(validatedSeverity.value, validatedLive.value))
146
+
147
+ function requestDismiss(nativeEvent: MouseEvent): void {
148
+ emit('dismiss', { source: 'close-button', nativeEvent })
149
+ }
150
+ </script>
151
+
152
+ <style lang="postcss" src="./SMessage.css" scoped></style>
@@ -0,0 +1,32 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-metric-card__layout { @apply flex min-h-0 min-w-0 w-full flex-1 flex-col gap-3 text-left; }
4
+ .s-metric-card__layout[data-centered='true'] { @apply items-center justify-center text-center; }
5
+ .s-metric-card__icon { @apply shrink-0 text-primary-500; }
6
+ .s-metric-card__icon-glyph { @apply h-8 w-8; }
7
+ .s-metric-card__body { @apply min-w-0; }
8
+ .s-metric-card__heading { @apply flex min-w-0 items-start justify-between gap-3; }
9
+ .s-metric-card__layout[data-centered='true'] .s-metric-card__heading { @apply justify-center; }
10
+ .s-metric-card__title { @apply min-w-0 text-xs font-medium uppercase tracking-[0.18em] text-surface-500 dark:text-surface-400; }
11
+ .s-metric-card__value { @apply mt-2 min-w-0 font-semibold tracking-tight text-surface-900 dark:text-surface-50; }
12
+ .s-metric-card__value[data-size='md'] { @apply text-2xl; }
13
+ .s-metric-card__value[data-size='lg'] { @apply text-[1.9rem] leading-tight; }
14
+ .s-metric-card__value[data-size='xl'] { @apply text-[2.2rem] leading-none; }
15
+ .s-metric-card__description { @apply mt-2 min-w-0 text-sm text-surface-500 dark:text-surface-400; }
16
+ .s-metric-card__value[data-overflow='wrap'], .s-metric-card__description[data-overflow='wrap'] {
17
+ @apply whitespace-normal;
18
+ overflow-wrap: anywhere;
19
+ }
20
+ .s-metric-card__value[data-overflow='clamp-2'], .s-metric-card__description[data-overflow='clamp-2'] {
21
+ @apply min-w-0 overflow-hidden;
22
+ display: -webkit-box;
23
+ -webkit-box-orient: vertical;
24
+ -webkit-line-clamp: 2;
25
+ overflow-wrap: anywhere;
26
+ word-break: break-word;
27
+ }
28
+ .s-metric-card__value[data-overflow='truncate'], .s-metric-card__description[data-overflow='truncate'] {
29
+ @apply min-w-0 overflow-hidden text-ellipsis whitespace-nowrap;
30
+ }
31
+ .s-metric-card__progress { @apply mt-auto space-y-2; }
32
+ .s-metric-card__progress-label { @apply text-xs text-surface-500; }
@@ -0,0 +1,117 @@
1
+ <script setup lang="ts">
2
+ import { computed, watchEffect } from 'vue'
3
+ import type { Component } from 'vue'
4
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
5
+ import type { InteractiveSurfaceContract, InteractiveSurfacePadding, InteractiveSurfaceRadius, InteractiveSurfaceTone } from '../controls/SInteractiveSurface.vue'
6
+ import SActionCard from './SActionCard.vue'
7
+ import SBadge from './SBadge.vue'
8
+ import type { BadgeSeverity } from './SBadge.vue'
9
+ import SProgressBar from './SProgressBar.vue'
10
+
11
+ defineOptions({ inheritAttrs: false })
12
+
13
+ export type MetricOverflow = 'wrap' | 'clamp-2' | 'truncate'
14
+ export type MetricValueSize = 'md' | 'lg' | 'xl'
15
+
16
+ export interface Props {
17
+ title: string
18
+ value?: string | number
19
+ description?: string
20
+ descriptionTitle?: string
21
+ icon?: Component
22
+ centered?: boolean
23
+ density?: 'compact' | 'comfortable'
24
+ tone?: InteractiveSurfaceTone
25
+ surface?: InteractiveSurfaceContract
26
+ selected?: boolean
27
+ fill?: boolean
28
+ padding?: InteractiveSurfacePadding
29
+ radius?: InteractiveSurfaceRadius
30
+ valueSize?: MetricValueSize
31
+ valueOverflow?: MetricOverflow
32
+ descriptionOverflow?: MetricOverflow
33
+ badgeLabel?: string
34
+ badgeSeverity?: BadgeSeverity
35
+ progressValue?: number | null
36
+ progressLabel?: string
37
+ progressSeverity?: 'primary' | 'success' | 'info' | 'warn' | 'danger' | 'neutral'
38
+ }
39
+
40
+ const props = withDefaults(defineProps<Props>(), {
41
+ value: undefined,
42
+ description: undefined,
43
+ descriptionTitle: undefined,
44
+ icon: undefined,
45
+ centered: false,
46
+ density: 'comfortable',
47
+ tone: 'default',
48
+ surface: undefined,
49
+ selected: false,
50
+ fill: false,
51
+ padding: undefined,
52
+ radius: 'xl',
53
+ valueSize: undefined,
54
+ valueOverflow: 'wrap',
55
+ descriptionOverflow: 'wrap',
56
+ badgeLabel: undefined,
57
+ badgeSeverity: 'secondary',
58
+ progressValue: null,
59
+ progressLabel: undefined,
60
+ progressSeverity: 'primary',
61
+ })
62
+
63
+ const emit = defineEmits<{ activate: [event: MouseEvent] }>()
64
+ const ownedAttrs = useOwnedAttrs({ component: 'SMetricCard', owner: 'metric card surface' })
65
+ const resolvedProgressValue = computed(() => props.progressValue)
66
+ const resolvedProgressLabel = computed(() => props.progressLabel ?? `${props.progressValue}%`)
67
+ const progressAccessibility = computed(() => ({
68
+ mode: 'label' as const,
69
+ label: `${props.title}: ${resolvedProgressLabel.value}`,
70
+ }))
71
+ const resolvedValueSize = computed(() => props.valueSize ?? (props.density === 'compact' ? 'md' : 'xl'))
72
+ const resolvedSurface = computed<InteractiveSurfaceContract>(() => props.surface ?? { mode: 'static' })
73
+ const resolvedPadding = computed<InteractiveSurfacePadding>(() => props.padding ?? (props.density === 'compact' ? 'sm' : 'md'))
74
+
75
+ watchEffect(() => {
76
+ if (props.progressValue === null) return
77
+ if (!Number.isFinite(props.progressValue) || props.progressValue < 0 || props.progressValue > 100) {
78
+ throw new RangeError('SMetricCard: progressValue должен быть конечным числом от 0 до 100')
79
+ }
80
+ })
81
+ </script>
82
+
83
+ <template>
84
+ <SActionCard
85
+ v-bind="ownedAttrs.bindings()"
86
+ :surface="resolvedSurface"
87
+ :selected="selected"
88
+ :tone="tone"
89
+ :density="density"
90
+ :padding="resolvedPadding"
91
+ :radius="radius"
92
+ :fill="fill ? 'container' : 'content'"
93
+ @activate="emit('activate', $event)"
94
+ >
95
+ <div class="s-metric-card__layout" :data-centered="centered || undefined">
96
+ <div v-if="icon || $slots.icon" class="s-metric-card__icon" aria-hidden="true">
97
+ <slot name="icon"><component :is="icon" class="s-metric-card__icon-glyph" /></slot>
98
+ </div>
99
+ <div class="s-metric-card__body">
100
+ <div class="s-metric-card__heading">
101
+ <div class="s-metric-card__title">{{ title }}</div>
102
+ <SBadge v-if="badgeLabel" :severity="badgeSeverity">{{ badgeLabel }}</SBadge>
103
+ </div>
104
+ <div v-if="value !== undefined" class="s-metric-card__value" :data-size="resolvedValueSize" :data-overflow="valueOverflow">{{ value }}</div>
105
+ <div v-if="description" class="s-metric-card__description" :data-overflow="descriptionOverflow" :title="descriptionTitle">{{ description }}</div>
106
+ </div>
107
+ <div v-if="resolvedProgressValue !== null" class="s-metric-card__progress">
108
+ <SProgressBar :accessibility="progressAccessibility" :value="resolvedProgressValue" :severity="progressSeverity" />
109
+ <div class="s-metric-card__progress-label">{{ resolvedProgressLabel }}</div>
110
+ </div>
111
+ <slot />
112
+ </div>
113
+ <template v-if="$slots.actions" #actions><slot name="actions" /></template>
114
+ </SActionCard>
115
+ </template>
116
+
117
+ <style lang="postcss" src="./SMetricCard.css" scoped></style>
@@ -0,0 +1,84 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-progress-bar {
4
+ @apply w-full h-4 rounded-full;
5
+ background-color: var(--s-progress-bar-track-surface);
6
+ @apply overflow-hidden relative;
7
+ }
8
+
9
+ .s-progress-bar[data-size='xs'] { @apply h-1.5; }
10
+ .s-progress-bar[data-size='sm'] { @apply h-2; }
11
+
12
+ .s-progress-bar-value {
13
+ @apply h-full rounded-full;
14
+ @apply transition-all ease-in-out;
15
+ transition-duration: var(--s-motion-deliberate);
16
+ @apply flex items-center justify-center;
17
+ }
18
+
19
+ .s-progress-bar-label {
20
+ color: var(--s-progress-bar-label-foreground);
21
+ @apply text-xs font-semibold;
22
+ }
23
+
24
+ /* Severity Colors */
25
+ .s-progress-bar-value[data-severity='primary'] {
26
+ background-color: var(--s-progress-bar-fill-primary);
27
+ }
28
+ .s-progress-bar-value[data-severity='success'] {
29
+ background-color: var(--s-progress-bar-fill-success);
30
+ }
31
+ .s-progress-bar-value[data-severity='info'] {
32
+ background-color: var(--s-progress-bar-fill-info);
33
+ }
34
+ .s-progress-bar-value[data-severity='warn'] {
35
+ background-color: var(--s-progress-bar-fill-warn);
36
+ }
37
+ .s-progress-bar-value[data-severity='danger'] {
38
+ background-color: var(--s-progress-bar-fill-danger);
39
+ }
40
+ .s-progress-bar-value[data-severity='neutral'] {
41
+ background-color: var(--s-progress-bar-fill-neutral);
42
+ }
43
+
44
+ /* --- Indeterminate Mode --- */
45
+ .s-progress-bar-value--indeterminate {
46
+ @apply absolute h-full w-full top-0 left-0;
47
+ }
48
+
49
+ .s-progress-bar-value--indeterminate::before {
50
+ content: '';
51
+ @apply absolute h-full top-0;
52
+ width: 40%;
53
+ border-radius: var(--s-radius-pill);
54
+ animation: s-progress-bar-indeterminate-anim 2s infinite ease-in-out;
55
+ }
56
+
57
+ @keyframes s-progress-bar-indeterminate-anim {
58
+ 0% {
59
+ left: -50%;
60
+ }
61
+ 100% {
62
+ left: 100%;
63
+ }
64
+ }
65
+
66
+ /* Indeterminate Severity Colors */
67
+ .s-progress-bar-value--indeterminate[data-severity='primary']::before {
68
+ background-color: var(--s-progress-bar-fill-primary);
69
+ }
70
+ .s-progress-bar-value--indeterminate[data-severity='success']::before {
71
+ background-color: var(--s-progress-bar-fill-success);
72
+ }
73
+ .s-progress-bar-value--indeterminate[data-severity='info']::before {
74
+ background-color: var(--s-progress-bar-fill-info);
75
+ }
76
+ .s-progress-bar-value--indeterminate[data-severity='warn']::before {
77
+ background-color: var(--s-progress-bar-fill-warn);
78
+ }
79
+ .s-progress-bar-value--indeterminate[data-severity='danger']::before {
80
+ background-color: var(--s-progress-bar-fill-danger);
81
+ }
82
+ .s-progress-bar-value--indeterminate[data-severity='neutral']::before {
83
+ background-color: var(--s-progress-bar-indeterminate-neutral-fill);
84
+ }
@@ -0,0 +1,114 @@
1
+ <template>
2
+ <div
3
+ v-bind="ownedAttrs.bindings()"
4
+ class="s-progress-bar"
5
+ :class="{ 's-progress-bar--indeterminate': mode === 'indeterminate' }"
6
+ :role="accessibility.mode === 'decorative' ? undefined : 'progressbar'"
7
+ :aria-label="accessibility.mode === 'label' ? accessibility.label : undefined"
8
+ :aria-labelledby="accessibility.mode === 'labelledby' ? accessibility.id : undefined"
9
+ :aria-hidden="accessibility.mode === 'decorative' || undefined"
10
+ :aria-valuenow="accessibility.mode !== 'decorative' && mode === 'determinate' ? value : undefined"
11
+ :data-size="size"
12
+ :aria-valuemin="accessibility.mode === 'decorative' ? undefined : 0"
13
+ :aria-valuemax="accessibility.mode === 'decorative' ? undefined : 100"
14
+ >
15
+ <div
16
+ v-if="mode === 'determinate'"
17
+ class="s-progress-bar-value"
18
+ :data-severity="severity"
19
+ :style="{ width: `${value}%` }"
20
+ >
21
+ <span v-if="showValue" class="s-progress-bar-label">
22
+ <slot :value="value">{{ value }}%</slot>
23
+ </span>
24
+ </div>
25
+ <div
26
+ v-else
27
+ class="s-progress-bar-value--indeterminate"
28
+ :data-severity="severity"
29
+ ></div>
30
+ </div>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import { watchEffect } from 'vue';
35
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs';
36
+
37
+ defineOptions({ inheritAttrs: false });
38
+
39
+ export type Severity = 'primary' | 'success' | 'info' | 'warn' | 'danger' | 'neutral';
40
+ export type ProgressBarMode = 'determinate' | 'indeterminate';
41
+ export type SProgressBarAccessibility =
42
+ | { readonly mode: 'decorative' }
43
+ | { readonly mode: 'label'; readonly label: string }
44
+ | { readonly mode: 'labelledby'; readonly id: string };
45
+
46
+ export interface Props {
47
+ /** Обязательное accessible-name решение либо явный decorative режим. / Required accessible-name decision or explicit decorative mode. */
48
+ accessibility: SProgressBarAccessibility;
49
+ value?: number;
50
+ severity?: Severity;
51
+ showValue?: boolean;
52
+ mode?: ProgressBarMode;
53
+ size?: 'xs' | 'sm' | 'md';
54
+ }
55
+
56
+ const props = withDefaults(defineProps<Props>(), {
57
+ value: 0,
58
+ severity: 'primary',
59
+ showValue: false,
60
+ mode: 'determinate',
61
+ size: 'md',
62
+ });
63
+
64
+ const ownedAttrs = useOwnedAttrs({ component: 'SProgressBar', owner: 'progressbar root' });
65
+
66
+ watchEffect(() => {
67
+ if (!Number.isFinite(props.value) || props.value < 0 || props.value > 100) {
68
+ throw new RangeError('SProgressBar: value должен быть конечным числом от 0 до 100');
69
+ }
70
+ if (!['determinate', 'indeterminate'].includes(props.mode)) {
71
+ throw new TypeError(`SProgressBar: неизвестный mode "${String(props.mode)}"`);
72
+ }
73
+ if (!['primary', 'success', 'info', 'warn', 'danger', 'neutral'].includes(props.severity)) {
74
+ throw new TypeError(`SProgressBar: неизвестный severity "${String(props.severity)}"`);
75
+ }
76
+ if (!['xs', 'sm', 'md'].includes(props.size)) {
77
+ throw new TypeError(`SProgressBar: неизвестный size "${String(props.size)}"`);
78
+ }
79
+ if (props.showValue && props.mode !== 'determinate') {
80
+ throw new Error('SProgressBar: showValue разрешён только в determinate mode');
81
+ }
82
+
83
+ const contract: unknown = props.accessibility;
84
+ if (typeof contract !== 'object' || contract === null || Array.isArray(contract)) {
85
+ throw new TypeError('SProgressBar: accessibility должен быть discriminated object');
86
+ }
87
+ const record = contract as Record<string, unknown>;
88
+ const allowedKeys = record.mode === 'label'
89
+ ? ['mode', 'label']
90
+ : record.mode === 'labelledby'
91
+ ? ['mode', 'id']
92
+ : ['mode'];
93
+ const unexpected = Object.keys(record).filter((key) => !allowedKeys.includes(key));
94
+ if (unexpected.length > 0) {
95
+ throw new Error(`SProgressBar: accessibility.mode="${String(record.mode)}" не допускает поля ${unexpected.join(', ')}`);
96
+ }
97
+ if (record.mode === 'decorative') return;
98
+ if (record.mode === 'label') {
99
+ if (typeof record.label !== 'string' || record.label.trim().length === 0) {
100
+ throw new TypeError('SProgressBar: accessibility.label должен быть непустой строкой');
101
+ }
102
+ return;
103
+ }
104
+ if (record.mode === 'labelledby') {
105
+ if (typeof record.id !== 'string' || record.id.trim().length === 0 || /\s/u.test(record.id)) {
106
+ throw new TypeError('SProgressBar: accessibility.id должен быть одним непустым DOM id');
107
+ }
108
+ return;
109
+ }
110
+ throw new TypeError(`SProgressBar: неизвестный accessibility.mode "${String(record.mode)}"`);
111
+ });
112
+ </script>
113
+
114
+ <style lang="postcss" src="./SProgressBar.css" scoped></style>
@@ -0,0 +1,59 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-progress-indicator {
4
+ display: inline-flex;
5
+ flex: 0 0 auto;
6
+ align-items: center;
7
+ justify-content: center;
8
+ width: var(--s-progress-indicator-size);
9
+ height: var(--s-progress-indicator-size);
10
+ color: currentColor;
11
+ }
12
+
13
+ .s-progress-indicator[data-show-label='true'] {
14
+ width: auto;
15
+ max-width: 100%;
16
+ height: auto;
17
+ gap: var(--spacing-2);
18
+ justify-content: flex-start;
19
+ }
20
+
21
+ .s-progress-indicator[data-size='xs'] { --s-progress-indicator-size: var(--s-icon-size-xs); }
22
+ .s-progress-indicator[data-size='sm'] { --s-progress-indicator-size: var(--s-icon-size-sm); }
23
+ .s-progress-indicator[data-size='md'] { --s-progress-indicator-size: var(--s-icon-size-md); }
24
+ .s-progress-indicator[data-size='lg'] { --s-progress-indicator-size: var(--s-icon-size-lg); }
25
+
26
+ .s-progress-indicator[data-tone='neutral'] { color: var(--s-progress-indicator-foreground-neutral); }
27
+ .s-progress-indicator[data-tone='info'] { color: var(--s-progress-indicator-foreground-info); }
28
+ .s-progress-indicator[data-tone='success'] { color: var(--s-progress-indicator-foreground-success); }
29
+ .s-progress-indicator[data-tone='warn'] { color: var(--s-progress-indicator-foreground-warn); }
30
+ .s-progress-indicator[data-tone='danger'] { color: var(--s-progress-indicator-foreground-danger); }
31
+
32
+ .s-progress-indicator__graphic {
33
+ width: 100%;
34
+ height: 100%;
35
+ }
36
+
37
+ .s-progress-indicator[data-show-label='true'] .s-progress-indicator__graphic {
38
+ flex: 0 0 var(--s-progress-indicator-size);
39
+ width: var(--s-progress-indicator-size);
40
+ height: var(--s-progress-indicator-size);
41
+ }
42
+
43
+ .s-progress-indicator__label {
44
+ min-width: 0;
45
+ overflow-wrap: anywhere;
46
+ color: currentColor;
47
+ }
48
+
49
+ .s-progress-indicator[data-motion='spin'] .s-progress-indicator__graphic {
50
+ animation: s-progress-indicator-spin calc(var(--s-motion-deliberate) * 4) linear infinite;
51
+ }
52
+
53
+ @keyframes s-progress-indicator-spin {
54
+ to { transform: rotate(360deg); }
55
+ }
56
+
57
+ @media (prefers-reduced-motion: reduce) {
58
+ .s-progress-indicator__graphic { animation: none; }
59
+ }
@@ -0,0 +1,104 @@
1
+ <template>
2
+ <span
3
+ v-bind="ownedAttrs.bindings()"
4
+ class="s-progress-indicator"
5
+ :data-size="size"
6
+ :data-tone="tone"
7
+ :data-motion="presentation.motion"
8
+ :data-show-label="validatedShowLabel || undefined"
9
+ :role="liveAttributes.role"
10
+ :aria-live="liveAttributes.ariaLive"
11
+ :aria-atomic="liveAttributes.ariaAtomic"
12
+ :aria-label="accessibleLabel"
13
+ :aria-hidden="accessibility.mode === 'decorative' || undefined"
14
+ >
15
+ <component
16
+ :is="presentation.icon"
17
+ class="s-progress-indicator__graphic"
18
+ :data-sputnig-ui-icon="presentation.iconName"
19
+ data-sputnig-ui-icon-color-mode="auto"
20
+ data-sputnig-ui-icon-set="classic"
21
+ />
22
+ <span v-if="validatedShowLabel && accessibility.mode === 'status'" class="s-progress-indicator__label">
23
+ {{ accessibility.label }}
24
+ </span>
25
+ </span>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { computed, watchEffect } from 'vue'
30
+
31
+ import { resolveFeedbackIndicator, resolveFeedbackLive, type FeedbackLiveMode } from '../../../internal/feedbackPresentation'
32
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
33
+ import { validateBoolean } from '../../../internal/runtimeContract'
34
+
35
+ defineOptions({ inheritAttrs: false })
36
+
37
+ export type SProgressIndicatorSize = 'xs' | 'sm' | 'md' | 'lg'
38
+ export type SProgressIndicatorTone = 'current' | 'neutral' | 'info' | 'success' | 'warn' | 'danger'
39
+
40
+ /**
41
+ * Декоративный progress принадлежит labelled parent; standalone status требует собственного имени.
42
+ * Decorative progress belongs to a labelled parent; a standalone status requires its own name.
43
+ */
44
+ export type SProgressIndicatorAccessibility =
45
+ | { readonly mode: 'decorative' }
46
+ | {
47
+ readonly mode: 'status'
48
+ readonly label: string
49
+ readonly live?: Exclude<FeedbackLiveMode, 'off'>
50
+ }
51
+
52
+ export interface Props {
53
+ accessibility: SProgressIndicatorAccessibility
54
+ size?: SProgressIndicatorSize
55
+ tone?: SProgressIndicatorTone
56
+ /** Показывает status label рядом с индикатором. / Shows the status label next to the indicator. */
57
+ showLabel?: boolean
58
+ }
59
+
60
+ const props = withDefaults(defineProps<Props>(), {
61
+ size: 'sm',
62
+ tone: 'current',
63
+ showLabel: false,
64
+ })
65
+
66
+ const ownedAttrs = useOwnedAttrs({ component: 'SProgressIndicator', owner: 'progress indicator root' })
67
+ const presentation = resolveFeedbackIndicator('progress')
68
+ const validatedShowLabel = computed(() => validateBoolean('SProgressIndicator', 'showLabel', props.showLabel))
69
+ const feedbackTone = computed(() => props.tone === 'current' ? 'info' : props.tone)
70
+ const accessibleLabel = computed(() => props.accessibility.mode === 'status'
71
+ ? props.accessibility.label
72
+ : undefined)
73
+ const liveAttributes = computed(() => props.accessibility.mode === 'status'
74
+ ? resolveFeedbackLive(feedbackTone.value, props.accessibility.live ?? 'auto')
75
+ : {})
76
+
77
+ watchEffect(() => {
78
+ const contract: unknown = props.accessibility
79
+ if (typeof contract !== 'object' || contract === null || Array.isArray(contract)) {
80
+ throw new TypeError('SProgressIndicator: accessibility должен быть discriminated object')
81
+ }
82
+ const record = contract as Record<string, unknown>
83
+ if (validatedShowLabel.value && record.mode === 'decorative') {
84
+ throw new Error('SProgressIndicator: showLabel требует accessibility.mode="status" / showLabel requires accessibility.mode="status"')
85
+ }
86
+ const allowedKeys = record.mode === 'decorative' ? ['mode'] : ['mode', 'label', 'live']
87
+ const unexpected = Object.keys(record).filter((key) => !allowedKeys.includes(key))
88
+ if (unexpected.length > 0) {
89
+ throw new Error(`SProgressIndicator: accessibility.mode="${String(record.mode)}" не допускает поля ${unexpected.join(', ')}`)
90
+ }
91
+ if (record.mode === 'decorative') return
92
+ if (record.mode !== 'status') {
93
+ throw new TypeError(`SProgressIndicator: неизвестный accessibility.mode "${String(record.mode)}"`)
94
+ }
95
+ if (typeof record.label !== 'string' || record.label.trim().length === 0) {
96
+ throw new TypeError('SProgressIndicator: accessibility.label должен быть непустой строкой для mode="status"')
97
+ }
98
+ if (record.live !== undefined && !['auto', 'polite', 'assertive'].includes(String(record.live))) {
99
+ throw new TypeError(`SProgressIndicator: accessibility.live имеет недопустимое значение "${String(record.live)}"`)
100
+ }
101
+ })
102
+ </script>
103
+
104
+ <style lang="postcss" src="./SProgressIndicator.css" scoped></style>