@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,388 @@
1
+ <template>
2
+ <ul
3
+ v-bind="ownedAttrs.bindings()"
4
+ :id="resolvedId"
5
+ class="s-listbox"
6
+ :data-variant="resolvedVariant"
7
+ :data-invalid="resolvedInvalid || undefined"
8
+ :data-readonly="resolvedReadonly || undefined"
9
+ role="listbox"
10
+ :aria-label="resolvedLabel"
11
+ :aria-multiselectable="resolvedSelectionMode === 'multiple' || undefined"
12
+ :aria-disabled="resolvedDisabled || undefined"
13
+ :aria-readonly="resolvedReadonly || undefined"
14
+ :aria-invalid="resolvedInvalid || undefined"
15
+ @keydown="handleKeydown"
16
+ >
17
+ <SListboxOption
18
+ v-if="resolvedClearOptionLabel"
19
+ :registration-token="CLEAR_OPTION_TOKEN"
20
+ :label="resolvedClearOptionLabel"
21
+ :title="resolvedClearOptionLabel"
22
+ :selected="modelValue === null"
23
+ :disabled="resolvedDisabled"
24
+ :appearance="optionAppearance"
25
+ @select="clearSelection"
26
+ />
27
+ <template v-for="entry in inventory.entries" :key="entry.token">
28
+ <SListboxOption
29
+ v-if="entry.kind === 'option'"
30
+ :registration-token="entry.record.token"
31
+ :label="entry.record.option.label"
32
+ :title="entry.record.option.title ?? entry.record.option.label"
33
+ :selected="selectedTokens.has(entry.record.token)"
34
+ :disabled="resolvedDisabled || entry.record.option.disabled"
35
+ :marker="entry.record.option.marker"
36
+ :appearance="optionAppearance"
37
+ @select="selectOption(entry.record.token)"
38
+ />
39
+ <SListboxGroup
40
+ v-else
41
+ :label="entry.label"
42
+ :label-id="groupLabelId(entry.token)"
43
+ :variant="resolvedVariant"
44
+ >
45
+ <SListboxOption
46
+ v-for="record in entry.records"
47
+ :key="record.token"
48
+ :registration-token="record.token"
49
+ :label="record.option.label"
50
+ :title="record.option.title ?? record.option.label"
51
+ :selected="selectedTokens.has(record.token)"
52
+ :disabled="resolvedDisabled || record.option.disabled"
53
+ :marker="record.option.marker"
54
+ :appearance="optionAppearance"
55
+ @select="selectOption(record.token)"
56
+ />
57
+ </SListboxGroup>
58
+ </template>
59
+ <li v-if="inventory.options.length === 0 && !resolvedClearOptionLabel" class="s-listbox__empty" role="presentation">
60
+ {{ resolvedEmptyLabel }}
61
+ </li>
62
+ </ul>
63
+ </template>
64
+
65
+ <script lang="ts">
66
+ import type { SelectionKey as PublicSelectionKey } from '../_internal/useSelectionRoving'
67
+ import type {
68
+ SelectionEntry as PublicSelectionEntry,
69
+ SelectionMode as PublicSelectionMode,
70
+ SelectionModelValue as PublicSelectionModelValue,
71
+ SelectionOption as PublicSelectionOption,
72
+ SelectionOptionGroup as PublicSelectionOptionGroup,
73
+ } from '../_internal/selectionContract'
74
+
75
+ export type SListboxVariant = 'select' | 'color'
76
+ export type SListboxOption<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionOption<TKey>
77
+ export type SListboxOptionGroup<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionOptionGroup<TKey>
78
+ export type SListboxEntry<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionEntry<TKey>
79
+ export type SListboxMode = PublicSelectionMode
80
+ export type SListboxModelValue<
81
+ TKey extends PublicSelectionKey,
82
+ TMode extends SListboxMode,
83
+ > = PublicSelectionModelValue<TKey, TMode>
84
+
85
+ /**
86
+ * Точная identity option для keyboard event, которым владеет SListbox.
87
+ * Exact option identity for a keyboard event owned by SListbox.
88
+ * Consumer может вызвать preventDefault(), чтобы забрать augmented keyboard chord
89
+ * до базовой keyboard-модели listbox. / A consumer may call preventDefault() to claim an
90
+ * augmented keyboard chord before the base listbox keyboard model runs.
91
+ */
92
+ export interface SListboxOptionKeydownPayload<
93
+ TKey extends PublicSelectionKey = PublicSelectionKey,
94
+ > {
95
+ readonly value: TKey | null
96
+ readonly event: KeyboardEvent
97
+ }
98
+
99
+ interface SharedProps<TKey extends PublicSelectionKey> {
100
+ options: readonly SListboxEntry<TKey>[]
101
+ label: string
102
+ disabled?: boolean
103
+ readonly?: boolean
104
+ invalid?: boolean
105
+ variant?: SListboxVariant
106
+ emptyLabel?: string
107
+ clearOptionLabel?: string
108
+ id?: string
109
+ }
110
+
111
+ export type SListboxProps<
112
+ TKey extends PublicSelectionKey = PublicSelectionKey,
113
+ TMode extends SListboxMode = 'single',
114
+ > = SharedProps<TKey> & (TMode extends 'multiple'
115
+ ? { selectionMode: 'multiple'; modelValue: readonly TKey[]; clearOptionLabel?: never }
116
+ : { selectionMode?: 'single'; modelValue: TKey | null; clearOptionLabel?: string })
117
+
118
+ type ComponentProps<TKey extends PublicSelectionKey> = SharedProps<TKey> & (
119
+ | { selectionMode?: 'single'; modelValue: TKey | null; clearOptionLabel?: string }
120
+ | { selectionMode: 'multiple'; modelValue: readonly TKey[]; clearOptionLabel?: never }
121
+ )
122
+ </script>
123
+
124
+ <script setup lang="ts" generic="TKey extends SelectionKey = string">
125
+ import { computed, onBeforeUnmount, provide, useId } from 'vue'
126
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
127
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
128
+ import {
129
+ validateBoolean,
130
+ validateDomId,
131
+ validateExactString,
132
+ validateNonEmptyString,
133
+ } from '../../../internal/runtimeContract'
134
+ import {
135
+ isSelectionModelList,
136
+ resolveSelectionInventory,
137
+ validateSelectionModel,
138
+ type SelectionMode,
139
+ } from '../_internal/selectionContract'
140
+ import { listboxContextKey } from '../_internal/listboxContext'
141
+ import type { ListboxOptionRegistration } from '../_internal/listboxContext'
142
+ import { selectionKeyToken, type SelectionKey } from '../_internal/useSelectionRoving'
143
+ import SListboxGroup from '../_internal/SListboxGroup.vue'
144
+ import SListboxOption from './SListboxOption.vue'
145
+
146
+ defineOptions({ inheritAttrs: false })
147
+
148
+ const props = withDefaults(defineProps<ComponentProps<TKey>>(), {
149
+ selectionMode: undefined,
150
+ disabled: false,
151
+ readonly: false,
152
+ invalid: false,
153
+ variant: 'select',
154
+ emptyLabel: 'Нет доступных вариантов',
155
+ clearOptionLabel: undefined,
156
+ id: undefined,
157
+ })
158
+ const emit = defineEmits<{
159
+ 'update:modelValue': [value: TKey | null] | [value: readonly TKey[]]
160
+ 'option-keydown': [payload: SListboxOptionKeydownPayload<TKey>]
161
+ escape: []
162
+ tab: [event: KeyboardEvent]
163
+ }>()
164
+ const ownedAttrs = useOwnedAttrs({ component: 'SListbox', owner: 'listbox root' })
165
+ useInteractiveLeafRegistration({ owner: 'SListbox' })
166
+ const generatedId = useId()
167
+ const contract = computed(() => {
168
+ const selectionMode: SelectionMode = validateExactString(
169
+ 'SListbox', 'selectionMode', props.selectionMode ?? 'single', ['single', 'multiple'] as const,
170
+ )
171
+ let clearOptionLabel: string | undefined
172
+ if (props.clearOptionLabel !== undefined) {
173
+ if (selectionMode === 'multiple') {
174
+ throw new Error('SListbox: clearOptionLabel is available only in single selection mode')
175
+ }
176
+ clearOptionLabel = validateNonEmptyString('SListbox', 'clearOptionLabel', props.clearOptionLabel)
177
+ }
178
+ return {
179
+ id: props.id === undefined
180
+ ? `s-listbox-${generatedId}`
181
+ : validateDomId('SListbox', 'id', props.id),
182
+ label: validateNonEmptyString('SListbox', 'label', props.label),
183
+ emptyLabel: validateNonEmptyString('SListbox', 'emptyLabel', props.emptyLabel),
184
+ variant: validateExactString(
185
+ 'SListbox', 'variant', props.variant, ['select', 'color'] as const,
186
+ ),
187
+ disabled: validateBoolean('SListbox', 'disabled', props.disabled),
188
+ readonly: validateBoolean('SListbox', 'readonly', props.readonly),
189
+ invalid: validateBoolean('SListbox', 'invalid', props.invalid),
190
+ selectionMode,
191
+ clearOptionLabel,
192
+ }
193
+ })
194
+ const resolvedId = computed(() => contract.value.id)
195
+ const resolvedLabel = computed(() => contract.value.label)
196
+ const resolvedEmptyLabel = computed(() => contract.value.emptyLabel)
197
+ const resolvedVariant = computed(() => contract.value.variant)
198
+ const resolvedDisabled = computed(() => contract.value.disabled)
199
+ const resolvedReadonly = computed(() => contract.value.readonly)
200
+ const resolvedInvalid = computed(() => contract.value.invalid)
201
+ const resolvedSelectionMode = computed(() => contract.value.selectionMode)
202
+ const resolvedClearOptionLabel = computed(() => contract.value.clearOptionLabel)
203
+ const selectionState = computed(() => {
204
+ const resolvedInventory = resolveSelectionInventory<TKey>('SListbox', props.options)
205
+ return {
206
+ inventory: resolvedInventory,
207
+ selectedTokens: validateSelectionModel(
208
+ 'SListbox', contract.value.selectionMode, props.modelValue, resolvedInventory,
209
+ ),
210
+ }
211
+ })
212
+ const inventory = computed(() => selectionState.value.inventory)
213
+ const selectedTokens = computed(() => selectionState.value.selectedTokens)
214
+ const optionAppearance = computed(() => resolvedVariant.value === 'color' ? 'color' : 'select')
215
+ const CLEAR_OPTION_TOKEN = 'clear-option'
216
+
217
+ function emitModelValue(value: TKey | null | readonly TKey[]): void {
218
+ if (contract.value.selectionMode === 'multiple') {
219
+ if (!isSelectionModelList(value)) {
220
+ throw new TypeError('SListbox: multiple selection update must be an array')
221
+ }
222
+ validateSelectionModel('SListbox update', 'multiple', value, inventory.value)
223
+ emit('update:modelValue', value)
224
+ return
225
+ }
226
+ if (isSelectionModelList(value)) {
227
+ throw new TypeError('SListbox: single selection update must be a selection key or null')
228
+ }
229
+ validateSelectionModel('SListbox update', 'single', value, inventory.value)
230
+ emit('update:modelValue', value)
231
+ }
232
+
233
+ function groupLabelId(token: string): string {
234
+ return `${resolvedId.value}-${token.replace(/[^a-zA-Z0-9_-]/gu, '-')}-label`
235
+ }
236
+
237
+ function selectOption(token: string): void {
238
+ if (resolvedDisabled.value || resolvedReadonly.value) return
239
+ const record = inventory.value.optionsByToken.get(token)
240
+ if (!record) throw new Error('SListbox: attempted to select a stale option identity')
241
+ if (record.option.disabled) return
242
+ if (resolvedSelectionMode.value === 'single') {
243
+ emitModelValue(record.option.value)
244
+ return
245
+ }
246
+ if (!Array.isArray(props.modelValue)) {
247
+ throw new TypeError('SListbox: multiple selection requires modelValue to be an array')
248
+ }
249
+ const current = [...props.modelValue]
250
+ const currentIndex = current.findIndex((value) => (
251
+ selectionKeyToken('SListbox', value, 'modelValue item') === token
252
+ ))
253
+ if (currentIndex >= 0) current.splice(currentIndex, 1)
254
+ else current.push(record.option.value)
255
+ emitModelValue(current)
256
+ }
257
+
258
+ function clearSelection(): void {
259
+ if (resolvedDisabled.value || resolvedReadonly.value) return
260
+ if (resolvedSelectionMode.value !== 'single') {
261
+ throw new Error('SListbox: clear selection is available only in single selection mode')
262
+ }
263
+ emitModelValue(null)
264
+ }
265
+
266
+ const registrations = new Set<ListboxOptionRegistration>()
267
+ let typeaheadBuffer = ''
268
+ let typeaheadTimer: ReturnType<typeof setTimeout> | undefined
269
+
270
+ function enabledRegistrations(): ListboxOptionRegistration[] {
271
+ return [...registrations]
272
+ .filter((option) => !option.disabled() && option.element.isConnected)
273
+ .sort((left, right) => {
274
+ const position = left.element.compareDocumentPosition(right.element)
275
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1
276
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1
277
+ return 0
278
+ })
279
+ }
280
+
281
+ function initializeTabstop(): void {
282
+ const options = enabledRegistrations()
283
+ for (const option of registrations) option.element.tabIndex = -1
284
+ const selected = options.find((option) => option.selected())
285
+ const target = selected ?? options[0]
286
+ if (target) target.element.tabIndex = 0
287
+ }
288
+
289
+ provide(listboxContextKey, {
290
+ register: (registration) => {
291
+ registrations.add(registration)
292
+ queueMicrotask(initializeTabstop)
293
+ return () => registrations.delete(registration)
294
+ },
295
+ })
296
+
297
+ function focusOption(target: ListboxOptionRegistration, options = enabledRegistrations()): void {
298
+ for (const option of registrations) option.element.tabIndex = -1
299
+ target.element.tabIndex = 0
300
+ target.element.focus()
301
+ }
302
+
303
+ function currentOption(
304
+ options: ListboxOptionRegistration[],
305
+ event: KeyboardEvent,
306
+ ): ListboxOptionRegistration | undefined {
307
+ return options.find((option) => option.element === event.target)
308
+ ?? options.find((option) => option.element === document.activeElement)
309
+ ?? options.find((option) => option.element.tabIndex === 0)
310
+ }
311
+
312
+ function optionValue(registration: ListboxOptionRegistration): TKey | null {
313
+ if (registration.token === CLEAR_OPTION_TOKEN) return null
314
+ const record = inventory.value.optionsByToken.get(registration.token)
315
+ if (!record) {
316
+ throw new Error(`SListbox: registered option token ${JSON.stringify(registration.token)} is stale`)
317
+ }
318
+ return record.option.value
319
+ }
320
+
321
+ function handleKeydown(event: KeyboardEvent): void {
322
+ if (event.key === 'Tab') {
323
+ emit('tab', event)
324
+ return
325
+ }
326
+ if (event.key === 'Escape') {
327
+ event.preventDefault()
328
+ emit('escape')
329
+ return
330
+ }
331
+ if (resolvedDisabled.value) return
332
+ const options = enabledRegistrations()
333
+ const activeOption = currentOption(options, event)
334
+ if (activeOption) {
335
+ emit('option-keydown', { value: optionValue(activeOption), event })
336
+ if (event.defaultPrevented) return
337
+ }
338
+ if (event.key === 'Enter' || event.key === ' ') {
339
+ if (resolvedReadonly.value) return
340
+ if (!activeOption) return
341
+ event.preventDefault()
342
+ activeOption.activate()
343
+ return
344
+ }
345
+ if (event.key.length === 1 && !event.altKey && !event.ctrlKey && !event.metaKey) {
346
+ typeaheadBuffer += event.key.toLocaleLowerCase()
347
+ if (typeaheadTimer) clearTimeout(typeaheadTimer)
348
+ typeaheadTimer = setTimeout(() => { typeaheadBuffer = ''; typeaheadTimer = undefined }, 500)
349
+ const query = [...typeaheadBuffer].every((character) => character === typeaheadBuffer[0])
350
+ ? typeaheadBuffer[0]!
351
+ : typeaheadBuffer
352
+ const currentIndex = activeOption === undefined ? -1 : options.indexOf(activeOption)
353
+ const searchOrder = currentIndex >= 0
354
+ ? [...options.slice(currentIndex + 1), ...options.slice(0, currentIndex + 1)]
355
+ : options
356
+ const match = searchOrder.find((option) => option.label().toLocaleLowerCase().startsWith(query))
357
+ if (match) {
358
+ event.preventDefault()
359
+ focusOption(match, options)
360
+ }
361
+ return
362
+ }
363
+ if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key) || options.length === 0) return
364
+ const currentIndex = activeOption === undefined ? -1 : options.indexOf(activeOption)
365
+ const nextIndex = event.key === 'Home'
366
+ ? 0
367
+ : event.key === 'End'
368
+ ? options.length - 1
369
+ : event.key === 'ArrowDown'
370
+ ? (Math.max(currentIndex, -1) + 1) % options.length
371
+ : (currentIndex <= 0 ? options.length : currentIndex) - 1
372
+ const target = options[nextIndex]
373
+ if (!target) return
374
+ event.preventDefault()
375
+ focusOption(target, options)
376
+ }
377
+
378
+ function focusSelectedOrFirst(): void {
379
+ const options = enabledRegistrations()
380
+ const target = options.find((option) => option.selected()) ?? options[0]
381
+ if (target) focusOption(target, options)
382
+ }
383
+
384
+ onBeforeUnmount(() => { if (typeaheadTimer) clearTimeout(typeaheadTimer) })
385
+ defineExpose({ focusSelectedOrFirst })
386
+ </script>
387
+
388
+ <style lang="postcss" src="./SListbox.css" scoped></style>
@@ -0,0 +1,63 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-listbox-option__control {
4
+ @apply relative flex w-full min-w-0 items-center justify-between rounded-md px-3 py-2 text-left text-sm text-surface-800;
5
+ @apply cursor-pointer hover:bg-surface-100;
6
+ @apply dark:text-surface-100 dark:hover:bg-surface-800;
7
+ transition: background-color var(--s-motion-standard);
8
+ }
9
+
10
+ .s-listbox-option__control:focus-visible {
11
+ outline: var(--s-focus-ring-width) solid var(--s-focus-ring-strong-color);
12
+ outline-offset: var(--s-focus-ring-inset-offset);
13
+ }
14
+
15
+ .s-listbox-option__control[aria-disabled='true'] {
16
+ @apply cursor-not-allowed opacity-50;
17
+ }
18
+
19
+ .s-listbox-option__control[aria-disabled='true']:hover {
20
+ @apply bg-transparent;
21
+ }
22
+
23
+ .s-listbox-option__control[aria-selected='true'] { @apply bg-primary-50 dark:bg-primary-950/40; }
24
+ .s-listbox-option__control[data-appearance='color'] {
25
+ width: var(--s-control-hitbox-lg);
26
+ height: var(--s-control-hitbox-lg);
27
+ @apply justify-center rounded-full p-1;
28
+ }
29
+
30
+ .s-listbox-option__main {
31
+ @apply flex min-w-0 items-center;
32
+ gap: var(--s-space-control-gap);
33
+ }
34
+
35
+ .s-listbox-option__label {
36
+ @apply block truncate;
37
+ }
38
+
39
+ .s-listbox-option__label[data-selected='true'] {
40
+ @apply font-semibold;
41
+ }
42
+
43
+ .s-listbox-option__check {
44
+ @apply absolute inset-y-0 right-0 flex items-center pr-4;
45
+ }
46
+
47
+ .s-listbox-option__check > svg {
48
+ width: var(--s-icon-size-sm);
49
+ height: var(--s-icon-size-sm);
50
+ }
51
+
52
+ .s-listbox-option__check[data-appearance='color'] {
53
+ @apply -right-0.5 -top-0.5 bottom-auto items-center justify-center rounded-full bg-surface-0 p-0 text-primary-600;
54
+ @apply dark:bg-surface-950 dark:text-primary-400;
55
+ width: var(--s-icon-size-sm);
56
+ height: var(--s-icon-size-sm);
57
+ box-shadow: var(--s-elevation-control);
58
+ }
59
+
60
+ .s-listbox-option__check[data-appearance='color'] > svg {
61
+ width: var(--s-icon-size-2xs);
62
+ height: var(--s-icon-size-2xs);
63
+ }
@@ -0,0 +1,85 @@
1
+ <template>
2
+ <li
3
+ ref="optionRef"
4
+ class="s-listbox-option__control"
5
+ :data-appearance="appearance"
6
+ :data-focused="focused || undefined"
7
+ :title="title ?? label"
8
+ :aria-label="appearance === 'color' ? label : undefined"
9
+ role="option"
10
+ :aria-selected="selected"
11
+ :aria-disabled="disabled || undefined"
12
+ :tabindex="-1"
13
+ @focus="focused = true"
14
+ @blur="focused = false"
15
+ @click.stop="select"
16
+ >
17
+ <span class="s-listbox-option__main" :data-appearance="appearance">
18
+ <SMarker v-if="marker" :marker="marker" :size="appearance === 'color' ? 'lg' : 'sm'" />
19
+ <span v-if="appearance === 'select'" class="s-listbox-option__label" :data-selected="selected || undefined">
20
+ {{ label }}
21
+ </span>
22
+ </span>
23
+ <span v-if="selected" class="s-listbox-option__check" :data-appearance="appearance" aria-hidden="true">
24
+ <Check />
25
+ </span>
26
+ </li>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
31
+ import { Check } from '../../icons/sputnigUiIcons'
32
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
33
+ import SMarker from './SMarker.vue'
34
+ import { listboxContextKey } from '../_internal/listboxContext'
35
+ import type { SMarker as MarkerContract } from '../_internal/markerContract'
36
+
37
+ export interface Props {
38
+ registrationToken: string
39
+ label: string
40
+ selected: boolean
41
+ disabled?: boolean
42
+ appearance?: 'select' | 'color'
43
+ marker?: MarkerContract
44
+ title?: string
45
+ }
46
+
47
+ defineOptions({ inheritAttrs: false })
48
+
49
+ const props = withDefaults(defineProps<Props>(), {
50
+ disabled: false,
51
+ appearance: 'select',
52
+ marker: undefined,
53
+ title: undefined,
54
+ })
55
+
56
+ const emit = defineEmits<{
57
+ select: []
58
+ }>()
59
+ useInteractiveLeafRegistration({ owner: 'SListboxOption' })
60
+ const context = inject(listboxContextKey)
61
+ if (!context) throw new Error('SListboxOption must be used inside SListbox')
62
+ const focused = ref(false)
63
+ const optionRef = ref<HTMLLIElement | null>(null)
64
+ let unregister: (() => void) | undefined
65
+ const select = (): void => {
66
+ if (props.disabled) return
67
+ optionRef.value?.focus()
68
+ emit('select')
69
+ }
70
+ onMounted(() => {
71
+ const element = optionRef.value
72
+ if (!(element instanceof HTMLLIElement)) throw new Error('SListboxOption: option element is unavailable')
73
+ unregister = context.register({
74
+ element,
75
+ token: props.registrationToken,
76
+ activate: select,
77
+ disabled: () => props.disabled,
78
+ label: () => props.label,
79
+ selected: () => props.selected,
80
+ })
81
+ })
82
+ onBeforeUnmount(() => unregister?.())
83
+ </script>
84
+
85
+ <style lang="postcss" src="./SListboxOption.css" scoped></style>
@@ -0,0 +1,19 @@
1
+ @reference "../../../styles/reference.css";
2
+ .s-marker { @apply inline-block h-2 w-2 shrink-0 rounded-full bg-surface-400 ring-1 ring-inset ring-surface-500/50; }
3
+ .s-marker[data-size='lg'] { @apply h-7 w-7 ring-2 ring-white/80 shadow-sm dark:ring-surface-950/80; }
4
+ .s-marker[data-tone='primary'] { @apply bg-primary-500 ring-primary-600/50; }
5
+ .s-marker[data-tone='success'] { @apply bg-success-500 ring-success-600/50; }
6
+ .s-marker[data-tone='blue'] { @apply bg-blue-400 ring-blue-500/50; }
7
+ .s-marker[data-tone='sky'] { @apply bg-sky-400 ring-sky-500/50; }
8
+ .s-marker[data-tone='indigo'] { @apply bg-indigo-400 ring-indigo-500/50; }
9
+ .s-marker[data-tone='emerald'] { @apply bg-emerald-400 ring-emerald-500/50; }
10
+ .s-marker[data-tone='green'] { @apply bg-green-400 ring-green-500/50; }
11
+ .s-marker[data-tone='teal'] { @apply bg-teal-400 ring-teal-500/50; }
12
+ .s-marker[data-tone='amber'] { @apply bg-amber-400 ring-amber-500/50; }
13
+ .s-marker[data-tone='orange'] { @apply bg-orange-400 ring-orange-500/50; }
14
+ .s-marker[data-tone='rose'] { @apply bg-rose-400 ring-rose-500/50; }
15
+ .s-marker[data-tone='pink'] { @apply bg-pink-400 ring-pink-500/50; }
16
+ .s-marker[data-tone='fuchsia'] { @apply bg-fuchsia-400 ring-fuchsia-500/50; }
17
+ .s-marker[data-tone='cyan'] { @apply bg-cyan-400 ring-cyan-500/50; }
18
+ .s-marker[data-tone='lime'] { @apply bg-lime-400 ring-lime-500/50; }
19
+ .s-marker[data-tone='custom'] { background-color: var(--s-marker-custom-color); --tw-ring-color: var(--s-marker-custom-color); }
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <span
3
+ v-bind="ownedAttrs.bindings()"
4
+ class="s-marker"
5
+ :data-tone="marker.tone"
6
+ :data-size="size"
7
+ :style="customProperties"
8
+ aria-hidden="true"
9
+ />
10
+ </template>
11
+
12
+ <script lang="ts">
13
+ export { createCustomMarker } from '../_internal/markerContract'
14
+ export type { SCustomMarker, SMarker, SMarkerTone } from '../_internal/markerContract'
15
+ </script>
16
+
17
+ <script setup lang="ts">
18
+ import { computed } from 'vue'
19
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
20
+ import { createCustomMarker, type SMarker } from '../_internal/markerContract'
21
+
22
+ defineOptions({ inheritAttrs: false })
23
+
24
+ const props = withDefaults(defineProps<{ marker: SMarker; size?: 'sm' | 'lg' }>(), { size: 'sm' })
25
+ const ownedAttrs = useOwnedAttrs({ component: 'SMarker', owner: 'marker root' })
26
+ const customProperties = computed(() => {
27
+ if (props.marker.tone !== 'custom') return undefined
28
+ createCustomMarker(props.marker.color)
29
+ return { '--s-marker-custom-color': props.marker.color }
30
+ })
31
+ </script>
32
+
33
+ <style lang="postcss" src="./SMarker.css" scoped></style>
@@ -0,0 +1,20 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-menu-surface {
4
+ @apply m-0 rounded-xl p-1.5;
5
+ @apply focus:outline-none;
6
+ background: var(--s-floating-surface);
7
+ color: var(--s-floating-foreground);
8
+ box-shadow: 0 0 0 1px var(--s-floating-outline), var(--s-elevation-floating);
9
+ width: var(--s-menu-surface-width);
10
+ min-width: var(--s-menu-surface-min-width);
11
+ max-width: var(--s-menu-surface-max-width);
12
+ }
13
+
14
+ .s-menu-surface[data-density='compact'] {
15
+ @apply min-w-40 rounded-md p-1;
16
+ }
17
+
18
+ .s-menu-surface[data-orientation='horizontal'] {
19
+ @apply flex max-w-full gap-1 overflow-x-auto;
20
+ }