@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,203 @@
1
+ import { inject, onBeforeUnmount, onMounted } from 'vue'
2
+ import type { ComputedRef, InjectionKey, Ref } from 'vue'
3
+ import type {
4
+ STreeNodeContentScope,
5
+ TreeActionsLayout,
6
+ TreeActivationOrigin,
7
+ TreeDensity,
8
+ TreeExpandControl,
9
+ SelectionKey,
10
+ TreeModel,
11
+ TreeRowVariant,
12
+ TreeSelection,
13
+ TreeSelectionRail,
14
+ } from '../complex/types'
15
+
16
+ export const TREE_RUNTIME_KEY = 's-tree-runtime'
17
+ export const TREE_ACTION_CONTEXT_KEY: InjectionKey<TreeActionContext> = Symbol('s-tree-action-context')
18
+
19
+ export interface RegisteredTreeItem<T extends object, K extends SelectionKey> {
20
+ key: K
21
+ parentKey?: K
22
+ node: T
23
+ element: HTMLElement
24
+ activate(origin: TreeActivationOrigin): void
25
+ select(): void
26
+ toggle(): void
27
+ expanded(): boolean
28
+ hasChildren(): boolean
29
+ disabled(): boolean
30
+ }
31
+
32
+ export type TreeActionKey = symbol
33
+
34
+ export interface RegisteredTreeAction {
35
+ readonly key: TreeActionKey
36
+ readonly element: HTMLElement
37
+ focus(options?: FocusOptions): void
38
+ disabled(): boolean
39
+ }
40
+
41
+ export interface TreeActionContext {
42
+ register(action: RegisteredTreeAction): void
43
+ unregister(action: RegisteredTreeAction): void
44
+ }
45
+
46
+ function compareRegisteredElements(
47
+ owner: string,
48
+ left: { readonly element: HTMLElement },
49
+ right: { readonly element: HTMLElement },
50
+ ): number {
51
+ if (left.element === right.element) return 0
52
+ const position = left.element.compareDocumentPosition(right.element)
53
+ const nodeConstructor = left.element.ownerDocument.defaultView?.Node
54
+ if (!nodeConstructor) {
55
+ throw new Error(`${owner}: registered elements require an active DOM document`)
56
+ }
57
+ if ((position & nodeConstructor.DOCUMENT_POSITION_DISCONNECTED) !== 0) {
58
+ throw new Error(`${owner}: registered elements must belong to the same DOM tree`)
59
+ }
60
+ if ((position & nodeConstructor.DOCUMENT_POSITION_FOLLOWING) !== 0) return -1
61
+ if ((position & nodeConstructor.DOCUMENT_POSITION_PRECEDING) !== 0) return 1
62
+ throw new Error(`${owner}: registered elements have no deterministic DOM order`)
63
+ }
64
+
65
+ function orderedRegistrations<R extends { readonly element: HTMLElement }>(
66
+ owner: string,
67
+ root: HTMLElement | null,
68
+ registrations: Iterable<R>,
69
+ ): R[] {
70
+ if (!root) return []
71
+ return [...registrations]
72
+ .filter((registration) => registration.element.isConnected && root.contains(registration.element))
73
+ .sort((left, right) => compareRegisteredElements(owner, left, right))
74
+ }
75
+
76
+ export class TreeItemRegistrationStore<T extends object, K extends SelectionKey> {
77
+ readonly #items = new Map<K, RegisteredTreeItem<T, K>>()
78
+ readonly #root: () => HTMLElement | null
79
+
80
+ constructor(root: () => HTMLElement | null) {
81
+ this.#root = root
82
+ }
83
+
84
+ register(item: RegisteredTreeItem<T, K>): void {
85
+ const current = this.#items.get(item.key)
86
+ if (current && current.element !== item.element) {
87
+ throw new Error(`STree: runtime registration collision for ${typeof item.key}:${String(item.key)}`)
88
+ }
89
+ this.#items.set(item.key, item)
90
+ }
91
+
92
+ unregister(key: K): void {
93
+ this.#items.delete(key)
94
+ }
95
+
96
+ get(key: K): RegisteredTreeItem<T, K> | undefined {
97
+ return this.#items.get(key)
98
+ }
99
+
100
+ ordered(): RegisteredTreeItem<T, K>[] {
101
+ return orderedRegistrations('STree item registry', this.#root(), this.#items.values())
102
+ }
103
+ }
104
+
105
+ export class TreeActionRegistrationStore implements TreeActionContext {
106
+ readonly #actions = new Map<TreeActionKey, RegisteredTreeAction>()
107
+ readonly #root: () => HTMLElement | null
108
+
109
+ constructor(root: () => HTMLElement | null) {
110
+ this.#root = root
111
+ }
112
+
113
+ register(action: RegisteredTreeAction): void {
114
+ const current = this.#actions.get(action.key)
115
+ if (current && current !== action) {
116
+ throw new Error(`STree: action registration collision for ${String(action.key)}`)
117
+ }
118
+ const elementOwner = [...this.#actions.values()].find((candidate) =>
119
+ candidate.key !== action.key && candidate.element === action.element,
120
+ )
121
+ if (elementOwner) {
122
+ throw new Error(
123
+ `STree: action element is already registered by ${String(elementOwner.key)}`,
124
+ )
125
+ }
126
+ this.#actions.set(action.key, action)
127
+ }
128
+
129
+ unregister(action: RegisteredTreeAction): void {
130
+ const current = this.#actions.get(action.key)
131
+ if (!current) return
132
+ if (current !== action) {
133
+ throw new Error(`STree: stale action unregistration for ${String(action.key)}`)
134
+ }
135
+ this.#actions.delete(action.key)
136
+ }
137
+
138
+ orderedEnabled(): RegisteredTreeAction[] {
139
+ return orderedRegistrations(
140
+ 'STree action registry',
141
+ this.#root(),
142
+ this.#actions.values(),
143
+ ).filter((action) => !action.disabled())
144
+ }
145
+
146
+ actionForTarget(target: EventTarget | null): RegisteredTreeAction | undefined {
147
+ if (!(target instanceof Node)) return undefined
148
+ return this.orderedEnabled().find((action) =>
149
+ action.element === target || action.element.contains(target),
150
+ )
151
+ }
152
+ }
153
+
154
+ export function useTreeActionRegistration(options: Readonly<{
155
+ owner: string
156
+ element: Readonly<Ref<HTMLElement | null>>
157
+ disabled: () => boolean
158
+ }>): boolean {
159
+ const context = inject(TREE_ACTION_CONTEXT_KEY, null)
160
+ if (!context) return false
161
+
162
+ const key = Symbol(options.owner)
163
+ let registration: RegisteredTreeAction | undefined
164
+ onMounted(() => {
165
+ const element = options.element.value
166
+ if (!element) {
167
+ throw new Error(`STree: ${options.owner} action registration requires a mounted element`)
168
+ }
169
+ registration = {
170
+ key,
171
+ element,
172
+ focus: (focusOptions) => element.focus(focusOptions),
173
+ disabled: options.disabled,
174
+ }
175
+ context.register(registration)
176
+ })
177
+ onBeforeUnmount(() => {
178
+ if (registration) context.unregister(registration)
179
+ })
180
+ return true
181
+ }
182
+
183
+ export interface TreeRuntime<T extends object, K extends SelectionKey> {
184
+ model: TreeModel<T, K>
185
+ selection: ComputedRef<TreeSelection<K>>
186
+ expanded: ComputedRef<ReadonlySet<K>>
187
+ density: ComputedRef<TreeDensity>
188
+ actionsLayout: ComputedRef<TreeActionsLayout>
189
+ expandControl: ComputedRef<TreeExpandControl>
190
+ selectionRail: ComputedRef<TreeSelectionRail>
191
+ rowVariant: ComputedRef<TreeRowVariant>
192
+ activeKey: Ref<K | undefined>
193
+ activateNode(node: T, origin: TreeActivationOrigin): void
194
+ selectNode(node: T): void
195
+ toggleNode(node: T): void
196
+ checkNode(node: T): void
197
+ multipleKeys(node: T): readonly K[]
198
+ register(item: RegisteredTreeItem<T, K>): void
199
+ unregister(key: K): void
200
+ handleItemKeydown(item: RegisteredTreeItem<T, K>, event: KeyboardEvent): void
201
+ }
202
+
203
+ export type TreeNodeScope<T extends object, K extends SelectionKey> = STreeNodeContentScope<T, K>
@@ -0,0 +1,162 @@
1
+ import { computed } from 'vue'
2
+ import type { ComputedRef, Ref } from 'vue'
3
+
4
+ /** Стабильный ключ, принимаемый владельцами выбора. / Stable key accepted by selection owners. */
5
+ export type SelectionKey = string | number
6
+
7
+ /** Ключ и точная координата его объявления для диагностики. / A key and its exact declaration coordinate for diagnostics. */
8
+ export interface SelectionIdentity<T extends SelectionKey = SelectionKey> {
9
+ key: T
10
+ coordinate: string
11
+ }
12
+
13
+ /** Опция общего roving-focus с состоянием доступности. / A shared roving-focus option with availability state. */
14
+ export interface SelectionRovingOption<T extends SelectionKey> {
15
+ value: T
16
+ disabled?: boolean
17
+ }
18
+
19
+ /** Публичный keyboard/focus-контракт владельца выбора. / Public keyboard/focus contract for a selection owner. */
20
+ export interface SelectionRovingApi<T extends SelectionKey> {
21
+ focusableValue: ComputedRef<T | undefined>
22
+ register: (value: T, element: Element | null) => void
23
+ handleKeydown: (event: KeyboardEvent, value: T) => void
24
+ }
25
+
26
+ function invalidKeyMessage(component: string, coordinate: string): string {
27
+ return `${component}: ${coordinate} must be a non-empty string or a finite number`
28
+ }
29
+
30
+ /** Проверяет runtime-контракт identity в точной координате вызова. / Asserts the runtime identity contract at the exact caller coordinate. */
31
+ export function assertSelectionKey(
32
+ component: string,
33
+ key: unknown,
34
+ coordinate: string,
35
+ ): asserts key is SelectionKey {
36
+ if (typeof key === 'string') {
37
+ if (key.trim().length === 0) throw new TypeError(invalidKeyMessage(component, coordinate))
38
+ return
39
+ }
40
+ if (typeof key === 'number' && Number.isFinite(key)) return
41
+ throw new TypeError(invalidKeyMessage(component, coordinate))
42
+ }
43
+
44
+ /** Возвращает type-aware token для DOM id, Vue key и lookup map. / Returns a type-aware token for DOM ids, Vue keys, and lookup maps. */
45
+ export function selectionKeyToken(
46
+ component: string,
47
+ key: SelectionKey,
48
+ coordinate: string,
49
+ ): string {
50
+ assertSelectionKey(component, key, coordinate)
51
+ return typeof key === 'string'
52
+ ? `string:${encodeURIComponent(key)}`
53
+ : `number:${Object.is(key, -0) ? '0' : String(key)}`
54
+ }
55
+
56
+ /** Проверяет полный inventory identity и возвращает канонический индекс. / Validates a complete identity inventory and returns its canonical index. */
57
+ export function validateSelectionIdentities<T extends SelectionKey>(
58
+ component: string,
59
+ identities: readonly SelectionIdentity<T>[],
60
+ ): ReadonlyMap<string, T> {
61
+ const keysByToken = new Map<string, T>()
62
+ const coordinatesByToken = new Map<string, string>()
63
+
64
+ for (const identity of identities) {
65
+ const token = selectionKeyToken(component, identity.key, identity.coordinate)
66
+ const previousCoordinate = coordinatesByToken.get(token)
67
+ if (previousCoordinate !== undefined) {
68
+ throw new Error(
69
+ `${component}: duplicate selection key at ${identity.coordinate}; first declared at ${previousCoordinate}`,
70
+ )
71
+ }
72
+ coordinatesByToken.set(token, identity.coordinate)
73
+ keysByToken.set(token, identity.key)
74
+ }
75
+
76
+ return keysByToken
77
+ }
78
+
79
+ function validateRovingOptions<T extends SelectionKey>(
80
+ component: string,
81
+ options: readonly SelectionRovingOption<T>[],
82
+ ): ReadonlyMap<string, T> {
83
+ return validateSelectionIdentities(component, options.map((option, index) => ({
84
+ key: option.value,
85
+ coordinate: `options[${index}].value`,
86
+ })))
87
+ }
88
+
89
+ function validateSelectedValue<T extends SelectionKey>(
90
+ component: string,
91
+ selectedValue: T | null | undefined,
92
+ optionsByToken: ReadonlyMap<string, T>,
93
+ ): void {
94
+ if (selectedValue === null || selectedValue === undefined) return
95
+ const token = selectionKeyToken(component, selectedValue, 'selectedValue')
96
+ if (!optionsByToken.has(token)) {
97
+ throw new Error(`${component}: selectedValue does not match any option`)
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Управляет общим roving-focus без потери доменного типа ключа.
103
+ * Manages shared roving focus without losing the domain key type.
104
+ */
105
+ export function useSelectionRoving<T extends SelectionKey>(config: {
106
+ options: ComputedRef<readonly SelectionRovingOption<T>[]>
107
+ selectedValue: Ref<T | null | undefined>
108
+ orientation?: 'horizontal' | 'vertical' | 'both'
109
+ select: (value: T) => void
110
+ component?: string
111
+ }): SelectionRovingApi<T> {
112
+ const elements = new Map<T, HTMLElement>()
113
+ const orientation = config.orientation ?? 'both'
114
+ const component = config.component ?? 'useSelectionRoving'
115
+
116
+ const validatedOptions = computed(() => {
117
+ const options = config.options.value
118
+ const optionsByToken = validateRovingOptions(component, options)
119
+ validateSelectedValue(component, config.selectedValue.value, optionsByToken)
120
+ return options
121
+ })
122
+ const enabledOptions = computed(() => validatedOptions.value.filter((option) => !option.disabled))
123
+ const focusableValue = computed(() => {
124
+ const selectedValue = config.selectedValue.value
125
+ return enabledOptions.value.find((option) => option.value === selectedValue)?.value
126
+ ?? enabledOptions.value[0]?.value
127
+ })
128
+
129
+ const register = (value: T, element: Element | null): void => {
130
+ selectionKeyToken(component, value, 'registered value')
131
+ if (element instanceof HTMLElement) elements.set(value, element)
132
+ else elements.delete(value)
133
+ }
134
+
135
+ const handleKeydown = (event: KeyboardEvent, value: T): void => {
136
+ const direction = (
137
+ orientation !== 'vertical' && event.key === 'ArrowLeft' ? -1
138
+ : orientation !== 'vertical' && event.key === 'ArrowRight' ? 1
139
+ : orientation !== 'horizontal' && event.key === 'ArrowUp' ? -1
140
+ : orientation !== 'horizontal' && event.key === 'ArrowDown' ? 1
141
+ : null
142
+ )
143
+ const enabled = enabledOptions.value
144
+ if (enabled.length === 0) return
145
+
146
+ const currentIndex = enabled.findIndex((option) => option.value === value)
147
+ const target = event.key === 'Home'
148
+ ? enabled[0]
149
+ : event.key === 'End'
150
+ ? enabled[enabled.length - 1]
151
+ : direction === null
152
+ ? undefined
153
+ : enabled[(Math.max(currentIndex, 0) + direction + enabled.length) % enabled.length]
154
+ if (!target) return
155
+
156
+ event.preventDefault()
157
+ config.select(target.value)
158
+ elements.get(target.value)?.focus()
159
+ }
160
+
161
+ return { focusableValue, register, handleKeydown }
162
+ }
@@ -0,0 +1,168 @@
1
+ import { StreamLanguage } from '@codemirror/language'
2
+ import type { Extension } from '@codemirror/state'
3
+ import { langNames, loadLanguage } from '@uiw/codemirror-extensions-langs'
4
+ import { makefile } from 'codemirror-lang-makefile'
5
+ import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile'
6
+ import { nginx } from '@codemirror/legacy-modes/mode/nginx'
7
+ import { shell } from '@codemirror/legacy-modes/mode/shell'
8
+ import {
9
+ type CodeEditorLanguageContract,
10
+ validateCodeEditorLanguage,
11
+ } from '../../internal/codeEditorContract'
12
+
13
+ type NullableText = string | null | undefined
14
+
15
+ const SPECIAL_FILENAMES: Record<string, string> = {
16
+ 'cmakelists.txt': 'cmake',
17
+ 'dockerfile': 'dockerfile',
18
+ 'docker-compose.yml': 'yaml',
19
+ 'docker-compose.yaml': 'yaml',
20
+ 'compose.yml': 'yaml',
21
+ 'compose.yaml': 'yaml',
22
+ '.env': 'properties',
23
+ '.env.example': 'properties',
24
+ '.env.local': 'properties',
25
+ '.env.development': 'properties',
26
+ '.env.production': 'properties',
27
+ 'makefile': 'makefile',
28
+ 'nginx.conf': 'nginx',
29
+ }
30
+
31
+ const LANGUAGE_ALIASES: Record<string, string> = {
32
+ bash: 'shell',
33
+ 'c#': 'cs',
34
+ csharp: 'cs',
35
+ env: 'properties',
36
+ fish: 'shell',
37
+ handlebars: 'handlebars',
38
+ htm: 'html',
39
+ js: 'javascript',
40
+ jsonc: 'json',
41
+ json5: 'json',
42
+ log: 'text',
43
+ make: 'makefile',
44
+ md: 'markdown',
45
+ mk: 'makefile',
46
+ plaintext: 'text',
47
+ proto3: 'proto',
48
+ ps1: 'powershell',
49
+ psm1: 'powershell',
50
+ psd1: 'powershell',
51
+ py: 'python',
52
+ pyw: 'python',
53
+ pyi: 'python',
54
+ rbw: 'ruby',
55
+ sh: 'shell',
56
+ shellsession: 'shell',
57
+ ts: 'typescript',
58
+ text: 'text',
59
+ txt: 'text',
60
+ yaml: 'yaml',
61
+ yml: 'yaml',
62
+ zsh: 'shell',
63
+ }
64
+
65
+ const LEGACY_CODEMIRROR_LANGUAGES: Record<string, Extension> = {
66
+ dockerfile: StreamLanguage.define(dockerFile),
67
+ makefile: makefile(),
68
+ nginx: StreamLanguage.define(nginx),
69
+ shell: StreamLanguage.define(shell),
70
+ }
71
+
72
+ const CODEMIRROR_LANGUAGE_ALIASES: Record<string, string> = {
73
+ javascript: 'js',
74
+ powershell: 'ps1',
75
+ python: 'py',
76
+ ruby: 'rb',
77
+ rust: 'rs',
78
+ shell: 'bash',
79
+ typescript: 'ts',
80
+ }
81
+
82
+ const HIGHLIGHT_LANGUAGE_ALIASES: Record<string, string> = {
83
+ shell: 'bash',
84
+ text: 'plaintext',
85
+ }
86
+
87
+ type SupportedCodeMirrorLanguage = Parameters<typeof loadLanguage>[0]
88
+
89
+ const SUPPORTED_CODEMIRROR_LANGUAGES = new Set<SupportedCodeMirrorLanguage>(langNames)
90
+
91
+ function normalizeDetectedToken(value: NullableText): string | null {
92
+ if (!value) {
93
+ return null
94
+ }
95
+
96
+ const normalized = value.trim().toLowerCase().replace(/^\./, '')
97
+ if (!normalized) {
98
+ return null
99
+ }
100
+
101
+ return LANGUAGE_ALIASES[normalized] ?? normalized
102
+ }
103
+
104
+ function resolveLanguageFromFilePath(filePath: NullableText): string | null {
105
+ if (!filePath) {
106
+ return null
107
+ }
108
+
109
+ const normalizedPath = filePath.trim().toLowerCase()
110
+ if (!normalizedPath) {
111
+ return null
112
+ }
113
+
114
+ const basename = normalizedPath.split('/').pop() ?? normalizedPath
115
+ if (SPECIAL_FILENAMES[basename]) {
116
+ return SPECIAL_FILENAMES[basename]
117
+ }
118
+
119
+ if (basename.startsWith('dockerfile.')) {
120
+ return 'dockerfile'
121
+ }
122
+
123
+ if (!basename.includes('.')) {
124
+ return null
125
+ }
126
+
127
+ const extension = basename.split('.').pop()
128
+ return normalizeDetectedToken(extension)
129
+ }
130
+
131
+ function isSupportedCodeMirrorLanguage(value: string): value is SupportedCodeMirrorLanguage {
132
+ return SUPPORTED_CODEMIRROR_LANGUAGES.has(value as SupportedCodeMirrorLanguage)
133
+ }
134
+
135
+ export function resolveCodeLanguageKey(input: CodeEditorLanguageContract): string {
136
+ const language = validateCodeEditorLanguage('codeLanguages', input)
137
+ if (language.mode === 'explicit') {
138
+ return LANGUAGE_ALIASES[language.id] ?? language.id
139
+ }
140
+ const detected = resolveLanguageFromFilePath(language.filePath)
141
+ if (!detected) {
142
+ throw new RangeError(
143
+ `codeLanguages: невозможно определить язык по filePath ${JSON.stringify(language.filePath)}. `
144
+ + `/ codeLanguages: cannot detect a language from filePath ${JSON.stringify(language.filePath)}.`,
145
+ )
146
+ }
147
+ return detected
148
+ }
149
+
150
+ export function getCodeMirrorLanguageExtension(input: CodeEditorLanguageContract): Extension | null {
151
+ const languageKey = resolveCodeLanguageKey(input)
152
+
153
+ const codeMirrorLanguageKey = CODEMIRROR_LANGUAGE_ALIASES[languageKey] ?? languageKey
154
+
155
+ if (isSupportedCodeMirrorLanguage(codeMirrorLanguageKey)) {
156
+ const loadedLanguage = loadLanguage(codeMirrorLanguageKey)
157
+ if (loadedLanguage) {
158
+ return loadedLanguage
159
+ }
160
+ }
161
+
162
+ return LEGACY_CODEMIRROR_LANGUAGES[codeMirrorLanguageKey] ?? LEGACY_CODEMIRROR_LANGUAGES[languageKey] ?? null
163
+ }
164
+
165
+ export function resolveHighlightLanguageKey(input: CodeEditorLanguageContract): string {
166
+ const languageKey = resolveCodeLanguageKey(input)
167
+ return HIGHLIGHT_LANGUAGE_ALIASES[languageKey] ?? languageKey
168
+ }
@@ -0,0 +1,8 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-kanban-card__content { @apply flex w-full min-w-0 flex-col; }
4
+ .s-kanban-card__copy { @apply min-w-0; }
5
+ .s-kanban-card__title { @apply block truncate text-sm font-medium text-surface-800 dark:text-surface-100; }
6
+ .s-kanban-card__subtitle { @apply mt-0.5 block truncate text-xs text-surface-500 dark:text-surface-400; }
7
+ .s-kanban-card__body { @apply mt-2 min-w-0; }
8
+ .s-kanban-card__meta { @apply mt-2 flex min-w-0 flex-wrap items-center gap-2 text-xs text-surface-500 dark:text-surface-400; }
@@ -0,0 +1,76 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type {
4
+ InteractiveSurfaceContract,
5
+ InteractiveSurfaceDensity,
6
+ InteractiveSurfacePadding,
7
+ InteractiveSurfaceRadius,
8
+ InteractiveSurfaceTone,
9
+ } from '../controls/SInteractiveSurface.vue'
10
+ import SActionCard from '../data-display/SActionCard.vue'
11
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
12
+
13
+ defineOptions({ inheritAttrs: false })
14
+
15
+ export interface Props {
16
+ title: string
17
+ subtitle?: string
18
+ surface?: InteractiveSurfaceContract
19
+ selected?: boolean
20
+ tone?: InteractiveSurfaceTone
21
+ density?: InteractiveSurfaceDensity
22
+ padding?: InteractiveSurfacePadding
23
+ radius?: InteractiveSurfaceRadius
24
+ fill?: boolean
25
+ }
26
+
27
+ const props = withDefaults(defineProps<Props>(), {
28
+ subtitle: undefined,
29
+ surface: undefined,
30
+ selected: false,
31
+ tone: 'default',
32
+ density: 'comfortable',
33
+ padding: 'sm',
34
+ radius: 'md',
35
+ fill: false,
36
+ })
37
+
38
+ const emit = defineEmits<{
39
+ activate: [event: MouseEvent]
40
+ }>()
41
+
42
+ const resolvedSurface = computed<InteractiveSurfaceContract>(() => props.surface ?? { mode: 'static' })
43
+ const ownedAttrs = useOwnedAttrs({ component: 'SKanbanCard', owner: 'canonical action-card surface' })
44
+
45
+ function activate(event: MouseEvent): void {
46
+ emit('activate', event)
47
+ }
48
+ </script>
49
+
50
+ <template>
51
+ <SActionCard
52
+ data-testid="s-kanban-card"
53
+ v-bind="ownedAttrs.bindings()"
54
+ :surface="resolvedSurface"
55
+ :selected="selected"
56
+ :tone="tone"
57
+ :density="density"
58
+ :padding="padding"
59
+ :radius="radius"
60
+ :fill="fill ? 'container' : 'content'"
61
+ @activate="activate"
62
+ >
63
+ <div class="s-kanban-card__content">
64
+ <span class="s-kanban-card__copy">
65
+ <span class="s-kanban-card__title">{{ title }}</span>
66
+ <span v-if="subtitle" class="s-kanban-card__subtitle">{{ subtitle }}</span>
67
+ </span>
68
+
69
+ <span v-if="$slots.default" class="s-kanban-card__body"><slot /></span>
70
+ <span v-if="$slots.meta" class="s-kanban-card__meta"><slot name="meta" /></span>
71
+ </div>
72
+ <template v-if="$slots.actions" #actions><slot name="actions" /></template>
73
+ </SActionCard>
74
+ </template>
75
+
76
+ <style lang="postcss" src="./SKanbanCard.css" scoped></style>