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,537 @@
1
+ // navigations/Scaffold.js
2
+ import { WidgetFactory } from "../widget-factory/index.js";
3
+ import { colors } from "../utils/themes.js";
4
+ import { initRouter, getCurrentRoute, subscribe } from "./Router.js";
5
+
6
+ /**
7
+ * @typedef {Object} ScaffoldProps
8
+ * @property {HTMLElement|Function} [appBar] - Top app bar widget or factory function.
9
+ * @property {HTMLElement|Function|Object} [body] - Main content widget, factory function, or route map object.
10
+ * @property {HTMLElement|Function} [bottomBar] - Bottom navigation bar widget or factory function.
11
+ * @property {HTMLElement|Function} [fab] - Floating action button widget or factory function.
12
+ * @property {Object} [drawer] - Drawer instance (created via the Drawer component).
13
+ * @property {HTMLElement|Function} [leftNavBar] - Persistent left sidebar widget or factory function.
14
+ * @property {number} [leftNavBarWidth=260] - Width of the left sidebar in pixels.
15
+ * @property {HTMLElement|Function} [rightNavBar] - Persistent right sidebar widget or factory function.
16
+ * @property {number} [rightNavBarWidth=260] - Width of the right sidebar in pixels.
17
+ * @property {HTMLElement|Function} [navSideBar] - Legacy alias for leftNavBar (deprecated).
18
+ * @property {number} [navSideBarWidth=260] - Legacy alias for leftNavBarWidth (deprecated).
19
+ * @property {"left"|"right"} [navSideBarPosition="left"] - Legacy: which side navSideBar renders on.
20
+ * @property {Object} [routes] - Route map object for router mode (path → component).
21
+ * @property {string} [backgroundColor] - Background color of the scaffold shell.
22
+ * @property {boolean} [closeDrawerOnNavigate=false] - Whether to close the drawer automatically on route change.
23
+ */
24
+
25
+ /**
26
+ * Scaffold is the top-level layout shell for a page or screen.
27
+ *
28
+ * It composes an optional AppBar, a scrollable body area, a bottom bar,
29
+ * a floating action button, and optional persistent sidebars. It also
30
+ * integrates with the Router module: when `routes` (or a route-map `body`)
31
+ * is provided it initialises the router and re-renders the body on every
32
+ * navigation event, optionally toggling per-route visibility of the AppBar,
33
+ * bottom bar, FAB, and sidebars.
34
+ *
35
+ * @param {ScaffoldProps} props
36
+ * @returns {HTMLElement} The scaffold container element, augmented with
37
+ * imperative control methods:
38
+ * - `openDrawer()` / `closeDrawer()` — programmatically show/hide the drawer.
39
+ * - `updateLeftNavBar(cfg)` / `updateRightNavBar(cfg)` — replace sidebar content.
40
+ * - `setLeftNavBarWidth(w)` / `setRightNavBarWidth(w)` — resize a sidebar at runtime.
41
+ */
42
+ export const Scaffold = (props) => {
43
+ const {
44
+ appBar: originalAppBar,
45
+ body,
46
+ bottomBar: originalBottomBar,
47
+ fab: originalFab,
48
+ drawer,
49
+ leftNavBar,
50
+ leftNavBarWidth = 260,
51
+ rightNavBar,
52
+ rightNavBarWidth = 260,
53
+ navSideBar, // legacy support
54
+ navSideBarWidth = 260,
55
+ navSideBarPosition = "left",
56
+ routes,
57
+ backgroundColor = colors.background,
58
+ closeDrawerOnNavigate = false,
59
+ ...rest
60
+ } = props;
61
+
62
+ let routesConfig = null;
63
+ let isRouterMode = false;
64
+ let unsubscribe = null;
65
+ let drawerInstance = null;
66
+
67
+ // Determine whether to operate in router mode.
68
+ // Router mode is active when an explicit routes map or a plain-object body
69
+ // (treated as a route map) is supplied.
70
+ if (routes && typeof routes === "object" && Object.keys(routes).length > 0) {
71
+ routesConfig = routes;
72
+ isRouterMode = true;
73
+ } else if (
74
+ body &&
75
+ typeof body === "object" &&
76
+ !(body instanceof HTMLElement) &&
77
+ !body.nodeType
78
+ ) {
79
+ routesConfig = body;
80
+ isRouterMode = true;
81
+ }
82
+
83
+ // Bootstrap the router with the current browser URL so deep-links work
84
+ // on first load without redirecting to "/".
85
+ if (isRouterMode && routesConfig) {
86
+ const currentUrl =
87
+ typeof window !== "undefined" ? window.location.pathname : "/";
88
+ initRouter(routesConfig, currentUrl);
89
+ }
90
+
91
+ // Resolve the legacy navSideBar prop to the appropriate side.
92
+ let finalLeftNavBar = leftNavBar;
93
+ let finalRightNavBar = rightNavBar;
94
+ if (navSideBar && !leftNavBar && !rightNavBar) {
95
+ if (navSideBarPosition === "right") {
96
+ finalRightNavBar = navSideBar;
97
+ } else {
98
+ finalLeftNavBar = navSideBar;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Normalises a prop value to an HTMLElement.
104
+ * Accepts an HTMLElement directly, a factory function, or null/undefined.
105
+ *
106
+ * @param {HTMLElement|Function|null} item
107
+ * @returns {HTMLElement|null}
108
+ */
109
+ const toElement = (item) => {
110
+ if (!item) return null;
111
+ if (item instanceof HTMLElement) return item;
112
+ if (typeof item === "function") return item();
113
+ return item;
114
+ };
115
+
116
+ // Root container: full-viewport flex column that holds all layout zones.
117
+ const container = WidgetFactory({
118
+ tag: "div",
119
+ display: "flex",
120
+ flexDirection: "column",
121
+ width: "100%",
122
+ height: "100vh",
123
+ backgroundColor: backgroundColor,
124
+ position: "relative",
125
+ overflow: "hidden",
126
+ ...rest,
127
+ });
128
+
129
+ // Slot references — populated during buildStructure().
130
+ let appBarContainer = null;
131
+ let bottomBarContainer = null;
132
+ let fabContainer = null;
133
+ let leftNavBarContainer = null;
134
+ let rightNavBarContainer = null;
135
+ let mainContentContainer = null;
136
+
137
+ let currentAppBar = null;
138
+
139
+ /**
140
+ * Removes all direct children from a container, calling their `_cleanup`
141
+ * hook first to release subscriptions and event listeners.
142
+ *
143
+ * @param {HTMLElement|null} target
144
+ */
145
+ const clearContainer = (target) => {
146
+ if (!target) return;
147
+ while (target.firstChild) {
148
+ const child = target.firstChild;
149
+ if (typeof child._cleanup === "function") child._cleanup();
150
+ target.removeChild(child);
151
+ }
152
+ };
153
+
154
+ /**
155
+ * Forces a widget to fill the available space inside the body area.
156
+ * Necessary because body children must stretch to the flex container.
157
+ *
158
+ * @param {HTMLElement} widget
159
+ * @returns {HTMLElement}
160
+ */
161
+ const makeFullSize = (widget) => {
162
+ if (widget && widget.style) {
163
+ widget.style.flex = "1";
164
+ widget.style.width = "100%";
165
+ widget.style.display = "flex";
166
+ widget.style.flexDirection = "column";
167
+ widget.style.minHeight = "0";
168
+ }
169
+ return widget;
170
+ };
171
+
172
+ /**
173
+ * Replaces the content of the AppBar slot.
174
+ * Pass `false` to hide the AppBar entirely; pass `true`/`undefined` to
175
+ * render the original AppBar; pass an element/function to render a custom one.
176
+ * Also wires the drawer open() to the AppBar's menu icon when a drawer is present.
177
+ *
178
+ * @param {boolean|HTMLElement|Function|undefined} cfg
179
+ */
180
+ const updateAppBar = (cfg) => {
181
+ if (!appBarContainer) return;
182
+ clearContainer(appBarContainer);
183
+ if (cfg === false) {
184
+ appBarContainer.style.display = "none";
185
+ return;
186
+ }
187
+ appBarContainer.style.display = "block";
188
+ const widget =
189
+ cfg === true || cfg === undefined
190
+ ? toElement(originalAppBar)
191
+ : toElement(cfg);
192
+ if (widget) {
193
+ appBarContainer.appendChild(widget);
194
+ currentAppBar = widget;
195
+ // Auto-wire the first Material Icons "menu" icon to open the drawer,
196
+ // so callers don't need to pass an explicit onPress handler.
197
+ if (drawer && drawerInstance) {
198
+ const menuIcon = widget.querySelector(".material-icons");
199
+ if (menuIcon && menuIcon.textContent === "menu") {
200
+ const oldClick = menuIcon.onclick;
201
+ menuIcon.onclick = (e) => {
202
+ e.stopPropagation();
203
+ if (oldClick) oldClick(e);
204
+ if (
205
+ drawerInstance.open &&
206
+ typeof drawerInstance.open === "function"
207
+ ) {
208
+ drawerInstance.open();
209
+ } else if (drawerInstance.toggle) {
210
+ drawerInstance.toggle();
211
+ }
212
+ };
213
+ }
214
+ }
215
+ }
216
+ };
217
+
218
+ /**
219
+ * Replaces the content of the bottom bar slot.
220
+ * Pass `false` to hide it; `true`/`undefined` renders the original bottomBar prop.
221
+ *
222
+ * @param {boolean|HTMLElement|Function|undefined} cfg
223
+ */
224
+ const updateBottomBar = (cfg) => {
225
+ if (!bottomBarContainer) return;
226
+ clearContainer(bottomBarContainer);
227
+ if (cfg === false) {
228
+ bottomBarContainer.style.display = "none";
229
+ return;
230
+ }
231
+ bottomBarContainer.style.display = "block";
232
+ const widget =
233
+ cfg === true || cfg === undefined
234
+ ? toElement(originalBottomBar)
235
+ : toElement(cfg);
236
+ if (widget) bottomBarContainer.appendChild(widget);
237
+ };
238
+
239
+ /**
240
+ * Replaces the content of the FAB slot.
241
+ * Pass `false` to hide it; `true`/`undefined` renders the original fab prop.
242
+ *
243
+ * @param {boolean|HTMLElement|Function|undefined} cfg
244
+ */
245
+ const updateFab = (cfg) => {
246
+ if (!fabContainer) return;
247
+ clearContainer(fabContainer);
248
+ if (cfg === false) {
249
+ fabContainer.style.display = "none";
250
+ return;
251
+ }
252
+ fabContainer.style.display = "block";
253
+ const widget =
254
+ cfg === true || cfg === undefined
255
+ ? toElement(originalFab)
256
+ : toElement(cfg);
257
+ if (widget) fabContainer.appendChild(widget);
258
+ };
259
+
260
+ /**
261
+ * Replaces the content of the left sidebar slot.
262
+ * When a sidebar is shown, the main content area is narrowed accordingly.
263
+ *
264
+ * @param {boolean|HTMLElement|Function|undefined} cfg
265
+ */
266
+ const updateLeftBar = (cfg) => {
267
+ if (!leftNavBarContainer) return;
268
+ clearContainer(leftNavBarContainer);
269
+ if (cfg === false) {
270
+ leftNavBarContainer.style.display = "none";
271
+ // Restore full width to the main content area.
272
+ if (mainContentContainer) mainContentContainer.style.width = "";
273
+ return;
274
+ }
275
+ leftNavBarContainer.style.display = "block";
276
+ const widget =
277
+ cfg === true || cfg === undefined
278
+ ? toElement(finalLeftNavBar)
279
+ : toElement(cfg);
280
+ if (widget) {
281
+ leftNavBarContainer.appendChild(widget);
282
+ if (mainContentContainer)
283
+ mainContentContainer.style.width = `calc(100% - ${leftNavBarWidth}px)`;
284
+ }
285
+ };
286
+
287
+ /**
288
+ * Replaces the content of the right sidebar slot.
289
+ *
290
+ * @param {boolean|HTMLElement|Function|undefined} cfg
291
+ */
292
+ const updateRightBar = (cfg) => {
293
+ if (!rightNavBarContainer) return;
294
+ clearContainer(rightNavBarContainer);
295
+ if (cfg === false) {
296
+ rightNavBarContainer.style.display = "none";
297
+ return;
298
+ }
299
+ rightNavBarContainer.style.display = "block";
300
+ const widget =
301
+ cfg === true || cfg === undefined
302
+ ? toElement(finalRightNavBar)
303
+ : toElement(cfg);
304
+ if (widget) rightNavBarContainer.appendChild(widget);
305
+ };
306
+
307
+ /**
308
+ * Re-renders the body area for the current route (router mode) or static body.
309
+ * In router mode, also updates the AppBar, bottom bar, FAB, and sidebars
310
+ * according to per-route visibility configuration.
311
+ */
312
+ const updateBody = () => {
313
+ if (!mainContentContainer) return;
314
+ clearContainer(mainContentContainer);
315
+
316
+ if (isRouterMode) {
317
+ const routeConfig = getCurrentRoute();
318
+ let route = routeConfig;
319
+ let appBarCfg = true;
320
+ let bottomBarCfg = true;
321
+ let fabCfg = true;
322
+ let leftBarCfg = true;
323
+ let rightBarCfg = true;
324
+
325
+ // A route entry can be a plain component or a config object with
326
+ // per-route visibility overrides for shell zones.
327
+ if (
328
+ routeConfig &&
329
+ typeof routeConfig === "object" &&
330
+ !(routeConfig instanceof HTMLElement)
331
+ ) {
332
+ route = routeConfig.body || routeConfig;
333
+ appBarCfg =
334
+ routeConfig.appBar !== undefined ? routeConfig.appBar : true;
335
+ bottomBarCfg =
336
+ routeConfig.bottomBar !== undefined ? routeConfig.bottomBar : true;
337
+ fabCfg = routeConfig.fab !== undefined ? routeConfig.fab : true;
338
+ leftBarCfg =
339
+ routeConfig.leftNavBar !== undefined ? routeConfig.leftNavBar : true;
340
+ rightBarCfg =
341
+ routeConfig.rightNavBar !== undefined
342
+ ? routeConfig.rightNavBar
343
+ : true;
344
+ }
345
+
346
+ const bodyWidget = makeFullSize(toElement(route));
347
+ if (bodyWidget) mainContentContainer.appendChild(bodyWidget);
348
+ updateAppBar(appBarCfg);
349
+ updateBottomBar(bottomBarCfg);
350
+ updateFab(fabCfg);
351
+ updateLeftBar(leftBarCfg);
352
+ updateRightBar(rightBarCfg);
353
+
354
+ // Auto-close the drawer after navigation to avoid leaving it open
355
+ // on the next page — only when the caller opts in via closeDrawerOnNavigate.
356
+ if (closeDrawerOnNavigate && drawerInstance && drawerInstance.close) {
357
+ drawerInstance.close();
358
+ }
359
+ } else if (body) {
360
+ const bodyWidget = makeFullSize(toElement(body));
361
+ if (bodyWidget) mainContentContainer.appendChild(bodyWidget);
362
+ }
363
+ };
364
+
365
+ /**
366
+ * Builds the full DOM structure of the scaffold from scratch.
367
+ * Called once on creation. Also attaches the drawer instance to the container
368
+ * so external code can call container.openDrawer() / container.closeDrawer().
369
+ */
370
+ const buildStructure = () => {
371
+ while (container.firstChild) container.removeChild(container.firstChild);
372
+
373
+ // AppBar zone — pinned at the top, does not scroll.
374
+ appBarContainer = document.createElement("div");
375
+ appBarContainer.style.flexShrink = "0";
376
+ container.appendChild(appBarContainer);
377
+
378
+ // Middle row — contains optional sidebars and the scrollable body.
379
+ const mainArea = document.createElement("div");
380
+ mainArea.style.display = "flex";
381
+ mainArea.style.flex = "1";
382
+ mainArea.style.flexDirection = "row";
383
+ mainArea.style.overflow = "hidden";
384
+ mainArea.style.minHeight = "0";
385
+ container.appendChild(mainArea);
386
+
387
+ if (finalLeftNavBar) {
388
+ leftNavBarContainer = document.createElement("div");
389
+ leftNavBarContainer.style.width = `${leftNavBarWidth}px`;
390
+ leftNavBarContainer.style.flexShrink = "0";
391
+ leftNavBarContainer.style.overflow = "auto";
392
+ leftNavBarContainer.style.height = "100%";
393
+ // Hidden by default; updateLeftBar(true) makes it visible.
394
+ leftNavBarContainer.style.display = "none";
395
+ mainArea.appendChild(leftNavBarContainer);
396
+ }
397
+
398
+ // Main content area — takes remaining horizontal space.
399
+ mainContentContainer = document.createElement("div");
400
+ mainContentContainer.style.flex = "1";
401
+ mainContentContainer.style.display = "flex";
402
+ mainContentContainer.style.flexDirection = "column";
403
+ mainContentContainer.style.overflow = "auto";
404
+ mainContentContainer.style.minHeight = "0";
405
+ mainArea.appendChild(mainContentContainer);
406
+
407
+ if (finalRightNavBar) {
408
+ rightNavBarContainer = document.createElement("div");
409
+ rightNavBarContainer.style.width = `${rightNavBarWidth}px`;
410
+ rightNavBarContainer.style.flexShrink = "0";
411
+ rightNavBarContainer.style.overflow = "auto";
412
+ rightNavBarContainer.style.height = "100%";
413
+ rightNavBarContainer.style.display = "none";
414
+ mainArea.appendChild(rightNavBarContainer);
415
+ }
416
+
417
+ // Bottom bar zone — pinned at the bottom, does not scroll.
418
+ bottomBarContainer = document.createElement("div");
419
+ bottomBarContainer.style.flexShrink = "0";
420
+ container.appendChild(bottomBarContainer);
421
+
422
+ // FAB slot — absolutely positioned so it floats above content.
423
+ fabContainer = document.createElement("div");
424
+ fabContainer.style.position = "absolute";
425
+ fabContainer.style.bottom = "16px";
426
+ fabContainer.style.right = "16px";
427
+ fabContainer.style.zIndex = "10";
428
+ container.appendChild(fabContainer);
429
+
430
+ // Attach the drawer instance and expose open/close helpers on the container.
431
+ if (drawer) {
432
+ drawerInstance = toElement(drawer);
433
+ if (drawerInstance) {
434
+ container._drawer = drawerInstance;
435
+ container.openDrawer = () =>
436
+ drawerInstance.open?.() ?? drawerInstance.toggle?.();
437
+ container.closeDrawer = () =>
438
+ drawerInstance.close?.() ?? drawerInstance.toggle?.();
439
+ }
440
+ }
441
+
442
+ if (finalLeftNavBar) updateLeftBar(true);
443
+ if (finalRightNavBar) updateRightBar(true);
444
+ updateBody();
445
+ };
446
+
447
+ // Subscribe to router changes so the body re-renders on navigation.
448
+ if (isRouterMode) {
449
+ unsubscribe = subscribe(() => {
450
+ updateBody();
451
+ });
452
+ }
453
+
454
+ buildStructure();
455
+
456
+ // Automatic garbage collection via MutationObserver:
457
+ // When any child node is removed from the tree, call its _cleanup hook
458
+ // (and the hooks of its descendants) to release event listeners and
459
+ // router subscriptions without requiring manual teardown at call sites.
460
+ const observer = new MutationObserver((mutations) => {
461
+ mutations.forEach((mutation) => {
462
+ mutation.removedNodes.forEach((node) => {
463
+ // Run cleanup on the removed node itself.
464
+ if (typeof node._cleanup === "function") {
465
+ node._cleanup();
466
+ }
467
+ // Recursively clean up any descendant widgets that carry a cleanup hook.
468
+ if (node.querySelectorAll) {
469
+ const children = node.querySelectorAll("*");
470
+ children.forEach((child) => {
471
+ if (typeof child._cleanup === "function") {
472
+ child._cleanup();
473
+ }
474
+ });
475
+ }
476
+ });
477
+ });
478
+ });
479
+
480
+ // Watch the entire scaffold subtree for removed nodes.
481
+ observer.observe(container, { childList: true, subtree: true });
482
+
483
+ const originalCleanup = container._cleanup;
484
+ container._cleanup = () => {
485
+ // Stop the MutationObserver when the scaffold itself is destroyed.
486
+ observer.disconnect();
487
+ if (unsubscribe) unsubscribe();
488
+ clearContainer(appBarContainer);
489
+ clearContainer(mainContentContainer);
490
+ clearContainer(bottomBarContainer);
491
+ clearContainer(fabContainer);
492
+ clearContainer(leftNavBarContainer);
493
+ clearContainer(rightNavBarContainer);
494
+ if (container._drawer?.parentNode)
495
+ container._drawer.parentNode.removeChild(container._drawer);
496
+ if (originalCleanup) originalCleanup();
497
+ };
498
+
499
+ /**
500
+ * Programmatically replace the left sidebar content at runtime.
501
+ * Accepts the same values as the internal `updateLeftBar` helper.
502
+ *
503
+ * @param {boolean|HTMLElement|Function} cfg
504
+ */
505
+ container.updateLeftNavBar = updateLeftBar;
506
+
507
+ /**
508
+ * Programmatically replace the right sidebar content at runtime.
509
+ *
510
+ * @param {boolean|HTMLElement|Function} cfg
511
+ */
512
+ container.updateRightNavBar = updateRightBar;
513
+
514
+ /**
515
+ * Resize the left sidebar at runtime without rebuilding the scaffold.
516
+ *
517
+ * @param {number|string} w - New width (number = pixels, string = CSS value).
518
+ */
519
+ container.setLeftNavBarWidth = (w) => {
520
+ if (leftNavBarContainer)
521
+ leftNavBarContainer.style.width = typeof w === "number" ? `${w}px` : w;
522
+ };
523
+
524
+ /**
525
+ * Resize the right sidebar at runtime without rebuilding the scaffold.
526
+ *
527
+ * @param {number|string} w - New width (number = pixels, string = CSS value).
528
+ */
529
+ container.setRightNavBarWidth = (w) => {
530
+ if (rightNavBarContainer)
531
+ rightNavBarContainer.style.width = typeof w === "number" ? `${w}px` : w;
532
+ };
533
+
534
+ return container;
535
+ };
536
+
537
+ export default Scaffold;