vlist 1.9.0 → 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,185 @@
1
+ /**
2
+ * vlist v2 — Autosize Plugin
3
+ *
4
+ * Enables dynamic item measurement via ResizeObserver for items with
5
+ * unknown sizes. Items are rendered without an explicit main-axis size,
6
+ * measured once by ResizeObserver, then pinned to their measured size.
7
+ *
8
+ * Priority 5 — runs before grid/masonry (10) so the measured cache
9
+ * is in place before layout plugins consume it.
10
+ *
11
+ * Requires: `item.estimatedHeight` or `item.estimatedWidth` in config
12
+ */
13
+ // =============================================================================
14
+ // Factory
15
+ // =============================================================================
16
+ export function autosize(config) {
17
+ let gap = config?.gap ?? 0;
18
+ let observer = null;
19
+ let storedCtx = null;
20
+ let engineState;
21
+ let hz;
22
+ let sizeProp;
23
+ let estimatedSize;
24
+ const measuredSizes = new Map();
25
+ const elementToIndex = new WeakMap();
26
+ let pendingScrollDelta = 0;
27
+ let pendingContentSizeUpdate = false;
28
+ const BOTTOM_THRESHOLD = 2;
29
+ function sizeFn(index) {
30
+ return measuredSizes.get(index) ?? estimatedSize;
31
+ }
32
+ function isAtBottom() {
33
+ const viewport = storedCtx.dom.viewport;
34
+ const maxScroll = viewport.scrollHeight - viewport.clientHeight;
35
+ return maxScroll > 0 && engineState.scrollPosition >= maxScroll - BOTTOM_THRESHOLD;
36
+ }
37
+ function snapToBottom() {
38
+ const viewport = storedCtx.dom.viewport;
39
+ void viewport.scrollHeight;
40
+ const maxScroll = viewport.scrollHeight - viewport.clientHeight;
41
+ if (maxScroll > engineState.scrollPosition) {
42
+ storedCtx.scrollTo(maxScroll);
43
+ }
44
+ }
45
+ function updateContentSize() {
46
+ storedCtx.updateContentSize(storedCtx.sizeCache.getTotalSize());
47
+ }
48
+ return {
49
+ name: "autosize",
50
+ priority: 5,
51
+ setup(ctx) {
52
+ storedCtx = ctx;
53
+ engineState = ctx.getState();
54
+ hz = ctx.config.horizontal;
55
+ sizeProp = hz ? "width" : "height";
56
+ if (gap === 0)
57
+ gap = ctx.config.gap;
58
+ // Read estimated size from the current sizeCache before replacing it.
59
+ // The initial cache already has gap baked in — read the raw spec size.
60
+ estimatedSize = typeof ctx.rawSizeSpec === "function"
61
+ ? ctx.rawSizeSpec(0) + gap
62
+ : ctx.rawSizeSpec + gap;
63
+ // Replace the fixed sizeCache with a variable one backed by measurements
64
+ ctx.setSizeConfig(sizeFn);
65
+ if (gap > 0) {
66
+ const orig = ctx.sizeCache.getTotalSize;
67
+ ctx.sizeCache.getTotalSize = () => {
68
+ const t = orig();
69
+ return t > 0 ? t - gap : 0;
70
+ };
71
+ }
72
+ // ResizeObserver for measuring items
73
+ observer = new ResizeObserver((entries) => {
74
+ if (engineState.destroyed || !storedCtx)
75
+ return;
76
+ let hasNewMeasurements = false;
77
+ const firstVisible = engineState.startIndex;
78
+ for (const entry of entries) {
79
+ const el = entry.target;
80
+ const index = elementToIndex.get(el);
81
+ if (index === undefined)
82
+ continue;
83
+ // Verify element wasn't recycled to a different item
84
+ if (el.getAttribute("data-index") !== String(index)) {
85
+ observer.unobserve(el);
86
+ continue;
87
+ }
88
+ if (measuredSizes.has(index))
89
+ continue;
90
+ const boxSize = entry.borderBoxSize[0];
91
+ if (!boxSize)
92
+ continue;
93
+ const newSize = hz ? boxSize.inlineSize : boxSize.blockSize;
94
+ if (newSize <= 0)
95
+ continue;
96
+ const sizeWithGap = newSize + gap;
97
+ const oldSize = estimatedSize;
98
+ measuredSizes.set(index, sizeWithGap);
99
+ hasNewMeasurements = true;
100
+ if (index < firstVisible && sizeWithGap !== oldSize) {
101
+ pendingScrollDelta += sizeWithGap - oldSize;
102
+ }
103
+ observer.unobserve(el);
104
+ // Pin the element to its measured size
105
+ el.style[sizeProp] = `${newSize}px`;
106
+ }
107
+ if (!hasNewMeasurements)
108
+ return;
109
+ const atBottom = isAtBottom();
110
+ // Rebuild prefix sums with new measurements
111
+ ctx.rebuildSizeCache();
112
+ // Apply scroll correction for items above viewport
113
+ if (pendingScrollDelta) {
114
+ ctx.scrollTo(engineState.scrollPosition + pendingScrollDelta);
115
+ pendingScrollDelta = 0;
116
+ }
117
+ const isScrolling = engineState.scrollDirection !== 0;
118
+ const nearEnd = engineState.totalItems > 0
119
+ && engineState.prevRangeEnd >= engineState.totalItems - 1;
120
+ if (atBottom || nearEnd || !isScrolling) {
121
+ updateContentSize();
122
+ pendingContentSizeUpdate = false;
123
+ if (atBottom) {
124
+ snapToBottom();
125
+ }
126
+ }
127
+ else {
128
+ pendingContentSizeUpdate = true;
129
+ }
130
+ ctx.forceRender();
131
+ });
132
+ // Public methods
133
+ ctx.registerMethod("isMeasured", (index) => measuredSizes.has(index));
134
+ ctx.registerMethod("setMeasuredSize", (index, size) => {
135
+ measuredSizes.set(index, size);
136
+ });
137
+ ctx.registerMethod("getMeasuredCount", () => measuredSizes.size);
138
+ // Cleanup
139
+ ctx.registerDestroyHandler(() => {
140
+ if (observer) {
141
+ observer.disconnect();
142
+ observer = null;
143
+ }
144
+ });
145
+ },
146
+ hooks: {
147
+ onCommit(state) {
148
+ if (!observer || !storedCtx)
149
+ return;
150
+ for (let i = 0; i < state.visibleCount; i++) {
151
+ const idx = state.visibleIndices[i];
152
+ if (measuredSizes.has(idx))
153
+ continue;
154
+ const el = storedCtx.getRenderedElement(idx);
155
+ if (!el)
156
+ continue;
157
+ // Clear the explicit size set by phase2Commit so
158
+ // ResizeObserver can measure the natural content size.
159
+ el.style[sizeProp] = "";
160
+ elementToIndex.set(el, idx);
161
+ observer.observe(el);
162
+ }
163
+ },
164
+ onIdle() {
165
+ if (!pendingContentSizeUpdate || !storedCtx)
166
+ return;
167
+ const atBottom = isAtBottom();
168
+ updateContentSize();
169
+ pendingContentSizeUpdate = false;
170
+ if (atBottom) {
171
+ snapToBottom();
172
+ storedCtx.forceRender();
173
+ }
174
+ },
175
+ },
176
+ destroy() {
177
+ if (observer) {
178
+ observer.disconnect();
179
+ observer = null;
180
+ }
181
+ measuredSizes.clear();
182
+ storedCtx = null;
183
+ },
184
+ };
185
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * vlist v2 — Grid Plugin
3
+ */
4
+ export { grid, type GridPluginConfig } from "./plugin";
5
+ export { createGridLayout, type GridConfigWithGroups, } from "./layout";
6
+ export type { GridLayout, GridPosition, ItemRange, } from "./types";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * vlist v2 — Grid Plugin
3
+ */
4
+ export { grid } from "./plugin";
5
+ export { createGridLayout, } from "./layout";
@@ -0,0 +1,275 @@
1
+ /**
2
+ * vlist - Grid Layout
3
+ * Pure O(1) calculations for mapping between flat item indices and grid positions.
4
+ *
5
+ * The grid transforms a flat list into rows:
6
+ * Items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
7
+ * Grid (columns=4):
8
+ * Row 0: [0, 1, 2, 3]
9
+ * Row 1: [4, 5, 6, 7]
10
+ * Row 2: [8, 9] ← partially filled last row
11
+ *
12
+ * All operations are O(1) — integer division and modulo only.
13
+ */
14
+ // =============================================================================
15
+ // Factory
16
+ // =============================================================================
17
+ /**
18
+ * Create a GridLayout instance.
19
+ *
20
+ * @param config - Grid configuration (columns, gap, optional isHeaderFn)
21
+ * @returns GridLayout with O(1) mapping functions (or groups-aware if isHeaderFn provided)
22
+ */
23
+ export const createGridLayout = (config) => {
24
+ let columns = Math.max(1, Math.floor(config.columns));
25
+ let gap = config.gap ?? 0;
26
+ let isHeaderFn = config.isHeaderFn;
27
+ // Reusable position object to avoid allocation on hot paths
28
+ const reusablePosition = { row: 0, col: 0 };
29
+ /**
30
+ * Total rows for a given item count.
31
+ * When isHeaderFn is provided, headers force new rows.
32
+ */
33
+ const getTotalRows = (totalItems) => {
34
+ if (totalItems <= 0)
35
+ return 0;
36
+ if (!isHeaderFn) {
37
+ return Math.ceil(totalItems / columns);
38
+ }
39
+ // Groups-aware calculation
40
+ let row = 0;
41
+ let colInRow = 0;
42
+ let headerCount = 0;
43
+ for (let i = 0; i < totalItems; i++) {
44
+ if (isHeaderFn(i)) {
45
+ headerCount++;
46
+ // Header: start new row if not at beginning
47
+ if (colInRow) {
48
+ row++;
49
+ colInRow = 0;
50
+ }
51
+ // Header occupies its own row
52
+ row++;
53
+ colInRow = 0;
54
+ }
55
+ else {
56
+ // Regular item
57
+ colInRow++;
58
+ if (colInRow >= columns) {
59
+ row++;
60
+ colInRow = 0;
61
+ }
62
+ }
63
+ }
64
+ // Add final row if there are items in it
65
+ if (colInRow) {
66
+ row++;
67
+ }
68
+ return row;
69
+ };
70
+ /**
71
+ * Get row/col position for a flat item index.
72
+ * Reuses a single object to reduce GC pressure on scroll hot path.
73
+ */
74
+ const getPosition = (itemIndex) => {
75
+ reusablePosition.row = getRow(itemIndex);
76
+ reusablePosition.col = getCol(itemIndex);
77
+ return reusablePosition;
78
+ };
79
+ /**
80
+ * Get row index for a flat item index — O(1)
81
+ * When isHeaderFn is provided, headers force new rows and span all columns.
82
+ */
83
+ const getRow = (itemIndex) => {
84
+ if (!isHeaderFn) {
85
+ return Math.floor(itemIndex / columns);
86
+ }
87
+ // Groups-aware calculation
88
+ let row = 0;
89
+ let colInRow = 0;
90
+ for (let i = 0; i <= itemIndex; i++) {
91
+ const isHeader = isHeaderFn(i);
92
+ if (isHeader) {
93
+ // Header: start new row if not at beginning
94
+ if (colInRow) {
95
+ row++;
96
+ colInRow = 0;
97
+ }
98
+ // Header occupies its own row
99
+ if (i === itemIndex) {
100
+ return row;
101
+ }
102
+ row++;
103
+ colInRow = 0;
104
+ }
105
+ else {
106
+ // Regular item
107
+ if (i === itemIndex) {
108
+ return row;
109
+ }
110
+ colInRow++;
111
+ if (colInRow >= columns) {
112
+ row++;
113
+ colInRow = 0;
114
+ }
115
+ }
116
+ }
117
+ console.warn(`⚠️ getRow(${itemIndex}) fell through - returning ${row}`);
118
+ return row;
119
+ };
120
+ /**
121
+ * Get column index for a flat item index — O(1)
122
+ * Headers always return col 0 when isHeaderFn is provided.
123
+ */
124
+ const getCol = (itemIndex) => {
125
+ if (!isHeaderFn) {
126
+ return itemIndex % columns;
127
+ }
128
+ // Headers always at column 0
129
+ if (isHeaderFn(itemIndex)) {
130
+ return 0;
131
+ }
132
+ // Calculate column for regular items
133
+ let colInRow = 0;
134
+ for (let i = 0; i <= itemIndex; i++) {
135
+ const isHeader = isHeaderFn(i);
136
+ if (isHeader) {
137
+ // Header: reset column counter
138
+ colInRow = 0;
139
+ }
140
+ else {
141
+ if (i === itemIndex) {
142
+ return colInRow;
143
+ }
144
+ colInRow++;
145
+ if (colInRow >= columns) {
146
+ colInRow = 0;
147
+ }
148
+ }
149
+ }
150
+ return colInRow;
151
+ };
152
+ /**
153
+ * Get the flat item range [start, end] for a range of rows.
154
+ *
155
+ * rowStart and rowEnd are inclusive row indices.
156
+ * The returned end is clamped to totalItems - 1.
157
+ * When isHeaderFn is provided, this accounts for headers disrupting the grid flow.
158
+ */
159
+ const getItemRange = (rowStart, rowEnd, totalItems) => {
160
+ if (totalItems <= 0)
161
+ return { start: 0, end: -1 };
162
+ if (!isHeaderFn) {
163
+ // Simple O(1) calculation for regular grids
164
+ const start = Math.max(0, rowStart * columns);
165
+ const end = Math.min(totalItems - 1, (rowEnd + 1) * columns - 1);
166
+ return { start, end };
167
+ }
168
+ // Groups-aware calculation - find items that fall in the row range
169
+ let start = -1;
170
+ let end = -1;
171
+ let currentRow = 0;
172
+ let colInRow = 0;
173
+ for (let i = 0; i < totalItems; i++) {
174
+ const isHeader = isHeaderFn(i);
175
+ if (isHeader) {
176
+ // Header: start new row if not at beginning
177
+ if (colInRow) {
178
+ currentRow++;
179
+ colInRow = 0;
180
+ }
181
+ // Check if this header's row is in range
182
+ if (currentRow >= rowStart && currentRow <= rowEnd) {
183
+ if (start < 0)
184
+ start = i;
185
+ end = i;
186
+ }
187
+ currentRow++;
188
+ colInRow = 0;
189
+ }
190
+ else {
191
+ // Regular item
192
+ if (currentRow >= rowStart && currentRow <= rowEnd) {
193
+ if (start < 0)
194
+ start = i;
195
+ end = i;
196
+ }
197
+ colInRow++;
198
+ if (colInRow >= columns) {
199
+ currentRow++;
200
+ colInRow = 0;
201
+ }
202
+ }
203
+ // Early exit if we're past the end row
204
+ if (currentRow > rowEnd && !colInRow) {
205
+ break;
206
+ }
207
+ }
208
+ // If no items found in range, return empty range
209
+ if (start < 0) {
210
+ return { start: 0, end: -1 };
211
+ }
212
+ return { start, end };
213
+ };
214
+ /**
215
+ * Get the flat item index from a row and column.
216
+ * Returns -1 if the position is out of bounds.
217
+ */
218
+ const getItemIndex = (row, col, totalItems) => {
219
+ if (col < 0 || col >= columns)
220
+ return -1;
221
+ const index = row * columns + col;
222
+ if (index < 0 || index >= totalItems)
223
+ return -1;
224
+ return index;
225
+ };
226
+ /**
227
+ * Calculate column width given the container's inner width.
228
+ * Distributes gaps evenly: totalGapWidth = (columns - 1) * gap
229
+ * columnWidth = (containerWidth - totalGapWidth) / columns
230
+ */
231
+ const getColumnWidth = (containerWidth) => {
232
+ const totalGap = (columns - 1) * gap;
233
+ return Math.max(0, (containerWidth - totalGap) / columns);
234
+ };
235
+ /**
236
+ * Calculate the X pixel offset for a given column index.
237
+ * offset = col * (columnWidth + gap)
238
+ */
239
+ const getColumnOffset = (col, containerWidth) => {
240
+ const colWidth = getColumnWidth(containerWidth);
241
+ return col * (colWidth + gap);
242
+ };
243
+ /**
244
+ * Update grid configuration without recreating the layout.
245
+ * This is more efficient than destroying and recreating.
246
+ */
247
+ const updateConfig = (newConfig) => {
248
+ if (newConfig.columns !== undefined) {
249
+ columns = Math.max(1, Math.floor(newConfig.columns));
250
+ }
251
+ if (newConfig.gap !== undefined) {
252
+ gap = newConfig.gap;
253
+ }
254
+ if (newConfig.isHeaderFn !== undefined) {
255
+ isHeaderFn = newConfig.isHeaderFn;
256
+ }
257
+ };
258
+ return {
259
+ get columns() {
260
+ return columns;
261
+ },
262
+ get gap() {
263
+ return gap;
264
+ },
265
+ update: updateConfig,
266
+ getTotalRows,
267
+ getPosition,
268
+ getRow,
269
+ getCol,
270
+ getItemRange,
271
+ getItemIndex,
272
+ getColumnWidth,
273
+ getColumnOffset,
274
+ };
275
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * vlist v2 — Grid Plugin
3
+ *
4
+ * Switches from list layout to a 2D grid with configurable columns and gap.
5
+ * Priority 10 — runs before selection (50) so layout is ready for other plugins.
6
+ *
7
+ * Architecture:
8
+ * - Replaces the default 1D render pipeline with a grid-aware render
9
+ * - Size cache operates in ROW space (each row has height = itemHeight + gap)
10
+ * - Visible range is calculated in row space, then expanded to flat item indices
11
+ * - Items are positioned with translate(colOffset, rowOffset)
12
+ *
13
+ * Restrictions:
14
+ * - Cannot be combined with masonry or table plugins
15
+ */
16
+ import type { VListItem } from "../../types";
17
+ import type { VListPlugin } from "../../core/types";
18
+ export interface GridPluginConfig {
19
+ columns: number;
20
+ gap?: number;
21
+ }
22
+ export declare function grid<T extends VListItem = VListItem>(config: GridPluginConfig): VListPlugin<T>;
23
+ //# sourceMappingURL=plugin.d.ts.map