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/src/index.js
CHANGED
|
@@ -165,6 +165,8 @@ export const AppBar = (props) => {
|
|
|
165
165
|
display: "flex",
|
|
166
166
|
alignItems: "center",
|
|
167
167
|
justifyContent: "space-between",
|
|
168
|
+
width: "100%",
|
|
169
|
+
boxSizing: "border-box",
|
|
168
170
|
minHeight: 56,
|
|
169
171
|
borderRadius: finalBorderRadius,
|
|
170
172
|
marginTop: finalMarginTop,
|
|
@@ -217,7 +219,10 @@ export const AppBar = (props) => {
|
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
const centerSection = Container({
|
|
220
|
-
flex:
|
|
222
|
+
// Always flex:1 so the title absorbs whatever the leading and action zones
|
|
223
|
+
// do not use. With flex:"none" a long title keeps its intrinsic width and
|
|
224
|
+
// pushes the actions past the viewport edge on narrow screens.
|
|
225
|
+
flex: 1,
|
|
221
226
|
display: "flex",
|
|
222
227
|
alignItems: "center",
|
|
223
228
|
justifyContent: centerTitle ? "center" : "flex-start",
|
|
@@ -232,9 +237,11 @@ export const AppBar = (props) => {
|
|
|
232
237
|
// ========== RIGHT SECTION (only rendered when there are valid actions) ==========
|
|
233
238
|
if (validActions.length > 0) {
|
|
234
239
|
const rightSection = Row({
|
|
240
|
+
// Row defaults to width:"100%", which inside the flex header demands the
|
|
241
|
+
// whole bar width and overflows it. The zone must be content-sized.
|
|
242
|
+
width: "auto",
|
|
235
243
|
alignItems: "center",
|
|
236
244
|
justifyContent: "flex-end",
|
|
237
|
-
paddingRight: paddingRight,
|
|
238
245
|
gap: actionsGap,
|
|
239
246
|
minWidth: 48,
|
|
240
247
|
flexShrink: 0,
|
|
@@ -270,9 +277,15 @@ export const AppBar = (props) => {
|
|
|
270
277
|
lastScrollY = currentScrollY;
|
|
271
278
|
};
|
|
272
279
|
window.addEventListener("scroll", handleScroll);
|
|
273
|
-
//
|
|
274
|
-
|
|
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 = () =>
|
|
275
286
|
window.removeEventListener("scroll", handleScroll);
|
|
287
|
+
appBar._cleanupScroll = removeScroll;
|
|
288
|
+
appBar.onUnmount(removeScroll);
|
|
276
289
|
}
|
|
277
290
|
|
|
278
291
|
// ========== PUBLIC API ==========
|
|
@@ -285,10 +285,22 @@ export const BottomNavigation = (props) => {
|
|
|
285
285
|
return getCurrentPath();
|
|
286
286
|
};
|
|
287
287
|
|
|
288
|
-
// Cleanup:
|
|
288
|
+
// Cleanup: release the router subscription on EITHER teardown path. `_cleanup`
|
|
289
|
+
// is only run by runApp's teardownTree / Scaffold; the lifecycle observer that
|
|
290
|
+
// fires on a plain DOM disconnect never calls it, so without `onUnmount` the
|
|
291
|
+
// router kept invoking `updateColorsFromRouter()` (and `onTabChange`) on a
|
|
292
|
+
// detached bar forever. The disposer is idempotent because both paths can run.
|
|
293
|
+
let unsubscribed = false;
|
|
294
|
+
const dispose = () => {
|
|
295
|
+
if (unsubscribed) return;
|
|
296
|
+
unsubscribed = true;
|
|
297
|
+
if (unsubscribe) unsubscribe();
|
|
298
|
+
};
|
|
299
|
+
container.onUnmount(dispose);
|
|
300
|
+
|
|
289
301
|
const originalCleanup = container._cleanup;
|
|
290
302
|
container._cleanup = () => {
|
|
291
|
-
|
|
303
|
+
dispose();
|
|
292
304
|
if (originalCleanup) originalCleanup();
|
|
293
305
|
};
|
|
294
306
|
|
|
@@ -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
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Dynamic segments: `/user/:id`, `/post/:slug/comments`
|
|
8
8
|
* - Wildcard / 404 routes: `*` or `/404`
|
|
9
9
|
* - Query string parameters
|
|
10
|
+
* - Deployment base paths (`/repo-name/`) for static hosts like GitHub Pages
|
|
10
11
|
* - Browser history integration (pushState / popstate)
|
|
11
12
|
* - Observer pattern (subscribe/notify) so layout shells like Scaffold can
|
|
12
13
|
* re-render when the route changes.
|
|
@@ -42,6 +43,15 @@ let currentQuery = {};
|
|
|
42
43
|
/** @type {Function|null} Removes the popstate event listener; set during initRouter. */
|
|
43
44
|
let removePopStateListener = null;
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Deployment base path the app is served under, e.g. "/flet-box-docs".
|
|
48
|
+
* Empty string means the app owns the host root. Router state stays
|
|
49
|
+
* app-relative; the base is only added when writing browser URLs.
|
|
50
|
+
*
|
|
51
|
+
* @type {string}
|
|
52
|
+
*/
|
|
53
|
+
let basePath = "";
|
|
54
|
+
|
|
45
55
|
// ========== UTILITIES ==========
|
|
46
56
|
|
|
47
57
|
/**
|
|
@@ -132,6 +142,93 @@ const findMatchingRoute = (path) => {
|
|
|
132
142
|
return null;
|
|
133
143
|
};
|
|
134
144
|
|
|
145
|
+
// ========== DEPLOYMENT BASE PATH ==========
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Normalize a deployment base to "" or "/segment(s)" with no trailing slash.
|
|
149
|
+
*
|
|
150
|
+
* @param {string|null|undefined} base - e.g. "/", "flet-box-docs", "/flet-box-docs/".
|
|
151
|
+
* @returns {string} "" for a root-hosted app, otherwise a leading-slash prefix.
|
|
152
|
+
*/
|
|
153
|
+
const normalizeBase = (base) => {
|
|
154
|
+
if (!base) return "";
|
|
155
|
+
let out = String(base).trim();
|
|
156
|
+
if (!out.startsWith("/")) out = `/${out}`;
|
|
157
|
+
out = out.replace(/\/+$/, "");
|
|
158
|
+
return out === "/" ? "" : out;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build a browser URL from an app-relative path by prefixing the base.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} path - App-relative path, possibly with a query string.
|
|
165
|
+
* @returns {string} Path as seen by the browser.
|
|
166
|
+
*/
|
|
167
|
+
const withBase = (path) => `${basePath}${path}`;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Remove the deployment base from a browser URL path.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} path - Path as seen by the browser.
|
|
173
|
+
* @returns {string} App-relative path.
|
|
174
|
+
*/
|
|
175
|
+
const stripBase = (path) =>
|
|
176
|
+
basePath && path.startsWith(basePath)
|
|
177
|
+
? path.slice(basePath.length) || "/"
|
|
178
|
+
: path;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Work out which prefix of a browser path is the deployment base.
|
|
182
|
+
*
|
|
183
|
+
* Static hosts such as GitHub Pages serve a project site under
|
|
184
|
+
* `/<repo>/`, so `location.pathname` is `/<repo>/<route>` while the app's
|
|
185
|
+
* route map only knows `/<route>`. Scanning prefixes from the shortest one
|
|
186
|
+
* and returning the first whose remainder matches a registered route keeps
|
|
187
|
+
* root-hosted apps untouched (`base` resolves to "").
|
|
188
|
+
*
|
|
189
|
+
* @param {string} rawPath - Browser path, including any base prefix.
|
|
190
|
+
* @returns {string} Normalized base, or "" when no prefix yields a match.
|
|
191
|
+
*/
|
|
192
|
+
const detectBase = (rawPath) => {
|
|
193
|
+
const clean = cleanPath(rawPath);
|
|
194
|
+
if (findMatchingRoute(clean)) return "";
|
|
195
|
+
|
|
196
|
+
const segments = clean.split("/").filter(Boolean);
|
|
197
|
+
// i === segments.length covers the bare subpath root ("/repo/"), where the
|
|
198
|
+
// remainder is the app's own "/" route.
|
|
199
|
+
for (let i = 1; i <= segments.length; i++) {
|
|
200
|
+
const candidate = `/${segments.slice(0, i).join("/")}`;
|
|
201
|
+
const rest = segments.slice(i);
|
|
202
|
+
const remainder = rest.length ? `/${rest.join("/")}` : "/";
|
|
203
|
+
if (findMatchingRoute(remainder)) return normalizeBase(candidate);
|
|
204
|
+
}
|
|
205
|
+
return "";
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolve the deployment base for the current document.
|
|
210
|
+
*
|
|
211
|
+
* Precedence: explicit `base` option > `window.__FLETBOX_BASE__` (only when
|
|
212
|
+
* the live URL actually sits under it, so the same bundle works when
|
|
213
|
+
* packaged by Capacitor at the origin root) > auto-detection from the URL.
|
|
214
|
+
*
|
|
215
|
+
* @param {string|null|undefined} explicitBase - Base passed to initRouter.
|
|
216
|
+
* @param {string} rawPath - Browser path the app is being entered on.
|
|
217
|
+
* @returns {string} Normalized base.
|
|
218
|
+
*/
|
|
219
|
+
const resolveBase = (explicitBase, rawPath) => {
|
|
220
|
+
const explicit = normalizeBase(explicitBase);
|
|
221
|
+
if (explicit) return explicit;
|
|
222
|
+
|
|
223
|
+
if (typeof window !== "undefined") {
|
|
224
|
+
const declared = normalizeBase(window.__FLETBOX_BASE__);
|
|
225
|
+
if (declared && (rawPath === `${declared}/` || rawPath.startsWith(`${declared}/`))) {
|
|
226
|
+
return declared;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return detectBase(rawPath);
|
|
230
|
+
};
|
|
231
|
+
|
|
135
232
|
// ========== INITIALIZATION ==========
|
|
136
233
|
|
|
137
234
|
/**
|
|
@@ -146,8 +243,9 @@ const findMatchingRoute = (path) => {
|
|
|
146
243
|
*
|
|
147
244
|
* @param {Object.<string, *>} routesConfig - Map of path patterns to components/config.
|
|
148
245
|
* @param {string|null} [initialPath=null] - Override the starting path; defaults to window.location.pathname.
|
|
246
|
+
* @param {{base?: string}} [options={}] - `base` pins the deployment prefix; auto-detected when omitted.
|
|
149
247
|
*/
|
|
150
|
-
export const initRouter = (routesConfig, initialPath = null) => {
|
|
248
|
+
export const initRouter = (routesConfig, initialPath = null, options = {}) => {
|
|
151
249
|
routes = routesConfig;
|
|
152
250
|
|
|
153
251
|
// Prefer the explicit initialPath; fall back to the live browser URL so
|
|
@@ -157,6 +255,11 @@ export const initRouter = (routesConfig, initialPath = null) => {
|
|
|
157
255
|
urlPath = window.location.pathname;
|
|
158
256
|
}
|
|
159
257
|
|
|
258
|
+
// Peel the deployment prefix off the browser URL before matching so a site
|
|
259
|
+
// served under /repo-name/ resolves to its routes instead of redirecting.
|
|
260
|
+
basePath = resolveBase(options.base, urlPath || "/");
|
|
261
|
+
if (urlPath) urlPath = stripBase(urlPath);
|
|
262
|
+
|
|
160
263
|
// Last resort: use the first registered route or the root.
|
|
161
264
|
if (!urlPath) {
|
|
162
265
|
urlPath = Object.keys(routes)[0] || "/";
|
|
@@ -188,9 +291,13 @@ export const initRouter = (routesConfig, initialPath = null) => {
|
|
|
188
291
|
// redirected to the first route above).
|
|
189
292
|
if (
|
|
190
293
|
typeof window !== "undefined" &&
|
|
191
|
-
window.location.pathname !== currentPath
|
|
294
|
+
window.location.pathname !== withBase(currentPath)
|
|
192
295
|
) {
|
|
193
|
-
window.history.replaceState(
|
|
296
|
+
window.history.replaceState(
|
|
297
|
+
{ path: currentPath },
|
|
298
|
+
"",
|
|
299
|
+
withBase(currentPath),
|
|
300
|
+
);
|
|
194
301
|
}
|
|
195
302
|
|
|
196
303
|
// Listen for browser-native back/forward navigation so the router state
|
|
@@ -198,7 +305,8 @@ export const initRouter = (routesConfig, initialPath = null) => {
|
|
|
198
305
|
if (typeof window !== "undefined") {
|
|
199
306
|
if (removePopStateListener) removePopStateListener();
|
|
200
307
|
const handlePopState = (event) => {
|
|
201
|
-
const path =
|
|
308
|
+
const path =
|
|
309
|
+
event.state?.path || stripBase(window.location.pathname);
|
|
202
310
|
if (path !== currentPath) {
|
|
203
311
|
currentPath = path;
|
|
204
312
|
const match = findMatchingRoute(currentPath);
|
|
@@ -243,7 +351,7 @@ export const goTo = (path, params = {}) => {
|
|
|
243
351
|
currentQuery = { ...queryParams, ...params };
|
|
244
352
|
|
|
245
353
|
if (typeof window !== "undefined") {
|
|
246
|
-
window.history.pushState({ path: currentPath }, "", path);
|
|
354
|
+
window.history.pushState({ path: currentPath }, "", withBase(path));
|
|
247
355
|
}
|
|
248
356
|
notify();
|
|
249
357
|
} else {
|
|
@@ -278,7 +386,7 @@ export const replace = (path, params = {}) => {
|
|
|
278
386
|
currentQuery = { ...queryParams, ...params };
|
|
279
387
|
|
|
280
388
|
if (typeof window !== "undefined") {
|
|
281
|
-
window.history.replaceState({ path: currentPath }, "", path);
|
|
389
|
+
window.history.replaceState({ path: currentPath }, "", withBase(path));
|
|
282
390
|
}
|
|
283
391
|
notify();
|
|
284
392
|
}
|
|
@@ -316,6 +424,15 @@ export const goForward = () => {
|
|
|
316
424
|
*/
|
|
317
425
|
export const getCurrentPath = () => currentPath;
|
|
318
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Returns the deployment base the router resolved at init, e.g. "/flet-box-docs".
|
|
429
|
+
* Empty string for a root-hosted app. Use it to build asset or document URLs
|
|
430
|
+
* that must survive being served from a subpath.
|
|
431
|
+
*
|
|
432
|
+
* @returns {string}
|
|
433
|
+
*/
|
|
434
|
+
export const getBasePath = () => basePath;
|
|
435
|
+
|
|
319
436
|
/**
|
|
320
437
|
* Returns a shallow copy of the current dynamic route params.
|
|
321
438
|
* Mutations to the returned object do not affect the router state.
|
|
@@ -454,6 +571,7 @@ export const clearRouter = () => {
|
|
|
454
571
|
listeners = [];
|
|
455
572
|
currentParams = {};
|
|
456
573
|
currentQuery = {};
|
|
574
|
+
basePath = "";
|
|
457
575
|
};
|
|
458
576
|
|
|
459
577
|
export default {
|
|
@@ -463,6 +581,7 @@ export default {
|
|
|
463
581
|
goBack,
|
|
464
582
|
goForward,
|
|
465
583
|
getCurrentPath,
|
|
584
|
+
getBasePath,
|
|
466
585
|
getCurrentRoute,
|
|
467
586
|
getCurrentRouteConfig,
|
|
468
587
|
getRoute,
|
|
@@ -18,6 +18,7 @@ import { initRouter, getCurrentRoute, subscribe } from "./Router.js";
|
|
|
18
18
|
* @property {number} [navSideBarWidth=260] - Legacy alias for leftNavBarWidth (deprecated).
|
|
19
19
|
* @property {"left"|"right"} [navSideBarPosition="left"] - Legacy: which side navSideBar renders on.
|
|
20
20
|
* @property {Object} [routes] - Route map object for router mode (path → component).
|
|
21
|
+
* @property {string} [basePath] - Deployment prefix, e.g. "/flet-box-docs". Auto-detected when omitted.
|
|
21
22
|
* @property {string} [backgroundColor] - Background color of the scaffold shell.
|
|
22
23
|
* @property {boolean} [closeDrawerOnNavigate=false] - Whether to close the drawer automatically on route change.
|
|
23
24
|
*/
|
|
@@ -54,6 +55,7 @@ export const Scaffold = (props) => {
|
|
|
54
55
|
navSideBarWidth = 260,
|
|
55
56
|
navSideBarPosition = "left",
|
|
56
57
|
routes,
|
|
58
|
+
basePath,
|
|
57
59
|
backgroundColor = colors.background,
|
|
58
60
|
closeDrawerOnNavigate = false,
|
|
59
61
|
...rest
|
|
@@ -81,11 +83,12 @@ export const Scaffold = (props) => {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
// Bootstrap the router with the current browser URL so deep-links work
|
|
84
|
-
// on first load without redirecting to "/".
|
|
86
|
+
// on first load without redirecting to "/". `basePath` pins the deployment
|
|
87
|
+
// prefix when the app is served from a subpath (e.g. GitHub Pages).
|
|
85
88
|
if (isRouterMode && routesConfig) {
|
|
86
89
|
const currentUrl =
|
|
87
90
|
typeof window !== "undefined" ? window.location.pathname : "/";
|
|
88
|
-
initRouter(routesConfig, currentUrl);
|
|
91
|
+
initRouter(routesConfig, currentUrl, { base: basePath });
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
// Resolve the legacy navSideBar prop to the appropriate side.
|
|
@@ -491,8 +494,15 @@ export const Scaffold = (props) => {
|
|
|
491
494
|
clearContainer(fabContainer);
|
|
492
495
|
clearContainer(leftNavBarContainer);
|
|
493
496
|
clearContainer(rightNavBarContainer);
|
|
494
|
-
|
|
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) {
|
|
495
504
|
container._drawer.parentNode.removeChild(container._drawer);
|
|
505
|
+
}
|
|
496
506
|
if (originalCleanup) originalCleanup();
|
|
497
507
|
};
|
|
498
508
|
|
package/src/navigations/Tabs.js
CHANGED
|
@@ -318,6 +318,11 @@ export const Tabs = (props) => {
|
|
|
318
318
|
updateContent();
|
|
319
319
|
};
|
|
320
320
|
|
|
321
|
+
// Mount the initially active pane during construction. Without this the pane
|
|
322
|
+
// area stayed empty until the user clicked a *different* tab, even though the
|
|
323
|
+
// first tab already rendered as `aria-selected="true"`.
|
|
324
|
+
updateContent();
|
|
325
|
+
|
|
321
326
|
// Defer the initial slider position calculation by one frame so the DOM
|
|
322
327
|
// has been laid out and getBoundingClientRect() returns real values.
|
|
323
328
|
let initialPositionTimer = setTimeout(() => {
|
|
@@ -341,18 +346,41 @@ export const Tabs = (props) => {
|
|
|
341
346
|
|
|
342
347
|
// Also observe the tab bar element itself so the thumb repositions when
|
|
343
348
|
// the container resizes independently of the window (e.g. sidebar toggle).
|
|
344
|
-
|
|
349
|
+
// Only the slider variant has a thumb, so skip it for the other variants.
|
|
350
|
+
if (variant === "slider" && typeof ResizeObserver !== "undefined") {
|
|
345
351
|
resizeObserver = new ResizeObserver(() => updateSliderPosition());
|
|
346
352
|
if (tabsWrapperRef) resizeObserver.observe(tabsWrapperRef);
|
|
347
353
|
}
|
|
348
354
|
|
|
349
|
-
// Cleanup:
|
|
355
|
+
// Cleanup: release the timers, the window listener and the observer on EITHER
|
|
356
|
+
// teardown path. `_cleanup` is only run by runApp's teardownTree / Scaffold; a
|
|
357
|
+
// plain DOM disconnect goes through the lifecycle observer, which never calls
|
|
358
|
+
// it — so without `onUnmount` the resize listener stayed registered and the
|
|
359
|
+
// ResizeObserver kept observing (and retaining) a detached tab bar. The
|
|
360
|
+
// disposer is idempotent because both paths can run.
|
|
361
|
+
let disposed = false;
|
|
362
|
+
const dispose = () => {
|
|
363
|
+
if (disposed) return;
|
|
364
|
+
disposed = true;
|
|
365
|
+
if (initialPositionTimer) {
|
|
366
|
+
clearTimeout(initialPositionTimer);
|
|
367
|
+
initialPositionTimer = null;
|
|
368
|
+
}
|
|
369
|
+
if (resizeTimeout) {
|
|
370
|
+
clearTimeout(resizeTimeout);
|
|
371
|
+
resizeTimeout = null;
|
|
372
|
+
}
|
|
373
|
+
window.removeEventListener("resize", handleResize);
|
|
374
|
+
if (resizeObserver) {
|
|
375
|
+
resizeObserver.disconnect();
|
|
376
|
+
resizeObserver = null;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
container.onUnmount(dispose);
|
|
380
|
+
|
|
350
381
|
const originalCleanup = container._cleanup;
|
|
351
382
|
container._cleanup = () => {
|
|
352
|
-
|
|
353
|
-
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
354
|
-
window.removeEventListener("resize", handleResize);
|
|
355
|
-
if (resizeObserver) resizeObserver.disconnect();
|
|
383
|
+
dispose();
|
|
356
384
|
if (originalCleanup) originalCleanup();
|
|
357
385
|
};
|
|
358
386
|
|
package/src/navigations/index.js
CHANGED
|
@@ -36,6 +36,8 @@ export {
|
|
|
36
36
|
replace,
|
|
37
37
|
/** Returns the currently active path. */
|
|
38
38
|
getCurrentPath,
|
|
39
|
+
/** Returns the deployment base path, e.g. "/flet-box-docs". */
|
|
40
|
+
getBasePath,
|
|
39
41
|
/** Returns the component/config for the current route. */
|
|
40
42
|
getCurrentRoute,
|
|
41
43
|
/** Returns the full route state (route, params, query, path). */
|
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.
|