@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,110 @@
1
+ import {
2
+ validateBoolean,
3
+ validateBoundedInteger,
4
+ validateExactString,
5
+ validateNonEmptyString,
6
+ } from './runtimeContract'
7
+
8
+ export const CODE_EDITOR_VIEWPORTS = ['content', 'compact', 'standard', 'expanded', 'fill'] as const
9
+ export const CODE_EDITOR_VARIANTS = ['code', 'sql'] as const
10
+ export const CODE_EDITOR_KEY_BEHAVIORS = ['indent', 'focus-next'] as const
11
+ export const CODE_EDITOR_CURSOR_ACTIVITY_MODES = ['editable', 'always'] as const
12
+
13
+ /**
14
+ * Язык задаётся явно либо определяется только из обязательного пути файла.
15
+ * Language is either explicit or detected exclusively from a required file path.
16
+ */
17
+ export type CodeEditorLanguageContract =
18
+ | { mode: 'explicit'; id: string; filePath?: never }
19
+ | { mode: 'detect'; filePath: string; id?: never }
20
+
21
+ /** Явные CodeMirror-возможности без вывода опций из presentation. / Explicit CodeMirror capabilities; no option is inferred from presentation. */
22
+ export interface CodeEditorCapabilitiesContract {
23
+ lineNumbers: boolean
24
+ indentation: {
25
+ tabSize: number
26
+ keyBehavior: typeof CODE_EDITOR_KEY_BEHAVIORS[number]
27
+ }
28
+ keyInterceptor?: (event: KeyboardEvent) => boolean
29
+ cursorActivity: typeof CODE_EDITOR_CURSOR_ACTIVITY_MODES[number]
30
+ }
31
+
32
+ function isCodeEditorKeyInterceptor(value: unknown): value is (event: KeyboardEvent) => boolean {
33
+ return typeof value === 'function'
34
+ }
35
+
36
+ function assertPlainExactRecord(
37
+ owner: string,
38
+ coordinate: string,
39
+ value: unknown,
40
+ allowed: readonly string[],
41
+ ): Record<string, unknown> {
42
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
43
+ throw new TypeError(`${owner}: ${coordinate} должен быть plain object. / ${owner}: ${coordinate} must be a plain object.`)
44
+ }
45
+ const record = value as Record<string, unknown>
46
+ const unexpected = Object.keys(record).filter((key) => !allowed.includes(key))
47
+ if (unexpected.length > 0) {
48
+ throw new TypeError(`${owner}.${coordinate}: неизвестные поля ${unexpected.map((key) => JSON.stringify(key)).join(', ')}. / ${owner}.${coordinate}: unknown fields ${unexpected.map((key) => JSON.stringify(key)).join(', ')}.`)
49
+ }
50
+ return record
51
+ }
52
+
53
+ export function validateCodeEditorLanguage(
54
+ owner: string,
55
+ value: unknown,
56
+ ): CodeEditorLanguageContract {
57
+ const record = assertPlainExactRecord(owner, 'language', value, ['mode', 'id', 'filePath'])
58
+ const mode = validateExactString(owner, 'language.mode', record.mode, ['explicit', 'detect'] as const)
59
+ const allowedFields = mode === 'explicit' ? ['mode', 'id'] : ['mode', 'filePath']
60
+ const unexpected = Object.keys(record).filter((key) => !allowedFields.includes(key))
61
+ if (unexpected.length > 0) {
62
+ throw new TypeError(
63
+ `${owner}.language: неизвестные поля ${unexpected.map((key) => JSON.stringify(key)).join(', ')}. `
64
+ + `/ ${owner}.language: unknown fields ${unexpected.map((key) => JSON.stringify(key)).join(', ')}.`,
65
+ )
66
+ }
67
+ if (mode === 'explicit') {
68
+ return { mode, id: validateNonEmptyString(owner, 'language.id', record.id) }
69
+ }
70
+ return { mode, filePath: validateNonEmptyString(owner, 'language.filePath', record.filePath) }
71
+ }
72
+
73
+ export function validateCodeEditorCapabilities(
74
+ owner: string,
75
+ value: unknown,
76
+ ): CodeEditorCapabilitiesContract {
77
+ const record = assertPlainExactRecord(
78
+ owner,
79
+ 'capabilities',
80
+ value,
81
+ ['lineNumbers', 'indentation', 'keyInterceptor', 'cursorActivity'],
82
+ )
83
+ const indentation = assertPlainExactRecord(
84
+ owner,
85
+ 'capabilities.indentation',
86
+ record.indentation,
87
+ ['tabSize', 'keyBehavior'],
88
+ )
89
+ const keyInterceptor = record.keyInterceptor
90
+ if (keyInterceptor !== undefined && !isCodeEditorKeyInterceptor(keyInterceptor)) {
91
+ throw new TypeError(`${owner}: capabilities.keyInterceptor должен быть функцией. / ${owner}: capabilities.keyInterceptor must be a function.`)
92
+ }
93
+ return {
94
+ lineNumbers: validateBoolean(owner, 'capabilities.lineNumbers', record.lineNumbers),
95
+ indentation: {
96
+ tabSize: validateBoundedInteger(owner, 'capabilities.indentation.tabSize', indentation.tabSize, { minimum: 1, maximum: 16 }),
97
+ keyBehavior: validateExactString(owner, 'capabilities.indentation.keyBehavior', indentation.keyBehavior, CODE_EDITOR_KEY_BEHAVIORS),
98
+ },
99
+ ...(keyInterceptor === undefined ? {} : { keyInterceptor }),
100
+ cursorActivity: validateExactString(owner, 'capabilities.cursorActivity', record.cursorActivity, CODE_EDITOR_CURSOR_ACTIVITY_MODES),
101
+ }
102
+ }
103
+
104
+ export function validateCodeEditorViewport(owner: string, value: unknown): typeof CODE_EDITOR_VIEWPORTS[number] {
105
+ return validateExactString(owner, 'viewport', value, CODE_EDITOR_VIEWPORTS)
106
+ }
107
+
108
+ export function validateCodeEditorVariant(owner: string, value: unknown): typeof CODE_EDITOR_VARIANTS[number] {
109
+ return validateExactString(owner, 'variant', value, CODE_EDITOR_VARIANTS)
110
+ }
@@ -0,0 +1,27 @@
1
+ import type { ComputedRef, InjectionKey } from 'vue'
2
+ import { inject, provide } from 'vue'
3
+ import type { Extension } from '@codemirror/state'
4
+
5
+ export type TrustedCodeEditorExtensionOwner = 'SSqlEditor'
6
+
7
+ interface TrustedCodeEditorExtensionChannel {
8
+ owner: TrustedCodeEditorExtensionOwner
9
+ extensions: ComputedRef<readonly Extension[]>
10
+ }
11
+
12
+ const TRUSTED_CODE_EDITOR_EXTENSION_KEY: InjectionKey<TrustedCodeEditorExtensionChannel> = Symbol(
13
+ 'sputnig-ui-kit-trusted-code-editor-extensions',
14
+ )
15
+
16
+ /** Internal-only CodeMirror extension channel for audited UI-kit adapters. */
17
+ export function provideTrustedCodeEditorExtensions(
18
+ owner: TrustedCodeEditorExtensionOwner,
19
+ extensions: ComputedRef<readonly Extension[]>,
20
+ ): void {
21
+ provide(TRUSTED_CODE_EDITOR_EXTENSION_KEY, { owner, extensions })
22
+ }
23
+
24
+ /** Consumes extensions supplied by the nearest audited UI-kit adapter. */
25
+ export function useTrustedCodeEditorExtensions(): TrustedCodeEditorExtensionChannel | null {
26
+ return inject(TRUSTED_CODE_EDITOR_EXTENSION_KEY, null)
27
+ }
@@ -0,0 +1,136 @@
1
+ import type { Component } from 'vue'
2
+
3
+ import { AlertTriangle, CheckCircle2, Info, LoaderCircle, XCircle } from '../components/icons/sputnigUiIcons'
4
+
5
+ export type FeedbackTone = 'neutral' | 'info' | 'success' | 'warn' | 'danger'
6
+ export type FeedbackSeverity = Exclude<FeedbackTone, 'neutral'>
7
+ export type FeedbackLiveMode = 'off' | 'polite' | 'assertive' | 'auto'
8
+ export type AsyncFeedbackStatus = 'loading' | 'empty' | 'error'
9
+ export type FeedbackIndicatorState = FeedbackTone | 'progress'
10
+
11
+ export interface FeedbackPresentation {
12
+ icon: Component
13
+ iconName: 'info' | 'check-circle' | 'alert-triangle' | 'x-circle'
14
+ fallbackTitle: string
15
+ }
16
+
17
+ export interface FeedbackLiveAttributes {
18
+ role?: 'status' | 'alert'
19
+ ariaLive?: 'polite' | 'assertive'
20
+ ariaAtomic?: 'true'
21
+ }
22
+
23
+ export interface AsyncFeedbackPresentation {
24
+ readonly tone: FeedbackTone
25
+ readonly icon: Component | null
26
+ readonly iconName: FeedbackPresentation['iconName'] | 'loader-circle' | null
27
+ readonly fallbackMessage: string
28
+ readonly busy: boolean
29
+ readonly live: Readonly<FeedbackLiveAttributes>
30
+ }
31
+
32
+ export interface FeedbackIndicatorPresentation {
33
+ readonly tone: FeedbackTone
34
+ readonly icon: Component
35
+ readonly iconName: FeedbackPresentation['iconName'] | 'loader-circle'
36
+ readonly fallbackTitle: string
37
+ readonly motion: 'static' | 'spin'
38
+ }
39
+
40
+ const presentations: Readonly<Record<FeedbackTone, Readonly<FeedbackPresentation>>> = {
41
+ neutral: { icon: Info, iconName: 'info', fallbackTitle: 'Статус' },
42
+ info: { icon: Info, iconName: 'info', fallbackTitle: 'Информация' },
43
+ success: { icon: CheckCircle2, iconName: 'check-circle', fallbackTitle: 'Успех' },
44
+ warn: { icon: AlertTriangle, iconName: 'alert-triangle', fallbackTitle: 'Предупреждение' },
45
+ danger: { icon: XCircle, iconName: 'x-circle', fallbackTitle: 'Ошибка' },
46
+ }
47
+
48
+ /**
49
+ * Возвращает единую icon/title-презентацию для feedback tone.
50
+ * Returns the canonical icon/title presentation for a feedback tone.
51
+ */
52
+ export function resolveFeedbackPresentation(tone: FeedbackTone): Readonly<FeedbackPresentation> {
53
+ return presentations[tone]
54
+ }
55
+
56
+ /**
57
+ * Возвращает единственный icon/tone/motion contract для status indicator.
58
+ * Returns the canonical icon/tone/motion contract for a status indicator.
59
+ */
60
+ export function resolveFeedbackIndicator(
61
+ state: FeedbackIndicatorState,
62
+ ): Readonly<FeedbackIndicatorPresentation> {
63
+ if (state === 'progress') {
64
+ return {
65
+ tone: 'info',
66
+ icon: LoaderCircle,
67
+ iconName: 'loader-circle',
68
+ fallbackTitle: 'Выполняется',
69
+ motion: 'spin',
70
+ }
71
+ }
72
+ const presentation = resolveFeedbackPresentation(state)
73
+ return {
74
+ tone: state,
75
+ icon: presentation.icon,
76
+ iconName: presentation.iconName,
77
+ fallbackTitle: presentation.fallbackTitle,
78
+ motion: 'static',
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Выражает live-region contract без привязки к визуальному компоненту.
84
+ * Resolves live-region semantics independently from a visual component.
85
+ */
86
+ export function resolveFeedbackLive(
87
+ tone: FeedbackTone,
88
+ mode: FeedbackLiveMode,
89
+ ): Readonly<FeedbackLiveAttributes> {
90
+ if (mode === 'off') return {}
91
+ const politeness = mode === 'auto' ? (tone === 'danger' ? 'assertive' : 'polite') : mode
92
+ return {
93
+ role: politeness === 'assertive' ? 'alert' : 'status',
94
+ ariaLive: politeness,
95
+ ariaAtomic: 'true',
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Возвращает единый визуальный и live-region contract для async surface state.
101
+ * Returns the canonical visual and live-region contract for an async surface state.
102
+ */
103
+ export function resolveAsyncFeedbackPresentation(
104
+ status: AsyncFeedbackStatus,
105
+ ): Readonly<AsyncFeedbackPresentation> {
106
+ if (status === 'loading') {
107
+ const progress = resolveFeedbackIndicator('progress')
108
+ return {
109
+ tone: progress.tone,
110
+ icon: progress.icon,
111
+ iconName: progress.iconName,
112
+ fallbackMessage: 'Загрузка…',
113
+ busy: true,
114
+ live: resolveFeedbackLive(progress.tone, 'auto'),
115
+ }
116
+ }
117
+ if (status === 'empty') {
118
+ return {
119
+ tone: 'neutral',
120
+ icon: null,
121
+ iconName: null,
122
+ fallbackMessage: 'Нет данных для отображения.',
123
+ busy: false,
124
+ live: resolveFeedbackLive('neutral', 'auto'),
125
+ }
126
+ }
127
+ const error = resolveFeedbackIndicator('danger')
128
+ return {
129
+ tone: 'danger',
130
+ icon: error.icon,
131
+ iconName: error.iconName,
132
+ fallbackMessage: error.fallbackTitle,
133
+ busy: false,
134
+ live: resolveFeedbackLive('danger', 'auto'),
135
+ }
136
+ }
@@ -0,0 +1,59 @@
1
+ import { validateBoolean, validateExactString, validateNonEmptyString } from './runtimeContract'
2
+
3
+ const FIELD_SURFACE_KINDS = [
4
+ 'text-input',
5
+ 'textarea',
6
+ 'inline-token-editor',
7
+ 'combobox-trigger',
8
+ ] as const
9
+ const FIELD_SURFACE_APPEARANCES = ['field', 'ghost', 'embedded'] as const
10
+
11
+ /** Canonical owners sharing one field-surface visual contract. / Канонические владельцы общего визуального контракта field surface. */
12
+ export type FieldSurfaceKind = typeof FIELD_SURFACE_KINDS[number]
13
+ /** Canonical participation mode in the shared field surface. / Режим участия в общей field surface. */
14
+ export type FieldSurfaceAppearance = typeof FIELD_SURFACE_APPEARANCES[number]
15
+
16
+ export interface FieldSurfaceContract {
17
+ kind: FieldSurfaceKind
18
+ appearance: FieldSurfaceAppearance
19
+ invalid: boolean
20
+ disabled: boolean
21
+ readonly: boolean
22
+ }
23
+
24
+ export interface FieldSurfaceBindings {
25
+ 'data-s-field-surface': FieldSurfaceKind
26
+ 'data-s-field-appearance': FieldSurfaceAppearance
27
+ 'data-s-field-invalid'?: 'true'
28
+ 'data-s-field-disabled'?: 'true'
29
+ 'data-s-field-readonly'?: 'true'
30
+ }
31
+
32
+ /**
33
+ * Создаёт exact DOM bindings единого field-surface foundation без truthy coercion.
34
+ * Creates exact DOM bindings for the shared field-surface foundation without truthy coercion.
35
+ */
36
+ export function resolveFieldSurfaceBindings(
37
+ owner: string,
38
+ contract: FieldSurfaceContract,
39
+ ): FieldSurfaceBindings {
40
+ validateNonEmptyString('fieldSurface', 'owner', owner)
41
+ const kind = validateExactString(owner, 'field surface kind', contract.kind, FIELD_SURFACE_KINDS)
42
+ const appearance = validateExactString(
43
+ owner,
44
+ 'field surface appearance',
45
+ contract.appearance,
46
+ FIELD_SURFACE_APPEARANCES,
47
+ )
48
+ const invalid = validateBoolean(owner, 'field surface invalid', contract.invalid)
49
+ const disabled = validateBoolean(owner, 'field surface disabled', contract.disabled)
50
+ const readonly = validateBoolean(owner, 'field surface readonly', contract.readonly)
51
+
52
+ return {
53
+ 'data-s-field-surface': kind,
54
+ 'data-s-field-appearance': appearance,
55
+ ...(invalid ? { 'data-s-field-invalid': 'true' as const } : {}),
56
+ ...(disabled ? { 'data-s-field-disabled': 'true' as const } : {}),
57
+ ...(readonly ? { 'data-s-field-readonly': 'true' as const } : {}),
58
+ }
59
+ }
@@ -0,0 +1,69 @@
1
+ const FOCUSABLE_SELECTOR = [
2
+ 'a[href]',
3
+ 'button:not([disabled])',
4
+ 'textarea:not([disabled])',
5
+ 'input:not([disabled])',
6
+ 'select:not([disabled])',
7
+ '[contenteditable="true"]',
8
+ '[tabindex]:not([tabindex="-1"])',
9
+ ].join(', ')
10
+
11
+ const isSemanticallyVisible = (element: HTMLElement): boolean => {
12
+ if (element.hidden || element.closest('[inert]') || element.closest('[aria-hidden="true"]')) return false
13
+ const style = window.getComputedStyle(element)
14
+ return style.display !== 'none' && style.visibility !== 'hidden'
15
+ }
16
+
17
+ /** Проверяет programmatic/browser focus owner без layout-эвристик. / Checks a programmatic/browser focus owner without layout heuristics. */
18
+ export const isFocusableElement = (element: HTMLElement): boolean => (
19
+ element.matches(FOCUSABLE_SELECTOR) && isSemanticallyVisible(element)
20
+ )
21
+
22
+ /**
23
+ * Поднимает произвольный event target до ближайшего canonical focus owner внутри optional boundary.
24
+ * Lifts an arbitrary event target to the nearest canonical focus owner inside an optional boundary.
25
+ */
26
+ export const findFocusableOwner = (
27
+ target: EventTarget | null,
28
+ boundary?: ParentNode | null,
29
+ ): HTMLElement | null => {
30
+ const targetElement = target instanceof Element
31
+ ? target
32
+ : target instanceof Node
33
+ ? target.parentElement
34
+ : null
35
+ const owner = targetElement?.closest<HTMLElement>(FOCUSABLE_SELECTOR) ?? null
36
+ if (!owner || !isFocusableElement(owner)) return null
37
+ if (boundary instanceof Node && !boundary.contains(owner)) return null
38
+ return owner
39
+ }
40
+
41
+ export const getFocusableElements = (
42
+ root: ParentNode,
43
+ options: { requireLayout?: boolean } = {},
44
+ ): HTMLElement[] => [...root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)].filter((element) => {
45
+ if (!isFocusableElement(element)) return false
46
+ if (!options.requireLayout) return true
47
+ return element.offsetParent !== null || element.getClientRects().length > 0
48
+ })
49
+
50
+ export const findRelativeTabTarget = (
51
+ reference: HTMLElement,
52
+ scope: ParentNode,
53
+ excludedRoot: HTMLElement | null,
54
+ reverse: boolean,
55
+ ): HTMLElement | null => {
56
+ const candidates = getFocusableElements(scope).filter((element) => !excludedRoot?.contains(element))
57
+ if (candidates.length === 0) return null
58
+ const exactIndex = candidates.indexOf(reference)
59
+ if (exactIndex >= 0) {
60
+ const offset = reverse ? -1 : 1
61
+ return candidates[(exactIndex + offset + candidates.length) % candidates.length] ?? null
62
+ }
63
+
64
+ const ordered = reverse ? [...candidates].reverse() : candidates
65
+ const relation = reverse ? Node.DOCUMENT_POSITION_PRECEDING : Node.DOCUMENT_POSITION_FOLLOWING
66
+ return ordered.find((candidate) => Boolean(reference.compareDocumentPosition(candidate) & relation))
67
+ ?? ordered[0]
68
+ ?? null
69
+ }
@@ -0,0 +1,136 @@
1
+ import { validateBoundedInteger, validateNonEmptyString } from './runtimeContract'
2
+
3
+ export type InlineTokenEditorSegmentContract =
4
+ | { readonly kind: 'text'; readonly text: string }
5
+ | {
6
+ readonly kind: 'token'
7
+ readonly occurrenceId: string
8
+ readonly id: string
9
+ readonly label: string
10
+ readonly title?: string
11
+ }
12
+
13
+ function assertPlainRecord(owner: string, value: unknown): Record<string, unknown> {
14
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
15
+ throw new TypeError(`${owner}: значение должно быть plain object. / ${owner}: value must be a plain object.`)
16
+ }
17
+ return value as Record<string, unknown>
18
+ }
19
+
20
+ function assertExactKeys(owner: string, record: Record<string, unknown>, allowed: readonly string[]): void {
21
+ const unexpected = Object.keys(record).filter((key) => !allowed.includes(key))
22
+ if (unexpected.length > 0) {
23
+ throw new TypeError(`${owner}: неизвестные поля ${unexpected.map((key) => JSON.stringify(key)).join(', ')}. / ${owner}: unknown fields ${unexpected.map((key) => JSON.stringify(key)).join(', ')}.`)
24
+ }
25
+ }
26
+
27
+ /** Собирает plain-text mirror без нормализации сегментов. / Builds the plain-text mirror without normalizing segments. */
28
+ export function inlineTokenSegmentsToText(segments: readonly InlineTokenEditorSegmentContract[]): string {
29
+ return segments.map((segment) => segment.kind === 'text' ? segment.text : segment.label).join('')
30
+ }
31
+
32
+ /** Проверяет canonical external segment source и его exact model mirror. / Validates the canonical external segment source and its exact model mirror. */
33
+ export function parseInlineTokenEditorSource(
34
+ modelValue: unknown,
35
+ segments: unknown,
36
+ ): readonly InlineTokenEditorSegmentContract[] {
37
+ if (typeof modelValue !== 'string') {
38
+ throw new TypeError('SInlineTokenEditor: modelValue должен быть строкой. / SInlineTokenEditor: modelValue must be a string.')
39
+ }
40
+ if (!Array.isArray(segments)) {
41
+ throw new TypeError('SInlineTokenEditor: segments должен быть массивом. / SInlineTokenEditor: segments must be an array.')
42
+ }
43
+
44
+ const parsed: InlineTokenEditorSegmentContract[] = []
45
+ const tokenOccurrences = new Map<string, number>()
46
+ for (let index = 0; index < segments.length; index += 1) {
47
+ if (!Object.hasOwn(segments, index)) {
48
+ throw new TypeError(`SInlineTokenEditor: segments[${index}] отсутствует в sparse array. / SInlineTokenEditor: segments[${index}] is missing from a sparse array.`)
49
+ }
50
+ const record = assertPlainRecord(`SInlineTokenEditor.segments[${index}]`, segments[index])
51
+ if (record.kind === 'text') {
52
+ assertExactKeys(`SInlineTokenEditor.segments[${index}]`, record, ['kind', 'text'])
53
+ if (typeof record.text !== 'string' || record.text.length === 0) {
54
+ throw new TypeError(`SInlineTokenEditor: segments[${index}].text должен быть непустой строкой. / SInlineTokenEditor: segments[${index}].text must be a non-empty string.`)
55
+ }
56
+ const text = record.text
57
+ if (parsed.at(-1)?.kind === 'text') {
58
+ throw new TypeError(`SInlineTokenEditor: segments[${index}] является смежным text segment; объедините его в источнике. / SInlineTokenEditor: segments[${index}] is an adjacent text segment; merge it at the source.`)
59
+ }
60
+ parsed.push({ kind: 'text', text })
61
+ continue
62
+ }
63
+ if (record.kind !== 'token') {
64
+ throw new TypeError(`SInlineTokenEditor: segments[${index}].kind должен быть "text" или "token". / SInlineTokenEditor: segments[${index}].kind must be "text" or "token".`)
65
+ }
66
+ assertExactKeys(`SInlineTokenEditor.segments[${index}]`, record, ['kind', 'occurrenceId', 'id', 'label', 'title'])
67
+ const occurrenceId = validateNonEmptyString(
68
+ 'SInlineTokenEditor', `segments[${index}].occurrenceId`, record.occurrenceId,
69
+ )
70
+ const firstOccurrenceIndex = tokenOccurrences.get(occurrenceId)
71
+ if (firstOccurrenceIndex !== undefined) {
72
+ throw new TypeError(`SInlineTokenEditor: segments[${index}].occurrenceId дублирует ${JSON.stringify(occurrenceId)} из segments[${firstOccurrenceIndex}].occurrenceId. / SInlineTokenEditor: segments[${index}].occurrenceId duplicates ${JSON.stringify(occurrenceId)} from segments[${firstOccurrenceIndex}].occurrenceId.`)
73
+ }
74
+ tokenOccurrences.set(occurrenceId, index)
75
+ const title = record.title === undefined
76
+ ? undefined
77
+ : validateNonEmptyString('SInlineTokenEditor', `segments[${index}].title`, record.title)
78
+ parsed.push({
79
+ kind: 'token',
80
+ occurrenceId,
81
+ id: validateNonEmptyString('SInlineTokenEditor', `segments[${index}].id`, record.id),
82
+ label: validateNonEmptyString('SInlineTokenEditor', `segments[${index}].label`, record.label),
83
+ ...(title === undefined ? {} : { title }),
84
+ })
85
+ }
86
+
87
+ const mirror = inlineTokenSegmentsToText(parsed)
88
+ if (modelValue !== mirror) {
89
+ throw new TypeError(`SInlineTokenEditor: modelValue не совпадает с text mirror segments; ожидалось ${JSON.stringify(mirror)}, получено ${JSON.stringify(modelValue)}. / SInlineTokenEditor: modelValue does not match the segments text mirror; expected ${JSON.stringify(mirror)}, received ${JSON.stringify(modelValue)}.`)
90
+ }
91
+ return parsed
92
+ }
93
+
94
+ /** Проверяет cursor offset относительно exact logical text. / Validates a cursor offset against exact logical text. */
95
+ export function validateInlineTokenCursorOffset(value: unknown, textLength: number, coordinate: string): number {
96
+ return validateBoundedInteger('SInlineTokenEditor', coordinate, value, { minimum: 0, maximum: textLength })
97
+ }
98
+
99
+ /** Запрещает cursor внутри atomic token label; разрешены только его границы. / Rejects cursor positions inside an atomic token label; only its boundaries are addressable. */
100
+ export function validateInlineTokenCursorBoundary(
101
+ segments: readonly InlineTokenEditorSegmentContract[],
102
+ value: unknown,
103
+ coordinate: string,
104
+ ): number {
105
+ const offset = validateInlineTokenCursorOffset(value, inlineTokenSegmentsToText(segments).length, coordinate)
106
+ let segmentStart = 0
107
+ for (const segment of segments) {
108
+ const segmentEnd = segmentStart + (segment.kind === 'text' ? segment.text.length : segment.label.length)
109
+ if (segment.kind === 'token' && offset > segmentStart && offset < segmentEnd) {
110
+ throw new RangeError(`SInlineTokenEditor: ${coordinate}=${offset} попадает внутрь atomic token occurrence ${JSON.stringify(segment.occurrenceId)}; допустимы ${segmentStart} или ${segmentEnd}. / SInlineTokenEditor: ${coordinate}=${offset} falls inside atomic token occurrence ${JSON.stringify(segment.occurrenceId)}; use ${segmentStart} or ${segmentEnd}.`)
111
+ }
112
+ segmentStart = segmentEnd
113
+ }
114
+ return offset
115
+ }
116
+
117
+ /** Создаёт canonical internally-generated sequence; external invalid input сюда не попадает. / Creates a canonical internally-generated sequence; external invalid input never enters this path. */
118
+ export function composeInlineTokenSegments(
119
+ segments: readonly InlineTokenEditorSegmentContract[],
120
+ ): InlineTokenEditorSegmentContract[] {
121
+ const result: InlineTokenEditorSegmentContract[] = []
122
+ for (const segment of segments) {
123
+ if (segment.kind === 'text') {
124
+ if (segment.text.length === 0) continue
125
+ const previous = result.at(-1)
126
+ if (previous?.kind === 'text') {
127
+ result[result.length - 1] = { kind: 'text', text: previous.text + segment.text }
128
+ } else {
129
+ result.push({ kind: 'text', text: segment.text })
130
+ }
131
+ } else {
132
+ result.push({ ...segment })
133
+ }
134
+ }
135
+ return [...parseInlineTokenEditorSource(inlineTokenSegmentsToText(result), result)]
136
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Типизированная element/focus-поверхность UI-kit leaf для составных владельцев.
3
+ * Typed element/focus surface exposed by a UI-kit leaf to composite owners.
4
+ */
5
+ export interface ElementApi<TElement extends HTMLElement = HTMLElement> {
6
+ getElement: () => TElement
7
+ }
8
+
9
+ export interface InteractiveElementApi<TElement extends HTMLElement = HTMLElement> extends ElementApi<TElement> {
10
+ focus: (options?: FocusOptions) => void
11
+ }
12
+
13
+ export type ElementRefTarget = Element | ElementApi | ComponentPublicInstance | null
14
+ export type InteractiveElementRefTarget = Element | InteractiveElementApi | ComponentPublicInstance | null
15
+
16
+ /** Разрешает callback-ref только через native HTMLElement или публичный leaf API. / Resolves a callback ref only through a native HTMLElement or a public leaf API. */
17
+ export function resolveInteractiveElement(
18
+ owner: string,
19
+ target: ElementRefTarget,
20
+ ): HTMLElement | null {
21
+ if (target === null) return null
22
+ if (target instanceof HTMLElement) return target
23
+ const getElement = Reflect.get(target, 'getElement')
24
+ if (typeof getElement !== 'function') {
25
+ throw new TypeError(`${owner}: ref must resolve to an HTMLElement or ElementApi`)
26
+ }
27
+ const element: unknown = Reflect.apply(getElement, target, [])
28
+ if (!(element instanceof HTMLElement)) {
29
+ throw new TypeError(`${owner}: ElementApi.getElement() must return an HTMLElement`)
30
+ }
31
+ return element
32
+ }
33
+ import type { ComponentPublicInstance } from 'vue'