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,1824 @@
1
+ // bin/utils/templates.js
2
+
3
+ export const indexHtml = (appName = "FletBox App") => `<!DOCTYPE html>
4
+ <html lang="en">
5
+ <head>
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0">
8
+
9
+ <link rel="stylesheet" href="./src/assets/fonts/icons.css">
10
+ <link rel="manifest" href="./manifest.json">
11
+ <link rel="icon" href="./src/assets/icon-192.png" sizes="192x192" type="image/png">
12
+
13
+ <title>${appName}</title>
14
+ <meta name="description" content="${appName} built with Flet-Box.">
15
+ <meta name="keywords" content="fletbox, framework, ui, javascript, animations, gradients">
16
+ <meta name="author" content="Your name">
17
+
18
+ <meta property="og:title" content="${appName}">
19
+ <meta property="og:description" content="${appName} built with Flet-Box.">
20
+ <meta property="og:type" content="website">
21
+
22
+ <meta name="theme-color" content="#1a1a2e">
23
+
24
+ <!-- ============================================================
25
+ GOOGLE ADSENSE (optional — disabled by default)
26
+
27
+ To monetize this app:
28
+ 1. Get approved at https://www.google.com/adsense (you need a
29
+ live domain with your own content first).
30
+ 2. Replace ca-pub-XXXXXXXXXXXXXXXX below with your publisher ID.
31
+ 3. Activate it: delete this whole comment block but KEEP the
32
+ <script> tag below, so only the live <script> remains in <head>.
33
+ 4. Place an <ins class="adsbygoogle"> unit where you want the ad,
34
+ or use the flet-box AdSense widget (see docs/widget/AdSense.md).
35
+
36
+ NOTE: ads only work on an approved domain, not on localhost.
37
+
38
+ <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXXXXXXXX"
39
+ crossorigin="anonymous"></script>
40
+ ============================================================ -->
41
+
42
+ <style>
43
+ * { -webkit-text-size-adjust: 100%; }
44
+ body { margin: 0; padding: 0; font-family: system-ui, sans-serif}
45
+ </style>
46
+ <script type="importmap">
47
+ {
48
+ "imports": {
49
+ "flet-box": "./node_modules/flet-box/src/index.js"
50
+ }
51
+ }
52
+ </script>
53
+ </head>
54
+ <body>
55
+ <div id="root"></div>
56
+ <script>
57
+ if ('serviceWorker' in navigator) {
58
+ navigator.serviceWorker.register('/service-worker.js')
59
+ .then(reg => console.log('Service Worker registered', reg))
60
+ .catch(err => console.log('Service Worker registration error', err));
61
+ }
62
+ </script>
63
+ <script type="module" src="src/app.js"></script>
64
+ </body>
65
+ </html>`;
66
+
67
+ export const appJs = () => `// app.js
68
+ import { runApp, Scaffold, AppBar, BottomNavigation, colors, Icon, goTo, openDrawer } from 'flet-box';
69
+ import { RootScreen } from './screens/RootScreen.js';
70
+ import { HomeScreen } from './screens/HomeScreen.js';
71
+ import { AboutScreen } from './screens/AboutScreen.js';
72
+ import { DrawerMenu } from './components/layouts/DrawerMenu.js';
73
+
74
+ // ========== CUSTOM APPBAR (commented out) ==========
75
+ /*
76
+ const CustomAppBar = () => {
77
+ return AppBar({
78
+ title: 'My App',
79
+ backgroundColor: colors.primary,
80
+ titleColor: '#ffffff',
81
+ centerTitle: true,
82
+ showBackButton: false,
83
+ leading: Icon({
84
+ name: 'menu',
85
+ size: 24,
86
+ color: '#ffffff',
87
+ onclick: () => openDrawer()
88
+ }),
89
+ actions: [
90
+ Icon({ name: 'search', size: 22, color: '#ffffff', onclick: () => console.log('Search') }),
91
+ Icon({ name: 'notifications', size: 22, color: '#ffffff', onclick: () => console.log('Notifications') })
92
+ ]
93
+ });
94
+ };
95
+ */
96
+
97
+ // ========== BOTTOM NAVIGATION (commented out) ==========
98
+ /*
99
+ const BottomNav = () => {
100
+ return BottomNavigation({
101
+ items: [
102
+ { icon: 'home', label: 'Home', route: '/home' },
103
+ { icon: 'favorite', label: 'Favorites', route: '/favorites' },
104
+ { icon: 'person', label: 'Profile', route: '/profile' }
105
+ ],
106
+ backgroundColor: colors.surface,
107
+ selectedColor: colors.primary
108
+ });
109
+ };
110
+ */
111
+
112
+ // ========== APPBAR PER ROUTE ==========
113
+ const HomeAppBar = () => {
114
+ return AppBar({
115
+ title: 'Dashboard',
116
+ backgroundColor: colors.primary,
117
+ titleColor: '#ffffff',
118
+ centerTitle: true,
119
+ showBackButton: false,
120
+ leading: Icon({
121
+ name: 'menu',
122
+ size: 24,
123
+ color: '#ffffff',
124
+ onclick: () => openDrawer()
125
+ })
126
+ });
127
+ };
128
+
129
+ const AboutAppBar = () => {
130
+ return AppBar({
131
+ title: 'About',
132
+ backgroundColor: colors.primary,
133
+ titleColor: '#ffffff',
134
+ centerTitle: true,
135
+ showBackButton: true,
136
+ leading: Icon({
137
+ name: 'arrow_back',
138
+ size: 24,
139
+ color: '#ffffff',
140
+ onclick: () => goTo('/home')
141
+ })
142
+ });
143
+ };
144
+
145
+ // ========== ROUTES ==========
146
+ const routes = {
147
+ '/': { body: RootScreen, appBar: false },
148
+ '/home': { body: HomeScreen, appBar: HomeAppBar() },
149
+ '/about': { body: AboutScreen, appBar: AboutAppBar() }
150
+ // Example with BottomNavigation (uncomment)
151
+ // '/favorites': { body: HomeScreen, appBar: HomeAppBar() },
152
+ // '/profile': { body: AboutScreen, appBar: AboutAppBar() }
153
+ };
154
+
155
+ const MyApp = () => {
156
+ return Scaffold({
157
+ routes: routes,
158
+ drawer: DrawerMenu(),
159
+ // bottomBar: BottomNav(), // ← Uncomment to enable BottomNavigation
160
+ routerMode: 'hash',
161
+ backgroundColor: colors.background
162
+ });
163
+ };
164
+
165
+ runApp(MyApp, 'root');
166
+ `;
167
+
168
+ // The rest of the templates (rootScreenJs, homeScreenJs, aboutScreenJs, drawerMenuJs, themesJs, etc.)
169
+ // remain the same as in your original file (they are already in English).
170
+ // For brevity, I include them below as you already have them, but ensure no Spanish comments.
171
+
172
+ export const rootScreenJs = () => `// screens/RootScreen.js
173
+ import { Container, Column, Text, Button, Icon, colors, goTo } from 'flet-box';
174
+
175
+ export const RootScreen = () => {
176
+ return Container({
177
+ flex: 1,
178
+ justifyContent: 'center',
179
+ alignItems: 'center',
180
+ padding: 20,
181
+ backgroundColor: colors.background,
182
+ child: Column({
183
+ gap: 28,
184
+ alignItems: 'center',
185
+ children: [
186
+ Container({
187
+ width: 80,
188
+ height: 80,
189
+ borderRadius: 20,
190
+ backgroundColor: colors.primary + '15',
191
+ justifyContent: 'center',
192
+ alignItems: 'center',
193
+ child: Icon({ name: 'rocket_launch', size: 48, color: colors.primary })
194
+ }),
195
+ Text({
196
+ text: 'FletBox',
197
+ size: 36,
198
+ weight: 'bold',
199
+ color: colors.primary,
200
+ align: 'center'
201
+ }),
202
+ Text({
203
+ text: 'Ultra-lightweight UI framework for vanilla JavaScript',
204
+ size: 14,
205
+ color: colors.textSecondary,
206
+ align: 'center'
207
+ }),
208
+ Button({
209
+ text: 'Get Started →',
210
+ iconRight: 'arrow_forward',
211
+ bgColor: colors.primary,
212
+ color: '#ffffff',
213
+ padding: '10px 28px',
214
+ onPress: () => goTo('/home')
215
+ })
216
+ ]
217
+ })
218
+ });
219
+ };
220
+
221
+ export default RootScreen;
222
+ `;
223
+
224
+ export const homeScreenJs = () => `// screens/HomeScreen.js
225
+ import { Container, Column, Text, Button, Icon, colors, goTo } from 'flet-box';
226
+
227
+ // ========== GOOGLE ADMOB (optional — disabled by default) ==========
228
+ // AdMob shows native ads in Android/iOS builds. To enable it:
229
+ // 1. Install the Capacitor plugin:
230
+ // npm install @capacitor-community/admob
231
+ // 2. Add your real AdMob App ID to the native projects (see the README
232
+ // "Ads (AdSense & AdMob)" section for the Android/iOS manifest keys),
233
+ // then run: npm run android:sync && npm run ios:sync
234
+ // 3. Uncomment the AdMob import and the <AdMob .../> widget below.
235
+ // While developing, keep isTesting: true so you serve Google's sample ads
236
+ // (never click your own live ads — AdMob can ban the account).
237
+ //
238
+ // import { AdMob } from 'flet-box';
239
+ // =====================================================================
240
+
241
+ export const HomeScreen = () => {
242
+ return Container({
243
+ flex: 1,
244
+ justifyContent: 'center',
245
+ alignItems: 'center',
246
+ padding: 24,
247
+ backgroundColor: colors.background,
248
+ child: Column({
249
+ gap: 32,
250
+ alignItems: 'center',
251
+ children: [
252
+ Container({
253
+ width: 96,
254
+ height: 96,
255
+ borderRadius: 24,
256
+ backgroundColor: colors.primary + '15',
257
+ justifyContent: 'center',
258
+ alignItems: 'center',
259
+ child: Icon({ name: 'rocket_launch', size: 56, color: colors.primary })
260
+ }),
261
+ Text({
262
+ text: 'Welcome to FletBox',
263
+ size: 32,
264
+ weight: 'bold',
265
+ color: colors.text,
266
+ align: 'center'
267
+ }),
268
+ Text({
269
+ text: 'Choose a section to explore:',
270
+ size: 16,
271
+ color: colors.textSecondary,
272
+ align: 'center'
273
+ }),
274
+ Column({
275
+ gap: 12,
276
+ alignItems: 'stretch',
277
+ style: { width: '100%', maxWidth: 280 },
278
+ children: [
279
+ Button({
280
+ text: '🏠 Home',
281
+ iconRight: 'arrow_forward',
282
+ bgColor: colors.primary,
283
+ color: '#ffffff',
284
+ padding: '12px 20px',
285
+ fullWidth: true,
286
+ onPress: () => goTo('/home')
287
+ }),
288
+ Button({
289
+ text: 'ℹ️ About',
290
+ iconRight: 'arrow_forward',
291
+ variant: 'outlined',
292
+ color: colors.info,
293
+ padding: '12px 20px',
294
+ fullWidth: true,
295
+ onPress: () => goTo('/about')
296
+ }),
297
+ Button({
298
+ text: '← Back to Root',
299
+ iconLeft: 'arrow_back',
300
+ variant: 'text',
301
+ color: colors.textSecondary,
302
+ padding: '12px 20px',
303
+ fullWidth: true,
304
+ onPress: () => goTo('/')
305
+ })
306
+ ]
307
+ })
308
+
309
+ // ===== ADMOB BANNER (optional — uncomment to enable) =====
310
+ // Uses Google's sample unit id while isTesting is true.
311
+ // Replace adId with your own banner unit id before going live.
312
+ // AdMob({
313
+ // position: 'bottom',
314
+ // isTesting: true,
315
+ // // adId: 'ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX',
316
+ // })
317
+ ]
318
+ })
319
+ });
320
+ };
321
+
322
+ export default HomeScreen;
323
+ `;
324
+
325
+ export const aboutScreenJs = () => `// screens/AboutScreen.js
326
+ import { Container, Column, Row, Text, Button, Icon, colors, goTo } from 'flet-box';
327
+
328
+ export const AboutScreen = () => {
329
+ return Container({
330
+ flex: 1,
331
+ justifyContent: 'center',
332
+ alignItems: 'center',
333
+ padding: 24,
334
+ backgroundColor: colors.background,
335
+ child: Column({
336
+ gap: 32,
337
+ alignItems: 'center',
338
+ children: [
339
+ Container({
340
+ width: 96,
341
+ height: 96,
342
+ borderRadius: 24,
343
+ backgroundColor: colors.primary + '15',
344
+ justifyContent: 'center',
345
+ alignItems: 'center',
346
+ child: Icon({ name: 'rocket_launch', size: 56, color: colors.primary })
347
+ }),
348
+ Text({
349
+ text: 'Welcome to FletBox',
350
+ size: 32,
351
+ weight: 'bold',
352
+ color: colors.text,
353
+ align: 'center'
354
+ }),
355
+ Text({
356
+ text: 'A simple and fast UI framework for building web apps with pure JavaScript.',
357
+ size: 16,
358
+ color: colors.textSecondary,
359
+ align: 'center',
360
+ style: { maxWidth: 400 }
361
+ }),
362
+ Container({
363
+ marginTop: 16,
364
+ padding: 20,
365
+ borderRadius: 16,
366
+ backgroundColor: colors.surface,
367
+ style: { width: '100%', maxWidth: 400 },
368
+ child: Column({
369
+ gap: 12,
370
+ children: [
371
+ Text({ text: '✨ What is FletBox?', size: 18, weight: 'bold', color: colors.primary }),
372
+ Text({ text: 'FletBox helps you create web apps easily. No complex tools. Just write JavaScript.', size: 14, color: colors.textSecondary }),
373
+ Container({ height: 1, backgroundColor: colors.border, marginVertical: 8 }),
374
+ Row({ gap: 12, alignItems: 'center', children: [
375
+ Icon({ name: 'check_circle', size: 20, color: colors.success }),
376
+ Text({ text: 'Zero dependencies - lightweight', size: 14, color: colors.text })
377
+ ] }),
378
+ Row({ gap: 12, alignItems: 'center', children: [
379
+ Icon({ name: 'check_circle', size: 20, color: colors.success }),
380
+ Text({ text: '40+ ready-to-use widgets', size: 14, color: colors.text })
381
+ ] }),
382
+ Row({ gap: 12, alignItems: 'center', children: [
383
+ Icon({ name: 'check_circle', size: 20, color: colors.success }),
384
+ Text({ text: 'Built-in navigation (router)', size: 14, color: colors.text })
385
+ ] }),
386
+ Row({ gap: 12, alignItems: 'center', children: [
387
+ Icon({ name: 'check_circle', size: 20, color: colors.success }),
388
+ Text({ text: 'Dark / Light theme', size: 14, color: colors.text })
389
+ ] }),
390
+ Row({ gap: 12, alignItems: 'center', children: [
391
+ Icon({ name: 'check_circle', size: 20, color: colors.success }),
392
+ Text({ text: 'Hot reload for fast development', size: 14, color: colors.text })
393
+ ] })
394
+ ]
395
+ })
396
+ }),
397
+ Button({
398
+ text: 'Back to Root',
399
+ iconLeft: 'arrow_back',
400
+ variant: 'outlined',
401
+ bgColor: colors.info,
402
+ color: colors.info,
403
+ onPress: () => goTo('/')
404
+ })
405
+ ]
406
+ })
407
+ });
408
+ };
409
+
410
+ export default AboutScreen;
411
+ `;
412
+
413
+ export const drawerMenuJs = () => `// components/layouts/DrawerMenu.js
414
+ import { Drawer, DrawerItem, Container, Column, Text, Icon, colors, closeDrawer } from 'flet-box';
415
+
416
+ export const DrawerMenu = () => {
417
+ return Drawer({
418
+ header: Container({
419
+ padding: 28,
420
+ backgroundColor: colors.primary,
421
+ child: Column({
422
+ gap: 12,
423
+ alignItems: 'center',
424
+ children: [
425
+ Container({
426
+ width: 60,
427
+ height: 60,
428
+ borderRadius: 30,
429
+ backgroundColor: '#ffffff',
430
+ justifyContent: 'center',
431
+ alignItems: 'center',
432
+ child: Icon({ name: 'rocket_launch', size: 32, color: colors.primary })
433
+ }),
434
+ Text({ text: 'FletBox', size: 18, weight: 'bold', color: '#ffffff' }),
435
+ Text({ text: 'UI Framework', size: 12, color: 'rgba(255,255,255,0.8)' })
436
+ ]
437
+ })
438
+ }),
439
+ body: [
440
+ DrawerItem({
441
+ icon: 'dashboard',
442
+ label: 'Dashboard',
443
+ route: '/home',
444
+ onPress: () => closeDrawer()
445
+ }),
446
+ DrawerItem({
447
+ icon: 'info',
448
+ label: 'About',
449
+ route: '/about',
450
+ onPress: () => closeDrawer()
451
+ }),
452
+ DrawerItem({
453
+ icon: 'home',
454
+ label: 'Root',
455
+ route: '/',
456
+ onPress: () => closeDrawer()
457
+ })
458
+ ],
459
+ footer: Container({
460
+ padding: 20,
461
+ borderTop: \`1px solid \${colors.border}\`,
462
+ child: Text({
463
+ text: 'Version 1.0.0',
464
+ size: 12,
465
+ color: colors.textSecondary,
466
+ align: 'center'
467
+ })
468
+ })
469
+ });
470
+ };
471
+
472
+ export default DrawerMenu;
473
+ `;
474
+
475
+ export const themesJs = () => `// database/themes.js
476
+ export let colors = {
477
+ primary: '#6366f1',
478
+ secondary: '#8b5cf6',
479
+ success: '#10b981',
480
+ warning: '#f59e0b',
481
+ danger: '#ef4444',
482
+ info: '#3b82f6',
483
+ background: '#ffffff',
484
+ surface: '#f8fafc',
485
+ text: '#0f172a',
486
+ textSecondary: '#64748b',
487
+ textDisabled: '#94a3b8',
488
+ border: '#e2e8f0',
489
+ white: '#ffffff',
490
+ black: '#000000',
491
+ overlay: 'rgba(0, 0, 0, 0.5)',
492
+ shadow: 'rgba(0, 0, 0, 0.1)'
493
+ };
494
+
495
+ export const darkColors = {
496
+ primary: '#818cf8',
497
+ secondary: '#a78bfa',
498
+ success: '#34d399',
499
+ warning: '#fbbf24',
500
+ danger: '#f87171',
501
+ info: '#60a5fa',
502
+ background: '#0f172a',
503
+ surface: '#1e293b',
504
+ text: '#f1f5f9',
505
+ textSecondary: '#94a3b8',
506
+ textDisabled: '#64748b',
507
+ border: '#334155',
508
+ white: '#ffffff',
509
+ black: '#000000',
510
+ overlay: 'rgba(0, 0, 0, 0.6)',
511
+ shadow: 'rgba(0, 0, 0, 0.3)'
512
+ };
513
+
514
+ let currentTheme = 'light';
515
+ const listeners = [];
516
+
517
+ export const setTheme = (themeName) => {
518
+ const theme = themeName === 'dark' ? darkColors : colors;
519
+ currentTheme = themeName;
520
+ Object.keys(theme).forEach(key => {
521
+ if (colors.hasOwnProperty(key)) {
522
+ colors[key] = theme[key];
523
+ }
524
+ });
525
+ const root = document.documentElement;
526
+ Object.keys(colors).forEach(key => {
527
+ root.style.setProperty(\`--color-\${key}\`, colors[key]);
528
+ });
529
+ listeners.forEach(cb => cb(colors, themeName));
530
+ };
531
+
532
+ export const getTheme = () => currentTheme;
533
+ export const toggleTheme = () => {
534
+ const newTheme = currentTheme === 'light' ? 'dark' : 'light';
535
+ setTheme(newTheme);
536
+ return newTheme;
537
+ };
538
+ export const subscribeTheme = (callback) => {
539
+ listeners.push(callback);
540
+ return () => { const i = listeners.indexOf(callback); if(i > -1) listeners.splice(i, 1); };
541
+ };
542
+
543
+ const initTheme = () => {
544
+ const root = document.documentElement;
545
+ Object.keys(colors).forEach(key => root.style.setProperty(\`--color-\${key}\`, colors[key]));
546
+ };
547
+ initTheme();
548
+ `;
549
+
550
+ export const runSh = () => `#!/bin/bash
551
+ PORT=8000
552
+ BIND="localhost"
553
+ echo "🚀 Starting FletBox development server..."
554
+ echo "📡 http://$BIND:$PORT"
555
+ echo ""
556
+ echo "Press Ctrl+C to stop"
557
+ python3 -m http.server $PORT --bind $BIND
558
+ `;
559
+
560
+ export const createBundleSh = (target = "www") => `#!/bin/bash
561
+ # createBundle.sh — Bundle UNIFICADO a \${TARGET}/ (un solo app.js + PWA)
562
+ # Uso: bash createBundle.sh [target]
563
+ set -euo pipefail
564
+ cd "\$(dirname "\$0")"
565
+ TARGET="\${1:-${target}}"
566
+
567
+ if ! npx --no-install esbuild --version >/dev/null 2>&1; then
568
+ echo "📦 Instalando esbuild (devDependency)…"
569
+ npm install --save-dev esbuild >/dev/null
570
+ fi
571
+
572
+ rm -rf "\$TARGET"
573
+ mkdir -p "\$TARGET/src" "\$TARGET/assets"
574
+
575
+ # Root static files (PWA + preview)
576
+ for f in index.html manifest.json run.sh service-worker.js; do
577
+ [ -f "\$f" ] && cp "\$f" "\$TARGET/"
578
+ done
579
+ [ -f "\$TARGET/run.sh" ] && chmod +x "\$TARGET/run.sh"
580
+
581
+ # Assets: iconos PWA + fuentes de iconos
582
+ if [ -d "src/assets" ]; then
583
+ cp -R src/assets/. "\$TARGET/src/assets/"
584
+ cp -R src/assets/. "\$TARGET/assets/"
585
+ fi
586
+
587
+ # CSS global (lo carga index.html)
588
+ if [ -f "src/styles/global.css" ]; then
589
+ mkdir -p "\$TARGET/src/styles"
590
+ cp src/styles/global.css "\$TARGET/src/styles/"
591
+ fi
592
+
593
+ # BUNDLE UNIFICADO: flet-box + widgets en UN solo app.js
594
+ echo "📦 Bundling (esbuild + tree-shaking)…"
595
+ npx esbuild src/app.js \\
596
+ --bundle --outfile="\$TARGET/src/app.js" --format=esm --minify \\
597
+ --tree-shaking=true --target=es2020 --define:FLETBOX_DEV=false \\
598
+ --external:*.css --external:*.woff2 --resolve-extensions=.js,.json
599
+
600
+ # App source code (required by the "View code" button)
601
+ for d in modules screens database components; do
602
+ [ -d "src/\$d" ] && cp -R "src/\$d" "\$TARGET/src/"
603
+ done
604
+
605
+ echo "✅ Bundle listo: \$TARGET/ Prueba: cd \$TARGET && bash run.sh"
606
+ `;
607
+
608
+ export const serviceWorkerJs = () => `// service-worker.js - PWA offline support
609
+ const CACHE_NAME = 'fletbox-v2';
610
+ // The shipped (baked-in) shell, precached so the app opens with no connection.
611
+ // loader.js owns a SEPARATE cache ('fletbox-ota') for downloaded bundles.
612
+ const PRECACHE = ['/', '/index.html', '/loader.js', '/config.json', '/src/app.js'];
613
+
614
+ self.addEventListener('install', event => {
615
+ event.waitUntil(
616
+ (async () => {
617
+ try {
618
+ const cache = await caches.open(CACHE_NAME);
619
+ const urls = PRECACHE;
620
+
621
+ for (const url of urls) {
622
+ try {
623
+ const response = await fetch(url);
624
+ if (response.ok) {
625
+ await cache.put(url, response);
626
+ } else {
627
+ console.log('🔧 [INSTALL] ⚠️ Not cached: ' + url + ' - status: ' + response.status);
628
+ }
629
+ } catch (err) {
630
+ console.log('🔧 [INSTALL] ❌ Error caching ' + url + ':', err.message);
631
+ }
632
+ }
633
+
634
+ self.skipWaiting();
635
+ } catch (error) {
636
+ console.error('🔧 [INSTALL] ❌ Fatal error:', error);
637
+ }
638
+ })()
639
+ );
640
+ });
641
+
642
+ self.addEventListener('activate', event => {
643
+ console.log('🔧 [ACTIVATE] Activate event started');
644
+
645
+ event.waitUntil(
646
+ (async () => {
647
+ try {
648
+ const cacheNames = await caches.keys();
649
+ for (const cacheName of cacheNames) {
650
+ if (cacheName !== CACHE_NAME && cacheName !== 'fletbox-ota') {
651
+ await caches.delete(cacheName);
652
+ }
653
+ }
654
+ await self.clients.claim();
655
+ } catch (error) {
656
+ console.error('🔧 [ACTIVATE] ❌ Error:', error);
657
+ }
658
+ })()
659
+ );
660
+ });
661
+
662
+ // Hybrid caching:
663
+ // - Static assets (fonts/icons/images/css): cache-first (they are immutable
664
+ // per release and re-fetched only when missing).
665
+ // - Updatable shell (html/js/json, incl. src/app.js + config.json):
666
+ // network-first so a new release is picked up immediately, falling back to
667
+ // the cache when offline. This is what keeps OTA correct: the service worker
668
+ // must never hand back a stale app.js while online.
669
+ // Cross-origin requests (the remote OTA config/bundle) are left to the network.
670
+ self.addEventListener('fetch', event => {
671
+ const req = event.request;
672
+ if (req.method !== 'GET') return;
673
+ const url = new URL(req.url);
674
+ if (url.origin !== self.location.origin) return;
675
+
676
+ const isStatic = /\\.(png|jpe?g|svg|gif|webp|ico|woff2?|ttf|otf|css)$/i.test(url.pathname);
677
+
678
+ event.respondWith(
679
+ (async () => {
680
+ const cache = await caches.open(CACHE_NAME);
681
+ if (isStatic) {
682
+ const hit = await cache.match(req);
683
+ if (hit) return hit;
684
+ }
685
+ try {
686
+ const fresh = await fetch(req, { cache: 'no-store' });
687
+ if (fresh.ok) cache.put(req, fresh.clone());
688
+ return fresh;
689
+ } catch (err) {
690
+ const cached = await cache.match(req);
691
+ if (cached) return cached;
692
+ if (req.mode === 'navigate') {
693
+ const shell = await cache.match('/index.html');
694
+ if (shell) return shell;
695
+ }
696
+ return Response.error();
697
+ }
698
+ })()
699
+ );
700
+ });
701
+ `;
702
+
703
+ export const packageJson = (name) => `{
704
+ "name": ${JSON.stringify(name)},
705
+ "version": "1.0.0",
706
+ "type": "module",
707
+ "scripts": {
708
+ "dev": "bash run.sh",
709
+ "build": "flet-box createBundle www",
710
+ "start": "npm run dev",
711
+ "android:init": "npm run build && cap add android",
712
+ "android:sync": "npm run build && cap sync android && npm run android:icons",
713
+ "android:icons": "node scripts/sync-android-icons.mjs",
714
+ "android:open": "cap open android",
715
+ "android:run": "cap run android",
716
+ "ios:init": "npm run build && cap add ios",
717
+ "ios:sync": "npm run build && cap sync ios && npm run ios:icons",
718
+ "ios:icons": "node scripts/sync-ios-assets.mjs",
719
+ "ios:open": "cap open ios",
720
+ "ios:run": "cap run ios"
721
+ },
722
+ "dependencies": {
723
+ "@capacitor/android": "^7.0.0",
724
+ "@capacitor/core": "^7.0.0",
725
+ "@capacitor/ios": "^7.0.0",
726
+ "flet-box": "^1.0.0"
727
+ },
728
+ "devDependencies": {
729
+ "@capacitor/cli": "^7.0.0",
730
+ "sharp": "^0.35.5"
731
+ },
732
+ "engines": {
733
+ "node": ">=20.9.0"
734
+ }
735
+ }`;
736
+
737
+ export const capacitorConfig = (appName, appId) => `${JSON.stringify(
738
+ {
739
+ appId,
740
+ appName,
741
+ webDir: "www",
742
+ backgroundColor: "#1a1a2e",
743
+ server: { androidScheme: "https", iosScheme: "https" },
744
+ },
745
+ null,
746
+ 2,
747
+ )}\n`;
748
+
749
+ // Placeholder update URL used when `flet-box create` is run without --repo.
750
+ // The user replaces TU-USUARIO/TU-REPO with their real public GitHub names.
751
+ export const OTA_PLACEHOLDER_URL = "https://TU-USUARIO.github.io/TU-REPO/";
752
+
753
+ // OTA manifest written to every generated project. The loader (www/loader.js)
754
+ // reads `update.url` via www/config.json to know where to fetch updates.
755
+ export const fletboxConfig = (url = OTA_PLACEHOLDER_URL) => `${JSON.stringify(
756
+ {
757
+ update: {
758
+ url,
759
+ mode: "prompt",
760
+ channel: "stable",
761
+ mandatory: false,
762
+ minNativeVersion: "0.0.0",
763
+ notes: "",
764
+ },
765
+ },
766
+ null,
767
+ 2,
768
+ )}\n`;
769
+
770
+ export const syncAndroidIconsScript = () => `import { mkdir, readFile, writeFile } from "node:fs/promises";
771
+ import path from "node:path";
772
+ import { fileURLToPath } from "node:url";
773
+ import sharp from "sharp";
774
+
775
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
776
+ const androidRes = path.join(projectRoot, "android", "app", "src", "main", "res");
777
+ const logoPath = path.join(projectRoot, "assets", "logo.png");
778
+ const backgroundPath = path.join(androidRes, "values", "ic_launcher_background.xml");
779
+
780
+ for (const requiredPath of [logoPath, backgroundPath]) {
781
+ try {
782
+ await readFile(requiredPath);
783
+ } catch {
784
+ throw new Error(
785
+ \`Missing \${requiredPath}. Run npm run android:init before generating Android icons.\`,
786
+ );
787
+ }
788
+ }
789
+
790
+ const logo = await readFile(logoPath);
791
+ const densities = [
792
+ ["mdpi", 48],
793
+ ["hdpi", 72],
794
+ ["xhdpi", 96],
795
+ ["xxhdpi", 144],
796
+ ["xxxhdpi", 192],
797
+ ];
798
+ const background = { r: 26, g: 26, b: 46 };
799
+
800
+ for (const [density, size] of densities) {
801
+ const directory = path.join(androidRes, \`mipmap-\${density}\`);
802
+ await mkdir(directory, { recursive: true });
803
+ const icon = await sharp(logo)
804
+ .resize(size, size, { fit: "contain" })
805
+ .flatten({ background })
806
+ .png()
807
+ .toBuffer();
808
+ const foregroundLayer = await sharp(logo)
809
+ .resize(Math.round(size * 0.66), Math.round(size * 0.66), { fit: "contain" })
810
+ .png()
811
+ .toBuffer();
812
+ const foreground = await sharp({
813
+ create: {
814
+ width: size,
815
+ height: size,
816
+ channels: 4,
817
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
818
+ },
819
+ })
820
+ .composite([{ input: foregroundLayer, gravity: "centre" }])
821
+ .png()
822
+ .toBuffer();
823
+
824
+ await Promise.all([
825
+ writeFile(path.join(directory, "ic_launcher.png"), icon),
826
+ writeFile(path.join(directory, "ic_launcher_round.png"), icon),
827
+ writeFile(path.join(directory, "ic_launcher_foreground.png"), foreground),
828
+ ]);
829
+ }
830
+
831
+ const xml = await readFile(backgroundPath, "utf8");
832
+ const backgroundColor = /(<color name="ic_launcher_background">)#(?:[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})(<[/]color>)/;
833
+ if (!backgroundColor.test(xml)) {
834
+ throw new Error(\`Invalid adaptive icon background resource: \${backgroundPath}\`);
835
+ }
836
+ await writeFile(backgroundPath, xml.replace(backgroundColor, "$1#1a1a2e$2"));
837
+
838
+ // SPLASH SCREENS
839
+ // The launch theme (AppTheme.NoActionBarLaunch) shows @drawable/splash while the
840
+ // WebView boots. Draw the app icon (the logo flattened on the brand color),
841
+ // centred and sized to ~30% of the shorter side, with rounded corners whose
842
+ // radius is 24dp at the mdpi baseline and scales with the device density.
843
+ const ICON_SCALE = 0.3;
844
+ const BORDER_RADIUS_DP = 24;
845
+
846
+ const roundedIcon = async (size, radius) => {
847
+ const tile = await sharp(logo)
848
+ .resize(size, size, { fit: "contain" })
849
+ .flatten({ background })
850
+ .png()
851
+ .toBuffer();
852
+ const mask = Buffer.from(
853
+ \`<svg xmlns="http://www.w3.org/2000/svg" width="\${size}" height="\${size}"><rect width="\${size}" height="\${size}" rx="\${radius}" ry="\${radius}"/></svg>\`,
854
+ );
855
+ return sharp(tile).composite([{ input: mask, blend: "dest-in" }]).png().toBuffer();
856
+ };
857
+
858
+ const splashScreens = [
859
+ ["drawable", 480, 320, 1],
860
+ ["drawable-port-mdpi", 320, 480, 1],
861
+ ["drawable-port-hdpi", 480, 800, 1.5],
862
+ ["drawable-port-xhdpi", 720, 1280, 2],
863
+ ["drawable-port-xxhdpi", 960, 1600, 3],
864
+ ["drawable-port-xxxhdpi", 1280, 1920, 4],
865
+ ["drawable-land-mdpi", 480, 320, 1],
866
+ ["drawable-land-hdpi", 800, 480, 1.5],
867
+ ["drawable-land-xhdpi", 1280, 720, 2],
868
+ ["drawable-land-xxhdpi", 1600, 960, 3],
869
+ ["drawable-land-xxxhdpi", 1920, 1280, 4],
870
+ ];
871
+
872
+ for (const [directory, width, height, scale] of splashScreens) {
873
+ const target = path.join(androidRes, directory);
874
+ await mkdir(target, { recursive: true });
875
+ const iconSize = Math.round(ICON_SCALE * Math.min(width, height));
876
+ const radius = Math.min(Math.round(BORDER_RADIUS_DP * scale), Math.floor(iconSize / 2));
877
+ const icon = await roundedIcon(iconSize, radius);
878
+ const splash = await sharp({
879
+ create: { width, height, channels: 4, background },
880
+ })
881
+ .composite([{ input: icon, gravity: "centre" }])
882
+ .removeAlpha()
883
+ .png()
884
+ .toBuffer();
885
+ await writeFile(path.join(target, "splash.png"), splash);
886
+ }
887
+
888
+ console.log("Android launcher icons and splash screens generated from assets/logo.png.");
889
+ `;
890
+
891
+ export const syncIOSAssetsScript = () => `import { access, readFile, writeFile } from "node:fs/promises";
892
+ import path from "node:path";
893
+ import { fileURLToPath } from "node:url";
894
+ import sharp from "sharp";
895
+
896
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
897
+ const nativeAppDir = path.join(projectRoot, "ios", "App", "App");
898
+ const appIconSet = path.join(nativeAppDir, "Assets.xcassets", "AppIcon.appiconset");
899
+ const splashSet = path.join(nativeAppDir, "Assets.xcassets", "Splash.imageset");
900
+ const infoPlistPath = path.join(nativeAppDir, "Info.plist");
901
+ const launchScreenPath = path.join(nativeAppDir, "Base.lproj", "LaunchScreen.storyboard");
902
+ const logoPath = path.join(projectRoot, "assets", "logo.png");
903
+
904
+ // Brand background, the same #1a1a2e as theme-color in index.html and manifest.json.
905
+ const background = { r: 26, g: 26, b: 46 };
906
+ const backgroundComponent = (value) => Number((value / 255).toFixed(6));
907
+
908
+ const requireExists = async (target) => {
909
+ try {
910
+ await access(target);
911
+ } catch {
912
+ throw new Error(
913
+ "Missing " + target + ". Run \`npm run ios:init\` before generating iOS assets.",
914
+ );
915
+ }
916
+ };
917
+
918
+ for (const target of [logoPath, appIconSet, splashSet, infoPlistPath, launchScreenPath]) {
919
+ await requireExists(target);
920
+ }
921
+
922
+ const logo = await readFile(logoPath);
923
+ const logoMetadata = await sharp(logo).metadata();
924
+ if ((logoMetadata.width ?? 0) < 512 || (logoMetadata.height ?? 0) < 512) {
925
+ throw new Error(
926
+ "assets/logo.png must be at least 512x512 pixels, found " +
927
+ logoMetadata.width +
928
+ "x" +
929
+ logoMetadata.height +
930
+ ".",
931
+ );
932
+ }
933
+
934
+ const readContents = async (directory) =>
935
+ JSON.parse(await readFile(path.join(directory, "Contents.json"), "utf8"));
936
+
937
+ // APP ICON
938
+ // The Capacitor template ships one universal 1024x1024 slot. Slot names and
939
+ // sizes come from Contents.json instead of being hardcoded, so a template with
940
+ // several slots keeps working. iOS rejects icons with an alpha channel, hence
941
+ // the flatten onto the brand color.
942
+ const iconContents = await readContents(appIconSet);
943
+ const iconSlots = iconContents.images.filter((image) => image.filename);
944
+ if (iconSlots.length === 0) {
945
+ throw new Error("No icon slots found in " + path.join(appIconSet, "Contents.json"));
946
+ }
947
+
948
+ const slotPixels = (image) => {
949
+ const base = Number(image.size ? image.size.split("x")[0] : 0);
950
+ if (!base) return 1024;
951
+ const scale = image.scale === "3x" ? 3 : image.scale === "2x" ? 2 : 1;
952
+ return Math.round(base * scale);
953
+ };
954
+
955
+ for (const slot of iconSlots) {
956
+ const size = slotPixels(slot);
957
+ const icon = await sharp(logo)
958
+ .resize(size, size, { fit: "contain" })
959
+ .flatten({ background })
960
+ .png()
961
+ .toBuffer();
962
+ await writeFile(path.join(appIconSet, slot.filename), icon);
963
+ }
964
+
965
+ // LAUNCH IMAGES
966
+ // LaunchScreen.storyboard stretches the "Splash" image with scaleAspectFill, so
967
+ // a square brand image covers every screen. Each file keeps the dimensions the
968
+ // template ships with, which the asset catalog scales per device.
969
+ // The app icon (logo flattened on the brand color) is centred at ~30% of the
970
+ // side with rounded corners. The corner radius is 24dp at the mdpi baseline, so
971
+ // the ratio comes from a 96px icon: 24 / 96.
972
+ const splashContents = await readContents(splashSet);
973
+ const splashSlots = splashContents.images.filter((image) => image.filename);
974
+ const ICON_SCALE = 0.3;
975
+ const BORDER_RADIUS_RATIO = 24 / 96;
976
+
977
+ for (const slot of splashSlots) {
978
+ const target = path.join(splashSet, slot.filename);
979
+ const metadata = await sharp(target).metadata();
980
+ const side = metadata.width && metadata.width === metadata.height ? metadata.width : 2732;
981
+ const iconSize = Math.round(side * ICON_SCALE);
982
+ const radius = Math.min(
983
+ Math.round(iconSize * BORDER_RADIUS_RATIO),
984
+ Math.floor(iconSize / 2),
985
+ );
986
+ const tile = await sharp(logo)
987
+ .resize(iconSize, iconSize, { fit: "contain" })
988
+ .flatten({ background })
989
+ .png()
990
+ .toBuffer();
991
+ const mask = Buffer.from(
992
+ \`<svg xmlns="http://www.w3.org/2000/svg" width="\${iconSize}" height="\${iconSize}"><rect width="\${iconSize}" height="\${iconSize}" rx="\${radius}" ry="\${radius}"/></svg>\`,
993
+ );
994
+ const badge = await sharp(tile)
995
+ .composite([{ input: mask, blend: "dest-in" }])
996
+ .png()
997
+ .toBuffer();
998
+ const splash = await sharp({
999
+ create: {
1000
+ width: side,
1001
+ height: side,
1002
+ channels: 4,
1003
+ background,
1004
+ },
1005
+ })
1006
+ .composite([{ input: badge, gravity: "centre" }])
1007
+ .removeAlpha()
1008
+ .png()
1009
+ .toBuffer();
1010
+ await writeFile(target, splash);
1011
+ }
1012
+
1013
+ // LAUNCH SCREEN BACKGROUND
1014
+ // The splash image is laid out inside the safe area, so the notch strip and the
1015
+ // home indicator keep the storyboard background color. Left as
1016
+ // systemBackgroundColor it flashes white over the dark launch image.
1017
+ const storyboard = await readFile(launchScreenPath, "utf8");
1018
+ const brandedColor =
1019
+ '<color key="backgroundColor" red="' +
1020
+ backgroundComponent(background.r) +
1021
+ '" green="' +
1022
+ backgroundComponent(background.g) +
1023
+ '" blue="' +
1024
+ backgroundComponent(background.b) +
1025
+ '" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>';
1026
+ const systemColor = '<color key="backgroundColor" systemColor="systemBackgroundColor"/>';
1027
+ const brandedStoryboard = storyboard.split(systemColor).join(brandedColor);
1028
+ if (brandedStoryboard !== storyboard) {
1029
+ await writeFile(launchScreenPath, brandedStoryboard);
1030
+ }
1031
+
1032
+ // STATUS BAR
1033
+ // FletBox apps paint a dark app bar, and the template Info.plist leaves
1034
+ // UIViewControllerBasedStatusBarAppearance enabled, which makes the plist status
1035
+ // bar style ignored. Disable it so the light content style actually applies.
1036
+ const setPlistValue = (xml, key, value) => {
1037
+ const openTag = "<key>" + key + "</key>";
1038
+ const valueTag = value.startsWith("<") ? value : "<string>" + value + "</string>";
1039
+ const keyStart = xml.indexOf(openTag);
1040
+ if (keyStart !== -1) {
1041
+ // The value follows on the next indented line, so jump to its first tag.
1042
+ const valueStart = xml.indexOf("<", keyStart + openTag.length);
1043
+ if (xml.startsWith("<string>", valueStart)) {
1044
+ const valueEnd = xml.indexOf("</string>", valueStart) + "</string>".length;
1045
+ return xml.slice(0, valueStart) + valueTag + xml.slice(valueEnd);
1046
+ }
1047
+ if (xml.startsWith("<true", valueStart) || xml.startsWith("<false", valueStart)) {
1048
+ const valueEnd = xml.indexOf("/>", valueStart) + 2;
1049
+ return xml.slice(0, valueStart) + valueTag + xml.slice(valueEnd);
1050
+ }
1051
+ }
1052
+ const closing = xml.lastIndexOf("</dict>");
1053
+ return (
1054
+ xml.slice(0, closing) +
1055
+ "\\t" + openTag + "\\n\\t" + valueTag + "\\n" +
1056
+ xml.slice(closing)
1057
+ );
1058
+ };
1059
+
1060
+ const plist = await readFile(infoPlistPath, "utf8");
1061
+ const brandedPlist = setPlistValue(
1062
+ setPlistValue(plist, "UIStatusBarStyle", "UIStatusBarStyleLightContent"),
1063
+ "UIViewControllerBasedStatusBarAppearance",
1064
+ "<false />",
1065
+ );
1066
+ if (brandedPlist !== plist) {
1067
+ await writeFile(infoPlistPath, brandedPlist);
1068
+ }
1069
+
1070
+ console.log(
1071
+ "iOS app icon, launch images and launch screen generated from assets/logo.png " +
1072
+ "(" + iconSlots.length + " icon slot(s), " + splashSlots.length + " launch image(s)).",
1073
+ );`;
1074
+
1075
+ export const manifest = (appName = "FletBox App") => `{
1076
+ "name": ${JSON.stringify(appName)},
1077
+ "short_name": ${JSON.stringify(appName.slice(0, 12))},
1078
+ "description": ${JSON.stringify(`${appName} built with Flet-Box`)},
1079
+ "start_url": "/",
1080
+ "scope": "/",
1081
+ "display": "standalone",
1082
+ "display_override": ["window-controls-overlay"],
1083
+ "theme_color": "#1a1a2e",
1084
+ "background_color": "#1a1a2e",
1085
+ "orientation": "any",
1086
+ "icons": [
1087
+ {
1088
+ "src": "./src/assets/icon-192.png",
1089
+ "sizes": "192x192",
1090
+ "type": "image/png",
1091
+ "purpose": "any"
1092
+ },
1093
+ {
1094
+ "src": "./src/assets/icon-192-maskable.png",
1095
+ "sizes": "192x192",
1096
+ "type": "image/png",
1097
+ "purpose": "maskable"
1098
+ },
1099
+ {
1100
+ "src": "./src/assets/icon-512.png",
1101
+ "sizes": "512x512",
1102
+ "type": "image/png",
1103
+ "purpose": "any"
1104
+ },
1105
+ {
1106
+ "src": "./src/assets/icon-512-maskable.png",
1107
+ "sizes": "512x512",
1108
+ "type": "image/png",
1109
+ "purpose": "maskable"
1110
+ }
1111
+ ],
1112
+ "launch_handler": {
1113
+ "client_mode": "focus-existing"
1114
+ },
1115
+ "handle_links": "preferred"
1116
+ }
1117
+ `;
1118
+
1119
+ // GitHub Actions workflow that builds web + Android + iOS in ONE run.
1120
+ // Placed at .github/workflows/ (the only location GitHub reads), NOT inside www/.
1121
+ // NOTE: every GitHub `${{ }}` expression is escaped as `\${{ }}` because this is
1122
+ // a JS template literal — an unescaped `${` would be parsed as interpolation.
1123
+ export const githubWorkflow = (appName = "FletBox App", appId = "com.fletbox.app") => {
1124
+ const slug = appName
1125
+ .toLowerCase()
1126
+ .replace(/[^a-z0-9]+/g, "-")
1127
+ .replace(/^-+|-+$/g, "")
1128
+ .slice(0, 40) || "fletbox-app";
1129
+ return `name: Build ${appName}
1130
+
1131
+ # One push (or manual trigger) fans out into three parallel jobs:
1132
+ # web -> bundles the PWA; publishes to GitHub Pages ONLY on release
1133
+ # tags (v*), which is what triggers OTA self-updates
1134
+ # android -> compiles a debug APK and uploads it as an artifact
1135
+ # ios -> compiles a simulator .app and uploads it as an artifact
1136
+ # All three run at the same time on separate runners.
1137
+ on:
1138
+ push:
1139
+ branches: [ main ]
1140
+ tags: [ 'v*' ]
1141
+ pull_request:
1142
+ branches: [ main ]
1143
+ workflow_dispatch:
1144
+
1145
+ # Cancel superseded runs on the same branch to save Actions minutes.
1146
+ concurrency:
1147
+ group: \${{ github.workflow }}-\${{ github.ref }}
1148
+ cancel-in-progress: true
1149
+
1150
+ # The generated app depends on the public \`flet-box\` npm package. If you are
1151
+ # developing against a local copy, publish it (or vendor it) before these jobs
1152
+ # can \`npm install\` it. package-lock.json is gitignored, so we use npm install.
1153
+
1154
+ jobs:
1155
+ # ---------- WEB: bundle + GitHub Pages preview ----------
1156
+ web:
1157
+ name: Web (Pages)
1158
+ runs-on: ubuntu-latest
1159
+ permissions:
1160
+ contents: read
1161
+ pages: write
1162
+ id-token: write
1163
+ environment:
1164
+ name: github-pages
1165
+ url: \${{ steps.deployment.outputs.page_url }}
1166
+ steps:
1167
+ - uses: actions/checkout@v5
1168
+
1169
+ - uses: actions/setup-node@v5
1170
+ with:
1171
+ node-version: 22
1172
+
1173
+ - name: Install dependencies
1174
+ run: npm install
1175
+
1176
+ - name: Bundle web app (flet-box createBundle www)
1177
+ run: npm run build
1178
+
1179
+ # Pages is the OTA update source (www/config.json), so it is published
1180
+ # ONLY for release tags (v*) created by \`flet-box release\`. Ordinary
1181
+ # pushes still build (to catch breakage in CI) but never publish an
1182
+ # update, so test builds cannot reach installed apps.
1183
+ - name: Upload Pages artifact
1184
+ if: startsWith(github.ref, 'refs/tags/v')
1185
+ uses: actions/upload-pages-artifact@v5
1186
+ with:
1187
+ path: www
1188
+
1189
+ # Enable Pages first: Settings -> Pages -> Source = "GitHub Actions".
1190
+ - name: Deploy to GitHub Pages
1191
+ id: deployment
1192
+ if: startsWith(github.ref, 'refs/tags/v')
1193
+ uses: actions/deploy-pages@v5
1194
+
1195
+ # ---------- ANDROID: debug APK ----------
1196
+ android:
1197
+ name: Android (APK)
1198
+ runs-on: ubuntu-latest
1199
+ steps:
1200
+ - uses: actions/checkout@v5
1201
+
1202
+ - uses: actions/setup-node@v5
1203
+ with:
1204
+ node-version: 22
1205
+
1206
+ - uses: actions/setup-java@v5
1207
+ with:
1208
+ distribution: temurin
1209
+ java-version: 17
1210
+
1211
+ # ubuntu runners ship the Android SDK; this step accepts licences and
1212
+ # installs any packages the Capacitor Gradle build asks for.
1213
+ - uses: android-actions/setup-android@v4
1214
+
1215
+ - name: Install dependencies
1216
+ run: npm install
1217
+
1218
+ - name: Bundle web app
1219
+ run: npm run build
1220
+
1221
+ - name: Add Android platform
1222
+ run: npx cap add android
1223
+
1224
+ - name: Sync web assets to Android
1225
+ run: npx cap sync android
1226
+
1227
+ - name: Build debug APK
1228
+ run: |
1229
+ cd android
1230
+ ./gradlew assembleDebug --no-daemon
1231
+
1232
+ - name: Upload APK
1233
+ uses: actions/upload-artifact@v6
1234
+ with:
1235
+ name: ${slug}-android-debug
1236
+ path: android/app/build/outputs/apk/debug/app-debug.apk
1237
+ if-no-files-found: error
1238
+
1239
+ # ---------- iOS: simulator build ----------
1240
+ ios:
1241
+ name: iOS (simulator)
1242
+ runs-on: macos-latest
1243
+ steps:
1244
+ - uses: actions/checkout@v5
1245
+
1246
+ - uses: actions/setup-node@v5
1247
+ with:
1248
+ node-version: 22
1249
+
1250
+ - name: Install dependencies
1251
+ run: npm install
1252
+
1253
+ - name: Bundle web app
1254
+ run: npm run build
1255
+
1256
+ - name: Add iOS platform
1257
+ run: npx cap add ios
1258
+
1259
+ - name: Sync web assets to iOS
1260
+ run: npx cap sync ios
1261
+
1262
+ - name: Install CocoaPods
1263
+ run: |
1264
+ cd ios/App
1265
+ pod install --repo-update
1266
+
1267
+ # Signed device/App Store builds need your Apple certificates; CI here
1268
+ # builds an unsigned simulator app so no secrets are required.
1269
+ - name: Build for simulator
1270
+ run: |
1271
+ cd ios/App
1272
+ xcodebuild \\
1273
+ -workspace App.xcworkspace \\
1274
+ -scheme App \\
1275
+ -configuration Debug \\
1276
+ -sdk iphonesimulator \\
1277
+ -destination 'generic/platform=iOS Simulator' \\
1278
+ -derivedDataPath build \\
1279
+ CODE_SIGNING_ALLOWED=NO \\
1280
+ build
1281
+
1282
+ - name: Upload .app
1283
+ uses: actions/upload-artifact@v6
1284
+ with:
1285
+ name: ${slug}-ios-simulator
1286
+ path: ios/App/build/Build/Products/Debug-iphonesimulator/App.app
1287
+ if-no-files-found: error
1288
+ `;
1289
+ };
1290
+
1291
+ export const gitignore = () => `node_modules/
1292
+ dist/
1293
+ www/
1294
+ ios/build/
1295
+ ios/App/Pods/
1296
+ ios/App/App/public/
1297
+ ios/capacitor-cordova-ios-plugins/
1298
+ android/.gradle/
1299
+ android/.idea/
1300
+ android/build/
1301
+ android/app/build/
1302
+ android/local.properties
1303
+ android/capacitor-cordova-android-plugins/
1304
+ package-lock.json
1305
+ .DS_Store
1306
+ *.log
1307
+ `;
1308
+
1309
+ export const readme = (name, _template, appId) => `# ${name}
1310
+
1311
+ FletBox app with 3 screens: Root, Home and About, plus Drawer menu.
1312
+ The generated Capacitor configuration uses package id \`${appId}\`.
1313
+
1314
+ ## Quick Start
1315
+
1316
+ \`\`\`bash
1317
+ npm install
1318
+ npm run dev
1319
+ \`\`\`
1320
+
1321
+ ## Android
1322
+
1323
+ This project is preconfigured for Capacitor. Install Node.js 20.9 or newer,
1324
+ Android Studio, and the Android SDK. Then create the native Android project:
1325
+
1326
+ \`\`\`bash
1327
+ npm install
1328
+ flet-box build android
1329
+ \`\`\`
1330
+
1331
+ The command initializes Android on first use, builds and synchronizes the web
1332
+ app, generates launcher icons from \`assets/logo.png\`, and compiles a debug APK
1333
+ at \`android/app/build/outputs/apk/debug/app-debug.apk\`. It installs missing
1334
+ project npm dependencies and prints the absolute APK path when finished.
1335
+ Android Studio, Java, and the Android SDK must already be installed. Replace
1336
+ that image with your app logo before distributing. To open the native project
1337
+ in Android Studio, run \`npm run android:open\`. For a signed release, use
1338
+ **Build → Generate Signed Bundle / APK** in Android Studio. The PWA manifest
1339
+ icons are in \`src/assets/\`.
1340
+
1341
+ ## iOS
1342
+
1343
+ This project is preconfigured for Capacitor iOS. On macOS, install the full
1344
+ Xcode app, its iOS simulator runtime, and CocoaPods
1345
+ (\`sudo gem install cocoapods\` then \`pod setup\`). Node.js 20.9 or newer is
1346
+ required. Then build the simulator app:
1347
+
1348
+ \`\`\`bash
1349
+ npm install
1350
+ flet-box build ios
1351
+ \`\`\`
1352
+
1353
+ The command initializes iOS on first use, builds and synchronizes the web app,
1354
+ generates the app icon, launch images and launch screen background from
1355
+ \`assets/logo.png\`, and compiles
1356
+ \`ios/build/Build/Products/Debug-iphonesimulator/App.app\`. It prints the
1357
+ absolute path when finished. Install it on a running simulator with
1358
+ \`xcrun simctl install booted <path>\`.
1359
+
1360
+ For a real device or the App Store, build the unsigned archive and sign it with
1361
+ your Apple team:
1362
+
1363
+ \`\`\`bash
1364
+ flet-box build ios --archive
1365
+ \`\`\`
1366
+
1367
+ That writes \`ios/build/App.xcarchive\`. Open it in Xcode (Product → Archive) with
1368
+ a signing team selected to export an IPA. To open the native project directly,
1369
+ run \`npm run ios:open\`.
1370
+
1371
+ Replace \`assets/logo.png\` with a 1024×1024 logo before distributing; the icon,
1372
+ the launch images and the launch background are all generated from it. The
1373
+ launch background is the app brand color \`#1a1a2e\`, which lives in
1374
+ \`scripts/sync-ios-assets.mjs\`.
1375
+
1376
+ Capacitor uses a local HTTPS origin (\`https://localhost\`) in the Android and
1377
+ iOS WebViews. If the app calls a remote API, allow that origin in the API's
1378
+ CORS configuration and use HTTPS for the remote service.
1379
+
1380
+ ## Continuous Integration (GitHub Actions)
1381
+
1382
+ Every project ships with \`.github/workflows/build.yml\`. Once the repo is on
1383
+ GitHub, each push to \`main\` (or a manual **Run workflow**) fans out into three
1384
+ parallel jobs:
1385
+
1386
+ - **web** — bundles the app into \`www/\` and deploys it to **GitHub Pages** so you
1387
+ can view it in a browser.
1388
+ - **android** — compiles a debug **APK** and uploads it as a run artifact.
1389
+ - **ios** — compiles an unsigned **simulator .app** and uploads it as an artifact.
1390
+
1391
+ ### Run it on the web (one-time setup)
1392
+
1393
+ 1. Push this project to a GitHub repository.
1394
+ 2. Open **Settings → Pages** in that repo.
1395
+ 3. Set **Source** to **GitHub Actions** (not "Deploy from a branch").
1396
+ 4. Push to \`main\`, or open the **Actions** tab and click **Run workflow**.
1397
+
1398
+ The site goes live at \`https://<your-user>.github.io/<repo-name>/\`. That is a
1399
+ sub-path, so \`index.html\` uses relative asset paths and works without changes.
1400
+
1401
+ ### Download the mobile artifacts
1402
+
1403
+ Open the workflow run in the **Actions** tab; the Android APK and the iOS
1404
+ \`.app\` are listed under **Artifacts** at the bottom of the run summary.
1405
+
1406
+ > The workflow installs dependencies with \`npm install\`, which resolves the
1407
+ > \`flet-box\` package from npm. If you develop against a local copy of the
1408
+ > framework, publish it first — or add a step that checks out the framework and
1409
+ > runs \`npm pkg set dependencies.flet-box=file:<path>\` before \`npm install\`.
1410
+
1411
+ ## Ads (AdSense & AdMob) — optional
1412
+
1413
+ Both ad integrations ship **disabled by default**, ready to switch on. Replace
1414
+ every \`ca-pub-XXXXXXXXXXXXXXXX\` / \`ca-app-pub-XXXXXXXXXXXXXXXX\` placeholder
1415
+ with your own IDs from the AdSense/AdMob console. While developing keep test
1416
+ mode on and never click your own live ads — AdMob can ban the account.
1417
+
1418
+ ### AdSense (web)
1419
+
1420
+ \`index.html\` has a commented-out AdSense \`<script>\` in the \`<head>\`. To
1421
+ enable: get an approved domain at https://www.google.com/adsense, put your
1422
+ publisher ID in the script, and delete the surrounding \`<!--\` / \`-->\` so the
1423
+ tag goes live. Ads only render on an approved domain, not on localhost. You can
1424
+ also drop the \`AdSense\` widget into any screen (see
1425
+ https://github.com/kuko53348/flet-box/blob/refactory/docs/widget/AdSense.md).
1426
+
1427
+ ### AdMob (Android / iOS)
1428
+
1429
+ 1. Install the Capacitor plugin:
1430
+
1431
+ \`\`\`bash
1432
+ npm install @capacitor-community/admob
1433
+ \`\`\`
1434
+
1435
+ 2. Add your AdMob **App ID** to the native projects.
1436
+
1437
+ Android — \`android/app/src/main/AndroidManifest.xml\`, inside \`<application>\`:
1438
+
1439
+ \`\`\`xml
1440
+ <meta-data
1441
+ android:name="com.google.android.gms.ads.APPLICATION_ID"
1442
+ android:value="ca-app-pub-XXXXXXXXXXXXXXXX~YYYYYYYYYY"/>
1443
+ \`\`\`
1444
+
1445
+ iOS — \`ios/App/App/Info.plist\`, inside the top-level \`<dict>\`:
1446
+
1447
+ \`\`\`xml
1448
+ <key>GADApplicationIdentifier</key>
1449
+ <string>ca-app-pub-XXXXXXXXXXXXXXXX~YYYYYYYYYY</string>
1450
+ \`\`\`
1451
+
1452
+ 3. Sync and rebuild:
1453
+
1454
+ \`\`\`bash
1455
+ npm run android:sync && npm run ios:sync
1456
+ \`\`\`
1457
+
1458
+ 4. Uncomment the \`AdMob\` import and the \`AdMob({...})\` widget in
1459
+ \`src/screens/HomeScreen.js\` (they are marked with ADMOB comments). Full
1460
+ props in
1461
+ https://github.com/kuko53348/flet-box/blob/refactory/docs/widget/AdMob.md.
1462
+
1463
+ ## Over-the-air updates (OTA)
1464
+
1465
+ This app can update its web layer without a new store submission. The project
1466
+ ships with \`fletbox.config.json\`, which tells the app where to check for
1467
+ updates:
1468
+
1469
+ \`\`\`json
1470
+ {
1471
+ "update": {
1472
+ "url": "https://TU-USUARIO.github.io/TU-REPO/",
1473
+ "mode": "prompt"
1474
+ }
1475
+ }
1476
+ \`\`\`
1477
+
1478
+ - The GitHub repository **must be public**.
1479
+ - Replace \`TU-USUARIO\` with your GitHub username and \`TU-REPO\` with the
1480
+ repository name, or set \`update.url\` to any HTTPS host that serves your
1481
+ \`config.json\` + bundle.
1482
+ - Enable GitHub Pages: **Settings → Pages → Source: GitHub Actions**.
1483
+ - Cut a release with \`flet-box release --push\` (bumps the version, rebuilds
1484
+ \`www/\`, commits and tags \`vX.Y.Z\`). CI then deploys \`www/\` and installed
1485
+ apps self-update the next time they open.
1486
+ - The update URL is also reflected in \`www/config.json\` after \`flet-box
1487
+ createBundle www\`; it is baked into Android/iOS builds at \`cap sync\` time.
1488
+ - Only web files are updated OTA. Native changes (plugins, permissions,
1489
+ manifest/Info.plist edits) still require a new store submission.
1490
+
1491
+ ## Features
1492
+
1493
+ - Welcome screen with rocket icon
1494
+ - Dashboard screen with welcome message
1495
+ - About screen with framework info
1496
+ - Drawer menu for navigation
1497
+ - PWA icons and Capacitor Android launcher/splash assets
1498
+ - Capacitor iOS project, icon and launch screen assets
1499
+ - Dark/Light theme support
1500
+
1501
+ ## Project Structure
1502
+
1503
+ \`\`\`
1504
+ ├── src/
1505
+ │ ├── app.js
1506
+ │ ├── components/
1507
+ │ │ └── layouts/
1508
+ │ │ └── DrawerMenu.js
1509
+ │ ├── screens/
1510
+ │ │ ├── RootScreen.js
1511
+ │ │ ├── HomeScreen.js
1512
+ │ │ └── AboutScreen.js
1513
+ │ └── database/
1514
+ │ └── themes.js
1515
+ ├── index.html
1516
+ ├── package.json
1517
+ └── run.sh
1518
+ \`\`\`
1519
+
1520
+ ## License
1521
+
1522
+ MIT
1523
+ `;
1524
+ // ============================================================
1525
+ // ADDITIONS: MISSING SCREENS, COMPONENTS, AND APP TEMPLATES
1526
+ // ============================================================
1527
+
1528
+ // Additional screen for adaptive template
1529
+ export const profileScreenJs = () => `// screens/ProfileScreen.js
1530
+ import { Container, Column, Text, Icon, colors } from 'flet-box';
1531
+
1532
+ export const ProfileScreen = () => {
1533
+ return Container({
1534
+ flex: 1,
1535
+ justifyContent: 'center',
1536
+ alignItems: 'center',
1537
+ padding: 24,
1538
+ backgroundColor: colors.background,
1539
+ child: Column({
1540
+ gap: 20,
1541
+ alignItems: 'center',
1542
+ children: [
1543
+ Icon({ name: 'person', size: 80, color: colors.primary }),
1544
+ Text({ text: 'User Profile', size: 28, weight: 'bold', color: colors.text }),
1545
+ Text({ text: 'This is your profile page.', size: 16, color: colors.textSecondary })
1546
+ ]
1547
+ })
1548
+ });
1549
+ };
1550
+ `;
1551
+
1552
+ // Screens index for easier imports
1553
+ export const screensIndexJs = () => `// screens/index.js
1554
+ export { RootScreen } from './RootScreen.js';
1555
+ export { HomeScreen } from './HomeScreen.js';
1556
+ export { AboutScreen } from './AboutScreen.js';
1557
+ export { ProfileScreen } from './ProfileScreen.js';
1558
+ `;
1559
+
1560
+ // Layout components
1561
+ export const appBarComponentJs = () => `// components/layouts/AppBarComponent.js
1562
+ import { AppBar, Icon, colors, openDrawer } from 'flet-box';
1563
+
1564
+ export const AppBarComponent = ({ title, isMobile = false, showBack = false, onBack }) => {
1565
+ return AppBar({
1566
+ title: title,
1567
+ backgroundColor: colors.primary,
1568
+ titleColor: '#ffffff',
1569
+ centerTitle: true,
1570
+ showBackButton: showBack,
1571
+ leading: isMobile ? Icon({
1572
+ name: 'menu',
1573
+ size: 24,
1574
+ color: '#ffffff',
1575
+ onclick: () => openDrawer()
1576
+ }) : null,
1577
+ onBackPress: onBack,
1578
+ elevation: 2
1579
+ });
1580
+ };
1581
+ `;
1582
+
1583
+ export const bottomNavJs = () => `// components/layouts/BottomNav.js
1584
+ import { BottomNavigation, colors, goTo } from 'flet-box';
1585
+
1586
+ export const BottomNav = ({ items = [], currentRoute }) => {
1587
+ const defaultItems = [
1588
+ { icon: 'home', label: 'Home', route: '/home' },
1589
+ { icon: 'info', label: 'About', route: '/about' }
1590
+ ];
1591
+ const navItems = items.length ? items : defaultItems;
1592
+ return BottomNavigation({
1593
+ items: navItems,
1594
+ currentRoute: currentRoute,
1595
+ onTabChange: (route) => goTo(route),
1596
+ backgroundColor: colors.surface,
1597
+ selectedColor: colors.primary
1598
+ });
1599
+ };
1600
+ `;
1601
+
1602
+ export const sidebarJs = () => `// components/layouts/Sidebar.js
1603
+ import { Container, Column, Row, Text, Icon, colors, goTo } from 'flet-box';
1604
+
1605
+ export const Sidebar = ({ items = [], currentRoute }) => {
1606
+ const defaultItems = [
1607
+ { icon: 'dashboard', label: 'Dashboard', route: '/home' },
1608
+ { icon: 'info', label: 'About', route: '/about' },
1609
+ { icon: 'home', label: 'Root', route: '/' }
1610
+ ];
1611
+ const menuItems = items.length ? items : defaultItems;
1612
+ return Container({
1613
+ width: 260,
1614
+ backgroundColor: colors.surface,
1615
+ borderRight: \`1px solid \${colors.border}\`,
1616
+ height: '100vh',
1617
+ position: 'fixed',
1618
+ left: 0,
1619
+ top: 0,
1620
+ child: Column({
1621
+ children: [
1622
+ Container({
1623
+ padding: 24,
1624
+ backgroundColor: colors.primary,
1625
+ child: Column({
1626
+ alignItems: 'center',
1627
+ children: [
1628
+ Icon({ name: 'rocket_launch', size: 40, color: '#ffffff' }),
1629
+ Text({ text: 'FletBox', size: 20, weight: 'bold', color: '#ffffff', marginTop: 12 })
1630
+ ]
1631
+ })
1632
+ }),
1633
+ ...menuItems.map(item => Container({
1634
+ padding: '12px 20px',
1635
+ cursor: 'pointer',
1636
+ backgroundColor: currentRoute === item.route ? \`\${colors.primary}15\` : 'transparent',
1637
+ borderLeft: currentRoute === item.route ? \`3px solid \${colors.primary}\` : 'none',
1638
+ child: Row({
1639
+ gap: 12,
1640
+ alignItems: 'center',
1641
+ children: [
1642
+ Icon({ name: item.icon, size: 20, color: currentRoute === item.route ? colors.primary : colors.textSecondary }),
1643
+ Text({ text: item.label, size: 14, color: currentRoute === item.route ? colors.primary : colors.text })
1644
+ ]
1645
+ }),
1646
+ onclick: () => goTo(item.route)
1647
+ }))
1648
+ ]
1649
+ })
1650
+ });
1651
+ };
1652
+ `;
1653
+
1654
+ // Modular drawer menu (accepts items) – keep original drawerMenuJs as is, this is new
1655
+ export const drawerMenuModularJs = () => `// components/layouts/DrawerMenu.js
1656
+ import { Drawer, DrawerItem, Container, Column, Text, Icon, colors, closeDrawer } from 'flet-box';
1657
+
1658
+ export const DrawerMenu = ({ items = [] }) => {
1659
+ const defaultItems = [
1660
+ { icon: 'dashboard', label: 'Dashboard', route: '/home' },
1661
+ { icon: 'info', label: 'About', route: '/about' },
1662
+ { icon: 'home', label: 'Root', route: '/' }
1663
+ ];
1664
+ const menuItems = items.length ? items : defaultItems;
1665
+ return Drawer({
1666
+ header: Container({
1667
+ padding: 28,
1668
+ backgroundColor: colors.primary,
1669
+ child: Column({
1670
+ alignItems: 'center',
1671
+ children: [
1672
+ Icon({ name: 'rocket_launch', size: 48, color: '#ffffff' }),
1673
+ Text({ text: 'FletBox', size: 18, weight: 'bold', color: '#ffffff', marginTop: 12 }),
1674
+ Text({ text: 'UI Framework', size: 12, color: 'rgba(255,255,255,0.8)' })
1675
+ ]
1676
+ })
1677
+ }),
1678
+ body: menuItems.map(item => DrawerItem({
1679
+ icon: item.icon,
1680
+ label: item.label,
1681
+ route: item.route,
1682
+ onPress: () => closeDrawer()
1683
+ })),
1684
+ footer: Container({
1685
+ padding: 20,
1686
+ borderTop: \`1px solid \${colors.border}\`,
1687
+ child: Text({ text: 'Version 1.0.0', size: 12, color: colors.textSecondary, align: 'center' })
1688
+ })
1689
+ });
1690
+ };
1691
+ `;
1692
+
1693
+ // App templates (blank, basic, full, sidebar, adaptive)
1694
+ export const blankAppJs = () => `// app.js - Blank template
1695
+ import { runApp, Container, Text, colors } from 'flet-box';
1696
+
1697
+ const App = () => Container({
1698
+ flex: 1,
1699
+ justifyContent: 'center',
1700
+ alignItems: 'center',
1701
+ backgroundColor: colors.background,
1702
+ child: Text({ text: 'Hello FletBox!', size: 24, color: colors.primary })
1703
+ });
1704
+
1705
+ runApp(App, 'root');
1706
+ `;
1707
+
1708
+ export const basicAppJs = () => `// app.js - Basic template (AppBar + Drawer)
1709
+ import { runApp, Scaffold, colors } from 'flet-box';
1710
+ import { AppBarComponent } from './components/layouts/AppBarComponent.js';
1711
+ import { DrawerMenu } from './components/layouts/DrawerMenu.js';
1712
+
1713
+ // make sure to export all screens in screens/index.js for easier imports
1714
+ import RootScreen from './screens/RootScreen.js';
1715
+ import HomeScreen from './screens/HomeScreen.js';
1716
+ import AboutScreen from './screens/AboutScreen.js';
1717
+
1718
+ const routes = {
1719
+ '/': RootScreen,
1720
+ '/home': HomeScreen,
1721
+ '/about': AboutScreen
1722
+ };
1723
+
1724
+ const MyApp = () => Scaffold({
1725
+ routes,
1726
+ appBar: AppBarComponent({ title: 'FletBox', isMobile: false }),
1727
+ drawer: DrawerMenu(),
1728
+ backgroundColor: colors.background
1729
+ });
1730
+
1731
+ runApp(MyApp, 'root');
1732
+ `;
1733
+
1734
+ export const fullAppJs =
1735
+ () => `// app.js - Full template (AppBar + Drawer + BottomNav)
1736
+ import { runApp, Scaffold, colors, useWindowSize } from 'flet-box';
1737
+ import { AppBarComponent } from './components/layouts/AppBarComponent.js';
1738
+ import { DrawerMenu } from './components/layouts/DrawerMenu.js';
1739
+ import { BottomNav } from './components/layouts/BottomNav.js';
1740
+
1741
+ // make sure to export all screens in screens/index.js for easier imports
1742
+ import RootScreen from './screens/RootScreen.js';
1743
+ import HomeScreen from './screens/HomeScreen.js';
1744
+ import AboutScreen from './screens/AboutScreen.js';
1745
+
1746
+ const routes = {
1747
+ '/': RootScreen,
1748
+ '/home': HomeScreen,
1749
+ '/about': AboutScreen
1750
+ };
1751
+
1752
+ const MyApp = () => {
1753
+ const { width } = useWindowSize();
1754
+ const isMobile = width < 768;
1755
+ return Scaffold({
1756
+ routes,
1757
+ appBar: AppBarComponent({ title: 'FletBox', isMobile }),
1758
+ drawer: DrawerMenu(),
1759
+ bottomBar: isMobile ? BottomNav() : null,
1760
+ backgroundColor: colors.background
1761
+ });
1762
+ };
1763
+
1764
+ runApp(MyApp, 'root');
1765
+ `;
1766
+
1767
+ export const sidebarAppJs = () => `// app.js - Sidebar template (desktop only)
1768
+ import { runApp, Scaffold, colors } from 'flet-box';
1769
+ import { AppBarComponent } from './components/layouts/AppBarComponent.js';
1770
+ import { Sidebar } from './components/layouts/Sidebar.js';
1771
+ import { RootScreen, HomeScreen, AboutScreen } from './screens/index.js';
1772
+
1773
+ const routes = {
1774
+ '/': RootScreen,
1775
+ '/home': HomeScreen,
1776
+ '/about': AboutScreen
1777
+ };
1778
+
1779
+ const MyApp = () => Scaffold({
1780
+ routes,
1781
+ appBar: AppBarComponent({ title: 'FletBox', isMobile: false }),
1782
+ sidebar: Sidebar(),
1783
+ backgroundColor: colors.background
1784
+ });
1785
+
1786
+ runApp(MyApp, 'root');
1787
+ `;
1788
+
1789
+ export const adaptiveAppJs =
1790
+ () => `// app.js - Adaptive template (mobile + desktop)
1791
+ import { runApp, Scaffold, colors, useWindowSize } from 'flet-box';
1792
+ import { AppBarComponent } from './components/layouts/AppBarComponent.js';
1793
+ import { DrawerMenu } from './components/layouts/DrawerMenu.js';
1794
+ import { BottomNav } from './components/layouts/BottomNav.js';
1795
+ import { Sidebar } from './components/layouts/Sidebar.js';
1796
+
1797
+ // make sure to export all screens in screens/index.js for easier imports
1798
+ import RootScreen from './screens/RootScreen.js';
1799
+ import HomeScreen from './screens/HomeScreen.js';
1800
+ import AboutScreen from './screens/AboutScreen.js';
1801
+ import ProfileScreen from './screens/ProfileScreen.js';
1802
+
1803
+ const routes = {
1804
+ '/': RootScreen,
1805
+ '/home': HomeScreen,
1806
+ '/about': AboutScreen,
1807
+ '/profile': ProfileScreen
1808
+ };
1809
+
1810
+ const MyApp = () => {
1811
+ const { width } = useWindowSize();
1812
+ const isMobile = width < 768;
1813
+ return Scaffold({
1814
+ routes,
1815
+ appBar: AppBarComponent({ title: 'FletBox', isMobile }),
1816
+ drawer: isMobile ? DrawerMenu() : null,
1817
+ bottomBar: isMobile ? BottomNav() : null,
1818
+ sidebar: !isMobile ? Sidebar() : null,
1819
+ backgroundColor: colors.background
1820
+ });
1821
+ };
1822
+
1823
+ runApp(MyApp, 'root');
1824
+ `;