@purestack/ts-components 1.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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/dist/ts-components.d.mts +1662 -0
  4. package/dist/ts-components.d.mts.map +1 -0
  5. package/dist/ts-components.mjs +9200 -0
  6. package/dist/ts-components.mjs.map +1 -0
  7. package/package.json +130 -0
  8. package/src/defineComponents.ts +87 -0
  9. package/src/index.ts +270 -0
  10. package/src/registerStyles.ts +63 -0
  11. package/src/standard/alertBox/alertBox.ts +64 -0
  12. package/src/standard/autoId.ts +25 -0
  13. package/src/standard/badge/badge.ts +41 -0
  14. package/src/standard/badge/badgeStyle.ts +21 -0
  15. package/src/standard/barChart/barChart.ts +338 -0
  16. package/src/standard/barChart/barChartStyle.ts +81 -0
  17. package/src/standard/btn/btn.ts +191 -0
  18. package/src/standard/btn/btnStyle.ts +59 -0
  19. package/src/standard/btnGroup/btnGroup.ts +208 -0
  20. package/src/standard/btnGroup/btnGroupStyle.ts +85 -0
  21. package/src/standard/chart/cartesianChart.ts +99 -0
  22. package/src/standard/chart/chartDefaults.ts +25 -0
  23. package/src/standard/chart/chartUtils.ts +84 -0
  24. package/src/standard/classicLogo/classicLogo.ts +429 -0
  25. package/src/standard/classicLogo/classicLogoStyle.ts +292 -0
  26. package/src/standard/componentVariant.ts +153 -0
  27. package/src/standard/composer/composer.ts +1035 -0
  28. package/src/standard/composer/composerCanvasInitialStyle.ts +225 -0
  29. package/src/standard/composer/composerCanvasRevertStyle.ts +14 -0
  30. package/src/standard/composer/composerCanvasSharedStyle.ts +60 -0
  31. package/src/standard/composer/composerCanvasStyleTypes.ts +8 -0
  32. package/src/standard/composer/composerCanvasThemeStyle.ts +53 -0
  33. package/src/standard/composer/composerStyle.ts +156 -0
  34. package/src/standard/composer/emailHtmlPolicy.ts +474 -0
  35. package/src/standard/consent/consent.ts +173 -0
  36. package/src/standard/consent/consentStyle.ts +44 -0
  37. package/src/standard/contactForm/contactForm.ts +135 -0
  38. package/src/standard/contactForm/contactFormStyle.ts +116 -0
  39. package/src/standard/doughnutChart/doughnutChart.ts +527 -0
  40. package/src/standard/doughnutChart/doughnutChartStyle.ts +64 -0
  41. package/src/standard/dropFiles/dropFiles.ts +480 -0
  42. package/src/standard/dropFiles/dropFilesStyle.ts +171 -0
  43. package/src/standard/expandablePanel/expandablePanel.ts +106 -0
  44. package/src/standard/expandablePanel/expandablePanelStyle.ts +56 -0
  45. package/src/standard/flex/flex.ts +269 -0
  46. package/src/standard/flex/flexStyle.ts +556 -0
  47. package/src/standard/footer/footer.ts +93 -0
  48. package/src/standard/footer/footerStyle.ts +39 -0
  49. package/src/standard/form/autoCompleteInput.ts +567 -0
  50. package/src/standard/form/form.ts +205 -0
  51. package/src/standard/form/formCheck.ts +96 -0
  52. package/src/standard/form/formInputField.ts +184 -0
  53. package/src/standard/form/formSelectField.ts +152 -0
  54. package/src/standard/form/formStyle.ts +585 -0
  55. package/src/standard/form/multiAutoCompleteInput.ts +880 -0
  56. package/src/standard/grid/grid.ts +177 -0
  57. package/src/standard/grid/gridStyle.ts +56 -0
  58. package/src/standard/icon/icon.ts +96 -0
  59. package/src/standard/icon/iconStyle.ts +50 -0
  60. package/src/standard/landing/codeShowcase.ts +54 -0
  61. package/src/standard/landing/comparisonColumn.ts +30 -0
  62. package/src/standard/landing/comparisonFeature.ts +22 -0
  63. package/src/standard/landing/comparisonTable.ts +38 -0
  64. package/src/standard/landing/ctaSection.ts +59 -0
  65. package/src/standard/landing/featureCard.ts +41 -0
  66. package/src/standard/landing/landing.ts +52 -0
  67. package/src/standard/landing/landingBand.ts +217 -0
  68. package/src/standard/landing/landingSection.ts +43 -0
  69. package/src/standard/landing/landingTypes.ts +141 -0
  70. package/src/standard/landing/metricItem.ts +25 -0
  71. package/src/standard/landing/metricStrip.ts +20 -0
  72. package/src/standard/lineChart/lineChart.ts +462 -0
  73. package/src/standard/lineChart/lineChartStyle.ts +91 -0
  74. package/src/standard/logo/logo.ts +192 -0
  75. package/src/standard/logo/logoStyle.ts +164 -0
  76. package/src/standard/modal/modal.ts +171 -0
  77. package/src/standard/modal/modalStore.ts +154 -0
  78. package/src/standard/modal/modalStyle.ts +190 -0
  79. package/src/standard/navMenu/navMenu.ts +278 -0
  80. package/src/standard/navMenu/navMenuStyle.ts +323 -0
  81. package/src/standard/pageLinks/pageLinks.ts +90 -0
  82. package/src/standard/pageScript/pageScript.ts +155 -0
  83. package/src/standard/pageToc/pageToc.ts +154 -0
  84. package/src/standard/pageToc/pageTocStyle.ts +372 -0
  85. package/src/standard/panel/panel.ts +55 -0
  86. package/src/standard/panel/panelStyle.ts +15 -0
  87. package/src/standard/pricing/pricing.ts +146 -0
  88. package/src/standard/searchBox/searchBox.ts +54 -0
  89. package/src/standard/searchBox/searchBoxStyle.ts +180 -0
  90. package/src/standard/sectionHeader/sectionHeader.ts +99 -0
  91. package/src/standard/signIn/signIn.ts +167 -0
  92. package/src/standard/signIn/signInStyle.ts +70 -0
  93. package/src/standard/tabs/tabs.ts +193 -0
  94. package/src/standard/tabs/tabsStyle.ts +231 -0
  95. package/src/standard/themeToggle/themeToggle.ts +33 -0
  96. package/src/standard/themeToggle/themeToggleStyle.ts +88 -0
  97. package/src/standard/toast/toastHost.ts +42 -0
  98. package/src/standard/toast/toastStore.ts +50 -0
  99. package/src/standard/topBar/topBar.ts +101 -0
  100. package/src/standard/topBar/topBarStyle.ts +169 -0
  101. package/src/standard/virtualList/variableVirtualTable.ts +371 -0
  102. package/src/standard/virtualList/virtualList.ts +423 -0
  103. package/src/standard/virtualList/virtualListStyle.ts +34 -0
  104. package/src/standard/virtualList/virtualTable.ts +179 -0
  105. package/src/standard/virtualList/virtualTableStyle.ts +24 -0
  106. package/tsconfig.json +23 -0
@@ -0,0 +1,101 @@
1
+ import type { SiteLogoConfig, TsSsgContext } from '@purestack/ts-common'
2
+
3
+ import { resolveTsSsgContext } from '@purestack/ts-common'
4
+ import type { SemanticTone } from '@purestack/ts-style'
5
+ import {
6
+ type ComputedRef,
7
+ computed,
8
+ defineComponent,
9
+ html,
10
+ type RefOrValue,
11
+ unref,
12
+ } from 'regor'
13
+ import {
14
+ type ComponentVariant,
15
+ type ComponentVariantMode,
16
+ resolveComponentClasses,
17
+ } from '../componentVariant'
18
+
19
+ export interface TopBar {
20
+ siteLogo: SiteLogoConfig
21
+ logoComponent?: RefOrValue<string>
22
+ resolvedLogoComponent?: ComputedRef<string>
23
+ tone?: RefOrValue<SemanticTone>
24
+ variant?: RefOrValue<ComponentVariant>
25
+ signInAvatarSrc?: RefOrValue<string>
26
+ signInAvatarAlt?: RefOrValue<string>
27
+ classes?: ComputedRef<string>
28
+ searchEnabled?: boolean
29
+ signInEnabled?: boolean
30
+ }
31
+
32
+ const DEFAULT_TOP_BAR_VARIANT: ComponentVariant = 'surfaceAlt'
33
+ const DEFAULT_TOP_BAR_VARIANT_MODE: ComponentVariantMode = 'stateless'
34
+
35
+ function resolveTopBar(context: TsSsgContext, props: TopBar): TopBar {
36
+ return {
37
+ ...props,
38
+ siteLogo: context.site.logo,
39
+ resolvedLogoComponent: computed(
40
+ () =>
41
+ unref(props.logoComponent) ?? context.site.logo.component ?? 'SiteLogo',
42
+ ),
43
+ searchEnabled: context.site.pagefind?.enabled === true,
44
+ classes: computed(() =>
45
+ resolveComponentClasses(props, {
46
+ defaultVariant: DEFAULT_TOP_BAR_VARIANT,
47
+ defaultVariantMode: DEFAULT_TOP_BAR_VARIANT_MODE,
48
+ }),
49
+ ),
50
+ }
51
+ }
52
+
53
+ const topBarTemplate = html`<input
54
+ class="doc-nav-toggle"
55
+ id="doc-nav-toggle"
56
+ type="checkbox"
57
+ autocomplete="off"
58
+ aria-hidden="true"/>
59
+ <header class="topbar" :class="classes" r-inherit>
60
+ <Flex align="center">
61
+ <div :is="resolvedLogoComponent" class="flex-none" :config="siteLogo"></div>
62
+ <SearchBox
63
+ r-if="searchEnabled"
64
+ class="topbar__search flex-auto rounded-md tone-text-surface"
65
+ variant="none"/>
66
+ <Flex class="topbar__controls flex-none" align="center" justify="end">
67
+ <ThemeToggle/>
68
+ <SignIn
69
+ r-if="signInEnabled"
70
+ class="topbar__account"
71
+ :avatarSrc="signInAvatarSrc"
72
+ :avatarAlt="signInAvatarAlt"/>
73
+ <label
74
+ class="topbar__icon topbar__toggle"
75
+ for="doc-nav-toggle"
76
+ role="button"
77
+ aria-label="Toggle navigation"
78
+ ></label>
79
+ </Flex>
80
+ </Flex>
81
+ </header>`
82
+
83
+ function defineTopBarComponent() {
84
+ return defineComponent<TopBar>(topBarTemplate, {
85
+ props: [
86
+ 'class',
87
+ 'logoComponent',
88
+ 'tone',
89
+ 'variant',
90
+ 'signInAvatarSrc',
91
+ 'signInAvatarAlt',
92
+ 'signInEnabled',
93
+ 'searchEnabled',
94
+ ],
95
+ context: (head) => resolveTopBar(resolveTsSsgContext(head), head.props),
96
+ })
97
+ }
98
+
99
+ export function defineTopBarComponents() {
100
+ return { topBar: defineTopBarComponent() }
101
+ }
@@ -0,0 +1,169 @@
1
+ import type { ThemePalette } from '@purestack/ts-style'
2
+ import {
3
+ BREAKPOINTS,
4
+ mediaBelow,
5
+ mediaMax,
6
+ mediaMin,
7
+ styleBuilder,
8
+ type ThemeMode,
9
+ themes,
10
+ } from '@purestack/ts-style'
11
+
12
+ export function registerTopBarStyles() {
13
+ themes.forEach((theme, palette) => {
14
+ registerTopBarShellStyles(theme, palette)
15
+ registerTopBarToggleStyles(theme, palette)
16
+ })
17
+ }
18
+
19
+ export function getTopBarToggleVisibleSelectors(targetSelector: string) {
20
+ return [
21
+ { selector: `.template-doc--nav-drawer ${targetSelector}` },
22
+ {
23
+ selector: `.template-doc--has-nav ${targetSelector}`,
24
+ media: mediaBelow(BREAKPOINTS.lg),
25
+ },
26
+ ]
27
+ }
28
+
29
+ export function getTopBarToggleHiddenSelectors(targetSelector: string) {
30
+ return [
31
+ {
32
+ selector: `body:not(.template-doc--nav-drawer):not(.template-doc--has-nav) ${targetSelector}`,
33
+ },
34
+ {
35
+ selector: `.template-doc--has-nav:not(.template-doc--nav-drawer) ${targetSelector}`,
36
+ media: mediaMin(BREAKPOINTS.lg),
37
+ },
38
+ ]
39
+ }
40
+
41
+ export function registerTopBarShellStyles(
42
+ theme: ThemeMode,
43
+ palette: ThemePalette,
44
+ ) {
45
+ styleBuilder
46
+ .select('.topbar', theme)
47
+ .padding('1rem')
48
+ .position('sticky')
49
+ .top('0')
50
+ .overflowX('clip')
51
+ .zIndex(40)
52
+ .backdropFilter('blur(0.625rem)')
53
+ .borderTopWidth('0 !important')
54
+ .borderRightWidth('0 !important')
55
+ .borderBottomWidth('1px !important')
56
+ .borderBottomStyle('solid !important')
57
+ .borderLeftWidth('0 !important')
58
+ .borderRadius('0 !important')
59
+
60
+ styleBuilder.select('.topbar__controls', theme).marginLeft('auto')
61
+ styleBuilder
62
+ .select('.topbar', theme)
63
+ .media(mediaBelow(BREAKPOINTS.sm))
64
+ .padding('0.75rem')
65
+ styleBuilder
66
+ .select('.topbar > .flex, .topbar__controls', theme)
67
+ .media(mediaBelow(BREAKPOINTS.sm))
68
+ .gap('0.5rem')
69
+ registerTopBarAccountVisibilityStyles(theme)
70
+
71
+ styleBuilder
72
+ .select('.topbar__icon', theme)
73
+ .width('2.625rem')
74
+ .height('2.625rem')
75
+ .borderRadius(palette.radii.pill)
76
+ .display('grid')
77
+ .placeItems('center')
78
+ .border('1px solid transparent')
79
+ .background('transparent')
80
+ .cursor('pointer')
81
+ .position('relative')
82
+ .padding('0')
83
+ .color(palette.current.text.subtle)
84
+ }
85
+
86
+ function registerTopBarAccountVisibilityStyles(theme: ThemeMode) {
87
+ styleBuilder.select('.topbar__account', theme).display('none')
88
+ for (const rule of getTopBarToggleHiddenSelectors('.topbar__account')) {
89
+ let selector = styleBuilder.select(rule.selector, theme)
90
+ if (rule.media) selector = selector.media(rule.media)
91
+ selector.display('inline-block')
92
+ }
93
+ }
94
+
95
+ export function registerTopBarToggleStyles(
96
+ theme: ThemeMode,
97
+ palette: ThemePalette,
98
+ ) {
99
+ registerTopBarToggleVisibilityStyles(theme, palette)
100
+ registerTopBarToggleGlyphStyles(theme)
101
+ registerTopBarToggleCheckedStateStyles(theme)
102
+ registerTopBarResponsiveSearchStyles(theme)
103
+ }
104
+
105
+ function registerTopBarToggleVisibilityStyles(
106
+ theme: ThemeMode,
107
+ palette: ThemePalette,
108
+ ) {
109
+ styleBuilder
110
+ .select('.topbar__toggle', theme)
111
+ .background(palette.current.surface.rest.background)
112
+ .borderColor(palette.current.border.default)
113
+ .color(palette.current.text.subtle)
114
+ .display('none')
115
+ styleBuilder
116
+ .select('.template-doc--nav-drawer .topbar__toggle', theme)
117
+ .display('grid')
118
+ styleBuilder
119
+ .select('.template-doc--has-nav .topbar__toggle', theme)
120
+ .media(mediaBelow(BREAKPOINTS.lg))
121
+ .display('grid')
122
+ styleBuilder
123
+ .select('.topbar__toggle:focus-visible', theme)
124
+ .outline(`2px solid ${palette.current.border.focus}`)
125
+ }
126
+
127
+ function registerTopBarToggleGlyphStyles(theme: ThemeMode) {
128
+ styleBuilder
129
+ .select('.topbar__toggle::before', theme)
130
+ .content('""')
131
+ .width('1.125rem')
132
+ .height('0.125rem')
133
+ .background('currentColor')
134
+ .position('absolute')
135
+ .top('0.875rem')
136
+ .left('0.75rem')
137
+ .transition('transform 200ms ease, top 200ms ease')
138
+ .boxShadow('0 0.375rem 0 0 currentColor')
139
+ styleBuilder
140
+ .select('.topbar__toggle::after', theme)
141
+ .content('""')
142
+ .width('1.125rem')
143
+ .height('0.125rem')
144
+ .background('currentColor')
145
+ .position('absolute')
146
+ .top('1.625rem')
147
+ .left('0.75rem')
148
+ .transition('transform 200ms ease, top 200ms ease')
149
+ }
150
+
151
+ function registerTopBarToggleCheckedStateStyles(theme: ThemeMode) {
152
+ styleBuilder.select('.doc-nav-toggle', theme).display('none')
153
+ styleBuilder
154
+ .select('.doc-nav-toggle:checked ~ .topbar .topbar__toggle::before', theme)
155
+ .top('1.25rem')
156
+ .transform('rotate(45deg)')
157
+ .boxShadow('none')
158
+ styleBuilder
159
+ .select('.doc-nav-toggle:checked ~ .topbar .topbar__toggle::after', theme)
160
+ .top('1.25rem')
161
+ .transform('rotate(-45deg)')
162
+ }
163
+
164
+ function registerTopBarResponsiveSearchStyles(theme: ThemeMode) {
165
+ styleBuilder
166
+ .select('.topbar__search', theme)
167
+ .media(mediaMax(BREAKPOINTS.sm))
168
+ .display('none')
169
+ }
@@ -0,0 +1,371 @@
1
+ import {
2
+ type ComponentHead,
3
+ type ComputedRef,
4
+ computed,
5
+ defineComponent,
6
+ html,
7
+ observe,
8
+ type RefOrValue,
9
+ ref,
10
+ sref,
11
+ unref,
12
+ } from 'regor'
13
+ import type { VirtualListRow } from './virtualList'
14
+ import type { VirtualTable } from './virtualTable'
15
+ // known limitation: vertical scroll bar is not synced perfectly to the mouse pointer during scroll. eiter fix it or delete this component!
16
+ export interface VariableVirtualTable extends VirtualTable {
17
+ estimateHeight?: RefOrValue<number | string>
18
+ viewportElement?: ReturnType<typeof sref<HTMLElement | null>>
19
+ unmounted?: () => void
20
+ }
21
+
22
+ const variableVirtualTableTemplate = html`<div
23
+ class="virtual-table variable-virtual-table"
24
+ :style="viewportStyle"
25
+ :ref="viewportElement"
26
+ @scroll="handleScroll"
27
+ >
28
+ <table class="virtual-table__table" :style="tableStyle">
29
+ <colgroup :is="colGroupComponent" r-if="hasColGroupComponent"></colgroup>
30
+ <thead :is="headerComponent" r-if="hasHeaderComponent"></thead>
31
+ <tbody>
32
+ <tr class="virtual-table__spacer-row">
33
+ <td :style="topSpacerStyle"></td>
34
+ </tr>
35
+ <tr
36
+ :is="rowComponent"
37
+ :data-virtual-row-index="row.index"
38
+ :item="row.item"
39
+ :index="row.index"
40
+ r-for="row in visibleRows"
41
+ ></tr>
42
+ <tr class="virtual-table__spacer-row">
43
+ <td :style="bottomSpacerStyle"></td>
44
+ </tr>
45
+ </tbody>
46
+ <tfoot :is="footerComponent" r-if="hasFooterComponent"></tfoot>
47
+ </table>
48
+ </div>`
49
+
50
+ function defineVariableVirtualTableComponent() {
51
+ return defineComponent<VariableVirtualTable>(variableVirtualTableTemplate, {
52
+ props: [
53
+ 'items',
54
+ 'height',
55
+ 'estimateHeight',
56
+ 'overscan',
57
+ 'rowComponent',
58
+ 'colGroupComponent',
59
+ 'headerComponent',
60
+ 'footerComponent',
61
+ ],
62
+ context: (head) => resolveVariableVirtualTable(head),
63
+ })
64
+ }
65
+
66
+ export function defineVariableVirtualTableComponents() {
67
+ return {
68
+ variableVirtualTable: defineVariableVirtualTableComponent(),
69
+ }
70
+ }
71
+
72
+ class VariableVirtualTableContext implements VariableVirtualTable {
73
+ readonly items?: RefOrValue<unknown[]>
74
+ readonly height?: RefOrValue<number | string>
75
+ readonly estimateHeight?: RefOrValue<number | string>
76
+ readonly overscan?: RefOrValue<number | string>
77
+ readonly rowComponent?: RefOrValue<string>
78
+ readonly colGroupComponent?: RefOrValue<string>
79
+ readonly headerComponent?: RefOrValue<string>
80
+ readonly footerComponent?: RefOrValue<string>
81
+ readonly scrollTop = ref(0)
82
+ readonly viewportElement = sref<HTMLElement | null>(null)
83
+ readonly visibleRows: ComputedRef<VirtualListRow[]>
84
+ readonly viewportStyle: ComputedRef<Record<string, string>>
85
+ readonly tableStyle: ComputedRef<Record<string, string>>
86
+ readonly topSpacerStyle: ComputedRef<Record<string, string>>
87
+ readonly bottomSpacerStyle: ComputedRef<Record<string, string>>
88
+ readonly hasColGroupComponent: ComputedRef<boolean>
89
+ readonly hasHeaderComponent: ComputedRef<boolean>
90
+ readonly hasFooterComponent: ComputedRef<boolean>
91
+ private readonly rowHeights: number[] = []
92
+ private rowWindow = new Map<number, VirtualListRow>()
93
+ private readonly rowElements = new Map<number, HTMLElement>()
94
+ private rowObserver?: ResizeObserver
95
+ private bodyObserver?: MutationObserver
96
+ private readonly measurementVersion = ref(0)
97
+ private measuredHeightTotal = 0
98
+ private measuredRowCount = 0
99
+ private pendingFrame = 0
100
+ private scrollEndTimer: ReturnType<typeof setTimeout> | undefined
101
+ private isScrolling = false
102
+ private hasDeferredMeasurements = false
103
+ private readonly resolvedItems: ComputedRef<unknown[]>
104
+ private readonly resolvedHeight: ComputedRef<number>
105
+ private readonly resolvedEstimateHeight: ComputedRef<number>
106
+ private readonly resolvedOverscan: ComputedRef<number>
107
+ private readonly offsets: ComputedRef<number[]>
108
+ private readonly startIndex: ComputedRef<number>
109
+ private readonly firstVisibleIndex: ComputedRef<number>
110
+ private readonly endIndex: ComputedRef<number>
111
+ private readonly pendingIndexes = new Set<number>()
112
+
113
+ constructor(props: VariableVirtualTable) {
114
+ this.items = props.items
115
+ this.height = props.height
116
+ this.estimateHeight = props.estimateHeight
117
+ this.overscan = props.overscan
118
+ this.rowComponent = props.rowComponent
119
+ this.colGroupComponent = props.colGroupComponent
120
+ this.headerComponent = props.headerComponent
121
+ this.footerComponent = props.footerComponent
122
+ this.resolvedItems = computed(() => {
123
+ const value = unref(props.items)
124
+ return Array.isArray(value) ? value : []
125
+ })
126
+ this.resolvedHeight = computed(() =>
127
+ resolvePositiveNumber(props.height, 560),
128
+ )
129
+ this.resolvedEstimateHeight = computed(() =>
130
+ resolvePositiveNumber(props.estimateHeight ?? props.itemHeight, 56),
131
+ )
132
+ this.resolvedOverscan = computed(() =>
133
+ Math.trunc(resolvePositiveNumber(props.overscan, 6)),
134
+ )
135
+ this.offsets = computed(() => {
136
+ this.measurementVersion()
137
+ const source = this.resolvedItems()
138
+ const estimate = this.currentEstimateHeight()
139
+ const offsets = new Array<number>(source.length + 1)
140
+ offsets[0] = 0
141
+ for (let index = 0; index < source.length; index += 1)
142
+ offsets[index + 1] =
143
+ (offsets[index] ?? 0) + (this.rowHeights[index] ?? estimate)
144
+
145
+ return offsets
146
+ })
147
+ this.firstVisibleIndex = computed(() =>
148
+ findOffsetIndex(this.offsets(), this.scrollTop()),
149
+ )
150
+ this.startIndex = computed(() =>
151
+ Math.max(0, this.firstVisibleIndex() - this.resolvedOverscan()),
152
+ )
153
+ this.visibleRows = computed(() => this.resolveVisibleRows())
154
+ this.endIndex = computed(() => {
155
+ const rows = this.visibleRows()
156
+ const last = rows[rows.length - 1]
157
+ return last ? last.index + 1 : this.startIndex()
158
+ })
159
+ this.viewportStyle = computed<Record<string, string>>(() => ({
160
+ height: `${this.resolvedHeight()}px`,
161
+ overflow: 'auto',
162
+ width: '100%',
163
+ maxWidth: '100%',
164
+ boxSizing: 'border-box',
165
+ minWidth: '0',
166
+ }))
167
+ this.tableStyle = computed<Record<string, string>>(() => ({
168
+ width: 'max-content',
169
+ minWidth: '100%',
170
+ borderCollapse: 'separate',
171
+ borderSpacing: '0',
172
+ }))
173
+ this.topSpacerStyle = computed<Record<string, string>>(() => ({
174
+ height: `${this.offsets()[this.startIndex()] ?? 0}px`,
175
+ padding: '0',
176
+ border: '0',
177
+ }))
178
+ this.bottomSpacerStyle = computed<Record<string, string>>(() => {
179
+ const offsets = this.offsets()
180
+ const totalHeight = offsets[offsets.length - 1] ?? 0
181
+ const visibleBottom = offsets[this.endIndex()] ?? totalHeight
182
+ return {
183
+ height: `${Math.max(0, totalHeight - visibleBottom)}px`,
184
+ padding: '0',
185
+ border: '0',
186
+ }
187
+ })
188
+ this.hasColGroupComponent = computed(() => !!unref(props.colGroupComponent))
189
+ this.hasHeaderComponent = computed(() => !!unref(props.headerComponent))
190
+ this.hasFooterComponent = computed(() => !!unref(props.footerComponent))
191
+ observe(this.viewportElement, (element) => this.observeViewport(element))
192
+ }
193
+
194
+ handleScroll = (event: Event) => {
195
+ const target = event.currentTarget
196
+ if (!(target instanceof HTMLElement)) return
197
+
198
+ this.scrollTop(target.scrollTop)
199
+ this.isScrolling = true
200
+ if (this.scrollEndTimer !== undefined) clearTimeout(this.scrollEndTimer)
201
+ this.scrollEndTimer = setTimeout(() => {
202
+ this.scrollEndTimer = undefined
203
+ this.isScrolling = false
204
+ if (!this.hasDeferredMeasurements) return
205
+
206
+ this.hasDeferredMeasurements = false
207
+ this.measurementVersion(this.measurementVersion() + 1)
208
+ }, 120)
209
+ }
210
+
211
+ unmounted = () => {
212
+ if (this.pendingFrame) cancelAnimationFrame(this.pendingFrame)
213
+ if (this.scrollEndTimer !== undefined) clearTimeout(this.scrollEndTimer)
214
+ this.rowObserver?.disconnect()
215
+ this.bodyObserver?.disconnect()
216
+ this.rowElements.clear()
217
+ }
218
+
219
+ private resolveVisibleRows() {
220
+ const source = this.resolvedItems()
221
+ const offsets = this.offsets()
222
+ const scrollTop = this.scrollTop()
223
+ const overscan = this.resolvedOverscan()
224
+ // Keep the two boundaries on the same scroll-position snapshot.
225
+ const start = Math.max(0, findOffsetIndex(offsets, scrollTop) - overscan)
226
+ const viewportBottom = scrollTop + this.resolvedHeight()
227
+ let end = findOffsetIndex(offsets, viewportBottom) + overscan + 1
228
+ end = Math.min(source.length, Math.max(start, end))
229
+
230
+ const rows: VirtualListRow[] = []
231
+ const nextWindow = new Map<number, VirtualListRow>()
232
+ for (let index = start; index < end; index += 1) {
233
+ const previous = this.rowWindow.get(index)
234
+ const row =
235
+ previous && previous.item === source[index]
236
+ ? previous
237
+ : { index, item: source[index] }
238
+ rows.push(row)
239
+ nextWindow.set(index, row)
240
+ }
241
+ this.rowWindow = nextWindow
242
+
243
+ return rows
244
+ }
245
+
246
+ private observeViewport(element: HTMLElement | null) {
247
+ this.rowObserver?.disconnect()
248
+ this.bodyObserver?.disconnect()
249
+ this.rowElements.clear()
250
+ if (!element) return
251
+
252
+ this.rowObserver = new ResizeObserver((entries) => {
253
+ for (const entry of entries) {
254
+ const row = entry.target as HTMLElement
255
+ const index = Number(row.getAttribute('data-virtual-row-index'))
256
+ if (this.rowElements.get(index) === row) this.queueMeasurement(index)
257
+ }
258
+ })
259
+ this.bodyObserver = new MutationObserver(() =>
260
+ this.syncMountedRows(element),
261
+ )
262
+ this.bodyObserver.observe(element, { childList: true, subtree: true })
263
+ this.syncMountedRows(element)
264
+ }
265
+
266
+ private syncMountedRows(viewport: HTMLElement) {
267
+ const next = new Map<number, HTMLElement>()
268
+ for (const row of viewport.querySelectorAll<HTMLElement>(
269
+ ':scope > table > tbody > tr[data-virtual-row-index]',
270
+ )) {
271
+ const index = Number(row.getAttribute('data-virtual-row-index'))
272
+ if (Number.isInteger(index) && index >= 0) next.set(index, row)
273
+ }
274
+ for (const [index, element] of this.rowElements) {
275
+ if (next.get(index) !== element) {
276
+ this.rowObserver?.unobserve(element)
277
+ this.rowElements.delete(index)
278
+ }
279
+ }
280
+ for (const [index, element] of next) {
281
+ if (this.rowElements.get(index) === element) continue
282
+ this.rowElements.set(index, element)
283
+ this.rowObserver?.observe(element)
284
+ this.queueMeasurement(index)
285
+ }
286
+ }
287
+
288
+ private queueMeasurement(index: number) {
289
+ this.pendingIndexes.add(index)
290
+ if (this.pendingFrame) return
291
+
292
+ this.pendingFrame = requestAnimationFrame(() => {
293
+ this.pendingFrame = 0
294
+ this.flushMeasurements()
295
+ })
296
+ }
297
+
298
+ private flushMeasurements() {
299
+ let changed = false
300
+
301
+ for (const index of this.pendingIndexes) {
302
+ const element = this.rowElements.get(index)
303
+ if (!element) continue
304
+
305
+ const previous = this.rowHeights[index] ?? this.currentEstimateHeight()
306
+ const next = element.getBoundingClientRect().height
307
+ if (!Number.isFinite(next) || next <= 0) continue
308
+ if (Math.abs(previous - next) < 0.5) continue
309
+
310
+ this.updateMeasuredAverage(index, next)
311
+ this.rowHeights[index] = next
312
+ changed = true
313
+ }
314
+
315
+ this.pendingIndexes.clear()
316
+ if (!changed) return
317
+
318
+ if (this.isScrolling) {
319
+ this.hasDeferredMeasurements = true
320
+ return
321
+ }
322
+
323
+ this.measurementVersion(this.measurementVersion() + 1)
324
+ }
325
+
326
+ private currentEstimateHeight() {
327
+ if (this.measuredRowCount === 0) return this.resolvedEstimateHeight()
328
+ return this.measuredHeightTotal / this.measuredRowCount
329
+ }
330
+
331
+ private updateMeasuredAverage(index: number, next: number) {
332
+ const previous = this.rowHeights[index]
333
+ if (previous === undefined) {
334
+ this.measuredRowCount += 1
335
+ this.measuredHeightTotal += next
336
+ return
337
+ }
338
+
339
+ this.measuredHeightTotal += next - previous
340
+ }
341
+ }
342
+
343
+ function resolveVariableVirtualTable(
344
+ head: ComponentHead<VariableVirtualTable>,
345
+ ) {
346
+ head.autoProps = false
347
+ return new VariableVirtualTableContext(head.props)
348
+ }
349
+
350
+ function resolvePositiveNumber(
351
+ value: RefOrValue<number | string> | undefined,
352
+ fallback: number,
353
+ ) {
354
+ const raw = unref(value)
355
+ const parsed = typeof raw === 'number' ? raw : Number.parseFloat(String(raw))
356
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
357
+ }
358
+
359
+ function findOffsetIndex(offsets: number[], scrollTop: number) {
360
+ if (offsets.length <= 1) return 0
361
+
362
+ let low = 0
363
+ let high = offsets.length - 1
364
+ while (low < high) {
365
+ const middle = Math.floor((low + high) / 2)
366
+ if ((offsets[middle] ?? 0) <= scrollTop) low = middle + 1
367
+ else high = middle
368
+ }
369
+
370
+ return Math.max(0, low - 1)
371
+ }