@foldkit/ui 0.112.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 (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/anchor.d.ts +38 -0
  4. package/dist/anchor.d.ts.map +1 -0
  5. package/dist/anchor.js +142 -0
  6. package/dist/animation/index.d.ts +49 -0
  7. package/dist/animation/index.d.ts.map +1 -0
  8. package/dist/animation/index.js +75 -0
  9. package/dist/animation/public.d.ts +3 -0
  10. package/dist/animation/public.d.ts.map +1 -0
  11. package/dist/animation/public.js +1 -0
  12. package/dist/animation/schema.d.ts +43 -0
  13. package/dist/animation/schema.d.ts.map +1 -0
  14. package/dist/animation/schema.js +41 -0
  15. package/dist/animation/update.d.ts +24 -0
  16. package/dist/animation/update.d.ts.map +1 -0
  17. package/dist/animation/update.js +67 -0
  18. package/dist/button/index.d.ts +17 -0
  19. package/dist/button/index.d.ts.map +1 -0
  20. package/dist/button/index.js +22 -0
  21. package/dist/button/public.d.ts +3 -0
  22. package/dist/button/public.d.ts.map +1 -0
  23. package/dist/button/public.js +1 -0
  24. package/dist/calendar/index.d.ts +462 -0
  25. package/dist/calendar/index.d.ts.map +1 -0
  26. package/dist/calendar/index.js +825 -0
  27. package/dist/calendar/public.d.ts +3 -0
  28. package/dist/calendar/public.d.ts.map +1 -0
  29. package/dist/calendar/public.js +1 -0
  30. package/dist/checkbox/index.d.ts +119 -0
  31. package/dist/checkbox/index.d.ts.map +1 -0
  32. package/dist/checkbox/index.js +111 -0
  33. package/dist/checkbox/public.d.ts +3 -0
  34. package/dist/checkbox/public.d.ts.map +1 -0
  35. package/dist/checkbox/public.js +1 -0
  36. package/dist/combobox/multi.d.ts +183 -0
  37. package/dist/combobox/multi.d.ts.map +1 -0
  38. package/dist/combobox/multi.js +81 -0
  39. package/dist/combobox/multiPublic.d.ts +3 -0
  40. package/dist/combobox/multiPublic.d.ts.map +1 -0
  41. package/dist/combobox/multiPublic.js +1 -0
  42. package/dist/combobox/public.d.ts +7 -0
  43. package/dist/combobox/public.d.ts.map +1 -0
  44. package/dist/combobox/public.js +3 -0
  45. package/dist/combobox/shared.d.ts +423 -0
  46. package/dist/combobox/shared.d.ts.map +1 -0
  47. package/dist/combobox/shared.js +708 -0
  48. package/dist/combobox/single.d.ts +198 -0
  49. package/dist/combobox/single.d.ts.map +1 -0
  50. package/dist/combobox/single.js +106 -0
  51. package/dist/datePicker/index.d.ts +457 -0
  52. package/dist/datePicker/index.d.ts.map +1 -0
  53. package/dist/datePicker/index.js +318 -0
  54. package/dist/datePicker/public.d.ts +3 -0
  55. package/dist/datePicker/public.d.ts.map +1 -0
  56. package/dist/datePicker/public.js +1 -0
  57. package/dist/dialog/index.d.ts +160 -0
  58. package/dist/dialog/index.d.ts.map +1 -0
  59. package/dist/dialog/index.js +211 -0
  60. package/dist/dialog/public.d.ts +3 -0
  61. package/dist/dialog/public.d.ts.map +1 -0
  62. package/dist/dialog/public.js +1 -0
  63. package/dist/disclosure/index.d.ts +110 -0
  64. package/dist/disclosure/index.d.ts.map +1 -0
  65. package/dist/disclosure/index.js +111 -0
  66. package/dist/disclosure/public.d.ts +3 -0
  67. package/dist/disclosure/public.d.ts.map +1 -0
  68. package/dist/disclosure/public.js +1 -0
  69. package/dist/dragAndDrop/index.d.ts +540 -0
  70. package/dist/dragAndDrop/index.d.ts.map +1 -0
  71. package/dist/dragAndDrop/index.js +535 -0
  72. package/dist/dragAndDrop/public.d.ts +3 -0
  73. package/dist/dragAndDrop/public.d.ts.map +1 -0
  74. package/dist/dragAndDrop/public.js +1 -0
  75. package/dist/fieldset/index.d.ts +21 -0
  76. package/dist/fieldset/index.d.ts.map +1 -0
  77. package/dist/fieldset/index.js +25 -0
  78. package/dist/fieldset/public.d.ts +3 -0
  79. package/dist/fieldset/public.d.ts.map +1 -0
  80. package/dist/fieldset/public.js +1 -0
  81. package/dist/fileDrop/index.d.ts +109 -0
  82. package/dist/fileDrop/index.d.ts.map +1 -0
  83. package/dist/fileDrop/index.js +127 -0
  84. package/dist/fileDrop/public.d.ts +3 -0
  85. package/dist/fileDrop/public.d.ts.map +1 -0
  86. package/dist/fileDrop/public.js +1 -0
  87. package/dist/group.d.ts +8 -0
  88. package/dist/group.d.ts.map +1 -0
  89. package/dist/group.js +13 -0
  90. package/dist/index.d.ts +25 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +24 -0
  93. package/dist/input/index.d.ts +26 -0
  94. package/dist/input/index.d.ts.map +1 -0
  95. package/dist/input/index.js +43 -0
  96. package/dist/input/public.d.ts +3 -0
  97. package/dist/input/public.d.ts.map +1 -0
  98. package/dist/input/public.js +1 -0
  99. package/dist/internal/optionExtensions.d.ts +6 -0
  100. package/dist/internal/optionExtensions.d.ts.map +1 -0
  101. package/dist/internal/optionExtensions.js +2 -0
  102. package/dist/keyboard.d.ts +6 -0
  103. package/dist/keyboard.d.ts.map +1 -0
  104. package/dist/keyboard.js +9 -0
  105. package/dist/listbox/multi.d.ts +189 -0
  106. package/dist/listbox/multi.d.ts.map +1 -0
  107. package/dist/listbox/multi.js +65 -0
  108. package/dist/listbox/multiPublic.d.ts +3 -0
  109. package/dist/listbox/multiPublic.d.ts.map +1 -0
  110. package/dist/listbox/multiPublic.js +1 -0
  111. package/dist/listbox/public.d.ts +7 -0
  112. package/dist/listbox/public.d.ts.map +1 -0
  113. package/dist/listbox/public.js +3 -0
  114. package/dist/listbox/shared.d.ts +432 -0
  115. package/dist/listbox/shared.d.ts.map +1 -0
  116. package/dist/listbox/shared.js +670 -0
  117. package/dist/listbox/single.d.ts +207 -0
  118. package/dist/listbox/single.d.ts.map +1 -0
  119. package/dist/listbox/single.js +73 -0
  120. package/dist/menu/index.d.ts +368 -0
  121. package/dist/menu/index.d.ts.map +1 -0
  122. package/dist/menu/index.js +682 -0
  123. package/dist/menu/public.d.ts +4 -0
  124. package/dist/menu/public.d.ts.map +1 -0
  125. package/dist/menu/public.js +1 -0
  126. package/dist/popover/index.d.ts +267 -0
  127. package/dist/popover/index.d.ts.map +1 -0
  128. package/dist/popover/index.js +346 -0
  129. package/dist/popover/public.d.ts +4 -0
  130. package/dist/popover/public.d.ts.map +1 -0
  131. package/dist/popover/public.js +1 -0
  132. package/dist/radioGroup/index.d.ts +169 -0
  133. package/dist/radioGroup/index.d.ts.map +1 -0
  134. package/dist/radioGroup/index.js +197 -0
  135. package/dist/radioGroup/public.d.ts +3 -0
  136. package/dist/radioGroup/public.d.ts.map +1 -0
  137. package/dist/radioGroup/public.js +1 -0
  138. package/dist/select/index.d.ts +24 -0
  139. package/dist/select/index.d.ts.map +1 -0
  140. package/dist/select/index.js +40 -0
  141. package/dist/select/public.d.ts +3 -0
  142. package/dist/select/public.d.ts.map +1 -0
  143. package/dist/select/public.js +1 -0
  144. package/dist/slider/index.d.ts +318 -0
  145. package/dist/slider/index.d.ts.map +1 -0
  146. package/dist/slider/index.js +337 -0
  147. package/dist/slider/public.d.ts +3 -0
  148. package/dist/slider/public.d.ts.map +1 -0
  149. package/dist/slider/public.js +1 -0
  150. package/dist/switch/index.d.ts +99 -0
  151. package/dist/switch/index.d.ts.map +1 -0
  152. package/dist/switch/index.js +107 -0
  153. package/dist/switch/public.d.ts +3 -0
  154. package/dist/switch/public.d.ts.map +1 -0
  155. package/dist/switch/public.js +1 -0
  156. package/dist/tabs/index.d.ts +155 -0
  157. package/dist/tabs/index.d.ts.map +1 -0
  158. package/dist/tabs/index.js +185 -0
  159. package/dist/tabs/public.d.ts +3 -0
  160. package/dist/tabs/public.d.ts.map +1 -0
  161. package/dist/tabs/public.js +1 -0
  162. package/dist/test/apps/disabledButton.d.ts +38 -0
  163. package/dist/test/apps/disabledButton.d.ts.map +1 -0
  164. package/dist/test/apps/disabledButton.js +71 -0
  165. package/dist/textarea/index.d.ts +26 -0
  166. package/dist/textarea/index.d.ts.map +1 -0
  167. package/dist/textarea/index.js +44 -0
  168. package/dist/textarea/public.d.ts +3 -0
  169. package/dist/textarea/public.d.ts.map +1 -0
  170. package/dist/textarea/public.js +1 -0
  171. package/dist/toast/index.d.ts +608 -0
  172. package/dist/toast/index.d.ts.map +1 -0
  173. package/dist/toast/index.js +146 -0
  174. package/dist/toast/public.d.ts +4 -0
  175. package/dist/toast/public.d.ts.map +1 -0
  176. package/dist/toast/public.js +1 -0
  177. package/dist/toast/schema.d.ts +154 -0
  178. package/dist/toast/schema.d.ts.map +1 -0
  179. package/dist/toast/schema.js +93 -0
  180. package/dist/toast/update.d.ts +510 -0
  181. package/dist/toast/update.d.ts.map +1 -0
  182. package/dist/toast/update.js +225 -0
  183. package/dist/tooltip/index.d.ts +170 -0
  184. package/dist/tooltip/index.d.ts.map +1 -0
  185. package/dist/tooltip/index.js +253 -0
  186. package/dist/tooltip/public.d.ts +4 -0
  187. package/dist/tooltip/public.d.ts.map +1 -0
  188. package/dist/tooltip/public.js +1 -0
  189. package/dist/typeahead.d.ts +4 -0
  190. package/dist/typeahead.d.ts.map +1 -0
  191. package/dist/typeahead.js +14 -0
  192. package/dist/virtualList/index.d.ts +203 -0
  193. package/dist/virtualList/index.d.ts.map +1 -0
  194. package/dist/virtualList/index.js +392 -0
  195. package/dist/virtualList/public.d.ts +3 -0
  196. package/dist/virtualList/public.d.ts.map +1 -0
  197. package/dist/virtualList/public.js +1 -0
  198. package/dist/vitest-setup.d.ts +2 -0
  199. package/dist/vitest-setup.d.ts.map +1 -0
  200. package/dist/vitest-setup.js +2 -0
  201. package/package.json +161 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typeahead.d.ts","sourceRoot":"","sources":["../src/typeahead.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAuB,MAAM,QAAQ,CAAA;AAI3D,oOAAoO;AACpO,eAAO,MAAM,qBAAqB,GAAI,IAAI,EACxC,OAAO,aAAa,CAAC,IAAI,CAAC,EAC1B,OAAO,MAAM,EACb,sBAAsB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAC3C,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,EACtC,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,EACvD,cAAc,OAAO,KACpB,MAAM,CAAC,MAAM,CAAC,MAAM,CA4BtB,CAAA"}
@@ -0,0 +1,14 @@
1
+ import { Array, Option, String as Str, pipe } from 'effect';
2
+ import { wrapIndex } from './keyboard.js';
3
+ /** Finds the first enabled item whose search text starts with the query, searching forward from the active item and wrapping around. On a fresh search, starts after the active item; on a refinement, includes the active item. */
4
+ export const resolveTypeaheadMatch = (items, query, maybeActiveItemIndex, isDisabled, itemToSearchText, isRefinement) => {
5
+ const lowerQuery = Str.toLowerCase(query);
6
+ const offset = isRefinement ? 0 : 1;
7
+ const startIndex = Option.match(maybeActiveItemIndex, {
8
+ onNone: () => 0,
9
+ onSome: index => index + offset,
10
+ });
11
+ const isEnabledMatch = (index) => !isDisabled(index) &&
12
+ pipe(items, Array.get(index), Option.exists(item => pipe(itemToSearchText(item, index), Str.toLowerCase, Str.startsWith(lowerQuery))));
13
+ return pipe(items, Array.length, Array.makeBy(step => wrapIndex(startIndex + step, items.length)), Array.findFirst(isEnabledMatch));
14
+ };
@@ -0,0 +1,203 @@
1
+ import { Effect, Option, Schema as S } from 'effect';
2
+ import * as Command from 'foldkit/command';
3
+ import { type ChildAttribute, type Html, type TagName } from 'foldkit/html';
4
+ import { type View as SubmodelView } from 'foldkit/submodel';
5
+ import * as Subscription from 'foldkit/subscription';
6
+ /** Schema for the virtual list's state. Tracks scroll position, container
7
+ * measurement, and any in-flight programmatic scroll. */
8
+ export declare const Model: S.Struct<{
9
+ readonly id: S.String;
10
+ readonly rowHeightPx: S.Number;
11
+ readonly scrollTop: S.Number;
12
+ readonly measurement: S.Union<readonly [import("foldkit/schema").CallableTaggedStruct<"Unmeasured", {}>, import("foldkit/schema").CallableTaggedStruct<"Measured", {
13
+ containerHeight: S.Number;
14
+ }>]>;
15
+ readonly pendingScroll: S.Union<readonly [import("foldkit/schema").CallableTaggedStruct<"Idle", {}>, import("foldkit/schema").CallableTaggedStruct<"ScrollingToIndex", {
16
+ index: S.Number;
17
+ version: S.Number;
18
+ }>]>;
19
+ readonly pendingScrollVersion: S.Number;
20
+ }>;
21
+ export type Model = typeof Model.Type;
22
+ /** Sent when the user scrolls the container. Carries the new scroll position
23
+ * read from the scroll event. */
24
+ export declare const ScrolledContainer: import("foldkit/schema").CallableTaggedStruct<"ScrolledContainer", {
25
+ scrollTop: S.Number;
26
+ }>;
27
+ /** Sent when the container resizes. Carries the new container height read
28
+ * from the `ResizeObserver` entry. */
29
+ export declare const MeasuredContainer: import("foldkit/schema").CallableTaggedStruct<"MeasuredContainer", {
30
+ containerHeight: S.Number;
31
+ }>;
32
+ /** Sent when a `scrollToIndex` Command completes. Carries the version it was
33
+ * issued with so the update can ignore stale completions. */
34
+ export declare const CompletedApplyScroll: import("foldkit/schema").CallableTaggedStruct<"CompletedApplyScroll", {
35
+ version: S.Number;
36
+ }>;
37
+ /** Union of all messages the virtual list component can produce. */
38
+ export declare const Message: S.Union<[
39
+ typeof ScrolledContainer,
40
+ typeof MeasuredContainer,
41
+ typeof CompletedApplyScroll
42
+ ]>;
43
+ export type ScrolledContainer = typeof ScrolledContainer.Type;
44
+ export type MeasuredContainer = typeof MeasuredContainer.Type;
45
+ export type Message = typeof Message.Type;
46
+ /** Configuration for creating a virtual list model with `init`. */
47
+ export type InitConfig = Readonly<{
48
+ id: string;
49
+ rowHeightPx: number;
50
+ initialScrollTop?: number;
51
+ }>;
52
+ /** Creates an initial virtual list model from a config. The container starts
53
+ * in `Unmeasured` state. The first `ResizeObserver` entry transitions it to
54
+ * `Measured`. */
55
+ export declare const init: (config: InitConfig) => Model;
56
+ export declare const ApplyScroll: Command.CommandDefinitionWithArgs<"ApplyScroll", {
57
+ id: S.String;
58
+ scrollTop: S.Number;
59
+ version: S.Number;
60
+ }, Effect.Effect<{
61
+ readonly _tag: "CompletedApplyScroll";
62
+ readonly version: number;
63
+ }, never, never>>;
64
+ /** Processes a virtual list message and returns the next model and commands. */
65
+ export declare const update: (model: Model, message: Message) => readonly [Model, ReadonlyArray<Command.Command<Message>>];
66
+ /** Programmatically scrolls the container so the row at `index` is visible.
67
+ * Returns the next model and a Command that mutates `element.scrollTop`. The
68
+ * natural scroll event then flows back through `ScrolledContainer` and the
69
+ * component re-renders the new visible slice.
70
+ *
71
+ * Uses version-based cancellation: each call increments
72
+ * `pendingScrollVersion` so a stale `CompletedApplyScroll` (e.g. from a
73
+ * previous in-flight scroll) is ignored when its version no longer matches.
74
+ *
75
+ * Should be called after the container has rendered. If the container is not
76
+ * yet in the DOM the Command silently no-ops (the model still transitions
77
+ * through `ScrollingToIndex` → `Idle` via the version-matched completion).
78
+ *
79
+ * Assumes uniform row heights: target scroll position is computed as
80
+ * `index * model.rowHeightPx`. For variable-height rows, use
81
+ * `scrollToIndexVariable`. */
82
+ export declare const scrollToIndex: (model: Model, index: number) => readonly [Model, ReadonlyArray<Command.Command<Message>>];
83
+ /** Variable-height counterpart of `scrollToIndex`. Walks the heights of items
84
+ * before `index` to compute the target `scrollTop`. Use this when rendering
85
+ * the list with `itemToRowHeightPx`; use `scrollToIndex` for uniform heights.
86
+ *
87
+ * Out-of-range indices clamp to the corresponding edge: negative or zero
88
+ * scrolls to the top, indices past the end scroll past the last row.
89
+ *
90
+ * Note: when restoring `initialScrollTop` on the first measurement of a
91
+ * variable-height list, the runtime falls back to uniform-height math (using
92
+ * `model.rowHeightPx`) because items aren't reachable from the `update`
93
+ * function. Consumers who need an accurate initial scroll on a
94
+ * variable-height list should call `scrollToIndexVariable` after the first
95
+ * `MeasuredContainer` arrives. */
96
+ export declare const scrollToIndexVariable: <Item>(model: Model, items: ReadonlyArray<Item>, itemToRowHeightPx: (item: Item, index: number) => number, index: number) => readonly [Model, ReadonlyArray<Command.Command<Message>>];
97
+ /** Slice of the data array that the view should render, plus the spacer
98
+ * heights that keep the scrollbar physically correct. The first row in the
99
+ * slice corresponds to data index `startIndex`. */
100
+ export type VisibleWindow = Readonly<{
101
+ startIndex: number;
102
+ endIndex: number;
103
+ topSpacerHeight: number;
104
+ bottomSpacerHeight: number;
105
+ }>;
106
+ /** Computes the visible slice of a data array given the current scroll
107
+ * position, container height, row height, and an overscan buffer.
108
+ *
109
+ * Assumes uniform row heights via `model.rowHeightPx`. For variable-height
110
+ * rows, use `visibleWindowVariable`.
111
+ *
112
+ * Returns `Option.none()` when the container has not yet been measured;
113
+ * callers should render a placeholder (or `Html.empty`) and wait for the
114
+ * first `MeasuredContainer` message. */
115
+ export declare const visibleWindow: (model: Model, itemCount: number, overscan: number) => Option.Option<VisibleWindow>;
116
+ /** Variable-height counterpart of `visibleWindow`. Walks the heights of every
117
+ * item to build a prefix-sum array, then locates the visible slice with two
118
+ * linear searches.
119
+ *
120
+ * Cost is O(N) per call, walking the whole `items` array once to build the
121
+ * prefix sums. For lists in the 10k-item range, this comfortably fits inside
122
+ * a 60Hz scroll budget. Larger lists or hotter scroll paths can layer a
123
+ * prefix-sum cache invalidated when items change; that lives behind the same
124
+ * return shape so consumers don't have to know.
125
+ *
126
+ * Returns `Option.none()` when the container has not yet been measured. */
127
+ export declare const visibleWindowVariable: <Item>(model: Model, items: ReadonlyArray<Item>, itemToRowHeightPx: (item: Item, index: number) => number, overscan: number) => Option.Option<VisibleWindow>;
128
+ /** Subscriptions that track the container's scroll position and size.
129
+ *
130
+ * - **scroll**: listens for `scroll` events on the container element and
131
+ * emits `ScrolledContainer` with the new `scrollTop`.
132
+ * - **resize**: observes the container with `ResizeObserver` and emits
133
+ * `MeasuredContainer` with the new height.
134
+ *
135
+ * A `MutationObserver` watches the document for the container element
136
+ * appearing and disappearing, so the listeners attach the moment the
137
+ * element is inserted into the DOM and clean up when it is removed. This
138
+ * makes the subscription robust across SPA route changes: navigating to a
139
+ * page that mounts the list, away, and back all reattach correctly without
140
+ * the consumer having to teach the framework about navigation. */
141
+ export declare const subscriptions: {
142
+ readonly containerEvents: Subscription.EntryWithoutKeepAlive<{
143
+ readonly id: string;
144
+ readonly rowHeightPx: number;
145
+ readonly scrollTop: number;
146
+ readonly measurement: {
147
+ readonly _tag: "Unmeasured";
148
+ } | {
149
+ readonly _tag: "Measured";
150
+ readonly containerHeight: number;
151
+ };
152
+ readonly pendingScroll: {
153
+ readonly _tag: "Idle";
154
+ } | {
155
+ readonly _tag: "ScrollingToIndex";
156
+ readonly index: number;
157
+ readonly version: number;
158
+ };
159
+ readonly pendingScrollVersion: number;
160
+ }, {
161
+ readonly _tag: "ScrolledContainer";
162
+ readonly scrollTop: number;
163
+ } | {
164
+ readonly _tag: "MeasuredContainer";
165
+ readonly containerHeight: number;
166
+ } | {
167
+ readonly _tag: "CompletedApplyScroll";
168
+ readonly version: number;
169
+ }, {
170
+ readonly id: string;
171
+ }, never> & {
172
+ readonly __subscription: never;
173
+ };
174
+ };
175
+ /** Per-render view inputs passed to `view` via `h.submodel`'s `viewInputs` field.
176
+ *
177
+ * VirtualList does not surface event handlers in the view. All input
178
+ * (scroll events and resize observations) flows through the
179
+ * `containerEvents` Subscription. The consumer wraps that
180
+ * Subscription's stream into their parent Message in their own
181
+ * `subscriptions` definition. */
182
+ export type ViewInputs<Item> = Readonly<{
183
+ items: ReadonlyArray<Item>;
184
+ itemToKey: (item: Item, index: number) => string;
185
+ itemToView: (item: Item, index: number) => Html;
186
+ itemToRowHeightPx?: (item: Item, index: number) => number;
187
+ overscan?: number;
188
+ rowElement?: TagName;
189
+ containerClassName?: string;
190
+ containerAttributes?: ReadonlyArray<ChildAttribute>;
191
+ }>;
192
+ /** Renders a virtualized list. Only items inside the viewport (plus an
193
+ * overscan buffer) are mounted; spacer elements above and below the
194
+ * slice keep the scrollbar's apparent total height correct.
195
+ *
196
+ * Generic over `Item`: call as `VirtualList.view<MyItem>()` at the
197
+ * embed site to get a `SubmodelView` typed for your item type. The
198
+ * underlying view implementation is shared; the call only narrows the
199
+ * type. */
200
+ type ViewForItem<Item> = SubmodelView<Model, Message, ViewInputs<Item>>;
201
+ export declare const view: <Item>() => ViewForItem<Item>;
202
+ export {};
203
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/virtualList/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,392 @@
1
+ import { Array, Effect, Match as M, Number, Option, Queue, Schema as S, Stream, pipe, } from 'effect';
2
+ import * as Command from 'foldkit/command';
3
+ import { childAttributes, html, } from 'foldkit/html';
4
+ import { m } from 'foldkit/message';
5
+ import { ts } from 'foldkit/schema';
6
+ import { evo } from 'foldkit/struct';
7
+ import { defineView } from 'foldkit/submodel';
8
+ import * as Subscription from 'foldkit/subscription';
9
+ // MODEL
10
+ const Unmeasured = ts('Unmeasured');
11
+ const Measured = ts('Measured', { containerHeight: S.Number });
12
+ /** Measurement state of the virtual list's scrollable container.
13
+ *
14
+ * Before the container's `ResizeObserver` fires for the first time we don't
15
+ * know its height and cannot compute a visible slice. The view must handle
16
+ * `Unmeasured` explicitly, typically by rendering a placeholder until the
17
+ * first measurement arrives.
18
+ */
19
+ const Measurement = S.Union([Unmeasured, Measured]);
20
+ const Idle = ts('Idle');
21
+ const ScrollingToIndex = ts('ScrollingToIndex', {
22
+ index: S.Number,
23
+ version: S.Number,
24
+ });
25
+ /** State of a programmatic scroll initiated by `scrollToIndex`. */
26
+ const PendingScroll = S.Union([Idle, ScrollingToIndex]);
27
+ /** Schema for the virtual list's state. Tracks scroll position, container
28
+ * measurement, and any in-flight programmatic scroll. */
29
+ export const Model = S.Struct({
30
+ id: S.String,
31
+ rowHeightPx: S.Number,
32
+ scrollTop: S.Number,
33
+ measurement: Measurement,
34
+ pendingScroll: PendingScroll,
35
+ pendingScrollVersion: S.Number,
36
+ });
37
+ // MESSAGE
38
+ /** Sent when the user scrolls the container. Carries the new scroll position
39
+ * read from the scroll event. */
40
+ export const ScrolledContainer = m('ScrolledContainer', {
41
+ scrollTop: S.Number,
42
+ });
43
+ /** Sent when the container resizes. Carries the new container height read
44
+ * from the `ResizeObserver` entry. */
45
+ export const MeasuredContainer = m('MeasuredContainer', {
46
+ containerHeight: S.Number,
47
+ });
48
+ /** Sent when a `scrollToIndex` Command completes. Carries the version it was
49
+ * issued with so the update can ignore stale completions. */
50
+ export const CompletedApplyScroll = m('CompletedApplyScroll', {
51
+ version: S.Number,
52
+ });
53
+ /** Union of all messages the virtual list component can produce. */
54
+ export const Message = S.Union([ScrolledContainer, MeasuredContainer, CompletedApplyScroll]);
55
+ /** Creates an initial virtual list model from a config. The container starts
56
+ * in `Unmeasured` state. The first `ResizeObserver` entry transitions it to
57
+ * `Measured`. */
58
+ export const init = (config) => ({
59
+ id: config.id,
60
+ rowHeightPx: config.rowHeightPx,
61
+ scrollTop: config.initialScrollTop ?? 0,
62
+ measurement: Unmeasured(),
63
+ pendingScroll: Idle(),
64
+ pendingScrollVersion: 0,
65
+ });
66
+ // UPDATE
67
+ export const ApplyScroll = Command.define('ApplyScroll', { id: S.String, scrollTop: S.Number, version: S.Number }, CompletedApplyScroll)(({ id, scrollTop, version }) => Effect.sync(() => {
68
+ const element = document.getElementById(id);
69
+ if (element !== null) {
70
+ element.scrollTop = scrollTop;
71
+ }
72
+ return CompletedApplyScroll({ version });
73
+ }));
74
+ /** Processes a virtual list message and returns the next model and commands. */
75
+ export const update = (model, message) => M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
76
+ ScrolledContainer: ({ scrollTop }) => [
77
+ evo(model, { scrollTop: () => scrollTop }),
78
+ [],
79
+ ],
80
+ MeasuredContainer: ({ containerHeight }) => {
81
+ const wasUnmeasured = model.measurement._tag === 'Unmeasured';
82
+ const needsInitialApply = wasUnmeasured && model.scrollTop !== 0;
83
+ if (needsInitialApply) {
84
+ const nextVersion = Number.increment(model.pendingScrollVersion);
85
+ return [
86
+ evo(model, {
87
+ measurement: () => Measured({ containerHeight }),
88
+ pendingScrollVersion: () => nextVersion,
89
+ pendingScroll: () => ScrollingToIndex({
90
+ index: Math.floor(model.scrollTop / model.rowHeightPx),
91
+ version: nextVersion,
92
+ }),
93
+ }),
94
+ [
95
+ ApplyScroll({
96
+ id: model.id,
97
+ scrollTop: model.scrollTop,
98
+ version: nextVersion,
99
+ }),
100
+ ],
101
+ ];
102
+ }
103
+ else {
104
+ return [
105
+ evo(model, { measurement: () => Measured({ containerHeight }) }),
106
+ [],
107
+ ];
108
+ }
109
+ },
110
+ CompletedApplyScroll: ({ version }) => {
111
+ if (version !== model.pendingScrollVersion) {
112
+ return [model, []];
113
+ }
114
+ else {
115
+ return [evo(model, { pendingScroll: () => Idle() }), []];
116
+ }
117
+ },
118
+ }));
119
+ const buildScrollToIndex = (model, index, targetScrollTop) => {
120
+ const nextVersion = Number.increment(model.pendingScrollVersion);
121
+ return [
122
+ evo(model, {
123
+ pendingScrollVersion: () => nextVersion,
124
+ pendingScroll: () => ScrollingToIndex({ index, version: nextVersion }),
125
+ }),
126
+ [
127
+ ApplyScroll({
128
+ id: model.id,
129
+ scrollTop: targetScrollTop,
130
+ version: nextVersion,
131
+ }),
132
+ ],
133
+ ];
134
+ };
135
+ /** Programmatically scrolls the container so the row at `index` is visible.
136
+ * Returns the next model and a Command that mutates `element.scrollTop`. The
137
+ * natural scroll event then flows back through `ScrolledContainer` and the
138
+ * component re-renders the new visible slice.
139
+ *
140
+ * Uses version-based cancellation: each call increments
141
+ * `pendingScrollVersion` so a stale `CompletedApplyScroll` (e.g. from a
142
+ * previous in-flight scroll) is ignored when its version no longer matches.
143
+ *
144
+ * Should be called after the container has rendered. If the container is not
145
+ * yet in the DOM the Command silently no-ops (the model still transitions
146
+ * through `ScrollingToIndex` → `Idle` via the version-matched completion).
147
+ *
148
+ * Assumes uniform row heights: target scroll position is computed as
149
+ * `index * model.rowHeightPx`. For variable-height rows, use
150
+ * `scrollToIndexVariable`. */
151
+ export const scrollToIndex = (model, index) => buildScrollToIndex(model, index, index * model.rowHeightPx);
152
+ /** Variable-height counterpart of `scrollToIndex`. Walks the heights of items
153
+ * before `index` to compute the target `scrollTop`. Use this when rendering
154
+ * the list with `itemToRowHeightPx`; use `scrollToIndex` for uniform heights.
155
+ *
156
+ * Out-of-range indices clamp to the corresponding edge: negative or zero
157
+ * scrolls to the top, indices past the end scroll past the last row.
158
+ *
159
+ * Note: when restoring `initialScrollTop` on the first measurement of a
160
+ * variable-height list, the runtime falls back to uniform-height math (using
161
+ * `model.rowHeightPx`) because items aren't reachable from the `update`
162
+ * function. Consumers who need an accurate initial scroll on a
163
+ * variable-height list should call `scrollToIndexVariable` after the first
164
+ * `MeasuredContainer` arrives. */
165
+ export const scrollToIndexVariable = (model, items, itemToRowHeightPx, index) => {
166
+ const cumulativeOffsets = prefixSum(items, itemToRowHeightPx);
167
+ const targetScrollTop = pipe(cumulativeOffsets, Array.get(Math.max(0, index)), Option.getOrElse(() => lastOrZero(cumulativeOffsets)));
168
+ return buildScrollToIndex(model, index, targetScrollTop);
169
+ };
170
+ const clampIndex = (index, itemCount) => Math.max(0, Math.min(index, itemCount));
171
+ const prefixSum = (items, itemToRowHeightPx) => {
172
+ const heights = Array.map(items, itemToRowHeightPx);
173
+ return Array.scan(heights, 0, (cumulative, height) => cumulative + height);
174
+ };
175
+ const lastOrZero = (values) => pipe(values, Array.last, Option.getOrElse(() => 0));
176
+ /** Computes the visible slice of a data array given the current scroll
177
+ * position, container height, row height, and an overscan buffer.
178
+ *
179
+ * Assumes uniform row heights via `model.rowHeightPx`. For variable-height
180
+ * rows, use `visibleWindowVariable`.
181
+ *
182
+ * Returns `Option.none()` when the container has not yet been measured;
183
+ * callers should render a placeholder (or `Html.empty`) and wait for the
184
+ * first `MeasuredContainer` message. */
185
+ export const visibleWindow = (model, itemCount, overscan) => M.value(model.measurement).pipe(M.withReturnType(), M.tagsExhaustive({
186
+ Unmeasured: () => Option.none(),
187
+ Measured: ({ containerHeight }) => {
188
+ const firstVisibleIndex = Math.floor(model.scrollTop / model.rowHeightPx);
189
+ const lastVisibleIndex = Math.ceil((model.scrollTop + containerHeight) / model.rowHeightPx);
190
+ const startIndex = clampIndex(firstVisibleIndex - overscan, itemCount);
191
+ const endIndex = clampIndex(lastVisibleIndex + overscan, itemCount);
192
+ const topSpacerHeight = startIndex * model.rowHeightPx;
193
+ const bottomSpacerHeight = (itemCount - endIndex) * model.rowHeightPx;
194
+ return Option.some({
195
+ startIndex,
196
+ endIndex,
197
+ topSpacerHeight,
198
+ bottomSpacerHeight,
199
+ });
200
+ },
201
+ }));
202
+ /** Variable-height counterpart of `visibleWindow`. Walks the heights of every
203
+ * item to build a prefix-sum array, then locates the visible slice with two
204
+ * linear searches.
205
+ *
206
+ * Cost is O(N) per call, walking the whole `items` array once to build the
207
+ * prefix sums. For lists in the 10k-item range, this comfortably fits inside
208
+ * a 60Hz scroll budget. Larger lists or hotter scroll paths can layer a
209
+ * prefix-sum cache invalidated when items change; that lives behind the same
210
+ * return shape so consumers don't have to know.
211
+ *
212
+ * Returns `Option.none()` when the container has not yet been measured. */
213
+ export const visibleWindowVariable = (model, items, itemToRowHeightPx, overscan) => M.value(model.measurement).pipe(M.withReturnType(), M.tagsExhaustive({
214
+ Unmeasured: () => Option.none(),
215
+ Measured: ({ containerHeight }) => {
216
+ const itemCount = items.length;
217
+ const cumulativeOffsets = prefixSum(items, itemToRowHeightPx);
218
+ const totalHeight = lastOrZero(cumulativeOffsets);
219
+ const firstVisibleIndex = pipe(cumulativeOffsets, Array.findFirstIndex(Number.isGreaterThan(model.scrollTop)), Option.match({
220
+ onNone: () => itemCount,
221
+ onSome: index => Math.max(0, index - 1),
222
+ }));
223
+ const lastVisibleIndex = pipe(cumulativeOffsets, Array.findFirstIndex(Number.isGreaterThanOrEqualTo(model.scrollTop + containerHeight)), Option.getOrElse(() => itemCount));
224
+ const startIndex = clampIndex(firstVisibleIndex - overscan, itemCount);
225
+ const endIndex = clampIndex(lastVisibleIndex + overscan, itemCount);
226
+ const topSpacerHeight = pipe(cumulativeOffsets, Array.get(startIndex), Option.getOrElse(() => 0));
227
+ const offsetAtEnd = pipe(cumulativeOffsets, Array.get(endIndex), Option.getOrElse(() => totalHeight));
228
+ const bottomSpacerHeight = totalHeight - offsetAtEnd;
229
+ return Option.some({
230
+ startIndex,
231
+ endIndex,
232
+ topSpacerHeight,
233
+ bottomSpacerHeight,
234
+ });
235
+ },
236
+ }));
237
+ // SUBSCRIPTION
238
+ const containerElement = (id) => Option.fromNullishOr(document.getElementById(id));
239
+ /** Subscriptions that track the container's scroll position and size.
240
+ *
241
+ * - **scroll**: listens for `scroll` events on the container element and
242
+ * emits `ScrolledContainer` with the new `scrollTop`.
243
+ * - **resize**: observes the container with `ResizeObserver` and emits
244
+ * `MeasuredContainer` with the new height.
245
+ *
246
+ * A `MutationObserver` watches the document for the container element
247
+ * appearing and disappearing, so the listeners attach the moment the
248
+ * element is inserted into the DOM and clean up when it is removed. This
249
+ * makes the subscription robust across SPA route changes: navigating to a
250
+ * page that mounts the list, away, and back all reattach correctly without
251
+ * the consumer having to teach the framework about navigation. */
252
+ export const subscriptions = Subscription.make()(entry => ({
253
+ containerEvents: entry({ id: S.String }, {
254
+ modelToDependencies: model => ({ id: model.id }),
255
+ dependenciesToStream: ({ id }) => Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
256
+ const state = {
257
+ scrollListener: null,
258
+ resizeObserver: null,
259
+ observedElement: null,
260
+ pendingFrame: null,
261
+ };
262
+ const detach = () => {
263
+ if (state.resizeObserver !== null) {
264
+ state.resizeObserver.disconnect();
265
+ state.resizeObserver = null;
266
+ }
267
+ if (state.observedElement !== null &&
268
+ state.scrollListener !== null) {
269
+ state.observedElement.removeEventListener('scroll', state.scrollListener);
270
+ }
271
+ state.observedElement = null;
272
+ state.scrollListener = null;
273
+ };
274
+ const attach = (element) => {
275
+ const listener = () => Queue.offerUnsafe(queue, ScrolledContainer({ scrollTop: element.scrollTop }));
276
+ element.addEventListener('scroll', listener, { passive: true });
277
+ state.scrollListener = listener;
278
+ state.observedElement = element;
279
+ state.resizeObserver = new ResizeObserver(entries => {
280
+ const lastEntry = Array.last(entries);
281
+ if (Option.isSome(lastEntry)) {
282
+ Queue.offerUnsafe(queue, MeasuredContainer({
283
+ containerHeight: lastEntry.value.contentRect.height,
284
+ }));
285
+ }
286
+ });
287
+ state.resizeObserver.observe(element);
288
+ };
289
+ const reconcile = () => {
290
+ const maybeElement = containerElement(id);
291
+ if (Option.isNone(maybeElement)) {
292
+ if (state.observedElement !== null) {
293
+ detach();
294
+ }
295
+ return;
296
+ }
297
+ if (state.observedElement === maybeElement.value) {
298
+ return;
299
+ }
300
+ detach();
301
+ attach(maybeElement.value);
302
+ };
303
+ reconcile();
304
+ // NOTE: observes the entire document subtree because the container
305
+ // can be inserted/removed by any parent the consumer chooses (route
306
+ // changes, conditional renders, modal mounts), and the framework
307
+ // has no way to know that hierarchy in advance. Reconcile is gated
308
+ // by rAF and short-circuits when the cached observedElement is
309
+ // still in the DOM, so per-mutation cost stays low even with
310
+ // subtree: true.
311
+ const mutationObserver = new MutationObserver(() => {
312
+ if (state.pendingFrame !== null) {
313
+ return;
314
+ }
315
+ state.pendingFrame = requestAnimationFrame(() => {
316
+ state.pendingFrame = null;
317
+ reconcile();
318
+ });
319
+ });
320
+ mutationObserver.observe(document.body, {
321
+ childList: true,
322
+ subtree: true,
323
+ });
324
+ return { state, detach, mutationObserver };
325
+ }), ({ state, detach, mutationObserver }) => Effect.sync(() => {
326
+ mutationObserver.disconnect();
327
+ if (state.pendingFrame !== null) {
328
+ cancelAnimationFrame(state.pendingFrame);
329
+ }
330
+ detach();
331
+ })).pipe(Effect.flatMap(() => Effect.never))),
332
+ }),
333
+ }));
334
+ // VIEW
335
+ const DEFAULT_OVERSCAN = 5;
336
+ export const view = () =>
337
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
338
+ viewImpl;
339
+ const viewImpl = defineView((model, viewInputs) => {
340
+ const h = html();
341
+ const { items, itemToKey, itemToView, itemToRowHeightPx, overscan = DEFAULT_OVERSCAN, rowElement = 'li', containerClassName, containerAttributes = [], } = viewInputs;
342
+ const baseContainerAttributes = [
343
+ h.Id(model.id),
344
+ h.Role('list'),
345
+ h.DataAttribute('virtual-list-id', model.id),
346
+ h.Style({
347
+ overflow: 'auto',
348
+ 'list-style': 'none',
349
+ margin: '0',
350
+ padding: '0',
351
+ }),
352
+ ...(containerClassName !== undefined
353
+ ? [h.Class(containerClassName)]
354
+ : []),
355
+ ];
356
+ const allContainerAttributes = [
357
+ ...childAttributes(baseContainerAttributes),
358
+ ...containerAttributes,
359
+ ];
360
+ const renderContainer = (children) => h.keyed('ul')(model.id, allContainerAttributes, children);
361
+ const maybeWindow = itemToRowHeightPx !== undefined
362
+ ? visibleWindowVariable(model, items, itemToRowHeightPx, overscan)
363
+ : visibleWindow(model, items.length, overscan);
364
+ const rowHeightFor = (item, dataIndex) => itemToRowHeightPx !== undefined
365
+ ? itemToRowHeightPx(item, dataIndex)
366
+ : model.rowHeightPx;
367
+ return Option.match(maybeWindow, {
368
+ onNone: () => renderContainer([]),
369
+ onSome: ({ startIndex, endIndex, topSpacerHeight, bottomSpacerHeight, }) => {
370
+ const visibleItems = items.slice(startIndex, endIndex);
371
+ const topSpacer = h.keyed('li')(`${model.id}-top-spacer`, [h.Role('presentation'), h.Style({ height: `${topSpacerHeight}px` })], []);
372
+ const bottomSpacer = h.keyed('li')(`${model.id}-bottom-spacer`, [
373
+ h.Role('presentation'),
374
+ h.Style({ height: `${bottomSpacerHeight}px` }),
375
+ ], []);
376
+ const renderedRows = Array.map(visibleItems, (item, sliceIndex) => {
377
+ const dataIndex = startIndex + sliceIndex;
378
+ return h.keyed(rowElement)(itemToKey(item, dataIndex), [
379
+ h.Role('listitem'),
380
+ h.DataAttribute('virtual-list-item-index', String(dataIndex)),
381
+ h.AriaSetsize(items.length),
382
+ h.AriaPosinset(dataIndex + 1),
383
+ h.Style({
384
+ height: `${rowHeightFor(item, dataIndex)}px`,
385
+ display: 'grid',
386
+ }),
387
+ ], [itemToView(item, dataIndex)]);
388
+ });
389
+ return renderContainer([topSpacer, ...renderedRows, bottomSpacer]);
390
+ },
391
+ });
392
+ });
@@ -0,0 +1,3 @@
1
+ export { init, update, scrollToIndex, scrollToIndexVariable, view, subscriptions, visibleWindow, visibleWindowVariable, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, } from './index.js';
2
+ export type { InitConfig, ViewInputs, VisibleWindow } from './index.js';
3
+ //# sourceMappingURL=public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/virtualList/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,qBAAqB,EACrB,IAAI,EACJ,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,KAAK,EACL,OAAO,EACP,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1 @@
1
+ export { init, update, scrollToIndex, scrollToIndexVariable, view, subscriptions, visibleWindow, visibleWindowVariable, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, } from './index.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=vitest-setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest-setup.d.ts","sourceRoot":"","sources":["../src/vitest-setup.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ import { setup } from 'foldkit/test/vitest';
2
+ setup();