@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,423 @@
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
+
14
+ export interface VirtualListRow {
15
+ index: number
16
+ item: unknown
17
+ }
18
+
19
+ export interface VirtualList {
20
+ items?: RefOrValue<unknown[]>
21
+ height?: RefOrValue<number | string>
22
+ itemHeight?: RefOrValue<number | string>
23
+ overscan?: RefOrValue<number | string>
24
+ rowComponent?: RefOrValue<string>
25
+ scrollTop?: ReturnType<typeof ref<number>>
26
+ visibleRows?: ComputedRef<VirtualListRow[]>
27
+ viewportStyle?: ComputedRef<Record<string, string>>
28
+ spacerStyle?: ComputedRef<Record<string, string>>
29
+ windowStyle?: ComputedRef<Record<string, string>>
30
+ itemStyle?: ComputedRef<Record<string, string>>
31
+ handleScroll?: (event: Event) => void
32
+ resolvedRowComponent?: ComputedRef<string>
33
+ }
34
+
35
+ export interface VariableVirtualList extends VirtualList {
36
+ estimateHeight?: RefOrValue<number | string>
37
+ rowElementRefs?: Array<ReturnType<typeof sref<HTMLElement | null>>>
38
+ viewportElement?: ReturnType<typeof sref<HTMLElement | null>>
39
+ unmounted?: () => void
40
+ }
41
+
42
+ const virtualListTemplate = html`<div class="virtual-list" :style="viewportStyle" @scroll="handleScroll">
43
+ <div class="virtual-list__spacer" :style="spacerStyle">
44
+ <div class="virtual-list__window" :style="windowStyle">
45
+ <div
46
+ class="virtual-list__item"
47
+ :style="itemStyle"
48
+ r-for="row in visibleRows"
49
+ >
50
+ <div
51
+ :is="resolvedRowComponent"
52
+ :item="row.item"
53
+ :index="row.index"
54
+ ></div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>`
59
+
60
+ function defineVirtualListComponent() {
61
+ return defineComponent<VirtualList>(virtualListTemplate, {
62
+ props: ['items', 'height', 'itemHeight', 'overscan', 'rowComponent'],
63
+ context: (head) => resolveVirtualList(head),
64
+ })
65
+ }
66
+
67
+ const variableVirtualListTemplate = html`<div
68
+ class="virtual-list variable-virtual-list"
69
+ :style="viewportStyle"
70
+ :ref="viewportElement"
71
+ @scroll="handleScroll"
72
+ >
73
+ <div class="virtual-list__spacer" :style="spacerStyle">
74
+ <div class="virtual-list__window" :style="windowStyle">
75
+ <div
76
+ class="virtual-list__item"
77
+ :ref="rowElementRefs[row.index]"
78
+ r-for="row in visibleRows"
79
+ >
80
+ <div
81
+ :is="resolvedRowComponent"
82
+ :item="row.item"
83
+ :index="row.index"
84
+ ></div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>`
89
+
90
+ function defineVariableVirtualListComponent() {
91
+ return defineComponent<VariableVirtualList>(variableVirtualListTemplate, {
92
+ props: ['items', 'height', 'estimateHeight', 'overscan', 'rowComponent'],
93
+ context: (head) => resolveVariableVirtualList(head),
94
+ })
95
+ }
96
+
97
+ export function defineVirtualListComponents() {
98
+ return {
99
+ virtualList: defineVirtualListComponent(),
100
+ variableVirtualList: defineVariableVirtualListComponent(),
101
+ }
102
+ }
103
+
104
+ function resolveVirtualList(head: ComponentHead<VirtualList>) {
105
+ const props = head.props
106
+ const scrollTop = ref(0)
107
+ const itemHeight = computed(() => resolvePositiveNumber(props.itemHeight, 44))
108
+ const height = computed(() => resolvePositiveNumber(props.height, 560))
109
+ const overscan = computed(() =>
110
+ Math.trunc(resolvePositiveNumber(props.overscan, 6)),
111
+ )
112
+ const items = computed(() => {
113
+ const value = unref(props.items)
114
+ return Array.isArray(value) ? value : []
115
+ })
116
+ const startIndex = computed(() =>
117
+ Math.max(0, Math.floor(scrollTop() / itemHeight()) - overscan()),
118
+ )
119
+ const visibleCount = computed(
120
+ () => Math.ceil(height() / itemHeight()) + overscan() * 2,
121
+ )
122
+ const visibleRows = computed(() => {
123
+ const source = items()
124
+ const start = startIndex()
125
+ const end = Math.min(source.length, start + visibleCount())
126
+ const rows: VirtualListRow[] = []
127
+ for (let index = start; index < end; index += 1)
128
+ rows.push({ index, item: source[index] })
129
+
130
+ return rows
131
+ })
132
+
133
+ return {
134
+ ...props,
135
+ scrollTop,
136
+ visibleRows,
137
+ viewportStyle: computed<Record<string, string>>(() => ({
138
+ height: `${height()}px`,
139
+ })),
140
+ spacerStyle: computed<Record<string, string>>(() => ({
141
+ height: `${items().length * itemHeight()}px`,
142
+ })),
143
+ windowStyle: computed<Record<string, string>>(() => ({
144
+ transform: `translateY(${startIndex() * itemHeight()}px)`,
145
+ })),
146
+ itemStyle: computed<Record<string, string>>(() => ({
147
+ height: `${itemHeight()}px`,
148
+ })),
149
+ resolvedRowComponent: computed(() => {
150
+ const component = unref(props.rowComponent)
151
+ return typeof component === 'string' && component.trim()
152
+ ? component
153
+ : 'div'
154
+ }),
155
+ handleScroll: (event: Event) => {
156
+ const target = event.currentTarget
157
+ if (target instanceof HTMLElement) scrollTop(target.scrollTop)
158
+ },
159
+ }
160
+ }
161
+
162
+ function resolvePositiveNumber(
163
+ value: RefOrValue<number | string> | undefined,
164
+ fallback: number,
165
+ ) {
166
+ const raw = unref(value)
167
+ const parsed = typeof raw === 'number' ? raw : Number.parseFloat(String(raw))
168
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
169
+ }
170
+
171
+ class VariableVirtualListContext {
172
+ readonly scrollTop = ref(0)
173
+ readonly viewportElement = sref<HTMLElement | null>(null)
174
+ readonly rowElementRefs: Array<ReturnType<typeof sref<HTMLElement | null>>> =
175
+ []
176
+ readonly visibleRows: ComputedRef<VirtualListRow[]>
177
+ readonly viewportStyle: ComputedRef<Record<string, string>>
178
+ readonly spacerStyle: ComputedRef<Record<string, string>>
179
+ readonly windowStyle: ComputedRef<Record<string, string>>
180
+ readonly resolvedRowComponent: ComputedRef<string>
181
+ private readonly rowHeights: number[] = []
182
+ private rowWindow = new Map<number, VirtualListRow>()
183
+ private readonly rowObservers = new Map<number, ResizeObserver>()
184
+ private readonly measurementVersion = ref(0)
185
+ private measuredHeightTotal = 0
186
+ private measuredRowCount = 0
187
+ private readonly resolvedItems: ComputedRef<unknown[]>
188
+ private readonly resolvedHeight: ComputedRef<number>
189
+ private readonly resolvedEstimateHeight: ComputedRef<number>
190
+ private readonly resolvedOverscan: ComputedRef<number>
191
+ private readonly offsets: ComputedRef<number[]>
192
+ private readonly startIndex: ComputedRef<number>
193
+ private readonly firstVisibleIndex: ComputedRef<number>
194
+ private readonly normalizedScrollTop: ComputedRef<number>
195
+ private pendingIndexes = new Set<number>()
196
+ private pendingFrame = 0
197
+ private scrollEndTimer: ReturnType<typeof setTimeout> | undefined
198
+ private isScrolling = false
199
+ private hasDeferredMeasurements = false
200
+
201
+ constructor(props: VariableVirtualList) {
202
+ this.resolvedItems = computed(() => {
203
+ const value = unref(props.items)
204
+ return Array.isArray(value) ? value : []
205
+ })
206
+ this.resolvedHeight = computed(() =>
207
+ resolvePositiveNumber(props.height, 560),
208
+ )
209
+ this.resolvedEstimateHeight = computed(() =>
210
+ resolvePositiveNumber(props.estimateHeight ?? props.itemHeight, 56),
211
+ )
212
+ this.resolvedOverscan = computed(() =>
213
+ Math.trunc(resolvePositiveNumber(props.overscan, 6)),
214
+ )
215
+ this.offsets = computed(() => {
216
+ this.measurementVersion()
217
+ const estimate = this.currentEstimateHeight()
218
+ const source = this.resolvedItems()
219
+ const offsets = new Array<number>(source.length + 1)
220
+ offsets[0] = 0
221
+ for (let index = 0; index < source.length; index += 1)
222
+ offsets[index + 1] =
223
+ (offsets[index] ?? 0) + (this.rowHeights[index] ?? estimate)
224
+
225
+ return offsets
226
+ })
227
+ this.normalizedScrollTop = computed(() => {
228
+ const value = this.scrollTop()
229
+ return value <= 0.5 ? 0 : value
230
+ })
231
+ this.firstVisibleIndex = computed(() =>
232
+ findOffsetIndex(this.offsets(), this.normalizedScrollTop()),
233
+ )
234
+ this.startIndex = computed(() =>
235
+ Math.max(0, this.firstVisibleIndex() - this.resolvedOverscan()),
236
+ )
237
+ this.visibleRows = computed(() => this.resolveVisibleRows())
238
+ this.viewportStyle = computed<Record<string, string>>(() => ({
239
+ height: `${this.resolvedHeight()}px`,
240
+ }))
241
+ this.spacerStyle = computed<Record<string, string>>(() => {
242
+ const offsets = this.offsets()
243
+ return {
244
+ height: `${offsets[offsets.length - 1] ?? 0}px`,
245
+ }
246
+ })
247
+ this.windowStyle = computed<Record<string, string>>(() => ({
248
+ transform: `translateY(${this.offsets()[this.startIndex()] ?? 0}px)`,
249
+ }))
250
+ this.resolvedRowComponent = computed(() => {
251
+ const component = unref(props.rowComponent)
252
+ return typeof component === 'string' && component.trim()
253
+ ? component
254
+ : 'div'
255
+ })
256
+ }
257
+
258
+ handleScroll = () => {
259
+ this.syncScrollTopFromViewport()
260
+
261
+ this.isScrolling = true
262
+ if (this.scrollEndTimer !== undefined) clearTimeout(this.scrollEndTimer)
263
+ this.scrollEndTimer = setTimeout(() => {
264
+ this.scrollEndTimer = undefined
265
+ this.isScrolling = false
266
+ this.syncScrollTopFromViewport()
267
+ if (!this.hasDeferredMeasurements) return
268
+
269
+ this.hasDeferredMeasurements = false
270
+ this.measurementVersion(this.measurementVersion() + 1)
271
+ }, 120)
272
+ }
273
+
274
+ unmounted = () => {
275
+ if (this.pendingFrame) cancelAnimationFrame(this.pendingFrame)
276
+ if (this.scrollEndTimer !== undefined) clearTimeout(this.scrollEndTimer)
277
+ for (const observer of this.rowObservers.values()) observer.disconnect()
278
+ this.rowObservers.clear()
279
+ }
280
+
281
+ private resolveVisibleRows() {
282
+ const source = this.resolvedItems()
283
+ const offsets = this.offsets()
284
+ const scrollTop = this.normalizedScrollTop()
285
+ const overscan = this.resolvedOverscan()
286
+ // Resolve both boundaries from the same scroll position. A separate
287
+ // computed start can still hold its previous value during notification.
288
+ const start = Math.max(0, findOffsetIndex(offsets, scrollTop) - overscan)
289
+ const viewportBottom = scrollTop + this.resolvedHeight()
290
+ let end = findOffsetIndex(offsets, viewportBottom) + overscan + 1
291
+ end = Math.min(source.length, Math.max(start, end))
292
+
293
+ const rows: VirtualListRow[] = []
294
+ const nextWindow = new Map<number, VirtualListRow>()
295
+ for (let index = start; index < end; index += 1) {
296
+ this.ensureRowRef(index)
297
+ const previous = this.rowWindow.get(index)
298
+ const row =
299
+ previous && previous.item === source[index]
300
+ ? previous
301
+ : { index, item: source[index] }
302
+ rows.push(row)
303
+ nextWindow.set(index, row)
304
+ }
305
+ this.rowWindow = nextWindow
306
+
307
+ return rows
308
+ }
309
+
310
+ private ensureRowRef(index: number) {
311
+ if (this.rowElementRefs[index]) return
312
+
313
+ const elementRef = sref<HTMLElement | null>(null)
314
+ this.rowElementRefs[index] = elementRef
315
+ observe(elementRef, (element) => {
316
+ this.observeRowElement(index, element)
317
+ })
318
+ }
319
+
320
+ private observeRowElement(index: number, element: HTMLElement | null) {
321
+ const existing = this.rowObservers.get(index)
322
+ if (existing) {
323
+ existing.disconnect()
324
+ this.rowObservers.delete(index)
325
+ }
326
+ if (!element) return
327
+
328
+ const observer = new ResizeObserver(() => {
329
+ this.queueMeasurement(index)
330
+ })
331
+ observer.observe(element)
332
+ this.rowObservers.set(index, observer)
333
+ this.queueMeasurement(index)
334
+ }
335
+
336
+ private queueMeasurement(index: number) {
337
+ this.pendingIndexes.add(index)
338
+ if (this.pendingFrame) return
339
+
340
+ this.pendingFrame = requestAnimationFrame(() => {
341
+ this.pendingFrame = 0
342
+ this.flushMeasurements()
343
+ })
344
+ }
345
+
346
+ private flushMeasurements() {
347
+ this.syncScrollTopFromViewport()
348
+ let changed = false
349
+
350
+ for (const index of this.pendingIndexes) {
351
+ const element = this.rowElementRefs[index]?.()
352
+ if (!element) continue
353
+
354
+ const previous = this.rowHeights[index] ?? this.currentEstimateHeight()
355
+ const next = element.getBoundingClientRect().height
356
+ if (!Number.isFinite(next) || next <= 0) continue
357
+ if (Math.abs(previous - next) < 0.5) continue
358
+
359
+ this.updateMeasuredAverage(index, next)
360
+ this.rowHeights[index] = next
361
+ changed = true
362
+ }
363
+
364
+ this.pendingIndexes.clear()
365
+ if (!changed) return
366
+
367
+ if (this.isScrolling) {
368
+ this.hasDeferredMeasurements = true
369
+ return
370
+ }
371
+
372
+ this.measurementVersion(this.measurementVersion() + 1)
373
+ }
374
+
375
+ private currentEstimateHeight() {
376
+ if (this.measuredRowCount === 0) return this.resolvedEstimateHeight()
377
+ return this.measuredHeightTotal / this.measuredRowCount
378
+ }
379
+
380
+ private updateMeasuredAverage(index: number, next: number) {
381
+ const previous = this.rowHeights[index]
382
+ if (previous === undefined) {
383
+ this.measuredRowCount += 1
384
+ this.measuredHeightTotal += next
385
+ return
386
+ }
387
+
388
+ this.measuredHeightTotal += next - previous
389
+ }
390
+
391
+ private syncScrollTopFromViewport() {
392
+ const viewport = this.viewportElement()
393
+ if (!viewport) return
394
+
395
+ this.setScrollTop(viewport.scrollTop)
396
+ }
397
+
398
+ private setScrollTop(value: number) {
399
+ const next = Number.isFinite(value) && value > 0 ? value : 0
400
+ if (this.scrollTop() === next) return
401
+
402
+ this.scrollTop(next)
403
+ }
404
+ }
405
+
406
+ function resolveVariableVirtualList(head: ComponentHead<VariableVirtualList>) {
407
+ head.autoProps = false
408
+ return new VariableVirtualListContext(head.props) as VariableVirtualList
409
+ }
410
+
411
+ function findOffsetIndex(offsets: number[], scrollTop: number) {
412
+ if (offsets.length <= 1) return 0
413
+
414
+ let low = 0
415
+ let high = offsets.length - 1
416
+ while (low < high) {
417
+ const middle = Math.floor((low + high) / 2)
418
+ if ((offsets[middle] ?? 0) <= scrollTop) low = middle + 1
419
+ else high = middle
420
+ }
421
+
422
+ return Math.max(0, low - 1)
423
+ }
@@ -0,0 +1,34 @@
1
+ import { styleBuilder, themes } from '@purestack/ts-style'
2
+
3
+ export function registerVirtualListStyles() {
4
+ themes.forEach((theme) => {
5
+ styleBuilder
6
+ .select('.virtual-list', theme)
7
+ .overflowY('auto')
8
+ .overflowX('auto')
9
+ .minWidth('0')
10
+
11
+ styleBuilder
12
+ .select('.virtual-list__spacer', theme)
13
+ .position('relative')
14
+ .minWidth('0')
15
+
16
+ styleBuilder
17
+ .select('.virtual-list__window', theme)
18
+ .position('absolute')
19
+ .top('0')
20
+ .right('0')
21
+ .left('0')
22
+ .willChange('transform')
23
+
24
+ styleBuilder
25
+ .select('.virtual-list__item', theme)
26
+ .overflow('visible')
27
+ .minWidth('0')
28
+
29
+ styleBuilder
30
+ .select('.variable-virtual-list .virtual-list__item', theme)
31
+ .overflow('visible')
32
+ .minWidth('0')
33
+ })
34
+ }
@@ -0,0 +1,179 @@
1
+ import {
2
+ type ComponentHead,
3
+ type ComputedRef,
4
+ computed,
5
+ defineComponent,
6
+ html,
7
+ type RefOrValue,
8
+ ref,
9
+ unref,
10
+ } from 'regor'
11
+ import type { VirtualListRow } from './virtualList'
12
+
13
+ export interface VirtualTable {
14
+ items?: RefOrValue<unknown[]>
15
+ height?: RefOrValue<number | string>
16
+ itemHeight?: RefOrValue<number | string>
17
+ overscan?: RefOrValue<number | string>
18
+ rowComponent?: RefOrValue<string>
19
+ colGroupComponent?: RefOrValue<string>
20
+ headerComponent?: RefOrValue<string>
21
+ footerComponent?: RefOrValue<string>
22
+ tableLayout?: RefOrValue<'auto' | 'fixed'>
23
+ scrollTop?: ReturnType<typeof ref<number>>
24
+ visibleRows?: ComputedRef<VirtualListRow[]>
25
+ viewportStyle?: ComputedRef<Record<string, string>>
26
+ tableStyle?: ComputedRef<Record<string, string>>
27
+ topSpacerStyle?: ComputedRef<Record<string, string>>
28
+ bottomSpacerStyle?: ComputedRef<Record<string, string>>
29
+ handleScroll?: (event: Event) => void
30
+ hasColGroupComponent?: ComputedRef<boolean>
31
+ hasHeaderComponent?: ComputedRef<boolean>
32
+ hasFooterComponent?: ComputedRef<boolean>
33
+ }
34
+
35
+ const virtualTableTemplate = html`<div class="virtual-table" :style="viewportStyle" @scroll="handleScroll">
36
+ <table class="virtual-table__table" :style="tableStyle">
37
+ <colgroup :is="colGroupComponent" r-if="hasColGroupComponent"></colgroup>
38
+ <thead :is="headerComponent" r-if="hasHeaderComponent"></thead>
39
+ <tbody>
40
+ <tr class="virtual-table__spacer-row">
41
+ <td :style="topSpacerStyle"></td>
42
+ </tr>
43
+ <tr
44
+ :is="rowComponent"
45
+ :item="row.item"
46
+ :index="row.index"
47
+ style="height: var(--virtual-table-item-height) !important;"
48
+ r-for="row in visibleRows"
49
+ ></tr>
50
+ <tr class="virtual-table__spacer-row">
51
+ <td :style="bottomSpacerStyle"></td>
52
+ </tr>
53
+ </tbody>
54
+ <tfoot :is="footerComponent" r-if="hasFooterComponent"></tfoot>
55
+ </table>
56
+ </div>`
57
+
58
+ function defineVirtualTableComponent() {
59
+ return defineComponent<VirtualTable>(virtualTableTemplate, {
60
+ props: [
61
+ 'items',
62
+ 'height',
63
+ 'itemHeight',
64
+ 'overscan',
65
+ 'rowComponent',
66
+ 'colGroupComponent',
67
+ 'headerComponent',
68
+ 'footerComponent',
69
+ 'tableLayout',
70
+ ],
71
+ context: (head) => resolveVirtualTable(head),
72
+ })
73
+ }
74
+
75
+ export function defineVirtualTableComponents() {
76
+ return {
77
+ virtualTable: defineVirtualTableComponent(),
78
+ }
79
+ }
80
+
81
+ function resolveVirtualTable(head: ComponentHead<VirtualTable>) {
82
+ const props = head.props
83
+ const window = resolveFixedVirtualWindow(props)
84
+ const endIndex = computed(() => {
85
+ const rows = window.visibleRows()
86
+ const last = rows[rows.length - 1]
87
+ return last ? last.index + 1 : window.startIndex()
88
+ })
89
+
90
+ return {
91
+ ...props,
92
+ scrollTop: window.scrollTop,
93
+ visibleRows: window.visibleRows,
94
+ viewportStyle: computed<Record<string, string>>(() => ({
95
+ height: `${window.height()}px`,
96
+ overflow: 'auto',
97
+ width: '100%',
98
+ maxWidth: '100%',
99
+ boxSizing: 'border-box',
100
+ minWidth: '0',
101
+ })),
102
+ tableStyle: computed<Record<string, string>>(() => {
103
+ const fixed = unref(props.tableLayout) === 'fixed'
104
+ return {
105
+ '--virtual-table-item-height': `${window.itemHeight()}px`,
106
+ width: fixed ? '100%' : 'max-content',
107
+ minWidth: '100%',
108
+ borderCollapse: 'separate',
109
+ borderSpacing: '0',
110
+ tableLayout: fixed ? 'fixed' : 'auto',
111
+ }
112
+ }),
113
+ topSpacerStyle: computed<Record<string, string>>(() => ({
114
+ height: `${window.startIndex() * window.itemHeight()}px`,
115
+ padding: '0',
116
+ border: '0',
117
+ })),
118
+ bottomSpacerStyle: computed<Record<string, string>>(() => ({
119
+ height: `${Math.max(0, window.items().length - endIndex()) * window.itemHeight()}px`,
120
+ padding: '0',
121
+ border: '0',
122
+ })),
123
+ hasColGroupComponent: computed(() => !!unref(props.colGroupComponent)),
124
+ hasHeaderComponent: computed(() => !!unref(props.headerComponent)),
125
+ hasFooterComponent: computed(() => !!unref(props.footerComponent)),
126
+ handleScroll: window.handleScroll,
127
+ }
128
+ }
129
+
130
+ function resolveFixedVirtualWindow(props: VirtualTable) {
131
+ const scrollTop = ref(0)
132
+ const itemHeight = computed(() => resolvePositiveNumber(props.itemHeight, 44))
133
+ const height = computed(() => resolvePositiveNumber(props.height, 560))
134
+ const overscan = computed(() =>
135
+ Math.trunc(resolvePositiveNumber(props.overscan, 6)),
136
+ )
137
+ const items = computed(() => {
138
+ const value = unref(props.items)
139
+ return Array.isArray(value) ? value : []
140
+ })
141
+ const startIndex = computed(() =>
142
+ Math.max(0, Math.floor(scrollTop() / itemHeight()) - overscan()),
143
+ )
144
+ const visibleCount = computed(
145
+ () => Math.ceil(height() / itemHeight()) + overscan() * 2,
146
+ )
147
+ const visibleRows = computed(() => {
148
+ const source = items()
149
+ const start = startIndex()
150
+ const end = Math.min(source.length, start + visibleCount())
151
+ const rows: VirtualListRow[] = []
152
+ for (let index = start; index < end; index += 1)
153
+ rows.push({ index, item: source[index] })
154
+
155
+ return rows
156
+ })
157
+
158
+ return {
159
+ scrollTop,
160
+ itemHeight,
161
+ height,
162
+ items,
163
+ startIndex,
164
+ visibleRows,
165
+ handleScroll: (event: Event) => {
166
+ const target = event.currentTarget
167
+ if (target instanceof HTMLElement) scrollTop(target.scrollTop)
168
+ },
169
+ }
170
+ }
171
+
172
+ function resolvePositiveNumber(
173
+ value: RefOrValue<number | string> | undefined,
174
+ fallback: number,
175
+ ) {
176
+ const raw = unref(value)
177
+ const parsed = typeof raw === 'number' ? raw : Number.parseFloat(String(raw))
178
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
179
+ }
@@ -0,0 +1,24 @@
1
+ import { styleBuilder, themes } from '@purestack/ts-style'
2
+
3
+ export function registerVirtualTableStyles() {
4
+ themes.forEach((theme) => {
5
+ styleBuilder
6
+ .select('.virtual-table__table th, .virtual-table__table td', theme)
7
+ .verticalAlign('middle')
8
+
9
+ styleBuilder
10
+ .select('.virtual-table__table thead', theme)
11
+ .position('sticky')
12
+ .top('0')
13
+ .zIndex(2)
14
+
15
+ styleBuilder
16
+ .select(
17
+ '.virtual-table__table tfoot th, .virtual-table__table tfoot td',
18
+ theme,
19
+ )
20
+ .position('sticky')
21
+ .bottom('0')
22
+ .zIndex(1)
23
+ })
24
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "isolatedModules": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "resolveJsonModule": true,
11
+ "noImplicitOverride": true,
12
+ "noImplicitAny": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "noEmit": true,
15
+ "types": [],
16
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
17
+ "paths": {
18
+ "@purestack/*": ["../../packages/*/src"]
19
+ }
20
+ },
21
+ "include": ["src/**/*.ts"],
22
+ "exclude": ["src/test/**", "src/**/*.test.ts", "src/**/*.spec.ts"]
23
+ }