@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,134 @@
1
+ <template>
2
+ <SFieldGroup
3
+ v-bind="ownedAttrs.bindings()"
4
+ :id="groupId"
5
+ :label="label"
6
+ :help-text="helpText"
7
+ :error-message="errorMessage"
8
+ :required="required"
9
+ :invalid="invalid"
10
+ :disabled="disabled"
11
+ :readonly="readonly"
12
+ :layout="layout"
13
+ :columns="columns"
14
+ >
15
+ <template #default="group">
16
+ <SCheckbox
17
+ v-for="record in validatedOptions"
18
+ :id="record.id"
19
+ :key="record.token"
20
+ :model-value="selectedTokens.has(record.token)"
21
+ :label="record.option.label"
22
+ :help-text="record.option.helpText"
23
+ :disabled="group.disabled || record.option.disabled"
24
+ :readonly="group.readonly"
25
+ :invalid="group.invalid"
26
+ :aria-describedby="group.describedBy"
27
+ @update:model-value="updateOption(record.option.value, $event)"
28
+ />
29
+ </template>
30
+ </SFieldGroup>
31
+ </template>
32
+
33
+ <script lang="ts">
34
+ import type { SelectionKey as PublicSelectionKey } from '../_internal/useSelectionRoving'
35
+
36
+ /** Одна checkbox option со стабильной domain identity. / One checkbox option with stable domain identity. */
37
+ export interface SCheckboxGroupOption<T extends PublicSelectionKey = PublicSelectionKey> {
38
+ value: T
39
+ label: string
40
+ helpText?: string
41
+ disabled?: boolean
42
+ }
43
+ </script>
44
+
45
+ <script setup lang="ts" generic="T extends SelectionKey = string">
46
+ import { computed, useId } from 'vue'
47
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
48
+ import { validateDomId, validateNonEmptyString } from '../../../internal/runtimeContract'
49
+ import {
50
+ selectionKeyToken,
51
+ validateSelectionIdentities,
52
+ type SelectionKey,
53
+ } from '../_internal/useSelectionRoving'
54
+ import SCheckbox from './SCheckbox.vue'
55
+ import SFieldGroup from './SFieldGroup.vue'
56
+ import type { SFieldGroupColumns, SFieldGroupLayout } from './SFieldGroup.vue'
57
+
58
+ defineOptions({ inheritAttrs: false })
59
+
60
+ const props = withDefaults(defineProps<{
61
+ modelValue: readonly T[]
62
+ options: readonly SCheckboxGroupOption<T>[]
63
+ label: string
64
+ id?: string
65
+ helpText?: string
66
+ errorMessage?: string
67
+ required?: boolean
68
+ invalid?: boolean
69
+ disabled?: boolean
70
+ readonly?: boolean
71
+ layout?: SFieldGroupLayout
72
+ columns?: SFieldGroupColumns
73
+ }>(), {
74
+ id: undefined,
75
+ helpText: undefined,
76
+ errorMessage: undefined,
77
+ required: false,
78
+ invalid: false,
79
+ disabled: false,
80
+ readonly: false,
81
+ layout: 'stacked',
82
+ columns: 1,
83
+ })
84
+ const emit = defineEmits<{ 'update:modelValue': [value: readonly T[]] }>()
85
+ const ownedAttrs = useOwnedAttrs({ component: 'SCheckboxGroup', owner: 'field group' })
86
+ const generatedId = useId()
87
+ const groupId = computed(() => validateDomId(
88
+ 'SCheckboxGroup',
89
+ 'id',
90
+ props.id ?? `s-checkbox-group-${generatedId}`,
91
+ ))
92
+
93
+ const validatedOptions = computed(() => {
94
+ validateSelectionIdentities('SCheckboxGroup', props.options.map((option, index) => ({
95
+ key: option.value,
96
+ coordinate: `options[${index}].value`,
97
+ })))
98
+ return props.options.map((option, index) => {
99
+ validateNonEmptyString('SCheckboxGroup', `options[${index}].label`, option.label)
100
+ const token = selectionKeyToken('SCheckboxGroup', option.value, `options[${index}].value`)
101
+ return { option, token, id: `${groupId.value}-option-${token}` }
102
+ })
103
+ })
104
+
105
+ const optionsByToken = computed<ReadonlyMap<string, T>>(() => new Map(
106
+ validatedOptions.value.map((record) => [record.token, record.option.value]),
107
+ ))
108
+
109
+ const selectedTokens = computed(() => {
110
+ const modelIndex = validateSelectionIdentities('SCheckboxGroup', props.modelValue.map((value, index) => ({
111
+ key: value,
112
+ coordinate: `modelValue[${index}]`,
113
+ })))
114
+ const optionIndex = optionsByToken.value
115
+ for (const [token] of modelIndex) {
116
+ if (!optionIndex.has(token)) {
117
+ const index = props.modelValue.findIndex((value) => selectionKeyToken('SCheckboxGroup', value, 'modelValue') === token)
118
+ throw new Error(`SCheckboxGroup: modelValue[${index}] does not match any option`)
119
+ }
120
+ }
121
+ return new Set(modelIndex.keys())
122
+ })
123
+
124
+ function updateOption(value: T, selected: boolean): void {
125
+ if (props.readonly || props.disabled) return
126
+ const targetToken = selectionKeyToken('SCheckboxGroup', value, 'updated option value')
127
+ const nextTokens = new Set(selectedTokens.value)
128
+ if (selected) nextTokens.add(targetToken)
129
+ else nextTokens.delete(targetToken)
130
+ emit('update:modelValue', validatedOptions.value
131
+ .filter((record) => nextTokens.has(record.token))
132
+ .map((record) => record.option.value))
133
+ }
134
+ </script>
@@ -0,0 +1,29 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-choice-cards {
4
+ display: grid;
5
+ gap: 0.75rem;
6
+ }
7
+
8
+ .s-choice-cards[data-columns='auto'] {
9
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
10
+ }
11
+
12
+ .s-choice-cards[data-columns='2'] {
13
+ grid-template-columns: repeat(2, minmax(0, 1fr));
14
+ }
15
+
16
+ .s-choice-cards[data-columns='3'] {
17
+ grid-template-columns: repeat(3, minmax(0, 1fr));
18
+ }
19
+
20
+ .s-choice-card { @apply relative flex min-h-28 min-w-0 flex-col items-start gap-2 overflow-hidden rounded-2xl border border-surface-300 bg-surface-0 p-4 text-left text-surface-800 hover:border-primary-300 hover:bg-primary-50/30 disabled:cursor-not-allowed disabled:opacity-50 dark:border-surface-700 dark:bg-surface-900 dark:text-surface-100; }
21
+ .s-choice-card:focus-visible { outline: var(--s-focus-ring-width) solid var(--s-focus-ring-color); outline-offset: var(--s-focus-ring-offset); }
22
+ .s-choice-card[aria-checked='true'] { @apply border-primary-500 ring-1 ring-primary-500/20; }
23
+ .s-choice-card__kicker { @apply text-[0.7rem] font-semibold uppercase tracking-wide text-surface-500; }
24
+ .s-choice-card__title { @apply text-sm font-semibold leading-5; }
25
+ .s-choice-card__description { @apply text-xs leading-5 text-surface-500; }
26
+ .s-choice-card__meta { @apply mt-auto text-xs leading-4 text-surface-500; }
27
+ .s-choice-card__mark { @apply absolute right-4 top-4 inline-flex h-6 w-6 items-center justify-center rounded-full bg-primary-100 font-bold text-primary-600; }
28
+ .s-choice-cards[data-density='compact'] .s-choice-card { @apply min-h-24 p-3; }
29
+ .s-choice-cards[aria-readonly='true'] .s-choice-card { @apply cursor-default; }
@@ -0,0 +1,145 @@
1
+ <template>
2
+ <SFieldGroup
3
+ v-bind="ownedAttrs.bindings()"
4
+ :id="id"
5
+ :label="label"
6
+ :help-text="helpText"
7
+ :error-message="errorMessage"
8
+ :required="required"
9
+ :invalid="invalid"
10
+ :disabled="disabled"
11
+ :readonly="readonly"
12
+ >
13
+ <template #default="{ describedBy, labelId }">
14
+ <div
15
+ class="s-choice-cards"
16
+ :data-columns="columns"
17
+ :data-density="density"
18
+ :data-selection-indicator="selectionIndicator"
19
+ role="radiogroup"
20
+ :aria-labelledby="labelId"
21
+ :aria-describedby="describedBy"
22
+ :aria-required="required || undefined"
23
+ :aria-invalid="invalid || undefined"
24
+ :aria-disabled="disabled || undefined"
25
+ :aria-readonly="readonly || undefined"
26
+ >
27
+ <button
28
+ v-for="option in options"
29
+ :key="optionToken(option)"
30
+ :ref="(element) => roving.register(option.value, resolveNativeElement(element))"
31
+ class="s-choice-card"
32
+ type="button"
33
+ :disabled="disabled || option.disabled"
34
+ :aria-checked="option.value === modelValue"
35
+ :tabindex="option.value === focusableValue ? 0 : -1"
36
+ role="radio"
37
+ @click="select(option.value)"
38
+ @keydown="roving.handleKeydown($event, option.value)"
39
+ >
40
+ <span v-if="option.kicker" class="s-choice-card__kicker">{{ option.kicker }}</span>
41
+ <span class="s-choice-card__title">{{ option.title }}</span>
42
+ <span v-if="option.description" class="s-choice-card__description">{{ option.description }}</span>
43
+ <span v-if="option.meta" class="s-choice-card__meta">{{ option.meta }}</span>
44
+ <span v-if="selectionIndicator === 'mark' && option.value === modelValue" class="s-choice-card__mark" aria-hidden="true">✓</span>
45
+ </button>
46
+ </div>
47
+ </template>
48
+ <template v-if="$slots.help" #help><slot name="help" /></template>
49
+ <template v-if="$slots.error" #error><slot name="error" /></template>
50
+ </SFieldGroup>
51
+ </template>
52
+
53
+ <script lang="ts">
54
+ import type { SelectionKey as PublicSelectionKey } from '../_internal/useSelectionRoving'
55
+
56
+ /** Одна radio-card option. / One radio-card option. */
57
+ export interface ChoiceCardOption<T extends PublicSelectionKey = string> {
58
+ value: T
59
+ title: string
60
+ description?: string
61
+ meta?: string
62
+ kicker?: string
63
+ disabled?: boolean
64
+ }
65
+
66
+ /** Публичный контракт группы карточек выбора. / Public choice-card group contract. */
67
+ export interface Props<T extends PublicSelectionKey = string> {
68
+ modelValue: T | null
69
+ options: readonly ChoiceCardOption<T>[]
70
+ label: string
71
+ id?: string
72
+ helpText?: string
73
+ errorMessage?: string
74
+ required?: boolean
75
+ invalid?: boolean
76
+ disabled?: boolean
77
+ readonly?: boolean
78
+ columns?: 'auto' | '2' | '3'
79
+ density?: 'default' | 'compact'
80
+ selectionIndicator?: 'mark' | 'none'
81
+ }
82
+ </script>
83
+
84
+ <script setup lang="ts" generic="T extends SelectionKey = string">
85
+ import { computed } from 'vue'
86
+ import type { ComponentPublicInstance, VNodeChild } from 'vue'
87
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
88
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
89
+ import { selectionKeyToken, useSelectionRoving } from '../_internal/useSelectionRoving'
90
+ import type { SelectionKey } from '../_internal/useSelectionRoving'
91
+ import SFieldGroup from './SFieldGroup.vue'
92
+
93
+ defineOptions({ inheritAttrs: false })
94
+
95
+ const props = withDefaults(defineProps<Props<T>>(), {
96
+ id: undefined,
97
+ helpText: undefined,
98
+ errorMessage: undefined,
99
+ required: false,
100
+ invalid: false,
101
+ disabled: false,
102
+ readonly: false,
103
+ columns: 'auto',
104
+ density: 'default',
105
+ selectionIndicator: 'mark',
106
+ })
107
+
108
+ const emit = defineEmits<{
109
+ 'update:modelValue': [value: T]
110
+ }>()
111
+ defineSlots<{
112
+ /** Дополнительное описание группы. / Supporting group description. */
113
+ help?: () => VNodeChild
114
+ /** Сообщение ошибки при invalid=true. / Error message when invalid=true. */
115
+ error?: () => VNodeChild
116
+ }>()
117
+ const ownedAttrs = useOwnedAttrs({ component: 'SChoiceCards', owner: 'field group' })
118
+ useInteractiveLeafRegistration({ owner: 'SChoiceCards' })
119
+ const select = (value: T): void => {
120
+ if (props.disabled || props.readonly) return
121
+ emit('update:modelValue', value)
122
+ }
123
+ const rovingOptions = computed<readonly ChoiceCardOption<T>[]>(() => (
124
+ props.disabled
125
+ ? props.options.map((option) => ({ ...option, disabled: true }))
126
+ : props.options
127
+ ))
128
+ const roving = useSelectionRoving({
129
+ options: rovingOptions,
130
+ selectedValue: computed(() => props.modelValue),
131
+ select,
132
+ component: 'SChoiceCards',
133
+ })
134
+ const focusableValue = roving.focusableValue
135
+ const optionToken = (option: ChoiceCardOption<T>): string => selectionKeyToken(
136
+ 'SChoiceCards',
137
+ option.value,
138
+ 'option.value',
139
+ )
140
+ const resolveNativeElement = (element: Element | ComponentPublicInstance | null): HTMLElement | null => (
141
+ element instanceof HTMLElement ? element : null
142
+ )
143
+ </script>
144
+
145
+ <style lang="postcss" src="./SChoiceCards.css" scoped></style>
@@ -0,0 +1,146 @@
1
+ <template>
2
+ <SSelect
3
+ v-bind="ownedAttrs.bindings()"
4
+ :id="id"
5
+ :model-value="modelValue"
6
+ :options="resolvedOptions"
7
+ selection-mode="single"
8
+ :disabled="disabled"
9
+ :placeholder="computedAriaLabel"
10
+ :title="resolvedTitle"
11
+ :size="size"
12
+ :align="align"
13
+ :trigger-marker="triggerMarker"
14
+ appearance="swatch"
15
+ @update:model-value="updateColor"
16
+ />
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { computed } from 'vue'
21
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
22
+ import { validateNonEmptyString } from '../../../internal/runtimeContract'
23
+ import SSelect, { type SSelectOption as BaseSelectOption } from './SSelect.vue'
24
+ import type { SMarker, SMarkerTone } from '../_internal/markerContract'
25
+
26
+ interface SColorSelectOptionBase {
27
+ value: string
28
+ label: string
29
+ title?: string
30
+ disabled?: boolean
31
+ }
32
+
33
+ /** Предопределённый цвет из палитры. / A predefined palette color. */
34
+ export type SColorSelectPresetOption = SColorSelectOptionBase & {
35
+ kind?: 'preset'
36
+ marker: { tone: SMarkerTone }
37
+ }
38
+
39
+ /**
40
+ * Явный пользовательский цвет с собственным значением и подписью.
41
+ * An explicit custom color with its own value and label.
42
+ */
43
+ export type SColorSelectCustomOption = SColorSelectOptionBase & {
44
+ kind: 'custom'
45
+ marker: Extract<SMarker, { tone: 'custom' }>
46
+ }
47
+
48
+ /** Закрытый контракт вариантов выбора цвета. / Closed color-selection option contract. */
49
+ export type SColorSelectOption = SColorSelectPresetOption | SColorSelectCustomOption
50
+ defineOptions({ inheritAttrs: false })
51
+
52
+ const props = withDefaults(defineProps<{
53
+ modelValue: string; options: readonly SColorSelectOption[]; id?: string; disabled?: boolean; title?: string;
54
+ ariaLabel?: string; size?: 'sm' | 'md'; align?: 'left' | 'right' | 'center'
55
+ }>(), { id: undefined, disabled: false, title: undefined, ariaLabel: undefined, size: 'md', align: 'left' })
56
+ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
57
+ const ownedAttrs = useOwnedAttrs({ component: 'SColorSelect', owner: 'combobox trigger' })
58
+
59
+ const allowedOptionKeys = new Set(['kind', 'value', 'label', 'marker', 'title', 'disabled'])
60
+ const resolvedOptions = computed<readonly BaseSelectOption<string>[]>(() => {
61
+ if (!Array.isArray(props.options)) {
62
+ throw new TypeError('SColorSelect: options должен быть массивом. / SColorSelect: options must be an array.')
63
+ }
64
+
65
+ return props.options.map((option, index) => {
66
+ const candidate: unknown = option
67
+ if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) {
68
+ throw new TypeError(
69
+ `SColorSelect: options[${index}] должен быть объектом option. `
70
+ + `/ SColorSelect: options[${index}] must be an option object`,
71
+ )
72
+ }
73
+ const unsupportedKey = Object.keys(candidate).find((key) => !allowedOptionKeys.has(key))
74
+ if (unsupportedKey !== undefined) {
75
+ throw new TypeError(
76
+ `SColorSelect: options[${index}].${unsupportedKey} не входит в контракт color option. `
77
+ + `/ SColorSelect: options[${index}].${unsupportedKey} is not part of the color option contract`,
78
+ )
79
+ }
80
+ const marker = (candidate as { marker?: SMarker }).marker
81
+ const kind = (candidate as { kind?: unknown }).kind
82
+ if (marker === undefined) {
83
+ throw new TypeError(
84
+ `SColorSelect: options[${index}].marker обязателен. `
85
+ + `/ SColorSelect: options[${index}].marker is required`,
86
+ )
87
+ }
88
+ if (marker?.tone === 'custom' && kind !== 'custom') {
89
+ throw new TypeError(
90
+ `SColorSelect: options[${index}] с custom marker должен объявлять kind "custom". `
91
+ + `/ SColorSelect: options[${index}] with a custom marker must declare kind "custom"`,
92
+ )
93
+ }
94
+ if (kind === 'custom' && marker?.tone !== 'custom') {
95
+ throw new TypeError(
96
+ `SColorSelect: options[${index}] kind "custom" требует custom marker. `
97
+ + `/ SColorSelect: options[${index}] kind "custom" requires a custom marker`,
98
+ )
99
+ }
100
+ if (kind !== undefined && kind !== 'preset' && kind !== 'custom') {
101
+ throw new TypeError(
102
+ `SColorSelect: options[${index}].kind должен быть "preset" или "custom". `
103
+ + `/ SColorSelect: options[${index}].kind must be "preset" or "custom"`,
104
+ )
105
+ }
106
+
107
+ const { value, label, title, disabled } = option
108
+ return {
109
+ value,
110
+ label,
111
+ marker,
112
+ ...(title === undefined ? {} : { title }),
113
+ ...(disabled === undefined ? {} : { disabled }),
114
+ }
115
+ })
116
+ })
117
+ const selectedOption = computed(() => {
118
+ const modelValue = validateNonEmptyString('SColorSelect', 'modelValue', props.modelValue)
119
+ const option = resolvedOptions.value.find((candidate) => candidate.value === modelValue)
120
+ if (option !== undefined) return option
121
+ throw new Error(
122
+ `SColorSelect: modelValue ${JSON.stringify(modelValue)} отсутствует в options inventory. `
123
+ + `/ SColorSelect: modelValue ${JSON.stringify(modelValue)} is absent from the options inventory.`,
124
+ )
125
+ })
126
+ const triggerMarker = computed(() => selectedOption.value.marker)
127
+ const resolvedTitle = computed(() => props.title === undefined
128
+ ? undefined
129
+ : validateNonEmptyString('SColorSelect', 'title', props.title))
130
+ const computedAriaLabel = computed(() => {
131
+ if (props.ariaLabel !== undefined) {
132
+ return validateNonEmptyString('SColorSelect', 'ariaLabel', props.ariaLabel)
133
+ }
134
+ if (resolvedTitle.value !== undefined) return resolvedTitle.value
135
+ return validateNonEmptyString('SColorSelect', 'selectedOption.label', selectedOption.value.label)
136
+ })
137
+ const updateColor = (value: string | null | readonly string[]): void => {
138
+ if (typeof value !== 'string') {
139
+ throw new TypeError(
140
+ 'SColorSelect: SSelect update должен быть строковым ключом цвета. '
141
+ + '/ SColorSelect: SSelect update must be a string color key',
142
+ )
143
+ }
144
+ emit('update:modelValue', value)
145
+ }
146
+ </script>
@@ -0,0 +1,35 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-combobox-trigger {
4
+ @apply relative inline-flex min-w-0 items-center gap-2 px-3 font-semibold;
5
+ }
6
+
7
+ .s-combobox-trigger[data-width='full'] { @apply w-full; }
8
+ .s-combobox-trigger[data-width='auto'] { @apply w-auto; }
9
+ .s-combobox-trigger[data-size='xs'] { min-height: var(--s-control-hitbox-xs); @apply px-2 text-xs; }
10
+ .s-combobox-trigger[data-size='sm'] { min-height: var(--s-control-hitbox-sm); @apply px-2.5 text-xs; }
11
+ .s-combobox-trigger[data-size='md'] { min-height: var(--s-control-hitbox-md); @apply text-sm; }
12
+ .s-combobox-trigger[data-appearance='swatch'] { width: var(--s-control-hitbox-md); @apply justify-center rounded-full border border-transparent bg-transparent p-1; box-shadow: none; }
13
+ .s-combobox-trigger[data-appearance='swatch']:disabled { @apply cursor-not-allowed opacity-50; }
14
+
15
+ .s-combobox-trigger__value {
16
+ @apply min-w-0 flex-1 truncate text-left;
17
+ }
18
+
19
+ .s-combobox-trigger__caret {
20
+ @apply ml-auto inline-flex shrink-0 items-center justify-center text-surface-400;
21
+ width: var(--s-icon-size-sm);
22
+ height: var(--s-icon-size-sm);
23
+ }
24
+
25
+ [data-appearance='swatch'] .s-combobox-trigger__caret {
26
+ @apply absolute -bottom-0.5 -right-0.5 rounded-full border border-surface-200 bg-surface-0 text-surface-500;
27
+ @apply dark:border-surface-700 dark:bg-surface-900 dark:text-surface-300;
28
+ width: var(--s-icon-size-xs);
29
+ height: var(--s-icon-size-xs);
30
+ box-shadow: var(--s-elevation-control);
31
+ }
32
+
33
+ .s-combobox-trigger__caret > svg {
34
+ @apply h-full w-full;
35
+ }
@@ -0,0 +1,122 @@
1
+ <template>
2
+ <button
3
+ v-bind="fieldSurfaceBindings"
4
+ ref="buttonRef"
5
+ type="button"
6
+ :id="id"
7
+ :title="title"
8
+ class="s-combobox-trigger"
9
+ :data-size="size"
10
+ :data-width="width"
11
+ :disabled="disabled"
12
+ role="combobox"
13
+ aria-haspopup="listbox"
14
+ :aria-label="accessibleLabel"
15
+ :aria-expanded="expanded"
16
+ :aria-controls="controls"
17
+ :aria-invalid="invalid || undefined"
18
+ :aria-describedby="describedBy"
19
+ :aria-required="required || undefined"
20
+ :aria-readonly="readonly || undefined"
21
+ :data-testid="testId"
22
+ :data-test="test"
23
+ :data-task-mapping-internal-value="taskMappingInternalValue"
24
+ :data-task-mapping-kind="taskMappingKind"
25
+ :data-appearance="appearance"
26
+ @click="emit('toggle', $event)"
27
+ @keydown.down.prevent="emit('toggle', $event)"
28
+ @keydown.up.prevent="emit('toggle', $event)"
29
+ >
30
+ <SMarker v-if="marker" :marker="marker" :size="appearance === 'swatch' ? 'lg' : 'sm'" />
31
+ <span v-if="appearance === 'field'" class="s-combobox-trigger__value">
32
+ {{ displayValue || placeholder }}
33
+ </span>
34
+ <span class="s-combobox-trigger__caret" aria-hidden="true">
35
+ <ChevronsUpDown v-if="appearance === 'field'" />
36
+ <ChevronDown v-else />
37
+ </span>
38
+ </button>
39
+ </template>
40
+
41
+ <script setup lang="ts">
42
+ import { computed, ref } from 'vue'
43
+ import { ChevronDown, ChevronsUpDown } from '../../icons/sputnigUiIcons'
44
+ import type { InteractiveElementApi } from '../../../internal/interactiveElement'
45
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
46
+ import { resolveFieldSurfaceBindings } from '../../../internal/fieldSurface'
47
+ import SMarker from './SMarker.vue'
48
+ import type { SMarker as MarkerContract } from '../_internal/markerContract'
49
+
50
+ export interface Props {
51
+ id: string
52
+ accessibleLabel: string
53
+ expanded: boolean
54
+ controls: string
55
+ displayValue?: string
56
+ placeholder?: string
57
+ marker?: MarkerContract
58
+ appearance?: 'field' | 'swatch'
59
+ size?: 'xs' | 'sm' | 'md'
60
+ width?: 'auto' | 'full'
61
+ disabled?: boolean
62
+ invalid?: boolean
63
+ readonly?: boolean
64
+ required?: boolean
65
+ describedBy?: string
66
+ title?: string
67
+ testId?: string | number | boolean
68
+ test?: string | number | boolean
69
+ taskMappingInternalValue?: string | number | boolean
70
+ taskMappingKind?: string | number | boolean
71
+ }
72
+
73
+ export type SComboboxTriggerApi = InteractiveElementApi<HTMLButtonElement>
74
+
75
+ defineOptions({ inheritAttrs: false })
76
+
77
+ const props = withDefaults(defineProps<Props>(), {
78
+ displayValue: undefined,
79
+ placeholder: 'Выберите значение',
80
+ marker: undefined,
81
+ appearance: 'field',
82
+ size: 'md',
83
+ width: 'full',
84
+ disabled: false,
85
+ invalid: false,
86
+ readonly: false,
87
+ required: false,
88
+ describedBy: undefined,
89
+ title: undefined,
90
+ testId: undefined,
91
+ test: undefined,
92
+ taskMappingInternalValue: undefined,
93
+ taskMappingKind: undefined,
94
+ })
95
+
96
+ const emit = defineEmits<{
97
+ toggle: [event: MouseEvent | KeyboardEvent]
98
+ }>()
99
+ const buttonRef = ref<HTMLButtonElement | null>(null)
100
+ const fieldSurfaceBindings = computed(() => resolveFieldSurfaceBindings('SComboboxTrigger', {
101
+ kind: 'combobox-trigger',
102
+ appearance: props.appearance === 'field' ? 'field' : 'embedded',
103
+ invalid: props.invalid,
104
+ disabled: props.disabled,
105
+ readonly: props.readonly,
106
+ }))
107
+
108
+ useInteractiveLeafRegistration({ owner: 'SComboboxTrigger' })
109
+
110
+ const getElement = (): HTMLButtonElement => {
111
+ const element = buttonRef.value
112
+ if (!element) throw new Error('SComboboxTrigger: native button element is unavailable')
113
+ return element
114
+ }
115
+
116
+ defineExpose<SComboboxTriggerApi>({
117
+ getElement,
118
+ focus: (options?: FocusOptions) => getElement().focus(options),
119
+ })
120
+ </script>
121
+
122
+ <style lang="postcss" src="./SComboboxTrigger.css" scoped></style>
@@ -0,0 +1,11 @@
1
+ .s-context-menu-boundary[data-layout='block'] {
2
+ display: block;
3
+ }
4
+
5
+ .s-context-menu-boundary[data-layout='inline'] {
6
+ display: inline;
7
+ }
8
+
9
+ .s-context-menu-boundary[data-layout='contents'] {
10
+ display: contents;
11
+ }