@cavulsqa/create 0.1.0

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 (75) hide show
  1. package/README.md +63 -0
  2. package/bin/create.mjs +142 -0
  3. package/lib/scaffold.mjs +124 -0
  4. package/package.json +60 -0
  5. package/templates/f7-app/.claude/rules/data-fetching.md +67 -0
  6. package/templates/f7-app/.claude/rules/database.md +65 -0
  7. package/templates/f7-app/.claude/rules/framework7-ui.md +79 -0
  8. package/templates/f7-app/.claude/rules/modules.md +43 -0
  9. package/templates/f7-app/.claude/rules/native.md +47 -0
  10. package/templates/f7-app/.claude/skills/f7-design/SKILL.md +67 -0
  11. package/templates/f7-app/.claude/skills/f7-design/components.md +49 -0
  12. package/templates/f7-app/.claude/skills/f7-design/icons.md +52 -0
  13. package/templates/f7-app/.claude/skills/module-architecture/SKILL.md +65 -0
  14. package/templates/f7-app/.claude/skills/module-architecture/file-templates.md +183 -0
  15. package/templates/f7-app/.claude/skills/reactive-data/SKILL.md +89 -0
  16. package/templates/f7-app/.claude/skills/reactive-data/testing.md +55 -0
  17. package/templates/f7-app/CLAUDE.md +105 -0
  18. package/templates/f7-app/auto-imports.d.ts +667 -0
  19. package/templates/f7-app/capacitor.config.ts +14 -0
  20. package/templates/f7-app/components.d.ts +59 -0
  21. package/templates/f7-app/index.html +15 -0
  22. package/templates/f7-app/package.json +51 -0
  23. package/templates/f7-app/src/App.vue +72 -0
  24. package/templates/f7-app/src/app/tabs.ts +25 -0
  25. package/templates/f7-app/src/assets/css/app.css +1 -0
  26. package/templates/f7-app/src/assets/css/icons.css +50 -0
  27. package/templates/f7-app/src/assets/fonts/material-icons-outlined.woff2 +0 -0
  28. package/templates/f7-app/src/assets/fonts/material-icons-round.woff2 +0 -0
  29. package/templates/f7-app/src/domains/sales/sales.repository.ts +458 -0
  30. package/templates/f7-app/src/env.d.ts +23 -0
  31. package/templates/f7-app/src/locales/en.json +179 -0
  32. package/templates/f7-app/src/locales/fr.json +179 -0
  33. package/templates/f7-app/src/main.ts +50 -0
  34. package/templates/f7-app/src/modules/demo/components/DemoBusLog.vue +26 -0
  35. package/templates/f7-app/src/modules/demo/components/DemoCreateOrderSheet.vue +160 -0
  36. package/templates/f7-app/src/modules/demo/components/DemoOrderList.vue +73 -0
  37. package/templates/f7-app/src/modules/demo/components/DemoPipelineBenchmark.vue +52 -0
  38. package/templates/f7-app/src/modules/demo/components/DemoStatCards.vue +63 -0
  39. package/templates/f7-app/src/modules/demo/composables/useReactiveDemo.ts +168 -0
  40. package/templates/f7-app/src/modules/demo/router/routes/demo.routes.ts +34 -0
  41. package/templates/f7-app/src/modules/demo/views/DemoView.vue +126 -0
  42. package/templates/f7-app/src/modules/demo/views/OrderDetailView.vue +131 -0
  43. package/templates/f7-app/src/modules/demo/views/OrderSearchView.vue +106 -0
  44. package/templates/f7-app/src/modules/home/composables/useHomeFeatures.ts +133 -0
  45. package/templates/f7-app/src/modules/home/router/routes/home.routes.ts +29 -0
  46. package/templates/f7-app/src/modules/home/views/FeatureDetailView.vue +108 -0
  47. package/templates/f7-app/src/modules/home/views/HomeView.vue +53 -0
  48. package/templates/f7-app/src/modules/settings/router/routes/settings.routes.ts +16 -0
  49. package/templates/f7-app/src/modules/settings/views/SettingsView.vue +126 -0
  50. package/templates/f7-app/src/plugins/capacitor/index.ts +14 -0
  51. package/templates/f7-app/src/plugins/capacitor/useAndroidBackButton.ts +68 -0
  52. package/templates/f7-app/src/plugins/capacitor/useKeyboard.ts +61 -0
  53. package/templates/f7-app/src/plugins/capacitor/useSplashScreen.ts +11 -0
  54. package/templates/f7-app/src/plugins/capacitor/useStatusBar.ts +15 -0
  55. package/templates/f7-app/src/plugins/framework7.plugin.ts +41 -0
  56. package/templates/f7-app/src/plugins/i18n.plugin.ts +10 -0
  57. package/templates/f7-app/src/plugins/seed.plugin.ts +17 -0
  58. package/templates/f7-app/src/plugins/sqlite.plugin.ts +21 -0
  59. package/templates/f7-app/src/router/global/global.routes.ts +16 -0
  60. package/templates/f7-app/src/router/index.ts +18 -0
  61. package/templates/f7-app/src/shared/components/error/404.vue +12 -0
  62. package/templates/f7-app/src/shared/components/metrics/MetricsPanel.vue +79 -0
  63. package/templates/f7-app/src/shared/composables/theme/useAppTheme.ts +68 -0
  64. package/templates/f7-app/src/shared/composables/useTabbarVisibility.ts +50 -0
  65. package/templates/f7-app/src/shared/database/database.ts +86 -0
  66. package/templates/f7-app/src/shared/database/index.ts +3 -0
  67. package/templates/f7-app/src/shared/database/migrations.ts +81 -0
  68. package/templates/f7-app/src/shared/database/queries.ts +22 -0
  69. package/templates/f7-app/src/shared/database/schema.ts +59 -0
  70. package/templates/f7-app/src/shared/utils/resolvers/resolvers.ts +152 -0
  71. package/templates/f7-app/tests/icons.test.ts +81 -0
  72. package/templates/f7-app/tests/sales.repository.test.ts +329 -0
  73. package/templates/f7-app/tsconfig.json +21 -0
  74. package/templates/f7-app/tsconfig.node.json +14 -0
  75. package/templates/f7-app/vite.config.ts +95 -0
@@ -0,0 +1,29 @@
1
+ import type { Router } from "framework7/types";
2
+
3
+ /**
4
+ * The detail route is a child of the tab's own view, which is what keeps the tab bar in place and
5
+ * the back gesture scoped to this tab's history.
6
+ */
7
+ const homeRoutes: Router.RouteParameters[] = [
8
+ {
9
+ name: "home",
10
+ path: "/home/",
11
+ // `async` is Framework7's route hook, not an async function - resolve from the promise.
12
+ async({ resolve }) {
13
+ void import("@/modules/home/views/HomeView.vue").then((view) => {
14
+ resolve({ component: view.default });
15
+ });
16
+ },
17
+ },
18
+ {
19
+ name: "home-feature",
20
+ path: "/home/feature/:id/",
21
+ async({ resolve }) {
22
+ void import("@/modules/home/views/FeatureDetailView.vue").then((view) => {
23
+ resolve({ component: view.default });
24
+ });
25
+ },
26
+ },
27
+ ];
28
+
29
+ export default homeRoutes;
@@ -0,0 +1,108 @@
1
+ <template>
2
+ <F7Page v-if="feature">
3
+ <F7Navbar
4
+ :title="t(feature.titleKey)"
5
+ :subtitle="t(feature.subtitleKey)"
6
+ :large="feature.navbar.large"
7
+ :transparent="feature.navbar.transparent"
8
+ :hide-on-scroll="feature.navbar.hideOnScroll"
9
+ back-link
10
+ >
11
+ <F7Subnavbar v-if="feature.navbar.subnavbar">
12
+ <F7Segmented strong>
13
+ <F7Button :active="pane === 'what'" @click="pane = 'what'">
14
+ {{ t("detail.what") }}
15
+ </F7Button>
16
+ <F7Button :active="pane === 'how'" @click="pane = 'how'">
17
+ {{ t("detail.how") }}
18
+ </F7Button>
19
+ </F7Segmented>
20
+ </F7Subnavbar>
21
+ </F7Navbar>
22
+
23
+ <F7Block strong inset class="rounded-2xl!">
24
+ <div class="flex items-start gap-3">
25
+ <F7Icon :f7="feature.icon" :color="feature.color" size="28" />
26
+ <p class="m-0">{{ pane === "what" ? t(feature.textKey) : t("detail.howText") }}</p>
27
+ </div>
28
+ </F7Block>
29
+
30
+ <F7BlockTitle>{{ t("detail.thisScreen") }}</F7BlockTitle>
31
+ <!--
32
+ media-list, not a plain list: `item-footer` is sized for a few words and long text in it
33
+ overlaps the title, while a long `after` value squeezes the same text into a one-word column.
34
+ Here the value is the subtitle and the explanation is the text, so neither competes for width.
35
+ -->
36
+ <F7List media-list strong inset dividers class="rounded-2xl!">
37
+ <F7ListItem :title="t('detail.transition')" :subtitle="feature.transition">
38
+ <template #media><F7Icon f7="arrow_right_circle_fill" color="blue" /></template>
39
+ <template #text>{{ t("detail.transitionNote") }}</template>
40
+ </F7ListItem>
41
+ <F7ListItem :title="t('detail.navbar')" :subtitle="navbarSummary">
42
+ <template #media><F7Icon f7="rectangle_3_offgrid_fill" color="purple" /></template>
43
+ <template #text>{{ t("detail.navbarNote") }}</template>
44
+ </F7ListItem>
45
+ <F7ListItem :title="t('detail.nesting')" :subtitle="t('detail.nestingAfter')">
46
+ <template #media><F7Icon f7="square_stack_3d_down_right_fill" color="green" /></template>
47
+ <template #text>{{ t("detail.nestingNote") }}</template>
48
+ </F7ListItem>
49
+ </F7List>
50
+
51
+ <!-- Enough content that hide-on-scroll and the collapsing large title have something to do. -->
52
+ <F7BlockTitle>{{ t("detail.scrollMe") }}</F7BlockTitle>
53
+ <F7List strong inset dividers class="rounded-2xl!">
54
+ <F7ListItem
55
+ v-for="line in 12"
56
+ :key="line"
57
+ :title="`${t('detail.row')} ${String(line)}`"
58
+ :after="String(line)"
59
+ />
60
+ </F7List>
61
+
62
+ <F7Block>
63
+ <F7Button large fill @click="openNext">{{ t("detail.next") }}</F7Button>
64
+ </F7Block>
65
+ </F7Page>
66
+
67
+ <F7Page v-else>
68
+ <F7Navbar :title="t('errors.notFoundTitle')" back-link />
69
+ <F7Block strong inset class="rounded-2xl!">
70
+ <p class="m-0">{{ t("errors.notFound") }}</p>
71
+ </F7Block>
72
+ </F7Page>
73
+ </template>
74
+
75
+ <script setup lang="ts">
76
+ import { useHiddenTabbar } from "@/shared/composables/useTabbarVisibility";
77
+ import type { Router } from "framework7/types";
78
+ import { features, findFeature } from "@/modules/home/composables/useHomeFeatures";
79
+
80
+ const { t } = useI18n();
81
+
82
+ // A pushed page owns the whole screen; the tab bar belongs to the tab roots.
83
+ useHiddenTabbar();
84
+
85
+ // Framework7 passes the route and this view's router to a route component as props.
86
+ const props = defineProps<{ f7route: Router.Route; f7router: Router.Router }>();
87
+
88
+ const feature = computed(() => findFeature(String(props.f7route.params.id ?? "")));
89
+ const pane = ref<"what" | "how">("what");
90
+
91
+ const navbarSummary = computed(() => {
92
+ if (!feature.value) return "";
93
+ const parts: string[] = [];
94
+ if (feature.value.navbar.large) parts.push("large");
95
+ if (feature.value.navbar.transparent) parts.push("transparent");
96
+ if (feature.value.navbar.hideOnScroll) parts.push("hide-on-scroll");
97
+ if (feature.value.navbar.subnavbar) parts.push("subnavbar");
98
+ return parts.length ? parts.join(" + ") : "plain";
99
+ });
100
+
101
+ /** Pushes the next feature onto this tab's own history, so the stack grows inside the tab. */
102
+ function openNext() {
103
+ const index = features.findIndex((entry) => entry.id === feature.value?.id);
104
+ const next = features[(index + 1) % features.length];
105
+ if (!next) return;
106
+ props.f7router.navigate(`/home/feature/${next.id}/`, { transition: next.transition });
107
+ }
108
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <F7Page>
3
+ <F7Navbar :title="t('home.title')" large transparent :sliding="true" />
4
+
5
+ <F7BlockTitle>{{ t("home.welcome") }}</F7BlockTitle>
6
+ <F7Block strong inset class="rounded-2xl!">
7
+ <p class="m-0">
8
+ <strong>{{ appName }}</strong> {{ t("home.intro") }}
9
+ </p>
10
+ </F7Block>
11
+
12
+ <F7BlockTitle>{{ t("home.included") }}</F7BlockTitle>
13
+ <F7List media-list strong inset dividers class="rounded-2xl!">
14
+ <F7ListItem
15
+ v-for="feature in features"
16
+ :key="feature.id"
17
+ :title="t(feature.titleKey)"
18
+ :subtitle="t(feature.subtitleKey)"
19
+ :after="feature.transition.replace('f7-', '')"
20
+ link="#"
21
+ @click="openFeature(feature)"
22
+ >
23
+ <template #media>
24
+ <F7Icon :f7="feature.icon" :color="feature.color" />
25
+ </template>
26
+ <template #text>{{ t(feature.textKey) }}</template>
27
+ </F7ListItem>
28
+ </F7List>
29
+
30
+ <F7Block class="text-center">
31
+ <p class="m-0 text-sm opacity-50">{{ t("home.tapAny") }}</p>
32
+ </F7Block>
33
+ </F7Page>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import type { Router } from "framework7/types";
38
+ import { features, type HomeFeature } from "@/modules/home/composables/useHomeFeatures";
39
+
40
+ const { t } = useI18n();
41
+ const appName = __APP_NAME__;
42
+
43
+ // This view's own router, so the push stays inside the Home tab.
44
+ const props = defineProps<{ f7router: Router.Router }>();
45
+
46
+ /**
47
+ * Navigating programmatically rather than through a `link` href, because each row carries its own
48
+ * transition. `f7router` is this view's router, so the push stays inside the Home tab.
49
+ */
50
+ function openFeature(feature: HomeFeature) {
51
+ props.f7router.navigate(`/home/feature/${feature.id}/`, { transition: feature.transition });
52
+ }
53
+ </script>
@@ -0,0 +1,16 @@
1
+ import type { Router } from "framework7/types";
2
+
3
+ const settingsRoutes: Router.RouteParameters[] = [
4
+ {
5
+ name: "settings",
6
+ path: "/settings/",
7
+ // `async` is Framework7's route hook, not an async function - resolve from the promise.
8
+ async({ resolve }) {
9
+ void import("@/modules/settings/views/SettingsView.vue").then((view) => {
10
+ resolve({ component: view.default });
11
+ });
12
+ },
13
+ },
14
+ ];
15
+
16
+ export default settingsRoutes;
@@ -0,0 +1,126 @@
1
+ <template>
2
+ <F7Page>
3
+ <F7Navbar :title="t('settings.title')" large transparent :sliding="true" />
4
+
5
+ <F7BlockTitle>{{ t("settings.appearance") }}</F7BlockTitle>
6
+ <F7List strong inset dividers class="rounded-2xl!">
7
+ <F7ListItem
8
+ :title="t('settings.theme')"
9
+ smart-select
10
+ :smart-select-params="{ openIn: 'sheet', closeOnSelect: true }"
11
+ >
12
+ <template #media><F7Icon f7="paintbrush_fill" color="pink" /></template>
13
+ <select :value="appTheme.theme" @change="onThemeChange">
14
+ <option value="auto">{{ t("settings.themeAuto") }}</option>
15
+ <option value="ios">iOS</option>
16
+ <option value="md">Material</option>
17
+ </select>
18
+ </F7ListItem>
19
+
20
+ <F7ListItem :title="t('settings.darkMode')">
21
+ <template #media><F7Icon f7="moon_fill" color="deeppurple" /></template>
22
+ <template #after>
23
+ <F7Toggle :checked="appTheme.darkMode === 'dark'" @change="onDarkModeChange" />
24
+ </template>
25
+ </F7ListItem>
26
+
27
+ <F7ListItem
28
+ :title="t('settings.language')"
29
+ smart-select
30
+ :smart-select-params="{ openIn: 'popover', closeOnSelect: true }"
31
+ >
32
+ <template #media><F7Icon f7="globe" color="blue" /></template>
33
+ <select :value="locale" @change="onLocaleChange">
34
+ <option v-for="option in availableLocales" :key="option" :value="option">
35
+ {{ option.toUpperCase() }}
36
+ </option>
37
+ </select>
38
+ </F7ListItem>
39
+ </F7List>
40
+
41
+ <F7BlockTitle>{{ t("settings.tryIt") }}</F7BlockTitle>
42
+ <F7List strong inset dividers class="rounded-2xl!">
43
+ <F7ListItem :title="t('settings.testToast')" link="#" @click="testToast">
44
+ <template #media><F7Icon f7="bell_fill" color="orange" /></template>
45
+ </F7ListItem>
46
+ <F7ListItem :title="t('settings.testDialog')" link="#" @click="testDialog">
47
+ <template #media><F7Icon f7="exclamationmark_bubble_fill" color="red" /></template>
48
+ </F7ListItem>
49
+ <F7ListItem :title="t('settings.testSheet')" link="#" @click="testSheet">
50
+ <template #media><F7Icon f7="rectangle_stack_fill" color="teal" /></template>
51
+ </F7ListItem>
52
+ </F7List>
53
+
54
+ <F7BlockTitle>{{ t("settings.about") }}</F7BlockTitle>
55
+ <F7List strong inset dividers class="rounded-2xl!">
56
+ <F7ListItem :title="t('settings.appName')" :after="appName">
57
+ <template #media><F7Icon f7="app_badge_fill" color="green" /></template>
58
+ </F7ListItem>
59
+ <F7ListItem :title="t('settings.version')" :after="appVersion">
60
+ <template #media><F7Icon f7="number" color="gray" /></template>
61
+ </F7ListItem>
62
+ <F7ListItem :title="t('settings.platform')" :after="platform">
63
+ <template #media><F7Icon f7="device_phone_portrait" color="purple" /></template>
64
+ </F7ListItem>
65
+ <F7ListItem :title="t('settings.aboutUs')" link="#" @click="showAbout">
66
+ <template #media><F7Icon f7="info_circle_fill" color="blue" /></template>
67
+ </F7ListItem>
68
+ </F7List>
69
+
70
+ <F7Block class="text-center">
71
+ <p class="m-0 text-sm opacity-50">{{ t("settings.footer") }}</p>
72
+ </F7Block>
73
+ </F7Page>
74
+ </template>
75
+
76
+ <script setup lang="ts">
77
+ import { Capacitor } from "@capacitor/core";
78
+ import { useAppTheme, type AppMode, type AppTheme } from "@/shared/composables/theme/useAppTheme";
79
+
80
+ const { t, locale, availableLocales } = useI18n();
81
+ const theme = useAppTheme();
82
+ const appTheme = computed(() => theme.value);
83
+
84
+ const appName = __APP_NAME__;
85
+ const appVersion = __APP_VERSION__;
86
+ const platform = Capacitor.getPlatform();
87
+
88
+ function onThemeChange(event: Event) {
89
+ appTheme.value.setTheme((event.target as HTMLSelectElement).value as AppTheme);
90
+ }
91
+
92
+ function onDarkModeChange(event: Event) {
93
+ const mode: AppMode = (event.target as HTMLInputElement).checked ? "dark" : "light";
94
+ void appTheme.value.setDarkMode(mode);
95
+ }
96
+
97
+ function onLocaleChange(event: Event) {
98
+ locale.value = (event.target as HTMLSelectElement).value;
99
+ }
100
+
101
+ function testToast() {
102
+ f7.toast.create({ text: t("settings.toastText"), position: "center", closeTimeout: 1800 }).open();
103
+ }
104
+
105
+ function testDialog() {
106
+ f7.dialog.confirm(t("settings.dialogText"), t("settings.dialogTitle"));
107
+ }
108
+
109
+ function testSheet() {
110
+ f7.sheet
111
+ .create({
112
+ content: `
113
+ <div class="sheet-modal">
114
+ <div class="sheet-modal-inner">
115
+ <div class="block"><p>${t("settings.sheetText")}</p></div>
116
+ </div>
117
+ </div>`,
118
+ backdrop: true,
119
+ })
120
+ .open();
121
+ }
122
+
123
+ function showAbout() {
124
+ f7.dialog.alert(t("settings.aboutText"), t("settings.aboutUs"));
125
+ }
126
+ </script>
@@ -0,0 +1,14 @@
1
+ import type Framework7 from "framework7";
2
+ import { useAndroidBackButton } from "./useAndroidBackButton";
3
+ import { useKeyboard } from "./useKeyboard";
4
+ import { useStatusBar } from "./useStatusBar";
5
+
6
+ /**
7
+ * Native wiring that needs the Framework7 instance, so it runs once the shell exists rather than at
8
+ * module load. Every handler no-ops off-device, which is what keeps `vp dev` in a browser working.
9
+ */
10
+ export async function initCapacitor(f7: Framework7): Promise<void> {
11
+ await useAndroidBackButton(f7);
12
+ useKeyboard(f7);
13
+ await useStatusBar();
14
+ }
@@ -0,0 +1,68 @@
1
+ import { App } from "@capacitor/app";
2
+ import { Capacitor } from "@capacitor/core";
3
+ import type Framework7 from "framework7";
4
+
5
+ /**
6
+ * Android's back button must close whatever is on top before it navigates, and the order matters:
7
+ * an actions sheet sitting over a popup has to close first, or back dismisses the popup underneath
8
+ * and leaves the sheet orphaned.
9
+ *
10
+ * A popup that hosts its own view is a special case - back should walk that view's history first
11
+ * and only close the popup once there is nothing left to go back to. Mark a popup
12
+ * `custom-popup-close` when it owns its dismissal (an unsaved-changes prompt, say) and this leaves
13
+ * it alone.
14
+ */
15
+ export async function useAndroidBackButton(f7: Framework7): Promise<void> {
16
+ if (!Capacitor.isNativePlatform()) return;
17
+
18
+ const $ = f7.$;
19
+
20
+ await App.addListener("backButton", () => {
21
+ if ($(".actions-modal.modal-in").length) {
22
+ f7.actions.close(".actions-modal.modal-in");
23
+ return;
24
+ }
25
+ if ($(".dialog.modal-in").length) {
26
+ f7.dialog.close(".dialog.modal-in");
27
+ return;
28
+ }
29
+ if ($(".sheet-modal.modal-in").length) {
30
+ f7.sheet.close(".sheet-modal.modal-in");
31
+ return;
32
+ }
33
+ if ($(".popover.modal-in").length) {
34
+ f7.popover.close(".popover.modal-in");
35
+ return;
36
+ }
37
+ if ($(".popup.modal-in").length) {
38
+ if ($(".popup.modal-in>.view").length) {
39
+ const popupView = f7.views.get(".popup.modal-in>.view");
40
+ if (popupView?.router && popupView.router.history.length > 1) {
41
+ popupView.router.back();
42
+ return;
43
+ }
44
+ }
45
+ if ($(".popup.custom-popup-close").length) return;
46
+ f7.popup.close(".popup.modal-in");
47
+ return;
48
+ }
49
+ if ($(".login-screen.modal-in").length) {
50
+ f7.loginScreen.close(".login-screen.modal-in");
51
+ return;
52
+ }
53
+ if ($(".panel.panel-in").length) {
54
+ f7.panel.close(".panel.panel-in");
55
+ return;
56
+ }
57
+
58
+ const view = f7.views.current;
59
+ if (view?.router && view.router.history.length > 1) {
60
+ view.router.back();
61
+ return;
62
+ }
63
+
64
+ // Nothing left to dismiss or unwind: let the app go to the background rather than exit, so
65
+ // returning to it restores the same screen.
66
+ void App.minimizeApp();
67
+ });
68
+ }
@@ -0,0 +1,61 @@
1
+ import { Capacitor } from "@capacitor/core";
2
+ import { Keyboard, KeyboardResize } from "@capacitor/keyboard";
3
+ import type Framework7 from "framework7";
4
+ import { useTabbarVisibility } from "@/shared/composables/useTabbarVisibility";
5
+
6
+ /**
7
+ * Two problems the platform will not solve for you.
8
+ *
9
+ * The focused input scrolls out of view as the keyboard animates in, so it is scrolled back on
10
+ * every phase of the transition rather than once: the layout is still settling when
11
+ * `keyboardWillShow` fires, and only `keyboardDidShow` sees the final height.
12
+ *
13
+ * The tab bar sits above the keyboard and steals a row of screen from the field being typed into,
14
+ * so it hides while the keyboard is open. That state is shared, not local, because the bar is
15
+ * rendered by the shell and the input is somewhere deep in a page.
16
+ */
17
+ export function useKeyboard(f7: Framework7): void {
18
+ if (!Capacitor.isNativePlatform()) return;
19
+
20
+ const $ = f7.$;
21
+ const { setKeyboardOpen } = useTabbarVisibility();
22
+
23
+ if (Capacitor.getPlatform() === "ios") {
24
+ void Keyboard.setResizeMode({ mode: KeyboardResize.Native });
25
+ void Keyboard.setScroll({ isDisabled: true });
26
+ }
27
+
28
+ const scrollFocusedIntoView = () => {
29
+ if (document.activeElement) {
30
+ f7.input.scrollIntoView(document.activeElement as HTMLElement, 0, true, true);
31
+ }
32
+ };
33
+
34
+ void Keyboard.addListener("keyboardWillShow", () => {
35
+ setKeyboardOpen(true);
36
+ scrollFocusedIntoView();
37
+ });
38
+
39
+ void Keyboard.addListener("keyboardDidShow", scrollFocusedIntoView);
40
+ void Keyboard.addListener("keyboardWillHide", scrollFocusedIntoView);
41
+
42
+ void Keyboard.addListener("keyboardDidHide", () => {
43
+ setKeyboardOpen(false);
44
+
45
+ // A message bar keeps its accessory row; restoring it here would push the composer up.
46
+ if (document.activeElement && $(document.activeElement).parents(".messagebar").length) return;
47
+
48
+ if (Capacitor.getPlatform() === "ios") {
49
+ void Keyboard.setAccessoryBarVisible({ isVisible: true });
50
+ }
51
+ });
52
+
53
+ // Tapping outside an input should dismiss the keyboard. Without this it stays up until the field
54
+ // is blurred by something else, covering half the screen.
55
+ $(document).on("touchstart", (event: Event) => {
56
+ const target = event.target as HTMLElement | null;
57
+ if (!target) return;
58
+ if ($(target).closest("input, textarea, [contenteditable], .messagebar").length) return;
59
+ if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
60
+ });
61
+ }
@@ -0,0 +1,11 @@
1
+ import { Capacitor } from "@capacitor/core";
2
+ import { SplashScreen } from "@capacitor/splash-screen";
3
+
4
+ /**
5
+ * `launchAutoHide` is off in `capacitor.config.ts`, so the splash stays up until this is called.
6
+ * Hiding it on a frame boundary avoids a flash of an unpainted shell between the two.
7
+ */
8
+ export function hideSplashScreen(): void {
9
+ if (!Capacitor.isNativePlatform()) return;
10
+ requestAnimationFrame(() => void SplashScreen.hide());
11
+ }
@@ -0,0 +1,15 @@
1
+ import { Capacitor } from "@capacitor/core";
2
+ import { Style, StatusBar } from "@capacitor/status-bar";
3
+
4
+ /**
5
+ * The bar is transparent and overlays the web view, so the page owns the space behind it - see the
6
+ * safe-area padding in `assets/css/app.css`. Style follows the theme, because a dark bar over a
7
+ * dark header renders invisible text.
8
+ */
9
+ export async function useStatusBar(): Promise<void> {
10
+ if (!Capacitor.isNativePlatform()) return;
11
+
12
+ await StatusBar.setOverlaysWebView({ overlay: true });
13
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
14
+ await StatusBar.setStyle({ style: prefersDark ? Style.Dark : Style.Light });
15
+ }
@@ -0,0 +1,41 @@
1
+ import { Capacitor } from "@capacitor/core";
2
+ import type { Framework7Parameters } from "framework7/types";
3
+ import routes from "@/router";
4
+ import type { AppTheme } from "@/shared/composables/theme/useAppTheme";
5
+
6
+ export function framework7Parameters(theme: AppTheme, darkMode: boolean): Framework7Parameters {
7
+ return {
8
+ name: "App",
9
+ // "auto" lets Framework7 pick iOS or Material from the device; Settings can pin either.
10
+ theme,
11
+ darkMode,
12
+ routes,
13
+
14
+ // Long-press is a real gesture on touch; without preventClicks it also fires a tap.
15
+ touch: {
16
+ tapHold: true,
17
+ tapHoldDelay: 500,
18
+ tapHoldPreventClicks: true,
19
+ },
20
+
21
+ input: { scrollIntoViewOnFocus: true },
22
+
23
+ // The bar is drawn by the OS and overlaid; see useStatusBar.
24
+ statusbar: { enabled: Capacitor.isNativePlatform() },
25
+
26
+ view: {
27
+ animate: true,
28
+ // Each tab view keeps its own history, so switching back returns to the same screen.
29
+ browserHistory: false,
30
+ },
31
+
32
+ panel: { swipe: true },
33
+
34
+ /**
35
+ * Framework7 derives the whole theme palette from this - tints, shades, ripples and the dark
36
+ * variants - so it is set here rather than by overriding `--f7-theme-color` in CSS, which would
37
+ * change the accent but leave everything derived from it still blue.
38
+ */
39
+ colors: { primary: "#c96442" },
40
+ };
41
+ }
@@ -0,0 +1,10 @@
1
+ import { createI18n } from "vue-i18n";
2
+ import en from "@/locales/en.json";
3
+ import fr from "@/locales/fr.json";
4
+
5
+ export const i18n = createI18n({
6
+ legacy: false,
7
+ locale: navigator.language.split("-")[0] ?? "en",
8
+ fallbackLocale: "en",
9
+ messages: { en, fr },
10
+ });
@@ -0,0 +1,17 @@
1
+ import { ensureReferenceData } from "@/domains/sales/sales.repository";
2
+ import { rdb } from "@/shared/database";
3
+
4
+ /**
5
+ * Reference data the app cannot work without: the product catalogue, the tags, a few customers.
6
+ *
7
+ * A template has to be usable the moment it opens. Without this, the first thing a new install asks
8
+ * you to do is press "seed sample data" before the New order sheet has anything to pick from - the
9
+ * schema is there and every screen is empty, which reads as broken rather than as new.
10
+ *
11
+ * `ensureReferenceData` is idempotent, so this runs on every start and does nothing after the first.
12
+ * Replace it with your own once the app has real data to load; delete it if data arrives from a
13
+ * server. Demo orders stay behind the button - those are a demo, this is a working state.
14
+ */
15
+ export async function seedPlugin(): Promise<void> {
16
+ await ensureReferenceData(rdb);
17
+ }
@@ -0,0 +1,21 @@
1
+ import { openDatabase } from "@/shared/database";
2
+
3
+ const OPEN_TIMEOUT_MS = 10_000;
4
+
5
+ /**
6
+ * Opening the database is the one thing that must succeed before the first screen renders, and the
7
+ * one thing that used to fail silently: awaited at the top level of `main.ts`, a rejection left an
8
+ * empty `#app` and nothing in the console. The timeout turns a hang into a message.
9
+ */
10
+ export async function sqlitePlugin(): Promise<void> {
11
+ await Promise.race([
12
+ openDatabase(),
13
+ new Promise<never>((_, reject) => {
14
+ setTimeout(
15
+ () =>
16
+ reject(new Error(`opening the database did not finish in ${String(OPEN_TIMEOUT_MS)}ms`)),
17
+ OPEN_TIMEOUT_MS,
18
+ );
19
+ }),
20
+ ]);
21
+ }
@@ -0,0 +1,16 @@
1
+ import type { Router } from "framework7/types";
2
+
3
+ const globalRoutes: Router.RouteParameters[] = [
4
+ {
5
+ name: "not-found",
6
+ path: "(.*)",
7
+ // `async` is Framework7's route hook, not an async function - resolve from the promise.
8
+ async({ resolve }) {
9
+ void import("@/shared/components/error/404.vue").then((view) => {
10
+ resolve({ component: view.default });
11
+ });
12
+ },
13
+ },
14
+ ];
15
+
16
+ export default globalRoutes;
@@ -0,0 +1,18 @@
1
+ import type { Router } from "framework7/types";
2
+ import homeRoutes from "@/modules/home/router/routes/home.routes";
3
+ import demoRoutes from "@/modules/demo/router/routes/demo.routes";
4
+ import settingsRoutes from "@/modules/settings/router/routes/settings.routes";
5
+ import globalRoutes from "./global/global.routes";
6
+
7
+ /**
8
+ * Each module owns its routes; this only orders them. The catch-all is last because Framework7
9
+ * takes the first match.
10
+ */
11
+ const routes: Router.RouteParameters[] = [
12
+ ...homeRoutes,
13
+ ...demoRoutes,
14
+ ...settingsRoutes,
15
+ ...globalRoutes,
16
+ ];
17
+
18
+ export default routes;
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <F7Page>
3
+ <F7Navbar :title="t('errors.notFoundTitle')" />
4
+ <F7Block strong class="text-center">
5
+ <p class="m-0">{{ t("errors.notFound") }}</p>
6
+ </F7Block>
7
+ </F7Page>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ const { t } = useI18n();
12
+ </script>