vlist 1.9.1 → 2.0.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 (182) hide show
  1. package/README.github.md +104 -97
  2. package/README.md +46 -33
  3. package/dist/constants.d.ts +11 -6
  4. package/dist/constants.js +83 -0
  5. package/dist/core/create.d.ts +10 -0
  6. package/dist/core/create.js +740 -0
  7. package/dist/core/dom.d.ts +8 -0
  8. package/dist/core/dom.js +47 -0
  9. package/dist/core/hooks.d.ts +16 -0
  10. package/dist/core/hooks.js +67 -0
  11. package/dist/core/index.d.ts +17 -0
  12. package/dist/core/index.js +13 -0
  13. package/dist/core/pipeline.d.ts +51 -0
  14. package/dist/core/pipeline.js +307 -0
  15. package/dist/core/pool.d.ts +9 -0
  16. package/dist/core/pool.js +42 -0
  17. package/dist/core/scroll.d.ts +32 -0
  18. package/dist/core/scroll.js +137 -0
  19. package/dist/core/sizes.d.ts +8 -0
  20. package/dist/core/sizes.js +6 -0
  21. package/dist/core/state.d.ts +47 -0
  22. package/dist/core/state.js +56 -0
  23. package/dist/core/types.d.ts +187 -0
  24. package/dist/core/types.js +7 -0
  25. package/dist/{builder → core}/velocity.d.ts +1 -1
  26. package/dist/core/velocity.js +33 -0
  27. package/dist/events/emitter.js +60 -0
  28. package/dist/events/index.js +6 -0
  29. package/dist/index.d.ts +28 -19
  30. package/dist/index.js +28 -1
  31. package/dist/internals.d.ts +11 -7
  32. package/dist/internals.js +60 -1
  33. package/dist/plugins/a11y/index.d.ts +2 -0
  34. package/dist/plugins/a11y/index.js +1 -0
  35. package/dist/plugins/a11y/plugin.d.ts +13 -0
  36. package/dist/plugins/a11y/plugin.js +259 -0
  37. package/dist/{features → plugins}/async/index.d.ts +1 -1
  38. package/dist/plugins/async/index.js +12 -0
  39. package/dist/{features → plugins}/async/manager.d.ts +5 -1
  40. package/dist/plugins/async/manager.js +568 -0
  41. package/dist/plugins/async/placeholder.js +154 -0
  42. package/dist/plugins/async/plugin.d.ts +48 -0
  43. package/dist/plugins/async/plugin.js +311 -0
  44. package/dist/plugins/async/sparse.js +540 -0
  45. package/dist/plugins/autosize/index.d.ts +5 -0
  46. package/dist/plugins/autosize/index.js +4 -0
  47. package/dist/plugins/autosize/plugin.d.ts +19 -0
  48. package/dist/plugins/autosize/plugin.js +185 -0
  49. package/dist/plugins/grid/index.d.ts +7 -0
  50. package/dist/plugins/grid/index.js +5 -0
  51. package/dist/plugins/grid/layout.js +275 -0
  52. package/dist/plugins/grid/plugin.d.ts +23 -0
  53. package/dist/plugins/grid/plugin.js +347 -0
  54. package/dist/plugins/grid/renderer.js +525 -0
  55. package/dist/plugins/grid/types.js +11 -0
  56. package/dist/plugins/groups/async-bridge.js +246 -0
  57. package/dist/{features → plugins}/groups/index.d.ts +1 -1
  58. package/dist/plugins/groups/index.js +13 -0
  59. package/dist/plugins/groups/layout.js +294 -0
  60. package/dist/plugins/groups/plugin.d.ts +22 -0
  61. package/dist/plugins/groups/plugin.js +571 -0
  62. package/dist/plugins/groups/sticky.js +255 -0
  63. package/dist/plugins/groups/types.js +12 -0
  64. package/dist/plugins/masonry/index.d.ts +8 -0
  65. package/dist/plugins/masonry/index.js +6 -0
  66. package/dist/plugins/masonry/layout.js +261 -0
  67. package/dist/plugins/masonry/plugin.d.ts +32 -0
  68. package/dist/plugins/masonry/plugin.js +381 -0
  69. package/dist/plugins/masonry/renderer.js +354 -0
  70. package/dist/plugins/masonry/types.js +9 -0
  71. package/dist/plugins/page/index.d.ts +5 -0
  72. package/dist/plugins/page/index.js +5 -0
  73. package/dist/plugins/page/plugin.d.ts +21 -0
  74. package/dist/plugins/page/plugin.js +166 -0
  75. package/dist/plugins/scale/index.d.ts +5 -0
  76. package/dist/plugins/scale/index.js +4 -0
  77. package/dist/plugins/scale/plugin.d.ts +24 -0
  78. package/dist/plugins/scale/plugin.js +507 -0
  79. package/dist/plugins/scrollbar/controller.js +574 -0
  80. package/dist/plugins/scrollbar/index.d.ts +7 -0
  81. package/dist/plugins/scrollbar/index.js +6 -0
  82. package/dist/plugins/scrollbar/plugin.d.ts +20 -0
  83. package/dist/plugins/scrollbar/plugin.js +93 -0
  84. package/dist/plugins/scrollbar/scrollbar.js +556 -0
  85. package/dist/plugins/selection/index.d.ts +6 -0
  86. package/dist/plugins/selection/index.js +7 -0
  87. package/dist/plugins/selection/plugin.d.ts +16 -0
  88. package/dist/plugins/selection/plugin.js +601 -0
  89. package/dist/{features → plugins}/selection/state.d.ts +8 -0
  90. package/dist/plugins/selection/state.js +332 -0
  91. package/dist/plugins/snapshots/index.d.ts +5 -0
  92. package/dist/plugins/snapshots/index.js +5 -0
  93. package/dist/plugins/snapshots/plugin.d.ts +17 -0
  94. package/dist/plugins/snapshots/plugin.js +301 -0
  95. package/dist/plugins/sortable/index.d.ts +6 -0
  96. package/dist/plugins/sortable/index.js +6 -0
  97. package/dist/plugins/sortable/plugin.d.ts +34 -0
  98. package/dist/plugins/sortable/plugin.js +753 -0
  99. package/dist/plugins/table/header.js +501 -0
  100. package/dist/{features → plugins}/table/index.d.ts +1 -1
  101. package/dist/plugins/table/index.js +12 -0
  102. package/dist/plugins/table/layout.js +211 -0
  103. package/dist/plugins/table/plugin.d.ts +20 -0
  104. package/dist/plugins/table/plugin.js +391 -0
  105. package/dist/plugins/table/renderer.js +625 -0
  106. package/dist/plugins/table/types.js +12 -0
  107. package/dist/plugins/transition/index.d.ts +5 -0
  108. package/dist/plugins/transition/index.js +5 -0
  109. package/dist/plugins/transition/plugin.d.ts +22 -0
  110. package/dist/plugins/transition/plugin.js +405 -0
  111. package/dist/rendering/aria.js +23 -0
  112. package/dist/rendering/index.js +18 -0
  113. package/dist/rendering/measured.js +98 -0
  114. package/dist/rendering/renderer.js +586 -0
  115. package/dist/rendering/scale.js +267 -0
  116. package/dist/rendering/scroll.js +71 -0
  117. package/dist/rendering/sizes.js +193 -0
  118. package/dist/rendering/sort.js +65 -0
  119. package/dist/rendering/viewport.js +268 -0
  120. package/dist/size.json +1 -1
  121. package/dist/types.js +5 -0
  122. package/dist/utils/padding.d.ts +2 -4
  123. package/dist/utils/padding.js +49 -0
  124. package/dist/utils/stats.js +124 -0
  125. package/dist/vlist-grid.css +1 -1
  126. package/dist/vlist-masonry.css +1 -1
  127. package/dist/vlist-table.css +1 -1
  128. package/dist/vlist.css +1 -1
  129. package/package.json +9 -4
  130. package/dist/builder/a11y.d.ts +0 -16
  131. package/dist/builder/api.d.ts +0 -21
  132. package/dist/builder/context.d.ts +0 -36
  133. package/dist/builder/core.d.ts +0 -16
  134. package/dist/builder/data.d.ts +0 -71
  135. package/dist/builder/dom.d.ts +0 -15
  136. package/dist/builder/index.d.ts +0 -25
  137. package/dist/builder/materialize.d.ts +0 -166
  138. package/dist/builder/pool.d.ts +0 -10
  139. package/dist/builder/range.d.ts +0 -10
  140. package/dist/builder/scroll.d.ts +0 -24
  141. package/dist/builder/types.d.ts +0 -512
  142. package/dist/features/async/feature.d.ts +0 -72
  143. package/dist/features/autosize/feature.d.ts +0 -34
  144. package/dist/features/autosize/index.d.ts +0 -2
  145. package/dist/features/grid/feature.d.ts +0 -48
  146. package/dist/features/grid/index.d.ts +0 -9
  147. package/dist/features/groups/feature.d.ts +0 -75
  148. package/dist/features/masonry/feature.d.ts +0 -45
  149. package/dist/features/masonry/index.d.ts +0 -9
  150. package/dist/features/page/feature.d.ts +0 -109
  151. package/dist/features/page/index.d.ts +0 -9
  152. package/dist/features/scale/feature.d.ts +0 -42
  153. package/dist/features/scale/index.d.ts +0 -10
  154. package/dist/features/scrollbar/feature.d.ts +0 -81
  155. package/dist/features/scrollbar/index.d.ts +0 -8
  156. package/dist/features/selection/feature.d.ts +0 -91
  157. package/dist/features/selection/index.d.ts +0 -7
  158. package/dist/features/snapshots/feature.d.ts +0 -79
  159. package/dist/features/snapshots/index.d.ts +0 -9
  160. package/dist/features/sortable/feature.d.ts +0 -101
  161. package/dist/features/sortable/index.d.ts +0 -6
  162. package/dist/features/table/feature.d.ts +0 -67
  163. package/dist/features/transition/feature.d.ts +0 -30
  164. package/dist/features/transition/index.d.ts +0 -9
  165. /package/dist/{features → plugins}/async/placeholder.d.ts +0 -0
  166. /package/dist/{features → plugins}/async/sparse.d.ts +0 -0
  167. /package/dist/{features → plugins}/grid/layout.d.ts +0 -0
  168. /package/dist/{features → plugins}/grid/renderer.d.ts +0 -0
  169. /package/dist/{features → plugins}/grid/types.d.ts +0 -0
  170. /package/dist/{features → plugins}/groups/async-bridge.d.ts +0 -0
  171. /package/dist/{features → plugins}/groups/layout.d.ts +0 -0
  172. /package/dist/{features → plugins}/groups/sticky.d.ts +0 -0
  173. /package/dist/{features → plugins}/groups/types.d.ts +0 -0
  174. /package/dist/{features → plugins}/masonry/layout.d.ts +0 -0
  175. /package/dist/{features → plugins}/masonry/renderer.d.ts +0 -0
  176. /package/dist/{features → plugins}/masonry/types.d.ts +0 -0
  177. /package/dist/{features → plugins}/scrollbar/controller.d.ts +0 -0
  178. /package/dist/{features → plugins}/scrollbar/scrollbar.d.ts +0 -0
  179. /package/dist/{features → plugins}/table/header.d.ts +0 -0
  180. /package/dist/{features → plugins}/table/layout.d.ts +0 -0
  181. /package/dist/{features → plugins}/table/renderer.d.ts +0 -0
  182. /package/dist/{features → plugins}/table/types.d.ts +0 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * vlist v2 — DOM Structure
3
+ * Container resolution and DOM scaffold creation.
4
+ */
5
+ import type { DOMStructure } from "./types";
6
+ export declare function resolveContainer(container: HTMLElement | string): HTMLElement;
7
+ export declare function createDOMStructure(container: HTMLElement, classPrefix: string, horizontal: boolean, interactive: boolean, ariaLabel?: string): DOMStructure;
8
+ //# sourceMappingURL=dom.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * vlist v2 — DOM Structure
3
+ * Container resolution and DOM scaffold creation.
4
+ */
5
+ // =============================================================================
6
+ // Container Resolution
7
+ // =============================================================================
8
+ export function resolveContainer(container) {
9
+ if (typeof container === "string") {
10
+ const el = document.querySelector(container);
11
+ if (!el)
12
+ throw new Error(`[vlist] Container not found: ${container}`);
13
+ return el;
14
+ }
15
+ return container;
16
+ }
17
+ // =============================================================================
18
+ // DOM Structure Factory
19
+ // =============================================================================
20
+ export function createDOMStructure(container, classPrefix, horizontal, interactive, ariaLabel) {
21
+ const rootCls = horizontal ? `${classPrefix} ${classPrefix}--horizontal` : classPrefix;
22
+ const vpStyle = horizontal
23
+ ? "overflow-x:auto;overflow-y:hidden;height:100%;width:100%"
24
+ : "overflow:auto;height:100%;width:100%";
25
+ const cStyle = horizontal ? "position:relative;height:100%" : "position:relative;width:100%";
26
+ let cAttrs = ` role="${interactive ? "listbox" : "list"}"`;
27
+ if (interactive)
28
+ cAttrs += ' tabindex="0"';
29
+ if (ariaLabel)
30
+ cAttrs += ` aria-label="${ariaLabel.replace(/"/g, """)}"`;
31
+ if (horizontal)
32
+ cAttrs += ' aria-orientation="horizontal"';
33
+ container.insertAdjacentHTML("beforeend", `<div class="${rootCls}"><div class="${classPrefix}-viewport" style="${vpStyle}" tabindex="-1"><div class="${classPrefix}-content" style="${cStyle}"${cAttrs}></div></div></div>`);
34
+ const root = container.lastElementChild;
35
+ const viewport = root.firstElementChild;
36
+ const content = viewport.firstElementChild;
37
+ const liveRegion = document.createElement("div");
38
+ liveRegion.className = `${classPrefix}-live`;
39
+ liveRegion.style.cssText =
40
+ "position:absolute;width:1px;height:1px;padding:0;margin:-1px;" +
41
+ "overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0";
42
+ liveRegion.setAttribute("aria-live", "polite");
43
+ liveRegion.setAttribute("aria-atomic", "true");
44
+ liveRegion.setAttribute("role", "status");
45
+ root.appendChild(liveRegion);
46
+ return { root, viewport, content, liveRegion };
47
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * vlist v2 — Build-Time Compiled Hooks
3
+ *
4
+ * Hooks are collected during createVList() and frozen into plain arrays.
5
+ * On the hot path they are iterated with a for-loop — no closures,
6
+ * no dynamic dispatch, no allocation.
7
+ */
8
+ import type { CalculateHook, CommitHook, AfterScrollHook, IdleHook, ResizeHook, CompiledHooks, VListPlugin } from "./types";
9
+ import type { VListItem } from "../types";
10
+ export declare function compileHooks<T extends VListItem>(plugins: readonly VListPlugin<T>[]): CompiledHooks;
11
+ export declare function runCalculateHooks(hooks: readonly CalculateHook[], state: import("./state").EngineState): void;
12
+ export declare function runCommitHooks(hooks: readonly CommitHook[], state: import("./state").EngineState): void;
13
+ export declare function runAfterScrollHooks(hooks: readonly AfterScrollHook[], scrollPosition: number, direction: number): void;
14
+ export declare function runIdleHooks(hooks: readonly IdleHook[]): void;
15
+ export declare function runResizeHooks(hooks: readonly ResizeHook[], width: number, height: number): void;
16
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * vlist v2 — Build-Time Compiled Hooks
3
+ *
4
+ * Hooks are collected during createVList() and frozen into plain arrays.
5
+ * On the hot path they are iterated with a for-loop — no closures,
6
+ * no dynamic dispatch, no allocation.
7
+ */
8
+ // =============================================================================
9
+ // Compiler — collects hooks from plugins, returns frozen arrays
10
+ // =============================================================================
11
+ const EMPTY = [];
12
+ const EMPTY_HOOKS = {
13
+ calculate: EMPTY, commit: EMPTY, afterScroll: EMPTY, idle: EMPTY, resize: EMPTY,
14
+ };
15
+ export function compileHooks(plugins) {
16
+ if (plugins.length === 0)
17
+ return EMPTY_HOOKS;
18
+ const calculate = [];
19
+ const commit = [];
20
+ const afterScroll = [];
21
+ const idle = [];
22
+ const resize = [];
23
+ for (let i = 0; i < plugins.length; i++) {
24
+ const h = plugins[i].hooks;
25
+ if (h === undefined)
26
+ continue;
27
+ if (h.onCalculate !== undefined)
28
+ calculate.push(h.onCalculate);
29
+ if (h.onCommit !== undefined)
30
+ commit.push(h.onCommit);
31
+ if (h.onAfterScroll !== undefined)
32
+ afterScroll.push(h.onAfterScroll);
33
+ if (h.onIdle !== undefined)
34
+ idle.push(h.onIdle);
35
+ if (h.onResize !== undefined)
36
+ resize.push(h.onResize);
37
+ }
38
+ return { calculate, commit, afterScroll, idle, resize };
39
+ }
40
+ // =============================================================================
41
+ // Runners — zero allocation, linear iteration
42
+ // =============================================================================
43
+ export function runCalculateHooks(hooks, state) {
44
+ for (let i = 0; i < hooks.length; i++) {
45
+ hooks[i](state);
46
+ }
47
+ }
48
+ export function runCommitHooks(hooks, state) {
49
+ for (let i = 0; i < hooks.length; i++) {
50
+ hooks[i](state);
51
+ }
52
+ }
53
+ export function runAfterScrollHooks(hooks, scrollPosition, direction) {
54
+ for (let i = 0; i < hooks.length; i++) {
55
+ hooks[i](scrollPosition, direction);
56
+ }
57
+ }
58
+ export function runIdleHooks(hooks) {
59
+ for (let i = 0; i < hooks.length; i++) {
60
+ hooks[i]();
61
+ }
62
+ }
63
+ export function runResizeHooks(hooks, width, height) {
64
+ for (let i = 0; i < hooks.length; i++) {
65
+ hooks[i](width, height);
66
+ }
67
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * vlist v2 — Core Public API
3
+ */
4
+ export { createVList } from "./create";
5
+ export { createEngineState } from "./state";
6
+ export type { EngineState } from "./state";
7
+ export { phase1Calculate, phase2Commit, render, createRenderConfig } from "./pipeline";
8
+ export type { RenderConfig } from "./pipeline";
9
+ export { compileHooks, runCalculateHooks, runCommitHooks, runAfterScrollHooks, runIdleHooks, runResizeHooks } from "./hooks";
10
+ export { createPool } from "./pool";
11
+ export { createSizeCache, countVisibleItems, countItemsFittingFromBottom, getOffsetForVirtualIndex } from "./sizes";
12
+ export { createDOMStructure, resolveContainer } from "./dom";
13
+ export { createScrollHandler } from "./scroll";
14
+ export type { SizeCache } from "./sizes";
15
+ export type { ScrollHandler, ScrollHandlerConfig } from "./scroll";
16
+ export type { CalculateHook, CommitHook, AfterScrollHook, IdleHook, ResizeHook, CompiledHooks, ResolvedConfig, DOMStructure, ElementPool, VisibleRangeFn, PluginContext, VListPlugin, VList, CreateVListConfig, } from "./types";
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * vlist v2 — Core Public API
3
+ */
4
+ // Factory
5
+ export { createVList } from "./create";
6
+ // Engine
7
+ export { createEngineState } from "./state";
8
+ export { phase1Calculate, phase2Commit, render, createRenderConfig } from "./pipeline";
9
+ export { compileHooks, runCalculateHooks, runCommitHooks, runAfterScrollHooks, runIdleHooks, runResizeHooks } from "./hooks";
10
+ export { createPool } from "./pool";
11
+ export { createSizeCache, countVisibleItems, countItemsFittingFromBottom, getOffsetForVirtualIndex } from "./sizes";
12
+ export { createDOMStructure, resolveContainer } from "./dom";
13
+ export { createScrollHandler } from "./scroll";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * vlist v2 — 2-Phase Pipeline
3
+ *
4
+ * Phase 1: Calculate & Reconcile — zero allocation hot path.
5
+ * Reads scroll position + size cache, writes into EngineState TypedArrays.
6
+ *
7
+ * Phase 2: Commit — reads EngineState buffers, updates DOM via pool.
8
+ * Sub-phases: acquire → identity bind → position → release.
9
+ *
10
+ * Both phases are synchronous. No intermediate objects are allocated.
11
+ */
12
+ import type { SizeCache } from "./sizes";
13
+ import type { VListItem, ItemTemplate, ItemState, VListEvents } from "../types";
14
+ import type { CompiledHooks, ElementPool } from "./types";
15
+ import type { EngineState } from "./state";
16
+ import type { Emitter } from "../events";
17
+ export interface RenderConfig {
18
+ readonly prefix: string;
19
+ readonly selectedClass: string;
20
+ readonly focusedClass: string;
21
+ readonly placeholderClass: string;
22
+ readonly replacedClass: string;
23
+ readonly translateProp: "translateX(" | "translateY(";
24
+ readonly itemRole: "option" | "listitem";
25
+ readonly interactive: boolean;
26
+ readonly horizontal: boolean;
27
+ readonly startPadding: number;
28
+ readonly gap: number;
29
+ readonly hasCrossPad: boolean;
30
+ readonly crossStartProp: string;
31
+ readonly crossEndProp: string;
32
+ readonly crossStartVal: string;
33
+ readonly crossEndVal: string;
34
+ readonly oddClass: string;
35
+ readonly emitter: Emitter<VListEvents> | null;
36
+ }
37
+ export declare function createRenderConfig(classPrefix: string, horizontal: boolean, interactive: boolean, startPadding: number, crossPadStart: number, crossPadEnd: number, oddClass: string, gap: number, emitter?: Emitter<VListEvents> | null): RenderConfig;
38
+ /**
39
+ * Calculate visible range and fill EngineState buffers.
40
+ * Zero allocation — all writes go into pre-allocated TypedArrays.
41
+ *
42
+ * Guards:
43
+ * - Zero container size early exit
44
+ * - Empty range sentinel: visibleCount = 0
45
+ * - Overscan application
46
+ * - Render count safety cap
47
+ */
48
+ export declare function phase1Calculate(state: EngineState, sizeCache: SizeCache, overscan: number, hooks: CompiledHooks, startPadding?: number): boolean;
49
+ export declare function phase2Commit<T extends VListItem>(state: EngineState, pool: ElementPool, contentElement: HTMLElement, template: ItemTemplate<T>, getItems: () => readonly T[], rendered: Map<number, HTMLElement>, rc: RenderConfig, hooks: CompiledHooks, getItemFn?: ((index: number) => T | undefined) | null, itemStateFn?: ((index: number, state: ItemState) => void) | null): void;
50
+ export declare function render<T extends VListItem>(state: EngineState, sizeCache: SizeCache, overscan: number, pool: ElementPool, contentElement: HTMLElement, template: ItemTemplate<T>, getItems: () => readonly T[], rendered: Map<number, HTMLElement>, rc: RenderConfig, hooks: CompiledHooks, getItemFn?: ((index: number) => T | undefined) | null, itemStateFn?: ((index: number, state: ItemState) => void) | null): void;
51
+ //# sourceMappingURL=pipeline.d.ts.map
@@ -0,0 +1,307 @@
1
+ /**
2
+ * vlist v2 — 2-Phase Pipeline
3
+ *
4
+ * Phase 1: Calculate & Reconcile — zero allocation hot path.
5
+ * Reads scroll position + size cache, writes into EngineState TypedArrays.
6
+ *
7
+ * Phase 2: Commit — reads EngineState buffers, updates DOM via pool.
8
+ * Sub-phases: acquire → identity bind → position → release.
9
+ *
10
+ * Both phases are synchronous. No intermediate objects are allocated.
11
+ */
12
+ import { runCalculateHooks, runCommitHooks } from "./hooks";
13
+ import { PLACEHOLDER_ID_PREFIX } from "../constants";
14
+ export function createRenderConfig(classPrefix, horizontal, interactive, startPadding, crossPadStart, crossPadEnd, oddClass, gap, emitter) {
15
+ const hasCrossPad = crossPadStart !== 0 || crossPadEnd !== 0;
16
+ return {
17
+ prefix: classPrefix,
18
+ selectedClass: `${classPrefix}-item--selected`,
19
+ focusedClass: `${classPrefix}-item--focused`,
20
+ placeholderClass: `${classPrefix}-item--placeholder`,
21
+ replacedClass: `${classPrefix}-item--replaced`,
22
+ translateProp: horizontal ? "translateX(" : "translateY(",
23
+ itemRole: interactive ? "option" : "listitem",
24
+ interactive,
25
+ horizontal,
26
+ startPadding,
27
+ gap,
28
+ hasCrossPad,
29
+ crossStartProp: hasCrossPad ? (horizontal ? "top" : "left") : "",
30
+ crossEndProp: hasCrossPad ? (horizontal ? "bottom" : "right") : "",
31
+ crossStartVal: hasCrossPad ? crossPadStart + "px" : "",
32
+ crossEndVal: hasCrossPad ? crossPadEnd + "px" : "",
33
+ oddClass,
34
+ emitter: emitter ?? null,
35
+ };
36
+ }
37
+ // =============================================================================
38
+ // Phase 1 — Calculate & Reconcile
39
+ // =============================================================================
40
+ /**
41
+ * Calculate visible range and fill EngineState buffers.
42
+ * Zero allocation — all writes go into pre-allocated TypedArrays.
43
+ *
44
+ * Guards:
45
+ * - Zero container size early exit
46
+ * - Empty range sentinel: visibleCount = 0
47
+ * - Overscan application
48
+ * - Render count safety cap
49
+ */
50
+ export function phase1Calculate(state, sizeCache, overscan, hooks, startPadding) {
51
+ if (state.containerSize <= 0 || state.totalItems === 0) {
52
+ state.clear();
53
+ return true;
54
+ }
55
+ const scrollPos = state.scrollPosition;
56
+ const containerSize = state.containerSize;
57
+ const totalItems = state.totalItems;
58
+ const sp = startPadding ?? 0;
59
+ state.totalSize = sizeCache.getTotalSize();
60
+ // Visible range — items are visually shifted by startPadding in the
61
+ // transform, so subtract it from the range start lookup to avoid
62
+ // missing items at the top of the viewport.
63
+ let visStart = sizeCache.indexAtOffset(sp > 0 ? Math.max(0, scrollPos - sp) : scrollPos);
64
+ let visEnd = sizeCache.indexAtOffset(scrollPos + containerSize);
65
+ if (visEnd < totalItems - 1)
66
+ visEnd++;
67
+ visStart = Math.max(0, visStart);
68
+ visEnd = Math.min(totalItems - 1, Math.max(0, visEnd));
69
+ // Overscan
70
+ const renderStart = Math.max(0, visStart - overscan);
71
+ const renderEnd = Math.min(totalItems - 1, visEnd + overscan);
72
+ // Safety cap
73
+ const maxRender = Math.ceil(containerSize / 1) + overscan * 2 + 10;
74
+ const count = renderEnd - renderStart + 1;
75
+ const safeCap = Math.min(count, state.capacity, maxRender);
76
+ // Range-unchanged fast path
77
+ if (renderStart === state.prevRangeStart && renderEnd === state.prevRangeEnd && !state.renderPending) {
78
+ return false;
79
+ }
80
+ // Fill TypedArray buffers
81
+ state.visibleCount = safeCap;
82
+ state.startIndex = renderStart;
83
+ for (let i = 0; i < safeCap; i++) {
84
+ const idx = renderStart + i;
85
+ state.visibleIndices[i] = idx;
86
+ state.visibleOffsets[i] = sizeCache.getOffset(idx);
87
+ state.visibleSizes[i] = sizeCache.getSize(idx);
88
+ }
89
+ runCalculateHooks(hooks.calculate, state);
90
+ state.prevRangeStart = renderStart;
91
+ state.prevRangeEnd = renderEnd;
92
+ state.renderPending = false;
93
+ return true;
94
+ }
95
+ // =============================================================================
96
+ // Phase 2 — Commit (DOM Update)
97
+ // =============================================================================
98
+ /** Reusable ItemState singleton — never allocated per frame */
99
+ const itemState = { selected: false, focused: false };
100
+ /** Linear scan for idx in visibleIndices[0..count). Handles arbitrary order. Zero allocation. */
101
+ function isInVisible(indices, count, idx) {
102
+ for (let i = 0; i < count; i++) {
103
+ if (indices[i] === idx)
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+ /** Module-scope release state — avoids per-frame closure in rendered.forEach */
109
+ let _relIndices;
110
+ let _relCount;
111
+ let _relPool;
112
+ let _relRendered;
113
+ function releaseIfNotVisible(element, idx) {
114
+ if (!isInVisible(_relIndices, _relCount, idx)) {
115
+ element.remove();
116
+ _relPool.release(element);
117
+ _relRendered.delete(idx);
118
+ }
119
+ }
120
+ export function phase2Commit(state, pool, contentElement, template, getItems, rendered, rc, hooks, getItemFn, itemStateFn) {
121
+ const items = getItemFn ? null : getItems();
122
+ const count = state.visibleCount;
123
+ const newIndices = state.visibleIndices;
124
+ const ariaTotal = rc.interactive ? String(state.totalItems) : "";
125
+ const totalChanged = rc.interactive && state.totalItems !== state.prevAriaTotal;
126
+ let fragment = null;
127
+ for (let i = 0; i < count; i++) {
128
+ const dataIndex = newIndices[i];
129
+ const offset = state.visibleOffsets[i];
130
+ const size = state.visibleSizes[i];
131
+ const item = getItemFn ? getItemFn(dataIndex) : items[dataIndex];
132
+ if (itemStateFn) {
133
+ itemStateFn(dataIndex, itemState);
134
+ }
135
+ else {
136
+ itemState.selected = false;
137
+ itemState.focused = false;
138
+ }
139
+ let element = rendered.get(dataIndex);
140
+ const el = element;
141
+ if (element === undefined) {
142
+ const acquired = pool.acquire();
143
+ if (item !== undefined) {
144
+ let result;
145
+ try {
146
+ result = template(item, dataIndex, itemState);
147
+ }
148
+ catch (err) {
149
+ if (rc.emitter) {
150
+ rc.emitter.emit("error", {
151
+ error: err instanceof Error ? err : new Error(String(err)),
152
+ context: `template:render:${dataIndex}`,
153
+ });
154
+ }
155
+ pool.release(acquired);
156
+ continue;
157
+ }
158
+ if (typeof result === "string") {
159
+ acquired.innerHTML = result;
160
+ }
161
+ else {
162
+ acquired.innerHTML = "";
163
+ acquired.appendChild(result);
164
+ }
165
+ }
166
+ acquired.setAttribute("role", rc.itemRole);
167
+ acquired.setAttribute("data-index", String(dataIndex));
168
+ if (rc.interactive) {
169
+ acquired.id = rc.prefix + "-item-" + dataIndex;
170
+ acquired.setAttribute("aria-posinset", String(dataIndex + 1));
171
+ acquired.setAttribute("aria-setsize", ariaTotal);
172
+ }
173
+ if (item !== undefined) {
174
+ const itemId = String(item.id);
175
+ acquired.setAttribute("data-id", itemId);
176
+ if (itemId.startsWith(PLACEHOLDER_ID_PREFIX)) {
177
+ acquired.classList.add(rc.placeholderClass);
178
+ }
179
+ }
180
+ if (rc.hasCrossPad) {
181
+ acquired.style.setProperty(rc.crossStartProp, rc.crossStartVal);
182
+ acquired.style.setProperty(rc.crossEndProp, rc.crossEndVal);
183
+ }
184
+ if (itemStateFn) {
185
+ acquired.classList.toggle(rc.selectedClass, itemState.selected);
186
+ acquired.classList.toggle(rc.focusedClass, itemState.focused);
187
+ if (itemState.selected)
188
+ acquired.setAttribute("aria-selected", "true");
189
+ else
190
+ acquired.removeAttribute("aria-selected");
191
+ acquired._lastSelected = itemState.selected;
192
+ acquired._lastFocused = itemState.focused;
193
+ }
194
+ if (rc.oddClass)
195
+ acquired.classList.toggle(rc.oddClass, (dataIndex & 1) === 1);
196
+ const transformOffset = offset + rc.startPadding;
197
+ acquired.style.transform = rc.translateProp + transformOffset + "px)";
198
+ acquired._lastOffset = transformOffset;
199
+ const sizeVal = size - rc.gap;
200
+ if (rc.horizontal) {
201
+ acquired.style.width = sizeVal + "px";
202
+ }
203
+ else {
204
+ acquired.style.height = sizeVal + "px";
205
+ }
206
+ acquired._lastSize = sizeVal;
207
+ acquired._lastItem = item;
208
+ rendered.set(dataIndex, acquired);
209
+ if (fragment === null)
210
+ fragment = document.createDocumentFragment();
211
+ fragment.appendChild(acquired);
212
+ }
213
+ else {
214
+ if (totalChanged) {
215
+ element.setAttribute("aria-setsize", ariaTotal);
216
+ }
217
+ if (item !== undefined && el._lastItem !== item) {
218
+ const oldId = element.getAttribute("data-id");
219
+ const newId = String(item.id);
220
+ let result;
221
+ try {
222
+ result = template(item, dataIndex, itemState);
223
+ }
224
+ catch (err) {
225
+ if (rc.emitter) {
226
+ rc.emitter.emit("error", {
227
+ error: err instanceof Error ? err : new Error(String(err)),
228
+ context: `template:render:${dataIndex}`,
229
+ });
230
+ }
231
+ continue;
232
+ }
233
+ if (typeof result === "string") {
234
+ element.innerHTML = result;
235
+ }
236
+ else {
237
+ element.innerHTML = "";
238
+ element.appendChild(result);
239
+ }
240
+ element.setAttribute("data-id", newId);
241
+ el._lastItem = item;
242
+ if (oldId !== newId) {
243
+ const wasPlaceholder = oldId !== null && oldId.startsWith(PLACEHOLDER_ID_PREFIX);
244
+ const isPlaceholder = newId.startsWith(PLACEHOLDER_ID_PREFIX);
245
+ if (wasPlaceholder !== isPlaceholder) {
246
+ element.classList.toggle(rc.placeholderClass, isPlaceholder);
247
+ }
248
+ if (wasPlaceholder && !isPlaceholder) {
249
+ element.classList.add(rc.replacedClass);
250
+ setTimeout(() => { element.classList.remove(rc.replacedClass); }, 300);
251
+ }
252
+ }
253
+ }
254
+ if (itemStateFn) {
255
+ if (el._lastSelected !== itemState.selected) {
256
+ element.classList.toggle(rc.selectedClass, itemState.selected);
257
+ if (itemState.selected)
258
+ element.setAttribute("aria-selected", "true");
259
+ else
260
+ element.removeAttribute("aria-selected");
261
+ el._lastSelected = itemState.selected;
262
+ }
263
+ if (el._lastFocused !== itemState.focused) {
264
+ element.classList.toggle(rc.focusedClass, itemState.focused);
265
+ el._lastFocused = itemState.focused;
266
+ }
267
+ }
268
+ const transformOffset = offset + rc.startPadding;
269
+ if (el._lastOffset !== transformOffset) {
270
+ element.style.transform = rc.translateProp + transformOffset + "px)";
271
+ el._lastOffset = transformOffset;
272
+ }
273
+ const sizeVal = size - rc.gap;
274
+ if (el._lastSize !== sizeVal) {
275
+ if (rc.horizontal) {
276
+ element.style.width = sizeVal + "px";
277
+ }
278
+ else {
279
+ element.style.height = sizeVal + "px";
280
+ }
281
+ el._lastSize = sizeVal;
282
+ }
283
+ }
284
+ }
285
+ // Flush all new elements in one DOM operation
286
+ if (fragment !== null)
287
+ contentElement.appendChild(fragment);
288
+ // Release nodes no longer visible (after acquire so new elements are in the
289
+ // DOM before stale ones are removed — no single-frame gaps).
290
+ _relIndices = newIndices;
291
+ _relCount = count;
292
+ _relPool = pool;
293
+ _relRendered = rendered;
294
+ rendered.forEach(releaseIfNotVisible);
295
+ if (rc.interactive)
296
+ state.prevAriaTotal = state.totalItems;
297
+ runCommitHooks(hooks.commit, state);
298
+ }
299
+ // =============================================================================
300
+ // Full Render Cycle
301
+ // =============================================================================
302
+ export function render(state, sizeCache, overscan, pool, contentElement, template, getItems, rendered, rc, hooks, getItemFn, itemStateFn) {
303
+ const changed = phase1Calculate(state, sizeCache, overscan, hooks, rc.startPadding);
304
+ if (changed) {
305
+ phase2Commit(state, pool, contentElement, template, getItems, rendered, rc, hooks, getItemFn, itemStateFn);
306
+ }
307
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * vlist v2 — Element Pool
3
+ *
4
+ * acquire() = pop or create, release() = reset + push.
5
+ * Max pool size: 100.
6
+ */
7
+ import type { ElementPool } from "./types";
8
+ export declare function createPool(classPrefix: string): ElementPool;
9
+ //# sourceMappingURL=pool.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * vlist v2 — Element Pool
3
+ *
4
+ * acquire() = pop or create, release() = reset + push.
5
+ * Max pool size: 100.
6
+ */
7
+ const MAX_POOL_SIZE = 100;
8
+ export function createPool(classPrefix) {
9
+ const pool = [];
10
+ const itemClass = `${classPrefix}-item`;
11
+ const tpl = document.createElement("div");
12
+ tpl.className = itemClass;
13
+ return {
14
+ acquire() {
15
+ if (pool.length > 0) {
16
+ return pool.pop();
17
+ }
18
+ return tpl.cloneNode(false);
19
+ },
20
+ release(element) {
21
+ element.className = itemClass;
22
+ element.removeAttribute("style");
23
+ element.removeAttribute("id");
24
+ element.removeAttribute("role");
25
+ element.removeAttribute("aria-selected");
26
+ element.removeAttribute("aria-posinset");
27
+ element.removeAttribute("aria-setsize");
28
+ element.removeAttribute("data-index");
29
+ element.removeAttribute("data-id");
30
+ element.innerHTML = "";
31
+ if (pool.length < MAX_POOL_SIZE) {
32
+ pool.push(element);
33
+ }
34
+ },
35
+ get size() {
36
+ return pool.length;
37
+ },
38
+ clear() {
39
+ pool.length = 0;
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * vlist v2 — Scroll Handling
3
+ *
4
+ * Wheel interception for synchronous rendering, scroll idle detection,
5
+ * and smooth scroll animation.
6
+ */
7
+ import type { EngineState } from "./state";
8
+ export interface ScrollHandler {
9
+ /** Attach scroll and wheel listeners */
10
+ attach(): void;
11
+ /** Detach all listeners */
12
+ detach(): void;
13
+ /** Cancel smooth scroll animation */
14
+ cancelScroll(): void;
15
+ /** Animate to a target scroll position */
16
+ smoothScrollTo(target: number, duration: number, setFn?: (pos: number) => void, easing?: (t: number) => number): void;
17
+ }
18
+ export interface ScrollHandlerConfig {
19
+ readonly state: EngineState;
20
+ readonly viewport: HTMLElement;
21
+ readonly horizontal: boolean;
22
+ readonly wheelEnabled: boolean;
23
+ readonly idleTimeout: number;
24
+ /** Override the event target for scroll/wheel listeners (default: viewport) */
25
+ readonly scrollTarget?: EventTarget;
26
+ /** Called synchronously on scroll — triggers the 2-phase pipeline */
27
+ readonly onFrame: () => void;
28
+ /** Called when scrolling becomes idle */
29
+ readonly onIdle: () => void;
30
+ }
31
+ export declare function createScrollHandler(config: ScrollHandlerConfig): ScrollHandler;
32
+ //# sourceMappingURL=scroll.d.ts.map