@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,326 @@
1
+ <script setup lang="ts">
2
+ import { computed, watchEffect, type VNodeChild } from 'vue'
3
+ import { RouterLink, type RouteLocationRaw } from 'vue-router'
4
+ import type { STooltipTriggerBinding } from '../data-display/STooltip.vue'
5
+ import { resolveLinkTarget } from '../../../internal/linkTarget'
6
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
7
+ import {
8
+ PassiveContentBoundary,
9
+ useInteractiveLeafRegistration,
10
+ type PassiveContentContract,
11
+ } from '../../../internal/passiveContentContract'
12
+
13
+ defineOptions({ inheritAttrs: false })
14
+
15
+ export type InteractiveSurfaceTone = 'default' | 'primary' | 'success' | 'danger'
16
+ export type InteractiveSurfaceDensity = 'compact' | 'comfortable'
17
+ export type InteractiveSurfacePadding = 'none' | 'sm' | 'md'
18
+ export type InteractiveSurfaceRadius = 'md' | 'lg' | 'xl'
19
+ export type InteractiveSurfaceFill = 'content' | 'container'
20
+
21
+ /**
22
+ * Единственный semantic-контракт поверхности: passive, action или ровно одна link-target.
23
+ * The sole surface semantics contract: passive, action, or exactly one link target.
24
+ */
25
+ export type InteractiveSurfaceContract =
26
+ | { readonly mode: 'static' }
27
+ | {
28
+ readonly mode: 'action'
29
+ readonly accessibleLabel: string
30
+ readonly disabled?: boolean
31
+ readonly pressed?: boolean
32
+ }
33
+ | ({
34
+ readonly mode: 'link'
35
+ readonly accessibleLabel: string
36
+ readonly disabled?: boolean
37
+ readonly target?: '_self' | '_blank' | '_parent' | '_top'
38
+ readonly rel?: string
39
+ } & (
40
+ | { readonly href: string; readonly to?: never }
41
+ | { readonly href?: never; readonly to: RouteLocationRaw }
42
+ ))
43
+ /** Разрешённые semantic-режимы поверхности. / Supported semantic surface modes. */
44
+ export type InteractiveSurfaceMode = InteractiveSurfaceContract['mode']
45
+
46
+ /**
47
+ * Контракт большой интерактивной поверхности с пассивным содержимым.
48
+ * Contract for a large interactive surface whose visual content remains passive.
49
+ */
50
+ export interface Props {
51
+ surface: InteractiveSurfaceContract
52
+ selected?: boolean
53
+ tone?: InteractiveSurfaceTone
54
+ density?: InteractiveSurfaceDensity
55
+ padding?: InteractiveSurfacePadding
56
+ radius?: InteractiveSurfaceRadius
57
+ fill?: InteractiveSurfaceFill
58
+ contentAlign?: 'start' | 'center'
59
+ /** Полный binding явного STooltip trigger, направляемый на activation owner. / Complete explicit STooltip trigger binding routed to the activation owner. */
60
+ tooltipTrigger?: STooltipTriggerBinding
61
+ }
62
+
63
+ const props = withDefaults(defineProps<Props>(), {
64
+ selected: false,
65
+ tone: 'default',
66
+ density: 'comfortable',
67
+ padding: 'md',
68
+ radius: 'lg',
69
+ fill: 'content',
70
+ contentAlign: 'start',
71
+ tooltipTrigger: undefined,
72
+ })
73
+
74
+ const emit = defineEmits<{
75
+ activate: [event: MouseEvent]
76
+ }>()
77
+ defineSlots<{
78
+ /** Пассивное flow-содержимое поверхности. / Passive flow content of the surface. */
79
+ default?: () => VNodeChild
80
+ }>()
81
+
82
+ const ownedAttrs = useOwnedAttrs({ component: 'SInteractiveSurface', owner: 'interactive surface owner' })
83
+
84
+ const assertOptionalBoolean = (surface: Record<string, unknown>, field: 'disabled' | 'pressed'): void => {
85
+ if (surface[field] !== undefined && typeof surface[field] !== 'boolean') {
86
+ throw new TypeError(`SInteractiveSurface: surface.${field} должен быть boolean`)
87
+ }
88
+ }
89
+
90
+ const assertExactSurfaceKeys = (surface: Record<string, unknown>, allowed: readonly string[]): void => {
91
+ const allowedKeys = new Set(allowed)
92
+ const unexpected = Object.keys(surface).filter((key) => !allowedKeys.has(key))
93
+ if (unexpected.length > 0) {
94
+ throw new Error(`SInteractiveSurface: surface mode="${String(surface.mode)}" не допускает поля ${unexpected.join(', ')}`)
95
+ }
96
+ }
97
+
98
+ const validatedSurface = computed<InteractiveSurfaceContract>(() => {
99
+ const candidate: unknown = props.surface
100
+ if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) {
101
+ throw new TypeError('SInteractiveSurface: surface должен быть discriminated object')
102
+ }
103
+ const surface = candidate as Record<string, unknown>
104
+ if (surface.mode === 'static') {
105
+ assertExactSurfaceKeys(surface, ['mode'])
106
+ return { mode: 'static' }
107
+ }
108
+ if (surface.mode === 'action') {
109
+ assertExactSurfaceKeys(surface, ['mode', 'accessibleLabel', 'disabled', 'pressed'])
110
+ assertOptionalBoolean(surface, 'disabled')
111
+ assertOptionalBoolean(surface, 'pressed')
112
+ if (typeof surface.accessibleLabel !== 'string' || !surface.accessibleLabel.trim()) {
113
+ throw new TypeError('SInteractiveSurface: accessibleLabel обязателен для mode="action"')
114
+ }
115
+ return {
116
+ mode: 'action',
117
+ accessibleLabel: surface.accessibleLabel,
118
+ disabled: surface.disabled as boolean | undefined,
119
+ pressed: surface.pressed as boolean | undefined,
120
+ }
121
+ }
122
+ if (surface.mode === 'link') {
123
+ assertExactSurfaceKeys(surface, ['mode', 'accessibleLabel', 'disabled', 'target', 'rel', 'href', 'to'])
124
+ assertOptionalBoolean(surface, 'disabled')
125
+ if (typeof surface.accessibleLabel !== 'string' || !surface.accessibleLabel.trim()) {
126
+ throw new TypeError('SInteractiveSurface: accessibleLabel обязателен для mode="link"')
127
+ }
128
+ const target = surface.target ?? '_self'
129
+ if (!['_self', '_blank', '_parent', '_top'].includes(String(target))) {
130
+ throw new TypeError('SInteractiveSurface: surface.target имеет недопустимое значение')
131
+ }
132
+ if (surface.rel !== undefined && typeof surface.rel !== 'string') {
133
+ throw new TypeError('SInteractiveSurface: surface.rel должен быть string')
134
+ }
135
+ if ('href' in surface && (typeof surface.href !== 'string' || surface.href.length === 0)) {
136
+ throw new TypeError('SInteractiveSurface: surface.href должен быть непустой string')
137
+ }
138
+ if (
139
+ 'to' in surface
140
+ && !(
141
+ typeof surface.to === 'string'
142
+ || (typeof surface.to === 'object' && surface.to !== null && !Array.isArray(surface.to))
143
+ )
144
+ ) {
145
+ throw new TypeError('SInteractiveSurface: surface.to должен быть RouteLocationRaw')
146
+ }
147
+ const resolved = resolveLinkTarget({
148
+ href: typeof surface.href === 'string' ? surface.href : undefined,
149
+ to: surface.to as RouteLocationRaw | undefined,
150
+ target: target as '_self' | '_blank' | '_parent' | '_top',
151
+ rel: surface.rel as string | undefined,
152
+ owner: 'SInteractiveSurface',
153
+ })
154
+ return resolved.kind === 'anchor'
155
+ ? {
156
+ mode: 'link',
157
+ accessibleLabel: surface.accessibleLabel,
158
+ disabled: surface.disabled as boolean | undefined,
159
+ href: resolved.bindings.href,
160
+ target: resolved.bindings.target,
161
+ rel: resolved.bindings.rel,
162
+ }
163
+ : {
164
+ mode: 'link',
165
+ accessibleLabel: surface.accessibleLabel,
166
+ disabled: surface.disabled as boolean | undefined,
167
+ to: resolved.bindings.to,
168
+ target: resolved.bindings.target,
169
+ rel: resolved.bindings.rel,
170
+ }
171
+ }
172
+ throw new TypeError(`SInteractiveSurface: неизвестный surface.mode "${String(surface.mode)}"`)
173
+ })
174
+
175
+ const TOOLTIP_TRIGGER_KEYS = [
176
+ 'ref',
177
+ 'aria-describedby',
178
+ 'onPointerenter',
179
+ 'onPointerleave',
180
+ 'onPointerdown',
181
+ 'onPointerup',
182
+ 'onPointercancel',
183
+ 'onFocusin',
184
+ 'onFocusout',
185
+ ] as const
186
+
187
+ const validatedTooltipTrigger = computed<STooltipTriggerBinding | undefined>(() => {
188
+ const candidate: unknown = props.tooltipTrigger
189
+ if (candidate === undefined) return undefined
190
+ if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) {
191
+ throw new TypeError('SInteractiveSurface: tooltipTrigger должен быть полным STooltip trigger binding. / SInteractiveSurface: tooltipTrigger must be a complete STooltip trigger binding.')
192
+ }
193
+ const record = candidate as Record<string, unknown>
194
+ const unexpected = Object.keys(record).filter((key) => !TOOLTIP_TRIGGER_KEYS.includes(key as typeof TOOLTIP_TRIGGER_KEYS[number]))
195
+ const missing = TOOLTIP_TRIGGER_KEYS.filter((key) => !Object.hasOwn(record, key))
196
+ if (unexpected.length > 0 || missing.length > 0) {
197
+ throw new Error(`SInteractiveSurface: tooltipTrigger имеет недопустимый набор ключей; missing=${missing.join(',')}; unexpected=${unexpected.join(',')}. / SInteractiveSurface: tooltipTrigger has an invalid key set; missing=${missing.join(',')}; unexpected=${unexpected.join(',')}.`)
198
+ }
199
+ if (record['aria-describedby'] !== undefined && typeof record['aria-describedby'] !== 'string') {
200
+ throw new TypeError('SInteractiveSurface: tooltipTrigger aria-describedby должен быть string или undefined. / SInteractiveSurface: tooltipTrigger aria-describedby must be a string or undefined.')
201
+ }
202
+ if (typeof record.ref !== 'function') {
203
+ throw new TypeError('SInteractiveSurface: tooltipTrigger.ref должен быть callback ref. / SInteractiveSurface: tooltipTrigger.ref must be a callback ref.')
204
+ }
205
+ for (const key of TOOLTIP_TRIGGER_KEYS) {
206
+ if (key !== 'ref' && key !== 'aria-describedby' && typeof record[key] !== 'function') {
207
+ throw new TypeError(`SInteractiveSurface: tooltipTrigger.${key} должен быть function. / SInteractiveSurface: tooltipTrigger.${key} must be a function.`)
208
+ }
209
+ }
210
+ return candidate as STooltipTriggerBinding
211
+ })
212
+
213
+ const activationBindings = (): Readonly<Record<string, unknown>> => ({
214
+ ...ownedAttrs.bindings(),
215
+ ...(validatedTooltipTrigger.value ?? {}),
216
+ })
217
+ const surfaceMode = computed<InteractiveSurfaceMode>(() => validatedSurface.value.mode)
218
+ const surfaceDisabled = computed(() => validatedSurface.value.mode === 'static'
219
+ ? false
220
+ : validatedSurface.value.disabled ?? false)
221
+ const surfacePressed = computed(() => validatedSurface.value.mode === 'action' ? validatedSurface.value.pressed : undefined)
222
+ const resolvedTarget = computed(() => {
223
+ const surface = validatedSurface.value
224
+ if (surface.mode !== 'link') return null
225
+ return resolveLinkTarget({
226
+ href: surface.href,
227
+ to: surface.to,
228
+ target: surface.target ?? '_self',
229
+ rel: surface.rel,
230
+ owner: 'SInteractiveSurface',
231
+ })
232
+ })
233
+ const linkComponent = computed(() => resolvedTarget.value?.kind === 'router' ? RouterLink : 'a')
234
+ const linkBindings = () => {
235
+ const target = resolvedTarget.value
236
+ if (!target) throw new Error('SInteractiveSurface: link bindings requested outside link surface')
237
+ return {
238
+ ...activationBindings(),
239
+ ...target.bindings,
240
+ }
241
+ }
242
+ const resolvedAccessibleLabel = computed(() => {
243
+ if (validatedSurface.value.mode === 'static') return undefined
244
+ return validatedSurface.value.accessibleLabel.trim()
245
+ })
246
+
247
+ watchEffect(() => {
248
+ if (validatedSurface.value.mode === 'static' && validatedTooltipTrigger.value) {
249
+ throw new Error('SInteractiveSurface: tooltipTrigger требует action или link surface. / SInteractiveSurface: tooltipTrigger requires an action or link surface.')
250
+ }
251
+ void validatedTooltipTrigger.value
252
+ })
253
+
254
+ useInteractiveLeafRegistration({
255
+ owner: 'SInteractiveSurface',
256
+ active: () => surfaceMode.value !== 'static',
257
+ })
258
+ const passiveContentContract: PassiveContentContract = {
259
+ owner: 'SInteractiveSurface',
260
+ region: 'passive content-region',
261
+ active: () => surfaceMode.value !== 'static',
262
+ guidance: 'Передайте дополнительное действие через sibling action-region. / Put the additional action in the sibling action region.',
263
+ }
264
+
265
+ function activateLink(event: MouseEvent): void {
266
+ if (surfaceDisabled.value) {
267
+ event.preventDefault()
268
+ return
269
+ }
270
+ emit('activate', event)
271
+ }
272
+
273
+ function activateAction(event: MouseEvent): void {
274
+ emit('activate', event)
275
+ }
276
+ </script>
277
+
278
+ <template>
279
+ <div
280
+ v-bind="surfaceMode === 'static' ? ownedAttrs.bindings() : undefined"
281
+ class="s-interactive-surface"
282
+ :data-mode="surfaceMode"
283
+ :data-tone="tone"
284
+ :data-density="density"
285
+ :data-padding="padding"
286
+ :data-radius="radius"
287
+ :data-fill="fill"
288
+ :data-selected="selected || undefined"
289
+ :data-pressed="surfacePressed"
290
+ :data-disabled="surfaceDisabled || undefined"
291
+ :data-content-align="contentAlign"
292
+ >
293
+ <button
294
+ v-if="surfaceMode === 'action'"
295
+ v-bind="activationBindings()"
296
+ class="s-interactive-surface__activation"
297
+ type="button"
298
+ :disabled="surfaceDisabled"
299
+ :aria-label="resolvedAccessibleLabel"
300
+ :aria-pressed="surfacePressed"
301
+ @click="activateAction"
302
+ >
303
+ <span class="s-interactive-surface__accessible-label">{{ resolvedAccessibleLabel }}</span>
304
+ </button>
305
+ <component
306
+ v-else-if="surfaceMode === 'link'"
307
+ :is="linkComponent"
308
+ v-bind="linkBindings()"
309
+ class="s-interactive-surface__activation"
310
+ :aria-label="resolvedAccessibleLabel"
311
+ :aria-current="selected ? 'page' : undefined"
312
+ :aria-disabled="surfaceDisabled || undefined"
313
+ :tabindex="surfaceDisabled ? -1 : undefined"
314
+ @click="activateLink"
315
+ >
316
+ <span class="s-interactive-surface__accessible-label">{{ resolvedAccessibleLabel }}</span>
317
+ </component>
318
+ <div class="s-interactive-surface__content" data-passive-content="">
319
+ <PassiveContentBoundary :contract="passiveContentContract">
320
+ <slot />
321
+ </PassiveContentBoundary>
322
+ </div>
323
+ </div>
324
+ </template>
325
+
326
+ <style lang="postcss" src="./SInteractiveSurface.css" scoped></style>
@@ -0,0 +1,114 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-link {
4
+ @apply inline-flex min-w-0 items-center rounded-sm font-medium text-primary-600 underline-offset-2;
5
+ @apply hover:text-primary-700;
6
+ @apply dark:text-primary-400 dark:hover:text-primary-300;
7
+ }
8
+
9
+ .s-link:focus-visible {
10
+ outline: var(--s-focus-ring-width) solid var(--s-focus-ring-color);
11
+ outline-offset: var(--s-focus-ring-offset);
12
+ }
13
+
14
+ .s-link[data-width='full'] {
15
+ @apply w-full;
16
+ }
17
+
18
+ .s-link[data-size='sm'] {
19
+ @apply text-sm;
20
+ }
21
+
22
+ .s-link[data-size='md'] {
23
+ @apply text-base;
24
+ }
25
+
26
+ .s-link[data-underline='hover'] {
27
+ @apply no-underline hover:underline;
28
+ }
29
+
30
+ .s-link[data-underline='always'] {
31
+ @apply underline;
32
+ }
33
+
34
+ .s-link[data-underline='none'] {
35
+ @apply no-underline hover:no-underline;
36
+ }
37
+
38
+ .s-link__icon {
39
+ @apply inline-flex shrink-0 items-center justify-center;
40
+ width: var(--s-icon-size-xs);
41
+ height: var(--s-icon-size-xs);
42
+ }
43
+
44
+ .s-link__icon-graphic {
45
+ @apply h-full w-full;
46
+ }
47
+
48
+ .s-link__content {
49
+ @apply min-w-0;
50
+ }
51
+
52
+ .s-link[data-wrap='anywhere'] .s-link__content {
53
+ overflow-wrap: anywhere;
54
+ word-break: break-word;
55
+ }
56
+
57
+ .s-link[data-content-kind='identifier'] {
58
+ @apply font-mono;
59
+ }
60
+
61
+ .s-link__trailing {
62
+ @apply ml-auto shrink-0;
63
+ }
64
+
65
+ .s-link[data-variant='subtle'] {
66
+ @apply text-surface-600 dark:text-surface-300;
67
+ }
68
+
69
+ .s-link[data-variant='document'] {
70
+ display: inline;
71
+ font: inherit;
72
+ @apply cursor-pointer text-primary-600 underline decoration-primary-300 underline-offset-2 transition-colors;
73
+ @apply hover:text-primary-700 hover:decoration-primary-500;
74
+ @apply dark:text-primary-400 dark:hover:text-primary-300;
75
+ }
76
+
77
+ .s-link[data-variant='document'] .s-link__content {
78
+ display: inline;
79
+ }
80
+
81
+ .s-link[data-variant='brand'] {
82
+ @apply gap-3 rounded-xl text-inherit no-underline transition-opacity hover:text-inherit hover:no-underline hover:opacity-90;
83
+ }
84
+
85
+ .s-link[data-variant='navigation'] {
86
+ @apply justify-between gap-2 rounded-2xl border border-transparent px-3 py-2.5 text-sm text-surface-700 no-underline transition;
87
+ @apply hover:border-surface-200 hover:bg-surface-100/80 hover:text-surface-900 hover:no-underline;
88
+ @apply dark:text-surface-200 dark:hover:border-surface-700 dark:hover:bg-surface-800/60 dark:hover:text-surface-0;
89
+ }
90
+
91
+ .s-link[data-variant='navigation'] .s-link__content {
92
+ @apply truncate;
93
+ }
94
+
95
+ .s-link[data-variant='navigation'][data-active='true'] {
96
+ @apply border-primary-200 bg-primary-50 text-primary-700;
97
+ @apply dark:border-primary-800 dark:bg-primary-950/50 dark:text-primary-200;
98
+ }
99
+
100
+ .s-link[data-variant='bottom-navigation'] {
101
+ @apply w-full justify-center gap-2 rounded-xl px-2 py-2 text-sm text-surface-600 no-underline transition;
102
+ @apply hover:bg-surface-100 hover:text-surface-900 hover:no-underline;
103
+ min-height: var(--s-control-hitbox-sm);
104
+ }
105
+
106
+ .s-link[data-variant='bottom-navigation'][data-active='true'] {
107
+ @apply bg-primary-50 text-primary-700;
108
+ @apply dark:bg-primary-950/50 dark:text-primary-200;
109
+ }
110
+
111
+ .s-link[data-disabled='true'] {
112
+ @apply cursor-not-allowed opacity-50;
113
+ pointer-events: auto;
114
+ }
@@ -0,0 +1,85 @@
1
+ import type { Component } from 'vue'
2
+ import type {
3
+ HistoryState,
4
+ LocationQueryRaw,
5
+ LocationQueryValueRaw,
6
+ RouteParamsRawGeneric,
7
+ } from 'vue-router'
8
+
9
+ /** Допустимое scalar-значение params/query. / Supported scalar params/query value. */
10
+ export type LinkRouteScalar = LocationQueryValueRaw
11
+
12
+ /** Exact params/query container for the UI-kit destination boundary. */
13
+ export type LinkRouteDictionary = LocationQueryRaw
14
+
15
+ /** Exact named-route params accepted by Vue Router. */
16
+ export type LinkRouteParams = RouteParamsRawGeneric
17
+
18
+ /** Exact serializable History API state supported by the UI-kit destination boundary. */
19
+ export type LinkHistoryStateValue = string | number | boolean | null | undefined | LinkHistoryState | LinkHistoryStateValue[]
20
+ export interface LinkHistoryState extends HistoryState {
21
+ [key: number]: LinkHistoryStateValue
22
+ [key: string]: LinkHistoryStateValue
23
+ }
24
+
25
+ interface LinkRouterOptions {
26
+ readonly query?: LinkRouteDictionary
27
+ readonly hash?: string
28
+ readonly replace?: boolean
29
+ readonly force?: boolean
30
+ readonly state?: LinkHistoryState
31
+ }
32
+
33
+ /**
34
+ * Намеренно суженный router destination: UI-kit требует явный named/path target.
35
+ * Intentionally narrowed router destination: the UI kit requires an explicit named/path target.
36
+ */
37
+ export type LinkRouterLocation = LinkRouterOptions & (
38
+ | { readonly name: string | symbol; readonly path?: never; readonly params?: LinkRouteParams }
39
+ | { readonly path: string; readonly name?: never; readonly params?: never }
40
+ )
41
+
42
+ /** Router destination supported by the UI-kit boundary. */
43
+ export type LinkRouterTarget = string | LinkRouterLocation
44
+
45
+ export const LINK_BROWSING_TARGETS = ['_self', '_blank', '_parent', '_top'] as const
46
+
47
+ /** Допустимый browsing-context target ссылки. / Supported link browsing-context target. */
48
+ export type LinkBrowsingTarget = typeof LINK_BROWSING_TARGETS[number]
49
+
50
+ interface LinkDestinationOptions {
51
+ readonly target?: LinkBrowsingTarget
52
+ readonly rel?: string
53
+ }
54
+
55
+ /** Native href destination; router target is structurally forbidden. */
56
+ export type HrefLinkDestination = LinkDestinationOptions & {
57
+ readonly href: string
58
+ readonly to?: never
59
+ }
60
+
61
+ /** Explicit named/path router destination; native href is structurally forbidden. */
62
+ export type RouterLinkDestination = LinkDestinationOptions & {
63
+ readonly href?: never
64
+ readonly to: LinkRouterTarget
65
+ }
66
+
67
+ /** Строгий XOR-контракт обычного destination. / Strict XOR contract for an ordinary destination. */
68
+ export type LinkDestination = HrefLinkDestination | RouterLinkDestination
69
+
70
+ /** Canonical presentation/state API owned by SLink. */
71
+ export interface LinkPresentationProps {
72
+ readonly variant?: 'default' | 'subtle' | 'brand' | 'navigation' | 'bottom-navigation' | 'document'
73
+ readonly external?: boolean
74
+ readonly active?: boolean
75
+ readonly disabled?: boolean
76
+ readonly width?: 'fit' | 'full'
77
+ readonly size?: 'inherit' | 'sm' | 'md'
78
+ readonly underline?: 'hover' | 'always' | 'none'
79
+ readonly wrap?: 'normal' | 'anywhere'
80
+ readonly contentKind?: 'text' | 'identifier'
81
+ readonly leadingIcon?: Component
82
+ }
83
+
84
+ /** Полный discriminated public props contract SLink. / Complete discriminated public SLink props contract. */
85
+ export type SLinkProps = LinkDestination & LinkPresentationProps
@@ -0,0 +1,114 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ computed,
4
+ type VNodeChild,
5
+ } from 'vue'
6
+ import { RouterLink } from 'vue-router'
7
+ import { resolveLinkTarget } from '../../../internal/linkTarget'
8
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
9
+ import {
10
+ PassiveContentBoundary,
11
+ useInteractiveLeafRegistration,
12
+ type PassiveContentContract,
13
+ } from '../../../internal/passiveContentContract'
14
+ import type {
15
+ SLinkProps,
16
+ } from './SLink'
17
+
18
+ defineOptions({ inheritAttrs: false })
19
+
20
+ /** Exact href/to XOR plus the canonical link presentation API. */
21
+ export type Props = SLinkProps
22
+
23
+ const props = withDefaults(defineProps<Props>(), {
24
+ href: undefined,
25
+ to: undefined,
26
+ target: '_self',
27
+ rel: undefined,
28
+ variant: 'default',
29
+ external: false,
30
+ active: false,
31
+ disabled: false,
32
+ width: 'fit',
33
+ size: 'inherit',
34
+ underline: 'hover',
35
+ wrap: 'normal',
36
+ contentKind: 'text',
37
+ leadingIcon: undefined,
38
+ })
39
+ const emit = defineEmits<{
40
+ click: [event: MouseEvent]
41
+ }>()
42
+ const ownedAttrs = useOwnedAttrs({ component: 'SLink', owner: 'native navigation link' })
43
+ const slots = defineSlots<{
44
+ /** Пассивный текст/inline-контент ссылки. / Passive text or inline link content. */
45
+ default?: () => VNodeChild
46
+ /** Пассивная trailing presentation, например badge. / Passive trailing presentation such as a badge. */
47
+ trailing?: () => VNodeChild
48
+ }>()
49
+ useInteractiveLeafRegistration({ owner: 'SLink' })
50
+ const passiveContentContract: PassiveContentContract = {
51
+ owner: 'SLink',
52
+ region: 'passive link content',
53
+ guidance: 'Вынесите control за границы ссылки. / Move the control outside the link.',
54
+ }
55
+
56
+ const resolvedTarget = computed(() => resolveLinkTarget({
57
+ href: props.href,
58
+ to: props.to,
59
+ target: props.target,
60
+ rel: props.rel,
61
+ owner: 'SLink',
62
+ }))
63
+
64
+ const linkComponent = computed(() => {
65
+ if (props.disabled) return 'a'
66
+ return resolvedTarget.value.kind === 'router' ? RouterLink : 'a'
67
+ })
68
+
69
+ const linkBindings = () => ({
70
+ ...ownedAttrs.bindings(),
71
+ ...(props.disabled ? {} : resolvedTarget.value.bindings),
72
+ })
73
+
74
+ function handleClick(event: MouseEvent): void {
75
+ if (props.disabled) {
76
+ event.preventDefault()
77
+ event.stopImmediatePropagation()
78
+ return
79
+ }
80
+ emit('click', event)
81
+ }
82
+ </script>
83
+
84
+ <template>
85
+ <component
86
+ :is="linkComponent"
87
+ v-bind="linkBindings()"
88
+ class="s-link"
89
+ :data-variant="variant"
90
+ :data-external="external || undefined"
91
+ :data-active="active || undefined"
92
+ :data-disabled="disabled || undefined"
93
+ :data-width="width"
94
+ :data-size="size"
95
+ :data-underline="underline"
96
+ :data-wrap="wrap"
97
+ :data-content-kind="contentKind"
98
+ :aria-current="active ? 'page' : undefined"
99
+ :aria-disabled="disabled || undefined"
100
+ :tabindex="disabled ? -1 : undefined"
101
+ :role="disabled ? 'link' : undefined"
102
+ @click="handleClick"
103
+ >
104
+ <span v-if="leadingIcon" class="s-link__icon" aria-hidden="true">
105
+ <component :is="leadingIcon" class="s-link__icon-graphic" />
106
+ </span>
107
+ <PassiveContentBoundary :contract="passiveContentContract">
108
+ <span class="s-link__content"><slot /></span>
109
+ <span v-if="slots.trailing" class="s-link__trailing"><slot name="trailing" /></span>
110
+ </PassiveContentBoundary>
111
+ </component>
112
+ </template>
113
+
114
+ <style lang="postcss" src="./SLink.css" scoped></style>
@@ -0,0 +1,31 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-listbox {
4
+ @apply m-0 list-none;
5
+ }
6
+
7
+ .s-listbox[aria-disabled='true'] {
8
+ @apply opacity-60;
9
+ }
10
+
11
+ .s-listbox[data-invalid='true'] {
12
+ outline: var(--s-focus-ring-width) solid var(--s-selection-invalid-outline-color);
13
+ outline-offset: var(--s-focus-ring-inset-offset);
14
+ }
15
+
16
+ .s-listbox__empty {
17
+ @apply px-3 py-2 text-sm text-surface-500 dark:text-surface-400;
18
+ }
19
+
20
+ .s-listbox[data-variant='select'] {
21
+ @apply mt-1 max-h-60 w-full overflow-auto rounded-md py-1 text-base;
22
+ background: var(--s-floating-surface);
23
+ color: var(--s-floating-foreground);
24
+ box-shadow: 0 0 0 1px var(--s-floating-outline), var(--s-elevation-floating);
25
+ }
26
+
27
+ .s-listbox[data-variant='color'] {
28
+ @apply grid grid-cols-5 gap-2 rounded-2xl border border-surface-200 bg-surface-0 p-2;
29
+ @apply dark:border-surface-700 dark:bg-surface-900;
30
+ box-shadow: var(--s-elevation-overlay);
31
+ }