@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.
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/docs/accessibility.md +27 -0
- package/docs/getting-started.md +80 -0
- package/docs/licensing.md +30 -0
- package/docs/public-api.md +39 -0
- package/docs/security.md +21 -0
- package/docs/theming.md +35 -0
- package/docs/versioning.md +23 -0
- package/index.ts +3 -0
- package/package.json +592 -0
- package/src/components/icons/_internal/generatedSputnigUiIconMarkup.ts +971 -0
- package/src/components/icons/sputnigUiIcons.ts +530 -0
- package/src/components/layout/SAppShell.vue +232 -0
- package/src/components/layout/SDockRegion.vue +200 -0
- package/src/components/layout/SResizeHandle.vue +251 -0
- package/src/components/layout/SSideMenuButton.vue +104 -0
- package/src/components/layout/SSideMenuRail.vue +47 -0
- package/src/components/layout/SStack.css +25 -0
- package/src/components/layout/SStack.vue +57 -0
- package/src/components/layout/SStatusBarShell.css +15 -0
- package/src/components/layout/SStatusBarShell.vue +26 -0
- package/src/components/layout/SWorkbenchLayout.vue +53 -0
- package/src/components/layout/SWorkbenchPanelShell.css +161 -0
- package/src/components/layout/SWorkbenchPanelShell.vue +286 -0
- package/src/components/layout/_internal/SAppBrand.css +45 -0
- package/src/components/layout/_internal/SAppBrand.vue +37 -0
- package/src/components/layout/_internal/SAppBrandContent.vue +30 -0
- package/src/components/layout/types.ts +28 -0
- package/src/components/shared/_internal/SDialogSurface.css +127 -0
- package/src/components/shared/_internal/SDialogSurface.vue +117 -0
- package/src/components/shared/_internal/SDisclosureSummary.css +92 -0
- package/src/components/shared/_internal/SDisclosureSummary.vue +121 -0
- package/src/components/shared/_internal/SListboxGroup.css +18 -0
- package/src/components/shared/_internal/SListboxGroup.vue +24 -0
- package/src/components/shared/_internal/SMenuItem.css +112 -0
- package/src/components/shared/_internal/SMenuItem.vue +213 -0
- package/src/components/shared/_internal/SMenuLayer.css +21 -0
- package/src/components/shared/_internal/SMenuLayer.vue +48 -0
- package/src/components/shared/_internal/SMenuSeparator.css +5 -0
- package/src/components/shared/_internal/SMenuSeparator.vue +13 -0
- package/src/components/shared/_internal/SToastItem.css +65 -0
- package/src/components/shared/_internal/SToastItem.vue +58 -0
- package/src/components/shared/_internal/STreeNode.vue +761 -0
- package/src/components/shared/_internal/dialogSurface.ts +105 -0
- package/src/components/shared/_internal/focusTrap.ts +40 -0
- package/src/components/shared/_internal/listboxContext.ts +16 -0
- package/src/components/shared/_internal/markerContract.ts +10 -0
- package/src/components/shared/_internal/menuContext.ts +23 -0
- package/src/components/shared/_internal/selectionContract.ts +239 -0
- package/src/components/shared/_internal/sidebarGroupContext.ts +31 -0
- package/src/components/shared/_internal/tabsContext.ts +57 -0
- package/src/components/shared/_internal/treeRuntime.ts +203 -0
- package/src/components/shared/_internal/useSelectionRoving.ts +162 -0
- package/src/components/shared/codeLanguages.ts +168 -0
- package/src/components/shared/complex/SKanbanCard.css +8 -0
- package/src/components/shared/complex/SKanbanCard.vue +76 -0
- package/src/components/shared/complex/SKanbanLane.vue +82 -0
- package/src/components/shared/complex/STree.css +24 -0
- package/src/components/shared/complex/STree.vue +288 -0
- package/src/components/shared/complex/treeAdapter.ts +210 -0
- package/src/components/shared/complex/types.ts +102 -0
- package/src/components/shared/containers/SCloudChrome.css +103 -0
- package/src/components/shared/containers/SCloudChrome.vue +87 -0
- package/src/components/shared/containers/SCloudChromeEmpty.css +10 -0
- package/src/components/shared/containers/SCloudChromeEmpty.vue +18 -0
- package/src/components/shared/containers/SCloudChromeRow.css +78 -0
- package/src/components/shared/containers/SCloudChromeRow.vue +108 -0
- package/src/components/shared/containers/SCollapsiblePanel.css +57 -0
- package/src/components/shared/containers/SCollapsiblePanel.vue +360 -0
- package/src/components/shared/containers/SDrawer.css +60 -0
- package/src/components/shared/containers/SDrawer.vue +86 -0
- package/src/components/shared/containers/SFormGrid.css +26 -0
- package/src/components/shared/containers/SFormGrid.vue +46 -0
- package/src/components/shared/containers/SFormGridItem.css +18 -0
- package/src/components/shared/containers/SFormGridItem.vue +29 -0
- package/src/components/shared/containers/SLeftSidebar.vue +74 -0
- package/src/components/shared/containers/SModal.css +27 -0
- package/src/components/shared/containers/SModal.vue +105 -0
- package/src/components/shared/containers/SPageHeader.css +66 -0
- package/src/components/shared/containers/SPageHeader.vue +45 -0
- package/src/components/shared/containers/SPanel.css +134 -0
- package/src/components/shared/containers/SPanel.vue +169 -0
- package/src/components/shared/containers/SPopover.css +35 -0
- package/src/components/shared/containers/SPopover.vue +551 -0
- package/src/components/shared/containers/SSidebarGroup.css +12 -0
- package/src/components/shared/containers/SSidebarGroup.vue +443 -0
- package/src/components/shared/containers/SSidebarSection.css +41 -0
- package/src/components/shared/containers/SSidebarSection.vue +268 -0
- package/src/components/shared/containers/disclosureSummary.ts +48 -0
- package/src/components/shared/containers/popover.ts +72 -0
- package/src/components/shared/containers/sidebar.ts +206 -0
- package/src/components/shared/controls/SButton.css +274 -0
- package/src/components/shared/controls/SButton.vue +211 -0
- package/src/components/shared/controls/SCheckbox.css +65 -0
- package/src/components/shared/controls/SCheckbox.vue +130 -0
- package/src/components/shared/controls/SCheckboxGroup.vue +134 -0
- package/src/components/shared/controls/SChoiceCards.css +29 -0
- package/src/components/shared/controls/SChoiceCards.vue +145 -0
- package/src/components/shared/controls/SColorSelect.vue +146 -0
- package/src/components/shared/controls/SComboboxTrigger.css +35 -0
- package/src/components/shared/controls/SComboboxTrigger.vue +122 -0
- package/src/components/shared/controls/SContextMenu.css +11 -0
- package/src/components/shared/controls/SContextMenu.vue +186 -0
- package/src/components/shared/controls/SContextToggleButton.vue +64 -0
- package/src/components/shared/controls/SDragHandle.vue +100 -0
- package/src/components/shared/controls/SDropdownMenu.css +5 -0
- package/src/components/shared/controls/SDropdownMenu.vue +188 -0
- package/src/components/shared/controls/SField.css +79 -0
- package/src/components/shared/controls/SField.vue +89 -0
- package/src/components/shared/controls/SFieldGroup.css +58 -0
- package/src/components/shared/controls/SFieldGroup.vue +117 -0
- package/src/components/shared/controls/SFieldLabel.css +12 -0
- package/src/components/shared/controls/SFieldLabel.vue +51 -0
- package/src/components/shared/controls/SInlineTokenEditor.css +61 -0
- package/src/components/shared/controls/SInlineTokenEditor.vue +1081 -0
- package/src/components/shared/controls/SInputText.css +66 -0
- package/src/components/shared/controls/SInputText.vue +321 -0
- package/src/components/shared/controls/SInteractiveSurface.css +72 -0
- package/src/components/shared/controls/SInteractiveSurface.vue +326 -0
- package/src/components/shared/controls/SLink.css +114 -0
- package/src/components/shared/controls/SLink.ts +85 -0
- package/src/components/shared/controls/SLink.vue +114 -0
- package/src/components/shared/controls/SListbox.css +31 -0
- package/src/components/shared/controls/SListbox.vue +388 -0
- package/src/components/shared/controls/SListboxOption.css +63 -0
- package/src/components/shared/controls/SListboxOption.vue +85 -0
- package/src/components/shared/controls/SMarker.css +19 -0
- package/src/components/shared/controls/SMarker.vue +33 -0
- package/src/components/shared/controls/SMenuSurface.css +20 -0
- package/src/components/shared/controls/SMenuSurface.vue +333 -0
- package/src/components/shared/controls/SSegmentedControl.css +38 -0
- package/src/components/shared/controls/SSegmentedControl.vue +134 -0
- package/src/components/shared/controls/SSelect.css +12 -0
- package/src/components/shared/controls/SSelect.vue +338 -0
- package/src/components/shared/controls/STextarea.css +31 -0
- package/src/components/shared/controls/STextarea.vue +316 -0
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +224 -0
- package/src/components/shared/controls/menu.ts +178 -0
- package/src/components/shared/data-display/SActionCard.css +6 -0
- package/src/components/shared/data-display/SActionCard.vue +49 -0
- package/src/components/shared/data-display/SActionList.css +15 -0
- package/src/components/shared/data-display/SActionList.vue +34 -0
- package/src/components/shared/data-display/SActionListItem.css +114 -0
- package/src/components/shared/data-display/SActionListItem.vue +230 -0
- package/src/components/shared/data-display/SBadge.css +73 -0
- package/src/components/shared/data-display/SBadge.vue +52 -0
- package/src/components/shared/data-display/SCodeBlock.css +56 -0
- package/src/components/shared/data-display/SCodeBlock.vue +188 -0
- package/src/components/shared/data-display/SCodeEditor.css +178 -0
- package/src/components/shared/data-display/SCodeEditor.vue +274 -0
- package/src/components/shared/data-display/SCodeSearchPanel.vue +327 -0
- package/src/components/shared/data-display/SCopyField.vue +77 -0
- package/src/components/shared/data-display/SDocBlock.css +136 -0
- package/src/components/shared/data-display/SDocBlock.vue +358 -0
- package/src/components/shared/data-display/SEmptyState.css +42 -0
- package/src/components/shared/data-display/SEmptyState.vue +79 -0
- package/src/components/shared/data-display/SExpandableText.vue +314 -0
- package/src/components/shared/data-display/SJsonTree.css +6 -0
- package/src/components/shared/data-display/SJsonTree.vue +111 -0
- package/src/components/shared/data-display/SKeyValueGrid.css +37 -0
- package/src/components/shared/data-display/SKeyValueGrid.vue +277 -0
- package/src/components/shared/data-display/SLinkedSystemsList.vue +270 -0
- package/src/components/shared/data-display/SMessage.css +88 -0
- package/src/components/shared/data-display/SMessage.vue +152 -0
- package/src/components/shared/data-display/SMetricCard.css +32 -0
- package/src/components/shared/data-display/SMetricCard.vue +117 -0
- package/src/components/shared/data-display/SProgressBar.css +84 -0
- package/src/components/shared/data-display/SProgressBar.vue +114 -0
- package/src/components/shared/data-display/SProgressIndicator.css +59 -0
- package/src/components/shared/data-display/SProgressIndicator.vue +104 -0
- package/src/components/shared/data-display/SSectionHeader.css +84 -0
- package/src/components/shared/data-display/SSectionHeader.vue +58 -0
- package/src/components/shared/data-display/SStatus.css +90 -0
- package/src/components/shared/data-display/SStatus.vue +79 -0
- package/src/components/shared/data-display/STable.css +83 -0
- package/src/components/shared/data-display/STable.vue +339 -0
- package/src/components/shared/data-display/SToastContainer.css +53 -0
- package/src/components/shared/data-display/SToastContainer.vue +31 -0
- package/src/components/shared/data-display/STooltip.css +114 -0
- package/src/components/shared/data-display/STooltip.vue +550 -0
- package/src/components/shared/data-display/STooltipAnchor.css +21 -0
- package/src/components/shared/data-display/STooltipAnchor.vue +119 -0
- package/src/components/shared/data-display/SVirtualList.css +24 -0
- package/src/components/shared/data-display/SVirtualList.ts +19 -0
- package/src/components/shared/data-display/SVirtualList.vue +361 -0
- package/src/components/shared/data-display/json.ts +136 -0
- package/src/components/shared/data-display/table.ts +76 -0
- package/src/components/shared/database/SDataGrid.css +3 -0
- package/src/components/shared/database/SDataGrid.vue +464 -0
- package/src/components/shared/database/SSqlEditor.vue +363 -0
- package/src/components/shared/database/dataGrid.ts +61 -0
- package/src/components/shared/feedback/SAsyncState.css +26 -0
- package/src/components/shared/feedback/SAsyncState.vue +190 -0
- package/src/components/shared/graph/SGraphViewport.ts +1 -0
- package/src/components/shared/graph/SGraphViewport.vue +858 -0
- package/src/components/shared/graph/_internal/graphViewportContract.ts +362 -0
- package/src/components/shared/markdown.ts +169 -0
- package/src/components/shared/navigation/SBottomNav.vue +692 -0
- package/src/components/shared/navigation/SBreadcrumbs.css +26 -0
- package/src/components/shared/navigation/SBreadcrumbs.vue +155 -0
- package/src/components/shared/navigation/SCatalogNavigator.css +100 -0
- package/src/components/shared/navigation/SCatalogNavigator.vue +308 -0
- package/src/components/shared/navigation/STab.vue +363 -0
- package/src/components/shared/navigation/STabList.vue +442 -0
- package/src/components/shared/navigation/STabPanel.vue +165 -0
- package/src/components/shared/navigation/STabPanels.vue +33 -0
- package/src/components/shared/navigation/STabs.vue +617 -0
- package/src/components/shared/navigation/SWizardSteps.css +69 -0
- package/src/components/shared/navigation/SWizardSteps.vue +171 -0
- package/src/components/shared/navigation/_internal/SCatalogNavigatorGroup.vue +63 -0
- package/src/components/shared/persona/SPersonaProjectPicker.css +36 -0
- package/src/components/shared/persona/SPersonaProjectPicker.vue +361 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.css +109 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.vue +549 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.css +40 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +298 -0
- package/src/components/shared/persona/_internal/inventory.ts +110 -0
- package/src/components/shared/persona/_internal/labels.ts +79 -0
- package/src/components/shared/persona/presentation.ts +371 -0
- package/src/components/shared/persona/types.ts +256 -0
- package/src/composables/uiPreferencesReset.ts +52 -0
- package/src/composables/useClipboard.ts +37 -0
- package/src/composables/useDialogOverlay.ts +85 -0
- package/src/composables/useDisclosure.ts +40 -0
- package/src/composables/useFloatingPosition.ts +245 -0
- package/src/composables/useNotifier.ts +26 -0
- package/src/composables/useSidebarPanelState.ts +610 -0
- package/src/internal/codeEditorContract.ts +110 -0
- package/src/internal/codeEditorExtensions.ts +27 -0
- package/src/internal/feedbackPresentation.ts +136 -0
- package/src/internal/fieldSurface.ts +59 -0
- package/src/internal/focusNavigation.ts +69 -0
- package/src/internal/inlineTokenEditorContract.ts +136 -0
- package/src/internal/interactiveElement.ts +33 -0
- package/src/internal/layerStack.ts +202 -0
- package/src/internal/linkTarget.ts +256 -0
- package/src/internal/ownedAttrs.ts +464 -0
- package/src/internal/passiveContentContract.ts +129 -0
- package/src/internal/pointerInteractionLease.ts +81 -0
- package/src/internal/runtimeContract.ts +194 -0
- package/src/internal/runtimeTheme.ts +159 -0
- package/src/internal/semanticSizing.ts +60 -0
- package/src/internal/useMenuLayer.ts +129 -0
- package/src/stores/useNotifierStore.ts +69 -0
- package/src/styles/reference.css +4 -0
- package/src/styles/style.css +151 -0
- package/src/styles/tokens.css +836 -0
- package/src/theme.ts +449 -0
- package/src/types/highlight-js-lib.d.ts +76 -0
- 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
|
+
}
|