@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,194 @@
1
+ export interface BoundedNumberDomain {
2
+ minimum: number
3
+ maximum: number
4
+ }
5
+
6
+ const ASCII_WHITESPACE_PATTERN = /[\t\n\f\r ]/u
7
+
8
+ function describeRuntimeValue(value: unknown): string {
9
+ if (typeof value === 'string') return JSON.stringify(value)
10
+ if (typeof value === 'number') return `number(${String(value)})`
11
+ if (typeof value === 'bigint') return `bigint(${String(value)})`
12
+ if (typeof value === 'boolean') return `boolean(${String(value)})`
13
+ if (typeof value === 'undefined') return 'undefined'
14
+ if (typeof value === 'symbol') return `symbol(${value.description ?? ''})`
15
+ if (typeof value === 'function') return 'function'
16
+ if (value === null) return 'null'
17
+ return Array.isArray(value) ? 'array' : 'object'
18
+ }
19
+
20
+ function assertBoundedDomain(
21
+ owner: string,
22
+ coordinate: string,
23
+ domain: BoundedNumberDomain,
24
+ integer: boolean,
25
+ ): void {
26
+ const validNumbers = Number.isFinite(domain.minimum) && Number.isFinite(domain.maximum)
27
+ const validIntegerBounds = !integer
28
+ || (Number.isInteger(domain.minimum) && Number.isInteger(domain.maximum))
29
+ if (validNumbers && validIntegerBounds && domain.minimum <= domain.maximum) return
30
+
31
+ const russianKind = integer ? 'целыми числами' : 'конечными числами'
32
+ const englishKind = integer ? 'integers' : 'finite numbers'
33
+ throw new TypeError(
34
+ `runtimeContract: границы domain для ${owner}.${coordinate} должны быть ${russianKind}, `
35
+ + `где minimum <= maximum; получено minimum=${describeRuntimeValue(domain.minimum)}, `
36
+ + `maximum=${describeRuntimeValue(domain.maximum)}. / runtimeContract: domain bounds for `
37
+ + `${owner}.${coordinate} must be ${englishKind} with minimum <= maximum; received `
38
+ + `minimum=${describeRuntimeValue(domain.minimum)}, maximum=${describeRuntimeValue(domain.maximum)}.`,
39
+ )
40
+ }
41
+
42
+ function assertExactStringDomain(
43
+ owner: string,
44
+ coordinate: string,
45
+ allowed: readonly string[],
46
+ ): void {
47
+ const uniqueValues = new Set(allowed)
48
+ if (
49
+ allowed.length > 0
50
+ && uniqueValues.size === allowed.length
51
+ && allowed.every((value) => value.trim().length > 0)
52
+ ) return
53
+
54
+ throw new TypeError(
55
+ `runtimeContract: domain для ${owner}.${coordinate} должен содержать непустой уникальный список строк. `
56
+ + `/ runtimeContract: domain for ${owner}.${coordinate} must contain a non-empty unique string list.`,
57
+ )
58
+ }
59
+
60
+ /**
61
+ * Проверяет непустую строку без trim/coercion и возвращает исходное значение.
62
+ * Validates a non-empty string without trimming/coercion and returns the original value.
63
+ */
64
+ export function validateNonEmptyString(
65
+ owner: string,
66
+ coordinate: string,
67
+ value: unknown,
68
+ ): string {
69
+ if (typeof value === 'string' && value.trim().length > 0) return value
70
+ const received = describeRuntimeValue(value)
71
+ throw new TypeError(
72
+ `${owner}: ${coordinate} должен быть непустой строкой; получено ${received}. `
73
+ + `/ ${owner}: ${coordinate} must be a non-empty string; received ${received}.`,
74
+ )
75
+ }
76
+
77
+ /**
78
+ * Проверяет boolean без truthy/falsy coercion.
79
+ * Validates a boolean without truthy/falsy coercion.
80
+ */
81
+ export function validateBoolean(owner: string, coordinate: string, value: unknown): boolean {
82
+ if (typeof value === 'boolean') return value
83
+ const received = describeRuntimeValue(value)
84
+ throw new TypeError(
85
+ `${owner}: ${coordinate} должен быть boolean; получено ${received}. `
86
+ + `/ ${owner}: ${coordinate} must be a boolean; received ${received}.`,
87
+ )
88
+ }
89
+
90
+ /**
91
+ * Проверяет DOM id по HTML-контракту: непустая строка без ASCII whitespace.
92
+ * Validates an HTML DOM id: a non-empty string without ASCII whitespace.
93
+ */
94
+ export function validateDomId(owner: string, coordinate: string, value: unknown): string {
95
+ if (typeof value === 'string' && value.length > 0 && !ASCII_WHITESPACE_PATTERN.test(value)) {
96
+ return value
97
+ }
98
+ const received = describeRuntimeValue(value)
99
+ throw new TypeError(
100
+ `${owner}: ${coordinate} должен быть непустым DOM id без ASCII-пробелов; получено ${received}. `
101
+ + `/ ${owner}: ${coordinate} must be a non-empty DOM id without ASCII whitespace; `
102
+ + `received ${received}.`,
103
+ )
104
+ }
105
+
106
+ /**
107
+ * Возвращает generated DOM id только при отсутствии явного значения и валидирует обе ветки.
108
+ * Returns a generated DOM id only when the explicit value is absent and validates both branches.
109
+ */
110
+ export function resolveOptionalDomId(
111
+ owner: string,
112
+ coordinate: string,
113
+ value: unknown,
114
+ generatedValue: unknown,
115
+ ): string {
116
+ return value === undefined
117
+ ? validateDomId(owner, `generated ${coordinate}`, generatedValue)
118
+ : validateDomId(owner, coordinate, value)
119
+ }
120
+
121
+ /**
122
+ * Проверяет exact string enum и сохраняет его literal union в return type.
123
+ * Validates an exact string enum and preserves its literal union in the return type.
124
+ */
125
+ export function validateExactString<const Allowed extends readonly string[]>(
126
+ owner: string,
127
+ coordinate: string,
128
+ value: unknown,
129
+ allowed: Allowed,
130
+ ): Allowed[number] {
131
+ assertExactStringDomain(owner, coordinate, allowed)
132
+ if (typeof value === 'string' && allowed.includes(value)) return value as Allowed[number]
133
+
134
+ const expected = `[${allowed.map((candidate) => JSON.stringify(candidate)).join(', ')}]`
135
+ const received = describeRuntimeValue(value)
136
+ throw new TypeError(
137
+ `${owner}: ${coordinate} должен быть одним из ${expected}; получено ${received}. `
138
+ + `/ ${owner}: ${coordinate} must be one of ${expected}; received ${received}.`,
139
+ )
140
+ }
141
+
142
+ /**
143
+ * Проверяет конечное число в inclusive domain без coercion.
144
+ * Validates a finite number in an inclusive domain without coercion.
145
+ */
146
+ export function validateBoundedNumber(
147
+ owner: string,
148
+ coordinate: string,
149
+ value: unknown,
150
+ domain: BoundedNumberDomain,
151
+ ): number {
152
+ assertBoundedDomain(owner, coordinate, domain, false)
153
+ if (
154
+ typeof value === 'number'
155
+ && Number.isFinite(value)
156
+ && value >= domain.minimum
157
+ && value <= domain.maximum
158
+ ) return value
159
+
160
+ const received = describeRuntimeValue(value)
161
+ throw new TypeError(
162
+ `${owner}: ${coordinate} должен быть конечным числом в диапазоне `
163
+ + `[${domain.minimum}, ${domain.maximum}]; получено ${received}. / ${owner}: ${coordinate} `
164
+ + `must be a finite number in the inclusive range [${domain.minimum}, ${domain.maximum}]; `
165
+ + `received ${received}.`,
166
+ )
167
+ }
168
+
169
+ /**
170
+ * Проверяет целое число в inclusive domain без coercion.
171
+ * Validates an integer in an inclusive domain without coercion.
172
+ */
173
+ export function validateBoundedInteger(
174
+ owner: string,
175
+ coordinate: string,
176
+ value: unknown,
177
+ domain: BoundedNumberDomain,
178
+ ): number {
179
+ assertBoundedDomain(owner, coordinate, domain, true)
180
+ if (
181
+ typeof value === 'number'
182
+ && Number.isInteger(value)
183
+ && value >= domain.minimum
184
+ && value <= domain.maximum
185
+ ) return value
186
+
187
+ const received = describeRuntimeValue(value)
188
+ throw new TypeError(
189
+ `${owner}: ${coordinate} должен быть целым числом в диапазоне `
190
+ + `[${domain.minimum}, ${domain.maximum}]; получено ${received}. / ${owner}: ${coordinate} `
191
+ + `must be an integer in the inclusive range [${domain.minimum}, ${domain.maximum}]; `
192
+ + `received ${received}.`,
193
+ )
194
+ }
@@ -0,0 +1,159 @@
1
+ import { onBeforeUnmount, onMounted, readonly, ref, type DeepReadonly, type Ref } from 'vue'
2
+ import {
3
+ isThemePreference,
4
+ themeVariants,
5
+ type ThemeMode,
6
+ type ThemePreference,
7
+ type ThemeVariant,
8
+ } from '../theme'
9
+
10
+ /**
11
+ * Текущее декларативное состояние темы документа.
12
+ * Current declarative document-theme state.
13
+ */
14
+ export interface RuntimeThemeState {
15
+ /** Точное значение `data-theme`; `null` только при отсутствии явной темы. / Exact `data-theme`; `null` only when no explicit theme is applied. */
16
+ readonly dataTheme: ThemePreference | null
17
+ /** Нормализованный режим для runtime adapter. / Normalized mode for a runtime adapter. */
18
+ readonly mode: ThemeMode
19
+ }
20
+
21
+ type RuntimeThemeListener = (state: RuntimeThemeState) => void
22
+
23
+ interface DocumentThemeRuntime {
24
+ readonly document: Document
25
+ readonly listeners: Set<RuntimeThemeListener>
26
+ observer: MutationObserver | null
27
+ state: RuntimeThemeState
28
+ }
29
+
30
+ const runtimes = new WeakMap<Document, DocumentThemeRuntime>()
31
+
32
+ const themeVariantByDataTheme = new Map<ThemePreference, ThemeVariant>(
33
+ themeVariants.map((variant): [ThemePreference, ThemeVariant] => [variant.dataTheme, variant]),
34
+ )
35
+
36
+ const statesEqual = (left: RuntimeThemeState, right: RuntimeThemeState): boolean => (
37
+ left.dataTheme === right.dataTheme && left.mode === right.mode
38
+ )
39
+
40
+ /**
41
+ * Читает theme contract документа без подписки и DOM-мутаций.
42
+ * Reads the document theme contract without subscribing or mutating DOM.
43
+ */
44
+ export function readRuntimeTheme(targetDocument?: Document): RuntimeThemeState {
45
+ if (targetDocument === undefined) return { dataTheme: null, mode: 'light' }
46
+ const root = targetDocument.documentElement
47
+ const rawDataTheme = root.getAttribute('data-theme')
48
+ if (rawDataTheme === null) {
49
+ if (root.classList.contains('dark')) {
50
+ throw new Error('UI-kit runtime theme: class="dark" требует canonical data-theme.')
51
+ }
52
+ return { dataTheme: null, mode: 'light' }
53
+ }
54
+ const dataTheme = rawDataTheme
55
+ if (!isThemePreference(dataTheme)) {
56
+ throw new TypeError(`UI-kit runtime theme: неизвестный data-theme "${rawDataTheme}".`)
57
+ }
58
+ const variant = themeVariantByDataTheme.get(dataTheme)
59
+ if (variant === undefined) {
60
+ throw new Error(
61
+ `UI-kit runtime theme: data-theme="${dataTheme}" отсутствует в canonical theme registry.`,
62
+ )
63
+ }
64
+ const mode = variant.mode
65
+ const hasDarkClass = root.classList.contains('dark')
66
+ if (mode === 'dark' && !hasDarkClass) {
67
+ throw new Error(`UI-kit runtime theme: data-theme="${dataTheme}" требует class="dark".`)
68
+ }
69
+ if (mode === 'light' && hasDarkClass) {
70
+ throw new Error(`UI-kit runtime theme: data-theme="${dataTheme}" конфликтует с class="dark".`)
71
+ }
72
+ return {
73
+ dataTheme,
74
+ mode,
75
+ }
76
+ }
77
+
78
+ const refreshRuntime = (runtime: DocumentThemeRuntime): void => {
79
+ const nextState = readRuntimeTheme(runtime.document)
80
+ if (statesEqual(runtime.state, nextState)) return
81
+ runtime.state = nextState
82
+ for (const listener of runtime.listeners) listener(nextState)
83
+ }
84
+
85
+ const createRuntime = (targetDocument: Document): DocumentThemeRuntime => ({
86
+ document: targetDocument,
87
+ listeners: new Set(),
88
+ observer: null,
89
+ state: readRuntimeTheme(targetDocument),
90
+ })
91
+
92
+ const startRuntime = (runtime: DocumentThemeRuntime): void => {
93
+ if (runtime.observer || typeof MutationObserver === 'undefined') return
94
+ runtime.observer = new MutationObserver(() => refreshRuntime(runtime))
95
+ runtime.observer.observe(runtime.document.documentElement, {
96
+ attributes: true,
97
+ attributeFilter: ['class', 'data-theme'],
98
+ })
99
+ }
100
+
101
+ const stopRuntime = (runtime: DocumentThemeRuntime): void => {
102
+ if (runtime.observer !== null) runtime.observer.disconnect()
103
+ runtime.observer = null
104
+ runtimes.delete(runtime.document)
105
+ }
106
+
107
+ /**
108
+ * Подписывает runtime-интеграцию на единый per-document theme owner.
109
+ * Subscribes a runtime integration to the single per-document theme owner.
110
+ */
111
+ export function subscribeRuntimeTheme(
112
+ listener: RuntimeThemeListener,
113
+ targetDocument?: Document,
114
+ ): () => void {
115
+ if (targetDocument === undefined) {
116
+ listener(readRuntimeTheme())
117
+ return () => undefined
118
+ }
119
+
120
+ let runtime = runtimes.get(targetDocument)
121
+ if (runtime === undefined) {
122
+ runtime = createRuntime(targetDocument)
123
+ runtimes.set(targetDocument, runtime)
124
+ }
125
+ const subscribedRuntime = runtime
126
+ subscribedRuntime.listeners.add(listener)
127
+ listener(subscribedRuntime.state)
128
+ startRuntime(subscribedRuntime)
129
+
130
+ let subscribed = true
131
+ return () => {
132
+ if (!subscribed) return
133
+ subscribed = false
134
+ subscribedRuntime.listeners.delete(listener)
135
+ if (subscribedRuntime.listeners.size === 0) stopRuntime(subscribedRuntime)
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Vue lifecycle adapter для единого runtime theme owner.
141
+ * Vue lifecycle adapter for the canonical runtime theme owner.
142
+ */
143
+ export function useRuntimeTheme(): DeepReadonly<Ref<RuntimeThemeState>> {
144
+ const targetDocument = typeof document === 'undefined' ? undefined : document
145
+ const state = ref<RuntimeThemeState>(readRuntimeTheme(targetDocument))
146
+ let unsubscribe: (() => void) | null = null
147
+
148
+ onMounted(() => {
149
+ unsubscribe = subscribeRuntimeTheme((nextState) => {
150
+ state.value = nextState
151
+ }, targetDocument)
152
+ })
153
+ onBeforeUnmount(() => {
154
+ if (unsubscribe !== null) unsubscribe()
155
+ unsubscribe = null
156
+ })
157
+
158
+ return readonly(state)
159
+ }
@@ -0,0 +1,60 @@
1
+ import { validateExactString } from './runtimeContract'
2
+
3
+ const SCROLLABLE_VIEWPORT_MAX_BLOCK_SIZES = ['none', 'sm', 'md', 'lg'] as const
4
+ const EXPANDED_CONTENT_MAX_BLOCK_SIZES = ['sm', 'md', 'lg'] as const
5
+ const TABLE_COLUMN_INLINE_SIZES = ['auto', 'xs', 'sm', 'md', 'lg', 'xl', 'fill'] as const
6
+ const TABLE_COLUMN_MIN_INLINE_SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const
7
+
8
+ /**
9
+ * Максимальный block-size прокручиваемого viewport; none сохраняет естественный flow.
10
+ * Maximum block size for a scroll viewport; none preserves natural flow.
11
+ */
12
+ export type ScrollableViewportMaxBlockSize = typeof SCROLLABLE_VIEWPORT_MAX_BLOCK_SIZES[number]
13
+
14
+ /**
15
+ * Максимальный block-size раскрытого текстового content region.
16
+ * Maximum block size for an expanded textual content region.
17
+ */
18
+ export type ExpandedContentMaxBlockSize = typeof EXPANDED_CONTENT_MAX_BLOCK_SIZES[number]
19
+
20
+ /** Семантическая ширина столбца таблицы. / Semantic table-column inline size. */
21
+ export type TableColumnInlineSize = typeof TABLE_COLUMN_INLINE_SIZES[number]
22
+
23
+ /** Семантическая минимальная ширина столбца таблицы. / Semantic table-column minimum inline size. */
24
+ export type TableColumnMinInlineSize = typeof TABLE_COLUMN_MIN_INLINE_SIZES[number]
25
+
26
+ /** Проверяет semantic scroll viewport size без CSS-length escape. / Validates a semantic scroll viewport size without a CSS-length escape. */
27
+ export function validateScrollableViewportMaxBlockSize(
28
+ owner: string,
29
+ coordinate: string,
30
+ value: unknown,
31
+ ): ScrollableViewportMaxBlockSize {
32
+ return validateExactString(owner, coordinate, value, SCROLLABLE_VIEWPORT_MAX_BLOCK_SIZES)
33
+ }
34
+
35
+ /** Проверяет semantic expanded-content size без CSS-length escape. / Validates a semantic expanded-content size without a CSS-length escape. */
36
+ export function validateExpandedContentMaxBlockSize(
37
+ owner: string,
38
+ coordinate: string,
39
+ value: unknown,
40
+ ): ExpandedContentMaxBlockSize {
41
+ return validateExactString(owner, coordinate, value, EXPANDED_CONTENT_MAX_BLOCK_SIZES)
42
+ }
43
+
44
+ /** Проверяет semantic column width без CSS-length escape. / Validates semantic column width without a CSS-length escape. */
45
+ export function validateTableColumnInlineSize(
46
+ owner: string,
47
+ coordinate: string,
48
+ value: unknown,
49
+ ): TableColumnInlineSize {
50
+ return validateExactString(owner, coordinate, value, TABLE_COLUMN_INLINE_SIZES)
51
+ }
52
+
53
+ /** Проверяет semantic minimum column width без CSS-length escape. / Validates semantic minimum column width without a CSS-length escape. */
54
+ export function validateTableColumnMinInlineSize(
55
+ owner: string,
56
+ coordinate: string,
57
+ value: unknown,
58
+ ): TableColumnMinInlineSize {
59
+ return validateExactString(owner, coordinate, value, TABLE_COLUMN_MIN_INLINE_SIZES)
60
+ }
@@ -0,0 +1,129 @@
1
+ import { nextTick, onBeforeUnmount, ref, type Ref } from 'vue'
2
+ import { useFloatingPosition } from '../composables/useFloatingPosition'
3
+ import { findFocusableOwner, findRelativeTabTarget } from './focusNavigation'
4
+ import { registerLayer, type LayerRegistration } from './layerStack'
5
+
6
+ export type MenuAnchor =
7
+ | { type: 'element'; element: HTMLElement; align: 'left' | 'right'; placement?: 'bottom' | 'inline' }
8
+ | { type: 'point'; x: number; y: number }
9
+
10
+ export type MenuCloseReason = 'escape' | 'outside' | 'programmatic' | 'selection' | 'trigger'
11
+
12
+ const VIEWPORT_PADDING = 5
13
+
14
+ const pointRect = (x: number, y: number): DOMRect => ({
15
+ x,
16
+ y,
17
+ left: x,
18
+ right: x,
19
+ top: y,
20
+ bottom: y,
21
+ width: 0,
22
+ height: 0,
23
+ toJSON: () => ({ x, y }),
24
+ }) as DOMRect
25
+
26
+ /** Управляет общей lifecycle/focus границей menu поверх canonical floating geometry. / Owns menu lifecycle and focus over canonical floating geometry. */
27
+ export function useMenuLayer(
28
+ layerRef: Ref<HTMLElement | null>,
29
+ options: { onDismiss?: (reason: 'escape' | 'outside') => void } = {},
30
+ ) {
31
+ const isOpen = ref(false)
32
+ const zIndex = ref('var(--s-layer-floating)')
33
+ let anchor: MenuAnchor | null = null
34
+ let focusAnchor: HTMLElement | null = null
35
+ let registration: LayerRegistration | null = null
36
+
37
+ function close(reason: MenuCloseReason = 'programmatic'): void {
38
+ if (!isOpen.value && !registration) return
39
+ isOpen.value = false
40
+ registration?.unregister({ restoreFocus: reason !== 'outside' })
41
+ registration = null
42
+ anchor = null
43
+ focusAnchor = null
44
+ zIndex.value = 'var(--s-layer-floating)'
45
+ }
46
+
47
+ function closeForTab(event: KeyboardEvent): void {
48
+ const reference = focusAnchor
49
+ if (!reference) {
50
+ close('outside')
51
+ return
52
+ }
53
+ const scope = registration?.focusScope() ?? document
54
+ const target = findRelativeTabTarget(reference, scope, layerRef.value, event.shiftKey)
55
+ event.preventDefault()
56
+ close('outside')
57
+ if (target) nextTick(() => target.focus())
58
+ }
59
+
60
+ const anchorElement = (): HTMLElement | null => anchor?.type === 'element' ? anchor.element : null
61
+ const anchorRect = (): DOMRect | null => {
62
+ if (!anchor) return null
63
+ if (anchor.type === 'point') return pointRect(anchor.x, anchor.y)
64
+ if (!anchor.element.isConnected) {
65
+ close('outside')
66
+ return null
67
+ }
68
+ return anchor.element.getBoundingClientRect()
69
+ }
70
+
71
+ const {
72
+ coordinates: position,
73
+ updatePosition: positionLayer,
74
+ } = useFloatingPosition({
75
+ active: isOpen,
76
+ placement: () => anchor?.type === 'element' && anchor.placement === 'inline'
77
+ ? (anchor.align === 'right' ? 'left-start' : 'right-start')
78
+ : anchor?.type === 'element' && anchor.align === 'right' ? 'bottom-end' : 'bottom-start',
79
+ anchorElement,
80
+ anchorRect,
81
+ panelRef: layerRef,
82
+ offset: () => 0,
83
+ boundaryPadding: () => VIEWPORT_PADDING,
84
+ flip: () => true,
85
+ shift: () => true,
86
+ widthMatch: () => 'none',
87
+ zIndex: () => zIndex.value,
88
+ scrollStrategy: 'reposition',
89
+ })
90
+
91
+ async function open(nextAnchor: MenuAnchor, focusTarget?: HTMLElement | null): Promise<void> {
92
+ if (isOpen.value) close('outside')
93
+ if (nextAnchor.type === 'element' && findFocusableOwner(nextAnchor.element) !== nextAnchor.element) {
94
+ throw new Error('useMenuLayer: element anchor must be the canonical focusable owner')
95
+ }
96
+ anchor = nextAnchor
97
+ const restoreTarget = findFocusableOwner(focusTarget ?? null)
98
+ ?? findFocusableOwner(document.activeElement)
99
+ focusAnchor = nextAnchor.type === 'element' ? nextAnchor.element : restoreTarget
100
+ isOpen.value = true
101
+ registration = registerLayer({
102
+ kind: 'menu',
103
+ root: () => layerRef.value,
104
+ anchor: () => nextAnchor.type === 'element' ? nextAnchor.element : restoreTarget,
105
+ restoreTarget,
106
+ requestDismiss: (reason) => {
107
+ const closeReason = reason === 'escape' ? 'escape' : 'outside'
108
+ close(closeReason)
109
+ options.onDismiss?.(closeReason)
110
+ },
111
+ })
112
+ zIndex.value = registration.zIndex
113
+ await nextTick()
114
+ if (!isOpen.value) return
115
+ if (!layerRef.value) {
116
+ close('outside')
117
+ throw new Error('useMenuLayer: layer element is unavailable after opening')
118
+ }
119
+ positionLayer()
120
+ }
121
+
122
+ function toggle(nextAnchor: MenuAnchor, focusTarget?: HTMLElement | null): void {
123
+ if (isOpen.value) close('trigger')
124
+ else void open(nextAnchor, focusTarget)
125
+ }
126
+
127
+ onBeforeUnmount(() => close('outside'))
128
+ return { isOpen, position, zIndex, open, close, closeForTab, toggle, positionLayer }
129
+ }
@@ -0,0 +1,69 @@
1
+ import { defineStore } from 'pinia'
2
+ import { v4 as uuidv4 } from 'uuid'
3
+ import type { FeedbackSeverity } from '../internal/feedbackPresentation'
4
+
5
+ /** Severity уведомления использует канонический feedback-контракт. / Notification severity uses the canonical feedback contract. */
6
+ export type Severity = FeedbackSeverity
7
+
8
+ export interface Notification {
9
+ id: string
10
+ severity: Severity
11
+ title?: string
12
+ message: string
13
+ duration?: number
14
+ createdAt: string
15
+ }
16
+
17
+ export type NotificationInput = Omit<Notification, 'id' | 'createdAt'>
18
+
19
+ interface NotifierState {
20
+ notifications: Notification[]
21
+ history: Notification[]
22
+ }
23
+
24
+ const DEFAULT_NOTIFICATION_DURATION_MS = 2800
25
+ const removalTimers = new Map<string, ReturnType<typeof setTimeout>>()
26
+
27
+ function stopRemovalTimer(id: string): void {
28
+ const timer = removalTimers.get(id)
29
+ if (!timer) {
30
+ return
31
+ }
32
+ clearTimeout(timer)
33
+ removalTimers.delete(id)
34
+ }
35
+
36
+ export const useNotifierStore = defineStore('notifier', {
37
+ state: (): NotifierState => ({
38
+ notifications: [],
39
+ history: [],
40
+ }),
41
+ actions: {
42
+ add(notification: NotificationInput) {
43
+ const id = uuidv4()
44
+ const newNotification: Notification = {
45
+ id,
46
+ createdAt: new Date().toISOString(),
47
+ duration: DEFAULT_NOTIFICATION_DURATION_MS,
48
+ ...notification,
49
+ }
50
+
51
+ this.history.unshift(newNotification)
52
+ this.notifications.push(newNotification)
53
+
54
+ if (newNotification.duration) {
55
+ const removalTimer = setTimeout(() => {
56
+ this.remove(id)
57
+ }, newNotification.duration)
58
+ removalTimers.set(id, removalTimer)
59
+ }
60
+ },
61
+ remove(id: string) {
62
+ stopRemovalTimer(id)
63
+ this.notifications = this.notifications.filter((n) => n.id !== id)
64
+ },
65
+ clearHistory() {
66
+ this.history = []
67
+ },
68
+ },
69
+ })
@@ -0,0 +1,4 @@
1
+ @import "./tokens.css";
2
+ @import "tailwindcss";
3
+ @plugin "@tailwindcss/typography";
4
+ @custom-variant dark (&:where(.dark, .dark *));