flet-box 1.0.2 → 1.0.3

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 CHANGED
@@ -331,7 +331,12 @@ async function checkAndUpdate(shippedConfig, activeHash) {
331
331
  (async function main() {
332
332
  let shippedConfig = null;
333
333
  try {
334
- shippedConfig = await fetchJson("./config.json");
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
  }
@@ -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 = ['/', '/index.html', '/loader.js', '/config.json', '/src/app.js'];
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('/index.html');
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.2",
3
+ "version": "1.0.3",
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",
@@ -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
 
package/src/index.d.ts CHANGED
@@ -977,12 +977,13 @@ declare module 'flet-box' {
977
977
  // =========================================================================
978
978
  // Router
979
979
  // =========================================================================
980
- export function initRouter(routes: Record<string, any>, initialPath?: string): void;
980
+ export function initRouter(routes: Record<string, any>, initialPath?: string, options?: { base?: string }): void;
981
981
  export function goTo(path: string, params?: Record<string, any>): void;
982
982
  export function goBack(): void;
983
983
  export function goForward(): void;
984
984
  export function replace(path: string, params?: Record<string, any>): void;
985
985
  export function getCurrentPath(): string;
986
+ export function getBasePath(): string;
986
987
  export function getCurrentRoute(): any;
987
988
  export function getCurrentRouteConfig(): { route: any; params: Record<string, any>; query: Record<string, any>; path: string };
988
989
  export function getRoute(path: string): any;
package/src/index.js CHANGED
@@ -208,6 +208,7 @@ export {
208
208
  goForward,
209
209
  replace,
210
210
  getCurrentPath,
211
+ getBasePath,
211
212
  getCurrentRoute,
212
213
  getCurrentRouteConfig,
213
214
  getRoute,
@@ -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: centerTitle ? 1 : "none",
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,
@@ -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({ path: currentPath }, "", currentPath);
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 = event.state?.path || window.location.pathname;
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.
@@ -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). */