flet-box 0.0.0-stage → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +190 -2
- package/bin/cli.js +394 -0
- package/bin/commands/buildAndroid.js +200 -0
- package/bin/commands/buildIOS.js +237 -0
- package/bin/commands/create.js +406 -0
- package/bin/commands/createBundle.js +375 -0
- package/bin/commands/createComponent.js +115 -0
- package/bin/commands/createScreen.js +161 -0
- package/bin/commands/killServer.js +107 -0
- package/bin/commands/package.js +131 -0
- package/bin/commands/release.js +189 -0
- package/bin/commands/runBundle.js +62 -0
- package/bin/commands/runCloudflare.js +89 -0
- package/bin/commands/runNative.js +109 -0
- package/bin/commands/runServer.js +390 -0
- package/bin/ota/loader.js +361 -0
- package/bin/utils/bundleConfig.js +178 -0
- package/bin/utils/capacitor.js +137 -0
- package/bin/utils/colors.js +215 -0
- package/bin/utils/helpers.js +80 -0
- package/bin/utils/spinner.js +96 -0
- package/bin/utils/templates.js +1824 -0
- package/bin/utils/version.js +108 -0
- package/package.json +58 -4
- package/src/animations/AnimatedBox.js +607 -0
- package/src/animations/AnimatedText.js +105 -0
- package/src/animations/MatrixRain.js +220 -0
- package/src/animations/ParallaxBox.js +369 -0
- package/src/animations/animate.js +332 -0
- package/src/animations/index.js +25 -0
- package/src/app.js +41 -0
- package/src/assets/icon-1024-maskable.png +0 -0
- package/src/assets/icon-192-maskable.png +0 -0
- package/src/assets/icon-192.png +0 -0
- package/src/assets/icon-512-maskable.png +0 -0
- package/src/assets/icon-512.png +0 -0
- package/src/assets/logo.png +0 -0
- package/src/core/App.js +44 -0
- package/src/core/hmr-client.js +142 -0
- package/src/core/index.js +34 -0
- package/src/core/pwa.js +100 -0
- package/src/core/runApp.js +430 -0
- package/src/create-index.sh +237 -0
- package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
- package/src/fonts/icons.css +23 -0
- package/src/index.d.ts +1930 -0
- package/src/index.js +288 -0
- package/src/navigations/AdaptiveScaffold.js +149 -0
- package/src/navigations/AppBar.js +331 -0
- package/src/navigations/BottomNavigation.js +298 -0
- package/src/navigations/CollapsibleSideBar.js +139 -0
- package/src/navigations/Drawer.js +333 -0
- package/src/navigations/DrawerItem.js +260 -0
- package/src/navigations/Router.js +475 -0
- package/src/navigations/Scaffold.js +537 -0
- package/src/navigations/Tabs.js +384 -0
- package/src/navigations/index.js +57 -0
- package/src/services/RamStore.js +213 -0
- package/src/services/Session.js +310 -0
- package/src/services/Storage.js +309 -0
- package/src/services/http.js +350 -0
- package/src/services/index.js +57 -0
- package/src/tools/animation.js +42 -0
- package/src/tools/array.js +95 -0
- package/src/tools/border.js +28 -0
- package/src/tools/clipboard.js +104 -0
- package/src/tools/color.js +329 -0
- package/src/tools/createList.js +215 -0
- package/src/tools/delay.js +55 -0
- package/src/tools/device.js +138 -0
- package/src/tools/dict.js +297 -0
- package/src/tools/dimensions.js +118 -0
- package/src/tools/filter.js +41 -0
- package/src/tools/flex.js +26 -0
- package/src/tools/gradient.js +34 -0
- package/src/tools/grid.js +57 -0
- package/src/tools/index.js +38 -0
- package/src/tools/mapList.js +107 -0
- package/src/tools/margin.js +93 -0
- package/src/tools/memo.js +60 -0
- package/src/tools/os.js +104 -0
- package/src/tools/padding.js +93 -0
- package/src/tools/print.js +69 -0
- package/src/tools/random.js +299 -0
- package/src/tools/ref.js +70 -0
- package/src/tools/rgba.js +4 -0
- package/src/tools/shadow.js +23 -0
- package/src/tools/string.js +85 -0
- package/src/tools/time.js +674 -0
- package/src/tools/transform.js +68 -0
- package/src/tools/transition.js +48 -0
- package/src/tools/useState.js +268 -0
- package/src/tools/uuid.js +42 -0
- package/src/utils/TextInputValidator.js +256 -0
- package/src/utils/composeUpdate.js +65 -0
- package/src/utils/getWidgetProps.js +35 -0
- package/src/utils/index.js +76 -0
- package/src/utils/markdownParser.js +368 -0
- package/src/utils/mediaTime.js +79 -0
- package/src/utils/navigation.js +263 -0
- package/src/utils/scriptInjector.js +73 -0
- package/src/utils/stackPosition.js +31 -0
- package/src/utils/stopWebRefresh.js +77 -0
- package/src/utils/styleInjector.js +38 -0
- package/src/utils/syntaxHighlight.js +339 -0
- package/src/utils/themes.js +347 -0
- package/src/utils/units.js +87 -0
- package/src/utils/visualEffects.js +169 -0
- package/src/widget-factory/addChildren.js +114 -0
- package/src/widget-factory/assignProps.js +256 -0
- package/src/widget-factory/createWidget.js +28 -0
- package/src/widget-factory/effects.js +132 -0
- package/src/widget-factory/index.js +7 -0
- package/src/widget-factory/lifecycle.js +210 -0
- package/src/widget-factory/makeParentable.js +89 -0
- package/src/widget-factory/pixTool.js +40 -0
- package/src/widget-factory/processProps.js +122 -0
- package/src/widget-factory/reactivity.js +98 -0
- package/src/widget-factory/remTool.js +34 -0
- package/src/widget-factory/stackPosition.js +87 -0
- package/src/widget-factory/stringTool.js +26 -0
- package/src/widget-factory/tools.js +249 -0
- package/src/widget-factory/translateProps.js +1049 -0
- package/src/widget-factory/warnings.js +284 -0
- package/src/widget-factory/widgetFactory.js +200 -0
- package/src/widgets/Accordion.js +562 -0
- package/src/widgets/AdMob.js +315 -0
- package/src/widgets/AdSense.js +268 -0
- package/src/widgets/AlertDialog.js +162 -0
- package/src/widgets/Audio.js +311 -0
- package/src/widgets/Avatar.js +210 -0
- package/src/widgets/Badge.js +189 -0
- package/src/widgets/BottomSheet.js +287 -0
- package/src/widgets/Button.js +247 -0
- package/src/widgets/Card.js +33 -0
- package/src/widgets/Carousel.js +301 -0
- package/src/widgets/Chart.js +632 -0
- package/src/widgets/Checkbox.js +106 -0
- package/src/widgets/Chip.js +153 -0
- package/src/widgets/CircularBar.js +352 -0
- package/src/widgets/CircularChart.js +549 -0
- package/src/widgets/CodeViewer.js +236 -0
- package/src/widgets/Column.js +29 -0
- package/src/widgets/Container.js +31 -0
- package/src/widgets/DataTable.js +257 -0
- package/src/widgets/Divider.js +93 -0
- package/src/widgets/DraggBox.js +115 -0
- package/src/widgets/Dropdown.js +505 -0
- package/src/widgets/DroppBox.js +180 -0
- package/src/widgets/FloatingActionButton.js +91 -0
- package/src/widgets/GridView.js +20 -0
- package/src/widgets/Icon.js +33 -0
- package/src/widgets/Image.js +15 -0
- package/src/widgets/Input.js +565 -0
- package/src/widgets/Inspector.js +254 -0
- package/src/widgets/InstallButton.js +110 -0
- package/src/widgets/ListTile.js +224 -0
- package/src/widgets/ListView.js +503 -0
- package/src/widgets/Markdown.js +1236 -0
- package/src/widgets/Modal.js +496 -0
- package/src/widgets/Pagination.js +195 -0
- package/src/widgets/ProgressBar.js +178 -0
- package/src/widgets/QRCode.js +467 -0
- package/src/widgets/Radio.js +132 -0
- package/src/widgets/Rating.js +261 -0
- package/src/widgets/Row.js +29 -0
- package/src/widgets/Skeleton.js +243 -0
- package/src/widgets/Slider.js +527 -0
- package/src/widgets/SnackBar.js +227 -0
- package/src/widgets/Stack.js +29 -0
- package/src/widgets/Stepper.js +296 -0
- package/src/widgets/Switch.js +102 -0
- package/src/widgets/Text.js +98 -0
- package/src/widgets/Tooltip.js +267 -0
- package/src/widgets/TreeView.js +370 -0
- package/src/widgets/Video.js +92 -0
- package/src/widgets/index.js +52 -0
|
@@ -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
|
+
`;
|