@foldkit/ui 0.166.0-canary.fe2701c2fa4b → 0.167.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.
@@ -1,28 +1,87 @@
1
1
  import { brandViewResult as __foldkitBrandViewResult } from 'foldkit/brand'
2
- import { Array, Effect, Number, Option, Queue, Schema, Stream, pipe, } from 'effect';
2
+ import { Array, Effect, HashSet, Match, Number, Option, Queue, Record, Schema, Stream, pipe, } from 'effect';
3
+ import { Update } from 'foldkit';
3
4
  import * as Command from 'foldkit/command';
4
5
  import { childAttributes, } from 'foldkit/html';
5
6
  import { defineMessageUnion } from 'foldkit/message';
7
+ import * as Mount from 'foldkit/mount';
8
+ import * as Render from 'foldkit/render';
6
9
  import { defineTaggedUnion } from 'foldkit/schema';
7
10
  import { modifyFields } from 'foldkit/struct';
8
11
  import { defineView } from 'foldkit/submodel';
9
- import * as Subscription from 'foldkit/subscription';
10
12
  // MODEL
11
13
  /** Measurement state of the virtual list's scrollable container.
12
14
  *
13
- * Before the container's `ResizeObserver` fires for the first time we don't
14
- * know its height and cannot compute a visible slice. The view must handle
15
- * `Unmeasured` explicitly, typically by rendering a placeholder until the
16
- * first measurement arrives.
15
+ * Before the Mount reports the container's dimensions we cannot compute a
16
+ * visible slice. The view handles `Unmeasured` by rendering an empty
17
+ * container until the first measurement arrives.
17
18
  */
18
19
  const Measurement = defineTaggedUnion({
19
20
  Unmeasured: {},
20
- Measured: { containerHeight: Schema.Number },
21
+ Measured: {
22
+ containerWidth: Schema.Number,
23
+ containerHeight: Schema.Number,
24
+ },
25
+ });
26
+ /** Alignment of a row within the viewport after a programmatic scroll. */
27
+ export const ScrollAlignment = Schema.Literals([
28
+ 'Start',
29
+ 'Center',
30
+ 'End',
31
+ 'Nearest',
32
+ ]);
33
+ /** Logical destination for initial and programmatic scrolling. */
34
+ export const ScrollTarget = defineTaggedUnion({
35
+ Index: { index: Schema.Number },
36
+ Key: { key: Schema.String },
37
+ Offset: { offset: Schema.Number },
38
+ End: {},
39
+ });
40
+ const ViewportAnchor = defineTaggedUnion({
41
+ Offset: { scrollTop: Schema.Number },
42
+ Row: {
43
+ key: Schema.String,
44
+ index: Schema.Number,
45
+ viewportOffset: Schema.Number,
46
+ },
47
+ End: {},
48
+ });
49
+ const ObservedAnchor = defineTaggedUnion({
50
+ None: {},
51
+ Row: {
52
+ key: Schema.String,
53
+ index: Schema.Number,
54
+ viewportOffset: Schema.Number,
55
+ },
56
+ });
57
+ /** Outcome of applying a logical scroll request after the view commits. */
58
+ export const ApplyScrollOutcome = defineTaggedUnion({
59
+ Applied: {
60
+ scrollTop: Schema.Number,
61
+ scrollHeight: Schema.Number,
62
+ containerHeight: Schema.Number,
63
+ anchor: ObservedAnchor,
64
+ },
65
+ Skipped: {},
66
+ });
67
+ const ScrollRequest = defineTaggedUnion({
68
+ Target: {
69
+ target: ScrollTarget,
70
+ alignment: ScrollAlignment,
71
+ },
72
+ Anchor: { anchor: ViewportAnchor },
21
73
  });
22
- /** State of a programmatic scroll initiated by `scrollToIndex`. */
23
74
  const PendingScroll = defineTaggedUnion({
24
75
  Idle: {},
25
- ScrollingToIndex: { index: Schema.Number, version: Schema.Number },
76
+ Pending: { request: ScrollRequest, version: Schema.Number },
77
+ });
78
+ const InitialScroll = defineTaggedUnion({
79
+ Applied: {},
80
+ Pending: { target: ScrollTarget, alignment: ScrollAlignment },
81
+ });
82
+ const EndBehavior = defineTaggedUnion({
83
+ PreserveAnchor: {},
84
+ Follow: { thresholdPx: Schema.Number },
26
85
  });
27
86
  /** Schema for the virtual list's state. Tracks scroll position, container
28
87
  * measurement, and any in-flight programmatic scroll. */
@@ -31,316 +90,636 @@ export const Model = Schema.Struct({
31
90
  rowHeightPx: Schema.Number,
32
91
  scrollTop: Schema.Number,
33
92
  measurement: Measurement,
93
+ viewportAnchor: ViewportAnchor,
94
+ measuredRowHeights: Schema.Record(Schema.String, Schema.Number),
95
+ layoutVersion: Schema.Number,
96
+ initialScroll: InitialScroll,
97
+ endBehavior: EndBehavior,
34
98
  pendingScroll: PendingScroll,
35
99
  pendingScrollVersion: Schema.Number,
36
100
  });
37
101
  // MESSAGE
38
102
  /** Union of all messages the virtual list component can produce. */
39
103
  export const Message = defineMessageUnion({
40
- ScrolledContainer: { scrollTop: Schema.Number },
41
- MeasuredContainer: { containerHeight: Schema.Number },
42
- CompletedApplyScroll: { version: Schema.Number },
104
+ ObservedContainerScroll: {
105
+ scrollTop: Schema.Number,
106
+ scrollHeight: Schema.Number,
107
+ containerHeight: Schema.Number,
108
+ anchor: ObservedAnchor,
109
+ },
110
+ ResizedContainer: {
111
+ containerWidth: Schema.Number,
112
+ containerHeight: Schema.Number,
113
+ },
114
+ MeasuredRows: {
115
+ measurements: Schema.Array(Schema.Struct({
116
+ key: Schema.String,
117
+ height: Schema.Number,
118
+ layoutVersion: Schema.Number,
119
+ })),
120
+ },
121
+ CompletedApplyScroll: {
122
+ version: Schema.Number,
123
+ outcome: ApplyScrollOutcome,
124
+ },
43
125
  });
126
+ const initialScrollFromConfig = (config) => {
127
+ if (config.initialScroll !== undefined) {
128
+ return __foldkitBrandViewResult((InitialScroll.Pending({
129
+ target: config.initialScroll.target,
130
+ alignment: config.initialScroll.alignment ?? 'Start',
131
+ })), "@foldkit/ui/virtualList/index.js#initialScrollFromConfig");
132
+ }
133
+ if (config.initialScrollTop !== undefined) {
134
+ return __foldkitBrandViewResult((InitialScroll.Pending({
135
+ target: ScrollTarget.Offset({ offset: config.initialScrollTop }),
136
+ alignment: 'Start',
137
+ })), "@foldkit/ui/virtualList/index.js#initialScrollFromConfig");
138
+ }
139
+ return __foldkitBrandViewResult((InitialScroll.Applied()), "@foldkit/ui/virtualList/index.js#initialScrollFromConfig");
140
+ };
44
141
  /** Creates an initial virtual list model from a config. The container starts
45
- * in `Unmeasured` state. The first `ResizeObserver` entry transitions it to
46
- * `Measured`. */
47
- export const init = (config) => (__foldkitBrandViewResult(({
48
- id: config.id,
49
- rowHeightPx: config.rowHeightPx,
50
- scrollTop: config.initialScrollTop ?? 0,
51
- measurement: Measurement.Unmeasured(),
52
- pendingScroll: PendingScroll.Idle(),
53
- pendingScrollVersion: 0,
54
- }), "@foldkit/ui/virtualList/index.js#init"));
142
+ * in `Unmeasured` state until its Mount reports the first measurement. */
143
+ export const init = (config) => {
144
+ const initialScroll = initialScrollFromConfig(config);
145
+ return __foldkitBrandViewResult(({
146
+ id: config.id,
147
+ rowHeightPx: config.rowHeightPx,
148
+ scrollTop: config.initialScrollTop ?? 0,
149
+ measurement: Measurement.Unmeasured(),
150
+ viewportAnchor: ViewportAnchor.Offset({
151
+ scrollTop: config.initialScrollTop ?? 0,
152
+ }),
153
+ measuredRowHeights: Record.empty(),
154
+ layoutVersion: 0,
155
+ initialScroll,
156
+ endBehavior: config.followEnd === undefined
157
+ ? EndBehavior.PreserveAnchor()
158
+ : EndBehavior.Follow({
159
+ thresholdPx: config.followEnd.thresholdPx ?? 1,
160
+ }),
161
+ pendingScroll: PendingScroll.Idle(),
162
+ pendingScrollVersion: 0,
163
+ }), "@foldkit/ui/virtualList/index.js#init");
164
+ };
55
165
  // UPDATE
166
+ const scrollTopForNearestAlignment = (currentScrollTop, containerHeight, startOffset, endOffset) => {
167
+ const viewportEnd = currentScrollTop + containerHeight;
168
+ const rowSpansViewport = startOffset < currentScrollTop && endOffset > viewportEnd;
169
+ if (rowSpansViewport) {
170
+ return __foldkitBrandViewResult((currentScrollTop), "@foldkit/ui/virtualList/index.js#scrollTopForNearestAlignment");
171
+ }
172
+ if (startOffset < currentScrollTop) {
173
+ return __foldkitBrandViewResult((startOffset), "@foldkit/ui/virtualList/index.js#scrollTopForNearestAlignment");
174
+ }
175
+ if (endOffset > viewportEnd) {
176
+ return __foldkitBrandViewResult((endOffset - containerHeight), "@foldkit/ui/virtualList/index.js#scrollTopForNearestAlignment");
177
+ }
178
+ return __foldkitBrandViewResult((currentScrollTop), "@foldkit/ui/virtualList/index.js#scrollTopForNearestAlignment");
179
+ };
180
+ const scrollTopForRow = (currentScrollTop, containerHeight, startOffset, endOffset, alignment) => {
181
+ const targetScrollTop = Match.value(alignment).pipe(Match.withReturnType(), Match.when('Start', () => __foldkitBrandViewResult((startOffset), "@foldkit/ui/virtualList/index.js#anonymous")), Match.when('Center', () => __foldkitBrandViewResult((startOffset + (endOffset - startOffset - containerHeight) / 2), "@foldkit/ui/virtualList/index.js#anonymous~2")), Match.when('End', () => __foldkitBrandViewResult((endOffset - containerHeight), "@foldkit/ui/virtualList/index.js#anonymous~3")), Match.when('Nearest', () => __foldkitBrandViewResult((scrollTopForNearestAlignment(currentScrollTop, containerHeight, startOffset, endOffset)), "@foldkit/ui/virtualList/index.js#anonymous~4")), Match.exhaustive);
182
+ return __foldkitBrandViewResult((Math.max(0, targetScrollTop)), "@foldkit/ui/virtualList/index.js#scrollTopForRow");
183
+ };
184
+ const renderedRows = (element) => __foldkitBrandViewResult((pipe(element.children, Array.fromIterable, Array.filter((row) => __foldkitBrandViewResult((row instanceof HTMLElement &&
185
+ row.hasAttribute('data-virtual-list-item-key')), "@foldkit/ui/virtualList/index.js#anonymous~5")))), "@foldkit/ui/virtualList/index.js#renderedRows");
186
+ const rowIndex = (element) => __foldkitBrandViewResult((pipe(element.getAttribute('data-virtual-list-item-index'), Option.fromNullishOr, Option.flatMap(Number.parse))), "@foldkit/ui/virtualList/index.js#rowIndex");
187
+ const observedAnchor = (element) => {
188
+ const containerRect = element.getBoundingClientRect();
189
+ const viewportTop = containerRect.top + element.clientTop;
190
+ const viewportBottom = viewportTop + element.clientHeight;
191
+ const maybeRow = Array.findFirst(renderedRows(element), row => {
192
+ const rowRect = row.getBoundingClientRect();
193
+ return __foldkitBrandViewResult((rowRect.bottom > viewportTop && rowRect.top < viewportBottom), "@foldkit/ui/virtualList/index.js#anonymous~6");
194
+ });
195
+ if (Option.isNone(maybeRow)) {
196
+ return __foldkitBrandViewResult((ObservedAnchor.None()), "@foldkit/ui/virtualList/index.js#observedAnchor");
197
+ }
198
+ const key = maybeRow.value.getAttribute('data-virtual-list-item-key');
199
+ const maybeIndex = rowIndex(maybeRow.value);
200
+ if (key === null || Option.isNone(maybeIndex)) {
201
+ return __foldkitBrandViewResult((ObservedAnchor.None()), "@foldkit/ui/virtualList/index.js#observedAnchor");
202
+ }
203
+ return __foldkitBrandViewResult((ObservedAnchor.Row({
204
+ key,
205
+ index: maybeIndex.value,
206
+ viewportOffset: maybeRow.value.getBoundingClientRect().top - viewportTop,
207
+ })), "@foldkit/ui/virtualList/index.js#observedAnchor");
208
+ };
209
+ const maxScrollTop = (element) => __foldkitBrandViewResult((Math.max(0, element.scrollHeight - element.clientHeight)), "@foldkit/ui/virtualList/index.js#maxScrollTop");
210
+ const clampScrollTop = (element, scrollTop) => __foldkitBrandViewResult((Math.max(0, Math.min(scrollTop, maxScrollTop(element)))), "@foldkit/ui/virtualList/index.js#clampScrollTop");
211
+ const rowForIndex = (element, index) => {
212
+ const rows = renderedRows(element);
213
+ const maybeExact = Array.findFirst(rows, row => __foldkitBrandViewResult((Option.exists(rowIndex(row), rowIndex => __foldkitBrandViewResult((rowIndex === index), "@foldkit/ui/virtualList/index.js#anonymous~8"))), "@foldkit/ui/virtualList/index.js#anonymous~7"));
214
+ if (Option.isSome(maybeExact)) {
215
+ return __foldkitBrandViewResult((maybeExact), "@foldkit/ui/virtualList/index.js#rowForIndex");
216
+ }
217
+ const maybeFirst = Array.head(rows);
218
+ const maybeLast = Array.last(rows);
219
+ if (Option.isSome(maybeFirst)) {
220
+ const maybeFirstIndex = rowIndex(maybeFirst.value);
221
+ if (Option.isSome(maybeFirstIndex) && index < maybeFirstIndex.value) {
222
+ return __foldkitBrandViewResult((maybeFirst), "@foldkit/ui/virtualList/index.js#rowForIndex");
223
+ }
224
+ }
225
+ if (Option.isSome(maybeLast)) {
226
+ const maybeLastIndex = rowIndex(maybeLast.value);
227
+ if (Option.isSome(maybeLastIndex) && index > maybeLastIndex.value) {
228
+ return __foldkitBrandViewResult((maybeLast), "@foldkit/ui/virtualList/index.js#rowForIndex");
229
+ }
230
+ }
231
+ return __foldkitBrandViewResult((Option.none()), "@foldkit/ui/virtualList/index.js#rowForIndex");
232
+ };
233
+ const rowForKey = (element, key) => __foldkitBrandViewResult((Array.findFirst(renderedRows(element), row => __foldkitBrandViewResult((row.getAttribute('data-virtual-list-item-key') === key), "@foldkit/ui/virtualList/index.js#anonymous~9"))), "@foldkit/ui/virtualList/index.js#rowForKey");
234
+ const rowOffsets = (container, row) => {
235
+ const viewportTop = container.getBoundingClientRect().top + container.clientTop;
236
+ const rowRect = row.getBoundingClientRect();
237
+ const startOffset = container.scrollTop + rowRect.top - viewportTop;
238
+ return __foldkitBrandViewResult(({ startOffset, endOffset: startOffset + rowRect.height }), "@foldkit/ui/virtualList/index.js#rowOffsets");
239
+ };
240
+ const rowForTarget = (element, target) => __foldkitBrandViewResult((ScrollTarget.matchOrElse(target, {
241
+ Index: ({ index }) => __foldkitBrandViewResult((rowForIndex(element, index)), "@foldkit/ui/virtualList/index.js#Index"),
242
+ Key: ({ key }) => __foldkitBrandViewResult((rowForKey(element, key)), "@foldkit/ui/virtualList/index.js#Key"),
243
+ }, () => __foldkitBrandViewResult((Option.none()), "@foldkit/ui/virtualList/index.js#anonymous~10"))), "@foldkit/ui/virtualList/index.js#rowForTarget");
244
+ const scrollTopForRowTarget = (element, target, alignment) => __foldkitBrandViewResult((Option.map(rowForTarget(element, target), row => {
245
+ const offsets = rowOffsets(element, row);
246
+ return __foldkitBrandViewResult((scrollTopForRow(element.scrollTop, element.clientHeight, offsets.startOffset, offsets.endOffset, alignment)), "@foldkit/ui/virtualList/index.js#anonymous~11");
247
+ })), "@foldkit/ui/virtualList/index.js#scrollTopForRowTarget");
248
+ const scrollTopForTarget = (element, target, alignment) => __foldkitBrandViewResult((ScrollTarget.match(target, {
249
+ Index: indexTarget => __foldkitBrandViewResult((scrollTopForRowTarget(element, indexTarget, alignment)), "@foldkit/ui/virtualList/index.js#Index~2"),
250
+ Key: keyTarget => __foldkitBrandViewResult((scrollTopForRowTarget(element, keyTarget, alignment)), "@foldkit/ui/virtualList/index.js#Key~2"),
251
+ Offset: ({ offset }) => __foldkitBrandViewResult((Option.some(offset)), "@foldkit/ui/virtualList/index.js#Offset"),
252
+ End: () => __foldkitBrandViewResult((Option.some(maxScrollTop(element))), "@foldkit/ui/virtualList/index.js#End"),
253
+ })), "@foldkit/ui/virtualList/index.js#scrollTopForTarget");
254
+ const rowForAnchor = (element, anchor) => __foldkitBrandViewResult((Option.orElse(rowForKey(element, anchor.key), () => __foldkitBrandViewResult((rowForIndex(element, anchor.index)), "@foldkit/ui/virtualList/index.js#anonymous~12"))), "@foldkit/ui/virtualList/index.js#rowForAnchor");
255
+ const scrollTopForAnchor = (element, anchor) => __foldkitBrandViewResult((ViewportAnchor.match(anchor, {
256
+ Offset: ({ scrollTop }) => __foldkitBrandViewResult((Option.some(scrollTop)), "@foldkit/ui/virtualList/index.js#Offset~2"),
257
+ Row: rowAnchor => __foldkitBrandViewResult((Option.map(rowForAnchor(element, rowAnchor), row => __foldkitBrandViewResult((Math.max(0, rowOffsets(element, row).startOffset - rowAnchor.viewportOffset)), "@foldkit/ui/virtualList/index.js#anonymous~13"))), "@foldkit/ui/virtualList/index.js#Row"),
258
+ End: () => __foldkitBrandViewResult((Option.some(maxScrollTop(element))), "@foldkit/ui/virtualList/index.js#End~2"),
259
+ })), "@foldkit/ui/virtualList/index.js#scrollTopForAnchor");
260
+ const scrollTopForRequest = (element, request) => __foldkitBrandViewResult((ScrollRequest.match(request, {
261
+ Target: ({ target, alignment }) => __foldkitBrandViewResult((scrollTopForTarget(element, target, alignment)), "@foldkit/ui/virtualList/index.js#Target"),
262
+ Anchor: ({ anchor }) => __foldkitBrandViewResult((scrollTopForAnchor(element, anchor)), "@foldkit/ui/virtualList/index.js#Anchor"),
263
+ })), "@foldkit/ui/virtualList/index.js#scrollTopForRequest");
264
+ const mountedContainers = new Map();
56
265
  export const ApplyScroll = Command.define('ApplyScroll', {
57
- args: { id: Schema.String, scrollTop: Schema.Number, version: Schema.Number },
266
+ args: {
267
+ id: Schema.String,
268
+ request: ScrollRequest,
269
+ version: Schema.Number,
270
+ },
58
271
  messages: [Message.CompletedApplyScroll],
59
- execute: ({ id, scrollTop, version }) => __foldkitBrandViewResult((Effect.sync(() => {
60
- const element = document.getElementById(id);
61
- if (element !== null) {
62
- element.scrollTop = scrollTop;
272
+ execute: ({ id, request, version }) => __foldkitBrandViewResult((Effect.gen(function* () {
273
+ yield* Render.afterCommit;
274
+ const containers = mountedContainers.get(id);
275
+ if (containers === undefined || containers.size !== 1) {
276
+ return __foldkitBrandViewResult((Message.CompletedApplyScroll({
277
+ version,
278
+ outcome: ApplyScrollOutcome.Skipped(),
279
+ })), "@foldkit/ui/virtualList/index.js#anonymous~14");
63
280
  }
64
- return __foldkitBrandViewResult((Message.CompletedApplyScroll({ version })), "@foldkit/ui/virtualList/index.js#anonymous");
65
- })), "@foldkit/ui/virtualList/index.js#execute"),
66
- });
67
- /** Processes a VirtualList Message and returns the next Model and optional Commands. */
68
- export const update = (model, message) => __foldkitBrandViewResult((Message.match(message, {
69
- ScrolledContainer: ({ scrollTop }) => (__foldkitBrandViewResult(({
70
- model: modifyFields(model, { scrollTop: () => __foldkitBrandViewResult((scrollTop), "@foldkit/ui/virtualList/index.js#scrollTop") }),
71
- }), "@foldkit/ui/virtualList/index.js#ScrolledContainer")),
72
- MeasuredContainer: ({ containerHeight }) => {
73
- const wasUnmeasured = model.measurement._tag === 'Unmeasured';
74
- const needsInitialApply = wasUnmeasured && model.scrollTop !== 0;
75
- if (needsInitialApply) {
76
- const nextVersion = Number.increment(model.pendingScrollVersion);
77
- return __foldkitBrandViewResult(({
78
- model: modifyFields(model, {
79
- measurement: () => __foldkitBrandViewResult((Measurement.Measured({ containerHeight })), "@foldkit/ui/virtualList/index.js#measurement"),
80
- pendingScrollVersion: () => __foldkitBrandViewResult((nextVersion), "@foldkit/ui/virtualList/index.js#pendingScrollVersion"),
81
- pendingScroll: () => __foldkitBrandViewResult((PendingScroll.ScrollingToIndex({
82
- index: Math.floor(model.scrollTop / model.rowHeightPx),
83
- version: nextVersion,
84
- })), "@foldkit/ui/virtualList/index.js#pendingScroll"),
85
- }),
86
- commands: [
87
- ApplyScroll({
88
- id: model.id,
89
- scrollTop: model.scrollTop,
90
- version: nextVersion,
91
- }),
92
- ],
93
- }), "@foldkit/ui/virtualList/index.js#MeasuredContainer");
281
+ const maybeElement = pipe(containers, Array.fromIterable, Array.head);
282
+ if (Option.isNone(maybeElement)) {
283
+ return __foldkitBrandViewResult((Message.CompletedApplyScroll({
284
+ version,
285
+ outcome: ApplyScrollOutcome.Skipped(),
286
+ })), "@foldkit/ui/virtualList/index.js#anonymous~14");
94
287
  }
95
- else {
96
- return __foldkitBrandViewResult(({
97
- model: modifyFields(model, {
98
- measurement: () => __foldkitBrandViewResult((Measurement.Measured({ containerHeight })), "@foldkit/ui/virtualList/index.js#measurement~2"),
99
- }),
100
- }), "@foldkit/ui/virtualList/index.js#MeasuredContainer");
288
+ const element = maybeElement.value;
289
+ const maybeActiveVersion = pipe(element.getAttribute('data-virtual-list-scroll-version'), Option.fromNullishOr, Option.flatMap(Number.parse));
290
+ if (Option.isNone(maybeActiveVersion) ||
291
+ maybeActiveVersion.value !== version) {
292
+ return __foldkitBrandViewResult((Message.CompletedApplyScroll({
293
+ version,
294
+ outcome: ApplyScrollOutcome.Skipped(),
295
+ })), "@foldkit/ui/virtualList/index.js#anonymous~14");
101
296
  }
102
- },
103
- CompletedApplyScroll: ({ version }) => {
104
- if (version !== model.pendingScrollVersion) {
105
- return __foldkitBrandViewResult(({ model }), "@foldkit/ui/virtualList/index.js#CompletedApplyScroll");
106
- }
107
- else {
108
- return __foldkitBrandViewResult(({
109
- model: modifyFields(model, {
110
- pendingScroll: () => __foldkitBrandViewResult((PendingScroll.Idle()), "@foldkit/ui/virtualList/index.js#pendingScroll~2"),
111
- }),
112
- }), "@foldkit/ui/virtualList/index.js#CompletedApplyScroll");
297
+ const maybeScrollTop = scrollTopForRequest(element, request);
298
+ if (Option.isNone(maybeScrollTop)) {
299
+ return __foldkitBrandViewResult((Message.CompletedApplyScroll({
300
+ version,
301
+ outcome: ApplyScrollOutcome.Skipped(),
302
+ })), "@foldkit/ui/virtualList/index.js#anonymous~14");
113
303
  }
114
- },
115
- })), "@foldkit/ui/virtualList/index.js#update");
116
- const buildScrollToIndex = (model, index, targetScrollTop) => {
304
+ element.scrollTop = clampScrollTop(element, maybeScrollTop.value);
305
+ return __foldkitBrandViewResult((Message.CompletedApplyScroll({
306
+ version,
307
+ outcome: ApplyScrollOutcome.Applied({
308
+ scrollTop: element.scrollTop,
309
+ scrollHeight: element.scrollHeight,
310
+ containerHeight: element.clientHeight,
311
+ anchor: observedAnchor(element),
312
+ }),
313
+ })), "@foldkit/ui/virtualList/index.js#anonymous~14");
314
+ })), "@foldkit/ui/virtualList/index.js#execute"),
315
+ });
316
+ const buildScrollRequest = (model, request) => {
117
317
  const nextVersion = Number.increment(model.pendingScrollVersion);
118
318
  return __foldkitBrandViewResult(({
119
319
  model: modifyFields(model, {
120
- pendingScrollVersion: () => __foldkitBrandViewResult((nextVersion), "@foldkit/ui/virtualList/index.js#pendingScrollVersion~2"),
121
- pendingScroll: () => __foldkitBrandViewResult((PendingScroll.ScrollingToIndex({ index, version: nextVersion })), "@foldkit/ui/virtualList/index.js#pendingScroll~3"),
320
+ pendingScrollVersion: () => __foldkitBrandViewResult((nextVersion), "@foldkit/ui/virtualList/index.js#pendingScrollVersion"),
321
+ pendingScroll: () => __foldkitBrandViewResult((PendingScroll.Pending({ request, version: nextVersion })), "@foldkit/ui/virtualList/index.js#pendingScroll"),
122
322
  }),
123
323
  commands: [
124
324
  ApplyScroll({
125
325
  id: model.id,
126
- scrollTop: targetScrollTop,
326
+ request,
127
327
  version: nextVersion,
128
328
  }),
129
329
  ],
130
- }), "@foldkit/ui/virtualList/index.js#buildScrollToIndex");
330
+ }), "@foldkit/ui/virtualList/index.js#buildScrollRequest");
331
+ };
332
+ const currentScrollRequest = (model) => __foldkitBrandViewResult((PendingScroll.match(model.pendingScroll, {
333
+ Idle: () => __foldkitBrandViewResult((InitialScroll.match(model.initialScroll, {
334
+ Applied: () => __foldkitBrandViewResult((ScrollRequest.Anchor({ anchor: model.viewportAnchor })), "@foldkit/ui/virtualList/index.js#Applied"),
335
+ Pending: ({ target, alignment }) => __foldkitBrandViewResult((ScrollRequest.Target({ target, alignment })), "@foldkit/ui/virtualList/index.js#Pending"),
336
+ })), "@foldkit/ui/virtualList/index.js#Idle"),
337
+ Pending: ({ request }) => __foldkitBrandViewResult((request), "@foldkit/ui/virtualList/index.js#Pending~2"),
338
+ })), "@foldkit/ui/virtualList/index.js#currentScrollRequest");
339
+ const reconcileLayout = (model) => __foldkitBrandViewResult((buildScrollRequest(model, currentScrollRequest(model))), "@foldkit/ui/virtualList/index.js#reconcileLayout");
340
+ const anchorFromSnapshot = (model, scrollTop, scrollHeight, containerHeight, anchor) => {
341
+ const distanceFromEnd = Math.max(0, scrollHeight - containerHeight - scrollTop);
342
+ const isFollowingEnd = EndBehavior.match(model.endBehavior, {
343
+ PreserveAnchor: () => __foldkitBrandViewResult((false), "@foldkit/ui/virtualList/index.js#PreserveAnchor"),
344
+ Follow: ({ thresholdPx }) => __foldkitBrandViewResult((distanceFromEnd <= thresholdPx), "@foldkit/ui/virtualList/index.js#Follow"),
345
+ });
346
+ if (isFollowingEnd) {
347
+ return __foldkitBrandViewResult((ViewportAnchor.End()), "@foldkit/ui/virtualList/index.js#anchorFromSnapshot");
348
+ }
349
+ return __foldkitBrandViewResult((ObservedAnchor.match(anchor, {
350
+ None: () => __foldkitBrandViewResult((ViewportAnchor.Offset({ scrollTop })), "@foldkit/ui/virtualList/index.js#None"),
351
+ Row: ({ key, index, viewportOffset }) => __foldkitBrandViewResult((ViewportAnchor.Row({ key, index, viewportOffset })), "@foldkit/ui/virtualList/index.js#Row~2"),
352
+ })), "@foldkit/ui/virtualList/index.js#anchorFromSnapshot");
353
+ };
354
+ const applyScrollSnapshot = (model, snapshot) => __foldkitBrandViewResult((modifyFields(model, {
355
+ scrollTop: () => __foldkitBrandViewResult((snapshot.scrollTop), "@foldkit/ui/virtualList/index.js#scrollTop"),
356
+ viewportAnchor: () => __foldkitBrandViewResult((anchorFromSnapshot(model, snapshot.scrollTop, snapshot.scrollHeight, snapshot.containerHeight, snapshot.anchor)), "@foldkit/ui/virtualList/index.js#viewportAnchor"),
357
+ })), "@foldkit/ui/virtualList/index.js#applyScrollSnapshot");
358
+ const measureContainer = (model, containerWidth, containerHeight) => {
359
+ const wasUnmeasured = model.measurement._tag === 'Unmeasured';
360
+ const didWidthChange = Measurement.match(model.measurement, {
361
+ Unmeasured: () => __foldkitBrandViewResult((false), "@foldkit/ui/virtualList/index.js#Unmeasured"),
362
+ Measured: measurement => __foldkitBrandViewResult((measurement.containerWidth !== containerWidth), "@foldkit/ui/virtualList/index.js#Measured"),
363
+ });
364
+ const recordContainerMeasurement = stepModel => (__foldkitBrandViewResult(({
365
+ model: modifyFields(stepModel, {
366
+ measurement: () => __foldkitBrandViewResult((Measurement.Measured({ containerWidth, containerHeight })), "@foldkit/ui/virtualList/index.js#measurement"),
367
+ measuredRowHeights: measuredRowHeights => __foldkitBrandViewResult((didWidthChange ? Record.empty() : measuredRowHeights), "@foldkit/ui/virtualList/index.js#measuredRowHeights"),
368
+ layoutVersion: layoutVersion => __foldkitBrandViewResult((didWidthChange ? Number.increment(layoutVersion) : layoutVersion), "@foldkit/ui/virtualList/index.js#layoutVersion"),
369
+ }),
370
+ }), "@foldkit/ui/virtualList/index.js#recordContainerMeasurement"));
371
+ const { initialScroll } = model;
372
+ if (initialScroll._tag === 'Pending') {
373
+ return __foldkitBrandViewResult((Update.combine(model, [
374
+ recordContainerMeasurement,
375
+ stepModel => __foldkitBrandViewResult((buildScrollRequest(stepModel, ScrollRequest.Target({
376
+ target: initialScroll.target,
377
+ alignment: initialScroll.alignment,
378
+ }))), "@foldkit/ui/virtualList/index.js#anonymous~15"),
379
+ ])), "@foldkit/ui/virtualList/index.js#measureContainer");
380
+ }
381
+ if (wasUnmeasured) {
382
+ return __foldkitBrandViewResult((recordContainerMeasurement(model)), "@foldkit/ui/virtualList/index.js#measureContainer");
383
+ }
384
+ return __foldkitBrandViewResult((Update.combine(model, [recordContainerMeasurement, reconcileLayout])), "@foldkit/ui/virtualList/index.js#measureContainer");
385
+ };
386
+ const hasChangedMeasurement = (model, measurement) => {
387
+ if (measurement.layoutVersion !== model.layoutVersion) {
388
+ return __foldkitBrandViewResult((false), "@foldkit/ui/virtualList/index.js#hasChangedMeasurement");
389
+ }
390
+ return __foldkitBrandViewResult((pipe(model.measuredRowHeights, Record.get(measurement.key), Option.match({
391
+ onNone: () => __foldkitBrandViewResult((true), "@foldkit/ui/virtualList/index.js#onNone"),
392
+ onSome: height => __foldkitBrandViewResult((height !== measurement.height), "@foldkit/ui/virtualList/index.js#onSome"),
393
+ }))), "@foldkit/ui/virtualList/index.js#hasChangedMeasurement");
394
+ };
395
+ const applyRowMeasurements = (model, measurements) => {
396
+ const changedMeasurements = Array.filter(measurements, measurement => __foldkitBrandViewResult((hasChangedMeasurement(model, measurement)), "@foldkit/ui/virtualList/index.js#anonymous~16"));
397
+ if (Array.isArrayEmpty(changedMeasurements)) {
398
+ return __foldkitBrandViewResult(({ model }), "@foldkit/ui/virtualList/index.js#applyRowMeasurements");
399
+ }
400
+ const measuredRowHeights = Array.reduce(changedMeasurements, model.measuredRowHeights, (heights, measurement) => __foldkitBrandViewResult((Record.set(heights, measurement.key, measurement.height)), "@foldkit/ui/virtualList/index.js#anonymous~17"));
401
+ return __foldkitBrandViewResult((Update.combine(model, [
402
+ stepModel => (__foldkitBrandViewResult(({
403
+ model: modifyFields(stepModel, {
404
+ measuredRowHeights: () => __foldkitBrandViewResult((measuredRowHeights), "@foldkit/ui/virtualList/index.js#measuredRowHeights~2"),
405
+ }),
406
+ }), "@foldkit/ui/virtualList/index.js#anonymous~18")),
407
+ reconcileLayout,
408
+ ])), "@foldkit/ui/virtualList/index.js#applyRowMeasurements");
131
409
  };
410
+ /** Processes a VirtualList Message and returns the next Model and optional Commands. */
411
+ export const update = (model, message) => __foldkitBrandViewResult((Message.match(message, {
412
+ ObservedContainerScroll: snapshot => {
413
+ const nextPendingScrollVersion = PendingScroll.match(model.pendingScroll, {
414
+ Idle: () => __foldkitBrandViewResult((model.pendingScrollVersion), "@foldkit/ui/virtualList/index.js#Idle~2"),
415
+ Pending: () => __foldkitBrandViewResult((Number.increment(model.pendingScrollVersion)), "@foldkit/ui/virtualList/index.js#Pending~3"),
416
+ });
417
+ return __foldkitBrandViewResult(({
418
+ model: modifyFields(applyScrollSnapshot(model, snapshot), {
419
+ initialScroll: () => __foldkitBrandViewResult((InitialScroll.Applied()), "@foldkit/ui/virtualList/index.js#initialScroll"),
420
+ pendingScroll: () => __foldkitBrandViewResult((PendingScroll.Idle()), "@foldkit/ui/virtualList/index.js#pendingScroll~2"),
421
+ pendingScrollVersion: () => __foldkitBrandViewResult((nextPendingScrollVersion), "@foldkit/ui/virtualList/index.js#pendingScrollVersion~2"),
422
+ }),
423
+ }), "@foldkit/ui/virtualList/index.js#ObservedContainerScroll");
424
+ },
425
+ ResizedContainer: ({ containerWidth, containerHeight }) => __foldkitBrandViewResult((measureContainer(model, containerWidth, containerHeight)), "@foldkit/ui/virtualList/index.js#ResizedContainer"),
426
+ MeasuredRows: ({ measurements }) => __foldkitBrandViewResult((applyRowMeasurements(model, measurements)), "@foldkit/ui/virtualList/index.js#MeasuredRows"),
427
+ CompletedApplyScroll: ({ version, outcome }) => {
428
+ if (version !== model.pendingScrollVersion) {
429
+ return __foldkitBrandViewResult(({ model }), "@foldkit/ui/virtualList/index.js#CompletedApplyScroll");
430
+ }
431
+ return __foldkitBrandViewResult((ApplyScrollOutcome.match(outcome, {
432
+ Applied: snapshot => {
433
+ const scrolledModel = applyScrollSnapshot(model, snapshot);
434
+ return __foldkitBrandViewResult(({
435
+ model: modifyFields(scrolledModel, {
436
+ initialScroll: () => __foldkitBrandViewResult((snapshot.anchor._tag === 'Row'
437
+ ? InitialScroll.Applied()
438
+ : model.initialScroll), "@foldkit/ui/virtualList/index.js#initialScroll~2"),
439
+ pendingScroll: () => __foldkitBrandViewResult((PendingScroll.Idle()), "@foldkit/ui/virtualList/index.js#pendingScroll~3"),
440
+ }),
441
+ }), "@foldkit/ui/virtualList/index.js#Applied~2");
442
+ },
443
+ Skipped: () => (__foldkitBrandViewResult(({
444
+ model: modifyFields(model, {
445
+ pendingScroll: () => __foldkitBrandViewResult((PendingScroll.Idle()), "@foldkit/ui/virtualList/index.js#pendingScroll~4"),
446
+ }),
447
+ }), "@foldkit/ui/virtualList/index.js#Skipped")),
448
+ })), "@foldkit/ui/virtualList/index.js#CompletedApplyScroll");
449
+ },
450
+ })), "@foldkit/ui/virtualList/index.js#update");
451
+ /** Programmatically scrolls to a logical target. */
452
+ export const scrollTo = (model, target, options = {}) => __foldkitBrandViewResult((Update.combine(model, [
453
+ stepModel => (__foldkitBrandViewResult(({
454
+ model: modifyFields(stepModel, {
455
+ initialScroll: () => __foldkitBrandViewResult((InitialScroll.Applied()), "@foldkit/ui/virtualList/index.js#initialScroll~3"),
456
+ }),
457
+ }), "@foldkit/ui/virtualList/index.js#anonymous~19")),
458
+ stepModel => __foldkitBrandViewResult((buildScrollRequest(stepModel, ScrollRequest.Target({
459
+ target,
460
+ alignment: options.alignment ?? 'Start',
461
+ }))), "@foldkit/ui/virtualList/index.js#anonymous~20"),
462
+ ])), "@foldkit/ui/virtualList/index.js#scrollTo");
132
463
  /** Programmatically scrolls the container so the row at `index` is visible.
133
- * Returns the next Model and a Command that mutates `element.scrollTop`. The
134
- * natural scroll event then flows back through `ScrolledContainer` and the
135
- * component re-renders the new visible slice.
136
- *
137
- * Uses version-based cancellation: each call increments
138
- * `pendingScrollVersion` so a stale `CompletedApplyScroll` (e.g. from a
139
- * previous in-flight scroll) is ignored when its version no longer matches.
140
- *
141
- * Should be called after the container has rendered. If the container is not
142
- * yet in the DOM the Command silently no-ops (the Model still transitions
143
- * through `ScrollingToIndex` → `Idle` via the version-matched completion).
144
- *
145
- * Assumes uniform row heights: target scroll position is computed as
146
- * `index * model.rowHeightPx`. For variable-height rows, use
147
- * `scrollToIndexVariable`. */
148
- export const scrollToIndex = (model, index) => __foldkitBrandViewResult((buildScrollToIndex(model, index, index * model.rowHeightPx)), "@foldkit/ui/virtualList/index.js#scrollToIndex");
149
- /** Variable-height counterpart of `scrollToIndex`. Walks the heights of items
150
- * before `index` to compute the target `scrollTop`. Use this when rendering
151
- * the list with `itemToRowHeightPx`; use `scrollToIndex` for uniform heights.
152
- *
153
- * Out-of-range indices clamp to the corresponding edge: negative or zero
154
- * scrolls to the top, indices past the end scroll past the last row.
155
- *
156
- * Note: when restoring `initialScrollTop` on the first measurement of a
157
- * variable-height list, the runtime falls back to uniform-height math (using
158
- * `model.rowHeightPx`) because items aren't reachable from the `update`
159
- * function. Consumers who need an accurate initial scroll on a
160
- * variable-height list should call `scrollToIndexVariable` after the first
161
- * `MeasuredContainer` arrives. */
162
- export const scrollToIndexVariable = (model, items, itemToRowHeightPx, index) => {
163
- const cumulativeOffsets = prefixSum(items, itemToRowHeightPx);
164
- const targetScrollTop = pipe(cumulativeOffsets, Array.get(Math.max(0, index)), Option.getOrElse(() => __foldkitBrandViewResult((lastOrZero(cumulativeOffsets)), "@foldkit/ui/virtualList/index.js#anonymous~2")));
165
- return __foldkitBrandViewResult((buildScrollToIndex(model, index, targetScrollTop)), "@foldkit/ui/virtualList/index.js#scrollToIndexVariable");
464
+ * The next view resolves the logical index with its current sizing mode, and
465
+ * the Command aligns the live rendered row. */
466
+ export const scrollToIndex = (model, index, options = {}) => __foldkitBrandViewResult((scrollTo(model, ScrollTarget.Index({ index }), options)), "@foldkit/ui/virtualList/index.js#scrollToIndex");
467
+ /** Programmatically scrolls to the row whose `itemToKey` result matches
468
+ * `key`. The next view resolves the key against its current items. */
469
+ export const scrollToKey = (model, key, options = {}) => __foldkitBrandViewResult((scrollTo(model, ScrollTarget.Key({ key }), options)), "@foldkit/ui/virtualList/index.js#scrollToKey");
470
+ /** Programmatically scrolls to an exact pixel offset from the start of the
471
+ * list. Negative offsets clamp to zero. */
472
+ export const scrollToOffset = (model, offset) => __foldkitBrandViewResult((scrollTo(model, ScrollTarget.Offset({ offset }))), "@foldkit/ui/virtualList/index.js#scrollToOffset");
473
+ /** Programmatically scrolls to the end of the list. */
474
+ export const scrollToEnd = (model) => __foldkitBrandViewResult((scrollTo(model, ScrollTarget.End())), "@foldkit/ui/virtualList/index.js#scrollToEnd");
475
+ /** Notifies VirtualList that its parent-owned items changed. The next view
476
+ * resolves the stored stable-key anchor against the new items, and the
477
+ * Command restores that anchor after the DOM patch. */
478
+ export const informItemsChanged = (model, itemKeys) => {
479
+ const currentKeys = HashSet.fromIterable(itemKeys);
480
+ return __foldkitBrandViewResult((Update.combine(model, [
481
+ stepModel => (__foldkitBrandViewResult(({
482
+ model: modifyFields(stepModel, {
483
+ measuredRowHeights: Record.filter((_, key) => __foldkitBrandViewResult((HashSet.has(currentKeys, key)), "@foldkit/ui/virtualList/index.js#anonymous~22")),
484
+ layoutVersion: Number.increment,
485
+ }),
486
+ }), "@foldkit/ui/virtualList/index.js#anonymous~21")),
487
+ reconcileLayout,
488
+ ])), "@foldkit/ui/virtualList/index.js#informItemsChanged");
166
489
  };
167
490
  const clampIndex = (index, itemCount) => __foldkitBrandViewResult((Math.max(0, Math.min(index, itemCount))), "@foldkit/ui/virtualList/index.js#clampIndex");
168
491
  const prefixSum = (items, itemToRowHeightPx) => {
169
492
  const heights = Array.map(items, itemToRowHeightPx);
170
- return __foldkitBrandViewResult((Array.scan(heights, 0, (cumulative, height) => __foldkitBrandViewResult((cumulative + height), "@foldkit/ui/virtualList/index.js#anonymous~3"))), "@foldkit/ui/virtualList/index.js#prefixSum");
493
+ return __foldkitBrandViewResult((Array.scan(heights, 0, (cumulative, height) => __foldkitBrandViewResult((cumulative + height), "@foldkit/ui/virtualList/index.js#anonymous~23"))), "@foldkit/ui/virtualList/index.js#prefixSum");
171
494
  };
172
- const lastOrZero = (values) => __foldkitBrandViewResult((pipe(values, Array.last, Option.getOrElse(() => __foldkitBrandViewResult((0), "@foldkit/ui/virtualList/index.js#anonymous~4")))), "@foldkit/ui/virtualList/index.js#lastOrZero");
173
- /** Computes the visible slice of a data array given the current scroll
174
- * position, container height, row height, and an overscan buffer.
175
- *
176
- * Assumes uniform row heights via `model.rowHeightPx`. For variable-height
177
- * rows, use `visibleWindowVariable`.
178
- *
179
- * Returns `Option.none()` when the container has not yet been measured;
180
- * callers should render a placeholder (or `Html.empty`) and wait for the
181
- * first `MeasuredContainer` message. */
182
- export const visibleWindow = (model, itemCount, overscan) => __foldkitBrandViewResult((Measurement.match(model.measurement, {
183
- Unmeasured: () => __foldkitBrandViewResult((Option.none()), "@foldkit/ui/virtualList/index.js#Unmeasured"),
184
- Measured: ({ containerHeight }) => {
185
- const firstVisibleIndex = Math.floor(model.scrollTop / model.rowHeightPx);
186
- const lastVisibleIndex = Math.ceil((model.scrollTop + containerHeight) / model.rowHeightPx);
187
- const startIndex = clampIndex(firstVisibleIndex - overscan, itemCount);
188
- const endIndex = clampIndex(lastVisibleIndex + overscan, itemCount);
189
- const topSpacerHeight = startIndex * model.rowHeightPx;
190
- const bottomSpacerHeight = (itemCount - endIndex) * model.rowHeightPx;
191
- return __foldkitBrandViewResult((Option.some({
192
- startIndex,
193
- endIndex,
194
- topSpacerHeight,
195
- bottomSpacerHeight,
196
- })), "@foldkit/ui/virtualList/index.js#Measured");
197
- },
198
- })), "@foldkit/ui/virtualList/index.js#visibleWindow");
199
- /** Variable-height counterpart of `visibleWindow`. Walks the heights of every
200
- * item to build a prefix-sum array, then locates the visible slice with two
201
- * linear searches.
202
- *
203
- * Cost is O(N) per call, walking the whole `items` array once to build the
204
- * prefix sums. For lists in the 10k-item range, this comfortably fits inside
205
- * a 60Hz scroll budget. Larger lists or hotter scroll paths can layer a
206
- * prefix-sum cache invalidated when items change; that lives behind the same
207
- * return shape so consumers don't have to know.
208
- *
209
- * Returns `Option.none()` when the container has not yet been measured. */
210
- export const visibleWindowVariable = (model, items, itemToRowHeightPx, overscan) => __foldkitBrandViewResult((Measurement.match(model.measurement, {
211
- Unmeasured: () => __foldkitBrandViewResult((Option.none()), "@foldkit/ui/virtualList/index.js#Unmeasured~2"),
212
- Measured: ({ containerHeight }) => {
213
- const itemCount = items.length;
214
- const cumulativeOffsets = prefixSum(items, itemToRowHeightPx);
215
- const totalHeight = lastOrZero(cumulativeOffsets);
216
- const firstVisibleIndex = pipe(cumulativeOffsets, Array.findFirstIndex(Number.isGreaterThan(model.scrollTop)), Option.match({
217
- onNone: () => __foldkitBrandViewResult((itemCount), "@foldkit/ui/virtualList/index.js#onNone"),
218
- onSome: index => __foldkitBrandViewResult((Math.max(0, index - 1)), "@foldkit/ui/virtualList/index.js#onSome"),
219
- }));
220
- const lastVisibleIndex = pipe(cumulativeOffsets, Array.findFirstIndex(Number.isGreaterThanOrEqualTo(model.scrollTop + containerHeight)), Option.getOrElse(() => __foldkitBrandViewResult((itemCount), "@foldkit/ui/virtualList/index.js#anonymous~5")));
221
- const startIndex = clampIndex(firstVisibleIndex - overscan, itemCount);
222
- const endIndex = clampIndex(lastVisibleIndex + overscan, itemCount);
223
- const topSpacerHeight = pipe(cumulativeOffsets, Array.get(startIndex), Option.getOrElse(() => __foldkitBrandViewResult((0), "@foldkit/ui/virtualList/index.js#anonymous~6")));
224
- const offsetAtEnd = pipe(cumulativeOffsets, Array.get(endIndex), Option.getOrElse(() => __foldkitBrandViewResult((totalHeight), "@foldkit/ui/virtualList/index.js#anonymous~7")));
225
- const bottomSpacerHeight = totalHeight - offsetAtEnd;
226
- return __foldkitBrandViewResult((Option.some({
227
- startIndex,
228
- endIndex,
229
- topSpacerHeight,
230
- bottomSpacerHeight,
231
- })), "@foldkit/ui/virtualList/index.js#Measured~2");
495
+ const lastOrZero = (values) => __foldkitBrandViewResult((pipe(values, Array.last, Option.getOrElse(() => __foldkitBrandViewResult((0), "@foldkit/ui/virtualList/index.js#anonymous~24")))), "@foldkit/ui/virtualList/index.js#lastOrZero");
496
+ /** Alignment of content when its total height is shorter than the viewport. */
497
+ export const ContentAlignment = Schema.Literals(['Start', 'End']);
498
+ const measuredRowHeight = (model, key) => __foldkitBrandViewResult((Record.get(model.measuredRowHeights, key)), "@foldkit/ui/virtualList/index.js#measuredRowHeight");
499
+ const listLayout = (model, items, itemToKey, itemToRowHeightPx, dynamicRowHeights, itemToEstimatedRowHeightPx, contentAlignment) => {
500
+ const rowHeightFor = (item, index) => {
501
+ if (dynamicRowHeights !== undefined) {
502
+ const key = itemToKey(item, index);
503
+ return __foldkitBrandViewResult((Option.getOrElse(measuredRowHeight(model, key), () => __foldkitBrandViewResult((itemToEstimatedRowHeightPx?.(item, index) ?? model.rowHeightPx), "@foldkit/ui/virtualList/index.js#anonymous~25"))), "@foldkit/ui/virtualList/index.js#rowHeightFor");
504
+ }
505
+ if (itemToRowHeightPx !== undefined) {
506
+ return __foldkitBrandViewResult((itemToRowHeightPx(item, index)), "@foldkit/ui/virtualList/index.js#rowHeightFor");
507
+ }
508
+ return __foldkitBrandViewResult((model.rowHeightPx), "@foldkit/ui/virtualList/index.js#rowHeightFor");
509
+ };
510
+ const maybeCumulativeOffsets = dynamicRowHeights === undefined && itemToRowHeightPx === undefined
511
+ ? Option.none()
512
+ : Option.some(prefixSum(items, rowHeightFor));
513
+ const totalHeight = Option.match(maybeCumulativeOffsets, {
514
+ onNone: () => __foldkitBrandViewResult((items.length * model.rowHeightPx), "@foldkit/ui/virtualList/index.js#onNone~2"),
515
+ onSome: lastOrZero,
516
+ });
517
+ const containerHeight = Measurement.match(model.measurement, {
518
+ Unmeasured: () => __foldkitBrandViewResult((0), "@foldkit/ui/virtualList/index.js#Unmeasured~2"),
519
+ Measured: ({ containerHeight }) => __foldkitBrandViewResult((containerHeight), "@foldkit/ui/virtualList/index.js#Measured~2"),
520
+ });
521
+ const leadingInset = contentAlignment === 'End' ? Math.max(0, containerHeight - totalHeight) : 0;
522
+ return __foldkitBrandViewResult(({
523
+ maybeCumulativeOffsets,
524
+ rowHeightPx: model.rowHeightPx,
525
+ totalHeight,
526
+ leadingInset,
527
+ containerHeight,
528
+ maxScrollTop: Math.max(0, leadingInset + totalHeight - containerHeight),
529
+ }), "@foldkit/ui/virtualList/index.js#listLayout");
530
+ };
531
+ const offsetAt = (layout, index) => __foldkitBrandViewResult((Option.match(layout.maybeCumulativeOffsets, {
532
+ onNone: () => __foldkitBrandViewResult((index * layout.rowHeightPx), "@foldkit/ui/virtualList/index.js#onNone~3"),
533
+ onSome: cumulativeOffsets => __foldkitBrandViewResult((pipe(cumulativeOffsets, Array.get(index), Option.getOrElse(() => __foldkitBrandViewResult((layout.totalHeight), "@foldkit/ui/virtualList/index.js#anonymous~26")))), "@foldkit/ui/virtualList/index.js#onSome~2"),
534
+ })), "@foldkit/ui/virtualList/index.js#offsetAt");
535
+ const rowOffsetsForIndex = (layout, itemCount, index) => {
536
+ if (itemCount === 0) {
537
+ return __foldkitBrandViewResult((Option.none()), "@foldkit/ui/virtualList/index.js#rowOffsetsForIndex");
538
+ }
539
+ const clampedIndex = Math.max(0, Math.min(index, itemCount - 1));
540
+ return __foldkitBrandViewResult((Option.some({
541
+ startOffset: layout.leadingInset + offsetAt(layout, clampedIndex),
542
+ endOffset: layout.leadingInset + offsetAt(layout, clampedIndex + 1),
543
+ })), "@foldkit/ui/virtualList/index.js#rowOffsetsForIndex");
544
+ };
545
+ const indexForKey = (items, itemToKey, key) => __foldkitBrandViewResult((Array.findFirstIndex(items, (item, index) => __foldkitBrandViewResult((itemToKey(item, index) === key), "@foldkit/ui/virtualList/index.js#anonymous~27"))), "@foldkit/ui/virtualList/index.js#indexForKey");
546
+ const clampLayoutScrollTop = (layout, scrollTop) => __foldkitBrandViewResult((Math.max(0, Math.min(scrollTop, layout.maxScrollTop))), "@foldkit/ui/virtualList/index.js#clampLayoutScrollTop");
547
+ const alignedLayoutScrollTop = (model, layout, offsets, alignment) => __foldkitBrandViewResult((clampLayoutScrollTop(layout, scrollTopForRow(model.scrollTop, layout.containerHeight, offsets.startOffset, offsets.endOffset, alignment))), "@foldkit/ui/virtualList/index.js#alignedLayoutScrollTop");
548
+ const scrollTopForLayoutTarget = (model, layout, items, itemToKey, target, alignment) => __foldkitBrandViewResult((ScrollTarget.match(target, {
549
+ Index: ({ index }) => __foldkitBrandViewResult((Option.map(rowOffsetsForIndex(layout, items.length, index), offsets => __foldkitBrandViewResult((alignedLayoutScrollTop(model, layout, offsets, alignment)), "@foldkit/ui/virtualList/index.js#anonymous~28"))), "@foldkit/ui/virtualList/index.js#Index~3"),
550
+ Key: ({ key }) => __foldkitBrandViewResult((pipe(indexForKey(items, itemToKey, key), Option.flatMap(index => __foldkitBrandViewResult((rowOffsetsForIndex(layout, items.length, index)), "@foldkit/ui/virtualList/index.js#anonymous~29")), Option.map(offsets => __foldkitBrandViewResult((alignedLayoutScrollTop(model, layout, offsets, alignment)), "@foldkit/ui/virtualList/index.js#anonymous~30")))), "@foldkit/ui/virtualList/index.js#Key~3"),
551
+ Offset: ({ offset }) => __foldkitBrandViewResult((Option.some(clampLayoutScrollTop(layout, offset))), "@foldkit/ui/virtualList/index.js#Offset~3"),
552
+ End: () => __foldkitBrandViewResult((Option.some(layout.maxScrollTop)), "@foldkit/ui/virtualList/index.js#End~3"),
553
+ })), "@foldkit/ui/virtualList/index.js#scrollTopForLayoutTarget");
554
+ const scrollTopForLayoutAnchor = (layout, items, itemToKey, anchor) => __foldkitBrandViewResult((ViewportAnchor.match(anchor, {
555
+ Offset: ({ scrollTop }) => __foldkitBrandViewResult((clampLayoutScrollTop(layout, scrollTop)), "@foldkit/ui/virtualList/index.js#Offset~4"),
556
+ Row: ({ key, index, viewportOffset }) => {
557
+ const anchorIndex = Option.getOrElse(indexForKey(items, itemToKey, key), () => __foldkitBrandViewResult((index), "@foldkit/ui/virtualList/index.js#anonymous~31"));
558
+ const maybeOffsets = rowOffsetsForIndex(layout, items.length, anchorIndex);
559
+ return __foldkitBrandViewResult((Option.match(maybeOffsets, {
560
+ onNone: () => __foldkitBrandViewResult((0), "@foldkit/ui/virtualList/index.js#onNone~4"),
561
+ onSome: offsets => __foldkitBrandViewResult((clampLayoutScrollTop(layout, offsets.startOffset - viewportOffset)), "@foldkit/ui/virtualList/index.js#onSome~3"),
562
+ })), "@foldkit/ui/virtualList/index.js#Row~3");
232
563
  },
233
- })), "@foldkit/ui/virtualList/index.js#visibleWindowVariable");
234
- // SUBSCRIPTION
235
- const containerElement = (id) => __foldkitBrandViewResult((Option.fromNullishOr(document.getElementById(id))), "@foldkit/ui/virtualList/index.js#containerElement");
236
- /** Subscriptions that track the container's scroll position and size.
237
- *
238
- * - **scroll**: listens for `scroll` events on the container element and
239
- * emits `ScrolledContainer` with the new `scrollTop`.
240
- * - **resize**: observes the container with `ResizeObserver` and emits
241
- * `MeasuredContainer` with the new height.
242
- *
243
- * A `MutationObserver` watches the document for the container element
244
- * appearing and disappearing, so the listeners attach the moment the
245
- * element is inserted into the DOM and clean up when it is removed. This
246
- * makes the subscription robust across SPA route changes: navigating to a
247
- * page that mounts the list, away, and back all reattach correctly without
248
- * the consumer having to teach the framework about navigation. */
249
- export const subscriptions = Subscription.make()(entry => (__foldkitBrandViewResult(({
250
- containerEvents: entry({ id: Schema.String }, {
251
- modelToDependencies: model => (__foldkitBrandViewResult(({ id: model.id }), "@foldkit/ui/virtualList/index.js#modelToDependencies")),
252
- dependenciesToStream: ({ id }) => __foldkitBrandViewResult((Stream.callback(queue => __foldkitBrandViewResult((Effect.acquireRelease(Effect.sync(() => {
253
- const state = {
254
- scrollListener: null,
255
- resizeObserver: null,
256
- observedElement: null,
257
- pendingFrame: null,
258
- };
259
- const detach = () => {
260
- if (state.resizeObserver !== null) {
261
- state.resizeObserver.disconnect();
262
- state.resizeObserver = null;
263
- }
264
- if (state.observedElement !== null &&
265
- state.scrollListener !== null) {
266
- state.observedElement.removeEventListener('scroll', state.scrollListener);
267
- }
268
- state.observedElement = null;
269
- state.scrollListener = null;
270
- };
271
- const attach = (element) => {
272
- const listener = () => __foldkitBrandViewResult((Queue.offerUnsafe(queue, Message.ScrolledContainer({ scrollTop: element.scrollTop }))), "@foldkit/ui/virtualList/index.js#listener");
273
- element.addEventListener('scroll', listener, { passive: true });
274
- state.scrollListener = listener;
275
- state.observedElement = element;
276
- state.resizeObserver = new ResizeObserver(entries => {
277
- const lastEntry = Array.last(entries);
278
- if (Option.isSome(lastEntry)) {
279
- Queue.offerUnsafe(queue, Message.MeasuredContainer({
280
- containerHeight: lastEntry.value.contentRect.height,
281
- }));
282
- }
283
- });
284
- state.resizeObserver.observe(element);
285
- };
286
- const reconcile = () => {
287
- const maybeElement = containerElement(id);
288
- if (Option.isNone(maybeElement)) {
289
- if (state.observedElement !== null) {
290
- detach();
291
- }
292
- return;
293
- }
294
- if (state.observedElement === maybeElement.value) {
295
- return;
296
- }
297
- detach();
298
- attach(maybeElement.value);
299
- };
300
- reconcile();
301
- // NOTE: observes the entire document subtree because the container
302
- // can be inserted/removed by any parent the consumer chooses (route
303
- // changes, conditional renders, modal mounts), and the framework
304
- // has no way to know that hierarchy in advance. Reconcile is gated
305
- // by rAF and short-circuits when the cached observedElement is
306
- // still in the DOM, so per-mutation cost stays low even with
307
- // subtree: true.
308
- const mutationObserver = new MutationObserver(() => {
309
- if (state.pendingFrame !== null) {
310
- return;
311
- }
312
- state.pendingFrame = requestAnimationFrame(() => {
313
- state.pendingFrame = null;
314
- reconcile();
315
- });
316
- });
317
- mutationObserver.observe(document.body, {
318
- childList: true,
319
- subtree: true,
320
- });
321
- return __foldkitBrandViewResult(({ state, detach, mutationObserver }), "@foldkit/ui/virtualList/index.js#anonymous~10");
322
- }), ({ state, detach, mutationObserver }) => __foldkitBrandViewResult((Effect.sync(() => {
323
- mutationObserver.disconnect();
324
- if (state.pendingFrame !== null) {
325
- cancelAnimationFrame(state.pendingFrame);
564
+ End: () => __foldkitBrandViewResult((layout.maxScrollTop), "@foldkit/ui/virtualList/index.js#End~4"),
565
+ })), "@foldkit/ui/virtualList/index.js#scrollTopForLayoutAnchor");
566
+ const scrollTopForView = (model, layout, items, itemToKey) => __foldkitBrandViewResult((PendingScroll.match(model.pendingScroll, {
567
+ Idle: () => __foldkitBrandViewResult((clampLayoutScrollTop(layout, model.scrollTop)), "@foldkit/ui/virtualList/index.js#Idle~3"),
568
+ Pending: ({ request }) => __foldkitBrandViewResult((ScrollRequest.match(request, {
569
+ Target: ({ target, alignment }) => __foldkitBrandViewResult((Option.getOrElse(scrollTopForLayoutTarget(model, layout, items, itemToKey, target, alignment), () => __foldkitBrandViewResult((clampLayoutScrollTop(layout, model.scrollTop)), "@foldkit/ui/virtualList/index.js#anonymous~32"))), "@foldkit/ui/virtualList/index.js#Target~2"),
570
+ Anchor: ({ anchor }) => __foldkitBrandViewResult((scrollTopForLayoutAnchor(layout, items, itemToKey, anchor)), "@foldkit/ui/virtualList/index.js#Anchor~2"),
571
+ })), "@foldkit/ui/virtualList/index.js#Pending~4"),
572
+ })), "@foldkit/ui/virtualList/index.js#scrollTopForView");
573
+ const visibleWindowForLayout = (layout, scrollTop, itemCount, overscan) => {
574
+ const contentScrollTop = Math.max(0, scrollTop - layout.leadingInset);
575
+ const contentViewportEnd = Math.max(0, scrollTop + layout.containerHeight - layout.leadingInset);
576
+ const firstVisibleIndex = Option.match(layout.maybeCumulativeOffsets, {
577
+ onNone: () => __foldkitBrandViewResult((Math.floor(contentScrollTop / layout.rowHeightPx)), "@foldkit/ui/virtualList/index.js#onNone~5"),
578
+ onSome: cumulativeOffsets => __foldkitBrandViewResult((pipe(cumulativeOffsets, Array.findFirstIndex(Number.isGreaterThan(contentScrollTop)), Option.match({
579
+ onNone: () => __foldkitBrandViewResult((itemCount), "@foldkit/ui/virtualList/index.js#onNone~6"),
580
+ onSome: index => __foldkitBrandViewResult((Math.max(0, index - 1)), "@foldkit/ui/virtualList/index.js#onSome~5"),
581
+ }))), "@foldkit/ui/virtualList/index.js#onSome~4"),
582
+ });
583
+ const lastVisibleIndex = Option.match(layout.maybeCumulativeOffsets, {
584
+ onNone: () => __foldkitBrandViewResult((Math.ceil(contentViewportEnd / layout.rowHeightPx)), "@foldkit/ui/virtualList/index.js#onNone~7"),
585
+ onSome: cumulativeOffsets => __foldkitBrandViewResult((pipe(cumulativeOffsets, Array.findFirstIndex(Number.isGreaterThanOrEqualTo(contentViewportEnd)), Option.getOrElse(() => __foldkitBrandViewResult((itemCount), "@foldkit/ui/virtualList/index.js#anonymous~33")))), "@foldkit/ui/virtualList/index.js#onSome~6"),
586
+ });
587
+ const startIndex = clampIndex(firstVisibleIndex - overscan, itemCount);
588
+ const endIndex = clampIndex(lastVisibleIndex + overscan, itemCount);
589
+ return __foldkitBrandViewResult(({
590
+ startIndex,
591
+ endIndex,
592
+ topSpacerHeight: layout.leadingInset + offsetAt(layout, startIndex),
593
+ bottomSpacerHeight: layout.totalHeight - offsetAt(layout, endIndex),
594
+ }), "@foldkit/ui/virtualList/index.js#visibleWindowForLayout");
595
+ };
596
+ // ELEMENT LIFECYCLE
597
+ const rowMeasurement = (entry) => {
598
+ if (!(entry.target instanceof HTMLElement)) {
599
+ return __foldkitBrandViewResult((Option.none()), "@foldkit/ui/virtualList/index.js#rowMeasurement");
600
+ }
601
+ const key = entry.target.getAttribute('data-virtual-list-item-key');
602
+ const layoutVersion = pipe(entry.target.getAttribute('data-virtual-list-layout-version'), Option.fromNullishOr, Option.flatMap(Number.parse));
603
+ if (key === null || Option.isNone(layoutVersion)) {
604
+ return __foldkitBrandViewResult((Option.none()), "@foldkit/ui/virtualList/index.js#rowMeasurement");
605
+ }
606
+ return __foldkitBrandViewResult((Option.some({
607
+ key,
608
+ height: entry.target.getBoundingClientRect().height,
609
+ layoutVersion: layoutVersion.value,
610
+ })), "@foldkit/ui/virtualList/index.js#rowMeasurement");
611
+ };
612
+ const observeVirtualList = (element, id) => __foldkitBrandViewResult((Stream.callback(queue => __foldkitBrandViewResult((Effect.acquireRelease(Effect.sync(() => {
613
+ if (!(element instanceof HTMLElement)) {
614
+ return __foldkitBrandViewResult((() => __foldkitBrandViewResult((undefined), "@foldkit/ui/virtualList/index.js#anonymous~36")), "@foldkit/ui/virtualList/index.js#anonymous~35");
615
+ }
616
+ const emitContainerMeasurement = () => __foldkitBrandViewResult((Queue.offerUnsafe(queue, Message.ResizedContainer({
617
+ containerWidth: element.clientWidth,
618
+ containerHeight: element.clientHeight,
619
+ }))), "@foldkit/ui/virtualList/index.js#emitContainerMeasurement");
620
+ emitContainerMeasurement();
621
+ const scrollListener = () => __foldkitBrandViewResult((Queue.offerUnsafe(queue, Message.ObservedContainerScroll({
622
+ scrollTop: element.scrollTop,
623
+ scrollHeight: element.scrollHeight,
624
+ containerHeight: element.clientHeight,
625
+ anchor: observedAnchor(element),
626
+ }))), "@foldkit/ui/virtualList/index.js#scrollListener");
627
+ element.addEventListener('scroll', scrollListener, { passive: true });
628
+ const containerResizeObserver = new ResizeObserver(entries => {
629
+ const lastEntry = Array.last(entries);
630
+ if (Option.isSome(lastEntry)) {
631
+ Queue.offerUnsafe(queue, Message.ResizedContainer({
632
+ containerWidth: lastEntry.value.contentRect.width,
633
+ containerHeight: lastEntry.value.contentRect.height,
634
+ }));
635
+ }
636
+ });
637
+ containerResizeObserver.observe(element);
638
+ const observedRows = new Map();
639
+ const rowResizeObserver = new ResizeObserver(entries => {
640
+ const measurements = Array.flatMap(entries, entry => __foldkitBrandViewResult((Option.match(rowMeasurement(entry), {
641
+ onNone: () => __foldkitBrandViewResult(([]), "@foldkit/ui/virtualList/index.js#onNone~8"),
642
+ onSome: measurement => __foldkitBrandViewResult(([measurement]), "@foldkit/ui/virtualList/index.js#onSome~7"),
643
+ })), "@foldkit/ui/virtualList/index.js#anonymous~39"));
644
+ if (Array.isArrayNonEmpty(measurements)) {
645
+ Queue.offerUnsafe(queue, Message.MeasuredRows({ measurements }));
646
+ }
647
+ });
648
+ const reconcileRows = () => {
649
+ if (element.childElementCount === 0) {
650
+ emitContainerMeasurement();
651
+ }
652
+ const rows = Array.filter(renderedRows(element), row => __foldkitBrandViewResult((row.getAttribute('data-virtual-list-measure') === 'true'), "@foldkit/ui/virtualList/index.js#anonymous~40"));
653
+ const measurableRows = new Set(rows);
654
+ for (const row of observedRows.keys()) {
655
+ if (!measurableRows.has(row)) {
656
+ rowResizeObserver.unobserve(row);
657
+ observedRows.delete(row);
658
+ }
659
+ }
660
+ for (const row of rows) {
661
+ const layoutVersion = row.getAttribute('data-virtual-list-layout-version') ?? '';
662
+ if (observedRows.get(row) !== layoutVersion) {
663
+ rowResizeObserver.unobserve(row);
664
+ observedRows.set(row, layoutVersion);
665
+ rowResizeObserver.observe(row);
326
666
  }
327
- detach();
328
- })), "@foldkit/ui/virtualList/index.js#anonymous~14")).pipe(Effect.flatMap(() => __foldkitBrandViewResult((Effect.never), "@foldkit/ui/virtualList/index.js#anonymous~16")))), "@foldkit/ui/virtualList/index.js#anonymous~9"))), "@foldkit/ui/virtualList/index.js#dependenciesToStream"),
329
- }),
330
- }), "@foldkit/ui/virtualList/index.js#anonymous~8")));
667
+ }
668
+ };
669
+ const mutationObserver = new MutationObserver(reconcileRows);
670
+ mutationObserver.observe(element, {
671
+ attributes: true,
672
+ attributeFilter: [
673
+ 'data-virtual-list-layout-version',
674
+ 'data-virtual-list-measure',
675
+ ],
676
+ childList: true,
677
+ subtree: true,
678
+ });
679
+ reconcileRows();
680
+ const containers = mountedContainers.get(id) ?? new Set();
681
+ containers.add(element);
682
+ mountedContainers.set(id, containers);
683
+ return __foldkitBrandViewResult((() => {
684
+ const containers = mountedContainers.get(id);
685
+ if (containers !== undefined) {
686
+ containers.delete(element);
687
+ if (containers.size === 0) {
688
+ mountedContainers.delete(id);
689
+ }
690
+ }
691
+ mutationObserver.disconnect();
692
+ rowResizeObserver.disconnect();
693
+ containerResizeObserver.disconnect();
694
+ element.removeEventListener('scroll', scrollListener);
695
+ }), "@foldkit/ui/virtualList/index.js#anonymous~35");
696
+ }), cleanup => __foldkitBrandViewResult((Effect.sync(cleanup)), "@foldkit/ui/virtualList/index.js#anonymous~42")).pipe(Effect.flatMap(() => __foldkitBrandViewResult((Effect.never), "@foldkit/ui/virtualList/index.js#anonymous~43")))), "@foldkit/ui/virtualList/index.js#anonymous~34"))), "@foldkit/ui/virtualList/index.js#observeVirtualList");
697
+ /** Container-owned Mount that tracks scrolling, container resizing, and
698
+ * rendered row measurements for dynamic-height lists. */
699
+ export const ObserveVirtualList = Mount.defineStream('ObserveVirtualList', {
700
+ args: { id: Schema.String },
701
+ messages: [
702
+ Message.ObservedContainerScroll,
703
+ Message.ResizedContainer,
704
+ Message.MeasuredRows,
705
+ ],
706
+ execute: ({ element, id, viewStateChanges }) => __foldkitBrandViewResult((viewStateChanges.pipe(Stream.switchMap(viewState => __foldkitBrandViewResult((viewState === 'Live' ? observeVirtualList(element, id) : Stream.never), "@foldkit/ui/virtualList/index.js#anonymous~44")))), "@foldkit/ui/virtualList/index.js#execute~2"),
707
+ });
331
708
  // VIEW
332
709
  const DEFAULT_OVERSCAN = 5;
333
710
  export const view = () =>
334
711
  /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
335
712
  __foldkitBrandViewResult((viewImpl), "@foldkit/ui/virtualList/index.js#view");
336
713
  const viewImpl = defineView((model, viewInputs, h) => {
337
- const { items, itemToKey, itemToView, itemToRowHeightPx, overscan = DEFAULT_OVERSCAN, rowElement = 'li', containerClassName, containerAttributes = [], } = viewInputs;
714
+ const { items, itemToKey, itemToView, itemToRowHeightPx, dynamicRowHeights, itemToEstimatedRowHeightPx, overscan = DEFAULT_OVERSCAN, rowElement = 'li', containerClassName, containerAttributes = [], contentAlignment = 'Start', } = viewInputs;
338
715
  const baseContainerAttributes = [
339
716
  h.Id(model.id),
340
717
  h.Role('list'),
341
- h.DataAttribute('virtual-list-id', model.id),
718
+ h.DataAttribute('virtual-list-scroll-version', String(model.pendingScrollVersion)),
719
+ h.OnMount(ObserveVirtualList({ id: model.id })),
342
720
  h.Style({
343
721
  overflow: 'auto',
722
+ 'overflow-anchor': 'none',
344
723
  'list-style': 'none',
345
724
  margin: '0',
346
725
  padding: '0',
@@ -354,14 +733,13 @@ const viewImpl = defineView((model, viewInputs, h) => {
354
733
  ...containerAttributes,
355
734
  ];
356
735
  const renderContainer = (children) => __foldkitBrandViewResult((h.keyed('ul')(model.id, allContainerAttributes, children)), "@foldkit/ui/virtualList/index.js#renderContainer");
357
- const maybeWindow = itemToRowHeightPx !== undefined
358
- ? visibleWindowVariable(model, items, itemToRowHeightPx, overscan)
359
- : visibleWindow(model, items.length, overscan);
360
- const rowHeightFor = (item, dataIndex) => __foldkitBrandViewResult((itemToRowHeightPx !== undefined
361
- ? itemToRowHeightPx(item, dataIndex)
362
- : model.rowHeightPx), "@foldkit/ui/virtualList/index.js#rowHeightFor");
736
+ const layout = listLayout(model, items, itemToKey, itemToRowHeightPx, dynamicRowHeights, itemToEstimatedRowHeightPx, contentAlignment);
737
+ const maybeWindow = Measurement.match(model.measurement, {
738
+ Unmeasured: () => __foldkitBrandViewResult((Option.none()), "@foldkit/ui/virtualList/index.js#Unmeasured~3"),
739
+ Measured: () => __foldkitBrandViewResult((Option.some(visibleWindowForLayout(layout, scrollTopForView(model, layout, items, itemToKey), items.length, overscan))), "@foldkit/ui/virtualList/index.js#Measured~3"),
740
+ });
363
741
  return __foldkitBrandViewResult((Option.match(maybeWindow, {
364
- onNone: () => __foldkitBrandViewResult((renderContainer([])), "@foldkit/ui/virtualList/index.js#onNone~2"),
742
+ onNone: () => __foldkitBrandViewResult((renderContainer([])), "@foldkit/ui/virtualList/index.js#onNone~9"),
365
743
  onSome: ({ startIndex, endIndex, topSpacerHeight, bottomSpacerHeight, }) => {
366
744
  const visibleItems = items.slice(startIndex, endIndex);
367
745
  const topSpacer = h.keyed('li')(`${model.id}-top-spacer`, [
@@ -374,18 +752,30 @@ const viewImpl = defineView((model, viewInputs, h) => {
374
752
  ]);
375
753
  const renderedRows = Array.map(visibleItems, (item, sliceIndex) => {
376
754
  const dataIndex = startIndex + sliceIndex;
377
- return __foldkitBrandViewResult((h.keyed(rowElement)(itemToKey(item, dataIndex), [
755
+ const key = itemToKey(item, dataIndex);
756
+ const rowHeight = offsetAt(layout, dataIndex + 1) - offsetAt(layout, dataIndex);
757
+ const rowSizingAttributes = dynamicRowHeights === undefined
758
+ ? [
759
+ h.Style({
760
+ height: `${rowHeight}px`,
761
+ display: 'grid',
762
+ }),
763
+ ]
764
+ : [
765
+ h.DataAttribute('virtual-list-measure', 'true'),
766
+ h.DataAttribute('virtual-list-layout-version', String(model.layoutVersion)),
767
+ h.Style({ display: 'grid' }),
768
+ ];
769
+ return __foldkitBrandViewResult((h.keyed(rowElement)(key, [
378
770
  h.Role('listitem'),
771
+ h.DataAttribute('virtual-list-item-key', key),
379
772
  h.DataAttribute('virtual-list-item-index', String(dataIndex)),
380
773
  h.AriaSetsize(items.length),
381
774
  h.AriaPosinset(dataIndex + 1),
382
- h.Style({
383
- height: `${rowHeightFor(item, dataIndex)}px`,
384
- display: 'grid',
385
- }),
386
- ], [itemToView(item, dataIndex)])), "@foldkit/ui/virtualList/index.js#anonymous~18");
775
+ ...rowSizingAttributes,
776
+ ], [itemToView(item, dataIndex)])), "@foldkit/ui/virtualList/index.js#anonymous~46");
387
777
  });
388
- return __foldkitBrandViewResult((renderContainer([topSpacer, ...renderedRows, bottomSpacer])), "@foldkit/ui/virtualList/index.js#onSome~2");
778
+ return __foldkitBrandViewResult((renderContainer([topSpacer, ...renderedRows, bottomSpacer])), "@foldkit/ui/virtualList/index.js#onSome~8");
389
779
  },
390
- })), "@foldkit/ui/virtualList/index.js#anonymous~17");
780
+ })), "@foldkit/ui/virtualList/index.js#anonymous~45");
391
781
  });