foldkit 0.78.0 → 0.79.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.
@@ -68,8 +68,26 @@ export declare const update: (model: Model, message: Message) => readonly [Model
68
68
  *
69
69
  * Should be called after the container has rendered. If the container is not
70
70
  * yet in the DOM the Command silently no-ops (the model still transitions
71
- * through `ScrollingToIndex` → `Idle` via the version-matched completion). */
71
+ * through `ScrollingToIndex` → `Idle` via the version-matched completion).
72
+ *
73
+ * Assumes uniform row heights: target scroll position is computed as
74
+ * `index * model.rowHeightPx`. For variable-height rows, use
75
+ * `scrollToIndexVariable`. */
72
76
  export declare const scrollToIndex: (model: Model, index: number) => readonly [Model, ReadonlyArray<Command.Command<Message>>];
77
+ /** Variable-height counterpart of `scrollToIndex`. Walks the heights of items
78
+ * before `index` to compute the target `scrollTop`. Use this when rendering
79
+ * the list with `itemToRowHeightPx`; use `scrollToIndex` for uniform heights.
80
+ *
81
+ * Out-of-range indices clamp to the corresponding edge: negative or zero
82
+ * scrolls to the top, indices past the end scroll past the last row.
83
+ *
84
+ * Note: when restoring `initialScrollTop` on the first measurement of a
85
+ * variable-height list, the runtime falls back to uniform-height math (using
86
+ * `model.rowHeightPx`) because items aren't reachable from the `update`
87
+ * function. Consumers who need an accurate initial scroll on a
88
+ * variable-height list should call `scrollToIndexVariable` after the first
89
+ * `MeasuredContainer` arrives. */
90
+ export declare const scrollToIndexVariable: <Item>(model: Model, items: ReadonlyArray<Item>, itemToRowHeightPx: (item: Item, index: number) => number, index: number) => readonly [Model, ReadonlyArray<Command.Command<Message>>];
73
91
  /** Slice of the data array that the view should render, plus the spacer
74
92
  * heights that keep the scrollbar physically correct. The first row in the
75
93
  * slice corresponds to data index `startIndex`. */
@@ -82,10 +100,25 @@ export type VisibleWindow = Readonly<{
82
100
  /** Computes the visible slice of a data array given the current scroll
83
101
  * position, container height, row height, and an overscan buffer.
84
102
  *
103
+ * Assumes uniform row heights via `model.rowHeightPx`. For variable-height
104
+ * rows, use `visibleWindowVariable`.
105
+ *
85
106
  * Returns `Option.none()` when the container has not yet been measured;
86
107
  * callers should render a placeholder (or `Html.empty`) and wait for the
87
108
  * first `MeasuredContainer` message. */
88
109
  export declare const visibleWindow: (model: Model, itemCount: number, overscan: number) => Option.Option<VisibleWindow>;
110
+ /** Variable-height counterpart of `visibleWindow`. Walks the heights of every
111
+ * item to build a prefix-sum array, then locates the visible slice with two
112
+ * linear searches.
113
+ *
114
+ * Cost is O(N) per call, walking the whole `items` array once to build the
115
+ * prefix sums. For lists in the 10k-item range, this comfortably fits inside
116
+ * a 60Hz scroll budget. Larger lists or hotter scroll paths can layer a
117
+ * prefix-sum cache invalidated when items change; that lives behind the same
118
+ * return shape so consumers don't have to know.
119
+ *
120
+ * Returns `Option.none()` when the container has not yet been measured. */
121
+ export declare const visibleWindowVariable: <Item>(model: Model, items: ReadonlyArray<Item>, itemToRowHeightPx: (item: Item, index: number) => number, overscan: number) => Option.Option<VisibleWindow>;
89
122
  /** Schema describing the subscription dependencies for container scroll and
90
123
  * resize tracking. */
91
124
  export declare const SubscriptionDeps: S.Struct<{
@@ -150,6 +183,14 @@ export type ViewConfig<Message, Item> = Readonly<{
150
183
  items: ReadonlyArray<Item>;
151
184
  itemToKey: (item: Item, index: number) => string;
152
185
  itemToView: (item: Item, index: number) => Html;
186
+ /** Optional per-item row height in pixels. When provided, the list renders
187
+ * with variable-height rows: each row's wrapper takes the height returned
188
+ * by this callback, and scroll math walks the items to compute the visible
189
+ * slice and spacers. When absent, all rows use `model.rowHeightPx`. Use
190
+ * this for tables with wrapping cells, taller detail rows, or any list
191
+ * where rows differ. Prefer the uniform `rowHeightPx` path when row
192
+ * heights are stable: it avoids the per-render walk over `items`. */
193
+ itemToRowHeightPx?: (item: Item, index: number) => number;
153
194
  /** Number of rows rendered above and below the visible viewport. Higher
154
195
  * values smooth out fast scroll at the cost of mounting more DOM. Default
155
196
  * is 5; react-window uses 1 and react-virtualized uses 3. Pick a value
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/index.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { Array, Effect, Match as M, Number, Option, Schema as S, Stream, } from 'effect';
1
+ import { Array, Effect, Match as M, Number, Option, Schema as S, Stream, pipe, } from 'effect';
2
2
  import * as Command from '../../command/index.js';
3
3
  import { createLazy, html, } from '../../html/index.js';
4
4
  import { m } from '../../message/index.js';
@@ -110,6 +110,16 @@ export const update = (model, message) => M.value(message).pipe(M.withReturnType
110
110
  }
111
111
  },
112
112
  }));
113
+ const buildScrollToIndex = (model, index, targetScrollTop) => {
114
+ const nextVersion = Number.increment(model.pendingScrollVersion);
115
+ return [
116
+ evo(model, {
117
+ pendingScrollVersion: () => nextVersion,
118
+ pendingScroll: () => ScrollingToIndex({ index, version: nextVersion }),
119
+ }),
120
+ [applyScroll(model.id, targetScrollTop, nextVersion)],
121
+ ];
122
+ };
113
123
  /** Programmatically scrolls the container so the row at `index` is visible.
114
124
  * Returns the next model and a Command that mutates `element.scrollTop`. The
115
125
  * natural scroll event then flows back through `ScrolledContainer` and the
@@ -121,22 +131,42 @@ export const update = (model, message) => M.value(message).pipe(M.withReturnType
121
131
  *
122
132
  * Should be called after the container has rendered. If the container is not
123
133
  * yet in the DOM the Command silently no-ops (the model still transitions
124
- * through `ScrollingToIndex` → `Idle` via the version-matched completion). */
125
- export const scrollToIndex = (model, index) => {
126
- const nextVersion = Number.increment(model.pendingScrollVersion);
127
- const targetScrollTop = index * model.rowHeightPx;
128
- return [
129
- evo(model, {
130
- pendingScrollVersion: () => nextVersion,
131
- pendingScroll: () => ScrollingToIndex({ index, version: nextVersion }),
132
- }),
133
- [applyScroll(model.id, targetScrollTop, nextVersion)],
134
- ];
134
+ * through `ScrollingToIndex` → `Idle` via the version-matched completion).
135
+ *
136
+ * Assumes uniform row heights: target scroll position is computed as
137
+ * `index * model.rowHeightPx`. For variable-height rows, use
138
+ * `scrollToIndexVariable`. */
139
+ export const scrollToIndex = (model, index) => buildScrollToIndex(model, index, index * model.rowHeightPx);
140
+ /** Variable-height counterpart of `scrollToIndex`. Walks the heights of items
141
+ * before `index` to compute the target `scrollTop`. Use this when rendering
142
+ * the list with `itemToRowHeightPx`; use `scrollToIndex` for uniform heights.
143
+ *
144
+ * Out-of-range indices clamp to the corresponding edge: negative or zero
145
+ * scrolls to the top, indices past the end scroll past the last row.
146
+ *
147
+ * Note: when restoring `initialScrollTop` on the first measurement of a
148
+ * variable-height list, the runtime falls back to uniform-height math (using
149
+ * `model.rowHeightPx`) because items aren't reachable from the `update`
150
+ * function. Consumers who need an accurate initial scroll on a
151
+ * variable-height list should call `scrollToIndexVariable` after the first
152
+ * `MeasuredContainer` arrives. */
153
+ export const scrollToIndexVariable = (model, items, itemToRowHeightPx, index) => {
154
+ const cumulativeOffsets = prefixSum(items, itemToRowHeightPx);
155
+ const targetScrollTop = pipe(cumulativeOffsets, Array.get(Math.max(0, index)), Option.getOrElse(() => lastOrZero(cumulativeOffsets)));
156
+ return buildScrollToIndex(model, index, targetScrollTop);
135
157
  };
136
158
  const clampIndex = (index, itemCount) => Math.max(0, Math.min(index, itemCount));
159
+ const prefixSum = (items, itemToRowHeightPx) => {
160
+ const heights = Array.map(items, itemToRowHeightPx);
161
+ return Array.scan(heights, 0, (cumulative, height) => cumulative + height);
162
+ };
163
+ const lastOrZero = (values) => pipe(values, Array.last, Option.getOrElse(() => 0));
137
164
  /** Computes the visible slice of a data array given the current scroll
138
165
  * position, container height, row height, and an overscan buffer.
139
166
  *
167
+ * Assumes uniform row heights via `model.rowHeightPx`. For variable-height
168
+ * rows, use `visibleWindowVariable`.
169
+ *
140
170
  * Returns `Option.none()` when the container has not yet been measured;
141
171
  * callers should render a placeholder (or `Html.empty`) and wait for the
142
172
  * first `MeasuredContainer` message. */
@@ -157,6 +187,41 @@ export const visibleWindow = (model, itemCount, overscan) => M.value(model.measu
157
187
  });
158
188
  },
159
189
  }));
190
+ /** Variable-height counterpart of `visibleWindow`. Walks the heights of every
191
+ * item to build a prefix-sum array, then locates the visible slice with two
192
+ * linear searches.
193
+ *
194
+ * Cost is O(N) per call, walking the whole `items` array once to build the
195
+ * prefix sums. For lists in the 10k-item range, this comfortably fits inside
196
+ * a 60Hz scroll budget. Larger lists or hotter scroll paths can layer a
197
+ * prefix-sum cache invalidated when items change; that lives behind the same
198
+ * return shape so consumers don't have to know.
199
+ *
200
+ * Returns `Option.none()` when the container has not yet been measured. */
201
+ export const visibleWindowVariable = (model, items, itemToRowHeightPx, overscan) => M.value(model.measurement).pipe(M.withReturnType(), M.tagsExhaustive({
202
+ Unmeasured: () => Option.none(),
203
+ Measured: ({ containerHeight }) => {
204
+ const itemCount = items.length;
205
+ const cumulativeOffsets = prefixSum(items, itemToRowHeightPx);
206
+ const totalHeight = lastOrZero(cumulativeOffsets);
207
+ const firstVisibleIndex = pipe(cumulativeOffsets, Array.findFirstIndex(Number.greaterThan(model.scrollTop)), Option.match({
208
+ onNone: () => itemCount,
209
+ onSome: index => Math.max(0, index - 1),
210
+ }));
211
+ const lastVisibleIndex = pipe(cumulativeOffsets, Array.findFirstIndex(Number.greaterThanOrEqualTo(model.scrollTop + containerHeight)), Option.getOrElse(() => itemCount));
212
+ const startIndex = clampIndex(firstVisibleIndex - overscan, itemCount);
213
+ const endIndex = clampIndex(lastVisibleIndex + overscan, itemCount);
214
+ const topSpacerHeight = pipe(cumulativeOffsets, Array.get(startIndex), Option.getOrElse(() => 0));
215
+ const offsetAtEnd = pipe(cumulativeOffsets, Array.get(endIndex), Option.getOrElse(() => totalHeight));
216
+ const bottomSpacerHeight = totalHeight - offsetAtEnd;
217
+ return Option.some({
218
+ startIndex,
219
+ endIndex,
220
+ topSpacerHeight,
221
+ bottomSpacerHeight,
222
+ });
223
+ },
224
+ }));
160
225
  // SUBSCRIPTION
161
226
  const containerElement = (id) => Option.fromNullable(document.getElementById(id));
162
227
  /** Schema describing the subscription dependencies for container scroll and
@@ -278,7 +343,7 @@ const DEFAULT_OVERSCAN = 5;
278
343
  * count of currently mounted rows. */
279
344
  export const view = (config) => {
280
345
  const { AriaPosinset, AriaSetsize, Class, DataAttribute, Id, Role, Style, keyed, } = html();
281
- const { model, items, itemToKey, itemToView, overscan = DEFAULT_OVERSCAN, rowElement = 'li', className, attributes = [], } = config;
346
+ const { model, items, itemToKey, itemToView, itemToRowHeightPx, overscan = DEFAULT_OVERSCAN, rowElement = 'li', className, attributes = [], } = config;
282
347
  const containerAttributes = [
283
348
  Id(model.id),
284
349
  Role('list'),
@@ -293,7 +358,13 @@ export const view = (config) => {
293
358
  ...attributes,
294
359
  ];
295
360
  const renderContainer = (children) => keyed('ul')(model.id, containerAttributes, children);
296
- return Option.match(visibleWindow(model, items.length, overscan), {
361
+ const maybeWindow = itemToRowHeightPx !== undefined
362
+ ? visibleWindowVariable(model, items, itemToRowHeightPx, overscan)
363
+ : visibleWindow(model, items.length, overscan);
364
+ const rowHeightFor = (item, dataIndex) => itemToRowHeightPx !== undefined
365
+ ? itemToRowHeightPx(item, dataIndex)
366
+ : model.rowHeightPx;
367
+ return Option.match(maybeWindow, {
297
368
  onNone: () => renderContainer([]),
298
369
  onSome: ({ startIndex, endIndex, topSpacerHeight, bottomSpacerHeight }) => {
299
370
  const visibleItems = items.slice(startIndex, endIndex);
@@ -306,7 +377,10 @@ export const view = (config) => {
306
377
  DataAttribute('virtual-list-item-index', String(dataIndex)),
307
378
  AriaSetsize(items.length),
308
379
  AriaPosinset(dataIndex + 1),
309
- Style({ height: `${model.rowHeightPx}px`, display: 'grid' }),
380
+ Style({
381
+ height: `${rowHeightFor(item, dataIndex)}px`,
382
+ display: 'grid',
383
+ }),
310
384
  ], [itemToView(item, dataIndex)]);
311
385
  });
312
386
  return renderContainer([topSpacer, ...renderedRows, bottomSpacer]);
@@ -1,3 +1,3 @@
1
- export { init, update, scrollToIndex, view, lazy, subscriptions, visibleWindow, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, SubscriptionDeps, } from './index.js';
1
+ export { init, update, scrollToIndex, scrollToIndexVariable, view, lazy, subscriptions, visibleWindow, visibleWindowVariable, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, SubscriptionDeps, } from './index.js';
2
2
  export type { InitConfig, ViewConfig, VisibleWindow } from './index.js';
3
3
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,IAAI,EACJ,IAAI,EACJ,aAAa,EACb,aAAa,EACb,KAAK,EACL,OAAO,EACP,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,qBAAqB,EACrB,IAAI,EACJ,IAAI,EACJ,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,KAAK,EACL,OAAO,EACP,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
@@ -1 +1 @@
1
- export { init, update, scrollToIndex, view, lazy, subscriptions, visibleWindow, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, SubscriptionDeps, } from './index.js';
1
+ export { init, update, scrollToIndex, scrollToIndexVariable, view, lazy, subscriptions, visibleWindow, visibleWindowVariable, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, SubscriptionDeps, } from './index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit",
3
- "version": "0.78.0",
3
+ "version": "0.79.0",
4
4
  "description": "A frontend framework for TypeScript, built on Effect, using The Elm Architecture",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",