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,246 @@
1
+ /**
2
+ * vlist - Async Group Bridge
3
+ * Virtual group layer that bridges withAsync's sparse data manager with
4
+ * withGroups' layout system.
5
+ *
6
+ * Instead of physically inserting header pseudo-items into the data array
7
+ * (which is impossible with sparse storage), this bridge:
8
+ * - Observes items as they load via onItemsLoaded callbacks
9
+ * - Discovers group boundaries incrementally from loaded data
10
+ * - Maps between "data indices" (what the async manager knows) and
11
+ * "layout indices" (what the renderer sees, including header slots)
12
+ * - Provides header/item resolution at render time
13
+ *
14
+ * Group boundaries are only computed for contiguous loaded ranges.
15
+ * Unloaded gaps have no headers — placeholders render as regular items.
16
+ * When gaps fill in, boundaries are recomputed.
17
+ */
18
+ import { findGroupByDataIndex, findGroupByLayoutIndex } from "./layout";
19
+ // =============================================================================
20
+ // Factory
21
+ // =============================================================================
22
+ const EMPTY_GROUP = {
23
+ key: "",
24
+ groupIndex: 0,
25
+ headerLayoutIndex: 0,
26
+ firstDataIndex: 0,
27
+ count: 0,
28
+ };
29
+ /**
30
+ * Create an async group bridge.
31
+ *
32
+ * The bridge discovers group boundaries incrementally as items load from
33
+ * the async adapter. It maintains a mapping between data indices (sparse
34
+ * storage) and layout indices (data items + group headers).
35
+ *
36
+ * @param config - Bridge configuration
37
+ * @param getItem - Item accessor from the async data manager
38
+ * @param isItemLoaded - Check if an item at index is loaded (not placeholder)
39
+ */
40
+ export const createAsyncGroupBridge = (config, _getItem, _isItemLoaded) => {
41
+ let groups = [];
42
+ let dataTotal = 0;
43
+ let totalEntries = 0;
44
+ // Track group key for each loaded data index.
45
+ // This survives across onItemsLoaded calls so we can detect
46
+ // cross-page group boundaries.
47
+ const groupKeyByIndex = new Map();
48
+ /**
49
+ * Rebuild group boundaries from all known group keys.
50
+ *
51
+ * Scans from index 0 to dataTotal-1. For loaded items, uses the cached
52
+ * group key. For unloaded items, skips them — no header is inserted at
53
+ * load/unload boundaries.
54
+ *
55
+ * This runs on each onItemsLoaded. It's O(dataTotal) in theory but
56
+ * only does map lookups per index — fast in practice.
57
+ */
58
+ const rebuildGroups = () => {
59
+ groups = [];
60
+ if (dataTotal === 0 || groupKeyByIndex.size === 0) {
61
+ totalEntries = dataTotal;
62
+ return;
63
+ }
64
+ let currentKey = null;
65
+ let groupStart = -1;
66
+ // Running header count — each discovered group adds 1 header
67
+ let headerCount = 0;
68
+ for (let i = 0; i < dataTotal; i++) {
69
+ const key = groupKeyByIndex.get(i);
70
+ if (key === undefined) {
71
+ // Unloaded item — close any open group
72
+ if (currentKey !== null && groupStart >= 0) {
73
+ const count = i - groupStart;
74
+ groups.push({
75
+ key: currentKey,
76
+ groupIndex: groups.length,
77
+ headerLayoutIndex: groupStart + headerCount,
78
+ firstDataIndex: groupStart,
79
+ count,
80
+ });
81
+ headerCount++;
82
+ currentKey = null;
83
+ groupStart = -1;
84
+ }
85
+ continue;
86
+ }
87
+ if (key !== currentKey) {
88
+ // Close previous group if any
89
+ if (currentKey !== null && groupStart >= 0) {
90
+ const count = i - groupStart;
91
+ groups.push({
92
+ key: currentKey,
93
+ groupIndex: groups.length,
94
+ headerLayoutIndex: groupStart + headerCount,
95
+ firstDataIndex: groupStart,
96
+ count,
97
+ });
98
+ headerCount++;
99
+ }
100
+ // Start new group
101
+ currentKey = key;
102
+ groupStart = i;
103
+ }
104
+ }
105
+ // Close last open group
106
+ if (currentKey !== null && groupStart >= 0) {
107
+ const count = dataTotal - groupStart;
108
+ groups.push({
109
+ key: currentKey,
110
+ groupIndex: groups.length,
111
+ headerLayoutIndex: groupStart + headerCount,
112
+ firstDataIndex: groupStart,
113
+ count,
114
+ });
115
+ headerCount++;
116
+ }
117
+ totalEntries = dataTotal + headerCount;
118
+ };
119
+ // ── Public API ──
120
+ const onItemsLoaded = (items, offset, total) => {
121
+ dataTotal = total;
122
+ // Cache group keys for loaded items
123
+ for (let i = 0; i < items.length; i++) {
124
+ const dataIndex = offset + i;
125
+ const item = items[i];
126
+ if (item !== undefined) {
127
+ const key = config.getGroupForIndex(dataIndex, item);
128
+ groupKeyByIndex.set(dataIndex, key);
129
+ }
130
+ }
131
+ rebuildGroups();
132
+ };
133
+ const isHeader = (layoutIndex) => {
134
+ if (groups.length === 0)
135
+ return false;
136
+ const gi = findGroupByLayoutIndex(groups, layoutIndex);
137
+ const group = groups[gi];
138
+ return layoutIndex === group.headerLayoutIndex;
139
+ };
140
+ const getHeaderItem = (layoutIndex) => {
141
+ if (groups.length === 0)
142
+ return undefined;
143
+ const gi = findGroupByLayoutIndex(groups, layoutIndex);
144
+ const group = groups[gi];
145
+ if (layoutIndex !== group.headerLayoutIndex)
146
+ return undefined;
147
+ return {
148
+ id: `__group_header_${group.groupIndex}`,
149
+ __groupHeader: true,
150
+ groupKey: group.key,
151
+ groupIndex: group.groupIndex,
152
+ };
153
+ };
154
+ const layoutToDataIndex = (layoutIndex) => {
155
+ if (groups.length === 0)
156
+ return layoutIndex;
157
+ const gi = findGroupByLayoutIndex(groups, layoutIndex);
158
+ const group = groups[gi];
159
+ if (layoutIndex === group.headerLayoutIndex) {
160
+ return -1;
161
+ }
162
+ const offsetInGroup = layoutIndex - group.headerLayoutIndex - 1;
163
+ return group.firstDataIndex + offsetInGroup;
164
+ };
165
+ const dataToLayoutIndex = (dataIndex) => {
166
+ if (groups.length === 0)
167
+ return dataIndex;
168
+ const gi = findGroupByDataIndex(groups, dataIndex);
169
+ const group = groups[gi];
170
+ const offsetInGroup = dataIndex - group.firstDataIndex;
171
+ return group.headerLayoutIndex + 1 + offsetInGroup;
172
+ };
173
+ const getHeaderHeight = (_groupIndex) => config.headerHeight;
174
+ const getGroupAtLayoutIndex = (layoutIndex) => {
175
+ if (groups.length === 0)
176
+ return EMPTY_GROUP;
177
+ const gi = findGroupByLayoutIndex(groups, layoutIndex);
178
+ return groups[gi];
179
+ };
180
+ const getGroupAtDataIndex = (dataIndex) => {
181
+ if (groups.length === 0)
182
+ return EMPTY_GROUP;
183
+ const gi = findGroupByDataIndex(groups, dataIndex);
184
+ return groups[gi];
185
+ };
186
+ const insertAt = (dataIndex, item) => {
187
+ const newMap = new Map();
188
+ for (const [idx, key] of groupKeyByIndex) {
189
+ if (idx < dataIndex)
190
+ newMap.set(idx, key);
191
+ else
192
+ newMap.set(idx + 1, key);
193
+ }
194
+ groupKeyByIndex.clear();
195
+ for (const [idx, key] of newMap)
196
+ groupKeyByIndex.set(idx, key);
197
+ const key = config.getGroupForIndex(dataIndex, item);
198
+ groupKeyByIndex.set(dataIndex, key);
199
+ dataTotal++;
200
+ rebuildGroups();
201
+ };
202
+ const removeAt = (dataIndex) => {
203
+ // Remove the key at dataIndex and shift all subsequent keys down by 1
204
+ const newMap = new Map();
205
+ for (const [idx, key] of groupKeyByIndex) {
206
+ if (idx < dataIndex)
207
+ newMap.set(idx, key);
208
+ else if (idx > dataIndex)
209
+ newMap.set(idx - 1, key);
210
+ // idx === dataIndex is dropped
211
+ }
212
+ groupKeyByIndex.clear();
213
+ for (const [idx, key] of newMap)
214
+ groupKeyByIndex.set(idx, key);
215
+ dataTotal--;
216
+ rebuildGroups();
217
+ };
218
+ const reset = () => {
219
+ groups = [];
220
+ dataTotal = 0;
221
+ totalEntries = 0;
222
+ groupKeyByIndex.clear();
223
+ };
224
+ return {
225
+ onItemsLoaded,
226
+ get totalEntries() {
227
+ return totalEntries;
228
+ },
229
+ get groupCount() {
230
+ return groups.length;
231
+ },
232
+ get groups() {
233
+ return groups;
234
+ },
235
+ isHeader,
236
+ getHeaderItem,
237
+ layoutToDataIndex,
238
+ dataToLayoutIndex,
239
+ getHeaderHeight,
240
+ getGroupAtLayoutIndex,
241
+ getGroupAtDataIndex,
242
+ insertAt,
243
+ removeAt,
244
+ reset,
245
+ };
246
+ };
@@ -2,7 +2,7 @@
2
2
  * vlist - Groups Domain
3
3
  * Sticky headers and grouped lists
4
4
  */
5
- export { withGroups, type GroupsFeatureConfig } from "./feature";
5
+ export { groups, type GroupsPluginConfig } from "./plugin";
6
6
  export type { GroupsConfig, GroupBoundary, LayoutEntry, GroupHeaderItem, GroupLayout, StickyHeader, } from "./types";
7
7
  export { isGroupHeader } from "./types";
8
8
  export { createGroupLayout, buildLayoutItems, createGroupedSizeFn, } from "./layout";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * vlist - Groups Domain
3
+ * Sticky headers and grouped lists
4
+ */
5
+ // v2 Plugin
6
+ export { groups } from "./plugin";
7
+ export { isGroupHeader } from "./types";
8
+ // Layout
9
+ export { createGroupLayout, buildLayoutItems, createGroupedSizeFn, } from "./layout";
10
+ // Sticky Header
11
+ export { createStickyHeader } from "./sticky";
12
+ // Async Bridge
13
+ export { createAsyncGroupBridge } from "./async-bridge";
@@ -0,0 +1,294 @@
1
+ /**
2
+ * vlist - Group Layout
3
+ * Computes group boundaries and maps between data indices and layout indices.
4
+ *
5
+ * The layout transforms a flat items array into a "layout" that includes
6
+ * group header pseudo-items interspersed at group boundaries:
7
+ *
8
+ * Data: [item0, item1, item2, item3, item4, item5]
9
+ * Groups: [ A, A, A, B, B, C ]
10
+ * Layout: [headerA, item0, item1, item2, headerB, item3, item4, headerC, item5]
11
+ * Index: [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
12
+ *
13
+ * All lookups are O(log g) where g = number of groups, using binary search
14
+ * on the sorted group boundaries array.
15
+ */
16
+ // =============================================================================
17
+ // Binary Search Helpers
18
+ // =============================================================================
19
+ /**
20
+ * Find the last group whose headerLayoutIndex <= layoutIndex.
21
+ * Returns the group's index in the groups array.
22
+ */
23
+ export const findGroupByLayoutIndex = (groups, layoutIndex) => {
24
+ let lo = 0;
25
+ let hi = groups.length - 1;
26
+ while (lo < hi) {
27
+ const mid = (lo + hi + 1) >>> 1;
28
+ if (groups[mid].headerLayoutIndex <= layoutIndex) {
29
+ lo = mid;
30
+ }
31
+ else {
32
+ hi = mid - 1;
33
+ }
34
+ }
35
+ return lo;
36
+ };
37
+ /**
38
+ * Find the last group whose firstDataIndex <= dataIndex.
39
+ * Returns the group's index in the groups array.
40
+ */
41
+ export const findGroupByDataIndex = (groups, dataIndex) => {
42
+ let lo = 0;
43
+ let hi = groups.length - 1;
44
+ while (lo < hi) {
45
+ const mid = (lo + hi + 1) >>> 1;
46
+ if (groups[mid].firstDataIndex <= dataIndex) {
47
+ lo = mid;
48
+ }
49
+ else {
50
+ hi = mid - 1;
51
+ }
52
+ }
53
+ return lo;
54
+ };
55
+ // =============================================================================
56
+ // Layout Builder
57
+ // =============================================================================
58
+ /**
59
+ * Build the groups array from the items using getGroupForIndex.
60
+ *
61
+ * Items MUST be pre-sorted by group — a new group boundary is created
62
+ * whenever getGroupForIndex returns a different value than the previous call.
63
+ */
64
+ const buildGroups = (itemCount, getGroupForIndex, getItem) => {
65
+ if (itemCount === 0)
66
+ return [];
67
+ const groups = [];
68
+ let currentKey = null;
69
+ let groupStart = 0;
70
+ let headerLayoutIndex = 0;
71
+ for (let i = 0; i < itemCount; i++) {
72
+ const item = getItem?.(i);
73
+ // Skip unloaded items — they extend the current group without
74
+ // creating boundaries. This avoids "Unknown" headers for placeholders.
75
+ if (!item)
76
+ continue;
77
+ const key = getGroupForIndex(i, item);
78
+ if (currentKey === null) {
79
+ // First loaded item — start first group
80
+ currentKey = key;
81
+ groupStart = 0;
82
+ // All items before this one (unloaded) belong to this group
83
+ }
84
+ else if (key !== currentKey) {
85
+ const count = i - groupStart;
86
+ groups.push({
87
+ key: currentKey,
88
+ groupIndex: groups.length,
89
+ headerLayoutIndex,
90
+ firstDataIndex: groupStart,
91
+ count,
92
+ });
93
+ headerLayoutIndex = headerLayoutIndex + 1 + count;
94
+ currentKey = key;
95
+ groupStart = i;
96
+ }
97
+ }
98
+ if (currentKey !== null) {
99
+ // Close the last group — includes all remaining unloaded items
100
+ groups.push({
101
+ key: currentKey,
102
+ groupIndex: groups.length,
103
+ headerLayoutIndex,
104
+ firstDataIndex: groupStart,
105
+ count: itemCount - groupStart,
106
+ });
107
+ }
108
+ return groups;
109
+ };
110
+ // =============================================================================
111
+ // Layout Items Builder
112
+ // =============================================================================
113
+ /**
114
+ * Build the transformed layout items array with header pseudo-items inserted
115
+ * at group boundaries.
116
+ *
117
+ * @param items - Original data items
118
+ * @param groups - Computed group boundaries
119
+ * @returns Array of items and header pseudo-items in layout order
120
+ */
121
+ export const buildLayoutItems = (items, groups) => {
122
+ if (items.length === 0 || groups.length === 0)
123
+ return [];
124
+ const totalEntries = items.length + groups.length;
125
+ const result = new Array(totalEntries);
126
+ let layoutIdx = 0;
127
+ for (const group of groups) {
128
+ // Insert header pseudo-item
129
+ result[layoutIdx] = {
130
+ id: `__group_header_${group.groupIndex}`,
131
+ __groupHeader: true,
132
+ groupKey: group.key,
133
+ groupIndex: group.groupIndex,
134
+ };
135
+ layoutIdx++;
136
+ // Insert data items for this group
137
+ for (let i = 0; i < group.count; i++) {
138
+ result[layoutIdx] = items[group.firstDataIndex + i];
139
+ layoutIdx++;
140
+ }
141
+ }
142
+ return result;
143
+ };
144
+ // =============================================================================
145
+ // Height Function Builder
146
+ // =============================================================================
147
+ /**
148
+ * Create a size function for the layout (items + headers).
149
+ *
150
+ * Maps layout indices to sizes, accounting for both group headers and data items.
151
+ *
152
+ * @param layout - The group layout instance
153
+ * @param itemSize - Original item size config (number or function)
154
+ * @returns A size function (layoutIndex) => number suitable for SizeCache
155
+ */
156
+ export const createGroupedSizeFn = (layout, itemSize, sticky = false) => {
157
+ const getItemSize = typeof itemSize === "number"
158
+ ? (_dataIndex) => itemSize
159
+ : itemSize;
160
+ return (layoutIndex) => {
161
+ const entry = layout.getEntry(layoutIndex);
162
+ if (entry.type === "header") {
163
+ // When sticky headers are active the first group's inline header
164
+ // is redundant (the sticky header already shows it). Collapse it
165
+ // to 0 so it occupies no space in the layout.
166
+ if (sticky && entry.group.groupIndex === 0)
167
+ return 0;
168
+ return layout.getHeaderHeight(entry.group.groupIndex);
169
+ }
170
+ return getItemSize(entry.dataIndex);
171
+ };
172
+ };
173
+ // =============================================================================
174
+ // GroupLayout Factory
175
+ // =============================================================================
176
+ /**
177
+ * Create a GroupLayout instance.
178
+ *
179
+ * The layout computes group boundaries from items and provides efficient
180
+ * O(log g) mappings between data indices and layout indices.
181
+ *
182
+ * @param itemCount - Number of data items
183
+ * @param config - Groups configuration
184
+ */
185
+ export const createGroupLayout = (itemCount, config, getItem) => {
186
+ let groups = buildGroups(itemCount, config.getGroupForIndex, getItem);
187
+ let totalEntries = itemCount + groups.length;
188
+ // Pre-compute header sizes — resolve from height, width, or legacy headerHeight
189
+ const headerHeightConfig = config.header?.height ?? config.header?.width ?? config.headerHeight;
190
+ const getHeaderHeight = typeof headerHeightConfig === "number"
191
+ ? (_groupIndex) => headerHeightConfig
192
+ : (groupIndex) => {
193
+ const group = groups[groupIndex];
194
+ if (!group)
195
+ return 0;
196
+ return headerHeightConfig(group.key, groupIndex);
197
+ };
198
+ // =========================================================================
199
+ // Public API
200
+ // =========================================================================
201
+ const getEntry = (layoutIndex) => {
202
+ if (groups.length === 0) {
203
+ // Fallback: shouldn't happen if totalEntries > 0
204
+ return {
205
+ type: "item",
206
+ dataIndex: layoutIndex,
207
+ group: {
208
+ key: "",
209
+ groupIndex: 0,
210
+ headerLayoutIndex: 0,
211
+ firstDataIndex: 0,
212
+ count: 0,
213
+ },
214
+ };
215
+ }
216
+ const gi = findGroupByLayoutIndex(groups, layoutIndex);
217
+ const group = groups[gi];
218
+ if (layoutIndex === group.headerLayoutIndex) {
219
+ return { type: "header", group };
220
+ }
221
+ // It's a data item within this group
222
+ const offsetInGroup = layoutIndex - group.headerLayoutIndex - 1;
223
+ const dataIndex = group.firstDataIndex + offsetInGroup;
224
+ return { type: "item", dataIndex, group };
225
+ };
226
+ const layoutToDataIndex = (layoutIndex) => {
227
+ if (groups.length === 0)
228
+ return layoutIndex;
229
+ const gi = findGroupByLayoutIndex(groups, layoutIndex);
230
+ const group = groups[gi];
231
+ if (layoutIndex === group.headerLayoutIndex) {
232
+ return -1; // It's a header
233
+ }
234
+ const offsetInGroup = layoutIndex - group.headerLayoutIndex - 1;
235
+ return group.firstDataIndex + offsetInGroup;
236
+ };
237
+ const dataToLayoutIndex = (dataIndex) => {
238
+ if (groups.length === 0)
239
+ return dataIndex;
240
+ const gi = findGroupByDataIndex(groups, dataIndex);
241
+ const group = groups[gi];
242
+ // Layout index = header layout index + 1 (skip header) + offset within group
243
+ const offsetInGroup = dataIndex - group.firstDataIndex;
244
+ return group.headerLayoutIndex + 1 + offsetInGroup;
245
+ };
246
+ const getGroupAtLayoutIndex = (layoutIndex) => {
247
+ if (groups.length === 0) {
248
+ return {
249
+ key: "",
250
+ groupIndex: 0,
251
+ headerLayoutIndex: 0,
252
+ firstDataIndex: 0,
253
+ count: 0,
254
+ };
255
+ }
256
+ const gi = findGroupByLayoutIndex(groups, layoutIndex);
257
+ return groups[gi];
258
+ };
259
+ const getGroupAtDataIndex = (dataIndex) => {
260
+ if (groups.length === 0) {
261
+ return {
262
+ key: "",
263
+ groupIndex: 0,
264
+ headerLayoutIndex: 0,
265
+ firstDataIndex: 0,
266
+ count: 0,
267
+ };
268
+ }
269
+ const gi = findGroupByDataIndex(groups, dataIndex);
270
+ return groups[gi];
271
+ };
272
+ const rebuild = (newItemCount, newGetItem) => {
273
+ groups = buildGroups(newItemCount, config.getGroupForIndex, newGetItem ?? getItem);
274
+ totalEntries = newItemCount + groups.length;
275
+ };
276
+ return {
277
+ get totalEntries() {
278
+ return totalEntries;
279
+ },
280
+ get groupCount() {
281
+ return groups.length;
282
+ },
283
+ get groups() {
284
+ return groups;
285
+ },
286
+ getEntry,
287
+ layoutToDataIndex,
288
+ dataToLayoutIndex,
289
+ getGroupAtLayoutIndex,
290
+ getGroupAtDataIndex,
291
+ getHeaderHeight,
292
+ rebuild,
293
+ };
294
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * vlist v2 — Groups Plugin
3
+ *
4
+ * Adds grouped lists with sticky headers.
5
+ * Priority 10 — runs before selection (50).
6
+ *
7
+ * Architecture:
8
+ * - Transforms items list: inserts group header pseudo-items at group boundaries
9
+ * - Replaces size function: headers use header height, items use item height
10
+ * - Replaces render pipeline: handles grouped layout rendering
11
+ * - Sticky header: floating header that updates on scroll
12
+ * - CSS class: adds .vlist--grouped to root
13
+ *
14
+ * Restrictions:
15
+ * - Items must be pre-sorted by group
16
+ */
17
+ import type { VListItem, GroupsConfig } from "../../types";
18
+ import type { VListPlugin } from "../../core/types";
19
+ export interface GroupsPluginConfig extends GroupsConfig {
20
+ }
21
+ export declare function groups<T extends VListItem = VListItem>(config: GroupsPluginConfig): VListPlugin<T>;
22
+ //# sourceMappingURL=plugin.d.ts.map