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.
Files changed (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -2
  3. package/bin/cli.js +394 -0
  4. package/bin/commands/buildAndroid.js +200 -0
  5. package/bin/commands/buildIOS.js +237 -0
  6. package/bin/commands/create.js +406 -0
  7. package/bin/commands/createBundle.js +375 -0
  8. package/bin/commands/createComponent.js +115 -0
  9. package/bin/commands/createScreen.js +161 -0
  10. package/bin/commands/killServer.js +107 -0
  11. package/bin/commands/package.js +131 -0
  12. package/bin/commands/release.js +189 -0
  13. package/bin/commands/runBundle.js +62 -0
  14. package/bin/commands/runCloudflare.js +89 -0
  15. package/bin/commands/runNative.js +109 -0
  16. package/bin/commands/runServer.js +390 -0
  17. package/bin/ota/loader.js +361 -0
  18. package/bin/utils/bundleConfig.js +178 -0
  19. package/bin/utils/capacitor.js +137 -0
  20. package/bin/utils/colors.js +215 -0
  21. package/bin/utils/helpers.js +80 -0
  22. package/bin/utils/spinner.js +96 -0
  23. package/bin/utils/templates.js +1824 -0
  24. package/bin/utils/version.js +108 -0
  25. package/package.json +58 -4
  26. package/src/animations/AnimatedBox.js +607 -0
  27. package/src/animations/AnimatedText.js +105 -0
  28. package/src/animations/MatrixRain.js +220 -0
  29. package/src/animations/ParallaxBox.js +369 -0
  30. package/src/animations/animate.js +332 -0
  31. package/src/animations/index.js +25 -0
  32. package/src/app.js +41 -0
  33. package/src/assets/icon-1024-maskable.png +0 -0
  34. package/src/assets/icon-192-maskable.png +0 -0
  35. package/src/assets/icon-192.png +0 -0
  36. package/src/assets/icon-512-maskable.png +0 -0
  37. package/src/assets/icon-512.png +0 -0
  38. package/src/assets/logo.png +0 -0
  39. package/src/core/App.js +44 -0
  40. package/src/core/hmr-client.js +142 -0
  41. package/src/core/index.js +34 -0
  42. package/src/core/pwa.js +100 -0
  43. package/src/core/runApp.js +430 -0
  44. package/src/create-index.sh +237 -0
  45. package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
  46. package/src/fonts/icons.css +23 -0
  47. package/src/index.d.ts +1930 -0
  48. package/src/index.js +288 -0
  49. package/src/navigations/AdaptiveScaffold.js +149 -0
  50. package/src/navigations/AppBar.js +331 -0
  51. package/src/navigations/BottomNavigation.js +298 -0
  52. package/src/navigations/CollapsibleSideBar.js +139 -0
  53. package/src/navigations/Drawer.js +333 -0
  54. package/src/navigations/DrawerItem.js +260 -0
  55. package/src/navigations/Router.js +475 -0
  56. package/src/navigations/Scaffold.js +537 -0
  57. package/src/navigations/Tabs.js +384 -0
  58. package/src/navigations/index.js +57 -0
  59. package/src/services/RamStore.js +213 -0
  60. package/src/services/Session.js +310 -0
  61. package/src/services/Storage.js +309 -0
  62. package/src/services/http.js +350 -0
  63. package/src/services/index.js +57 -0
  64. package/src/tools/animation.js +42 -0
  65. package/src/tools/array.js +95 -0
  66. package/src/tools/border.js +28 -0
  67. package/src/tools/clipboard.js +104 -0
  68. package/src/tools/color.js +329 -0
  69. package/src/tools/createList.js +215 -0
  70. package/src/tools/delay.js +55 -0
  71. package/src/tools/device.js +138 -0
  72. package/src/tools/dict.js +297 -0
  73. package/src/tools/dimensions.js +118 -0
  74. package/src/tools/filter.js +41 -0
  75. package/src/tools/flex.js +26 -0
  76. package/src/tools/gradient.js +34 -0
  77. package/src/tools/grid.js +57 -0
  78. package/src/tools/index.js +38 -0
  79. package/src/tools/mapList.js +107 -0
  80. package/src/tools/margin.js +93 -0
  81. package/src/tools/memo.js +60 -0
  82. package/src/tools/os.js +104 -0
  83. package/src/tools/padding.js +93 -0
  84. package/src/tools/print.js +69 -0
  85. package/src/tools/random.js +299 -0
  86. package/src/tools/ref.js +70 -0
  87. package/src/tools/rgba.js +4 -0
  88. package/src/tools/shadow.js +23 -0
  89. package/src/tools/string.js +85 -0
  90. package/src/tools/time.js +674 -0
  91. package/src/tools/transform.js +68 -0
  92. package/src/tools/transition.js +48 -0
  93. package/src/tools/useState.js +268 -0
  94. package/src/tools/uuid.js +42 -0
  95. package/src/utils/TextInputValidator.js +256 -0
  96. package/src/utils/composeUpdate.js +65 -0
  97. package/src/utils/getWidgetProps.js +35 -0
  98. package/src/utils/index.js +76 -0
  99. package/src/utils/markdownParser.js +368 -0
  100. package/src/utils/mediaTime.js +79 -0
  101. package/src/utils/navigation.js +263 -0
  102. package/src/utils/scriptInjector.js +73 -0
  103. package/src/utils/stackPosition.js +31 -0
  104. package/src/utils/stopWebRefresh.js +77 -0
  105. package/src/utils/styleInjector.js +38 -0
  106. package/src/utils/syntaxHighlight.js +339 -0
  107. package/src/utils/themes.js +347 -0
  108. package/src/utils/units.js +87 -0
  109. package/src/utils/visualEffects.js +169 -0
  110. package/src/widget-factory/addChildren.js +114 -0
  111. package/src/widget-factory/assignProps.js +256 -0
  112. package/src/widget-factory/createWidget.js +28 -0
  113. package/src/widget-factory/effects.js +132 -0
  114. package/src/widget-factory/index.js +7 -0
  115. package/src/widget-factory/lifecycle.js +210 -0
  116. package/src/widget-factory/makeParentable.js +89 -0
  117. package/src/widget-factory/pixTool.js +40 -0
  118. package/src/widget-factory/processProps.js +122 -0
  119. package/src/widget-factory/reactivity.js +98 -0
  120. package/src/widget-factory/remTool.js +34 -0
  121. package/src/widget-factory/stackPosition.js +87 -0
  122. package/src/widget-factory/stringTool.js +26 -0
  123. package/src/widget-factory/tools.js +249 -0
  124. package/src/widget-factory/translateProps.js +1049 -0
  125. package/src/widget-factory/warnings.js +284 -0
  126. package/src/widget-factory/widgetFactory.js +200 -0
  127. package/src/widgets/Accordion.js +562 -0
  128. package/src/widgets/AdMob.js +315 -0
  129. package/src/widgets/AdSense.js +268 -0
  130. package/src/widgets/AlertDialog.js +162 -0
  131. package/src/widgets/Audio.js +311 -0
  132. package/src/widgets/Avatar.js +210 -0
  133. package/src/widgets/Badge.js +189 -0
  134. package/src/widgets/BottomSheet.js +287 -0
  135. package/src/widgets/Button.js +247 -0
  136. package/src/widgets/Card.js +33 -0
  137. package/src/widgets/Carousel.js +301 -0
  138. package/src/widgets/Chart.js +632 -0
  139. package/src/widgets/Checkbox.js +106 -0
  140. package/src/widgets/Chip.js +153 -0
  141. package/src/widgets/CircularBar.js +352 -0
  142. package/src/widgets/CircularChart.js +549 -0
  143. package/src/widgets/CodeViewer.js +236 -0
  144. package/src/widgets/Column.js +29 -0
  145. package/src/widgets/Container.js +31 -0
  146. package/src/widgets/DataTable.js +257 -0
  147. package/src/widgets/Divider.js +93 -0
  148. package/src/widgets/DraggBox.js +115 -0
  149. package/src/widgets/Dropdown.js +505 -0
  150. package/src/widgets/DroppBox.js +180 -0
  151. package/src/widgets/FloatingActionButton.js +91 -0
  152. package/src/widgets/GridView.js +20 -0
  153. package/src/widgets/Icon.js +33 -0
  154. package/src/widgets/Image.js +15 -0
  155. package/src/widgets/Input.js +565 -0
  156. package/src/widgets/Inspector.js +254 -0
  157. package/src/widgets/InstallButton.js +110 -0
  158. package/src/widgets/ListTile.js +224 -0
  159. package/src/widgets/ListView.js +503 -0
  160. package/src/widgets/Markdown.js +1236 -0
  161. package/src/widgets/Modal.js +496 -0
  162. package/src/widgets/Pagination.js +195 -0
  163. package/src/widgets/ProgressBar.js +178 -0
  164. package/src/widgets/QRCode.js +467 -0
  165. package/src/widgets/Radio.js +132 -0
  166. package/src/widgets/Rating.js +261 -0
  167. package/src/widgets/Row.js +29 -0
  168. package/src/widgets/Skeleton.js +243 -0
  169. package/src/widgets/Slider.js +527 -0
  170. package/src/widgets/SnackBar.js +227 -0
  171. package/src/widgets/Stack.js +29 -0
  172. package/src/widgets/Stepper.js +296 -0
  173. package/src/widgets/Switch.js +102 -0
  174. package/src/widgets/Text.js +98 -0
  175. package/src/widgets/Tooltip.js +267 -0
  176. package/src/widgets/TreeView.js +370 -0
  177. package/src/widgets/Video.js +92 -0
  178. package/src/widgets/index.js +52 -0
@@ -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;