@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,224 @@
1
+ <script lang="ts">
2
+ import {
3
+ computed,
4
+ createTextVNode,
5
+ defineComponent,
6
+ h,
7
+ type ComponentPublicInstance,
8
+ type PropType,
9
+ } from 'vue'
10
+
11
+ import { X } from '../../../icons/sputnigUiIcons'
12
+ import type { FieldSurfaceBindings } from '../../../../internal/fieldSurface'
13
+ import type { InlineTokenEditorSegmentContract } from '../../../../internal/inlineTokenEditorContract'
14
+ import type { OwnedAttributeBindings } from '../../../../internal/ownedAttrs'
15
+ import { useInteractiveLeafRegistration } from '../../../../internal/passiveContentContract'
16
+ import { validateBoundedInteger } from '../../../../internal/runtimeContract'
17
+ import SButton, { type Props as SButtonProps } from '../SButton.vue'
18
+
19
+ /** Текст визуального caret helper. / Visual caret-helper text. */
20
+ export const INLINE_TOKEN_EDITOR_VISUAL_CARET_TEXT = '\u200B'
21
+
22
+ /** Действие над одним token occurrence. / Action for one token occurrence. */
23
+ export type InlineTokenAction = 'open' | 'remove'
24
+
25
+ /** Точное событие token action. / Exact token-action event. */
26
+ export interface InlineTokenActionEvent {
27
+ readonly action: InlineTokenAction
28
+ readonly occurrenceId: string
29
+ readonly id: string
30
+ }
31
+
32
+ /** Ref-контракт native editing surface. / Native editing-surface ref contract. */
33
+ export type InlineTokenElementRef = (
34
+ target: Element | ComponentPublicInstance | null,
35
+ refs: Record<string, unknown>,
36
+ ) => void
37
+
38
+ interface InlineTokenActionButtonBindings extends SButtonProps {
39
+ readonly 'aria-label': string
40
+ readonly title?: string
41
+ readonly onClick: (event: MouseEvent) => void
42
+ readonly onKeydown: (event: KeyboardEvent) => void
43
+ }
44
+
45
+ function resolveSurfaceStyle(
46
+ rowsInput: number,
47
+ maxRowsInput: number,
48
+ appearance: 'default' | 'ghost',
49
+ ): Record<string, string> {
50
+ const rows = validateBoundedInteger('SInlineTokenEditor', 'rows', rowsInput, { minimum: 1, maximum: 100 })
51
+ const maxRows = validateBoundedInteger('SInlineTokenEditor', 'maxRows', maxRowsInput, { minimum: 0, maximum: 100 })
52
+ if (maxRows !== 0 && maxRows < rows) {
53
+ throw new RangeError(`SInlineTokenEditor: maxRows должен быть 0 или >= rows (${rows}); получено ${maxRows}. / SInlineTokenEditor: maxRows must be 0 or >= rows (${rows}); received ${maxRows}.`)
54
+ }
55
+ const verticalPaddingRem = appearance === 'ghost' ? 0 : 1
56
+ const lineHeightRem = appearance === 'ghost' ? 0.875 * 1.45 : 0.875 * 1.5
57
+ const style: Record<string, string> = {
58
+ minHeight: `${lineHeightRem * rows + verticalPaddingRem}rem`,
59
+ }
60
+ if (maxRows > 0) {
61
+ style.maxHeight = `${lineHeightRem * maxRows + verticalPaddingRem}rem`
62
+ style.overflowY = 'auto'
63
+ }
64
+ else {
65
+ style.maxHeight = 'none'
66
+ style.overflowY = 'visible'
67
+ }
68
+ return style
69
+ }
70
+
71
+ /**
72
+ * Private leaf-владелец exact contenteditable DOM, ARIA, field surface и token actions.
73
+ * Private leaf owner of the exact contenteditable DOM, ARIA, field surface, and token actions.
74
+ */
75
+ export default defineComponent({
76
+ name: 'SInlineTokenSurface',
77
+ inheritAttrs: false,
78
+ props: {
79
+ id: { type: String, required: true },
80
+ elementRef: { type: Function as PropType<InlineTokenElementRef>, required: true },
81
+ ownedAttributes: {
82
+ type: Object as PropType<OwnedAttributeBindings<'SInlineTokenEditor'>>,
83
+ required: true,
84
+ },
85
+ fieldBindings: { type: Object as PropType<FieldSurfaceBindings>, required: true },
86
+ segments: {
87
+ type: Array as PropType<readonly InlineTokenEditorSegmentContract[]>,
88
+ required: true,
89
+ },
90
+ trailingCaretHelper: { type: Boolean, required: true },
91
+ openDisabled: { type: Boolean, required: true },
92
+ removeDisabled: { type: Boolean, required: true },
93
+ appearance: { type: String as PropType<'default' | 'ghost'>, required: true },
94
+ invalid: { type: Boolean, required: true },
95
+ disabled: { type: Boolean, required: true },
96
+ readonly: { type: Boolean, required: true },
97
+ required: { type: Boolean, required: true },
98
+ rows: { type: Number, required: true },
99
+ maxRows: { type: Number, required: true },
100
+ placeholder: { type: String, default: undefined },
101
+ placeholderVisible: { type: Boolean, required: true },
102
+ labelledBy: { type: String, default: undefined },
103
+ describedBy: { type: String, default: undefined },
104
+ },
105
+ emits: {
106
+ beforeinput: (_event: InputEvent) => true,
107
+ input: (_event: Event) => true,
108
+ keydown: (_event: KeyboardEvent) => true,
109
+ keyup: (_event: KeyboardEvent) => true,
110
+ click: (_event: MouseEvent) => true,
111
+ paste: (_event: ClipboardEvent) => true,
112
+ tokenAction: (event: InlineTokenActionEvent) => (
113
+ (event.action === 'open' || event.action === 'remove')
114
+ && typeof event.occurrenceId === 'string'
115
+ && event.occurrenceId.length > 0
116
+ && typeof event.id === 'string'
117
+ && event.id.length > 0
118
+ ),
119
+ },
120
+ setup(props, { emit }) {
121
+ useInteractiveLeafRegistration({
122
+ owner: 'SInlineTokenEditor',
123
+ active: () => !props.disabled,
124
+ })
125
+ const surfaceStyle = computed(() => resolveSurfaceStyle(props.rows, props.maxRows, props.appearance))
126
+ const stopKeyPropagation = (event: KeyboardEvent): void => event.stopPropagation()
127
+ const emitTokenAction = (
128
+ event: MouseEvent,
129
+ action: InlineTokenAction,
130
+ segment: Extract<InlineTokenEditorSegmentContract, { kind: 'token' }>,
131
+ ): void => {
132
+ event.stopPropagation()
133
+ emit('tokenAction', { action, occurrenceId: segment.occurrenceId, id: segment.id })
134
+ }
135
+
136
+ const renderTokenAction = (
137
+ action: InlineTokenAction,
138
+ segment: Extract<InlineTokenEditorSegmentContract, { kind: 'token' }>,
139
+ ) => {
140
+ const bindings: InlineTokenActionButtonBindings = action === 'open'
141
+ ? {
142
+ label: segment.label,
143
+ 'aria-label': segment.title ? `Открыть ${segment.title}` : `Открыть ${segment.label}`,
144
+ ...(segment.title === undefined ? {} : { title: segment.title }),
145
+ size: 'xs',
146
+ width: 'fit',
147
+ appearance: 'text',
148
+ shape: 'pill',
149
+ disabled: props.openDisabled,
150
+ onClick: (event) => emitTokenAction(event, action, segment),
151
+ onKeydown: stopKeyPropagation,
152
+ }
153
+ : {
154
+ leadingIcon: X,
155
+ 'aria-label': `Убрать ${segment.label}`,
156
+ size: 'xs',
157
+ appearance: 'ghost',
158
+ shape: 'circle',
159
+ iconOnly: true,
160
+ disabled: props.removeDisabled,
161
+ onClick: (event) => emitTokenAction(event, action, segment),
162
+ onKeydown: stopKeyPropagation,
163
+ }
164
+ return h(SButton, { ...bindings, key: action })
165
+ }
166
+
167
+ return () => h('div', {
168
+ ...props.ownedAttributes,
169
+ ...props.fieldBindings,
170
+ id: props.id,
171
+ ref: props.elementRef,
172
+ class: 's-inline-token-editor',
173
+ 'data-appearance': props.appearance,
174
+ 'data-invalid': props.invalid || undefined,
175
+ 'data-disabled': props.disabled || undefined,
176
+ 'data-readonly': props.readonly || undefined,
177
+ 'data-placeholder': props.placeholder,
178
+ 'data-placeholder-visible': props.placeholderVisible ? 'true' : 'false',
179
+ 'data-component': 's-inline-token-editor',
180
+ style: surfaceStyle.value,
181
+ role: 'textbox',
182
+ 'aria-multiline': 'true',
183
+ contenteditable: props.disabled || props.readonly ? 'false' : 'true',
184
+ tabindex: props.disabled ? -1 : 0,
185
+ 'aria-disabled': props.disabled || undefined,
186
+ 'aria-invalid': props.invalid || undefined,
187
+ 'aria-required': props.required || undefined,
188
+ 'aria-readonly': props.readonly || undefined,
189
+ 'aria-labelledby': props.labelledBy,
190
+ 'aria-describedby': props.describedBy,
191
+ onBeforeinput: (event: InputEvent) => emit('beforeinput', event),
192
+ onInput: (event: Event) => emit('input', event),
193
+ onKeydown: (event: KeyboardEvent) => emit('keydown', event),
194
+ onKeyup: (event: KeyboardEvent) => emit('keyup', event),
195
+ onClick: (event: MouseEvent) => emit('click', event),
196
+ onPaste: (event: ClipboardEvent) => emit('paste', event),
197
+ }, [
198
+ ...props.segments.map((segment) => (
199
+ segment.kind === 'text'
200
+ ? createTextVNode(segment.text)
201
+ : h('span', {
202
+ key: `token:${segment.occurrenceId}`,
203
+ class: 's-inline-token-editor__token',
204
+ 'data-testid': 's-inline-token-editor-token',
205
+ 'data-segment-kind': 'token',
206
+ 'data-segment-occurrence-id': segment.occurrenceId,
207
+ 'data-segment-id': segment.id,
208
+ 'data-segment-label': segment.label,
209
+ 'data-segment-title': segment.title,
210
+ contenteditable: 'false',
211
+ }, [
212
+ renderTokenAction('open', segment),
213
+ renderTokenAction('remove', segment),
214
+ ])
215
+ )),
216
+ ...(props.trailingCaretHelper
217
+ ? [createTextVNode(INLINE_TOKEN_EDITOR_VISUAL_CARET_TEXT)]
218
+ : []),
219
+ ])
220
+ },
221
+ })
222
+ </script>
223
+
224
+ <style lang="postcss" src="../SInlineTokenEditor.css" scoped></style>
@@ -0,0 +1,178 @@
1
+ import type { Component } from 'vue'
2
+ import { resolveLinkTarget } from '../../../internal/linkTarget'
3
+ import {
4
+ validateBoolean,
5
+ validateExactString,
6
+ validateNonEmptyString,
7
+ } from '../../../internal/runtimeContract'
8
+ import type { BadgeSeverity } from '../data-display/SBadge.vue'
9
+ import type { LinkDestination } from './SLink'
10
+
11
+ export type SMenuItemLayout = 'row' | 'tile'
12
+ export type SMenuCheckedState = boolean | 'mixed'
13
+ export type SMenuActivationEvent = MouseEvent | KeyboardEvent
14
+
15
+ interface SMenuLabelledEntry {
16
+ readonly id: string
17
+ readonly label: string
18
+ readonly icon?: Component
19
+ readonly disabled?: boolean
20
+ readonly shortcut?: string
21
+ readonly badge?: string | number
22
+ readonly badgeSeverity?: BadgeSeverity
23
+ readonly layout?: SMenuItemLayout
24
+ }
25
+
26
+ /** Команда без собственной navigation-state semantics. / A command without navigation-state semantics. */
27
+ export interface SMenuAction extends SMenuLabelledEntry {
28
+ readonly kind: 'action'
29
+ readonly current?: boolean
30
+ }
31
+
32
+ /** Навигационное назначение с ровно одним href/to target. / A navigation destination with exactly one href/to target. */
33
+ export type SMenuDestination = SMenuLabelledEntry & LinkDestination & {
34
+ readonly kind: 'destination'
35
+ readonly current?: boolean
36
+ }
37
+
38
+ /** Управляемый checkbox intent; parent обновляет checked-state. / A controlled checkbox intent; the parent updates checked state. */
39
+ export interface SMenuCheckbox extends SMenuLabelledEntry {
40
+ readonly kind: 'checkbox'
41
+ readonly checked: SMenuCheckedState
42
+ }
43
+
44
+ /** Управляемый radio intent внутри стабильной groupId. / A controlled radio intent within a stable groupId. */
45
+ export interface SMenuRadio extends SMenuLabelledEntry {
46
+ readonly kind: 'radio'
47
+ readonly groupId: string
48
+ readonly checked: boolean
49
+ }
50
+
51
+ /** Ветка вложенного меню; сама не является доменной командой. / A nested-menu branch; it is not itself a domain command. */
52
+ export interface SMenuSubmenu extends SMenuLabelledEntry {
53
+ readonly kind: 'submenu'
54
+ readonly children: readonly SMenuEntry[]
55
+ }
56
+
57
+ export interface SMenuSeparatorEntry {
58
+ readonly kind: 'separator'
59
+ readonly id: string
60
+ }
61
+
62
+ export type SMenuSelectableEntry = SMenuAction | SMenuDestination | SMenuCheckbox | SMenuRadio
63
+ export type SMenuInteractiveEntry = SMenuSelectableEntry | SMenuSubmenu
64
+ export type SMenuEntry = SMenuInteractiveEntry | SMenuSeparatorEntry
65
+
66
+ const ENTRY_KINDS = ['action', 'destination', 'checkbox', 'radio', 'separator', 'submenu'] as const
67
+ const LAYOUTS = ['row', 'tile'] as const
68
+ const BADGE_SEVERITIES = ['secondary', 'success', 'info', 'warn', 'danger', 'contrast'] as const
69
+
70
+ const COMMON_KEYS = ['kind', 'id', 'label', 'icon', 'disabled', 'shortcut', 'badge', 'badgeSeverity', 'layout'] as const
71
+ const KIND_KEYS = {
72
+ action: ['current'],
73
+ destination: ['href', 'to', 'target', 'rel', 'current'],
74
+ checkbox: ['checked'],
75
+ radio: ['groupId', 'checked'],
76
+ separator: [],
77
+ submenu: ['children'],
78
+ } as const
79
+
80
+ function assertRecord(value: unknown, coordinate: string): asserts value is Record<string, unknown> {
81
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) return
82
+ throw new TypeError(`SMenuSurface: ${coordinate} must be an object`)
83
+ }
84
+
85
+ function validateOptionalBoolean(entry: Record<string, unknown>, key: string, coordinate: string): void {
86
+ if (entry[key] !== undefined) validateBoolean('SMenuSurface', `${coordinate}.${key}`, entry[key])
87
+ }
88
+
89
+ function validateOptionalString(entry: Record<string, unknown>, key: string, coordinate: string): void {
90
+ if (entry[key] !== undefined) validateNonEmptyString('SMenuSurface', `${coordinate}.${key}`, entry[key])
91
+ }
92
+
93
+ function validateCommon(entry: Record<string, unknown>, coordinate: string): void {
94
+ validateNonEmptyString('SMenuSurface', `${coordinate}.id`, entry.id)
95
+ validateNonEmptyString('SMenuSurface', `${coordinate}.label`, entry.label)
96
+ validateOptionalBoolean(entry, 'disabled', coordinate)
97
+ validateOptionalString(entry, 'shortcut', coordinate)
98
+ if (entry.icon !== undefined && typeof entry.icon !== 'object' && typeof entry.icon !== 'function') {
99
+ throw new TypeError(`SMenuSurface: ${coordinate}.icon must be a Vue component`)
100
+ }
101
+ if (entry.badge !== undefined && (
102
+ (typeof entry.badge !== 'string' || entry.badge.length === 0)
103
+ && (typeof entry.badge !== 'number' || !Number.isFinite(entry.badge))
104
+ )) throw new TypeError(`SMenuSurface: ${coordinate}.badge must be a non-empty string or finite number`)
105
+ if (entry.badgeSeverity !== undefined) {
106
+ validateExactString('SMenuSurface', `${coordinate}.badgeSeverity`, entry.badgeSeverity, BADGE_SEVERITIES)
107
+ }
108
+ if (entry.layout !== undefined) validateExactString('SMenuSurface', `${coordinate}.layout`, entry.layout, LAYOUTS)
109
+ }
110
+
111
+ function assertExactKeys(entry: Record<string, unknown>, kind: typeof ENTRY_KINDS[number], coordinate: string): void {
112
+ const allowed = new Set<string>(kind === 'separator'
113
+ ? ['kind', 'id']
114
+ : [...COMMON_KEYS, ...KIND_KEYS[kind]])
115
+ for (const key of Object.keys(entry)) {
116
+ if (!allowed.has(key)) throw new TypeError(`SMenuSurface: ${coordinate} has unsupported field '${key}'`)
117
+ }
118
+ }
119
+
120
+ function validateList(
121
+ value: unknown,
122
+ coordinate: string,
123
+ ids: Set<string>,
124
+ ): asserts value is readonly SMenuEntry[] {
125
+ if (!Array.isArray(value)) throw new TypeError(`SMenuSurface: ${coordinate} must be an array`)
126
+ if (coordinate !== 'items' && value.length === 0) {
127
+ throw new TypeError(`SMenuSurface: ${coordinate} children must be a non-empty array`)
128
+ }
129
+ const checkedRadioGroups = new Set<string>()
130
+ let previousSeparator = false
131
+ for (const [index, candidate] of value.entries()) {
132
+ const entryCoordinate = `${coordinate}[${index}]`
133
+ assertRecord(candidate, entryCoordinate)
134
+ const kind = validateExactString('SMenuSurface', `${entryCoordinate}.kind`, candidate.kind, ENTRY_KINDS)
135
+ assertExactKeys(candidate, kind, entryCoordinate)
136
+ const id = validateNonEmptyString('SMenuSurface', `${entryCoordinate}.id`, candidate.id)
137
+ if (ids.has(id)) throw new TypeError(`SMenuSurface: duplicate entry id '${id}'`)
138
+ ids.add(id)
139
+ if (kind === 'separator') {
140
+ if (index === 0 || index === value.length - 1 || previousSeparator) {
141
+ throw new TypeError('SMenuSurface: separator cannot be first, last, or consecutive')
142
+ }
143
+ previousSeparator = true
144
+ continue
145
+ }
146
+ previousSeparator = false
147
+ validateCommon(candidate, entryCoordinate)
148
+ if (kind === 'action' || kind === 'destination') validateOptionalBoolean(candidate, 'current', entryCoordinate)
149
+ if (kind === 'destination') {
150
+ resolveLinkTarget({
151
+ owner: `SMenuSurface: ${entryCoordinate}`,
152
+ href: candidate.href,
153
+ to: candidate.to,
154
+ target: candidate.target,
155
+ rel: candidate.rel,
156
+ })
157
+ }
158
+ if (kind === 'checkbox') {
159
+ if (candidate.checked !== 'mixed' && typeof candidate.checked !== 'boolean') {
160
+ throw new TypeError(`SMenuSurface: ${entryCoordinate}.checked must be a boolean or "mixed"`)
161
+ }
162
+ }
163
+ if (kind === 'radio') {
164
+ const groupId = validateNonEmptyString('SMenuSurface', `${entryCoordinate}.groupId`, candidate.groupId)
165
+ const checked = validateBoolean('SMenuSurface', `${entryCoordinate}.checked`, candidate.checked)
166
+ if (checked && checkedRadioGroups.has(groupId)) {
167
+ throw new TypeError(`SMenuSurface: radio group '${groupId}' has more than one checked entry`)
168
+ }
169
+ if (checked) checkedRadioGroups.add(groupId)
170
+ }
171
+ if (kind === 'submenu') validateList(candidate.children, `${entryCoordinate}.children`, ids)
172
+ }
173
+ }
174
+
175
+ /** Проверяет полный recursive discriminated menu contract без coercion/fallback. / Validates the complete recursive discriminated menu contract without coercion/fallback. */
176
+ export function validateSMenuEntries(value: unknown): asserts value is readonly SMenuEntry[] {
177
+ validateList(value, 'items', new Set<string>())
178
+ }
@@ -0,0 +1,6 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-action-card { @apply grid min-h-0 min-w-0 items-start gap-2; grid-template-columns: minmax(0, 1fr) auto; }
4
+ .s-action-card[data-fill='container'] { @apply h-full w-full; }
5
+ .s-action-card__content { @apply flex min-h-0 min-w-0 w-full flex-1 flex-col; }
6
+ .s-action-card__actions { @apply flex shrink-0 items-center gap-1; }
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import type { InteractiveSurfaceContract, InteractiveSurfaceDensity, InteractiveSurfaceFill, InteractiveSurfacePadding, InteractiveSurfaceRadius, InteractiveSurfaceTone } from '../controls/SInteractiveSurface.vue'
3
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
4
+ import SInteractiveSurface from '../controls/SInteractiveSurface.vue'
5
+
6
+ defineOptions({ inheritAttrs: false })
7
+
8
+ export interface Props {
9
+ surface: InteractiveSurfaceContract
10
+ selected?: boolean
11
+ tone?: InteractiveSurfaceTone
12
+ density?: InteractiveSurfaceDensity
13
+ padding?: InteractiveSurfacePadding
14
+ radius?: InteractiveSurfaceRadius
15
+ fill?: InteractiveSurfaceFill
16
+ }
17
+
18
+ withDefaults(defineProps<Props>(), {
19
+ selected: false,
20
+ tone: 'default',
21
+ density: 'comfortable',
22
+ padding: 'md',
23
+ radius: 'lg',
24
+ fill: 'content',
25
+ })
26
+
27
+ const emit = defineEmits<{ activate: [event: MouseEvent] }>()
28
+ const ownedAttrs = useOwnedAttrs({ component: 'SActionCard', owner: 'action card root' })
29
+ </script>
30
+
31
+ <template>
32
+ <div v-bind="ownedAttrs.bindings()" class="s-action-card" :data-has-actions="Boolean($slots.actions) || undefined" :data-fill="fill" :data-mode="surface.mode">
33
+ <SInteractiveSurface
34
+ :surface="surface"
35
+ :selected="selected"
36
+ :tone="tone"
37
+ :density="density"
38
+ :padding="padding"
39
+ :radius="radius"
40
+ :fill="fill"
41
+ @activate="emit('activate', $event)"
42
+ >
43
+ <div class="s-action-card__content"><slot /></div>
44
+ </SInteractiveSurface>
45
+ <div v-if="$slots.actions" class="s-action-card__actions"><slot name="actions" /></div>
46
+ </div>
47
+ </template>
48
+
49
+ <style lang="postcss" src="./SActionCard.css" scoped></style>
@@ -0,0 +1,15 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-action-list {
4
+ @apply grid min-w-0 gap-1 overflow-y-auto rounded-md border border-surface-200 p-1 dark:border-surface-700;
5
+ container-type: inline-size;
6
+ overscroll-behavior: contain;
7
+ }
8
+
9
+ .s-action-list[data-surface='plain'] {
10
+ @apply rounded-none border-0 p-0;
11
+ }
12
+
13
+ .s-action-list[data-max-height='sm'] { max-block-size: var(--s-scroll-viewport-max-block-size-sm); }
14
+ .s-action-list[data-max-height='md'] { max-block-size: var(--s-scroll-viewport-max-block-size-md); }
15
+ .s-action-list[data-max-height='lg'] { max-block-size: var(--s-scroll-viewport-max-block-size-lg); }
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
4
+ import {
5
+ validateScrollableViewportMaxBlockSize,
6
+ type ScrollableViewportMaxBlockSize,
7
+ } from '../../../internal/semanticSizing'
8
+
9
+ defineOptions({ inheritAttrs: false })
10
+
11
+ const props = withDefaults(defineProps<{
12
+ accessibleLabel: string
13
+ maxHeight?: ScrollableViewportMaxBlockSize
14
+ surface?: 'bordered' | 'plain'
15
+ }>(), {
16
+ maxHeight: 'none',
17
+ surface: 'bordered',
18
+ })
19
+
20
+ const ownedAttrs = useOwnedAttrs({ component: 'SActionList', owner: 'action list root' })
21
+ const validatedMaxHeight = computed(() => validateScrollableViewportMaxBlockSize(
22
+ 'SActionList',
23
+ 'maxHeight',
24
+ props.maxHeight,
25
+ ))
26
+ </script>
27
+
28
+ <template>
29
+ <div v-bind="ownedAttrs.bindings()" class="s-action-list" :data-max-height="validatedMaxHeight" :data-surface="surface" role="list" :aria-label="accessibleLabel">
30
+ <slot />
31
+ </div>
32
+ </template>
33
+
34
+ <style lang="postcss" src="./SActionList.css" scoped></style>
@@ -0,0 +1,114 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-action-list-item {
4
+ @apply grid min-h-12 min-w-0 items-center gap-2;
5
+ height: max-content;
6
+ grid-template-columns: minmax(0, 1fr) auto;
7
+ }
8
+
9
+ .s-action-list-item[data-height-mode='fill'] {
10
+ @apply h-full overflow-hidden;
11
+ }
12
+
13
+ .s-action-list-item[data-height-mode='fill'] .s-action-list-item__main--static {
14
+ @apply h-full min-h-0;
15
+ }
16
+
17
+ .s-action-list-item__main {
18
+ @apply flex min-h-11 min-w-0 flex-1 items-center gap-3 rounded-md px-3 py-2 text-left;
19
+ padding-left: calc(0.75rem + var(--s-action-list-item-indent));
20
+ }
21
+
22
+ .s-action-list-item__main--static {
23
+ @apply cursor-default;
24
+ }
25
+
26
+ .s-action-list-item__icon {
27
+ @apply grid h-9 w-9 shrink-0 place-items-center rounded-md bg-primary-100 text-primary-700;
28
+ @apply dark:bg-primary-900 dark:text-primary-200;
29
+ }
30
+
31
+ .s-action-list-item[data-tone='success'] .s-action-list-item__icon {
32
+ @apply bg-success-100 text-success-700 dark:bg-success-900 dark:text-success-200;
33
+ }
34
+
35
+ .s-action-list-item[data-tone='danger'] .s-action-list-item__icon {
36
+ @apply bg-danger-100 text-danger-700 dark:bg-danger-900 dark:text-danger-200;
37
+ }
38
+
39
+ .s-action-list-item__icon svg {
40
+ @apply h-5 w-5;
41
+ }
42
+
43
+ .s-action-list-item__copy {
44
+ @apply grid min-w-0 flex-1 gap-0.5;
45
+ }
46
+
47
+ .s-action-list-item__title,
48
+ .s-action-list-item__description,
49
+ .s-action-list-item__detail {
50
+ @apply truncate;
51
+ }
52
+
53
+ .s-action-list-item__title {
54
+ @apply text-sm font-semibold text-surface-900 dark:text-surface-50;
55
+ }
56
+
57
+ .s-action-list-item[data-title-weight='regular'] .s-action-list-item__title {
58
+ @apply font-normal;
59
+ }
60
+
61
+ .s-action-list-item[data-title-presentation='code'] .s-action-list-item__title {
62
+ @apply font-mono;
63
+ }
64
+
65
+ .s-action-list-item__description {
66
+ @apply text-xs text-surface-600 dark:text-surface-400;
67
+ }
68
+
69
+ .s-action-list-item__detail {
70
+ @apply line-clamp-2 whitespace-normal text-surface-600 dark:text-surface-300;
71
+ }
72
+
73
+ .s-action-list-item[data-detail-overflow='wrap'] .s-action-list-item__detail {
74
+ @apply line-clamp-none;
75
+ overflow-wrap: anywhere;
76
+ word-break: break-word;
77
+ }
78
+
79
+ .s-action-list-item[data-detail-presentation='code'] .s-action-list-item__detail {
80
+ @apply rounded-md bg-surface-100 p-2 font-mono text-xs dark:bg-surface-800;
81
+ white-space: pre-wrap;
82
+ }
83
+
84
+ .s-action-list-item__trailing {
85
+ @apply flex shrink-0 items-center gap-2 pr-2;
86
+ }
87
+
88
+ .s-action-list-item__trailing-icon {
89
+ @apply h-4 w-4 shrink-0 text-surface-400;
90
+ }
91
+
92
+ .s-action-list-item[data-density='compact'] { @apply min-h-10 gap-1; }
93
+ .s-action-list-item[data-density='compact'] .s-action-list-item__main { @apply min-h-9 gap-2 px-2 py-1.5; }
94
+ .s-action-list-item[data-density='compact'] .s-action-list-item__icon { @apply h-7 w-7; }
95
+
96
+ .s-action-list-item[data-trailing-layout='stacked'] {
97
+ grid-template-columns: minmax(0, 1fr);
98
+ }
99
+
100
+ .s-action-list-item[data-trailing-layout='stacked'] .s-action-list-item__trailing {
101
+ @apply w-full justify-end pb-2 pr-2;
102
+ padding-left: calc(3.5rem + var(--s-action-list-item-indent));
103
+ }
104
+
105
+ @container (max-width: 32rem) {
106
+ .s-action-list-item[data-trailing-layout='responsive'] {
107
+ grid-template-columns: minmax(0, 1fr);
108
+ }
109
+
110
+ .s-action-list-item[data-trailing-layout='responsive'] .s-action-list-item__trailing {
111
+ @apply w-full justify-end pb-2 pr-2;
112
+ padding-left: calc(3.5rem + var(--s-action-list-item-indent));
113
+ }
114
+ }