foldkit-primitives 0.1.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.
- package/LICENSE +21 -0
- package/README.md +454 -0
- package/dist/device/index.d.mts +312 -0
- package/dist/device/index.d.mts.map +1 -0
- package/dist/device/index.mjs +490 -0
- package/dist/device/index.mjs.map +1 -0
- package/dist/dom/index.d.mts +73 -0
- package/dist/dom/index.d.mts.map +1 -0
- package/dist/dom/index.mjs +218 -0
- package/dist/dom/index.mjs.map +1 -0
- package/dist/events/index.d.mts +154 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +287 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +10 -0
- package/dist/media/index.d.mts +79 -0
- package/dist/media/index.d.mts.map +1 -0
- package/dist/media/index.mjs +127 -0
- package/dist/media/index.mjs.map +1 -0
- package/dist/motion/index.d.mts +112 -0
- package/dist/motion/index.d.mts.map +1 -0
- package/dist/motion/index.mjs +208 -0
- package/dist/motion/index.mjs.map +1 -0
- package/dist/net/index.d.mts +199 -0
- package/dist/net/index.d.mts.map +1 -0
- package/dist/net/index.mjs +329 -0
- package/dist/net/index.mjs.map +1 -0
- package/dist/observers/index.d.mts +59 -0
- package/dist/observers/index.d.mts.map +1 -0
- package/dist/observers/index.mjs +156 -0
- package/dist/observers/index.mjs.map +1 -0
- package/dist/state/index.d.mts +342 -0
- package/dist/state/index.d.mts.map +1 -0
- package/dist/state/index.mjs +504 -0
- package/dist/state/index.mjs.map +1 -0
- package/dist/time/index.d.mts +129 -0
- package/dist/time/index.d.mts.map +1 -0
- package/dist/time/index.mjs +213 -0
- package/dist/time/index.mjs.map +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { Effect, Queue, Schema, Stream } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
import { defineMessageUnion } from "foldkit/message";
|
|
4
|
+
import { Bundle } from "foldkit-bundle";
|
|
5
|
+
//#region src/state/history.ts
|
|
6
|
+
/**
|
|
7
|
+
* Undo/redo as a bundle factory: the past, present, and future of one value
|
|
8
|
+
* live in the Model, so every step replays. Pushing records, even an unchanged
|
|
9
|
+
* value; skipping duplicates is the application's policy, not the stack's.
|
|
10
|
+
*/
|
|
11
|
+
const HistoryModel = (value) => Schema.Struct({
|
|
12
|
+
past: Schema.Array(value),
|
|
13
|
+
present: value,
|
|
14
|
+
future: Schema.Array(value)
|
|
15
|
+
});
|
|
16
|
+
const history = (config) => {
|
|
17
|
+
const capacity = config.capacity ?? 100;
|
|
18
|
+
if (!Number.isInteger(capacity) || capacity < 0) throw new Error(`History "${config.name}": capacity must be a non-negative integer`);
|
|
19
|
+
const Model = HistoryModel(config.value);
|
|
20
|
+
const Message = defineMessageUnion({
|
|
21
|
+
Push: { value: config.value },
|
|
22
|
+
Undo: {},
|
|
23
|
+
Redo: {},
|
|
24
|
+
Clear: {}
|
|
25
|
+
});
|
|
26
|
+
return Object.assign(Bundle.make(config.name, {
|
|
27
|
+
Model,
|
|
28
|
+
Message,
|
|
29
|
+
args: Schema.Struct({ initial: config.value }),
|
|
30
|
+
init: (args) => ({ model: {
|
|
31
|
+
past: [],
|
|
32
|
+
present: args.initial,
|
|
33
|
+
future: []
|
|
34
|
+
} }),
|
|
35
|
+
update: (model, message) => Message.match(message, {
|
|
36
|
+
Push: ({ value }) => ({ model: {
|
|
37
|
+
past: capacity === 0 ? [] : [...model.past, model.present].slice(-capacity),
|
|
38
|
+
present: value,
|
|
39
|
+
future: []
|
|
40
|
+
} }),
|
|
41
|
+
Undo: () => {
|
|
42
|
+
const previous = model.past[model.past.length - 1];
|
|
43
|
+
if (previous === void 0) return { model };
|
|
44
|
+
return { model: {
|
|
45
|
+
past: model.past.slice(0, -1),
|
|
46
|
+
present: previous,
|
|
47
|
+
future: [model.present, ...model.future]
|
|
48
|
+
} };
|
|
49
|
+
},
|
|
50
|
+
Redo: () => {
|
|
51
|
+
const next = model.future[0];
|
|
52
|
+
if (next === void 0) return { model };
|
|
53
|
+
return { model: {
|
|
54
|
+
past: [...model.past, model.present],
|
|
55
|
+
present: next,
|
|
56
|
+
future: model.future.slice(1)
|
|
57
|
+
} };
|
|
58
|
+
},
|
|
59
|
+
Clear: () => ({ model: {
|
|
60
|
+
past: [],
|
|
61
|
+
present: model.present,
|
|
62
|
+
future: []
|
|
63
|
+
} })
|
|
64
|
+
})
|
|
65
|
+
}), { Message });
|
|
66
|
+
};
|
|
67
|
+
/** Whether Undo would move: the past is non-empty. */
|
|
68
|
+
const canUndo = (model) => model.past.length > 0;
|
|
69
|
+
/** Whether Redo would move: the future is non-empty. */
|
|
70
|
+
const canRedo = (model) => model.future.length > 0;
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/state/locale.ts
|
|
73
|
+
/**
|
|
74
|
+
* Locale as a bundle: the current locale string in the Model. Starts from
|
|
75
|
+
* `navigator.language` where available, falling back to the configured
|
|
76
|
+
* default; changes arrive as Messages. No subscriptions: language changes
|
|
77
|
+
* take effect on reload.
|
|
78
|
+
*/
|
|
79
|
+
const LocaleModel = Schema.Struct({ locale: Schema.String });
|
|
80
|
+
const LocaleMessage = defineMessageUnion({ SetLocale: { locale: Schema.String } });
|
|
81
|
+
const Locale = Bundle.make("Locale", {
|
|
82
|
+
Model: LocaleModel,
|
|
83
|
+
Message: LocaleMessage,
|
|
84
|
+
args: Schema.Struct({ default: Schema.String }),
|
|
85
|
+
init: (args) => ({ model: { locale: typeof navigator !== "undefined" && typeof navigator.language === "string" ? navigator.language : args.default } }),
|
|
86
|
+
update: (model, message) => LocaleMessage.match(message, { SetLocale: ({ locale }) => ({ model: {
|
|
87
|
+
...model,
|
|
88
|
+
locale
|
|
89
|
+
} }) })
|
|
90
|
+
});
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/state/pagination.ts
|
|
93
|
+
/**
|
|
94
|
+
* Pagination as a bundle: the page, page size, and total live in the Model,
|
|
95
|
+
* so page changes replay and every transition clamps into range. Pure logic,
|
|
96
|
+
* no streams: data loading stays the application's job.
|
|
97
|
+
*/
|
|
98
|
+
const PaginationModel = Schema.Struct({
|
|
99
|
+
page: Schema.Number,
|
|
100
|
+
perPage: Schema.Number,
|
|
101
|
+
total: Schema.NullOr(Schema.Number)
|
|
102
|
+
});
|
|
103
|
+
const PaginationMessage = defineMessageUnion({
|
|
104
|
+
GoToPage: { page: Schema.Number },
|
|
105
|
+
NextPage: {},
|
|
106
|
+
PrevPage: {},
|
|
107
|
+
SetPerPage: { perPage: Schema.Number },
|
|
108
|
+
SetTotal: { total: Schema.NullOr(Schema.Number) }
|
|
109
|
+
});
|
|
110
|
+
const clampPage = (page, perPage, total) => {
|
|
111
|
+
if (!Number.isInteger(page)) return 1;
|
|
112
|
+
if (page < 1) return 1;
|
|
113
|
+
if (total === null) return page;
|
|
114
|
+
return Math.min(page, Math.max(1, Math.ceil(total / perPage)));
|
|
115
|
+
};
|
|
116
|
+
/** The number of pages, or null while the total is unknown. */
|
|
117
|
+
const pageCount = (model) => model.total === null ? null : Math.max(1, Math.ceil(model.total / model.perPage));
|
|
118
|
+
/** The offset of the first item on the page, for a slice or a query. */
|
|
119
|
+
const offset = (model) => (model.page - 1) * model.perPage;
|
|
120
|
+
const Pagination = Bundle.make("Pagination", {
|
|
121
|
+
Model: PaginationModel,
|
|
122
|
+
Message: PaginationMessage,
|
|
123
|
+
args: Schema.Struct({
|
|
124
|
+
perPage: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())),
|
|
125
|
+
total: Schema.optional(Schema.NullOr(Schema.Number.pipe(Schema.check(Schema.isFinite()))))
|
|
126
|
+
}),
|
|
127
|
+
init: (args) => ({ model: {
|
|
128
|
+
page: 1,
|
|
129
|
+
perPage: args.perPage,
|
|
130
|
+
total: args.total ?? null
|
|
131
|
+
} }),
|
|
132
|
+
update: (model, message) => PaginationMessage.match(message, {
|
|
133
|
+
GoToPage: ({ page }) => ({ model: {
|
|
134
|
+
...model,
|
|
135
|
+
page: clampPage(page, model.perPage, model.total)
|
|
136
|
+
} }),
|
|
137
|
+
NextPage: () => ({ model: {
|
|
138
|
+
...model,
|
|
139
|
+
page: clampPage(model.page + 1, model.perPage, model.total)
|
|
140
|
+
} }),
|
|
141
|
+
PrevPage: () => ({ model: {
|
|
142
|
+
...model,
|
|
143
|
+
page: clampPage(model.page - 1, model.perPage, model.total)
|
|
144
|
+
} }),
|
|
145
|
+
SetPerPage: ({ perPage }) => perPage > 0 ? { model: {
|
|
146
|
+
...model,
|
|
147
|
+
perPage,
|
|
148
|
+
page: clampPage(model.page, perPage, model.total)
|
|
149
|
+
} } : { model },
|
|
150
|
+
SetTotal: ({ total }) => total === null || total >= 0 ? { model: {
|
|
151
|
+
...model,
|
|
152
|
+
total,
|
|
153
|
+
page: clampPage(model.page, model.perPage, total)
|
|
154
|
+
} } : { model }
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/state/range.ts
|
|
159
|
+
/**
|
|
160
|
+
* A numeric range as an array: `range(1, 4)` is `[1, 2, 3]`, inclusive of
|
|
161
|
+
* the start and exclusive of the end, like `Array.from` length math. The
|
|
162
|
+
* pagination companion: `range(1, (pageCount(model) ?? 0) + 1)` is the page
|
|
163
|
+
* list. A zero or non-finite step would loop forever, so it throws, naming
|
|
164
|
+
* the caller — like History's capacity check.
|
|
165
|
+
*/
|
|
166
|
+
const range = (start, end, step = 1) => {
|
|
167
|
+
if (!Number.isFinite(step) || step === 0) throw new Error(`range: step must be a non-zero finite number, got ${step}`);
|
|
168
|
+
const values = [];
|
|
169
|
+
if (step > 0) for (let value = start; value < end; value += step) values.push(value);
|
|
170
|
+
else for (let value = start; value > end; value += step) values.push(value);
|
|
171
|
+
return values;
|
|
172
|
+
};
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/state/selection.ts
|
|
175
|
+
/**
|
|
176
|
+
* A selection set as a bundle: the selected ids in the Model, changed one at
|
|
177
|
+
* a time or replaced whole. Order follows first selection; toggling moves a
|
|
178
|
+
* reselected id to the end. Pure logic, no streams.
|
|
179
|
+
*/
|
|
180
|
+
const SelectionSetModel = Schema.Struct({ selected: Schema.Array(Schema.String) });
|
|
181
|
+
const SelectionSetMessage = defineMessageUnion({
|
|
182
|
+
Select: { id: Schema.String },
|
|
183
|
+
Deselect: { id: Schema.String },
|
|
184
|
+
Toggle: { id: Schema.String },
|
|
185
|
+
ReplaceAll: { ids: Schema.Array(Schema.String) },
|
|
186
|
+
Clear: {}
|
|
187
|
+
});
|
|
188
|
+
const without = (selected, id) => selected.filter((other) => other !== id);
|
|
189
|
+
/** Whether the id is currently selected. */
|
|
190
|
+
const isSelected = (model, id) => model.selected.includes(id);
|
|
191
|
+
const SelectionSet = Bundle.make("SelectionSet", {
|
|
192
|
+
Model: SelectionSetModel,
|
|
193
|
+
Message: SelectionSetMessage,
|
|
194
|
+
init: () => ({ model: { selected: [] } }),
|
|
195
|
+
update: (model, message) => SelectionSetMessage.match(message, {
|
|
196
|
+
Select: ({ id }) => isSelected(model, id) ? { model } : { model: { selected: [...model.selected, id] } },
|
|
197
|
+
Deselect: ({ id }) => ({ model: { selected: without(model.selected, id) } }),
|
|
198
|
+
Toggle: ({ id }) => isSelected(model, id) ? { model: { selected: without(model.selected, id) } } : { model: { selected: [...model.selected, id] } },
|
|
199
|
+
ReplaceAll: ({ ids }) => ({ model: { selected: [...new Set(ids)] } }),
|
|
200
|
+
Clear: () => ({ model: { selected: [] } })
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/state/virtual.ts
|
|
205
|
+
/**
|
|
206
|
+
* Windowing math for virtualized lists as pure functions: which rows to
|
|
207
|
+
* render, how tall the scrollable area is, and where each row starts. Row
|
|
208
|
+
* heights come from measured values with a per-row estimate fallback, so
|
|
209
|
+
* unmeasured rows still occupy space and measuring later only corrects.
|
|
210
|
+
* Keys are strings; heights are non-negative, and non-finite measurements
|
|
211
|
+
* fall back to the estimate instead of poisoning the sums.
|
|
212
|
+
*
|
|
213
|
+
* The `Virtual` bundle below owns the scroll position and the measured
|
|
214
|
+
* heights; `Viewport` reports container scrolls and `MeasureRow` reports
|
|
215
|
+
* row heights, both as Mounts on the elements they observe. The application
|
|
216
|
+
* renders `windowFor(...)` rows with its own views (keyed, so `each`
|
|
217
|
+
* placements keep identity) inside a spacer of `totalHeight(...)`.
|
|
218
|
+
*/
|
|
219
|
+
const saneHeight = (height, estimatedHeight) => Number.isFinite(height) && height >= 0 ? height : estimatedHeight;
|
|
220
|
+
const heightAt = (index, heights, keys, layout) => {
|
|
221
|
+
const key = keys[index];
|
|
222
|
+
if (key === void 0) return layout.estimatedHeight;
|
|
223
|
+
return saneHeight(heights[key] ?? layout.estimatedHeight, layout.estimatedHeight);
|
|
224
|
+
};
|
|
225
|
+
/** Total scrollable height: padding, measured rows, estimates, and gaps. */
|
|
226
|
+
const totalHeight = (keys, heights, layout) => {
|
|
227
|
+
let total = layout.paddingStart + layout.paddingEnd;
|
|
228
|
+
for (let index = 0; index < keys.length; index++) {
|
|
229
|
+
total += heightAt(index, heights, keys, layout);
|
|
230
|
+
if (index < keys.length - 1) total += layout.gap;
|
|
231
|
+
}
|
|
232
|
+
return total;
|
|
233
|
+
};
|
|
234
|
+
/** Pixel offset of a row's top edge: padding plus the prefix before it. */
|
|
235
|
+
const offsetFor = (index, keys, heights, layout) => {
|
|
236
|
+
const clamped = Math.max(0, Math.min(index, keys.length));
|
|
237
|
+
let offset = layout.paddingStart;
|
|
238
|
+
for (let i = 0; i < clamped; i++) offset += heightAt(i, heights, keys, layout) + layout.gap;
|
|
239
|
+
return offset;
|
|
240
|
+
};
|
|
241
|
+
/**
|
|
242
|
+
* Which rows to render for a scroll position: the rows intersecting
|
|
243
|
+
* `[scrollTop, scrollTop + viewportHeight)`, widened by `overscan` on both
|
|
244
|
+
* sides and clamped to `[0, count]`. Gaps and padding are dead space — no
|
|
245
|
+
* row — so boxes are tested, not cumulative coverage. An overscan of zero
|
|
246
|
+
* renders exactly the visible rows.
|
|
247
|
+
*/
|
|
248
|
+
const visibleRange = (keys, heights, layout, scrollTop, viewportHeight, overscan) => {
|
|
249
|
+
const count = keys.length;
|
|
250
|
+
if (count === 0 || viewportHeight <= 0) return {
|
|
251
|
+
start: 0,
|
|
252
|
+
end: 0
|
|
253
|
+
};
|
|
254
|
+
const top = Math.max(0, scrollTop);
|
|
255
|
+
const bottom = top + viewportHeight;
|
|
256
|
+
let start = 0;
|
|
257
|
+
let offset = layout.paddingStart;
|
|
258
|
+
for (let index = 0; index < count; index++) {
|
|
259
|
+
const height = heightAt(index, heights, keys, layout);
|
|
260
|
+
if (offset + height <= top) {
|
|
261
|
+
start = index + 1;
|
|
262
|
+
offset += height + layout.gap;
|
|
263
|
+
} else break;
|
|
264
|
+
}
|
|
265
|
+
let end = start;
|
|
266
|
+
let cursor = offset;
|
|
267
|
+
while (end < count && cursor < bottom) {
|
|
268
|
+
cursor += heightAt(end, heights, keys, layout) + layout.gap;
|
|
269
|
+
end += 1;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
start: Math.max(0, start - Math.max(0, overscan)),
|
|
273
|
+
end: Math.min(count, end + Math.max(0, overscan))
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
const VirtualModel = Schema.Struct({
|
|
277
|
+
scrollTop: Schema.Number,
|
|
278
|
+
heights: Schema.Record(Schema.String, Schema.Number),
|
|
279
|
+
scrolling: Schema.Boolean,
|
|
280
|
+
generation: Schema.Number,
|
|
281
|
+
estimatedHeight: Schema.Number,
|
|
282
|
+
overscan: Schema.Number,
|
|
283
|
+
gap: Schema.Number,
|
|
284
|
+
paddingStart: Schema.Number,
|
|
285
|
+
paddingEnd: Schema.Number
|
|
286
|
+
});
|
|
287
|
+
const VirtualMessage = defineMessageUnion({
|
|
288
|
+
Scrolled: { top: Schema.Number },
|
|
289
|
+
Measured: {
|
|
290
|
+
key: Schema.String,
|
|
291
|
+
height: Schema.Number
|
|
292
|
+
},
|
|
293
|
+
/** Drops heights for keys no longer listed: the bundle never sees the key
|
|
294
|
+
order, so the application tells it what left. */
|
|
295
|
+
Prune: { keys: Schema.Array(Schema.String) },
|
|
296
|
+
/** Fires when scrolling has been silent for `settleMs`: only the latest
|
|
297
|
+
generation counts, so a fling's intermediate timers emit nothing. */
|
|
298
|
+
Settled: { generation: Schema.Number }
|
|
299
|
+
});
|
|
300
|
+
const layoutOf = (model) => ({
|
|
301
|
+
estimatedHeight: model.estimatedHeight,
|
|
302
|
+
gap: model.gap,
|
|
303
|
+
paddingStart: model.paddingStart,
|
|
304
|
+
paddingEnd: model.paddingEnd
|
|
305
|
+
});
|
|
306
|
+
/**
|
|
307
|
+
* The rows to render plus the spacer height, from the Model, the key order,
|
|
308
|
+
* and the viewport height. Layout and overscan come from the Model, so the
|
|
309
|
+
* call cannot disagree with the placement. Keys stay the application's:
|
|
310
|
+
* render each row keyed, and per-row placements keep identity.
|
|
311
|
+
*/
|
|
312
|
+
const windowFor = (model, keys, viewportHeight) => {
|
|
313
|
+
const layout = layoutOf(model);
|
|
314
|
+
return {
|
|
315
|
+
...visibleRange(keys, model.heights, layout, model.scrollTop, viewportHeight, model.overscan),
|
|
316
|
+
totalHeight: totalHeight(keys, model.heights, layout)
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
/**
|
|
320
|
+
* Whether the viewport rests at (or past) the end, within `threshold`
|
|
321
|
+
* pixels: the infinite-scroll check. An empty list counts as ended — there
|
|
322
|
+
* is nothing to scroll, so more should load. Thresholds below zero clamp
|
|
323
|
+
* to zero.
|
|
324
|
+
*/
|
|
325
|
+
const isAtEnd = (model, keys, viewportHeight, threshold = 0) => {
|
|
326
|
+
const total = totalHeight(keys, model.heights, layoutOf(model));
|
|
327
|
+
if (total <= 0) return true;
|
|
328
|
+
return model.scrollTop + viewportHeight >= total - Math.max(0, threshold);
|
|
329
|
+
};
|
|
330
|
+
const saneTop = (top) => Number.isFinite(top) ? Math.max(0, top) : null;
|
|
331
|
+
const saneMeasured = (height) => Number.isFinite(height) && height >= 0 ? height : null;
|
|
332
|
+
const Virtual = Bundle.make("Virtual", {
|
|
333
|
+
Model: VirtualModel,
|
|
334
|
+
Message: VirtualMessage,
|
|
335
|
+
args: Schema.Struct({
|
|
336
|
+
estimatedHeight: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())),
|
|
337
|
+
overscan: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
338
|
+
gap: Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.check(Schema.isFinite())),
|
|
339
|
+
paddingStart: Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.check(Schema.isFinite())),
|
|
340
|
+
paddingEnd: Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.check(Schema.isFinite())),
|
|
341
|
+
settleMs: Schema.optional(Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite()))),
|
|
342
|
+
initialScrollTop: Schema.optional(Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.check(Schema.isFinite()))),
|
|
343
|
+
initialHeights: Schema.optional(Schema.Record(Schema.String, Schema.Number))
|
|
344
|
+
}),
|
|
345
|
+
init: (args) => {
|
|
346
|
+
const heights = {};
|
|
347
|
+
for (const [key, height] of Object.entries(args.initialHeights ?? {})) {
|
|
348
|
+
const sane = saneMeasured(height);
|
|
349
|
+
if (sane !== null) heights[key] = sane;
|
|
350
|
+
}
|
|
351
|
+
return { model: {
|
|
352
|
+
scrollTop: args.initialScrollTop ?? 0,
|
|
353
|
+
heights,
|
|
354
|
+
scrolling: false,
|
|
355
|
+
generation: 0,
|
|
356
|
+
estimatedHeight: args.estimatedHeight,
|
|
357
|
+
overscan: args.overscan,
|
|
358
|
+
gap: args.gap,
|
|
359
|
+
paddingStart: args.paddingStart,
|
|
360
|
+
paddingEnd: args.paddingEnd
|
|
361
|
+
} };
|
|
362
|
+
},
|
|
363
|
+
update: (model, message, args) => VirtualMessage.match(message, {
|
|
364
|
+
Scrolled: ({ top }) => {
|
|
365
|
+
const sane = saneTop(top);
|
|
366
|
+
if (sane === null) return { model };
|
|
367
|
+
const generation = model.generation + 1;
|
|
368
|
+
return {
|
|
369
|
+
model: {
|
|
370
|
+
...model,
|
|
371
|
+
scrollTop: sane,
|
|
372
|
+
scrolling: true,
|
|
373
|
+
generation
|
|
374
|
+
},
|
|
375
|
+
commands: [{
|
|
376
|
+
name: "Virtual.settle",
|
|
377
|
+
args: { generation },
|
|
378
|
+
effect: Effect.as(Effect.sleep(args.settleMs ?? 150), VirtualMessage.Settled({ generation }))
|
|
379
|
+
}]
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
Measured: ({ key, height }) => {
|
|
383
|
+
const sane = saneMeasured(height);
|
|
384
|
+
return sane === null ? { model } : { model: {
|
|
385
|
+
...model,
|
|
386
|
+
heights: {
|
|
387
|
+
...model.heights,
|
|
388
|
+
[key]: sane
|
|
389
|
+
}
|
|
390
|
+
} };
|
|
391
|
+
},
|
|
392
|
+
Prune: ({ keys }) => {
|
|
393
|
+
const kept = new Set(keys);
|
|
394
|
+
const heights = {};
|
|
395
|
+
for (const [key, height] of Object.entries(model.heights)) if (kept.has(key)) heights[key] = height;
|
|
396
|
+
return { model: {
|
|
397
|
+
...model,
|
|
398
|
+
heights
|
|
399
|
+
} };
|
|
400
|
+
},
|
|
401
|
+
Settled: ({ generation }) => generation === model.generation ? { model: {
|
|
402
|
+
...model,
|
|
403
|
+
scrolling: false
|
|
404
|
+
} } : { model }
|
|
405
|
+
})
|
|
406
|
+
});
|
|
407
|
+
const ViewportScrolled = Schema.TaggedStruct("ViewportScrolled", { top: Schema.Number });
|
|
408
|
+
/**
|
|
409
|
+
* The scroll container's position, reported on its own scrolls starting
|
|
410
|
+
* with the current one. Attach to the scrolling element — viewport scrolls
|
|
411
|
+
* arrive through the window-level scroll entry instead.
|
|
412
|
+
*/
|
|
413
|
+
const Viewport = Mount.defineStream("Viewport", {
|
|
414
|
+
messages: [ViewportScrolled],
|
|
415
|
+
execute: ({ element }) => {
|
|
416
|
+
const read = () => {
|
|
417
|
+
const scroller = element;
|
|
418
|
+
return ViewportScrolled.make({ top: scroller.scrollTop });
|
|
419
|
+
};
|
|
420
|
+
return Stream.concat(Stream.make(read()), Stream.fromEventListener(element, "scroll").pipe(Stream.map(read)));
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
const RowMeasured = Schema.TaggedStruct("RowMeasured", {
|
|
424
|
+
key: Schema.String,
|
|
425
|
+
height: Schema.Number
|
|
426
|
+
});
|
|
427
|
+
/**
|
|
428
|
+
* One row's height, reported on every resize starting with the current
|
|
429
|
+
* one. The key is the application's row identity, echoed back so the
|
|
430
|
+
* parent files the height without tracking which Mount sent what.
|
|
431
|
+
*/
|
|
432
|
+
const MeasureRow = Mount.defineStream("MeasureRow", {
|
|
433
|
+
messages: [RowMeasured],
|
|
434
|
+
args: { key: Schema.String },
|
|
435
|
+
execute: ({ element, key }) => Stream.callback((queue) => Effect.gen(function* () {
|
|
436
|
+
const Observed = globalThis.ResizeObserver;
|
|
437
|
+
if (Observed === void 0) return;
|
|
438
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
439
|
+
const observer = new Observed((entries) => {
|
|
440
|
+
const rect = entries[0]?.contentRect;
|
|
441
|
+
if (rect !== void 0) Queue.offerUnsafe(queue, RowMeasured.make({
|
|
442
|
+
key,
|
|
443
|
+
height: rect.height
|
|
444
|
+
}));
|
|
445
|
+
});
|
|
446
|
+
observer.observe(element);
|
|
447
|
+
return observer;
|
|
448
|
+
}), (observer) => Effect.sync(() => observer.disconnect()));
|
|
449
|
+
}))
|
|
450
|
+
});
|
|
451
|
+
/**
|
|
452
|
+
* Which section header sticks for a window: the last section starting at
|
|
453
|
+
* or before the first rendered row, or null above the first section. Order
|
|
454
|
+
* independent — the scan takes the maximum, so unsorted input still
|
|
455
|
+
* answers. CSS `position: sticky` does the sticking; this answers *what*
|
|
456
|
+
* sticks, for styling and callbacks.
|
|
457
|
+
*/
|
|
458
|
+
const stickyHeader = (sections, startIndex) => {
|
|
459
|
+
let current = null;
|
|
460
|
+
let currentIndex = -1;
|
|
461
|
+
for (const section of sections) if (section.index <= startIndex && section.index > currentIndex) {
|
|
462
|
+
current = section.key;
|
|
463
|
+
currentIndex = section.index;
|
|
464
|
+
}
|
|
465
|
+
return current;
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* Masonry layout as a pure function: fixed column count and width, each
|
|
469
|
+
* item packed into the currently shortest column. Unmeasured items use the
|
|
470
|
+
* estimate; poisoned heights fall back the same way. This is layout only,
|
|
471
|
+
* not windowing — every placed item renders, so it fits hundreds of images,
|
|
472
|
+
* not hundred-thousands. Needing a windowed grid is a different algorithm.
|
|
473
|
+
*/
|
|
474
|
+
const masonry = (keys, heights, options) => {
|
|
475
|
+
if (!Number.isInteger(options.columns) || options.columns < 1) throw new Error(`masonry: columns must be a positive integer, got ${options.columns}`);
|
|
476
|
+
if (!(options.columnWidth > 0) || !Number.isFinite(options.columnWidth)) throw new Error(`masonry: columnWidth must be a positive finite number, got ${options.columnWidth}`);
|
|
477
|
+
if (options.gap < 0 || !Number.isFinite(options.gap)) throw new Error(`masonry: gap must be a non-negative finite number, got ${options.gap}`);
|
|
478
|
+
if (options.estimatedHeight < 0 || !Number.isFinite(options.estimatedHeight)) throw new Error(`masonry: estimatedHeight must be a non-negative finite number, got ${options.estimatedHeight}`);
|
|
479
|
+
const columnHeights = new Array(options.columns).fill(0);
|
|
480
|
+
const placements = [];
|
|
481
|
+
for (const key of keys) {
|
|
482
|
+
const raw = heights[key] ?? options.estimatedHeight;
|
|
483
|
+
const height = Number.isFinite(raw) && raw >= 0 ? raw : options.estimatedHeight;
|
|
484
|
+
let column = 0;
|
|
485
|
+
for (let c = 1; c < columnHeights.length; c++) if (columnHeights[c] < columnHeights[column]) column = c;
|
|
486
|
+
placements.push({
|
|
487
|
+
key,
|
|
488
|
+
column,
|
|
489
|
+
x: column * (options.columnWidth + options.gap),
|
|
490
|
+
y: columnHeights[column],
|
|
491
|
+
width: options.columnWidth,
|
|
492
|
+
height
|
|
493
|
+
});
|
|
494
|
+
columnHeights[column] = columnHeights[column] + height + options.gap;
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
placements,
|
|
498
|
+
totalHeight: placements.length === 0 ? 0 : Math.max(...columnHeights) - options.gap
|
|
499
|
+
};
|
|
500
|
+
};
|
|
501
|
+
//#endregion
|
|
502
|
+
export { HistoryModel, Locale, LocaleMessage, LocaleModel, MeasureRow, Pagination, PaginationMessage, PaginationModel, RowMeasured, SelectionSet, SelectionSetMessage, SelectionSetModel, Viewport, ViewportScrolled, Virtual, VirtualMessage, VirtualModel, canRedo, canUndo, history, isAtEnd, isSelected, masonry, offset, offsetFor, pageCount, range, stickyHeader, totalHeight, visibleRange, windowFor };
|
|
503
|
+
|
|
504
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/state/history.ts","../../src/state/locale.ts","../../src/state/pagination.ts","../../src/state/range.ts","../../src/state/selection.ts","../../src/state/virtual.ts"],"sourcesContent":["/**\n * Undo/redo as a bundle factory: the past, present, and future of one value\n * live in the Model, so every step replays. Pushing records, even an unchanged\n * value; skipping duplicates is the application's policy, not the stack's.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const HistoryModel = <Value>(value: Schema.Codec<Value, unknown>) =>\n Schema.Struct({\n past: Schema.Array(value),\n present: value,\n future: Schema.Array(value),\n })\nexport type HistoryModel<Value> = {\n readonly past: ReadonlyArray<Value>\n readonly present: Value\n readonly future: ReadonlyArray<Value>\n}\n\nexport const history = <const Name extends string, Value>(config: {\n readonly name: Name\n readonly value: Schema.Codec<Value, unknown>\n readonly capacity?: number | undefined\n}) => {\n const capacity = config.capacity ?? 100\n // A negative or fractional capacity would silently keep a wrong-sized past;\n // fail at the factory, naming the caller, instead.\n if (!Number.isInteger(capacity) || capacity < 0) {\n throw new Error(`History \"${config.name}\": capacity must be a non-negative integer`)\n }\n const Model = HistoryModel(config.value)\n const Message = defineMessageUnion({\n Push: { value: config.value },\n Undo: {},\n Redo: {},\n Clear: {},\n })\n return Object.assign(\n Bundle.make(config.name, {\n Model,\n Message,\n args: Schema.Struct({ initial: config.value }),\n init: args => ({ model: { past: [], present: args.initial, future: [] } }),\n update: (model, message) =>\n Message.match(message, {\n Push: ({ value }) => ({\n model: {\n // slice(-0) is slice(0): a zero capacity keeps nothing, explicitly.\n past: capacity === 0 ? [] : [...model.past, model.present].slice(-capacity),\n present: value,\n future: [],\n },\n }),\n Undo: () => {\n const previous = model.past[model.past.length - 1]\n if (previous === undefined) return { model }\n return {\n model: {\n past: model.past.slice(0, -1),\n present: previous,\n future: [model.present, ...model.future],\n },\n }\n },\n Redo: () => {\n const next = model.future[0]\n if (next === undefined) return { model }\n return {\n model: {\n past: [...model.past, model.present],\n present: next,\n future: model.future.slice(1),\n },\n }\n },\n Clear: () => ({ model: { past: [], present: model.present, future: [] } }),\n }),\n }),\n // The union object is both the Message codec and its constructors, so a\n // placement dispatches `EditHistory.Message.Push(...)` like any bundle's\n // exported Message.\n { Message },\n )\n}\n\n/** Whether Undo would move: the past is non-empty. */\nexport const canUndo = <Value>(model: HistoryModel<Value>): boolean => model.past.length > 0\n\n/** Whether Redo would move: the future is non-empty. */\nexport const canRedo = <Value>(model: HistoryModel<Value>): boolean => model.future.length > 0\n","/**\n * Locale as a bundle: the current locale string in the Model. Starts from\n * `navigator.language` where available, falling back to the configured\n * default; changes arrive as Messages. No subscriptions: language changes\n * take effect on reload.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const LocaleModel = Schema.Struct({ locale: Schema.String })\nexport type LocaleModel = typeof LocaleModel.Type\n\nexport const LocaleMessage = defineMessageUnion({ SetLocale: { locale: Schema.String } })\nexport type LocaleMessage = typeof LocaleMessage.Type\n\nexport const Locale = Bundle.make('Locale', {\n Model: LocaleModel,\n Message: LocaleMessage,\n args: Schema.Struct({ default: Schema.String }),\n init: args => ({\n model: {\n locale:\n typeof navigator !== 'undefined' && typeof navigator.language === 'string'\n ? navigator.language\n : args.default,\n },\n }),\n update: (model, message) =>\n LocaleMessage.match(message, {\n SetLocale: ({ locale }) => ({ model: { ...model, locale } }),\n }),\n})\n","/**\n * Pagination as a bundle: the page, page size, and total live in the Model,\n * so page changes replay and every transition clamps into range. Pure logic,\n * no streams: data loading stays the application's job.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const PaginationModel = Schema.Struct({\n page: Schema.Number,\n perPage: Schema.Number,\n total: Schema.NullOr(Schema.Number),\n})\nexport type PaginationModel = typeof PaginationModel.Type\n\nexport const PaginationMessage = defineMessageUnion({\n GoToPage: { page: Schema.Number },\n NextPage: {},\n PrevPage: {},\n SetPerPage: { perPage: Schema.Number },\n SetTotal: { total: Schema.NullOr(Schema.Number) },\n})\nexport type PaginationMessage = typeof PaginationMessage.Type\n\nconst clampPage = (page: number, perPage: number, total: number | null): number => {\n if (!Number.isInteger(page)) return 1\n if (page < 1) return 1\n if (total === null) return page\n return Math.min(page, Math.max(1, Math.ceil(total / perPage)))\n}\n\n/** The number of pages, or null while the total is unknown. */\nexport const pageCount = (model: PaginationModel): number | null =>\n model.total === null ? null : Math.max(1, Math.ceil(model.total / model.perPage))\n\n/** The offset of the first item on the page, for a slice or a query. */\nexport const offset = (model: PaginationModel): number => (model.page - 1) * model.perPage\n\nexport const Pagination = Bundle.make('Pagination', {\n Model: PaginationModel,\n Message: PaginationMessage,\n args: Schema.Struct({\n perPage: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n total: Schema.optional(Schema.NullOr(Schema.Number.pipe(Schema.check(Schema.isFinite())))),\n }),\n init: args => ({\n model: { page: 1, perPage: args.perPage, total: args.total ?? null },\n }),\n update: (model, message) =>\n PaginationMessage.match(message, {\n GoToPage: ({ page }) => ({\n model: { ...model, page: clampPage(page, model.perPage, model.total) },\n }),\n NextPage: () => ({\n model: { ...model, page: clampPage(model.page + 1, model.perPage, model.total) },\n }),\n PrevPage: () => ({\n model: { ...model, page: clampPage(model.page - 1, model.perPage, model.total) },\n }),\n SetPerPage: ({ perPage }) =>\n perPage > 0\n ? { model: { ...model, perPage, page: clampPage(model.page, perPage, model.total) } }\n : { model },\n SetTotal: ({ total }) =>\n total === null || total >= 0\n ? {\n model: { ...model, total, page: clampPage(model.page, model.perPage, total) },\n }\n : { model },\n }),\n})\n","/**\n * A numeric range as an array: `range(1, 4)` is `[1, 2, 3]`, inclusive of\n * the start and exclusive of the end, like `Array.from` length math. The\n * pagination companion: `range(1, (pageCount(model) ?? 0) + 1)` is the page\n * list. A zero or non-finite step would loop forever, so it throws, naming\n * the caller — like History's capacity check.\n */\nexport const range = (start: number, end: number, step = 1): ReadonlyArray<number> => {\n if (!Number.isFinite(step) || step === 0) {\n throw new Error(`range: step must be a non-zero finite number, got ${step}`)\n }\n const values: Array<number> = []\n if (step > 0) {\n for (let value = start; value < end; value += step) {\n values.push(value)\n }\n } else {\n for (let value = start; value > end; value += step) {\n values.push(value)\n }\n }\n return values\n}\n","/**\n * A selection set as a bundle: the selected ids in the Model, changed one at\n * a time or replaced whole. Order follows first selection; toggling moves a\n * reselected id to the end. Pure logic, no streams.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const SelectionSetModel = Schema.Struct({ selected: Schema.Array(Schema.String) })\nexport type SelectionSetModel = typeof SelectionSetModel.Type\n\nexport const SelectionSetMessage = defineMessageUnion({\n Select: { id: Schema.String },\n Deselect: { id: Schema.String },\n Toggle: { id: Schema.String },\n ReplaceAll: { ids: Schema.Array(Schema.String) },\n Clear: {},\n})\nexport type SelectionSetMessage = typeof SelectionSetMessage.Type\n\nconst without = (selected: ReadonlyArray<string>, id: string): ReadonlyArray<string> =>\n selected.filter(other => other !== id)\n\n/** Whether the id is currently selected. */\nexport const isSelected = (model: SelectionSetModel, id: string): boolean =>\n model.selected.includes(id)\n\nexport const SelectionSet = Bundle.make('SelectionSet', {\n Model: SelectionSetModel,\n Message: SelectionSetMessage,\n init: () => ({ model: { selected: [] } }),\n update: (model, message) =>\n SelectionSetMessage.match(message, {\n Select: ({ id }) =>\n isSelected(model, id) ? { model } : { model: { selected: [...model.selected, id] } },\n Deselect: ({ id }) => ({ model: { selected: without(model.selected, id) } }),\n Toggle: ({ id }) =>\n isSelected(model, id)\n ? { model: { selected: without(model.selected, id) } }\n : { model: { selected: [...model.selected, id] } },\n ReplaceAll: ({ ids }) => ({ model: { selected: [...new Set(ids)] } }),\n Clear: () => ({ model: { selected: [] } }),\n }),\n})\n","/**\n * Windowing math for virtualized lists as pure functions: which rows to\n * render, how tall the scrollable area is, and where each row starts. Row\n * heights come from measured values with a per-row estimate fallback, so\n * unmeasured rows still occupy space and measuring later only corrects.\n * Keys are strings; heights are non-negative, and non-finite measurements\n * fall back to the estimate instead of poisoning the sums.\n *\n * The `Virtual` bundle below owns the scroll position and the measured\n * heights; `Viewport` reports container scrolls and `MeasureRow` reports\n * row heights, both as Mounts on the elements they observe. The application\n * renders `windowFor(...)` rows with its own views (keyed, so `each`\n * placements keep identity) inside a spacer of `totalHeight(...)`.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Mount from 'foldkit/mount'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport interface VirtualWindow {\n /** First visible row index, inclusive. */\n readonly start: number\n /** End row index, exclusive. */\n readonly end: number\n}\n\n/** Layout constants: per-row estimate, inter-row gap, and end padding. */\nexport interface VirtualLayout {\n readonly estimatedHeight: number\n readonly gap: number\n readonly paddingStart: number\n readonly paddingEnd: number\n}\n\nconst saneHeight = (height: number, estimatedHeight: number): number =>\n Number.isFinite(height) && height >= 0 ? height : estimatedHeight\n\nconst heightAt = (\n index: number,\n heights: Readonly<Record<string, number>>,\n keys: ReadonlyArray<string>,\n layout: VirtualLayout,\n): number => {\n const key = keys[index]\n if (key === undefined) return layout.estimatedHeight\n return saneHeight(heights[key] ?? layout.estimatedHeight, layout.estimatedHeight)\n}\n\n/** Total scrollable height: padding, measured rows, estimates, and gaps. */\nexport const totalHeight = (\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n layout: VirtualLayout,\n): number => {\n let total = layout.paddingStart + layout.paddingEnd\n for (let index = 0; index < keys.length; index++) {\n total += heightAt(index, heights, keys, layout)\n if (index < keys.length - 1) total += layout.gap\n }\n return total\n}\n\n/** Pixel offset of a row's top edge: padding plus the prefix before it. */\nexport const offsetFor = (\n index: number,\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n layout: VirtualLayout,\n): number => {\n const clamped = Math.max(0, Math.min(index, keys.length))\n let offset = layout.paddingStart\n for (let i = 0; i < clamped; i++) {\n offset += heightAt(i, heights, keys, layout) + layout.gap\n }\n return offset\n}\n\n/**\n * Which rows to render for a scroll position: the rows intersecting\n * `[scrollTop, scrollTop + viewportHeight)`, widened by `overscan` on both\n * sides and clamped to `[0, count]`. Gaps and padding are dead space — no\n * row — so boxes are tested, not cumulative coverage. An overscan of zero\n * renders exactly the visible rows.\n */\nexport const visibleRange = (\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n layout: VirtualLayout,\n scrollTop: number,\n viewportHeight: number,\n overscan: number,\n): VirtualWindow => {\n const count = keys.length\n if (count === 0 || viewportHeight <= 0) return { start: 0, end: 0 }\n const top = Math.max(0, scrollTop)\n const bottom = top + viewportHeight\n let start = 0\n let offset = layout.paddingStart\n for (let index = 0; index < count; index++) {\n const height = heightAt(index, heights, keys, layout)\n if (offset + height <= top) {\n start = index + 1\n offset += height + layout.gap\n } else {\n break\n }\n }\n let end = start\n let cursor = offset\n while (end < count && cursor < bottom) {\n cursor += heightAt(end, heights, keys, layout) + layout.gap\n end += 1\n }\n return {\n start: Math.max(0, start - Math.max(0, overscan)),\n end: Math.min(count, end + Math.max(0, overscan)),\n }\n}\n\nexport const VirtualModel = Schema.Struct({\n scrollTop: Schema.Number,\n heights: Schema.Record(Schema.String, Schema.Number),\n scrolling: Schema.Boolean,\n generation: Schema.Number,\n estimatedHeight: Schema.Number,\n overscan: Schema.Number,\n gap: Schema.Number,\n paddingStart: Schema.Number,\n paddingEnd: Schema.Number,\n})\nexport type VirtualModel = typeof VirtualModel.Type\n\nexport const VirtualMessage = defineMessageUnion({\n Scrolled: { top: Schema.Number },\n Measured: { key: Schema.String, height: Schema.Number },\n /** Drops heights for keys no longer listed: the bundle never sees the key\n order, so the application tells it what left. */\n Prune: { keys: Schema.Array(Schema.String) },\n /** Fires when scrolling has been silent for `settleMs`: only the latest\n generation counts, so a fling's intermediate timers emit nothing. */\n Settled: { generation: Schema.Number },\n})\nexport type VirtualMessage = typeof VirtualMessage.Type\n\nconst layoutOf = (model: VirtualModel): VirtualLayout => ({\n estimatedHeight: model.estimatedHeight,\n gap: model.gap,\n paddingStart: model.paddingStart,\n paddingEnd: model.paddingEnd,\n})\n\n/**\n * The rows to render plus the spacer height, from the Model, the key order,\n * and the viewport height. Layout and overscan come from the Model, so the\n * call cannot disagree with the placement. Keys stay the application's:\n * render each row keyed, and per-row placements keep identity.\n */\nexport const windowFor = (\n model: VirtualModel,\n keys: ReadonlyArray<string>,\n viewportHeight: number,\n): VirtualWindow & { readonly totalHeight: number } => {\n const layout = layoutOf(model)\n const window = visibleRange(\n keys,\n model.heights,\n layout,\n model.scrollTop,\n viewportHeight,\n model.overscan,\n )\n return { ...window, totalHeight: totalHeight(keys, model.heights, layout) }\n}\n\n/**\n * Whether the viewport rests at (or past) the end, within `threshold`\n * pixels: the infinite-scroll check. An empty list counts as ended — there\n * is nothing to scroll, so more should load. Thresholds below zero clamp\n * to zero.\n */\nexport const isAtEnd = (\n model: VirtualModel,\n keys: ReadonlyArray<string>,\n viewportHeight: number,\n threshold = 0,\n): boolean => {\n const total = totalHeight(keys, model.heights, layoutOf(model))\n if (total <= 0) return true\n return model.scrollTop + viewportHeight >= total - Math.max(0, threshold)\n}\n\nconst saneTop = (top: number): number | null => (Number.isFinite(top) ? Math.max(0, top) : null)\n\nconst saneMeasured = (height: number): number | null =>\n Number.isFinite(height) && height >= 0 ? height : null\n\nexport const Virtual = Bundle.make<\n 'Virtual',\n VirtualModel,\n VirtualMessage,\n {\n readonly estimatedHeight: number\n readonly overscan: number\n readonly gap: number\n readonly paddingStart: number\n readonly paddingEnd: number\n readonly settleMs?: number | undefined\n readonly initialScrollTop?: number | undefined\n readonly initialHeights?: Readonly<Record<string, number>> | undefined\n }\n>('Virtual', {\n Model: VirtualModel,\n Message: VirtualMessage,\n args: Schema.Struct({\n estimatedHeight: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n overscan: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))),\n gap: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n paddingStart: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n paddingEnd: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n settleMs: Schema.optional(\n Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())),\n ),\n initialScrollTop: Schema.optional(\n Schema.Number.pipe(\n Schema.check(Schema.isGreaterThanOrEqualTo(0)),\n Schema.check(Schema.isFinite()),\n ),\n ),\n initialHeights: Schema.optional(Schema.Record(Schema.String, Schema.Number)),\n }),\n init: args => {\n // Restored measurements pass through the same sanitation as live ones,\n // so a poisoned cache cannot wedge the sums.\n const heights: Record<string, number> = {}\n for (const [key, height] of Object.entries(args.initialHeights ?? {})) {\n const sane = saneMeasured(height)\n if (sane !== null) heights[key] = sane\n }\n return {\n model: {\n scrollTop: args.initialScrollTop ?? 0,\n heights,\n scrolling: false,\n generation: 0,\n estimatedHeight: args.estimatedHeight,\n overscan: args.overscan,\n gap: args.gap,\n paddingStart: args.paddingStart,\n paddingEnd: args.paddingEnd,\n },\n }\n },\n update: (model, message, args) =>\n VirtualMessage.match<Update.ReturnWithOutMessage<VirtualModel, VirtualMessage, never>>(\n message,\n {\n Scrolled: ({ top }) => {\n const sane = saneTop(top)\n if (sane === null) return { model }\n const generation = model.generation + 1\n return {\n model: { ...model, scrollTop: sane, scrolling: true, generation },\n commands: [\n {\n name: 'Virtual.settle',\n args: { generation },\n effect: Effect.as(\n Effect.sleep(args.settleMs ?? 150),\n VirtualMessage.Settled({ generation }),\n ),\n },\n ],\n }\n },\n Measured: ({ key, height }) => {\n const sane = saneMeasured(height)\n return sane === null\n ? { model }\n : { model: { ...model, heights: { ...model.heights, [key]: sane } } }\n },\n Prune: ({ keys }) => {\n const kept = new Set(keys)\n const heights: Record<string, number> = {}\n for (const [key, height] of Object.entries(model.heights)) {\n if (kept.has(key)) heights[key] = height\n }\n return { model: { ...model, heights } }\n },\n // A superseded silence timer carries an old generation: ignore it.\n Settled: ({ generation }) =>\n generation === model.generation ? { model: { ...model, scrolling: false } } : { model },\n },\n ),\n})\n\nexport const ViewportScrolled = Schema.TaggedStruct('ViewportScrolled', { top: Schema.Number })\nexport type ViewportScrolled = typeof ViewportScrolled.Type\n\n/**\n * The scroll container's position, reported on its own scrolls starting\n * with the current one. Attach to the scrolling element — viewport scrolls\n * arrive through the window-level scroll entry instead.\n */\nexport const Viewport = Mount.defineStream('Viewport', {\n messages: [ViewportScrolled],\n execute: ({ element }) => {\n // The mount needs only a scroll position, not a full Element.\n const read = (): ViewportScrolled => {\n const scroller = element as unknown as { readonly scrollTop: number }\n return ViewportScrolled.make({ top: scroller.scrollTop })\n }\n return Stream.concat(\n Stream.make(read()),\n Stream.fromEventListener(element, 'scroll').pipe(Stream.map(read)),\n )\n },\n})\n\nexport const RowMeasured = Schema.TaggedStruct('RowMeasured', {\n key: Schema.String,\n height: Schema.Number,\n})\nexport type RowMeasured = typeof RowMeasured.Type\n\ntype RowObserverCtor = new (callback: ResizeObserverCallback) => ResizeObserver\n\n/**\n * One row's height, reported on every resize starting with the current\n * one. The key is the application's row identity, echoed back so the\n * parent files the height without tracking which Mount sent what.\n */\nexport const MeasureRow = Mount.defineStream('MeasureRow', {\n messages: [RowMeasured],\n args: { key: Schema.String },\n execute: ({ element, key }) =>\n Stream.callback<typeof RowMeasured.Type>(queue =>\n Effect.gen(function* () {\n const Observed = (globalThis as { ResizeObserver?: RowObserverCtor }).ResizeObserver\n if (Observed === undefined) return\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n const observer = new Observed(entries => {\n const rect = entries[0]?.contentRect\n if (rect !== undefined) {\n Queue.offerUnsafe(queue, RowMeasured.make({ key, height: rect.height }))\n }\n })\n observer.observe(element)\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\n )\n }),\n ),\n})\n\n/**\n * Which section header sticks for a window: the last section starting at\n * or before the first rendered row, or null above the first section. Order\n * independent — the scan takes the maximum, so unsorted input still\n * answers. CSS `position: sticky` does the sticking; this answers *what*\n * sticks, for styling and callbacks.\n */\nexport const stickyHeader = (\n sections: ReadonlyArray<{ readonly key: string; readonly index: number }>,\n startIndex: number,\n): string | null => {\n let current: string | null = null\n let currentIndex = -1\n for (const section of sections) {\n if (section.index <= startIndex && section.index > currentIndex) {\n current = section.key\n currentIndex = section.index\n }\n }\n return current\n}\n\nexport interface MasonryPlacement {\n readonly key: string\n readonly column: number\n readonly x: number\n readonly y: number\n readonly width: number\n readonly height: number\n}\n\n/**\n * Masonry layout as a pure function: fixed column count and width, each\n * item packed into the currently shortest column. Unmeasured items use the\n * estimate; poisoned heights fall back the same way. This is layout only,\n * not windowing — every placed item renders, so it fits hundreds of images,\n * not hundred-thousands. Needing a windowed grid is a different algorithm.\n */\nexport const masonry = (\n keys: ReadonlyArray<string>,\n heights: Readonly<Record<string, number>>,\n options: {\n readonly columns: number\n readonly columnWidth: number\n readonly gap: number\n readonly estimatedHeight: number\n },\n): { readonly placements: ReadonlyArray<MasonryPlacement>; readonly totalHeight: number } => {\n if (!Number.isInteger(options.columns) || options.columns < 1) {\n throw new Error(`masonry: columns must be a positive integer, got ${options.columns}`)\n }\n if (!(options.columnWidth > 0) || !Number.isFinite(options.columnWidth)) {\n throw new Error(\n `masonry: columnWidth must be a positive finite number, got ${options.columnWidth}`,\n )\n }\n if (options.gap < 0 || !Number.isFinite(options.gap)) {\n throw new Error(`masonry: gap must be a non-negative finite number, got ${options.gap}`)\n }\n if (options.estimatedHeight < 0 || !Number.isFinite(options.estimatedHeight)) {\n throw new Error(\n `masonry: estimatedHeight must be a non-negative finite number, got ${options.estimatedHeight}`,\n )\n }\n const columnHeights: Array<number> = new Array(options.columns).fill(0)\n const placements: Array<MasonryPlacement> = []\n for (const key of keys) {\n const raw = heights[key] ?? options.estimatedHeight\n const height = Number.isFinite(raw) && raw >= 0 ? raw : options.estimatedHeight\n let column = 0\n for (let c = 1; c < columnHeights.length; c++) {\n if (columnHeights[c]! < columnHeights[column]!) column = c\n }\n placements.push({\n key,\n column,\n x: column * (options.columnWidth + options.gap),\n y: columnHeights[column]!,\n width: options.columnWidth,\n height,\n })\n columnHeights[column] = columnHeights[column]! + height + options.gap\n }\n return {\n placements,\n totalHeight: placements.length === 0 ? 0 : Math.max(...columnHeights) - options.gap,\n }\n}\n"],"mappings":"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"}
|