flet-box 1.0.3 → 1.0.5

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 (172) hide show
  1. package/README.md +10 -0
  2. package/bin/cli.js +15 -0
  3. package/bin/commands/checkApi.js +761 -0
  4. package/bin/commands/create.js +77 -5
  5. package/bin/utils/dtsProps.js +191 -0
  6. package/bin/utils/moduleExports.js +165 -0
  7. package/bin/utils/neutralise.js +64 -0
  8. package/bin/utils/templates.js +366 -27
  9. package/docs/CONTRIBUTING.md +156 -0
  10. package/docs/README.md +68 -0
  11. package/docs/arquitectura.md +164 -0
  12. package/docs/cli/README.md +542 -0
  13. package/docs/guides/README.md +33 -0
  14. package/docs/guides/app-templates.md +197 -0
  15. package/docs/guides/compatibility.md +95 -0
  16. package/docs/guides/embedding.md +284 -0
  17. package/docs/guides/first-app.md +208 -0
  18. package/docs/guides/frontend-services.md +171 -0
  19. package/docs/guides/minification-and-protection.md +260 -0
  20. package/docs/guides/mobile-and-platforms.md +439 -0
  21. package/docs/guides/online-updates.md +196 -0
  22. package/docs/guides/router.md +282 -0
  23. package/docs/guides/state.md +283 -0
  24. package/docs/guides/syntax-philosophy.md +150 -0
  25. package/docs/guides/utilities.md +241 -0
  26. package/docs/guides/widget-structure.md +163 -0
  27. package/docs/server/README.md +68 -0
  28. package/docs/server/START_HERE.md +121 -0
  29. package/docs/server/api.md +225 -0
  30. package/docs/server/data-services.md +300 -0
  31. package/docs/server/deployment.md +113 -0
  32. package/docs/server/security.md +196 -0
  33. package/docs/services/README.md +79 -0
  34. package/docs/services/RamStore.md +84 -0
  35. package/docs/services/Session.md +84 -0
  36. package/docs/services/Storage.md +88 -0
  37. package/docs/services/http.md +86 -0
  38. package/docs/tools/README.md +138 -0
  39. package/docs/tools/animation.md +87 -0
  40. package/docs/tools/async-ids-log.md +80 -0
  41. package/docs/tools/device.md +132 -0
  42. package/docs/tools/introspection.md +67 -0
  43. package/docs/tools/lists.md +96 -0
  44. package/docs/tools/markdown.md +79 -0
  45. package/docs/tools/reactivity.md +288 -0
  46. package/docs/tools/state.md +103 -0
  47. package/docs/tools/styling.md +92 -0
  48. package/docs/tools/text-and-time.md +89 -0
  49. package/docs/tools/theme.md +102 -0
  50. package/docs/tools/validation.md +75 -0
  51. package/docs/tools/visual-effects.md +84 -0
  52. package/docs/widget/Accordion.md +144 -0
  53. package/docs/widget/AdMob.md +144 -0
  54. package/docs/widget/AdSense.md +133 -0
  55. package/docs/widget/AdaptiveScaffold.md +105 -0
  56. package/docs/widget/AlertDialog.md +103 -0
  57. package/docs/widget/AnimatedBox.md +130 -0
  58. package/docs/widget/AnimatedText.md +123 -0
  59. package/docs/widget/AppBar.md +118 -0
  60. package/docs/widget/Audio.md +135 -0
  61. package/docs/widget/Avatar.md +119 -0
  62. package/docs/widget/Badge.md +128 -0
  63. package/docs/widget/BottomNavigation.md +116 -0
  64. package/docs/widget/BottomSheet.md +125 -0
  65. package/docs/widget/Button.md +137 -0
  66. package/docs/widget/COMMON_PROPS.md +150 -0
  67. package/docs/widget/Card.md +125 -0
  68. package/docs/widget/Carousel.md +151 -0
  69. package/docs/widget/Chart.md +151 -0
  70. package/docs/widget/Checkbox.md +118 -0
  71. package/docs/widget/Chip.md +126 -0
  72. package/docs/widget/CircularBar.md +168 -0
  73. package/docs/widget/CircularChart.md +164 -0
  74. package/docs/widget/CodeViewer.md +130 -0
  75. package/docs/widget/CollapsibleSideBar.md +112 -0
  76. package/docs/widget/Column.md +129 -0
  77. package/docs/widget/Container.md +136 -0
  78. package/docs/widget/DataTable.md +150 -0
  79. package/docs/widget/Divider.md +109 -0
  80. package/docs/widget/DraggBox.md +129 -0
  81. package/docs/widget/Drawer.md +135 -0
  82. package/docs/widget/DrawerItem.md +98 -0
  83. package/docs/widget/Dropdown.md +139 -0
  84. package/docs/widget/DroppBox.md +152 -0
  85. package/docs/widget/FloatingActionButton.md +120 -0
  86. package/docs/widget/GridView.md +140 -0
  87. package/docs/widget/Icon.md +117 -0
  88. package/docs/widget/Image.md +125 -0
  89. package/docs/widget/Input.md +151 -0
  90. package/docs/widget/Inspector.md +92 -0
  91. package/docs/widget/InstallButton.md +128 -0
  92. package/docs/widget/ListTile.md +132 -0
  93. package/docs/widget/ListView.md +138 -0
  94. package/docs/widget/Markdown.md +175 -0
  95. package/docs/widget/MatrixRain.md +122 -0
  96. package/docs/widget/Modal.md +129 -0
  97. package/docs/widget/Pagination.md +135 -0
  98. package/docs/widget/ParallaxBox.md +128 -0
  99. package/docs/widget/ProgressBar.md +130 -0
  100. package/docs/widget/QRCode.md +117 -0
  101. package/docs/widget/README.md +188 -0
  102. package/docs/widget/Radio.md +123 -0
  103. package/docs/widget/Rating.md +131 -0
  104. package/docs/widget/Row.md +131 -0
  105. package/docs/widget/START_HERE.md +146 -0
  106. package/docs/widget/Scaffold.md +152 -0
  107. package/docs/widget/Skeleton.md +130 -0
  108. package/docs/widget/Slider.md +146 -0
  109. package/docs/widget/SnackBar.md +102 -0
  110. package/docs/widget/Stack.md +128 -0
  111. package/docs/widget/Stepper.md +145 -0
  112. package/docs/widget/Switch.md +118 -0
  113. package/docs/widget/Tabs.md +124 -0
  114. package/docs/widget/Text.md +133 -0
  115. package/docs/widget/Tooltip.md +145 -0
  116. package/docs/widget/TreeView.md +176 -0
  117. package/docs/widget/Video.md +127 -0
  118. package/docs/widget/_template.md +61 -0
  119. package/package.json +4 -2
  120. package/src/animations/AnimatedBox.js +32 -1
  121. package/src/animations/AnimatedText.js +7 -3
  122. package/src/animations/MatrixRain.js +37 -12
  123. package/src/animations/ParallaxBox.js +16 -3
  124. package/src/animations/animate.js +187 -58
  125. package/src/animations/index.js +1 -0
  126. package/src/core/runApp.js +13 -1
  127. package/src/create-index.sh +1 -1
  128. package/src/index.d.ts +227 -91
  129. package/src/index.js +1 -0
  130. package/src/navigations/AdaptiveScaffold.js +11 -2
  131. package/src/navigations/AppBar.js +8 -2
  132. package/src/navigations/BottomNavigation.js +24 -5
  133. package/src/navigations/DrawerItem.js +16 -3
  134. package/src/navigations/Scaffold.js +8 -1
  135. package/src/navigations/Tabs.js +40 -8
  136. package/src/tools/dimensions.js +11 -3
  137. package/src/tools/useState.js +139 -101
  138. package/src/utils/getWidgetProps.js +5 -3
  139. package/src/widget-factory/assignProps.js +30 -11
  140. package/src/widget-factory/processProps.js +17 -5
  141. package/src/widget-factory/reactiveValue.js +172 -0
  142. package/src/widget-factory/translateProps.js +777 -901
  143. package/src/widget-factory/warnings.js +15 -0
  144. package/src/widget-factory/widgetFactory.js +27 -0
  145. package/src/widgets/Accordion.js +6 -5
  146. package/src/widgets/AlertDialog.js +7 -5
  147. package/src/widgets/Audio.js +30 -8
  148. package/src/widgets/BottomSheet.js +24 -1
  149. package/src/widgets/Chart.js +4 -1
  150. package/src/widgets/Checkbox.js +16 -8
  151. package/src/widgets/CircularBar.js +12 -1
  152. package/src/widgets/CircularChart.js +4 -1
  153. package/src/widgets/CodeViewer.js +26 -8
  154. package/src/widgets/DataTable.js +11 -2
  155. package/src/widgets/Dropdown.js +5 -1
  156. package/src/widgets/FloatingActionButton.js +4 -7
  157. package/src/widgets/Image.js +7 -2
  158. package/src/widgets/Input.js +119 -6
  159. package/src/widgets/Inspector.js +16 -10
  160. package/src/widgets/ListView.js +80 -30
  161. package/src/widgets/Markdown.js +53 -8
  162. package/src/widgets/Modal.js +24 -1
  163. package/src/widgets/Pagination.js +6 -3
  164. package/src/widgets/ProgressBar.js +16 -13
  165. package/src/widgets/QRCode.js +9 -2
  166. package/src/widgets/Radio.js +11 -8
  167. package/src/widgets/Slider.js +26 -4
  168. package/src/widgets/SnackBar.js +20 -2
  169. package/src/widgets/Stack.js +36 -3
  170. package/src/widgets/Stepper.js +4 -3
  171. package/src/widgets/Switch.js +8 -5
  172. package/src/widgets/TreeView.js +3 -2
@@ -341,12 +341,16 @@ export const ParallaxBox = (props = {}) => {
341
341
 
342
342
  const originalCleanup = container._cleanup;
343
343
  /**
344
- * Removes all event listeners, cancels pending animation frames, clears
345
- * timers, and calls any previously assigned `_cleanup` function.
344
+ * Removes all event listeners, cancels pending animation frames and clears
345
+ * timers. Idempotent, because it runs from BOTH teardown paths: the lifecycle
346
+ * observer fires `onUnmount`, while runApp/Scaffold call `_cleanup`.
346
347
  *
347
348
  * @returns {void}
348
349
  */
349
- container._cleanup = () => {
350
+ let disposed = false;
351
+ const dispose = () => {
352
+ if (disposed) return;
353
+ disposed = true;
350
354
  if (type === "scroll") {
351
355
  window.removeEventListener("scroll", handleScroll);
352
356
  } else {
@@ -360,6 +364,15 @@ export const ParallaxBox = (props = {}) => {
360
364
  if (initialTimer) clearTimeout(initialTimer);
361
365
  transitionTimers.forEach((timer) => clearTimeout(timer));
362
366
  transitionTimers.clear();
367
+ };
368
+
369
+ // The lifecycle observer fires `onUnmount` on a plain DOM disconnect but never
370
+ // calls `_cleanup`, so without this the window scroll listener and the timers
371
+ // survived every removal that was not a full app rebuild.
372
+ container.onUnmount(dispose);
373
+
374
+ container._cleanup = () => {
375
+ dispose();
363
376
  if (originalCleanup) originalCleanup();
364
377
  };
365
378
 
@@ -3,11 +3,16 @@
3
3
  *
4
4
  * Provides two tiers of animation helpers:
5
5
  * - **Fire-and-forget** (`animate`, `fadeOut`, `fadeIn`, `pulse`): start an
6
- * animation and move on — no way to await completion.
6
+ * animation and move on — each returns a cancel function instead of awaiting.
7
7
  * - **Async/awaitable** (`animateAsync`, `fadeOutAsync`, `fadeInAsync`,
8
8
  * `pulseAsync`): return a Promise that resolves when the animation finishes,
9
9
  * allowing sequential composition with `await`.
10
10
  *
11
+ * Every loop is registered per widget and property, so starting a second
12
+ * animation on the same property replaces the first (two loops writing one style
13
+ * fight each other), and {@link stopAnimation} ends whatever is still running on
14
+ * a widget — the usual cleanup an unmount needs when the caller kept no handle.
15
+ *
11
16
  * All functions drive the browser's `requestAnimationFrame` loop directly and
12
17
  * do not rely on CSS transitions or the Web Animations API.
13
18
  *
@@ -114,7 +119,13 @@ const getEasing = (type, t) => {
114
119
  const readAnimatedValue = (widget, property) => {
115
120
  if (property === "scale") return 1;
116
121
  if (widget?.style && property in widget.style) {
117
- return widget.style[property];
122
+ const inline = widget.style[property];
123
+ // An inline-unset property reads as "" and `interpolate` turns that into the
124
+ // string "NaN", which the browser silently drops — so `animate(el, "opacity",
125
+ // undefined, 1, 300)` (the documented API) wrote "NaN" every frame and never
126
+ // animated anything. Fall back to the property's neutral value.
127
+ if (inline !== "" && inline !== null && inline !== undefined) return inline;
128
+ return property === "opacity" ? 1 : 0;
118
129
  }
119
130
  return widget?.[property];
120
131
  };
@@ -156,6 +167,151 @@ const writeAnimatedValue = (widget, property, value) => {
156
167
  widget[property] = value;
157
168
  };
158
169
 
170
+ // ========== ACTIVE LOOP REGISTRY ==========
171
+
172
+ /**
173
+ * Live loops by widget and property.
174
+ *
175
+ * Two loops writing the same style property fight each other: the older one keeps
176
+ * overwriting the newer value on every frame and neither can be stopped. The
177
+ * registry lets a fresh animation replace whatever already animates that
178
+ * property, and lets {@link stopAnimation} end loops whose caller never kept the
179
+ * handle — the usual case for a widget that unmounts mid-fade.
180
+ *
181
+ * @type {WeakMap<object, Map<string, { raf: number, running: boolean, cancel: () => boolean }>>}
182
+ */
183
+ const activeLoops = new WeakMap();
184
+
185
+ const isTrackable = (widget) =>
186
+ widget !== null &&
187
+ (typeof widget === "object" || typeof widget === "function");
188
+
189
+ const registerLoop = (widget, property, state) => {
190
+ if (!isTrackable(widget)) return;
191
+ let byProperty = activeLoops.get(widget);
192
+ if (!byProperty) {
193
+ byProperty = new Map();
194
+ activeLoops.set(widget, byProperty);
195
+ }
196
+ byProperty.set(property, state);
197
+ };
198
+
199
+ const unregisterLoop = (widget, property, state) => {
200
+ if (!isTrackable(widget)) return;
201
+ const byProperty = activeLoops.get(widget);
202
+ if (byProperty && byProperty.get(property) === state) byProperty.delete(property);
203
+ };
204
+
205
+ /**
206
+ * Stops the animation currently running on a widget.
207
+ *
208
+ * @param {HTMLElement|object} widget - The animated widget or DOM element.
209
+ * @param {string} [property] - Stop only this property; every animated property when omitted.
210
+ * @returns {boolean} True when at least one loop was stopped.
211
+ */
212
+ export const stopAnimation = (widget, property) => {
213
+ if (!isTrackable(widget)) return false;
214
+ const byProperty = activeLoops.get(widget);
215
+ if (!byProperty) return false;
216
+
217
+ if (property !== undefined) {
218
+ const state = byProperty.get(property);
219
+ if (!state) return false;
220
+ return state.cancel();
221
+ }
222
+
223
+ const loops = Array.from(byProperty.values());
224
+ loops.forEach((state) => state.cancel());
225
+ return loops.length > 0;
226
+ };
227
+
228
+ /**
229
+ * Runs one property animation and returns its cancel function.
230
+ *
231
+ * Shared engine behind `animate`, `animateAsync` and `pulse`, so all three get
232
+ * the same guarantees: the loop is bounded by a sanitised duration, it replaces
233
+ * any animation already driving that property, and it stops the moment its
234
+ * handle is called.
235
+ *
236
+ * @param {HTMLElement|object} widget
237
+ * @param {string} property
238
+ * @param {number|string} from - Starting value; reads the widget when `undefined`.
239
+ * @param {number|string} to
240
+ * @param {number} duration
241
+ * @param {string} easing
242
+ * @param {(progress: number) => number|string} [valueAt] - Custom value curve,
243
+ * receiving eased progress in `[0, 1]`. Used by `pulse` to keep the whole
244
+ * up-and-down gesture inside a single cancelable loop.
245
+ * @param {(progress: number) => number|string} [valueAt] - Custom value curve,
246
+ * receiving eased progress in `[0, 1]`. Used by `pulse` to keep the whole
247
+ * up-and-down gesture inside a single cancelable loop.
248
+ * @param {(widget: HTMLElement|object) => void} [onDone] - Called when the loop
249
+ * ends — also when it is cancelled or replaced, so awaiting code never hangs.
250
+ * @returns {() => boolean} Cancel function; true when it stopped a live loop.
251
+ */
252
+ const runAnimation = (
253
+ widget,
254
+ property,
255
+ from,
256
+ to,
257
+ duration,
258
+ easing,
259
+ valueAt,
260
+ onDone,
261
+ ) => {
262
+ // A negative duration makes elapsed/duration increasingly negative, so progress
263
+ // never reaches 1 and the loop re-arms forever (and `animateAsync` never
264
+ // resolves). Clamping the progress is not enough — it just sits at 0 and
265
+ // `0 < 1` stays true — so sanitise the duration itself. `0` collapses to one frame.
266
+ const safeDuration = Math.max(0, Number(duration) || 0);
267
+ const startValue = from !== undefined ? from : readAnimatedValue(widget, property);
268
+
269
+ // Replace the loop already animating this property instead of racing it.
270
+ stopAnimation(widget, property);
271
+
272
+ const startTime = performance.now();
273
+ const state = { raf: 0, running: true, cancel: null };
274
+
275
+ const finish = () => {
276
+ if (!state.running) return;
277
+ state.running = false;
278
+ unregisterLoop(widget, property, state);
279
+ if (onDone) onDone(widget);
280
+ };
281
+
282
+ state.cancel = () => {
283
+ if (!state.running) return false;
284
+ cancelAnimationFrame(state.raf);
285
+ finish();
286
+ return true;
287
+ };
288
+
289
+ registerLoop(widget, property, state);
290
+
291
+ const step = (currentTime) => {
292
+ if (!state.running) return;
293
+ const progress = Math.max(
294
+ 0,
295
+ Math.min(1, (currentTime - startTime) / safeDuration),
296
+ );
297
+ const easedProgress = getEasing(easing, progress);
298
+ const currentValue = valueAt
299
+ ? valueAt(easedProgress)
300
+ : interpolate(startValue, to, easedProgress);
301
+
302
+ writeAnimatedValue(widget, property, currentValue);
303
+
304
+ if (progress < 1) {
305
+ state.raf = requestAnimationFrame(step);
306
+ return;
307
+ }
308
+ finish();
309
+ };
310
+
311
+ state.raf = requestAnimationFrame(step);
312
+ return state.cancel;
313
+ };
314
+
159
315
  // ========== FIRE AND FORGET (No Promise) ==========
160
316
 
161
317
  /**
@@ -170,7 +326,9 @@ const writeAnimatedValue = (widget, property, value) => {
170
326
  * @param {number|string} to - Ending value.
171
327
  * @param {number} [duration=300] - Animation duration in milliseconds.
172
328
  * @param {string} [easing="linear"] - Easing curve name (see {@link getEasing}).
173
- * @returns {void}
329
+ * @returns {() => boolean} Cancel function — true if it stopped a loop still running.
330
+ * The animation also stops on its own when a new animation takes the same property,
331
+ * or via {@link stopAnimation}.
174
332
  */
175
333
  export const animate = (
176
334
  widget,
@@ -179,26 +337,7 @@ export const animate = (
179
337
  to,
180
338
  duration = 300,
181
339
  easing = "linear",
182
- ) => {
183
- const startTime = performance.now();
184
- const startValue = from !== undefined ? from : readAnimatedValue(widget, property);
185
- const endValue = to;
186
-
187
- const step = (currentTime) => {
188
- const elapsed = currentTime - startTime;
189
- const progress = Math.min(1, elapsed / duration);
190
- const easedProgress = getEasing(easing, progress);
191
- const currentValue = interpolate(startValue, endValue, easedProgress);
192
-
193
- writeAnimatedValue(widget, property, currentValue);
194
-
195
- if (progress < 1) {
196
- requestAnimationFrame(step);
197
- }
198
- };
199
-
200
- requestAnimationFrame(step);
201
- };
340
+ ) => runAnimation(widget, property, from, to, duration, easing, null);
202
341
 
203
342
  // ========== ASYNC ANIMATION (Returns Promise) ==========
204
343
 
@@ -214,7 +353,8 @@ export const animate = (
214
353
  * @param {number|string} to - Ending value.
215
354
  * @param {number} [duration=300] - Animation duration in milliseconds.
216
355
  * @param {string} [easing="linear"] - Easing curve name (see {@link getEasing}).
217
- * @returns {Promise<HTMLElement|object>} Resolves with `widget` when the animation finishes.
356
+ * @returns {Promise<HTMLElement|object>} Resolves with `widget` when the animation
357
+ * finishes — or as soon as it is cancelled or replaced, so `await` never hangs.
218
358
  */
219
359
  export const animateAsync = (
220
360
  widget,
@@ -223,30 +363,12 @@ export const animateAsync = (
223
363
  to,
224
364
  duration = 300,
225
365
  easing = "linear",
226
- ) => {
227
- return new Promise((resolve) => {
228
- const startTime = performance.now();
229
- const startValue = from !== undefined ? from : readAnimatedValue(widget, property);
230
- const endValue = to;
231
-
232
- const step = (currentTime) => {
233
- const elapsed = currentTime - startTime;
234
- const progress = Math.min(1, elapsed / duration);
235
- const easedProgress = getEasing(easing, progress);
236
- const currentValue = interpolate(startValue, endValue, easedProgress);
237
-
238
- writeAnimatedValue(widget, property, currentValue);
239
-
240
- if (progress < 1) {
241
- requestAnimationFrame(step);
242
- } else {
243
- resolve(widget);
244
- }
245
- };
246
-
247
- requestAnimationFrame(step);
366
+ ) =>
367
+ new Promise((resolve) => {
368
+ // `resolve` runs on completion AND when the loop is cancelled or replaced, so
369
+ // an `await` chain cannot hang on an animation that never got to finish.
370
+ runAnimation(widget, property, from, to, duration, easing, null, resolve);
248
371
  });
249
- };
250
372
 
251
373
  // ========== HELPER FUNCTIONS (Fire and Forget) ==========
252
374
 
@@ -256,10 +378,10 @@ export const animateAsync = (
256
378
  *
257
379
  * @param {HTMLElement|object} widget - The target widget.
258
380
  * @param {number} [duration=300] - Fade duration in milliseconds.
259
- * @returns {void}
381
+ * @returns {() => boolean} Cancel function (see {@link animate}).
260
382
  */
261
383
  export const fadeOut = (widget, duration = 300) => {
262
- animate(widget, "opacity", 1, 0, duration);
384
+ return animate(widget, "opacity", 1, 0, duration);
263
385
  };
264
386
 
265
387
  /**
@@ -268,10 +390,10 @@ export const fadeOut = (widget, duration = 300) => {
268
390
  *
269
391
  * @param {HTMLElement|object} widget - The target widget.
270
392
  * @param {number} [duration=300] - Fade duration in milliseconds.
271
- * @returns {void}
393
+ * @returns {() => boolean} Cancel function (see {@link animate}).
272
394
  */
273
395
  export const fadeIn = (widget, duration = 300) => {
274
- animate(widget, "opacity", 0, 1, duration);
396
+ return animate(widget, "opacity", 0, 1, duration);
275
397
  };
276
398
 
277
399
  /**
@@ -282,14 +404,21 @@ export const fadeIn = (widget, duration = 300) => {
282
404
  * @param {HTMLElement|object} widget - The target widget.
283
405
  * @param {number} [duration=300] - Total pulse duration in milliseconds
284
406
  * (split evenly between scale-up and scale-down).
285
- * @returns {void}
407
+ * @returns {() => boolean} Cancel function (see {@link animate}).
286
408
  */
287
- export const pulse = (widget, duration = 300) => {
288
- animate(widget, "scale", 1, 1.1, duration / 2);
289
- setTimeout(() => {
290
- animate(widget, "scale", 1.1, 1, duration / 2);
291
- }, duration / 2);
292
- };
409
+ export const pulse = (widget, duration = 300) =>
410
+ runAnimation(
411
+ widget,
412
+ "scale",
413
+ 1,
414
+ 1,
415
+ duration,
416
+ "linear",
417
+ // One loop with a triangular curve instead of an animation plus a
418
+ // `setTimeout`: the timer used to start phase two on a widget that had
419
+ // already been removed from the DOM, and nothing could cancel it.
420
+ (progress) => interpolate(1, 1.1, progress < 0.5 ? progress * 2 : 2 - progress * 2),
421
+ );
293
422
 
294
423
  // ========== ASYNC HELPER FUNCTIONS (Returns Promise) ==========
295
424
 
@@ -16,6 +16,7 @@ export { MatrixRain } from "./MatrixRain.js";
16
16
  export {
17
17
  animate,
18
18
  animateAsync,
19
+ stopAnimation,
19
20
  fadeOut,
20
21
  fadeIn,
21
22
  pulse,
@@ -9,6 +9,7 @@ import {
9
9
  } from "../utils/themes.js";
10
10
  import { Container } from "../widgets/Container.js";
11
11
  import { setGlobalRender } from "../tools/useState.js";
12
+ import { beginRenderPass, endRenderPass } from "../widget-factory/reactiveValue.js";
12
13
 
13
14
  /**
14
15
  * Mounts a FletBox application into the DOM and manages its full lifecycle.
@@ -182,7 +183,18 @@ export const runApp = (
182
183
  currentMainContainer = null;
183
184
  }
184
185
 
185
- const appContent = typeof App === "function" ? App() : App;
186
+ // Wrap the component call in a render pass. A reactive value the component
187
+ // reads *here* — a template literal, a conditional, a list — is a structural
188
+ // read, so its key is flagged for an app render. Reads the factory performs
189
+ // while filling props are not flagged, which is what keeps a directly-passed
190
+ // state value surgical.
191
+ beginRenderPass();
192
+ let appContent;
193
+ try {
194
+ appContent = typeof App === "function" ? App() : App;
195
+ } finally {
196
+ endRenderPass();
197
+ }
186
198
 
187
199
  const mainContainer = Container({
188
200
  margin: 0,
@@ -32,7 +32,7 @@ export {
32
32
  useState, useWatchState,
33
33
  tokenize, generateHighlightedHtml, highlightColors,
34
34
  parseMarkdown, parseInlineMarkdown, escapeHtml, wrapListItems,
35
- animate, fadeOut, fadeIn, pulse,
35
+ animate, stopAnimation, fadeOut, fadeIn, pulse,
36
36
  applyStripes, removeStripes, applyShimmer, applyGlow,
37
37
  applyIndeterminate, applyPulse, injectKeyframes,
38
38
  Gradient, Shadow, rgba, stackPosition,