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.
- package/README.md +10 -0
- package/bin/cli.js +15 -0
- package/bin/commands/checkApi.js +761 -0
- package/bin/commands/create.js +77 -5
- package/bin/utils/dtsProps.js +191 -0
- package/bin/utils/moduleExports.js +165 -0
- package/bin/utils/neutralise.js +64 -0
- package/bin/utils/templates.js +366 -27
- package/docs/CONTRIBUTING.md +156 -0
- package/docs/README.md +68 -0
- package/docs/arquitectura.md +164 -0
- package/docs/cli/README.md +542 -0
- package/docs/guides/README.md +33 -0
- package/docs/guides/app-templates.md +197 -0
- package/docs/guides/compatibility.md +95 -0
- package/docs/guides/embedding.md +284 -0
- package/docs/guides/first-app.md +208 -0
- package/docs/guides/frontend-services.md +171 -0
- package/docs/guides/minification-and-protection.md +260 -0
- package/docs/guides/mobile-and-platforms.md +439 -0
- package/docs/guides/online-updates.md +196 -0
- package/docs/guides/router.md +282 -0
- package/docs/guides/state.md +283 -0
- package/docs/guides/syntax-philosophy.md +150 -0
- package/docs/guides/utilities.md +241 -0
- package/docs/guides/widget-structure.md +163 -0
- package/docs/server/README.md +68 -0
- package/docs/server/START_HERE.md +121 -0
- package/docs/server/api.md +225 -0
- package/docs/server/data-services.md +300 -0
- package/docs/server/deployment.md +113 -0
- package/docs/server/security.md +196 -0
- package/docs/services/README.md +79 -0
- package/docs/services/RamStore.md +84 -0
- package/docs/services/Session.md +84 -0
- package/docs/services/Storage.md +88 -0
- package/docs/services/http.md +86 -0
- package/docs/tools/README.md +138 -0
- package/docs/tools/animation.md +87 -0
- package/docs/tools/async-ids-log.md +80 -0
- package/docs/tools/device.md +132 -0
- package/docs/tools/introspection.md +67 -0
- package/docs/tools/lists.md +96 -0
- package/docs/tools/markdown.md +79 -0
- package/docs/tools/reactivity.md +288 -0
- package/docs/tools/state.md +103 -0
- package/docs/tools/styling.md +92 -0
- package/docs/tools/text-and-time.md +89 -0
- package/docs/tools/theme.md +102 -0
- package/docs/tools/validation.md +75 -0
- package/docs/tools/visual-effects.md +84 -0
- package/docs/widget/Accordion.md +144 -0
- package/docs/widget/AdMob.md +144 -0
- package/docs/widget/AdSense.md +133 -0
- package/docs/widget/AdaptiveScaffold.md +105 -0
- package/docs/widget/AlertDialog.md +103 -0
- package/docs/widget/AnimatedBox.md +130 -0
- package/docs/widget/AnimatedText.md +123 -0
- package/docs/widget/AppBar.md +118 -0
- package/docs/widget/Audio.md +135 -0
- package/docs/widget/Avatar.md +119 -0
- package/docs/widget/Badge.md +128 -0
- package/docs/widget/BottomNavigation.md +116 -0
- package/docs/widget/BottomSheet.md +125 -0
- package/docs/widget/Button.md +137 -0
- package/docs/widget/COMMON_PROPS.md +150 -0
- package/docs/widget/Card.md +125 -0
- package/docs/widget/Carousel.md +151 -0
- package/docs/widget/Chart.md +151 -0
- package/docs/widget/Checkbox.md +118 -0
- package/docs/widget/Chip.md +126 -0
- package/docs/widget/CircularBar.md +168 -0
- package/docs/widget/CircularChart.md +164 -0
- package/docs/widget/CodeViewer.md +130 -0
- package/docs/widget/CollapsibleSideBar.md +112 -0
- package/docs/widget/Column.md +129 -0
- package/docs/widget/Container.md +136 -0
- package/docs/widget/DataTable.md +150 -0
- package/docs/widget/Divider.md +109 -0
- package/docs/widget/DraggBox.md +129 -0
- package/docs/widget/Drawer.md +135 -0
- package/docs/widget/DrawerItem.md +98 -0
- package/docs/widget/Dropdown.md +139 -0
- package/docs/widget/DroppBox.md +152 -0
- package/docs/widget/FloatingActionButton.md +120 -0
- package/docs/widget/GridView.md +140 -0
- package/docs/widget/Icon.md +117 -0
- package/docs/widget/Image.md +125 -0
- package/docs/widget/Input.md +151 -0
- package/docs/widget/Inspector.md +92 -0
- package/docs/widget/InstallButton.md +128 -0
- package/docs/widget/ListTile.md +132 -0
- package/docs/widget/ListView.md +138 -0
- package/docs/widget/Markdown.md +175 -0
- package/docs/widget/MatrixRain.md +122 -0
- package/docs/widget/Modal.md +129 -0
- package/docs/widget/Pagination.md +135 -0
- package/docs/widget/ParallaxBox.md +128 -0
- package/docs/widget/ProgressBar.md +130 -0
- package/docs/widget/QRCode.md +117 -0
- package/docs/widget/README.md +188 -0
- package/docs/widget/Radio.md +123 -0
- package/docs/widget/Rating.md +131 -0
- package/docs/widget/Row.md +131 -0
- package/docs/widget/START_HERE.md +146 -0
- package/docs/widget/Scaffold.md +152 -0
- package/docs/widget/Skeleton.md +130 -0
- package/docs/widget/Slider.md +146 -0
- package/docs/widget/SnackBar.md +102 -0
- package/docs/widget/Stack.md +128 -0
- package/docs/widget/Stepper.md +145 -0
- package/docs/widget/Switch.md +118 -0
- package/docs/widget/Tabs.md +124 -0
- package/docs/widget/Text.md +133 -0
- package/docs/widget/Tooltip.md +145 -0
- package/docs/widget/TreeView.md +176 -0
- package/docs/widget/Video.md +127 -0
- package/docs/widget/_template.md +61 -0
- package/package.json +4 -2
- package/src/animations/AnimatedBox.js +32 -1
- package/src/animations/AnimatedText.js +7 -3
- package/src/animations/MatrixRain.js +37 -12
- package/src/animations/ParallaxBox.js +16 -3
- package/src/animations/animate.js +187 -58
- package/src/animations/index.js +1 -0
- package/src/core/runApp.js +13 -1
- package/src/create-index.sh +1 -1
- package/src/index.d.ts +227 -91
- package/src/index.js +1 -0
- package/src/navigations/AdaptiveScaffold.js +11 -2
- package/src/navigations/AppBar.js +8 -2
- package/src/navigations/BottomNavigation.js +24 -5
- package/src/navigations/DrawerItem.js +16 -3
- package/src/navigations/Scaffold.js +8 -1
- package/src/navigations/Tabs.js +40 -8
- package/src/tools/dimensions.js +11 -3
- package/src/tools/useState.js +139 -101
- package/src/utils/getWidgetProps.js +5 -3
- package/src/widget-factory/assignProps.js +30 -11
- package/src/widget-factory/processProps.js +17 -5
- package/src/widget-factory/reactiveValue.js +172 -0
- package/src/widget-factory/translateProps.js +777 -901
- package/src/widget-factory/warnings.js +15 -0
- package/src/widget-factory/widgetFactory.js +27 -0
- package/src/widgets/Accordion.js +6 -5
- package/src/widgets/AlertDialog.js +7 -5
- package/src/widgets/Audio.js +30 -8
- package/src/widgets/BottomSheet.js +24 -1
- package/src/widgets/Chart.js +4 -1
- package/src/widgets/Checkbox.js +16 -8
- package/src/widgets/CircularBar.js +12 -1
- package/src/widgets/CircularChart.js +4 -1
- package/src/widgets/CodeViewer.js +26 -8
- package/src/widgets/DataTable.js +11 -2
- package/src/widgets/Dropdown.js +5 -1
- package/src/widgets/FloatingActionButton.js +4 -7
- package/src/widgets/Image.js +7 -2
- package/src/widgets/Input.js +119 -6
- package/src/widgets/Inspector.js +16 -10
- package/src/widgets/ListView.js +80 -30
- package/src/widgets/Markdown.js +53 -8
- package/src/widgets/Modal.js +24 -1
- package/src/widgets/Pagination.js +6 -3
- package/src/widgets/ProgressBar.js +16 -13
- package/src/widgets/QRCode.js +9 -2
- package/src/widgets/Radio.js +11 -8
- package/src/widgets/Slider.js +26 -4
- package/src/widgets/SnackBar.js +20 -2
- package/src/widgets/Stack.js +36 -3
- package/src/widgets/Stepper.js +4 -3
- package/src/widgets/Switch.js +8 -5
- package/src/widgets/TreeView.js +3 -2
package/src/index.js
CHANGED
|
@@ -81,7 +81,10 @@ export const AdaptiveScaffold = (props) => {
|
|
|
81
81
|
// button to drawerInstance.open().
|
|
82
82
|
drawerInstance = Drawer({
|
|
83
83
|
header: null,
|
|
84
|
-
body
|
|
84
|
+
// `body` is an array of widgets, and `leftNavBar` may itself be one, so
|
|
85
|
+
// the content is flattened here. A nested array would reach `children`
|
|
86
|
+
// as a single item and break the append.
|
|
87
|
+
body: Array.isArray(content) ? content : [content],
|
|
85
88
|
footer: null,
|
|
86
89
|
position: "left",
|
|
87
90
|
width: leftNavBarWidth,
|
|
@@ -125,6 +128,12 @@ export const AdaptiveScaffold = (props) => {
|
|
|
125
128
|
widthExpanded: rightNavBarWidth,
|
|
126
129
|
widthCollapsed: 50,
|
|
127
130
|
bgColor: colors.surface,
|
|
131
|
+
// FIX: CollapsibleSideBar aplica `borderRight` por defecto (default
|
|
132
|
+
// `1px solid ${colors.border}`), así que un sidebar derecho acababa
|
|
133
|
+
// con doble borde: `borderRight` (heredado del default) + `borderLeft`
|
|
134
|
+
// (el que pedimos aquí). Forzamos `borderRight: "none"` para que solo
|
|
135
|
+
// tenga el borde izquierdo (el que da al contenido central).
|
|
136
|
+
borderRight: "none",
|
|
128
137
|
borderLeft: `1px solid ${colors.border}`,
|
|
129
138
|
children: content,
|
|
130
139
|
});
|
|
@@ -146,4 +155,4 @@ export const AdaptiveScaffold = (props) => {
|
|
|
146
155
|
});
|
|
147
156
|
};
|
|
148
157
|
|
|
149
|
-
export default AdaptiveScaffold;
|
|
158
|
+
export default AdaptiveScaffold;
|
|
@@ -277,9 +277,15 @@ export const AppBar = (props) => {
|
|
|
277
277
|
lastScrollY = currentScrollY;
|
|
278
278
|
};
|
|
279
279
|
window.addEventListener("scroll", handleScroll);
|
|
280
|
-
//
|
|
281
|
-
|
|
280
|
+
// Register the remover through BOTH teardown paths. `_cleanup` is only called
|
|
281
|
+
// by runApp's teardownTree / Scaffold, while a plain DOM disconnect goes
|
|
282
|
+
// through the lifecycle observer, which never calls `_cleanup` — so without
|
|
283
|
+
// `onUnmount` this listener (and the detached bar) stayed alive forever.
|
|
284
|
+
// `removeEventListener` is idempotent, so running both is harmless.
|
|
285
|
+
const removeScroll = () =>
|
|
282
286
|
window.removeEventListener("scroll", handleScroll);
|
|
287
|
+
appBar._cleanupScroll = removeScroll;
|
|
288
|
+
appBar.onUnmount(removeScroll);
|
|
283
289
|
}
|
|
284
290
|
|
|
285
291
|
// ========== PUBLIC API ==========
|
|
@@ -149,6 +149,7 @@ export const BottomNavigation = (props) => {
|
|
|
149
149
|
if (!useRouter) return;
|
|
150
150
|
|
|
151
151
|
const currentPath = getCurrentPath();
|
|
152
|
+
const previousIndex = activeIndex;
|
|
152
153
|
|
|
153
154
|
buttons.forEach((btnData, idx) => {
|
|
154
155
|
const isSelected = btnData.route === currentPath;
|
|
@@ -163,7 +164,10 @@ export const BottomNavigation = (props) => {
|
|
|
163
164
|
if (isSelected) activeIndex = idx;
|
|
164
165
|
});
|
|
165
166
|
|
|
166
|
-
|
|
167
|
+
// Fire only when the route actually moved the selection. A consumer that
|
|
168
|
+
// navigates from `onTabChange` (the normal case) would otherwise recurse:
|
|
169
|
+
// route change -> callback -> goTo -> route change, until the stack blows.
|
|
170
|
+
if (activeIndex !== previousIndex && onTabChange) onTabChange(activeIndex);
|
|
167
171
|
};
|
|
168
172
|
|
|
169
173
|
// ========== BUILD TAB BUTTONS ==========
|
|
@@ -214,8 +218,11 @@ export const BottomNavigation = (props) => {
|
|
|
214
218
|
|
|
215
219
|
let labelWidget = null;
|
|
216
220
|
if (showLabels && item.label) {
|
|
221
|
+
// FIX: `value` es una prop `special` que `applySpecial` solo maneja
|
|
222
|
+
// en <input>/<textarea>. En un <span> no hace nada → el label quedaba
|
|
223
|
+
// vacío. La prop correcta para contenido de texto es `text`.
|
|
217
224
|
labelWidget = Text({
|
|
218
|
-
|
|
225
|
+
text: item.label,
|
|
219
226
|
size: 11,
|
|
220
227
|
color: isActive ? selectedColor : unselectedColor,
|
|
221
228
|
fontWeight: isActive ? "500" : "400",
|
|
@@ -285,14 +292,26 @@ export const BottomNavigation = (props) => {
|
|
|
285
292
|
return getCurrentPath();
|
|
286
293
|
};
|
|
287
294
|
|
|
288
|
-
// Cleanup:
|
|
295
|
+
// Cleanup: release the router subscription on EITHER teardown path. `_cleanup`
|
|
296
|
+
// is only run by runApp's teardownTree / Scaffold; the lifecycle observer that
|
|
297
|
+
// fires on a plain DOM disconnect never calls it, so without `onUnmount` the
|
|
298
|
+
// router kept invoking `updateColorsFromRouter()` (and `onTabChange`) on a
|
|
299
|
+
// detached bar forever. The disposer is idempotent because both paths can run.
|
|
300
|
+
let unsubscribed = false;
|
|
301
|
+
const dispose = () => {
|
|
302
|
+
if (unsubscribed) return;
|
|
303
|
+
unsubscribed = true;
|
|
304
|
+
if (unsubscribe) unsubscribe();
|
|
305
|
+
};
|
|
306
|
+
container.onUnmount(dispose);
|
|
307
|
+
|
|
289
308
|
const originalCleanup = container._cleanup;
|
|
290
309
|
container._cleanup = () => {
|
|
291
|
-
|
|
310
|
+
dispose();
|
|
292
311
|
if (originalCleanup) originalCleanup();
|
|
293
312
|
};
|
|
294
313
|
|
|
295
314
|
return container;
|
|
296
315
|
};
|
|
297
316
|
|
|
298
|
-
export default BottomNavigation;
|
|
317
|
+
export default BottomNavigation;
|
|
@@ -246,11 +246,24 @@ export const DrawerItem = (props) => {
|
|
|
246
246
|
// Apply the initial visual state now that DOM refs are set.
|
|
247
247
|
updateUI();
|
|
248
248
|
|
|
249
|
-
// Cleanup:
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
// Cleanup: release the router subscription and the selection listener on EITHER
|
|
250
|
+
// teardown path. `_cleanup` is only run by runApp's teardownTree / Scaffold; the
|
|
251
|
+
// lifecycle observer that fires on a plain DOM disconnect never calls it, so
|
|
252
|
+
// without `onUnmount` every removed item kept a live router callback (retaining
|
|
253
|
+
// its DOM refs) plus a listener on the shared selection EventTarget. The
|
|
254
|
+
// disposer is idempotent because both paths can run.
|
|
255
|
+
let disposed = false;
|
|
256
|
+
const dispose = () => {
|
|
257
|
+
if (disposed) return;
|
|
258
|
+
disposed = true;
|
|
252
259
|
if (unsubscribeRouter) unsubscribeRouter();
|
|
253
260
|
if (unsubscribeGlobal) unsubscribeGlobal();
|
|
261
|
+
};
|
|
262
|
+
container.onUnmount(dispose);
|
|
263
|
+
|
|
264
|
+
const originalCleanup = container._cleanup;
|
|
265
|
+
container._cleanup = () => {
|
|
266
|
+
dispose();
|
|
254
267
|
if (originalCleanup) originalCleanup();
|
|
255
268
|
};
|
|
256
269
|
|
|
@@ -494,8 +494,15 @@ export const Scaffold = (props) => {
|
|
|
494
494
|
clearContainer(fabContainer);
|
|
495
495
|
clearContainer(leftNavBarContainer);
|
|
496
496
|
clearContainer(rightNavBarContainer);
|
|
497
|
-
|
|
497
|
+
// `destroy()` releases the viewport-resize listener, the Escape key handler
|
|
498
|
+
// and the pending close timer, then removes the node itself. Removing only
|
|
499
|
+
// the node (the previous behaviour) left all three behind, so every rebuilt
|
|
500
|
+
// scaffold leaked one more listener and one more body child.
|
|
501
|
+
if (typeof container._drawer?.destroy === "function") {
|
|
502
|
+
container._drawer.destroy();
|
|
503
|
+
} else if (container._drawer?.parentNode) {
|
|
498
504
|
container._drawer.parentNode.removeChild(container._drawer);
|
|
505
|
+
}
|
|
499
506
|
if (originalCleanup) originalCleanup();
|
|
500
507
|
};
|
|
501
508
|
|
package/src/navigations/Tabs.js
CHANGED
|
@@ -67,7 +67,11 @@ export const Tabs = (props) => {
|
|
|
67
67
|
size = "medium",
|
|
68
68
|
color = colors.primary,
|
|
69
69
|
textColor = colors.text,
|
|
70
|
-
|
|
70
|
+
// FIX: `colors.white` no existe en themes.js → caía a `undefined`
|
|
71
|
+
// y el tab activo no se distinguía visualmente. Uso `colors.background`
|
|
72
|
+
// (que es "#ffffff" en el tema claro y "#0f172a" en el oscuro, que es
|
|
73
|
+
// lo correcto: contraste sobre el fondo del tab activo).
|
|
74
|
+
activeTextColor = colors.background,
|
|
71
75
|
bgColor = colors.gray100,
|
|
72
76
|
buttonColor = colors.secondary,
|
|
73
77
|
alignment = "left",
|
|
@@ -318,6 +322,11 @@ export const Tabs = (props) => {
|
|
|
318
322
|
updateContent();
|
|
319
323
|
};
|
|
320
324
|
|
|
325
|
+
// Mount the initially active pane during construction. Without this the pane
|
|
326
|
+
// area stayed empty until the user clicked a *different* tab, even though the
|
|
327
|
+
// first tab already rendered as `aria-selected="true"`.
|
|
328
|
+
updateContent();
|
|
329
|
+
|
|
321
330
|
// Defer the initial slider position calculation by one frame so the DOM
|
|
322
331
|
// has been laid out and getBoundingClientRect() returns real values.
|
|
323
332
|
let initialPositionTimer = setTimeout(() => {
|
|
@@ -341,18 +350,41 @@ export const Tabs = (props) => {
|
|
|
341
350
|
|
|
342
351
|
// Also observe the tab bar element itself so the thumb repositions when
|
|
343
352
|
// the container resizes independently of the window (e.g. sidebar toggle).
|
|
344
|
-
|
|
353
|
+
// Only the slider variant has a thumb, so skip it for the other variants.
|
|
354
|
+
if (variant === "slider" && typeof ResizeObserver !== "undefined") {
|
|
345
355
|
resizeObserver = new ResizeObserver(() => updateSliderPosition());
|
|
346
356
|
if (tabsWrapperRef) resizeObserver.observe(tabsWrapperRef);
|
|
347
357
|
}
|
|
348
358
|
|
|
349
|
-
// Cleanup:
|
|
359
|
+
// Cleanup: release the timers, the window listener and the observer on EITHER
|
|
360
|
+
// teardown path. `_cleanup` is only run by runApp's teardownTree / Scaffold; a
|
|
361
|
+
// plain DOM disconnect goes through the lifecycle observer, which never calls
|
|
362
|
+
// it — so without `onUnmount` the resize listener stayed registered and the
|
|
363
|
+
// ResizeObserver kept observing (and retaining) a detached tab bar. The
|
|
364
|
+
// disposer is idempotent because both paths can run.
|
|
365
|
+
let disposed = false;
|
|
366
|
+
const dispose = () => {
|
|
367
|
+
if (disposed) return;
|
|
368
|
+
disposed = true;
|
|
369
|
+
if (initialPositionTimer) {
|
|
370
|
+
clearTimeout(initialPositionTimer);
|
|
371
|
+
initialPositionTimer = null;
|
|
372
|
+
}
|
|
373
|
+
if (resizeTimeout) {
|
|
374
|
+
clearTimeout(resizeTimeout);
|
|
375
|
+
resizeTimeout = null;
|
|
376
|
+
}
|
|
377
|
+
window.removeEventListener("resize", handleResize);
|
|
378
|
+
if (resizeObserver) {
|
|
379
|
+
resizeObserver.disconnect();
|
|
380
|
+
resizeObserver = null;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
container.onUnmount(dispose);
|
|
384
|
+
|
|
350
385
|
const originalCleanup = container._cleanup;
|
|
351
386
|
container._cleanup = () => {
|
|
352
|
-
|
|
353
|
-
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
354
|
-
window.removeEventListener("resize", handleResize);
|
|
355
|
-
if (resizeObserver) resizeObserver.disconnect();
|
|
387
|
+
dispose();
|
|
356
388
|
if (originalCleanup) originalCleanup();
|
|
357
389
|
};
|
|
358
390
|
|
|
@@ -381,4 +413,4 @@ export const Tabs = (props) => {
|
|
|
381
413
|
return container;
|
|
382
414
|
};
|
|
383
415
|
|
|
384
|
-
export default Tabs;
|
|
416
|
+
export default Tabs;
|
package/src/tools/dimensions.js
CHANGED
|
@@ -16,8 +16,14 @@
|
|
|
16
16
|
* // Call remove() to stop listening.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// SSR / Node guard: this module must stay importable where `window` does not
|
|
20
|
+
// exist (SSR, test runners, build tooling). Reading `window.innerWidth` at
|
|
21
|
+
// module scope used to throw a ReferenceError on import. Same convention as
|
|
22
|
+
// `src/core/pwa.js`.
|
|
23
|
+
const hasWindow = typeof window !== "undefined";
|
|
24
|
+
|
|
25
|
+
let windowWidth = hasWindow ? window.innerWidth : 0;
|
|
26
|
+
let windowHeight = hasWindow ? window.innerHeight : 0;
|
|
21
27
|
|
|
22
28
|
// Live named exports — is-module live bindings: importers of { width, height }
|
|
23
29
|
// always read the current value, kept in sync on resize.
|
|
@@ -45,7 +51,9 @@ const handleResize = () => {
|
|
|
45
51
|
});
|
|
46
52
|
};
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
// Only subscribe inside a browser; in Node the listener list stays inert and
|
|
55
|
+
// `addListener` / `removeListener` keep working without throwing.
|
|
56
|
+
if (hasWindow) window.addEventListener("resize", handleResize);
|
|
49
57
|
|
|
50
58
|
/**
|
|
51
59
|
* Dimensions object — provides getter properties and listener management.
|
package/src/tools/useState.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// src/tools/useState.js
|
|
2
2
|
import { saveRam, getRam, subscribeRam } from "../services/RamStore.js";
|
|
3
|
+
import {
|
|
4
|
+
createReactiveValue,
|
|
5
|
+
isStructuralRead,
|
|
6
|
+
resetStructuralReads,
|
|
7
|
+
} from "../widget-factory/reactiveValue.js";
|
|
3
8
|
|
|
4
9
|
// Store callbacks for each state key
|
|
5
10
|
const subscribers = new Map();
|
|
@@ -35,6 +40,56 @@ const triggerRender = () => {
|
|
|
35
40
|
}, 0);
|
|
36
41
|
};
|
|
37
42
|
|
|
43
|
+
let ramDispatcherReady = false;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Installs **one** RamStore subscriber that dispatches to the widgets and
|
|
47
|
+
* callbacks registered for the key that actually changed.
|
|
48
|
+
*
|
|
49
|
+
* RamStore walks a flat subscriber array on every `saveRam`, so registering one
|
|
50
|
+
* subscriber per key made a single state change cost O(number of keys) — with
|
|
51
|
+
* every callback immediately rejecting the key. One dispatcher makes the lookup
|
|
52
|
+
* O(1), and the work proportional to what the key really has bound.
|
|
53
|
+
*
|
|
54
|
+
* @returns {void}
|
|
55
|
+
*/
|
|
56
|
+
const ensureRamDispatcher = () => {
|
|
57
|
+
if (ramDispatcherReady) return;
|
|
58
|
+
ramDispatcherReady = true;
|
|
59
|
+
|
|
60
|
+
const unsubscribe = subscribeRam((changedKey, newValue, oldValue) => {
|
|
61
|
+
const callbacks = subscribers.get(changedKey);
|
|
62
|
+
// A key nobody ever passed to `useState` has no listeners: ignore it.
|
|
63
|
+
if (!callbacks) return;
|
|
64
|
+
|
|
65
|
+
// 1. Surgical update: patch only the widgets bound to this key.
|
|
66
|
+
const bound = widgetsByKey.get(changedKey);
|
|
67
|
+
let handled = 0;
|
|
68
|
+
if (bound) {
|
|
69
|
+
bound.forEach((propNames, widget) => {
|
|
70
|
+
if (!widget || typeof widget.update !== "function") return;
|
|
71
|
+
propNames.forEach((propName) => {
|
|
72
|
+
widget.update({ [propName]: newValue });
|
|
73
|
+
handled++;
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Notify the manual subscribers.
|
|
79
|
+
callbacks.forEach((callback) => callback(newValue, oldValue));
|
|
80
|
+
|
|
81
|
+
// 3. Re-render the app when nothing was bound, or when the last render pass
|
|
82
|
+
// also read this key structurally (a template literal, a conditional, a
|
|
83
|
+
// list): the surgical patch above cannot change what the component computes.
|
|
84
|
+
if (handled === 0 || isStructuralRead(changedKey)) triggerRender();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (typeof window !== "undefined") {
|
|
88
|
+
if (!window._useStateCleanups) window._useStateCleanups = [];
|
|
89
|
+
window._useStateCleanups.push(unsubscribe);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
38
93
|
/**
|
|
39
94
|
* Registers a widget instance so it receives automatic prop updates whenever
|
|
40
95
|
* the state value associated with `key` changes.
|
|
@@ -45,50 +100,49 @@ const triggerRender = () => {
|
|
|
45
100
|
* @returns {void}
|
|
46
101
|
*/
|
|
47
102
|
export const registerWidget = (key, widget, propName) => {
|
|
48
|
-
|
|
49
|
-
|
|
103
|
+
// Two levels — key -> widget -> prop names — so looking a widget up is O(1).
|
|
104
|
+
// A flat Set with a linear scan made registering N widgets on the same key
|
|
105
|
+
// O(N^2): 1,600 widgets on one key cost ~44 ms.
|
|
106
|
+
let perWidget = widgetsByKey.get(key);
|
|
107
|
+
if (!perWidget) {
|
|
108
|
+
perWidget = new Map();
|
|
109
|
+
widgetsByKey.set(key, perWidget);
|
|
110
|
+
}
|
|
111
|
+
let propNames = perWidget.get(widget);
|
|
112
|
+
if (!propNames) {
|
|
113
|
+
propNames = new Set();
|
|
114
|
+
perWidget.set(widget, propNames);
|
|
50
115
|
}
|
|
51
|
-
|
|
116
|
+
// Idempotent: rebuilding the same widget position must not stack duplicates.
|
|
117
|
+
const already = propNames.has(propName);
|
|
118
|
+
propNames.add(propName);
|
|
52
119
|
|
|
53
|
-
//
|
|
120
|
+
// Push the current value straight away when the widget can already take it.
|
|
121
|
+
// (During construction `update` does not exist yet — the value has just been
|
|
122
|
+
// applied by the factory, so there is nothing to push.)
|
|
54
123
|
const currentValue = getRam(key);
|
|
55
124
|
if (currentValue !== undefined && widget && widget.update) {
|
|
56
125
|
widget.update({ [propName]: currentValue });
|
|
57
126
|
}
|
|
58
127
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
128
|
+
if (already) return;
|
|
129
|
+
|
|
130
|
+
// Release the binding through `onUnmount`, which the framework fires on BOTH
|
|
131
|
+
// teardown paths. Patching `_cleanup` (the previous approach) leaked: the
|
|
132
|
+
// lifecycle observer never calls `_cleanup`, so a widget removed without a full
|
|
133
|
+
// app rebuild stayed registered forever and kept receiving updates.
|
|
134
|
+
const release = () => {
|
|
135
|
+
const perWidget = widgetsByKey.get(key);
|
|
136
|
+
const props = perWidget && perWidget.get(widget);
|
|
137
|
+
if (!props) return;
|
|
138
|
+
props.delete(propName);
|
|
139
|
+
if (props.size === 0) perWidget.delete(widget);
|
|
140
|
+
if (perWidget.size === 0) widgetsByKey.delete(key);
|
|
72
141
|
};
|
|
73
|
-
};
|
|
74
142
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* @param {string} key - The state key the widget was registered under.
|
|
79
|
-
* @param {Object} widget - The widget instance to unregister.
|
|
80
|
-
* @returns {void}
|
|
81
|
-
*/
|
|
82
|
-
export const unregisterWidget = (key, widget) => {
|
|
83
|
-
const widgets = widgetsByKey.get(key);
|
|
84
|
-
if (widgets) {
|
|
85
|
-
for (const item of widgets) {
|
|
86
|
-
if (item.widget === widget) {
|
|
87
|
-
widgets.delete(item);
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
143
|
+
// The factory installs lifecycle hooks before it binds reactive props, so a
|
|
144
|
+
// widget reaching this point always owns `onUnmount`.
|
|
145
|
+
widget.onUnmount(release);
|
|
92
146
|
};
|
|
93
147
|
|
|
94
148
|
/**
|
|
@@ -110,43 +164,21 @@ export const unregisterWidget = (key, widget) => {
|
|
|
110
164
|
*/
|
|
111
165
|
export const useState = (key, initialState, widget = null, propName = null) => {
|
|
112
166
|
// Initialize the state in RamStore if it does not exist yet
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
167
|
+
const existing = getRam(key);
|
|
168
|
+
if (existing === undefined || existing === null) {
|
|
169
|
+
saveRam(
|
|
170
|
+
key,
|
|
171
|
+
typeof initialState === "function" ? initialState() : initialState,
|
|
172
|
+
);
|
|
118
173
|
}
|
|
119
174
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// 🔥 Push the new value to all registered widgets for this key
|
|
128
|
-
const widgets = widgetsByKey.get(key);
|
|
129
|
-
if (widgets) {
|
|
130
|
-
widgets.forEach(({ widget, propName }) => {
|
|
131
|
-
if (widget && widget.update) {
|
|
132
|
-
widget.update({ [propName]: newValue });
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Notify all subscriber callbacks
|
|
138
|
-
const callbacks = subscribers.get(key);
|
|
139
|
-
callbacks.forEach((callback) => callback(newValue, oldValue));
|
|
140
|
-
|
|
141
|
-
// Trigger a global re-render
|
|
142
|
-
triggerRender();
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Track the unsubscribe handle for hot-reload cleanup
|
|
147
|
-
if (!window._useStateCleanups) window._useStateCleanups = [];
|
|
148
|
-
window._useStateCleanups.push(unsubscribe);
|
|
149
|
-
}
|
|
175
|
+
// Register the key so the shared dispatcher knows it has listeners. Previously
|
|
176
|
+
// every key installed its OWN RamStore subscriber, and RamStore notifies all of
|
|
177
|
+
// them on every save — so one state change cost O(number of keys), each callback
|
|
178
|
+
// rejecting the key immediately. One dispatcher makes it O(1) + the work for the
|
|
179
|
+
// key that actually changed.
|
|
180
|
+
if (!subscribers.has(key)) subscribers.set(key, new Set());
|
|
181
|
+
ensureRamDispatcher();
|
|
150
182
|
|
|
151
183
|
// 🔥 Register the widget for automatic updates if one was provided
|
|
152
184
|
if (widget && propName) {
|
|
@@ -161,19 +193,18 @@ export const useState = (key, initialState, widget = null, propName = null) => {
|
|
|
161
193
|
* @returns {void}
|
|
162
194
|
*/
|
|
163
195
|
const setState = (newValueOrUpdater) => {
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
// Read the store rather than a value captured when the hook ran: a setter
|
|
197
|
+
// kept across renders would otherwise compare against a stale `oldValue` and
|
|
198
|
+
// skip the dispatch when an external `saveState` had already moved the key to
|
|
199
|
+
// the value being set — leaving the store, the widgets and the callbacks
|
|
200
|
+
// disagreeing about what the state is.
|
|
201
|
+
const oldValue = getRam(key);
|
|
202
|
+
const newValue =
|
|
203
|
+
typeof newValueOrUpdater === "function"
|
|
204
|
+
? newValueOrUpdater(oldValue)
|
|
205
|
+
: newValueOrUpdater;
|
|
166
206
|
|
|
167
|
-
if (
|
|
168
|
-
newValue = newValueOrUpdater(oldValue);
|
|
169
|
-
} else {
|
|
170
|
-
newValue = newValueOrUpdater;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (oldValue !== newValue) {
|
|
174
|
-
currentValue = newValue;
|
|
175
|
-
saveRam(key, currentValue);
|
|
176
|
-
}
|
|
207
|
+
if (oldValue !== newValue) saveRam(key, newValue);
|
|
177
208
|
};
|
|
178
209
|
|
|
179
210
|
/**
|
|
@@ -188,24 +219,15 @@ export const useState = (key, initialState, widget = null, propName = null) => {
|
|
|
188
219
|
}
|
|
189
220
|
subscribers.get(key).add(callback);
|
|
190
221
|
return () => {
|
|
191
|
-
|
|
192
|
-
subscribers.get(key).delete(callback);
|
|
193
|
-
}
|
|
222
|
+
subscribers.get(key)?.delete(callback);
|
|
194
223
|
};
|
|
195
224
|
};
|
|
196
225
|
|
|
197
|
-
// 🔥
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
valueOf() {
|
|
203
|
-
return this._value;
|
|
204
|
-
},
|
|
205
|
-
toString() {
|
|
206
|
-
return String(this._value);
|
|
207
|
-
},
|
|
208
|
-
};
|
|
226
|
+
// 🔥 A LIVE reactive value: reads the store on every access (so it never goes
|
|
227
|
+
// stale) and carries the key, so WidgetFactory can auto-bind any prop it is
|
|
228
|
+
// passed to. That binding is what makes a state change update **only the widget
|
|
229
|
+
// that uses it** instead of rebuilding the app.
|
|
230
|
+
const reactiveValue = createReactiveValue(() => getRam(key), key);
|
|
209
231
|
|
|
210
232
|
// Return an array matching React's useState pattern: [value, setter, subscriber]
|
|
211
233
|
return [reactiveValue, setState, subscribe];
|
|
@@ -224,7 +246,9 @@ export const useWatchState = (key, callback) => {
|
|
|
224
246
|
subscribers.set(key, new Set());
|
|
225
247
|
}
|
|
226
248
|
subscribers.get(key).add(callback);
|
|
227
|
-
return () =>
|
|
249
|
+
return () => {
|
|
250
|
+
subscribers.get(key)?.delete(callback);
|
|
251
|
+
};
|
|
228
252
|
};
|
|
229
253
|
|
|
230
254
|
/**
|
|
@@ -242,6 +266,9 @@ export const cleanupUseState = () => {
|
|
|
242
266
|
}
|
|
243
267
|
subscribers.clear();
|
|
244
268
|
widgetsByKey.clear();
|
|
269
|
+
resetStructuralReads();
|
|
270
|
+
// The dispatcher was unsubscribed above, so a later call must reinstall it.
|
|
271
|
+
ramDispatcherReady = false;
|
|
245
272
|
globalRender = null;
|
|
246
273
|
if (renderTimeout) clearTimeout(renderTimeout);
|
|
247
274
|
};
|
|
@@ -250,12 +277,23 @@ export const cleanupUseState = () => {
|
|
|
250
277
|
* Returns diagnostic statistics about the current state of the useState module.
|
|
251
278
|
* Useful for debugging subscription leaks or widget registration issues.
|
|
252
279
|
*
|
|
253
|
-
* @returns {{
|
|
280
|
+
* @returns {{ stateKeys: number, boundWidgets: number, watchers: number, globalRender: boolean }}
|
|
254
281
|
*/
|
|
255
282
|
export const getUseStateStats = () => {
|
|
283
|
+
// `widgetsByKey` maps key -> widget -> prop names, so its size is the number of
|
|
284
|
+
// watched *keys*, not of widgets. Count one per widget.
|
|
285
|
+
let boundWidgets = 0;
|
|
286
|
+
let watchers = 0;
|
|
287
|
+
widgetsByKey.forEach((perWidget) => {
|
|
288
|
+
boundWidgets += perWidget.size;
|
|
289
|
+
});
|
|
290
|
+
subscribers.forEach((callbacks) => {
|
|
291
|
+
watchers += callbacks.size;
|
|
292
|
+
});
|
|
256
293
|
return {
|
|
257
|
-
|
|
258
|
-
|
|
294
|
+
stateKeys: subscribers.size,
|
|
295
|
+
boundWidgets,
|
|
296
|
+
watchers,
|
|
259
297
|
globalRender: !!globalRender,
|
|
260
298
|
};
|
|
261
299
|
};
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* // { text: 'Click', variant: 'filled', color: '#007aff' }
|
|
11
11
|
*/
|
|
12
12
|
export const getWidgetProps = (widget) => {
|
|
13
|
-
|
|
13
|
+
// The factory stores props in `_originalProps`. `_props` was never written by
|
|
14
|
+
// anything, so this always returned `{}` — Inspector printed no props at all.
|
|
15
|
+
return { ...(widget._originalProps || {}) };
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -20,7 +22,7 @@ export const getWidgetProps = (widget) => {
|
|
|
20
22
|
* @returns {any} Prop value or undefined
|
|
21
23
|
*/
|
|
22
24
|
export const getWidgetProp = (widget, propName) => {
|
|
23
|
-
return widget.
|
|
25
|
+
return widget._originalProps ? widget._originalProps[propName] : undefined;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -29,7 +31,7 @@ export const getWidgetProp = (widget, propName) => {
|
|
|
29
31
|
* @returns {string} Formatted props string
|
|
30
32
|
*/
|
|
31
33
|
export const stringifyWidgetProps = (widget) => {
|
|
32
|
-
return JSON.stringify(widget.
|
|
34
|
+
return JSON.stringify(widget._originalProps || {}, null, 2);
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
export default { getWidgetProps, getWidgetProp, stringifyWidgetProps };
|