foldkit-primitives 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +514 -43
  2. package/dist/device/index.d.mts +11 -11
  3. package/dist/device/index.d.mts.map +1 -1
  4. package/dist/device/index.mjs +95 -87
  5. package/dist/device/index.mjs.map +1 -1
  6. package/dist/dom/index.d.mts +71 -1
  7. package/dist/dom/index.d.mts.map +1 -1
  8. package/dist/dom/index.mjs +303 -5
  9. package/dist/dom/index.mjs.map +1 -1
  10. package/dist/editable-text-CKkS_mWt.d.mts +235 -0
  11. package/dist/editable-text-CKkS_mWt.d.mts.map +1 -0
  12. package/dist/events/index.d.mts +10 -1
  13. package/dist/events/index.d.mts.map +1 -1
  14. package/dist/events/index.mjs +34 -18
  15. package/dist/events/index.mjs.map +1 -1
  16. package/dist/follow-tab-stop-DpxwLXvU.mjs +800 -0
  17. package/dist/follow-tab-stop-DpxwLXvU.mjs.map +1 -0
  18. package/dist/index-Cppf-lBx.d.mts +130 -0
  19. package/dist/index-Cppf-lBx.d.mts.map +1 -0
  20. package/dist/index.d.mts +8 -6
  21. package/dist/index.mjs +8 -6
  22. package/dist/input-modality-DGOhAknO.d.mts +26 -0
  23. package/dist/input-modality-DGOhAknO.d.mts.map +1 -0
  24. package/dist/input-modality-m6v7__Xs.mjs +43 -0
  25. package/dist/input-modality-m6v7__Xs.mjs.map +1 -0
  26. package/dist/interaction/index.d.mts +1017 -0
  27. package/dist/interaction/index.d.mts.map +1 -0
  28. package/dist/interaction/index.mjs +1647 -0
  29. package/dist/interaction/index.mjs.map +1 -0
  30. package/dist/internal-DP-xLKbT.mjs +37 -0
  31. package/dist/internal-DP-xLKbT.mjs.map +1 -0
  32. package/dist/media/index.d.mts.map +1 -1
  33. package/dist/media/index.mjs +3 -3
  34. package/dist/media/index.mjs.map +1 -1
  35. package/dist/motion/index.d.mts +2 -112
  36. package/dist/motion/index.mjs +2 -208
  37. package/dist/motion-DGGBZ0wt.mjs +240 -0
  38. package/dist/motion-DGGBZ0wt.mjs.map +1 -0
  39. package/dist/net/index.d.mts +68 -4
  40. package/dist/net/index.d.mts.map +1 -1
  41. package/dist/net/index.mjs +139 -56
  42. package/dist/net/index.mjs.map +1 -1
  43. package/dist/observers/index.d.mts +1 -1
  44. package/dist/observers/index.mjs +3 -0
  45. package/dist/observers/index.mjs.map +1 -1
  46. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  47. package/dist/state/index.d.mts +57 -15
  48. package/dist/state/index.d.mts.map +1 -1
  49. package/dist/state/index.mjs +198 -90
  50. package/dist/state/index.mjs.map +1 -1
  51. package/dist/time/index.d.mts +21 -1
  52. package/dist/time/index.d.mts.map +1 -1
  53. package/dist/time/index.mjs +48 -19
  54. package/dist/time/index.mjs.map +1 -1
  55. package/package.json +31 -8
  56. package/dist/motion/index.d.mts.map +0 -1
  57. package/dist/motion/index.mjs.map +0 -1
@@ -6,11 +6,53 @@ export declare const HistoryModel: <Value>(value: Schema.Codec<Value, unknown>)
6
6
  readonly past: Schema.$Array<Schema.Codec<Value, unknown, never, never>>;
7
7
  readonly present: Schema.Codec<Value, unknown, never, never>;
8
8
  readonly future: Schema.$Array<Schema.Codec<Value, unknown, never, never>>;
9
+ /** The group of the last push, so the next push of the same group joins it; `null` for none. */
10
+ readonly group: Schema.NullOr<Schema.String>;
9
11
  }>;
10
12
  export type HistoryModel<Value> = {
11
13
  readonly past: ReadonlyArray<Value>;
12
14
  readonly present: Value;
13
15
  readonly future: ReadonlyArray<Value>;
16
+ readonly group: string | null;
17
+ };
18
+ /** Whether Undo would move: the past is non-empty. */
19
+ export declare const canUndo: <Value>(model: HistoryModel<Value>) => boolean;
20
+ /** Whether Redo would move: the future is non-empty. */
21
+ export declare const canRedo: <Value>(model: HistoryModel<Value>) => boolean;
22
+ /** The steps of a history as pure functions of its Model. */
23
+ export declare const History: {
24
+ /** A history whose present is `value`, with nothing to undo. */
25
+ start: <Value>(present: Value) => HistoryModel<Value>;
26
+ /** Records `value` as the present; the same `group` as the last push joins its step. */
27
+ push: <Value>(model: HistoryModel<Value>, value: Value, options: {
28
+ readonly capacity: number;
29
+ readonly group?: string | null | undefined;
30
+ }) => HistoryModel<Value>;
31
+ /** One step back, or the Model as it was when there is none. */
32
+ undo: <Value>(model: HistoryModel<Value>) => HistoryModel<Value>;
33
+ /** One step forward, or the Model as it was when there is none. */
34
+ redo: <Value>(model: HistoryModel<Value>) => HistoryModel<Value>;
35
+ /**
36
+ * The value at `step` made the present, counting every kept value from the
37
+ * oldest (0), so the present is at `past.length`: what a click on a step's
38
+ * thumbnail does. The Model as it was for the present or a step not kept.
39
+ */
40
+ goTo: <Value>(model: HistoryModel<Value>, step: number) => HistoryModel<Value>;
41
+ /** Nothing to undo or redo, the present kept. */
42
+ clear: <Value>(model: HistoryModel<Value>) => HistoryModel<Value>;
43
+ /** The next push starts a step of its own, whatever its group: a group ends here. */
44
+ close: <Value>(model: HistoryModel<Value>) => HistoryModel<Value>;
45
+ /**
46
+ * The step `group` is making taken back, and not redoable: what a cancelled
47
+ * edit, or one that ended where it began, leaves. The Model as it was when
48
+ * the last push was not of `group`. What the group's first push dropped
49
+ * stays dropped: the redo future it cleared, and an entry capacity evicted;
50
+ * with a capacity of 0 nothing was kept, so the present stays and only the
51
+ * group ends.
52
+ */
53
+ revert: <Value>(model: HistoryModel<Value>, group: string) => HistoryModel<Value>;
54
+ canUndo: <Value>(model: HistoryModel<Value>) => boolean;
55
+ canRedo: <Value>(model: HistoryModel<Value>) => boolean;
14
56
  };
15
57
  export declare const history: <const Name extends string, Value>(config: {
16
58
  readonly name: Name;
@@ -18,33 +60,33 @@ export declare const history: <const Name extends string, Value>(config: {
18
60
  readonly capacity?: number | undefined;
19
61
  }) => Bundle.Bundle<Name, {
20
62
  readonly initial: Value;
21
- }, {
22
- readonly past: readonly Value[];
23
- readonly present: Value;
24
- readonly future: readonly Value[];
25
- }, {
63
+ }, HistoryModel<Value>, {
64
+ readonly _tag: "Clear";
65
+ } | {
26
66
  readonly _tag: "Push";
27
67
  readonly value: Value;
68
+ readonly group?: string | undefined;
28
69
  } | {
29
70
  readonly _tag: "Undo";
30
71
  } | {
31
72
  readonly _tag: "Redo";
32
73
  } | {
33
- readonly _tag: "Clear";
74
+ readonly _tag: "GoTo";
75
+ readonly step: number;
34
76
  }, never, never, never, void, {}, {}> & {
35
77
  Message: import("foldkit/message").MessageUnion<{
36
78
  readonly Push: {
37
79
  readonly value: Schema.Codec<Value, unknown, never, never>;
80
+ readonly group: Schema.optional<Schema.String>;
38
81
  };
39
82
  readonly Undo: {};
40
83
  readonly Redo: {};
84
+ readonly GoTo: {
85
+ readonly step: Schema.Number;
86
+ };
41
87
  readonly Clear: {};
42
88
  }>;
43
89
  };
44
- /** Whether Undo would move: the past is non-empty. */
45
- export declare const canUndo: <Value>(model: HistoryModel<Value>) => boolean;
46
- /** Whether Redo would move: the future is non-empty. */
47
- export declare const canRedo: <Value>(model: HistoryModel<Value>) => boolean;
48
90
  //#endregion
49
91
  //#region src/state/locale.d.ts
50
92
  export declare const LocaleModel: Schema.Struct<{
@@ -149,8 +191,6 @@ export declare const isSelected: (model: SelectionSetModel, id: string) => boole
149
191
  export declare const SelectionSet: Bundle.Bundle<"SelectionSet", void, {
150
192
  readonly selected: readonly string[];
151
193
  }, {
152
- readonly _tag: "Clear";
153
- } | {
154
194
  readonly _tag: "Select";
155
195
  readonly id: string;
156
196
  } | {
@@ -162,6 +202,8 @@ export declare const SelectionSet: Bundle.Bundle<"SelectionSet", void, {
162
202
  } | {
163
203
  readonly _tag: "ReplaceAll";
164
204
  readonly ids: readonly string[];
205
+ } | {
206
+ readonly _tag: "Clear";
165
207
  }, never, never, never, void, {}, {}>;
166
208
  //#endregion
167
209
  //#region src/state/virtual.d.ts
@@ -269,15 +311,15 @@ export declare const Virtual: Bundle.Bundle<"Virtual", {
269
311
  readonly _tag: "Measured";
270
312
  readonly key: string;
271
313
  readonly height: number;
272
- } | {
273
- readonly _tag: "Settled";
274
- readonly generation: number;
275
314
  } | {
276
315
  readonly _tag: "Scrolled";
277
316
  readonly top: number;
278
317
  } | {
279
318
  readonly _tag: "Prune";
280
319
  readonly keys: readonly string[];
320
+ } | {
321
+ readonly _tag: "Settled";
322
+ readonly generation: number;
281
323
  }, never, never, never, void, {}, {}>;
282
324
  export declare const ViewportScrolled: Schema.TaggedStruct<"ViewportScrolled", {
283
325
  readonly top: Schema.Number;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","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"],"mappings":";;;;qBASa,eAAgB,OAAO,OAAO,OAAO,MAAM,oBAAe,OAAA;;;;;YAM3D,aAAa;WACd,MAAM,cAAc;WACpB,SAAS;WACT,QAAQ,cAAc;;qBAGpB,gBAAiB,qBAAqB,OAAO;WAC/C,MAAM;WACN,OAAO,OAAO,MAAM;WACpB;MACV,OAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;qBA+DY,UAAW,OAAO,OAAO,aAAa;;qBAGtC,UAAW,OAAO,OAAO,aAAa;;;qBCjFtC,aAAW,OAAA;;;YACZ,qBAAqB,YAAY;qBAEhC,yCAAa;;qBAAb,OAAA;;;YACD,uBAAuB,cAAc;qBAEpC,QAAM,OAAA;;;;;;;;;;qBCPN,iBAAe,OAAA;;;;;YAKhB,yBAAyB,gBAAgB;qBAExC,6CAAiB;;;;;;;;;;;;;YAOlB,2BAA2B,kBAAkB;;qBAU5C,YAAa,OAAO;;qBAIpB,SAAU,OAAO;qBAEjB,YAAU,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBChCV,QAAS,eAAe,aAAa,kBAAW;;;qBCEhD,mBAAiB,OAAA;;;YAClB,2BAA2B,kBAAkB;qBAE5C,+CAAmB;;;;;;;;;;;;;;;YAOpB,6BAA6B,oBAAoB;;qBAMhD,aAAc,OAAO,mBAAmB;qBAGxC,cAAY,OAAA;;;;;;;;;;;;;;;;;;;iBCRR;;WAEN;;WAEA;;;iBAIM;WACN;WACA;WACA;WACA;;;qBAkBE,cACX,MAAM,uBACN,SAAS,SAAS,yBAClB,QAAQ;;qBAWG,YACX,eACA,MAAM,uBACN,SAAS,SAAS,yBAClB,QAAQ;;;;;;;;qBAiBG,eACX,MAAM,uBACN,SAAS,SAAS,yBAClB,QAAQ,eACR,mBACA,wBACA,qBACC;qBA4BU,cAAY,OAAA;;;;;;;;;;;YAWb,sBAAsB,aAAa;qBAElC,0CAAc;;;;;;;;;;;;;;;;;;;YAUf,wBAAwB,eAAe;;;;;;;qBAetC,YACX,OAAO,cACP,MAAM,uBACN,2BACC;WAA2B;;;;;;;qBAkBjB,gBACX,OAAO,cACP,MAAM,uBACN;;;;;;;qBAYW,UACX,OAAO,cACP,MAAM,uBACN,wBACA;qBAYW,SAAO,OAAA;WAKU;WACP;WACL;WACS;WACF;WACD;WACQ;WACF,iBAAA,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAmG1B,kBAAgB,OAAA;;;YACjB,0BAA0B,iBAAiB;;;;;;qBAO1C,UAAQ,MAAA;;;;qBAeR,aAAW,OAAA;;;;YAIZ,qBAAqB,YAAY;;;;;;qBAShC,YAAU,MAAA;;;;;;;;;;;;;;qBAgCV,eACX,UAAU;WAAyB;WAAsB;IACzD;iBAae;WACN;WACA;WACA;WACA;WACA;WACA;;;;;;;;;qBAUE,UACX,MAAM,uBACN,SAAS,SAAS,yBAClB;WACW;WACA;WACA;WACA;;WAEC,YAAY,cAAc;WAA4B"}
1
+ {"version":3,"file":"index.d.mts","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"],"mappings":";;;;qBAiBa,eAAgB,OAAO,OAAO,OAAO,MAAM,oBAAe,OAAA;;;;;;;YAQ3D,aAAa;WACd,MAAM,cAAc;WACpB,SAAS;WACT,QAAQ,cAAc;WACtB;;;qBA2FE,UAAW,OAAO,OAAO,aAAa;;qBAGtC,UAAW,OAAO,OAAO,aAAa;;qBAGtC;;EA9FE,QAAA,OAAK,SAAW,UAAQ,aAAa;;EAetC,OAAA,OAAK,OACV,aAAa,QAAM,OACnB,OAAK;aACQ;aAA2B;QAC9C,aAAa;;EAcF,OAAA,OAAK,OAAS,aAAa,WAAS,aAAa;;EAWjD,OAAA,OAAK,OAAS,aAAa,WAAS,aAAa;;;;;;EAWjD,OAAA,OAAK,OAAS,aAAa,QAAM,iBAAiB,aAAa;;EAY9D,QAAA,OAAK,OAAS,aAAa,WAAS,aAAa;;EAKjD,QAAA,OAAK,OAAS,aAAa,WAAS,aAAa;;;;;;;;;EAGhD,SAAA,OAAK,OAAS,aAAa,QAAM,kBAAkB,aAAa;EAaxD,UAAA,OAAK,OAAS,aAAa;EAG3B,UAAA,OAAK,OAAS,aAAa;;qBAmCtC,gBAAiB,qBAAqB,OAAO;WAC/C,MAAM;WACN,OAAO,OAAO,MAAM;WACpB;MACV,OAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCxJY,aAAW,OAAA;;;YACZ,qBAAqB,YAAY;qBAEhC,yCAAa;;qBAAb,OAAA;;;YACD,uBAAuB,cAAc;qBAEpC,QAAM,OAAA;;;;;;;;;;qBCNN,iBAAe,OAAA;;;;;YAKhB,yBAAyB,gBAAgB;qBAExC,6CAAiB;;;;;;;;;;;;;YAOlB,2BAA2B,kBAAkB;;qBAU5C,YAAa,OAAO;;qBAIpB,SAAU,OAAO;qBAEjB,YAAU,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCjCV,QAAS,eAAe,aAAa,kBAAW;;;qBCGhD,mBAAiB,OAAA;;;YAClB,2BAA2B,kBAAkB;qBAE5C,+CAAmB;;;;;;;;;;;;;;;YAOpB,6BAA6B,oBAAoB;;qBAMhD,aAAc,OAAO,mBAAmB;qBAGxC,cAAY,OAAA;;;;;;;;;;;;;;;;;;;iBCTR;;WAEN;;WAEA;;;iBAIM;WACN;WACA;WACA;WACA;;;qBAmEE,cACX,MAAM,uBACN,SAAS,SAAS,yBAClB,QAAQ;;qBAcG,YACX,eACA,MAAM,uBACN,SAAS,SAAS,yBAClB,QAAQ;;;;;;;;qBAUG,eACX,MAAM,uBACN,SAAS,SAAS,yBAClB,QAAQ,eACR,mBACA,wBACA,qBACC;qBAmBU,cAAY,OAAA;;;;;;;;;;;YAWb,sBAAsB,aAAa;qBAElC,0CAAc;;;;;;;;;;;;;;;;;;;YAUf,wBAAwB,eAAe;;;;;;;qBAetC,YACX,OAAO,cACP,MAAM,uBACN,2BACC;WAA2B;;;;;;;qBAkBjB,gBACX,OAAO,cACP,MAAM,uBACN;;;;;;;qBAYW,UACX,OAAO,cACP,MAAM,uBACN,wBACA;qBAYW,SAAO,OAAA;WAKU;WACP;WACL;WACS;WACF;WACD;WACQ;WACF,iBAAA,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAyG1B,kBAAgB,OAAA;;;YACjB,0BAA0B,iBAAiB;;;;;;qBAO1C,UAAQ,MAAA;;;;qBAeR,aAAW,OAAA;;;;YAIZ,qBAAqB,YAAY;;;;;;qBAShC,YAAU,MAAA;;;;;;;;;;;;;;qBAgCV,eACX,UAAU;WAAyB;WAAsB;IACzD;iBAae;WACN;WACA;WACA;WACA;WACA;WACA;;;;;;;;;qBAUE,UACX,MAAM,uBACN,SAAS,SAAS,yBAClB;WACW;WACA;WACA;WACA;;WAEC,YAAY,cAAc;WAA4B"}
@@ -1,3 +1,4 @@
1
+ import { i as unlessSame, r as sameIds } from "../internal-DP-xLKbT.mjs";
1
2
  import { Effect, Queue, Schema, Stream } from "effect";
2
3
  import * as Mount from "foldkit/mount";
3
4
  import { defineMessageUnion } from "foldkit/message";
@@ -7,67 +8,162 @@ import { Bundle } from "foldkit-bundle";
7
8
  * Undo/redo as a bundle factory: the past, present, and future of one value
8
9
  * live in the Model, so every step replays. Pushing records, even an unchanged
9
10
  * value; skipping duplicates is the application's policy, not the stack's.
11
+ *
12
+ * A push may name a `group`. Consecutive pushes of the same group are one step:
13
+ * the present moves and nothing new is recorded, so typing a word undoes as a
14
+ * whole. Grouping needs no clock; the application names what belongs together.
15
+ *
16
+ * The steps are also pure functions (`History.push`, `undo`, `redo`, `goTo`, `clear`),
17
+ * for a parent that records an edit in the same transition that makes it
18
+ * rather than dispatching a Message to a placed bundle.
10
19
  */
11
20
  const HistoryModel = (value) => Schema.Struct({
12
21
  past: Schema.Array(value),
13
22
  present: value,
14
- future: Schema.Array(value)
23
+ future: Schema.Array(value),
24
+ /** The group of the last push, so the next push of the same group joins it; `null` for none. */
25
+ group: Schema.NullOr(Schema.String)
26
+ });
27
+ const start = (present) => ({
28
+ past: [],
29
+ present,
30
+ future: [],
31
+ group: null
15
32
  });
33
+ /**
34
+ * No redo steps, keeping an already empty future: a view that reads the
35
+ * future compares it by identity, so a push per keystroke or stroke cell
36
+ * must not hand it a new empty array each time.
37
+ */
38
+ const noFuture = (model) => model.future.length === 0 ? model.future : [];
39
+ const push = (model, value, options) => {
40
+ const group = options.group ?? null;
41
+ if (group !== null && group === model.group && model.past.length > 0) return {
42
+ ...model,
43
+ present: value,
44
+ future: noFuture(model)
45
+ };
46
+ return {
47
+ past: options.capacity === 0 ? [] : [...model.past, model.present].slice(-options.capacity),
48
+ present: value,
49
+ future: noFuture(model),
50
+ group
51
+ };
52
+ };
53
+ const undo = (model) => {
54
+ const previous = model.past[model.past.length - 1];
55
+ if (previous === void 0) return model;
56
+ return {
57
+ past: model.past.slice(0, -1),
58
+ present: previous,
59
+ future: [model.present, ...model.future],
60
+ group: null
61
+ };
62
+ };
63
+ const redo = (model) => {
64
+ const next = model.future[0];
65
+ if (next === void 0) return model;
66
+ return {
67
+ past: [...model.past, model.present],
68
+ present: next,
69
+ future: model.future.slice(1),
70
+ group: null
71
+ };
72
+ };
73
+ const goTo = (model, step) => {
74
+ const steps = [
75
+ ...model.past,
76
+ model.present,
77
+ ...model.future
78
+ ];
79
+ if (!Number.isInteger(step) || step < 0 || step >= steps.length || step === model.past.length) return model;
80
+ return {
81
+ past: steps.slice(0, step),
82
+ present: steps[step],
83
+ future: steps.slice(step + 1),
84
+ group: null
85
+ };
86
+ };
87
+ const clear = (model) => model.past.length === 0 && model.future.length === 0 && model.group === null ? model : start(model.present);
88
+ const close = (model) => model.group === null ? model : {
89
+ ...model,
90
+ group: null
91
+ };
92
+ const revert = (model, group) => model.group === group ? {
93
+ ...undo(model),
94
+ future: [],
95
+ group: null
96
+ } : model;
97
+ const checkCapacity = (name, capacity) => {
98
+ if (!Number.isInteger(capacity) || capacity < 0) throw new Error(`History "${name}": capacity must be a non-negative integer`);
99
+ return capacity;
100
+ };
101
+ /** Whether Undo would move: the past is non-empty. */
102
+ const canUndo = (model) => model.past.length > 0;
103
+ /** Whether Redo would move: the future is non-empty. */
104
+ const canRedo = (model) => model.future.length > 0;
105
+ /** The steps of a history as pure functions of its Model. */
106
+ const History = {
107
+ /** A history whose present is `value`, with nothing to undo. */
108
+ start,
109
+ /** Records `value` as the present; the same `group` as the last push joins its step. */
110
+ push,
111
+ /** One step back, or the Model as it was when there is none. */
112
+ undo,
113
+ /** One step forward, or the Model as it was when there is none. */
114
+ redo,
115
+ /**
116
+ * The value at `step` made the present, counting every kept value from the
117
+ * oldest (0), so the present is at `past.length`: what a click on a step's
118
+ * thumbnail does. The Model as it was for the present or a step not kept.
119
+ */
120
+ goTo,
121
+ /** Nothing to undo or redo, the present kept. */
122
+ clear,
123
+ /** The next push starts a step of its own, whatever its group: a group ends here. */
124
+ close,
125
+ /**
126
+ * The step `group` is making taken back, and not redoable: what a cancelled
127
+ * edit, or one that ended where it began, leaves. The Model as it was when
128
+ * the last push was not of `group`. What the group's first push dropped
129
+ * stays dropped: the redo future it cleared, and an entry capacity evicted;
130
+ * with a capacity of 0 nothing was kept, so the present stays and only the
131
+ * group ends.
132
+ */
133
+ revert,
134
+ canUndo,
135
+ canRedo
136
+ };
16
137
  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`);
138
+ const capacity = checkCapacity(config.name, config.capacity ?? 100);
19
139
  const Model = HistoryModel(config.value);
20
140
  const Message = defineMessageUnion({
21
- Push: { value: config.value },
141
+ Push: {
142
+ value: config.value,
143
+ group: Schema.optional(Schema.String)
144
+ },
22
145
  Undo: {},
23
146
  Redo: {},
147
+ GoTo: { step: Schema.Number },
24
148
  Clear: {}
25
149
  });
26
150
  return Object.assign(Bundle.make(config.name, {
27
151
  Model,
28
152
  Message,
29
153
  args: Schema.Struct({ initial: config.value }),
30
- init: (args) => ({ model: {
31
- past: [],
32
- present: args.initial,
33
- future: []
34
- } }),
154
+ init: (args) => ({ model: start(args.initial) }),
35
155
  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
- } })
156
+ Push: ({ value, group }) => ({ model: push(model, value, {
157
+ capacity,
158
+ group
159
+ }) }),
160
+ Undo: () => ({ model: undo(model) }),
161
+ Redo: () => ({ model: redo(model) }),
162
+ GoTo: ({ step }) => ({ model: goTo(model, step) }),
163
+ Clear: () => ({ model: clear(model) })
64
164
  })
65
165
  }), { Message });
66
166
  };
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
167
  //#endregion
72
168
  //#region src/state/locale.ts
73
169
  /**
@@ -83,10 +179,10 @@ const Locale = Bundle.make("Locale", {
83
179
  Message: LocaleMessage,
84
180
  args: Schema.Struct({ default: Schema.String }),
85
181
  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: {
182
+ update: (model, message) => LocaleMessage.match(message, { SetLocale: ({ locale }) => locale === model.locale ? { model } : { model: {
87
183
  ...model,
88
184
  locale
89
- } }) })
185
+ } } })
90
186
  });
91
187
  //#endregion
92
188
  //#region src/state/pagination.ts
@@ -130,28 +226,28 @@ const Pagination = Bundle.make("Pagination", {
130
226
  total: args.total ?? null
131
227
  } }),
132
228
  update: (model, message) => PaginationMessage.match(message, {
133
- GoToPage: ({ page }) => ({ model: {
229
+ GoToPage: ({ page }) => ({ model: unlessSame(model, {
134
230
  ...model,
135
231
  page: clampPage(page, model.perPage, model.total)
136
- } }),
137
- NextPage: () => ({ model: {
232
+ }) }),
233
+ NextPage: () => ({ model: unlessSame(model, {
138
234
  ...model,
139
235
  page: clampPage(model.page + 1, model.perPage, model.total)
140
- } }),
141
- PrevPage: () => ({ model: {
236
+ }) }),
237
+ PrevPage: () => ({ model: unlessSame(model, {
142
238
  ...model,
143
239
  page: clampPage(model.page - 1, model.perPage, model.total)
144
- } }),
145
- SetPerPage: ({ perPage }) => perPage > 0 ? { model: {
240
+ }) }),
241
+ SetPerPage: ({ perPage }) => perPage > 0 ? { model: unlessSame(model, {
146
242
  ...model,
147
243
  perPage,
148
244
  page: clampPage(model.page, perPage, model.total)
149
- } } : { model },
150
- SetTotal: ({ total }) => total === null || total >= 0 ? { model: {
245
+ }) } : { model },
246
+ SetTotal: ({ total }) => total === null || total >= 0 ? { model: unlessSame(model, {
151
247
  ...model,
152
248
  total,
153
249
  page: clampPage(model.page, model.perPage, total)
154
- } } : { model }
250
+ }) } : { model }
155
251
  })
156
252
  });
157
253
  //#endregion
@@ -194,10 +290,13 @@ const SelectionSet = Bundle.make("SelectionSet", {
194
290
  init: () => ({ model: { selected: [] } }),
195
291
  update: (model, message) => SelectionSetMessage.match(message, {
196
292
  Select: ({ id }) => isSelected(model, id) ? { model } : { model: { selected: [...model.selected, id] } },
197
- Deselect: ({ id }) => ({ model: { selected: without(model.selected, id) } }),
293
+ Deselect: ({ id }) => isSelected(model, id) ? { model: { selected: without(model.selected, id) } } : { model },
198
294
  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: [] } })
295
+ ReplaceAll: ({ ids }) => {
296
+ const selected = [...new Set(ids)];
297
+ return sameIds(selected, model.selected) ? { model } : { model: { selected } };
298
+ },
299
+ Clear: () => model.selected.length === 0 ? { model } : { model: { selected: [] } }
201
300
  })
202
301
  });
203
302
  //#endregion
@@ -222,22 +321,40 @@ const heightAt = (index, heights, keys, layout) => {
222
321
  if (key === void 0) return layout.estimatedHeight;
223
322
  return saneHeight(heights[key] ?? layout.estimatedHeight, layout.estimatedHeight);
224
323
  };
324
+ const offsetsCache = /* @__PURE__ */ new WeakMap();
325
+ const sameLayout = (a, b) => a.estimatedHeight === b.estimatedHeight && a.gap === b.gap && a.paddingStart === b.paddingStart && a.paddingEnd === b.paddingEnd;
326
+ const topsFor = (keys, heights, layout) => {
327
+ const cached = offsetsCache.get(keys);
328
+ if (cached !== void 0 && cached.heights === heights && sameLayout(cached.layout, layout)) return cached.tops;
329
+ const tops = new Float64Array(keys.length + 1);
330
+ tops[0] = layout.paddingStart;
331
+ for (let index = 0; index < keys.length; index++) tops[index + 1] = tops[index] + heightAt(index, heights, keys, layout) + layout.gap;
332
+ offsetsCache.set(keys, {
333
+ heights,
334
+ layout: { ...layout },
335
+ tops
336
+ });
337
+ return tops;
338
+ };
339
+ /** The first index in `[from, to)` whose value passes `test`, or `to`; `test` must be monotonic. */
340
+ const firstIndex = (from, to, test) => {
341
+ let low = from;
342
+ let high = to;
343
+ while (low < high) {
344
+ const middle = low + high >>> 1;
345
+ if (test(middle)) high = middle;
346
+ else low = middle + 1;
347
+ }
348
+ return low;
349
+ };
225
350
  /** Total scrollable height: padding, measured rows, estimates, and gaps. */
226
351
  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;
352
+ const last = keys.length - 1;
353
+ if (last === -1) return layout.paddingStart + layout.paddingEnd;
354
+ return topsFor(keys, heights, layout)[last] + heightAt(last, heights, keys, layout) + layout.paddingEnd;
233
355
  };
234
356
  /** 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
- };
357
+ const offsetFor = (index, keys, heights, layout) => topsFor(keys, heights, layout)[Math.max(0, Math.min(index, keys.length))];
241
358
  /**
242
359
  * Which rows to render for a scroll position: the rows intersecting
243
360
  * `[scrollTop, scrollTop + viewportHeight)`, widened by `overscan` on both
@@ -251,23 +368,11 @@ const visibleRange = (keys, heights, layout, scrollTop, viewportHeight, overscan
251
368
  start: 0,
252
369
  end: 0
253
370
  };
371
+ const tops = topsFor(keys, heights, layout);
254
372
  const top = Math.max(0, scrollTop);
255
373
  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
- }
374
+ const start = firstIndex(0, count, (index) => tops[index] + heightAt(index, heights, keys, layout) > top);
375
+ const end = firstIndex(start, count, (index) => tops[index] >= bottom);
271
376
  return {
272
377
  start: Math.max(0, start - Math.max(0, overscan)),
273
378
  end: Math.min(count, end + Math.max(0, overscan))
@@ -331,8 +436,9 @@ const distanceToEnd = (model, keys, viewportHeight) => {
331
436
  * to zero.
332
437
  */
333
438
  const isAtEnd = (model, keys, viewportHeight, threshold = 0) => {
334
- if (totalHeight(keys, model.heights, layoutOf(model)) <= 0) return true;
335
- return distanceToEnd(model, keys, viewportHeight) <= Math.max(0, threshold);
439
+ const total = totalHeight(keys, model.heights, layoutOf(model));
440
+ if (total <= 0) return true;
441
+ return total - (model.scrollTop + viewportHeight) <= Math.max(0, threshold);
336
442
  };
337
443
  const saneTop = (top) => Number.isFinite(top) ? Math.max(0, top) : null;
338
444
  const saneMeasured = (height) => Number.isFinite(height) && height >= 0 ? height : null;
@@ -370,7 +476,7 @@ const Virtual = Bundle.make("Virtual", {
370
476
  update: (model, message, args) => VirtualMessage.match(message, {
371
477
  Scrolled: ({ top }) => {
372
478
  const sane = saneTop(top);
373
- if (sane === null) return { model };
479
+ if (sane === null || top === model.scrollTop) return { model };
374
480
  const generation = model.generation + 1;
375
481
  return {
376
482
  model: {
@@ -388,7 +494,7 @@ const Virtual = Bundle.make("Virtual", {
388
494
  },
389
495
  Measured: ({ key, height }) => {
390
496
  const sane = saneMeasured(height);
391
- return sane === null ? { model } : { model: {
497
+ return sane === null || Object.hasOwn(model.heights, key) && model.heights[key] === sane ? { model } : { model: {
392
498
  ...model,
393
499
  heights: {
394
500
  ...model.heights,
@@ -399,11 +505,13 @@ const Virtual = Bundle.make("Virtual", {
399
505
  Prune: ({ keys }) => {
400
506
  const kept = new Set(keys);
401
507
  const heights = {};
508
+ let dropped = false;
402
509
  for (const [key, height] of Object.entries(model.heights)) if (kept.has(key)) heights[key] = height;
403
- return { model: {
510
+ else dropped = true;
511
+ return dropped ? { model: {
404
512
  ...model,
405
513
  heights
406
- } };
514
+ } } : { model };
407
515
  },
408
516
  Settled: ({ generation }) => generation === model.generation ? { model: {
409
517
  ...model,
@@ -506,6 +614,6 @@ const masonry = (keys, heights, options) => {
506
614
  };
507
615
  };
508
616
  //#endregion
509
- export { HistoryModel, Locale, LocaleMessage, LocaleModel, MeasureRow, Pagination, PaginationMessage, PaginationModel, RowMeasured, SelectionSet, SelectionSetMessage, SelectionSetModel, Viewport, ViewportScrolled, Virtual, VirtualMessage, VirtualModel, canRedo, canUndo, distanceToEnd, history, isAtEnd, isSelected, masonry, offset, offsetFor, pageCount, range, stickyHeader, totalHeight, visibleRange, windowFor };
617
+ export { History, HistoryModel, Locale, LocaleMessage, LocaleModel, MeasureRow, Pagination, PaginationMessage, PaginationModel, RowMeasured, SelectionSet, SelectionSetMessage, SelectionSetModel, Viewport, ViewportScrolled, Virtual, VirtualMessage, VirtualModel, canRedo, canUndo, distanceToEnd, history, isAtEnd, isSelected, masonry, offset, offsetFor, pageCount, range, stickyHeader, totalHeight, visibleRange, windowFor };
510
618
 
511
619
  //# sourceMappingURL=index.mjs.map