flet-box 1.0.2 → 1.0.4
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/bin/ota/loader.js +6 -1
- package/bin/utils/templates.js +6 -2
- package/package.json +3 -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 +22 -3
- package/src/core/runApp.js +13 -1
- package/src/create-index.sh +2 -2
- package/src/index.d.ts +193 -79
- package/src/index.js +1 -0
- package/src/navigations/AppBar.js +17 -4
- package/src/navigations/BottomNavigation.js +14 -2
- package/src/navigations/DrawerItem.js +16 -3
- package/src/navigations/Router.js +125 -6
- package/src/navigations/Scaffold.js +13 -3
- package/src/navigations/Tabs.js +34 -6
- package/src/navigations/index.js +2 -0
- package/src/tools/dimensions.js +11 -3
- package/src/tools/useState.js +115 -68
- package/src/utils/getWidgetProps.js +5 -3
- package/src/widget-factory/assignProps.js +7 -10
- package/src/widget-factory/processProps.js +7 -2
- package/src/widget-factory/reactiveValue.js +147 -0
- 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/Audio.js +30 -8
- package/src/widgets/BottomSheet.js +24 -1
- package/src/widgets/Chart.js +4 -1
- package/src/widgets/Checkbox.js +7 -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/Image.js +8 -1
- package/src/widgets/Input.js +27 -6
- package/src/widgets/Inspector.js +16 -10
- package/src/widgets/ListView.js +7 -1
- package/src/widgets/Markdown.js +53 -8
- package/src/widgets/Modal.js +24 -1
- package/src/widgets/QRCode.js +9 -2
- package/src/widgets/Radio.js +5 -1
- package/src/widgets/Slider.js +3 -0
- package/src/widgets/SnackBar.js +20 -2
- package/src/widgets/Switch.js +5 -1
- package/src/widgets/TreeView.js +3 -1
package/bin/ota/loader.js
CHANGED
|
@@ -331,7 +331,12 @@ async function checkAndUpdate(shippedConfig, activeHash) {
|
|
|
331
331
|
(async function main() {
|
|
332
332
|
let shippedConfig = null;
|
|
333
333
|
try {
|
|
334
|
-
|
|
334
|
+
// Anchored to this script, not the document: on a static host with SPA
|
|
335
|
+
// fallback (GitHub Pages) the app can boot from a deep URL, where a
|
|
336
|
+
// document-relative "./config.json" would resolve to the wrong folder.
|
|
337
|
+
shippedConfig = await fetchJson(
|
|
338
|
+
new URL("./config.json", import.meta.url).href,
|
|
339
|
+
);
|
|
335
340
|
} catch (e) {
|
|
336
341
|
warn("no shipped config.json:", e.message);
|
|
337
342
|
}
|
package/bin/utils/templates.js
CHANGED
|
@@ -607,9 +607,13 @@ echo "✅ Bundle listo: \$TARGET/ Prueba: cd \$TARGET && bash run.sh"
|
|
|
607
607
|
|
|
608
608
|
export const serviceWorkerJs = () => `// service-worker.js - PWA offline support
|
|
609
609
|
const CACHE_NAME = 'fletbox-v2';
|
|
610
|
+
// Deployment prefix: a worker's own URL always sits at the app root, so this
|
|
611
|
+
// is '/' at the host root and '/repo-name/' on a GitHub Pages project site.
|
|
612
|
+
// Hard-coded '/' entries would miss every file under a subpath.
|
|
613
|
+
const BASE = new URL('./', self.location).pathname;
|
|
610
614
|
// The shipped (baked-in) shell, precached so the app opens with no connection.
|
|
611
615
|
// loader.js owns a SEPARATE cache ('fletbox-ota') for downloaded bundles.
|
|
612
|
-
const PRECACHE = [
|
|
616
|
+
const PRECACHE = [BASE, BASE + 'index.html', BASE + 'loader.js', BASE + 'config.json', BASE + 'src/app.js'];
|
|
613
617
|
|
|
614
618
|
self.addEventListener('install', event => {
|
|
615
619
|
event.waitUntil(
|
|
@@ -690,7 +694,7 @@ self.addEventListener('fetch', event => {
|
|
|
690
694
|
const cached = await cache.match(req);
|
|
691
695
|
if (cached) return cached;
|
|
692
696
|
if (req.mode === 'navigate') {
|
|
693
|
-
const shell = await cache.match('
|
|
697
|
+
const shell = await cache.match(BASE + 'index.html');
|
|
694
698
|
if (shell) return shell;
|
|
695
699
|
}
|
|
696
700
|
return Response.error();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flet-box",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Declarative, zero-dependency UI framework in vanilla JS — ship web, PWA, Android and iOS from one codebase",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"dev": "node bin/cli.js run --hot",
|
|
20
20
|
"build": "node bin/cli.js createBundle",
|
|
21
21
|
"release": "node bin/cli.js release",
|
|
22
|
-
"test": "node scripts/check-widgets.mjs"
|
|
22
|
+
"test": "node scripts/check-widgets.mjs && node scripts/check-api-surface.mjs",
|
|
23
|
+
"test:browser": "node scripts/run-browser-tests.mjs"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
|
25
26
|
"fletbox",
|
|
@@ -334,6 +334,24 @@ const interpolateGradient = (gradient1, gradient2, t) => {
|
|
|
334
334
|
* @param {string} keyframes - The keyframe body (everything inside `@keyframes name { … }`).
|
|
335
335
|
* @returns {void}
|
|
336
336
|
*/
|
|
337
|
+
/**
|
|
338
|
+
* Stable hash of a keyframe body, used as the generated animation name so that
|
|
339
|
+
* identical keyframes are injected only once.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} keyframes
|
|
342
|
+
* @returns {string}
|
|
343
|
+
*/
|
|
344
|
+
const hashKeyframes = (keyframes) => {
|
|
345
|
+
let h1 = 0x811c9dc5;
|
|
346
|
+
let h2 = 0x01000193;
|
|
347
|
+
for (let i = 0; i < keyframes.length; i++) {
|
|
348
|
+
const c = keyframes.charCodeAt(i);
|
|
349
|
+
h1 = Math.imul(h1 ^ c, 0x01000193);
|
|
350
|
+
h2 = Math.imul(h2 + c, 0x85ebca6b) ^ (h2 >>> 13);
|
|
351
|
+
}
|
|
352
|
+
return (h1 >>> 0).toString(36) + (h2 >>> 0).toString(36);
|
|
353
|
+
};
|
|
354
|
+
|
|
337
355
|
const injectKeyframes = (name, keyframes) => {
|
|
338
356
|
if (injectedKeyframes.has(name)) return;
|
|
339
357
|
const style = document.createElement("style");
|
|
@@ -365,6 +383,15 @@ const generateKeyframes = (animations) => {
|
|
|
365
383
|
const effect = effectMap[anim.effect] || anim.effect;
|
|
366
384
|
const { from, to, reverse = true } = anim;
|
|
367
385
|
|
|
386
|
+
// A descriptor without `from`/`to` is malformed. Skip it with a warning
|
|
387
|
+
// rather than throwing a TypeError out of the caller's entire render.
|
|
388
|
+
if (from === undefined || to === undefined) {
|
|
389
|
+
console.warn(
|
|
390
|
+
`⚠️ [AnimatedBox] Skipping animation "${anim.effect}": "from" and "to" are required.`,
|
|
391
|
+
);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
368
395
|
let value;
|
|
369
396
|
let color;
|
|
370
397
|
|
|
@@ -589,8 +616,12 @@ export const AnimatedBox = ({
|
|
|
589
616
|
child.style.background = firstAnim.from;
|
|
590
617
|
}
|
|
591
618
|
|
|
592
|
-
const name = `anim-${Date.now()}-${animationCounter++}`;
|
|
593
619
|
const keyframes = generateKeyframes(animations);
|
|
620
|
+
// The name is derived from the keyframe body so identical animations reuse a
|
|
621
|
+
// single <style>. A per-call unique name (`Date.now()`) made the dedupe check
|
|
622
|
+
// in `injectKeyframes` dead code: every call appended a permanent <style> to
|
|
623
|
+
// document.head, and AnimatedText calls this once per letter.
|
|
624
|
+
const name = `anim-${hashKeyframes(keyframes)}`;
|
|
594
625
|
injectKeyframes(name, keyframes);
|
|
595
626
|
|
|
596
627
|
const duration = animations[0]?.duration || 500;
|
|
@@ -39,9 +39,13 @@ export const AnimatedText = ({
|
|
|
39
39
|
if (!child) return null;
|
|
40
40
|
if (!animations || animations.length === 0) return child;
|
|
41
41
|
|
|
42
|
-
// Extract text content and props from the original Text widget
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// Extract text content and props from the original Text widget. The factory
|
|
43
|
+
// stores props in `_originalProps`; `_props` is never assigned by anything, so
|
|
44
|
+
// reading it here silently dropped every style (font, size, colour) and the
|
|
45
|
+
// documented per-letter inheritance never happened.
|
|
46
|
+
const sourceProps = child._originalProps || child._props || {};
|
|
47
|
+
const originalText = sourceProps.text || child.textContent || "";
|
|
48
|
+
const originalProps = { ...sourceProps };
|
|
45
49
|
// Ensure no background animation styles bleed into individual letter widgets
|
|
46
50
|
delete originalProps.animations;
|
|
47
51
|
|
|
@@ -52,6 +52,11 @@ export const MatrixRain = (props = {}) => {
|
|
|
52
52
|
let drops = [];
|
|
53
53
|
let columns = 0;
|
|
54
54
|
let resizeObserver = null;
|
|
55
|
+
let observedParent = null;
|
|
56
|
+
// The canvas is returned to the caller and appended afterwards, so it is NOT
|
|
57
|
+
// connected when the animation starts. `wasConnected` tracks that first mount
|
|
58
|
+
// so the loop can stop when the canvas is later removed from the document.
|
|
59
|
+
let wasConnected = false;
|
|
55
60
|
|
|
56
61
|
/**
|
|
57
62
|
* Determines the canvas dimensions based on positioning mode.
|
|
@@ -84,14 +89,17 @@ export const MatrixRain = (props = {}) => {
|
|
|
84
89
|
*/
|
|
85
90
|
const resizeCanvas = () => {
|
|
86
91
|
if (!canvas) return;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
// The canvas is returned to the caller and appended afterwards, so on the
|
|
93
|
+
// first call there is no parent yet. Create the observer once and (re)attach
|
|
94
|
+
// it whenever the canvas ends up living in a different parent.
|
|
95
|
+
if (position === "absolute" && typeof ResizeObserver !== "undefined") {
|
|
96
|
+
if (!resizeObserver) resizeObserver = new ResizeObserver(resizeCanvas);
|
|
97
|
+
const parent = canvas.parentElement;
|
|
98
|
+
if (parent && observedParent !== parent) {
|
|
99
|
+
if (observedParent) resizeObserver.unobserve(observedParent);
|
|
100
|
+
resizeObserver.observe(parent);
|
|
101
|
+
observedParent = parent;
|
|
102
|
+
}
|
|
95
103
|
}
|
|
96
104
|
const { width, height } = getCanvasSize();
|
|
97
105
|
canvas.width = width;
|
|
@@ -154,6 +162,25 @@ export const MatrixRain = (props = {}) => {
|
|
|
154
162
|
const draw = () => {
|
|
155
163
|
if (!ctx || !canvas) return;
|
|
156
164
|
|
|
165
|
+
// The canvas is handed back to the caller and appended afterwards, so it is
|
|
166
|
+
// disconnected while the animation starts. Once it HAS been mounted, a
|
|
167
|
+
// disconnect means it was removed from the document — stop there.
|
|
168
|
+
//
|
|
169
|
+
// Nothing else covers that path: `runApp`'s teardownTree and Scaffold's
|
|
170
|
+
// observer call `_cleanup()`, but the lifecycle observer in
|
|
171
|
+
// `src/widget-factory/lifecycle.js` only fires `onUnmount`, and a raw canvas
|
|
172
|
+
// has no such hook. Without this guard the loop would keep re-arming at
|
|
173
|
+
// 60fps painting into a detached canvas, forever.
|
|
174
|
+
if (canvas.isConnected) {
|
|
175
|
+
if (!wasConnected) {
|
|
176
|
+
wasConnected = true;
|
|
177
|
+
resizeCanvas(); // size to the real parent and attach the observer
|
|
178
|
+
}
|
|
179
|
+
} else if (wasConnected) {
|
|
180
|
+
cleanup();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
157
184
|
// Semi-transparent black overlay produces the trailing-fade effect
|
|
158
185
|
ctx.fillStyle = `rgba(0, 0, 0, ${fadeAmount})`;
|
|
159
186
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
@@ -188,10 +215,8 @@ export const MatrixRain = (props = {}) => {
|
|
|
188
215
|
};
|
|
189
216
|
|
|
190
217
|
window.addEventListener("resize", handleResize);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (canvas.parentElement) resizeObserver.observe(canvas.parentElement);
|
|
194
|
-
}
|
|
218
|
+
// NOTE: the parent ResizeObserver is attached from `resizeCanvas()` (see
|
|
219
|
+
// above), because the canvas has no parent until the caller appends it.
|
|
195
220
|
|
|
196
221
|
/**
|
|
197
222
|
* Stops the animation loop, removes event listeners, disconnects the
|
|
@@ -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
|
|
345
|
-
* timers,
|
|
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
|
-
|
|
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
|
|
|
@@ -114,7 +114,13 @@ const getEasing = (type, t) => {
|
|
|
114
114
|
const readAnimatedValue = (widget, property) => {
|
|
115
115
|
if (property === "scale") return 1;
|
|
116
116
|
if (widget?.style && property in widget.style) {
|
|
117
|
-
|
|
117
|
+
const inline = widget.style[property];
|
|
118
|
+
// An inline-unset property reads as "" and `interpolate` turns that into the
|
|
119
|
+
// string "NaN", which the browser silently drops — so `animate(el, "opacity",
|
|
120
|
+
// undefined, 1, 300)` (the documented API) wrote "NaN" every frame and never
|
|
121
|
+
// animated anything. Fall back to the property's neutral value.
|
|
122
|
+
if (inline !== "" && inline !== null && inline !== undefined) return inline;
|
|
123
|
+
return property === "opacity" ? 1 : 0;
|
|
118
124
|
}
|
|
119
125
|
return widget?.[property];
|
|
120
126
|
};
|
|
@@ -180,13 +186,21 @@ export const animate = (
|
|
|
180
186
|
duration = 300,
|
|
181
187
|
easing = "linear",
|
|
182
188
|
) => {
|
|
189
|
+
// A negative duration makes elapsed/duration increasingly negative, so the
|
|
190
|
+
// loop re-armed forever and the property was driven backwards. Clamping the
|
|
191
|
+
// progress is not enough (it just sits at 0 and `0 < 1` stays true), so
|
|
192
|
+
// sanitise the duration itself. `0` collapses to a single frame.
|
|
193
|
+
duration = Math.max(0, Number(duration) || 0);
|
|
183
194
|
const startTime = performance.now();
|
|
184
195
|
const startValue = from !== undefined ? from : readAnimatedValue(widget, property);
|
|
185
196
|
const endValue = to;
|
|
186
197
|
|
|
187
198
|
const step = (currentTime) => {
|
|
188
199
|
const elapsed = currentTime - startTime;
|
|
189
|
-
|
|
200
|
+
// Clamp both ends. A negative `duration` makes elapsed/duration increasingly
|
|
201
|
+
// negative, so `progress` never reached 1 and the loop re-armed forever (and
|
|
202
|
+
// `animateAsync` never resolved). `duration: 0` yields Infinity, clamped to 1.
|
|
203
|
+
const progress = Math.max(0, Math.min(1, elapsed / duration));
|
|
190
204
|
const easedProgress = getEasing(easing, progress);
|
|
191
205
|
const currentValue = interpolate(startValue, endValue, easedProgress);
|
|
192
206
|
|
|
@@ -224,6 +238,9 @@ export const animateAsync = (
|
|
|
224
238
|
duration = 300,
|
|
225
239
|
easing = "linear",
|
|
226
240
|
) => {
|
|
241
|
+
// See `animate`: a negative duration would keep the loop alive forever and this
|
|
242
|
+
// promise would never resolve.
|
|
243
|
+
duration = Math.max(0, Number(duration) || 0);
|
|
227
244
|
return new Promise((resolve) => {
|
|
228
245
|
const startTime = performance.now();
|
|
229
246
|
const startValue = from !== undefined ? from : readAnimatedValue(widget, property);
|
|
@@ -231,7 +248,9 @@ export const animateAsync = (
|
|
|
231
248
|
|
|
232
249
|
const step = (currentTime) => {
|
|
233
250
|
const elapsed = currentTime - startTime;
|
|
234
|
-
|
|
251
|
+
// Same clamp as `animate`: a negative `duration` would otherwise never
|
|
252
|
+
// reach progress 1 and this promise would never resolve.
|
|
253
|
+
const progress = Math.max(0, Math.min(1, elapsed / duration));
|
|
235
254
|
const easedProgress = getEasing(easing, progress);
|
|
236
255
|
const currentValue = interpolate(startValue, endValue, easedProgress);
|
|
237
256
|
|
package/src/core/runApp.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/create-index.sh
CHANGED
|
@@ -60,7 +60,7 @@ export {
|
|
|
60
60
|
Scaffold, AppBar, Drawer, BottomNavigation, Tabs, DrawerItem,
|
|
61
61
|
openDrawer, closeDrawer, toggleDrawer, destroyDrawer,
|
|
62
62
|
initRouter, goTo, goBack, goForward, replace,
|
|
63
|
-
getCurrentPath, getCurrentRoute, getCurrentRouteConfig,
|
|
63
|
+
getCurrentPath, getBasePath, getCurrentRoute, getCurrentRouteConfig,
|
|
64
64
|
getRoute, isActive, subscribe, useParams, useQueryParams,
|
|
65
65
|
buildUrl, clearRouter
|
|
66
66
|
} from './src/navigations/index.js';
|
|
@@ -178,7 +178,7 @@ export { Drawer, openDrawer, closeDrawer, toggleDrawer, destroyDrawer } from './
|
|
|
178
178
|
export { BottomNavigation } from './BottomNavigation.js';
|
|
179
179
|
export { Tabs } from './Tabs.js';
|
|
180
180
|
export { DrawerItem } from './DrawerItem.js';
|
|
181
|
-
export { initRouter, goTo, goBack, goForward, replace, getCurrentPath, getCurrentRoute, getCurrentRouteConfig, getRoute, isActive, subscribe, useParams, useQueryParams, buildUrl, clearRouter } from './Router.js';
|
|
181
|
+
export { initRouter, goTo, goBack, goForward, replace, getCurrentPath, getBasePath, getCurrentRoute, getCurrentRouteConfig, getRoute, isActive, subscribe, useParams, useQueryParams, buildUrl, clearRouter } from './Router.js';
|
|
182
182
|
EOF
|
|
183
183
|
echo "✅ Created: src/navigations/index.js"
|
|
184
184
|
|