@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,24 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-virtual-list {
4
+ @apply w-full min-w-0;
5
+ overflow-anchor: none;
6
+ }
7
+
8
+ .s-virtual-list:focus-visible {
9
+ outline: var(--s-focus-ring-width) solid var(--s-focus-ring-strong-color);
10
+ outline-offset: var(--s-focus-ring-inset-offset);
11
+ }
12
+
13
+ .s-virtual-list__spacer {
14
+ @apply pointer-events-none w-full;
15
+ }
16
+
17
+ .s-virtual-list__item {
18
+ @apply box-border w-full min-w-0;
19
+ }
20
+
21
+ .s-virtual-list__item:focus-visible {
22
+ outline: var(--s-focus-ring-width) solid var(--s-focus-ring-strong-color);
23
+ outline-offset: var(--s-focus-ring-inset-offset);
24
+ }
@@ -0,0 +1,19 @@
1
+ /** Ключ элемента виртуального списка. / Virtual-list item key. */
2
+ export type SVirtualListItemKey = string | number
3
+
4
+ /**
5
+ * Публичный контракт виртуального списка с измеряемыми строками.
6
+ * Public contract for a virtual list with measured rows.
7
+ */
8
+ export interface SVirtualListProps<Item = unknown> {
9
+ items: readonly Item[]
10
+ /** Оценочная высота строки до фактического измерения ResizeObserver. / Estimated row height before ResizeObserver measurement. */
11
+ itemSize: number
12
+ overscan?: number
13
+ height?: number | null
14
+ /** Устойчивый доменный ключ; индекс массива не является допустимым fallback. / Stable domain key; an array index is not a valid fallback. */
15
+ itemKey: (item: Item) => SVirtualListItemKey
16
+ itemLabel: (item: Item, index: number) => string
17
+ accessibleLabel: string
18
+ externalScrollTarget?: HTMLElement | null
19
+ }
@@ -0,0 +1,361 @@
1
+ <template>
2
+ <div
3
+ v-bind="ownedAttrs.bindings()"
4
+ ref="container"
5
+ class="s-virtual-list"
6
+ :style="containerStyle"
7
+ :data-item-size="itemSize"
8
+ :data-viewport-height="height ?? undefined"
9
+ role="list"
10
+ :aria-label="accessibleLabel"
11
+ tabindex="0"
12
+ @scroll.passive="updateViewport"
13
+ >
14
+ <div class="s-virtual-list__spacer" :style="spacerStyle" aria-hidden="true" />
15
+ <div
16
+ v-for="entry in visibleEntries"
17
+ :key="entry.key"
18
+ :ref="renderedItemRef(entry.key)"
19
+ class="s-virtual-list__item"
20
+ :style="itemStyle(entry.index)"
21
+ :data-virtual-index="entry.index"
22
+ role="listitem"
23
+ :aria-label="resolveItemLabel(entry.item, entry.index)"
24
+ :aria-posinset="entry.index + 1"
25
+ :aria-setsize="items.length"
26
+ tabindex="-1"
27
+ >
28
+ <slot
29
+ :item="entry.item"
30
+ :index="entry.index"
31
+ :position="entry.index + 1"
32
+ :set-size="items.length"
33
+ />
34
+ </div>
35
+ </div>
36
+ </template>
37
+
38
+ <script setup lang="ts" generic="T">
39
+ import {
40
+ computed,
41
+ nextTick,
42
+ onBeforeUnmount,
43
+ onMounted,
44
+ ref,
45
+ type VNodeRef,
46
+ watch,
47
+ } from 'vue'
48
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
49
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
50
+ import type { SVirtualListItemKey, SVirtualListProps } from './SVirtualList'
51
+
52
+ defineOptions({ inheritAttrs: false })
53
+
54
+ type Alignment = 'auto' | 'start' | 'center' | 'end'
55
+ type StyleObject = Record<string, string>
56
+
57
+ const props = withDefaults(defineProps<SVirtualListProps<T>>(), {
58
+ overscan: 8,
59
+ height: null,
60
+ externalScrollTarget: null,
61
+ })
62
+
63
+ defineSlots<{
64
+ default(props: { item: T; index: number; position: number; setSize: number }): unknown
65
+ }>()
66
+
67
+ const container = ref<HTMLElement | null>(null)
68
+ const ownedAttrs = useOwnedAttrs({ component: 'SVirtualList', owner: 'virtual list viewport' })
69
+ useInteractiveLeafRegistration({ owner: 'SVirtualList' })
70
+ const scrollTop = ref(0)
71
+ const viewportHeight = ref(0)
72
+ const measurements = ref(new Map<SVirtualListItemKey, number>())
73
+ const renderedItemsByKey = new Map<SVirtualListItemKey, HTMLElement>()
74
+ const renderedItemKeysByElement = new Map<HTMLElement, SVirtualListItemKey>()
75
+ const renderedItemRefs = new Map<SVirtualListItemKey, VNodeRef>()
76
+ let resizeObserver: ResizeObserver | null = null
77
+ let boundExternalTarget: HTMLElement | null = null
78
+
79
+ const isExternal = computed(() => props.externalScrollTarget !== null)
80
+
81
+ function describeItemKey(key: SVirtualListItemKey): string {
82
+ return `${typeof key}:${JSON.stringify(key)}`
83
+ }
84
+
85
+ function validateItemKey(candidate: unknown, index: number): asserts candidate is SVirtualListItemKey {
86
+ if (typeof candidate === 'string') {
87
+ if (candidate.trim().length === 0) {
88
+ throw new TypeError(`SVirtualList itemKey returned an empty string for item at index ${index}`)
89
+ }
90
+ return
91
+ }
92
+ if (typeof candidate === 'number') {
93
+ if (!Number.isFinite(candidate)) {
94
+ throw new TypeError(`SVirtualList itemKey returned a non-finite number for item at index ${index}`)
95
+ }
96
+ return
97
+ }
98
+ throw new TypeError(`SVirtualList itemKey returned unsupported ${typeof candidate} key for item at index ${index}`)
99
+ }
100
+
101
+ function resolveItemLabel(item: T, index: number): string {
102
+ return props.itemLabel(item, index)
103
+ }
104
+
105
+ const itemKeys = computed(() => {
106
+ const seen = new Set<SVirtualListItemKey>()
107
+ return props.items.map((item, index) => {
108
+ const key: unknown = props.itemKey(item)
109
+ validateItemKey(key, index)
110
+ if (seen.has(key)) {
111
+ throw new TypeError(`SVirtualList itemKey returned duplicate key ${describeItemKey(key)} at index ${index}`)
112
+ }
113
+ seen.add(key)
114
+ return key
115
+ })
116
+ })
117
+ const itemKeyIndexes = computed(() => new Map(itemKeys.value.map((key, index) => [key, index])))
118
+ const itemSizes = computed(() => itemKeys.value.map((key) => measurements.value.get(key) ?? props.itemSize))
119
+ const offsets = computed(() => {
120
+ const values = new Array<number>(props.items.length + 1)
121
+ values[0] = 0
122
+ for (let index = 0; index < props.items.length; index += 1) {
123
+ values[index + 1] = (values[index] ?? 0) + (itemSizes.value[index] ?? props.itemSize)
124
+ }
125
+ return values
126
+ })
127
+ const totalHeight = computed(() => offsets.value[props.items.length] ?? 0)
128
+
129
+ const containerStyle = computed<StyleObject>(() => {
130
+ if (isExternal.value) {
131
+ const externalStyle: StyleObject = {
132
+ position: 'relative',
133
+ overflow: 'visible',
134
+ height: `${totalHeight.value}px`,
135
+ }
136
+ return externalStyle
137
+ }
138
+ const internalStyle: StyleObject = {
139
+ position: 'relative',
140
+ overflowY: 'auto',
141
+ height: typeof props.height === 'number' && props.height > 0 ? `${props.height}px` : '100%',
142
+ }
143
+ return internalStyle
144
+ })
145
+ const spacerStyle = computed<StyleObject>(() => ({ height: `${totalHeight.value}px` }))
146
+
147
+ function indexAtOffset(offset: number): number {
148
+ let low = 0
149
+ let high = props.items.length
150
+ while (low < high) {
151
+ const middle = Math.floor((low + high) / 2)
152
+ if ((offsets.value[middle + 1] ?? totalHeight.value) <= offset) low = middle + 1
153
+ else high = middle
154
+ }
155
+ return Math.min(low, Math.max(props.items.length - 1, 0))
156
+ }
157
+
158
+ const firstVisibleIndex = computed(() => props.items.length === 0 ? 0 : indexAtOffset(scrollTop.value))
159
+ const lastVisibleIndex = computed(() => props.items.length === 0
160
+ ? -1
161
+ : indexAtOffset(scrollTop.value + viewportHeight.value))
162
+ const startIndex = computed(() => Math.max(0, firstVisibleIndex.value - props.overscan))
163
+ const endIndex = computed(() => Math.min(props.items.length, lastVisibleIndex.value + props.overscan + 1))
164
+ const visibleEntries = computed(() => props.items.slice(startIndex.value, endIndex.value).map((item, localIndex) => {
165
+ const index = startIndex.value + localIndex
166
+ return { item, index, key: itemKeys.value[index] as SVirtualListItemKey }
167
+ }))
168
+
169
+ function itemStyle(index: number): StyleObject {
170
+ return {
171
+ position: 'absolute',
172
+ top: `${offsets.value[index] ?? 0}px`,
173
+ left: '0',
174
+ right: '0',
175
+ minHeight: `${props.itemSize}px`,
176
+ }
177
+ }
178
+
179
+ function externalOffset(): number {
180
+ const target = props.externalScrollTarget
181
+ const root = container.value
182
+ if (!target || !root) return 0
183
+ return root.getBoundingClientRect().top - target.getBoundingClientRect().top + target.scrollTop
184
+ }
185
+
186
+ function updateViewport(): void {
187
+ const target = props.externalScrollTarget ?? container.value
188
+ if (!target) return
189
+ viewportHeight.value = target.clientHeight
190
+ scrollTop.value = Math.max(0, target.scrollTop - (isExternal.value ? externalOffset() : 0))
191
+ }
192
+
193
+ function preserveAnchor(index: number, delta: number): void {
194
+ if (index >= firstVisibleIndex.value || delta === 0) return
195
+ const target = props.externalScrollTarget ?? container.value
196
+ if (target) target.scrollTop += delta
197
+ }
198
+
199
+ function unregisterRenderedItem(key: SVirtualListItemKey): void {
200
+ const element = renderedItemsByKey.get(key)
201
+ if (!element) return
202
+ const registeredKey = renderedItemKeysByElement.get(element)
203
+ if (registeredKey !== key) {
204
+ throw new Error(
205
+ `SVirtualList rendered item registry mismatch for key ${describeItemKey(key)}: element belongs to ${registeredKey === undefined ? 'no key' : describeItemKey(registeredKey)}`,
206
+ )
207
+ }
208
+ resizeObserver?.unobserve(element)
209
+ renderedItemsByKey.delete(key)
210
+ renderedItemKeysByElement.delete(element)
211
+ }
212
+
213
+ function registerRenderedItem(key: SVirtualListItemKey, element: HTMLElement): void {
214
+ const index = itemKeyIndexes.value.get(key)
215
+ if (index === undefined) {
216
+ throw new Error(`SVirtualList received a rendered item ref for stale key ${describeItemKey(key)}`)
217
+ }
218
+ const existingElement = renderedItemsByKey.get(key)
219
+ if (existingElement && existingElement !== element) {
220
+ throw new Error(`SVirtualList rendered item key collision for ${describeItemKey(key)} at index ${index}`)
221
+ }
222
+ const existingKey = renderedItemKeysByElement.get(element)
223
+ if (existingKey !== undefined && existingKey !== key) {
224
+ throw new Error(
225
+ `SVirtualList rendered item element collision between ${describeItemKey(existingKey)} and ${describeItemKey(key)}`,
226
+ )
227
+ }
228
+ if (existingElement === element) return
229
+ renderedItemsByKey.set(key, element)
230
+ renderedItemKeysByElement.set(element, key)
231
+ resizeObserver?.observe(element)
232
+ }
233
+
234
+ function renderedItemRef(key: SVirtualListItemKey): VNodeRef {
235
+ const existingRef = renderedItemRefs.get(key)
236
+ if (existingRef) return existingRef
237
+ const itemRef: VNodeRef = (target) => {
238
+ if (target === null) {
239
+ unregisterRenderedItem(key)
240
+ return
241
+ }
242
+ if (!(target instanceof HTMLElement)) {
243
+ throw new TypeError(`SVirtualList rendered item ref for ${describeItemKey(key)} must resolve to an HTMLElement`)
244
+ }
245
+ registerRenderedItem(key, target)
246
+ }
247
+ renderedItemRefs.set(key, itemRef)
248
+ return itemRef
249
+ }
250
+
251
+ function measureEntry(entry: ResizeObserverEntry): void {
252
+ if (entry.target === container.value || entry.target === props.externalScrollTarget) {
253
+ updateViewport()
254
+ return
255
+ }
256
+ if (!(entry.target instanceof HTMLElement)) {
257
+ throw new TypeError('SVirtualList received a ResizeObserver entry for a non-HTMLElement target')
258
+ }
259
+ const key = renderedItemKeysByElement.get(entry.target)
260
+ if (key === undefined || renderedItemsByKey.get(key) !== entry.target) {
261
+ throw new Error('SVirtualList received a ResizeObserver entry for an unregistered rendered item')
262
+ }
263
+ const index = itemKeyIndexes.value.get(key)
264
+ if (index === undefined) {
265
+ throw new Error(`SVirtualList received a ResizeObserver entry for stale key ${describeItemKey(key)}`)
266
+ }
267
+ const size = entry.borderBoxSize[0]?.blockSize ?? entry.contentRect.height
268
+ if (!Number.isFinite(size) || size <= 0 || measurements.value.get(key) === size) return
269
+ const previous = measurements.value.get(key) ?? props.itemSize
270
+ const next = new Map(measurements.value)
271
+ next.set(key, size)
272
+ measurements.value = next
273
+ preserveAnchor(index, size - previous)
274
+ }
275
+
276
+ function scrollToIndex(index: number, alignment: Alignment = 'auto'): void {
277
+ if (!Number.isInteger(index) || index < 0 || index >= props.items.length) {
278
+ throw new RangeError(`SVirtualList index ${index} is outside 0..${Math.max(props.items.length - 1, 0)}`)
279
+ }
280
+ const target = props.externalScrollTarget ?? container.value
281
+ if (!target) return
282
+ const start = offsets.value[index] ?? 0
283
+ const end = offsets.value[index + 1] ?? start + props.itemSize
284
+ const viewportStart = scrollTop.value
285
+ const viewportEnd = viewportStart + viewportHeight.value
286
+ let nextOffset = start
287
+ if (alignment === 'end') nextOffset = end - viewportHeight.value
288
+ if (alignment === 'center') nextOffset = start - (viewportHeight.value - (end - start)) / 2
289
+ if (alignment === 'auto' && start >= viewportStart && end <= viewportEnd) return
290
+ if (alignment === 'auto' && end > viewportEnd) nextOffset = end - viewportHeight.value
291
+ target.scrollTop = Math.max(0, nextOffset + (isExternal.value ? externalOffset() : 0))
292
+ updateViewport()
293
+ }
294
+
295
+ async function focusItem(index: number, alignment: Alignment = 'auto'): Promise<void> {
296
+ const key = itemKeys.value[index]
297
+ scrollToIndex(index, alignment)
298
+ await nextTick()
299
+ if (key === undefined || itemKeyIndexes.value.get(key) === undefined) {
300
+ throw new Error(`SVirtualList focusItem key ${key === undefined ? `at index ${index}` : describeItemKey(key)} became stale before focus`)
301
+ }
302
+ const currentIndex = itemKeyIndexes.value.get(key)
303
+ if (currentIndex !== index) {
304
+ throw new Error(
305
+ `SVirtualList focusItem key ${describeItemKey(key)} moved from index ${index} to ${currentIndex} before focus`,
306
+ )
307
+ }
308
+ const element = renderedItemsByKey.get(key)
309
+ if (!element) {
310
+ throw new Error(`SVirtualList focusItem has no rendered item registration for key ${describeItemKey(key)} at index ${index}`)
311
+ }
312
+ if (renderedItemKeysByElement.get(element) !== key) {
313
+ throw new Error(`SVirtualList focusItem found a stale rendered item registration for key ${describeItemKey(key)}`)
314
+ }
315
+ element.focus()
316
+ }
317
+
318
+ defineExpose({ scrollToIndex, focusItem })
319
+
320
+ function bindExternalTarget(target: HTMLElement | null): void {
321
+ boundExternalTarget?.removeEventListener('scroll', updateViewport)
322
+ if (boundExternalTarget) resizeObserver?.unobserve(boundExternalTarget)
323
+ boundExternalTarget = target
324
+ boundExternalTarget?.addEventListener('scroll', updateViewport, { passive: true })
325
+ if (boundExternalTarget) resizeObserver?.observe(boundExternalTarget)
326
+ updateViewport()
327
+ }
328
+
329
+ onMounted(() => {
330
+ if (typeof ResizeObserver !== 'undefined') {
331
+ resizeObserver = new ResizeObserver((entries) => entries.forEach(measureEntry))
332
+ if (container.value) resizeObserver.observe(container.value)
333
+ for (const element of renderedItemsByKey.values()) resizeObserver.observe(element)
334
+ }
335
+ bindExternalTarget(props.externalScrollTarget)
336
+ updateViewport()
337
+ })
338
+
339
+ watch(() => props.externalScrollTarget, bindExternalTarget)
340
+ watch(() => itemKeys.value, (keys) => {
341
+ const activeKeys = new Set(keys)
342
+ measurements.value = new Map([...measurements.value].filter(([key]) => activeKeys.has(key)))
343
+ for (const key of renderedItemsByKey.keys()) {
344
+ if (!activeKeys.has(key)) unregisterRenderedItem(key)
345
+ }
346
+ for (const key of renderedItemRefs.keys()) {
347
+ if (!activeKeys.has(key)) renderedItemRefs.delete(key)
348
+ }
349
+ updateViewport()
350
+ })
351
+
352
+ onBeforeUnmount(() => {
353
+ boundExternalTarget?.removeEventListener('scroll', updateViewport)
354
+ resizeObserver?.disconnect()
355
+ renderedItemsByKey.clear()
356
+ renderedItemKeysByElement.clear()
357
+ renderedItemRefs.clear()
358
+ })
359
+ </script>
360
+
361
+ <style lang="postcss" src="./SVirtualList.css" scoped></style>
@@ -0,0 +1,136 @@
1
+ /** JSON scalar according to the JSON data model. / JSON-скаляр по модели данных JSON. */
2
+ export type JsonScalar = string | number | boolean | null
3
+
4
+ /** JSON object with recursively exact values. / JSON-объект с рекурсивно точными значениями. */
5
+ export type JsonObject = { readonly [key: string]: JsonValue }
6
+
7
+ /** JSON array with recursively exact values. / JSON-массив с рекурсивно точными значениями. */
8
+ export type JsonArray = readonly JsonValue[]
9
+
10
+ /** Value representable by JSON without coercion or omission. / Значение, представимое JSON без преобразования или пропуска. */
11
+ export type JsonValue = JsonScalar | JsonObject | JsonArray
12
+
13
+ /** Escapes one RFC 6901 reference token. / Экранирует один reference token по RFC 6901. */
14
+ export function escapeJsonPointerToken(token: string): string {
15
+ return token.replaceAll('~', '~0').replaceAll('/', '~1')
16
+ }
17
+
18
+ /** Appends a reference token to an RFC 6901 JSON Pointer. / Добавляет reference token к JSON Pointer по RFC 6901. */
19
+ export function appendJsonPointer(pointer: string, token: string): string {
20
+ return `${pointer}/${escapeJsonPointerToken(token)}`
21
+ }
22
+
23
+ function pointerCoordinate(pointer: string): string {
24
+ return `JSON Pointer ${JSON.stringify(pointer)}`
25
+ }
26
+
27
+ function unsupportedValueMessage(component: string, value: unknown, pointer: string): string {
28
+ return `${component}: unsupported ${typeof value} value at ${pointerCoordinate(pointer)}`
29
+ }
30
+
31
+ function assertArray(
32
+ value: unknown[],
33
+ component: string,
34
+ pointer: string,
35
+ activeContainers: Set<object>,
36
+ ): void {
37
+ const keys = Reflect.ownKeys(value)
38
+ if (keys.some((key) => typeof key === 'symbol')) {
39
+ throw new TypeError(`${component}: symbol properties are not representable at ${pointerCoordinate(pointer)}`)
40
+ }
41
+
42
+ for (const key of keys) {
43
+ if (typeof key !== 'string') {
44
+ throw new TypeError(`${component}: symbol properties are not representable at ${pointerCoordinate(pointer)}`)
45
+ }
46
+ if (key === 'length') continue
47
+ const index = Number(key)
48
+ if (!Number.isInteger(index) || index < 0 || String(index) !== key || index >= value.length) {
49
+ throw new TypeError(`${component}: non-JSON array property at ${pointerCoordinate(appendJsonPointer(pointer, key))}`)
50
+ }
51
+ }
52
+
53
+ for (let index = 0; index < value.length; index += 1) {
54
+ const itemPointer = appendJsonPointer(pointer, String(index))
55
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index))
56
+ if (!descriptor) {
57
+ throw new TypeError(`${component}: sparse array item at ${pointerCoordinate(itemPointer)}`)
58
+ }
59
+ if ('get' in descriptor || 'set' in descriptor) {
60
+ throw new TypeError(`${component}: accessor property at ${pointerCoordinate(itemPointer)}`)
61
+ }
62
+ assertJsonValueAt(descriptor.value, component, itemPointer, activeContainers)
63
+ }
64
+ }
65
+
66
+ function assertObject(
67
+ value: object,
68
+ component: string,
69
+ pointer: string,
70
+ activeContainers: Set<object>,
71
+ ): void {
72
+ const prototype = Object.getPrototypeOf(value)
73
+ if (prototype !== Object.prototype && prototype !== null) {
74
+ throw new TypeError(`${component}: expected a plain JSON object at ${pointerCoordinate(pointer)}`)
75
+ }
76
+
77
+ const keys = Reflect.ownKeys(value)
78
+ if (keys.some((key) => typeof key === 'symbol')) {
79
+ throw new TypeError(`${component}: symbol properties are not representable at ${pointerCoordinate(pointer)}`)
80
+ }
81
+
82
+ for (const key of keys) {
83
+ if (typeof key !== 'string') {
84
+ throw new TypeError(`${component}: symbol properties are not representable at ${pointerCoordinate(pointer)}`)
85
+ }
86
+ const propertyPointer = appendJsonPointer(pointer, key)
87
+ const descriptor = Object.getOwnPropertyDescriptor(value, key)
88
+ if (!descriptor) {
89
+ throw new TypeError(`${component}: unavailable property at ${pointerCoordinate(propertyPointer)}`)
90
+ }
91
+ if (!descriptor.enumerable) {
92
+ throw new TypeError(`${component}: non-enumerable property at ${pointerCoordinate(propertyPointer)}`)
93
+ }
94
+ if ('get' in descriptor || 'set' in descriptor) {
95
+ throw new TypeError(`${component}: accessor property at ${pointerCoordinate(propertyPointer)}`)
96
+ }
97
+ assertJsonValueAt(descriptor.value, component, propertyPointer, activeContainers)
98
+ }
99
+ }
100
+
101
+ function assertJsonValueAt(
102
+ value: unknown,
103
+ component: string,
104
+ pointer: string,
105
+ activeContainers: Set<object>,
106
+ ): asserts value is JsonValue {
107
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return
108
+ if (typeof value === 'number') {
109
+ if (!Number.isFinite(value)) {
110
+ throw new TypeError(`${component}: expected a finite number at ${pointerCoordinate(pointer)}`)
111
+ }
112
+ if (Object.is(value, -0)) {
113
+ throw new TypeError(`${component}: negative zero is not exactly representable at ${pointerCoordinate(pointer)}`)
114
+ }
115
+ return
116
+ }
117
+ if (typeof value !== 'object') {
118
+ throw new TypeError(unsupportedValueMessage(component, value, pointer))
119
+ }
120
+ if (activeContainers.has(value)) {
121
+ throw new TypeError(`${component}: cyclic reference at ${pointerCoordinate(pointer)}`)
122
+ }
123
+
124
+ activeContainers.add(value)
125
+ if (Array.isArray(value)) assertArray(value, component, pointer, activeContainers)
126
+ else assertObject(value, component, pointer, activeContainers)
127
+ activeContainers.delete(value)
128
+ }
129
+
130
+ /**
131
+ * Verifies that a runtime value is exact JSON without JSON.stringify coercions.
132
+ * Проверяет, что runtime-значение является точным JSON без преобразований JSON.stringify.
133
+ */
134
+ export function assertJsonValue(value: unknown, component = 'JSON value'): asserts value is JsonValue {
135
+ assertJsonValueAt(value, component, '', new Set<object>())
136
+ }
@@ -0,0 +1,76 @@
1
+ import type {
2
+ ScrollableViewportMaxBlockSize,
3
+ TableColumnInlineSize,
4
+ TableColumnMinInlineSize,
5
+ } from '../../../internal/semanticSizing'
6
+
7
+ export type STableRowKey = string | number
8
+ export type STableAlignment = 'left' | 'center' | 'right'
9
+ export type STableRowState = 'default' | 'inserted' | 'updated' | 'deleted'
10
+ export type STableCellState = 'default' | 'pending'
11
+ export type STableSurface = 'bordered' | 'plain'
12
+ export type STableMinWidth = 'content' | 'md' | 'lg'
13
+ export type STableDensity = 'default' | 'compact'
14
+ export type STableColumnContent = 'field' | 'value' | 'slot'
15
+
16
+ interface STableColumnBase<Field extends string> {
17
+ field: Field
18
+ header: string
19
+ width?: TableColumnInlineSize
20
+ minWidth?: TableColumnMinInlineSize
21
+ align?: STableAlignment
22
+ }
23
+
24
+ type STableFieldColumn<Field extends string> = STableColumnBase<Field> & {
25
+ content?: 'field'
26
+ value?: never
27
+ }
28
+
29
+ type STableValueColumn<Row extends object, Field extends string> = STableColumnBase<Field> & {
30
+ content?: 'value'
31
+ value: (row: Row) => unknown
32
+ }
33
+
34
+ type STableSlotColumn<Field extends string> = STableColumnBase<Field> & {
35
+ content: 'slot'
36
+ value?: never
37
+ }
38
+
39
+ /**
40
+ * Столбец напрямую читает typed field строки; derived field обязан предоставить value resolver.
41
+ * A column reads a typed row field directly; a derived field must provide a value resolver.
42
+ */
43
+ export type STableColumn<
44
+ Row extends object = Record<string, unknown>,
45
+ Field extends string = Extract<keyof Row, string>,
46
+ > = STableValueColumn<Row, Field>
47
+ | STableSlotColumn<Field>
48
+ | (Field extends Extract<keyof Row, string> ? STableFieldColumn<Field> : never)
49
+
50
+ /** Публичный generic-контракт таблицы. / Public generic table contract. */
51
+ export interface STableProps<
52
+ Row extends object,
53
+ RowKey extends STableRowKey,
54
+ Field extends string,
55
+ > {
56
+ columns: readonly STableColumn<Row, Field>[]
57
+ data: readonly Row[]
58
+ getRowKey: (row: Row) => RowKey
59
+ loading?: boolean
60
+ loadingMessage?: string
61
+ errorMessage?: string
62
+ emptyMessage?: string
63
+ selectedRowKey?: RowKey
64
+ maxHeight?: ScrollableViewportMaxBlockSize
65
+ density?: STableDensity
66
+ surface?: STableSurface
67
+ minWidth?: STableMinWidth
68
+ rowState?: (row: Row) => STableRowState
69
+ cellState?: (row: Row, column: STableColumn<Row, Field>) => STableCellState
70
+ /** Accessible name без visible owner. / Accessible name when there is no visible owner. */
71
+ ariaLabel?: string
72
+ /** DOM id visible owner, mutually exclusive with ariaLabel. / DOM id of the visible owner, mutually exclusive with ariaLabel. */
73
+ ariaLabelledby?: string
74
+ /** DOM id supplemental status/diagnostic owner. / DOM id of a supplemental status/diagnostic owner. */
75
+ ariaDescribedby?: string
76
+ }
@@ -0,0 +1,3 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-data-grid__value { @apply block min-w-0 truncate; }