flet-box 0.0.0-stage → 1.0.2

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 (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -2
  3. package/bin/cli.js +394 -0
  4. package/bin/commands/buildAndroid.js +200 -0
  5. package/bin/commands/buildIOS.js +237 -0
  6. package/bin/commands/create.js +406 -0
  7. package/bin/commands/createBundle.js +375 -0
  8. package/bin/commands/createComponent.js +115 -0
  9. package/bin/commands/createScreen.js +161 -0
  10. package/bin/commands/killServer.js +107 -0
  11. package/bin/commands/package.js +131 -0
  12. package/bin/commands/release.js +189 -0
  13. package/bin/commands/runBundle.js +62 -0
  14. package/bin/commands/runCloudflare.js +89 -0
  15. package/bin/commands/runNative.js +109 -0
  16. package/bin/commands/runServer.js +390 -0
  17. package/bin/ota/loader.js +361 -0
  18. package/bin/utils/bundleConfig.js +178 -0
  19. package/bin/utils/capacitor.js +137 -0
  20. package/bin/utils/colors.js +215 -0
  21. package/bin/utils/helpers.js +80 -0
  22. package/bin/utils/spinner.js +96 -0
  23. package/bin/utils/templates.js +1824 -0
  24. package/bin/utils/version.js +108 -0
  25. package/package.json +58 -4
  26. package/src/animations/AnimatedBox.js +607 -0
  27. package/src/animations/AnimatedText.js +105 -0
  28. package/src/animations/MatrixRain.js +220 -0
  29. package/src/animations/ParallaxBox.js +369 -0
  30. package/src/animations/animate.js +332 -0
  31. package/src/animations/index.js +25 -0
  32. package/src/app.js +41 -0
  33. package/src/assets/icon-1024-maskable.png +0 -0
  34. package/src/assets/icon-192-maskable.png +0 -0
  35. package/src/assets/icon-192.png +0 -0
  36. package/src/assets/icon-512-maskable.png +0 -0
  37. package/src/assets/icon-512.png +0 -0
  38. package/src/assets/logo.png +0 -0
  39. package/src/core/App.js +44 -0
  40. package/src/core/hmr-client.js +142 -0
  41. package/src/core/index.js +34 -0
  42. package/src/core/pwa.js +100 -0
  43. package/src/core/runApp.js +430 -0
  44. package/src/create-index.sh +237 -0
  45. package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
  46. package/src/fonts/icons.css +23 -0
  47. package/src/index.d.ts +1930 -0
  48. package/src/index.js +288 -0
  49. package/src/navigations/AdaptiveScaffold.js +149 -0
  50. package/src/navigations/AppBar.js +331 -0
  51. package/src/navigations/BottomNavigation.js +298 -0
  52. package/src/navigations/CollapsibleSideBar.js +139 -0
  53. package/src/navigations/Drawer.js +333 -0
  54. package/src/navigations/DrawerItem.js +260 -0
  55. package/src/navigations/Router.js +475 -0
  56. package/src/navigations/Scaffold.js +537 -0
  57. package/src/navigations/Tabs.js +384 -0
  58. package/src/navigations/index.js +57 -0
  59. package/src/services/RamStore.js +213 -0
  60. package/src/services/Session.js +310 -0
  61. package/src/services/Storage.js +309 -0
  62. package/src/services/http.js +350 -0
  63. package/src/services/index.js +57 -0
  64. package/src/tools/animation.js +42 -0
  65. package/src/tools/array.js +95 -0
  66. package/src/tools/border.js +28 -0
  67. package/src/tools/clipboard.js +104 -0
  68. package/src/tools/color.js +329 -0
  69. package/src/tools/createList.js +215 -0
  70. package/src/tools/delay.js +55 -0
  71. package/src/tools/device.js +138 -0
  72. package/src/tools/dict.js +297 -0
  73. package/src/tools/dimensions.js +118 -0
  74. package/src/tools/filter.js +41 -0
  75. package/src/tools/flex.js +26 -0
  76. package/src/tools/gradient.js +34 -0
  77. package/src/tools/grid.js +57 -0
  78. package/src/tools/index.js +38 -0
  79. package/src/tools/mapList.js +107 -0
  80. package/src/tools/margin.js +93 -0
  81. package/src/tools/memo.js +60 -0
  82. package/src/tools/os.js +104 -0
  83. package/src/tools/padding.js +93 -0
  84. package/src/tools/print.js +69 -0
  85. package/src/tools/random.js +299 -0
  86. package/src/tools/ref.js +70 -0
  87. package/src/tools/rgba.js +4 -0
  88. package/src/tools/shadow.js +23 -0
  89. package/src/tools/string.js +85 -0
  90. package/src/tools/time.js +674 -0
  91. package/src/tools/transform.js +68 -0
  92. package/src/tools/transition.js +48 -0
  93. package/src/tools/useState.js +268 -0
  94. package/src/tools/uuid.js +42 -0
  95. package/src/utils/TextInputValidator.js +256 -0
  96. package/src/utils/composeUpdate.js +65 -0
  97. package/src/utils/getWidgetProps.js +35 -0
  98. package/src/utils/index.js +76 -0
  99. package/src/utils/markdownParser.js +368 -0
  100. package/src/utils/mediaTime.js +79 -0
  101. package/src/utils/navigation.js +263 -0
  102. package/src/utils/scriptInjector.js +73 -0
  103. package/src/utils/stackPosition.js +31 -0
  104. package/src/utils/stopWebRefresh.js +77 -0
  105. package/src/utils/styleInjector.js +38 -0
  106. package/src/utils/syntaxHighlight.js +339 -0
  107. package/src/utils/themes.js +347 -0
  108. package/src/utils/units.js +87 -0
  109. package/src/utils/visualEffects.js +169 -0
  110. package/src/widget-factory/addChildren.js +114 -0
  111. package/src/widget-factory/assignProps.js +256 -0
  112. package/src/widget-factory/createWidget.js +28 -0
  113. package/src/widget-factory/effects.js +132 -0
  114. package/src/widget-factory/index.js +7 -0
  115. package/src/widget-factory/lifecycle.js +210 -0
  116. package/src/widget-factory/makeParentable.js +89 -0
  117. package/src/widget-factory/pixTool.js +40 -0
  118. package/src/widget-factory/processProps.js +122 -0
  119. package/src/widget-factory/reactivity.js +98 -0
  120. package/src/widget-factory/remTool.js +34 -0
  121. package/src/widget-factory/stackPosition.js +87 -0
  122. package/src/widget-factory/stringTool.js +26 -0
  123. package/src/widget-factory/tools.js +249 -0
  124. package/src/widget-factory/translateProps.js +1049 -0
  125. package/src/widget-factory/warnings.js +284 -0
  126. package/src/widget-factory/widgetFactory.js +200 -0
  127. package/src/widgets/Accordion.js +562 -0
  128. package/src/widgets/AdMob.js +315 -0
  129. package/src/widgets/AdSense.js +268 -0
  130. package/src/widgets/AlertDialog.js +162 -0
  131. package/src/widgets/Audio.js +311 -0
  132. package/src/widgets/Avatar.js +210 -0
  133. package/src/widgets/Badge.js +189 -0
  134. package/src/widgets/BottomSheet.js +287 -0
  135. package/src/widgets/Button.js +247 -0
  136. package/src/widgets/Card.js +33 -0
  137. package/src/widgets/Carousel.js +301 -0
  138. package/src/widgets/Chart.js +632 -0
  139. package/src/widgets/Checkbox.js +106 -0
  140. package/src/widgets/Chip.js +153 -0
  141. package/src/widgets/CircularBar.js +352 -0
  142. package/src/widgets/CircularChart.js +549 -0
  143. package/src/widgets/CodeViewer.js +236 -0
  144. package/src/widgets/Column.js +29 -0
  145. package/src/widgets/Container.js +31 -0
  146. package/src/widgets/DataTable.js +257 -0
  147. package/src/widgets/Divider.js +93 -0
  148. package/src/widgets/DraggBox.js +115 -0
  149. package/src/widgets/Dropdown.js +505 -0
  150. package/src/widgets/DroppBox.js +180 -0
  151. package/src/widgets/FloatingActionButton.js +91 -0
  152. package/src/widgets/GridView.js +20 -0
  153. package/src/widgets/Icon.js +33 -0
  154. package/src/widgets/Image.js +15 -0
  155. package/src/widgets/Input.js +565 -0
  156. package/src/widgets/Inspector.js +254 -0
  157. package/src/widgets/InstallButton.js +110 -0
  158. package/src/widgets/ListTile.js +224 -0
  159. package/src/widgets/ListView.js +503 -0
  160. package/src/widgets/Markdown.js +1236 -0
  161. package/src/widgets/Modal.js +496 -0
  162. package/src/widgets/Pagination.js +195 -0
  163. package/src/widgets/ProgressBar.js +178 -0
  164. package/src/widgets/QRCode.js +467 -0
  165. package/src/widgets/Radio.js +132 -0
  166. package/src/widgets/Rating.js +261 -0
  167. package/src/widgets/Row.js +29 -0
  168. package/src/widgets/Skeleton.js +243 -0
  169. package/src/widgets/Slider.js +527 -0
  170. package/src/widgets/SnackBar.js +227 -0
  171. package/src/widgets/Stack.js +29 -0
  172. package/src/widgets/Stepper.js +296 -0
  173. package/src/widgets/Switch.js +102 -0
  174. package/src/widgets/Text.js +98 -0
  175. package/src/widgets/Tooltip.js +267 -0
  176. package/src/widgets/TreeView.js +370 -0
  177. package/src/widgets/Video.js +92 -0
  178. package/src/widgets/index.js +52 -0
@@ -0,0 +1,503 @@
1
+ /**
2
+ * @file ListView.js
3
+ * @description Virtualized list/grid with scroll RAF throttling and pull-to-refresh.
4
+ */
5
+ import { WidgetFactory } from "../widget-factory/index.js";
6
+ import { injectKeyframes } from "../utils/styleInjector.js";
7
+
8
+ /**
9
+ * @typedef {Object} ListViewProps
10
+ * @property {Array} [data=[]] - Array of data items to render.
11
+ * @property {Function} renderItem - Called with `(item, index)` and must return an `HTMLElement`.
12
+ * @property {number|string} [height=400] - Height of the scroll container. Overridden when `expand` is true.
13
+ * @property {number|string} [width="100%"] - Width of the widget.
14
+ * @property {number} [itemSize=60] - Fixed height of each row in pixels (required for virtualization math).
15
+ * @property {number} [gap=0] - Vertical gap between rows in pixels.
16
+ * @property {number} [bufferSize=5] - Number of extra rows to render above and below the visible viewport.
17
+ * Larger values reduce the chance of blank flashes during fast scrolling at the cost of more DOM nodes.
18
+ * @property {boolean} [showsScrollIndicator=true] - Whether to show the native scrollbar.
19
+ * @property {boolean} [wrapItems=false] - When true, items are laid out in a grid (see `crossAxisCount`).
20
+ * @property {number} [crossAxisCount=2] - Number of grid columns when `wrapItems` is true.
21
+ * @property {Function} [onEndReached] - Called when the user scrolls within `onEndReachedThreshold` of the bottom.
22
+ * @property {number} [onEndReachedThreshold=0.5] - Fraction (0–1) of the viewport height used as the trigger distance.
23
+ * @property {Function} [onRefresh] - Enables pull-to-refresh. Called with a `done` callback to end the refresh.
24
+ * @property {boolean} [expand=true] - When true the widget takes `flex: 1` and fills its parent instead of using `height`.
25
+ * @property {Function|HTMLElement} [ListHeaderComponent] - Rendered above the list, outside the virtual window.
26
+ * @property {Function|HTMLElement} [ListFooterComponent] - Rendered below the list, outside the virtual window.
27
+ * @property {Function|HTMLElement} [ListEmptyComponent] - Rendered when `data` is empty.
28
+ */
29
+
30
+ /**
31
+ * A virtualized, scrollable list widget optimized for large datasets.
32
+ *
33
+ * Only the rows currently visible (plus a configurable buffer) are present in the DOM.
34
+ * Items are absolutely positioned inside an inner container whose height equals the
35
+ * total logical height of the list, so the scrollbar behaves as if every row existed.
36
+ *
37
+ * Rendered items are cached by index in a `Map` (capped at 200 entries) so that
38
+ * scrolling back up reuses already-created elements rather than calling `renderItem` again.
39
+ *
40
+ * Optional features:
41
+ * - Grid layout (`wrapItems` + `crossAxisCount`)
42
+ * - Pull-to-refresh (`onRefresh`)
43
+ * - Infinite scroll (`onEndReached`)
44
+ * - Header / footer / empty-state slots
45
+ *
46
+ * Public API (attached to the returned element):
47
+ * - `updateData(newData)` — replaces the dataset and re-renders
48
+ * - `scrollToIndex(index, animated?)` — scrolls to a specific row
49
+ * - `scrollToStart(animated?)` — scrolls to the top
50
+ * - `scrollToEnd(animated?)` — scrolls to the bottom
51
+ * - `data` (property) — reactive getter/setter for the dataset
52
+ * - `refreshing` (property) — getter/setter to control the refresh indicator programmatically
53
+ *
54
+ * @param {ListViewProps} props
55
+ * @returns {HTMLElement} The list container element.
56
+ */
57
+ export const ListView = (props) => {
58
+ const {
59
+ data = [],
60
+ renderItem,
61
+ height = 400,
62
+ width = "100%",
63
+ itemSize = 60,
64
+ gap = 0,
65
+ bufferSize = 5,
66
+ showsScrollIndicator = true,
67
+ wrapItems = false,
68
+ crossAxisCount = 2,
69
+ onEndReached,
70
+ onEndReachedThreshold = 0.5,
71
+ onRefresh,
72
+ expand = true,
73
+ ListHeaderComponent,
74
+ ListFooterComponent,
75
+ ListEmptyComponent,
76
+ ...rest
77
+ } = props;
78
+
79
+ // When expand is true the widget fills its flex parent, ignoring the fixed height prop
80
+ let finalHeight = height;
81
+ if (expand) {
82
+ finalHeight = "100%";
83
+ }
84
+
85
+ // Outer element — owns the overall dimensions
86
+ const element = WidgetFactory({
87
+ tag: "div",
88
+ widgetName: "ListView",
89
+ width: width,
90
+ height: height,
91
+ display: "flex",
92
+ flex: expand ? 1 : undefined,
93
+ flexDirection: "column",
94
+ overflow: "hidden",
95
+ ...rest,
96
+ });
97
+
98
+ // Scrollable container — the only element that actually scrolls
99
+ const scrollContainer = document.createElement("div");
100
+ scrollContainer.style.flex = "1";
101
+ scrollContainer.style.overflowY = "auto";
102
+ scrollContainer.style.overflowX = "hidden";
103
+ if (!showsScrollIndicator) {
104
+ // Hide the scrollbar while keeping scroll functionality
105
+ scrollContainer.style.scrollbarWidth = "none";
106
+ scrollContainer.style.msOverflowStyle = "none";
107
+ }
108
+
109
+ // innerContainer has the full logical height so the scrollbar is proportionally sized
110
+ const innerContainer = document.createElement("div");
111
+ innerContainer.style.position = "relative";
112
+ innerContainer.style.width = "100%";
113
+
114
+ // visibleContainer holds only the currently rendered (visible + buffered) rows
115
+ const visibleContainer = document.createElement("div");
116
+ visibleContainer.style.position = "relative";
117
+ visibleContainer.style.width = "100%";
118
+ visibleContainer.style.minHeight = "100%";
119
+
120
+ innerContainer.appendChild(visibleContainer);
121
+ scrollContainer.appendChild(innerContainer);
122
+ element.appendChild(scrollContainer);
123
+
124
+ // ---- Internal state ----
125
+ let _data = [...data];
126
+ let _refreshing = false;
127
+ let itemCache = new Map(); // index → rendered element, capped to avoid unbounded growth
128
+ let visibleStart = 0,
129
+ visibleEnd = 0; // current render window (avoids redundant re-renders when nothing changed)
130
+ let ticking = false; // requestAnimationFrame guard for the scroll handler
131
+ let headerElement = null,
132
+ footerElement = null,
133
+ emptyElement = null;
134
+ let refreshIndicator = null;
135
+ let isGridMode = wrapItems;
136
+ let cols = isGridMode ? Math.max(1, crossAxisCount) : 1;
137
+
138
+ // ---- Helper functions ----
139
+
140
+ /** @returns {number} Total number of rows (accounting for grid columns). */
141
+ const getTotalRows = () => Math.ceil(_data.length / cols);
142
+
143
+ /**
144
+ * Calculates the absolute top offset of an item, accounting for the header height.
145
+ * @param {number} index - Zero-based item index.
146
+ * @returns {number} Top position in pixels.
147
+ */
148
+ const getItemTop = (index) => {
149
+ const row = Math.floor(index / cols);
150
+ const headerHeight = headerElement?.offsetHeight || 0;
151
+ return headerHeight + row * (itemSize + gap);
152
+ };
153
+
154
+ /**
155
+ * Calculates the total logical height of the list including header and footer.
156
+ * This value is set on `innerContainer` so the scrollbar reflects the full list size.
157
+ * @returns {number} Total height in pixels.
158
+ */
159
+ const getTotalHeight = () => {
160
+ const rows = getTotalRows();
161
+ const headerH = headerElement?.offsetHeight || 0;
162
+ const footerH = footerElement?.offsetHeight || 0;
163
+ return headerH + (rows * (itemSize + gap) - gap) + footerH;
164
+ };
165
+
166
+ /** Applies the correct logical height to the inner container. */
167
+ const updateInnerHeight = () => {
168
+ innerContainer.style.height = `${getTotalHeight()}px`;
169
+ };
170
+
171
+ /**
172
+ * Core virtualization routine — determines which items should be in the DOM based
173
+ * on the current scroll position plus the buffer, then replaces `visibleContainer`'s
174
+ * content in one `DocumentFragment` batch to minimize reflows.
175
+ *
176
+ * Short-circuits when the visible window has not changed since the last call.
177
+ */
178
+ const renderVisibleItems = () => {
179
+ if (!scrollContainer) return;
180
+
181
+ const scrollTop = scrollContainer.scrollTop;
182
+ const viewportH = scrollContainer.clientHeight;
183
+ const itemTotalH = itemSize + gap;
184
+
185
+ // Clamp start/end rows to valid range, including the buffer zone
186
+ let startRow = Math.max(0, Math.floor(scrollTop / itemTotalH) - bufferSize);
187
+ let endRow = Math.min(
188
+ getTotalRows(),
189
+ Math.ceil((scrollTop + viewportH) / itemTotalH) + bufferSize,
190
+ );
191
+
192
+ let start = startRow * cols;
193
+ let end = Math.min(_data.length, endRow * cols);
194
+
195
+ // Nothing changed — skip the DOM update entirely
196
+ if (start === visibleStart && end === visibleEnd) return;
197
+ visibleStart = start;
198
+ visibleEnd = end;
199
+
200
+ // Build all wrappers off-screen in a fragment to minimize layout thrashing
201
+ const fragment = document.createDocumentFragment();
202
+
203
+ for (let i = start; i < end; i++) {
204
+ let itemEl = itemCache.get(i);
205
+ if (!itemEl) {
206
+ try {
207
+ itemEl = renderItem(_data[i], i);
208
+ // Evict the oldest cached item when the cache exceeds 200 entries
209
+ if (itemCache.size > 200) {
210
+ const firstKey = itemCache.keys().next().value;
211
+ itemCache.delete(firstKey);
212
+ }
213
+ itemCache.set(i, itemEl);
214
+ } catch (err) {
215
+ console.error("Error in renderItem:", err);
216
+ itemEl = document.createElement("div");
217
+ itemEl.textContent = "Error";
218
+ itemEl.style.padding = "8px";
219
+ }
220
+ }
221
+ if (!itemEl) continue;
222
+
223
+ const wrapper = document.createElement("div");
224
+ wrapper.style.position = "absolute";
225
+ wrapper.style.top = `${getItemTop(i)}px`;
226
+ if (isGridMode) {
227
+ const col = i % cols;
228
+ wrapper.style.left = `${(col / cols) * 100}%`;
229
+ wrapper.style.width = `${100 / cols}%`;
230
+ wrapper.style.padding = `0 ${gap / 2}px`;
231
+ wrapper.style.boxSizing = "border-box";
232
+ } else {
233
+ wrapper.style.left = "0";
234
+ wrapper.style.right = "0";
235
+ wrapper.style.padding = `0 ${gap}px`;
236
+ }
237
+ wrapper.appendChild(itemEl);
238
+ fragment.appendChild(wrapper);
239
+ }
240
+
241
+ // Replace visible content in one operation
242
+ visibleContainer.innerHTML = "";
243
+ visibleContainer.appendChild(fragment);
244
+ };
245
+
246
+ /**
247
+ * Scroll event handler. Uses `requestAnimationFrame` throttling so the handler
248
+ * runs at most once per frame even during rapid scroll events. Also checks
249
+ * whether the user has scrolled close to the bottom and triggers `onEndReached`.
250
+ */
251
+ const handleScroll = () => {
252
+ if (!ticking) {
253
+ requestAnimationFrame(() => {
254
+ renderVisibleItems();
255
+ const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
256
+ if (
257
+ onEndReached &&
258
+ _data.length &&
259
+ scrollHeight - (scrollTop + clientHeight) <=
260
+ clientHeight * onEndReachedThreshold
261
+ ) {
262
+ onEndReached();
263
+ }
264
+ ticking = false;
265
+ });
266
+ ticking = true;
267
+ }
268
+ };
269
+
270
+ // ---- Header / footer / empty-state ----
271
+
272
+ /** Removes any existing header and renders the new one if provided. */
273
+ const renderHeader = () => {
274
+ if (headerElement) headerElement.remove();
275
+ if (ListHeaderComponent) {
276
+ headerElement =
277
+ typeof ListHeaderComponent === "function"
278
+ ? ListHeaderComponent()
279
+ : ListHeaderComponent;
280
+ if (headerElement) {
281
+ headerElement.style.position = "relative";
282
+ scrollContainer.insertBefore(headerElement, scrollContainer.firstChild);
283
+ }
284
+ }
285
+ };
286
+
287
+ /** Removes any existing footer and renders the new one if provided. */
288
+ const renderFooter = () => {
289
+ if (footerElement) footerElement.remove();
290
+ if (ListFooterComponent) {
291
+ footerElement =
292
+ typeof ListFooterComponent === "function"
293
+ ? ListFooterComponent()
294
+ : ListFooterComponent;
295
+ if (footerElement) {
296
+ footerElement.style.position = "relative";
297
+ scrollContainer.appendChild(footerElement);
298
+ }
299
+ }
300
+ };
301
+
302
+ /** Renders the empty-state component when the dataset is empty. */
303
+ const renderEmpty = () => {
304
+ if (emptyElement) emptyElement.remove();
305
+ if (_data.length === 0 && ListEmptyComponent) {
306
+ emptyElement =
307
+ typeof ListEmptyComponent === "function"
308
+ ? ListEmptyComponent()
309
+ : ListEmptyComponent;
310
+ if (emptyElement) {
311
+ emptyElement.style.position = "relative";
312
+ scrollContainer.appendChild(emptyElement);
313
+ }
314
+ }
315
+ };
316
+
317
+ /** Recalculates dimensions and re-renders visible items and the empty state. */
318
+ const refreshUI = () => {
319
+ updateInnerHeight();
320
+ renderVisibleItems();
321
+ renderEmpty();
322
+ };
323
+
324
+ // ---- Pull-to-refresh ----
325
+
326
+ /**
327
+ * Attaches touch listeners to enable pull-to-refresh behavior.
328
+ * A spinner indicator slides in when the user pulls down from the top.
329
+ * The `onRefresh` callback receives a `done()` function to call when loading is complete.
330
+ */
331
+ const setupPullToRefresh = () => {
332
+ if (!onRefresh) return;
333
+ refreshIndicator = document.createElement("div");
334
+ refreshIndicator.style.display = "flex";
335
+ refreshIndicator.style.justifyContent = "center";
336
+ refreshIndicator.style.alignItems = "center";
337
+ refreshIndicator.style.height = "0";
338
+ refreshIndicator.style.overflow = "hidden";
339
+ refreshIndicator.style.transition = "height 0.2s";
340
+
341
+ const spinner = document.createElement("div");
342
+ spinner.style.width = "24px";
343
+ spinner.style.height = "24px";
344
+ spinner.style.border = "2px solid #e0e0e0";
345
+ spinner.style.borderTop = "2px solid #007aff";
346
+ spinner.style.borderRadius = "50%";
347
+ spinner.style.animation = "flet-spin 0.8s linear infinite";
348
+ refreshIndicator.appendChild(spinner);
349
+ scrollContainer.insertBefore(refreshIndicator, scrollContainer.firstChild);
350
+
351
+ let startY = 0,
352
+ pulling = false;
353
+ const onTouchStart = (e) => {
354
+ // Only start a pull gesture when at the very top and not already refreshing
355
+ if (scrollContainer.scrollTop === 0 && !_refreshing) {
356
+ startY = e.touches[0].clientY;
357
+ pulling = true;
358
+ }
359
+ };
360
+ const onTouchMove = (e) => {
361
+ if (pulling && !_refreshing && scrollContainer.scrollTop === 0) {
362
+ const delta = e.touches[0].clientY - startY;
363
+ if (delta > 0) {
364
+ // Cap the indicator height at 80px to prevent excessive drag
365
+ refreshIndicator.style.height = `${Math.min(delta, 80)}px`;
366
+ e.preventDefault();
367
+ }
368
+ }
369
+ };
370
+ const onTouchEnd = () => {
371
+ if (pulling && !_refreshing) {
372
+ const h = refreshIndicator?.offsetHeight || 0;
373
+ if (h >= 60) {
374
+ // Threshold met — trigger the refresh
375
+ _refreshing = true;
376
+ refreshIndicator.style.height = "60px";
377
+ onRefresh(() => {
378
+ _refreshing = false;
379
+ refreshIndicator.style.height = "0";
380
+ if (element.refreshing !== undefined) element.refreshing = false;
381
+ refreshUI();
382
+ });
383
+ } else {
384
+ // Threshold not met — snap back without refreshing
385
+ refreshIndicator.style.height = "0";
386
+ }
387
+ }
388
+ pulling = false;
389
+ };
390
+ scrollContainer.addEventListener("touchstart", onTouchStart);
391
+ scrollContainer.addEventListener("touchmove", onTouchMove);
392
+ scrollContainer.addEventListener("touchend", onTouchEnd);
393
+
394
+ element.onUnmount(() => {
395
+ scrollContainer.removeEventListener("touchstart", onTouchStart);
396
+ scrollContainer.removeEventListener("touchmove", onTouchMove);
397
+ scrollContainer.removeEventListener("touchend", onTouchEnd);
398
+ });
399
+ };
400
+
401
+ // ---- Public methods ----
402
+
403
+ /**
404
+ * Replaces the current dataset, clears the item cache, and triggers a full re-render.
405
+ * @param {Array} newData - The new array of data items.
406
+ */
407
+ const updateData = (newData) => {
408
+ _data = [...newData];
409
+ itemCache.clear();
410
+ refreshUI();
411
+ element.data = _data;
412
+ };
413
+
414
+ /**
415
+ * Scrolls to a specific item by its zero-based index.
416
+ * @param {number} index - Target item index.
417
+ * @param {boolean} [animated=true] - Whether to use smooth scrolling.
418
+ */
419
+ const scrollToIndex = (index, animated = true) => {
420
+ if (index < 0 || index >= _data.length) return;
421
+ const top = getItemTop(index);
422
+ scrollContainer.scrollTo({ top, behavior: animated ? "smooth" : "auto" });
423
+ };
424
+
425
+ /**
426
+ * Scrolls to the top of the list.
427
+ * @param {boolean} [animated=true]
428
+ */
429
+ const scrollToTop = (animated = true) => {
430
+ scrollContainer.scrollTo({
431
+ top: 0,
432
+ behavior: animated ? "smooth" : "auto",
433
+ });
434
+ };
435
+
436
+ /**
437
+ * Scrolls to the bottom of the list.
438
+ * @param {boolean} [animated=true]
439
+ */
440
+ const scrollToBottom = (animated = true) => {
441
+ scrollContainer.scrollTo({
442
+ top: scrollContainer.scrollHeight,
443
+ behavior: animated ? "smooth" : "auto",
444
+ });
445
+ };
446
+
447
+ element.updateData = updateData;
448
+ element.scrollToIndex = scrollToIndex;
449
+ element.scrollToStart = scrollToTop;
450
+ element.scrollToEnd = scrollToBottom;
451
+
452
+ // Reactive `data` property — setting it is equivalent to calling `updateData`
453
+ Object.defineProperty(element, "data", {
454
+ get: () => _data,
455
+ set: (newVal) => updateData(newVal),
456
+ enumerable: true,
457
+ configurable: true,
458
+ });
459
+
460
+ // Reactive `refreshing` property — lets the parent show/hide the spinner programmatically
461
+ Object.defineProperty(element, "refreshing", {
462
+ get: () => _refreshing,
463
+ set: (val) => {
464
+ _refreshing = val;
465
+ if (refreshIndicator)
466
+ refreshIndicator.style.height = _refreshing ? "60px" : "0";
467
+ },
468
+ enumerable: true,
469
+ });
470
+
471
+ // ---- Lifecycle hooks ----
472
+ element.onMount(() => {
473
+ renderHeader();
474
+ renderFooter();
475
+ updateInnerHeight();
476
+
477
+ // Defer the first actual render to the next animation frame so the header/footer
478
+ // have already been measured and their heights are available.
479
+ requestAnimationFrame(() => {
480
+ updateInnerHeight(); // Recalculate total height now that header/footer are in the DOM
481
+ renderVisibleItems(); // Render the initially visible rows
482
+ });
483
+
484
+ renderEmpty();
485
+ scrollContainer.addEventListener("scroll", handleScroll);
486
+ if (onRefresh) setupPullToRefresh();
487
+
488
+ // Inject the spinner keyframes once, globally, so pull-to-refresh has its animation
489
+ injectKeyframes(
490
+ "flet-spin",
491
+ "0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); }",
492
+ );
493
+ });
494
+
495
+ element.onUnmount(() => {
496
+ scrollContainer.removeEventListener("scroll", handleScroll);
497
+ itemCache.clear();
498
+ });
499
+
500
+ return element;
501
+ };
502
+
503
+ export default ListView;