flet-box 0.0.0-stage → 1.0.2
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/LICENSE +21 -0
- package/README.md +190 -2
- package/bin/cli.js +394 -0
- package/bin/commands/buildAndroid.js +200 -0
- package/bin/commands/buildIOS.js +237 -0
- package/bin/commands/create.js +406 -0
- package/bin/commands/createBundle.js +375 -0
- package/bin/commands/createComponent.js +115 -0
- package/bin/commands/createScreen.js +161 -0
- package/bin/commands/killServer.js +107 -0
- package/bin/commands/package.js +131 -0
- package/bin/commands/release.js +189 -0
- package/bin/commands/runBundle.js +62 -0
- package/bin/commands/runCloudflare.js +89 -0
- package/bin/commands/runNative.js +109 -0
- package/bin/commands/runServer.js +390 -0
- package/bin/ota/loader.js +361 -0
- package/bin/utils/bundleConfig.js +178 -0
- package/bin/utils/capacitor.js +137 -0
- package/bin/utils/colors.js +215 -0
- package/bin/utils/helpers.js +80 -0
- package/bin/utils/spinner.js +96 -0
- package/bin/utils/templates.js +1824 -0
- package/bin/utils/version.js +108 -0
- package/package.json +58 -4
- package/src/animations/AnimatedBox.js +607 -0
- package/src/animations/AnimatedText.js +105 -0
- package/src/animations/MatrixRain.js +220 -0
- package/src/animations/ParallaxBox.js +369 -0
- package/src/animations/animate.js +332 -0
- package/src/animations/index.js +25 -0
- package/src/app.js +41 -0
- package/src/assets/icon-1024-maskable.png +0 -0
- package/src/assets/icon-192-maskable.png +0 -0
- package/src/assets/icon-192.png +0 -0
- package/src/assets/icon-512-maskable.png +0 -0
- package/src/assets/icon-512.png +0 -0
- package/src/assets/logo.png +0 -0
- package/src/core/App.js +44 -0
- package/src/core/hmr-client.js +142 -0
- package/src/core/index.js +34 -0
- package/src/core/pwa.js +100 -0
- package/src/core/runApp.js +430 -0
- package/src/create-index.sh +237 -0
- package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
- package/src/fonts/icons.css +23 -0
- package/src/index.d.ts +1930 -0
- package/src/index.js +288 -0
- package/src/navigations/AdaptiveScaffold.js +149 -0
- package/src/navigations/AppBar.js +331 -0
- package/src/navigations/BottomNavigation.js +298 -0
- package/src/navigations/CollapsibleSideBar.js +139 -0
- package/src/navigations/Drawer.js +333 -0
- package/src/navigations/DrawerItem.js +260 -0
- package/src/navigations/Router.js +475 -0
- package/src/navigations/Scaffold.js +537 -0
- package/src/navigations/Tabs.js +384 -0
- package/src/navigations/index.js +57 -0
- package/src/services/RamStore.js +213 -0
- package/src/services/Session.js +310 -0
- package/src/services/Storage.js +309 -0
- package/src/services/http.js +350 -0
- package/src/services/index.js +57 -0
- package/src/tools/animation.js +42 -0
- package/src/tools/array.js +95 -0
- package/src/tools/border.js +28 -0
- package/src/tools/clipboard.js +104 -0
- package/src/tools/color.js +329 -0
- package/src/tools/createList.js +215 -0
- package/src/tools/delay.js +55 -0
- package/src/tools/device.js +138 -0
- package/src/tools/dict.js +297 -0
- package/src/tools/dimensions.js +118 -0
- package/src/tools/filter.js +41 -0
- package/src/tools/flex.js +26 -0
- package/src/tools/gradient.js +34 -0
- package/src/tools/grid.js +57 -0
- package/src/tools/index.js +38 -0
- package/src/tools/mapList.js +107 -0
- package/src/tools/margin.js +93 -0
- package/src/tools/memo.js +60 -0
- package/src/tools/os.js +104 -0
- package/src/tools/padding.js +93 -0
- package/src/tools/print.js +69 -0
- package/src/tools/random.js +299 -0
- package/src/tools/ref.js +70 -0
- package/src/tools/rgba.js +4 -0
- package/src/tools/shadow.js +23 -0
- package/src/tools/string.js +85 -0
- package/src/tools/time.js +674 -0
- package/src/tools/transform.js +68 -0
- package/src/tools/transition.js +48 -0
- package/src/tools/useState.js +268 -0
- package/src/tools/uuid.js +42 -0
- package/src/utils/TextInputValidator.js +256 -0
- package/src/utils/composeUpdate.js +65 -0
- package/src/utils/getWidgetProps.js +35 -0
- package/src/utils/index.js +76 -0
- package/src/utils/markdownParser.js +368 -0
- package/src/utils/mediaTime.js +79 -0
- package/src/utils/navigation.js +263 -0
- package/src/utils/scriptInjector.js +73 -0
- package/src/utils/stackPosition.js +31 -0
- package/src/utils/stopWebRefresh.js +77 -0
- package/src/utils/styleInjector.js +38 -0
- package/src/utils/syntaxHighlight.js +339 -0
- package/src/utils/themes.js +347 -0
- package/src/utils/units.js +87 -0
- package/src/utils/visualEffects.js +169 -0
- package/src/widget-factory/addChildren.js +114 -0
- package/src/widget-factory/assignProps.js +256 -0
- package/src/widget-factory/createWidget.js +28 -0
- package/src/widget-factory/effects.js +132 -0
- package/src/widget-factory/index.js +7 -0
- package/src/widget-factory/lifecycle.js +210 -0
- package/src/widget-factory/makeParentable.js +89 -0
- package/src/widget-factory/pixTool.js +40 -0
- package/src/widget-factory/processProps.js +122 -0
- package/src/widget-factory/reactivity.js +98 -0
- package/src/widget-factory/remTool.js +34 -0
- package/src/widget-factory/stackPosition.js +87 -0
- package/src/widget-factory/stringTool.js +26 -0
- package/src/widget-factory/tools.js +249 -0
- package/src/widget-factory/translateProps.js +1049 -0
- package/src/widget-factory/warnings.js +284 -0
- package/src/widget-factory/widgetFactory.js +200 -0
- package/src/widgets/Accordion.js +562 -0
- package/src/widgets/AdMob.js +315 -0
- package/src/widgets/AdSense.js +268 -0
- package/src/widgets/AlertDialog.js +162 -0
- package/src/widgets/Audio.js +311 -0
- package/src/widgets/Avatar.js +210 -0
- package/src/widgets/Badge.js +189 -0
- package/src/widgets/BottomSheet.js +287 -0
- package/src/widgets/Button.js +247 -0
- package/src/widgets/Card.js +33 -0
- package/src/widgets/Carousel.js +301 -0
- package/src/widgets/Chart.js +632 -0
- package/src/widgets/Checkbox.js +106 -0
- package/src/widgets/Chip.js +153 -0
- package/src/widgets/CircularBar.js +352 -0
- package/src/widgets/CircularChart.js +549 -0
- package/src/widgets/CodeViewer.js +236 -0
- package/src/widgets/Column.js +29 -0
- package/src/widgets/Container.js +31 -0
- package/src/widgets/DataTable.js +257 -0
- package/src/widgets/Divider.js +93 -0
- package/src/widgets/DraggBox.js +115 -0
- package/src/widgets/Dropdown.js +505 -0
- package/src/widgets/DroppBox.js +180 -0
- package/src/widgets/FloatingActionButton.js +91 -0
- package/src/widgets/GridView.js +20 -0
- package/src/widgets/Icon.js +33 -0
- package/src/widgets/Image.js +15 -0
- package/src/widgets/Input.js +565 -0
- package/src/widgets/Inspector.js +254 -0
- package/src/widgets/InstallButton.js +110 -0
- package/src/widgets/ListTile.js +224 -0
- package/src/widgets/ListView.js +503 -0
- package/src/widgets/Markdown.js +1236 -0
- package/src/widgets/Modal.js +496 -0
- package/src/widgets/Pagination.js +195 -0
- package/src/widgets/ProgressBar.js +178 -0
- package/src/widgets/QRCode.js +467 -0
- package/src/widgets/Radio.js +132 -0
- package/src/widgets/Rating.js +261 -0
- package/src/widgets/Row.js +29 -0
- package/src/widgets/Skeleton.js +243 -0
- package/src/widgets/Slider.js +527 -0
- package/src/widgets/SnackBar.js +227 -0
- package/src/widgets/Stack.js +29 -0
- package/src/widgets/Stepper.js +296 -0
- package/src/widgets/Switch.js +102 -0
- package/src/widgets/Text.js +98 -0
- package/src/widgets/Tooltip.js +267 -0
- package/src/widgets/TreeView.js +370 -0
- package/src/widgets/Video.js +92 -0
- package/src/widgets/index.js +52 -0
package/src/core/pwa.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/core/pwa.js - PWA utility functions ready to be wired to onPress handlers
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Holds the deferred `beforeinstallprompt` event so it can be triggered later
|
|
5
|
+
* by `installPWA`. Set to `null` when no install prompt is available (e.g. the
|
|
6
|
+
* app is already installed or the browser does not support PWA installation).
|
|
7
|
+
*
|
|
8
|
+
* @type {Event|null}
|
|
9
|
+
*/
|
|
10
|
+
let deferredPrompt = null;
|
|
11
|
+
|
|
12
|
+
// Listen for the install prompt event.
|
|
13
|
+
// The guard keeps the module safely importable in Node / SSR / test environments
|
|
14
|
+
// where `window` is not defined.
|
|
15
|
+
if (typeof window !== "undefined") {
|
|
16
|
+
window.addEventListener("beforeinstallprompt", (e) => {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
deferredPrompt = e;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns whether the PWA is currently running in standalone (installed) mode.
|
|
24
|
+
*
|
|
25
|
+
* Uses the `display-mode: standalone` CSS media query, which matches when the
|
|
26
|
+
* app was launched from the home screen or app drawer rather than from a
|
|
27
|
+
* browser tab.
|
|
28
|
+
*
|
|
29
|
+
* @returns {boolean} `true` if the app is running as an installed PWA.
|
|
30
|
+
*/
|
|
31
|
+
export const isPWAInstalled = () => {
|
|
32
|
+
return window.matchMedia("(display-mode: standalone)").matches;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Triggers the browser's native PWA installation prompt.
|
|
37
|
+
*
|
|
38
|
+
* Can be wired directly to an `onPress` handler. The prompt is only shown when
|
|
39
|
+
* the browser previously fired `beforeinstallprompt` and the user has not
|
|
40
|
+
* already installed the app.
|
|
41
|
+
*
|
|
42
|
+
* @returns {boolean} `true` if the install prompt was shown; `false` if no
|
|
43
|
+
* deferred prompt was available (app already installed or browser does not
|
|
44
|
+
* support installation).
|
|
45
|
+
*/
|
|
46
|
+
export const installPWA = () => {
|
|
47
|
+
if (deferredPrompt) {
|
|
48
|
+
deferredPrompt.prompt();
|
|
49
|
+
deferredPrompt = null;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
alert("Install PWA not available");
|
|
53
|
+
return false;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Checks for a pending Service Worker update and prompts the user to refresh.
|
|
58
|
+
*
|
|
59
|
+
* Can be wired directly to an `onPress` handler. Calls
|
|
60
|
+
* `ServiceWorkerRegistration.update()` on the active registration, then alerts
|
|
61
|
+
* the user that a refresh is needed to apply the update.
|
|
62
|
+
*
|
|
63
|
+
* @returns {boolean} `true` if a Service Worker registration was found and
|
|
64
|
+
* `update()` was called; `false` if Service Workers are not supported in the
|
|
65
|
+
* current browser.
|
|
66
|
+
*/
|
|
67
|
+
export const updatePWA = () => {
|
|
68
|
+
if (navigator.serviceWorker) {
|
|
69
|
+
navigator.serviceWorker.getRegistration().then((reg) => {
|
|
70
|
+
if (reg) {
|
|
71
|
+
reg.update();
|
|
72
|
+
alert("Update available. Refresh to apply.");
|
|
73
|
+
} else {
|
|
74
|
+
alert("No Service Worker found");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
alert("Service Worker not supported");
|
|
80
|
+
return false;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Guides the user through uninstalling the PWA.
|
|
85
|
+
*
|
|
86
|
+
* On Chrome, opens `chrome://apps` in a new tab where the user can remove the
|
|
87
|
+
* app. On other browsers, falls back to a generic instruction to use the
|
|
88
|
+
* browser's own settings.
|
|
89
|
+
*
|
|
90
|
+
* Can be wired directly to an `onPress` handler.
|
|
91
|
+
*/
|
|
92
|
+
export const removePWA = () => {
|
|
93
|
+
if (navigator.userAgent.includes("Chrome")) {
|
|
94
|
+
window.open("chrome://apps", "_blank");
|
|
95
|
+
} else {
|
|
96
|
+
alert("Use your browser settings to uninstall");
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default { installPWA, updatePWA, removePWA, isPWAInstalled };
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// core/runApp.js
|
|
2
|
+
import { stopWebRefresh } from "../utils/stopWebRefresh.js";
|
|
3
|
+
import { initRouter } from "../navigations/Router.js"; // ← IMPORTANT: add this line
|
|
4
|
+
import {
|
|
5
|
+
applySystemTheme,
|
|
6
|
+
watchSystemTheme,
|
|
7
|
+
colors,
|
|
8
|
+
subscribeTheme,
|
|
9
|
+
} from "../utils/themes.js";
|
|
10
|
+
import { Container } from "../widgets/Container.js";
|
|
11
|
+
import { setGlobalRender } from "../tools/useState.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mounts a FletBox application into the DOM and manages its full lifecycle.
|
|
15
|
+
*
|
|
16
|
+
* Sets up the root container, applies the system theme, initialises the router
|
|
17
|
+
* (when routes are provided), and subscribes to theme changes so the app
|
|
18
|
+
* re-renders automatically whenever the user switches between light/dark mode.
|
|
19
|
+
*
|
|
20
|
+
* @param {Function|HTMLElement} App - The root component factory function, or a
|
|
21
|
+
* pre-built HTMLElement to mount directly.
|
|
22
|
+
* @param {string} [rootId="root"] - The id of the DOM element that will host
|
|
23
|
+
* the application.
|
|
24
|
+
* @param {boolean} [preventRefresh=true] - When true, browser-level page refresh
|
|
25
|
+
* is suppressed (feature currently disabled but kept for API compatibility).
|
|
26
|
+
* @param {Object|null} [routes=null] - Route map passed to `initRouter`. When
|
|
27
|
+
* null, no router is initialised.
|
|
28
|
+
* @returns {{ destroy: Function }} A handle with a `destroy()` method that
|
|
29
|
+
* tears down the app cleanly (unsubscribes theme listeners, removes DOM nodes).
|
|
30
|
+
*/
|
|
31
|
+
export const runApp = (
|
|
32
|
+
App,
|
|
33
|
+
rootId = "root",
|
|
34
|
+
preventRefresh = true,
|
|
35
|
+
routes = null,
|
|
36
|
+
) => {
|
|
37
|
+
const root = document.getElementById(rootId);
|
|
38
|
+
if (!root) return;
|
|
39
|
+
|
|
40
|
+
// If preventRefresh is true (default), we disable refreshes
|
|
41
|
+
// if (preventRefresh) {
|
|
42
|
+
// stopWebRefresh();
|
|
43
|
+
// }
|
|
44
|
+
|
|
45
|
+
let currentMainContainer = null;
|
|
46
|
+
let unsubscribeTheme = null;
|
|
47
|
+
let unwatchSystemTheme = null;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Recursively releases the resources of the entire widget tree before
|
|
51
|
+
* rebuilding it.
|
|
52
|
+
*
|
|
53
|
+
* The core `_cleanup` method is NOT recursive and the MutationObserver does
|
|
54
|
+
* not invoke it, so without this sweep each re-render (setState / theme
|
|
55
|
+
* change) would leak listeners, rAF handles, and timers from descendant
|
|
56
|
+
* widgets. It also fires `onUnmount` handlers (Slider/ListView release
|
|
57
|
+
* there) before each widget's own `_cleanup`.
|
|
58
|
+
*
|
|
59
|
+
* @param {HTMLElement|null} rootEl - The root element of the tree to tear down.
|
|
60
|
+
*/
|
|
61
|
+
const teardownTree = (rootEl) => {
|
|
62
|
+
if (!rootEl) return;
|
|
63
|
+
const walk = (el) => {
|
|
64
|
+
const kids = el.children ? Array.from(el.children) : [];
|
|
65
|
+
for (const kid of kids) walk(kid);
|
|
66
|
+
if (Array.isArray(el._unmountFns) && el._unmountFns.length) {
|
|
67
|
+
const fns = el._unmountFns;
|
|
68
|
+
el._unmountFns = [];
|
|
69
|
+
for (const fn of fns) {
|
|
70
|
+
try {
|
|
71
|
+
fn(el);
|
|
72
|
+
} catch (_) {}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (typeof el._cleanup === "function") {
|
|
76
|
+
try {
|
|
77
|
+
el._cleanup();
|
|
78
|
+
} catch (_) {}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
walk(rootEl);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Records which element holds focus inside a tree, so focus and the text caret
|
|
86
|
+
* can be handed back to the rebuilt tree.
|
|
87
|
+
*
|
|
88
|
+
* `renderApp` discards the whole tree on every state change, and removing a
|
|
89
|
+
* focused element drops focus to `<body>`. That made typing impossible in a
|
|
90
|
+
* controlled `<input>`: one keystroke fired `setState`, the rebuild removed the
|
|
91
|
+
* node, and focus was gone before the second character could be entered.
|
|
92
|
+
*
|
|
93
|
+
* The position of the element is recorded as the chain of child indices from
|
|
94
|
+
* `treeRoot` down to it, which is stable as long as the shape of the tree does
|
|
95
|
+
* not change between renders.
|
|
96
|
+
*
|
|
97
|
+
* @param {HTMLElement} treeRoot - Root of the tree about to be discarded.
|
|
98
|
+
* @returns {{ path: number[], tagName: string, start: number|null, end: number|null } | null}
|
|
99
|
+
* Snapshot to pass to {@link restoreFocus}, or null when focus is elsewhere.
|
|
100
|
+
*/
|
|
101
|
+
const captureFocus = (treeRoot) => {
|
|
102
|
+
if (!treeRoot || typeof document === "undefined") return null;
|
|
103
|
+
const active = document.activeElement;
|
|
104
|
+
if (!active || active === document.body) return null;
|
|
105
|
+
if (!treeRoot.contains(active)) return null;
|
|
106
|
+
|
|
107
|
+
const path = [];
|
|
108
|
+
let node = active;
|
|
109
|
+
while (node && node !== treeRoot) {
|
|
110
|
+
const parent = node.parentNode;
|
|
111
|
+
if (!parent) return null;
|
|
112
|
+
path.unshift(Array.prototype.indexOf.call(parent.children, node));
|
|
113
|
+
node = parent;
|
|
114
|
+
}
|
|
115
|
+
if (node !== treeRoot) return null;
|
|
116
|
+
|
|
117
|
+
// Only <input>/<textarea>/<select> expose a selection; other elements (and
|
|
118
|
+
// inputs like checkbox or number) throw or return null, so guard on type.
|
|
119
|
+
const selectable =
|
|
120
|
+
active instanceof HTMLInputElement &&
|
|
121
|
+
!["checkbox", "radio", "range", "color", "file", "button", "submit", "reset"].includes(
|
|
122
|
+
active.type,
|
|
123
|
+
);
|
|
124
|
+
let start = null;
|
|
125
|
+
let end = null;
|
|
126
|
+
if (selectable || active instanceof HTMLTextAreaElement) {
|
|
127
|
+
try {
|
|
128
|
+
start = active.selectionStart;
|
|
129
|
+
end = active.selectionEnd;
|
|
130
|
+
} catch (_) {
|
|
131
|
+
start = null;
|
|
132
|
+
end = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { path, tagName: active.tagName, start, end };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Gives focus back to the element matching a {@link captureFocus} snapshot and
|
|
141
|
+
* restores its caret. A no-op when the rebuilt tree no longer has a focusable
|
|
142
|
+
* element at that position, which is the safe outcome after a route change.
|
|
143
|
+
*
|
|
144
|
+
* @param {{ path: number[], tagName: string, start: number|null, end: number|null } | null} snap
|
|
145
|
+
* @param {HTMLElement} treeRoot - Root of the freshly built tree.
|
|
146
|
+
* @returns {void}
|
|
147
|
+
*/
|
|
148
|
+
const restoreFocus = (snap, treeRoot) => {
|
|
149
|
+
if (!snap || !treeRoot) return;
|
|
150
|
+
let node = treeRoot;
|
|
151
|
+
for (const index of snap.path) {
|
|
152
|
+
node = node && node.children ? node.children[index] : null;
|
|
153
|
+
if (!node) return;
|
|
154
|
+
}
|
|
155
|
+
if (!node || node.tagName !== snap.tagName) return;
|
|
156
|
+
if (typeof node.focus !== "function") return;
|
|
157
|
+
|
|
158
|
+
node.focus({ preventScroll: true });
|
|
159
|
+
if (snap.start !== null && typeof node.setSelectionRange === "function") {
|
|
160
|
+
try {
|
|
161
|
+
node.setSelectionRange(snap.start, snap.end ?? snap.start);
|
|
162
|
+
} catch (_) {
|
|
163
|
+
/* element does not support a selection after all */
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Builds the application tree and appends it to the root DOM node.
|
|
170
|
+
*
|
|
171
|
+
* Tears down the previous container (if any) before creating the new one so
|
|
172
|
+
* that every re-render starts from a clean state.
|
|
173
|
+
*/
|
|
174
|
+
const renderApp = () => {
|
|
175
|
+
const focusSnapshot = currentMainContainer
|
|
176
|
+
? captureFocus(currentMainContainer)
|
|
177
|
+
: null;
|
|
178
|
+
|
|
179
|
+
if (currentMainContainer) {
|
|
180
|
+
teardownTree(currentMainContainer);
|
|
181
|
+
if (currentMainContainer.parentNode) currentMainContainer.remove();
|
|
182
|
+
currentMainContainer = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const appContent = typeof App === "function" ? App() : App;
|
|
186
|
+
|
|
187
|
+
const mainContainer = Container({
|
|
188
|
+
margin: 0,
|
|
189
|
+
padding: 0,
|
|
190
|
+
width: "100vw", // ← 100% of the viewport
|
|
191
|
+
minHeight: "100vh", // ← 100% of the viewport
|
|
192
|
+
display: "flex",
|
|
193
|
+
flexDirection: "column",
|
|
194
|
+
backgroundColor: colors.background,
|
|
195
|
+
color: colors.text,
|
|
196
|
+
transition: "background-color 0.3s ease, color 0.3s ease",
|
|
197
|
+
child: appContent,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
root.appendChild(mainContainer);
|
|
201
|
+
if (mainContainer.triggerMount) mainContainer.triggerMount();
|
|
202
|
+
currentMainContainer = mainContainer;
|
|
203
|
+
|
|
204
|
+
restoreFocus(focusSnapshot, mainContainer);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Register renderApp as the global re-render callback so that setState calls
|
|
208
|
+
// anywhere in the tree can trigger a full rebuild.
|
|
209
|
+
setGlobalRender(() => {
|
|
210
|
+
renderApp();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
applySystemTheme();
|
|
214
|
+
unwatchSystemTheme = watchSystemTheme();
|
|
215
|
+
|
|
216
|
+
root.style.margin = "0";
|
|
217
|
+
root.style.padding = "0";
|
|
218
|
+
root.style.width = "100vw"; // ← 100% of the viewport
|
|
219
|
+
root.style.minHeight = "100vh"; // ← 100% of the viewport
|
|
220
|
+
root.style.display = "flex";
|
|
221
|
+
root.style.flexDirection = "column";
|
|
222
|
+
root.style.backgroundColor = colors.background;
|
|
223
|
+
root.style.color = colors.text;
|
|
224
|
+
root.style.transition = "background-color 0.3s ease, color 0.3s ease";
|
|
225
|
+
|
|
226
|
+
// Release any previous tree (e.g. from a prior runApp call or HMR) before clearing.
|
|
227
|
+
Array.from(root.children).forEach((child) => teardownTree(child));
|
|
228
|
+
root.innerHTML = "";
|
|
229
|
+
|
|
230
|
+
// 🔥 INITIALIZE ROUTER if routes are provided
|
|
231
|
+
if (routes) {
|
|
232
|
+
initRouter(routes);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
renderApp();
|
|
236
|
+
|
|
237
|
+
// The theme subscription lives in runApp's scope (NOT attached to a
|
|
238
|
+
// transient container), because renderApp() rebuilds the container on every
|
|
239
|
+
// change. If it were attached to the first container it would auto-cancel
|
|
240
|
+
// after a single use.
|
|
241
|
+
unsubscribeTheme = subscribeTheme(() => {
|
|
242
|
+
root.style.backgroundColor = colors.background;
|
|
243
|
+
root.style.color = colors.text;
|
|
244
|
+
renderApp();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Teardown handle — lets callers unmount the app cleanly.
|
|
248
|
+
return {
|
|
249
|
+
/**
|
|
250
|
+
* Unmounts the application: cancels theme subscriptions, stops the system
|
|
251
|
+
* theme watcher, and removes the container from the DOM.
|
|
252
|
+
*/
|
|
253
|
+
destroy: () => {
|
|
254
|
+
if (unsubscribeTheme) {
|
|
255
|
+
unsubscribeTheme();
|
|
256
|
+
unsubscribeTheme = null;
|
|
257
|
+
}
|
|
258
|
+
if (unwatchSystemTheme) {
|
|
259
|
+
unwatchSystemTheme();
|
|
260
|
+
unwatchSystemTheme = null;
|
|
261
|
+
}
|
|
262
|
+
if (currentMainContainer) {
|
|
263
|
+
teardownTree(currentMainContainer);
|
|
264
|
+
if (currentMainContainer.parentNode) currentMainContainer.remove();
|
|
265
|
+
currentMainContainer = null;
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Appends a widget to the end of a root DOM element and triggers its mount
|
|
273
|
+
* lifecycle.
|
|
274
|
+
*
|
|
275
|
+
* @param {HTMLElement} widget - The widget element to insert.
|
|
276
|
+
* @param {string} [rootId="root"] - The id of the container element.
|
|
277
|
+
* @returns {HTMLElement} The same widget that was passed in.
|
|
278
|
+
*/
|
|
279
|
+
export const insertBy = (widget, rootId = "root") => {
|
|
280
|
+
const root = document.getElementById(rootId);
|
|
281
|
+
if (root && widget instanceof HTMLElement) {
|
|
282
|
+
root.appendChild(widget);
|
|
283
|
+
if (widget.triggerMount) widget.triggerMount();
|
|
284
|
+
}
|
|
285
|
+
return widget;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Prepends a widget to the beginning of a root DOM element and triggers its
|
|
290
|
+
* mount lifecycle.
|
|
291
|
+
*
|
|
292
|
+
* @param {HTMLElement} widget - The widget element to prepend.
|
|
293
|
+
* @param {string} [rootId="root"] - The id of the container element.
|
|
294
|
+
* @returns {HTMLElement} The same widget that was passed in.
|
|
295
|
+
*/
|
|
296
|
+
export const prependBy = (widget, rootId = "root") => {
|
|
297
|
+
const root = document.getElementById(rootId);
|
|
298
|
+
if (root && widget instanceof HTMLElement) {
|
|
299
|
+
root.insertBefore(widget, root.firstChild);
|
|
300
|
+
if (widget.triggerMount) widget.triggerMount();
|
|
301
|
+
}
|
|
302
|
+
return widget;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Inserts a widget immediately before a target element and triggers its mount
|
|
307
|
+
* lifecycle.
|
|
308
|
+
*
|
|
309
|
+
* @param {HTMLElement} widget - The widget element to insert.
|
|
310
|
+
* @param {string} targetId - The id of the reference element.
|
|
311
|
+
* @returns {HTMLElement} The same widget that was passed in.
|
|
312
|
+
*/
|
|
313
|
+
export const insertBefore = (widget, targetId) => {
|
|
314
|
+
const target = document.getElementById(targetId);
|
|
315
|
+
if (target && target.parentNode && widget instanceof HTMLElement) {
|
|
316
|
+
target.parentNode.insertBefore(widget, target);
|
|
317
|
+
if (widget.triggerMount) widget.triggerMount();
|
|
318
|
+
}
|
|
319
|
+
return widget;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Inserts a widget immediately after a target element and triggers its mount
|
|
324
|
+
* lifecycle.
|
|
325
|
+
*
|
|
326
|
+
* @param {HTMLElement} widget - The widget element to insert.
|
|
327
|
+
* @param {string} targetId - The id of the reference element.
|
|
328
|
+
* @returns {HTMLElement} The same widget that was passed in.
|
|
329
|
+
*/
|
|
330
|
+
export const insertAfter = (widget, targetId) => {
|
|
331
|
+
const target = document.getElementById(targetId);
|
|
332
|
+
if (target && target.parentNode && widget instanceof HTMLElement) {
|
|
333
|
+
target.parentNode.insertBefore(widget, target.nextSibling);
|
|
334
|
+
if (widget.triggerMount) widget.triggerMount();
|
|
335
|
+
}
|
|
336
|
+
return widget;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Replaces a target element with a widget and triggers its mount lifecycle.
|
|
341
|
+
*
|
|
342
|
+
* @param {HTMLElement} widget - The widget element to insert in place of the target.
|
|
343
|
+
* @param {string} targetId - The id of the element to be replaced.
|
|
344
|
+
* @returns {HTMLElement} The same widget that was passed in.
|
|
345
|
+
*/
|
|
346
|
+
export const replaceBy = (widget, targetId) => {
|
|
347
|
+
const target = document.getElementById(targetId);
|
|
348
|
+
if (target && target.parentNode && widget instanceof HTMLElement) {
|
|
349
|
+
target.parentNode.replaceChild(widget, target);
|
|
350
|
+
if (widget.triggerMount) widget.triggerMount();
|
|
351
|
+
}
|
|
352
|
+
return widget;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Clones a widget into multiple root elements and triggers the mount lifecycle
|
|
357
|
+
* on each clone.
|
|
358
|
+
*
|
|
359
|
+
* @param {HTMLElement} widget - The widget to clone and mount.
|
|
360
|
+
* @param {string[]} rootIds - An array of element ids to mount the widget into.
|
|
361
|
+
* @returns {HTMLElement} The original (un-cloned) widget.
|
|
362
|
+
*/
|
|
363
|
+
export const mountAll = (widget, rootIds) => {
|
|
364
|
+
rootIds.forEach((id) => {
|
|
365
|
+
const root = document.getElementById(id);
|
|
366
|
+
if (root && widget instanceof HTMLElement) {
|
|
367
|
+
const clone = widget.cloneNode(true);
|
|
368
|
+
root.appendChild(clone);
|
|
369
|
+
if (clone.triggerMount) clone.triggerMount();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
return widget;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Creates a router-based application instance without immediately rendering a
|
|
377
|
+
* component tree.
|
|
378
|
+
*
|
|
379
|
+
* Useful when the entry point is entirely route-driven and there is no single
|
|
380
|
+
* top-level component to pass to `runApp`.
|
|
381
|
+
*
|
|
382
|
+
* @param {Object} routes - Route map passed to `initRouter`.
|
|
383
|
+
* @param {Object} [options={}] - Configuration options.
|
|
384
|
+
* @param {string} [options.rootId="root"] - The id of the host DOM element.
|
|
385
|
+
* @param {Function|HTMLElement|null} [options.fallback=null] - An optional
|
|
386
|
+
* element (or factory) to render while the router resolves the first route.
|
|
387
|
+
* @returns {{ start: Function, destroy: Function }} An object with `start()` to
|
|
388
|
+
* bootstrap the app and `destroy()` to tear it down.
|
|
389
|
+
*/
|
|
390
|
+
export const createApp = (routes, options = {}) => {
|
|
391
|
+
const { rootId = "root", fallback = null } = options;
|
|
392
|
+
|
|
393
|
+
const root = document.getElementById(rootId);
|
|
394
|
+
if (root) {
|
|
395
|
+
root.style.margin = "0";
|
|
396
|
+
root.style.padding = "0";
|
|
397
|
+
root.style.width = "100%";
|
|
398
|
+
root.style.minHeight = "100vh";
|
|
399
|
+
root.style.display = "flex";
|
|
400
|
+
root.style.flexDirection = "column";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
/**
|
|
405
|
+
* Bootstraps the application: applies the system theme, starts the theme
|
|
406
|
+
* watcher, initialises the router, and renders the fallback element (if any).
|
|
407
|
+
*/
|
|
408
|
+
start: () => {
|
|
409
|
+
applySystemTheme();
|
|
410
|
+
watchSystemTheme();
|
|
411
|
+
initRouter(routes);
|
|
412
|
+
const root = document.getElementById(rootId);
|
|
413
|
+
if (root && fallback) {
|
|
414
|
+
root.innerHTML = "";
|
|
415
|
+
const fallbackEl =
|
|
416
|
+
typeof fallback === "function" ? fallback() : fallback;
|
|
417
|
+
if (fallbackEl instanceof HTMLElement) root.appendChild(fallbackEl);
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
/**
|
|
421
|
+
* Clears the root element, effectively unmounting the application.
|
|
422
|
+
*/
|
|
423
|
+
destroy: () => {
|
|
424
|
+
const root = document.getElementById(rootId);
|
|
425
|
+
if (root) root.innerHTML = "";
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
export default runApp;
|