@cavulsqa/create 2.9.3 → 2.10.1
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/README.md +12 -1
- package/bin/create.mjs +18 -25
- package/lib/templates.mjs +56 -0
- package/package.json +13 -5
- package/templates/f7-app/auto-imports.d.ts +9 -0
- package/templates/f7-app/package.json +6 -3
- package/templates/f7-app/src/env.d.ts +1 -0
- package/templates/f7-app/src/main.ts +3 -0
- package/templates/f7-app/src/plugins/bootstrapError.ts +2 -0
- package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
- package/templates/f7-app/src/shared/recorder/capacitorSink.ts +124 -0
- package/templates/f7-app/vite.config.ts +1 -0
- package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
- package/templates/m3e-app/.claude/rules/database.md +106 -0
- package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
- package/templates/m3e-app/.claude/rules/modules.md +43 -0
- package/templates/m3e-app/.claude/rules/native.md +60 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
- package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
- package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
- package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
- package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
- package/templates/m3e-app/.env.example +32 -0
- package/templates/m3e-app/CLAUDE.md +93 -0
- package/templates/m3e-app/auto-imports.d.ts +851 -0
- package/templates/m3e-app/capacitor.config.ts +44 -0
- package/templates/m3e-app/components.d.ts +252 -0
- package/templates/m3e-app/index.html +16 -0
- package/templates/m3e-app/package.json +108 -0
- package/templates/m3e-app/src/App.vue +129 -0
- package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
- package/templates/m3e-app/src/app/scroll.config.ts +20 -0
- package/templates/m3e-app/src/app/storage.config.ts +65 -0
- package/templates/m3e-app/src/app/tabs.ts +33 -0
- package/templates/m3e-app/src/app/theme.config.ts +51 -0
- package/templates/m3e-app/src/assets/css/app.css +18 -0
- package/templates/m3e-app/src/assets/css/base.css +53 -0
- package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
- package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
- package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
- package/templates/m3e-app/src/assets/css/m3e.css +6 -0
- package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
- package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
- package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
- package/templates/m3e-app/src/env.d.ts +40 -0
- package/templates/m3e-app/src/locales/ar.json +1260 -0
- package/templates/m3e-app/src/locales/en.json +1260 -0
- package/templates/m3e-app/src/locales/fr.json +1260 -0
- package/templates/m3e-app/src/main.ts +41 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
- package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
- package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
- package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
- package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
- package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
- package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
- package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
- package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
- package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
- package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
- package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
- package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
- package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
- package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
- package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
- package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
- package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
- package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
- package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
- package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
- package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
- package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
- package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
- package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
- package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
- package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
- package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
- package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
- package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
- package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
- package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
- package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
- package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
- package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
- package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
- package/templates/m3e-app/src/modules/settings/types.ts +7 -0
- package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
- package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
- package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
- package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
- package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
- package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
- package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
- package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
- package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
- package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
- package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
- package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
- package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
- package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
- package/templates/m3e-app/src/router/index.ts +20 -0
- package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
- package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
- package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
- package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
- package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
- package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
- package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
- package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
- package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
- package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
- package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
- package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
- package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
- package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
- package/templates/m3e-app/src/shared/database/database.ts +219 -0
- package/templates/m3e-app/src/shared/database/index.ts +3 -0
- package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
- package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
- package/templates/m3e-app/src/shared/database/queries.ts +26 -0
- package/templates/m3e-app/src/shared/database/schema.ts +153 -0
- package/templates/m3e-app/src/shared/database/storage.ts +64 -0
- package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
- package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
- package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
- package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
- package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
- package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
- package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
- package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
- package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
- package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
- package/templates/m3e-app/tests/locales.test.ts +73 -0
- package/templates/m3e-app/tests/migrations.test.ts +74 -0
- package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
- package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
- package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
- package/templates/m3e-app/tests/storage.test.ts +117 -0
- package/templates/m3e-app/tests/textDirection.test.ts +11 -0
- package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
- package/templates/m3e-app/tsconfig.json +21 -0
- package/templates/m3e-app/vite.config.ts +144 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# File templates
|
|
2
|
+
|
|
3
|
+
Copy these shapes. They encode decisions that are easy to get wrong once and then repeat everywhere.
|
|
4
|
+
|
|
5
|
+
## Route file
|
|
6
|
+
|
|
7
|
+
`modules/<feature>/router/routes/<feature>.routes.ts`
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import type { Router } from "framework7/types";
|
|
11
|
+
|
|
12
|
+
const featureRoutes: Router.RouteParameters[] = [
|
|
13
|
+
{
|
|
14
|
+
name: "feature",
|
|
15
|
+
path: "/feature/",
|
|
16
|
+
// `async` is Framework7's route hook, not an async function - resolve from the promise.
|
|
17
|
+
async({ resolve }) {
|
|
18
|
+
void import("@/modules/feature/views/FeatureView.vue").then((view) => {
|
|
19
|
+
resolve({ component: view.default });
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "feature-detail",
|
|
25
|
+
path: "/feature/:id/",
|
|
26
|
+
async({ resolve }) {
|
|
27
|
+
void import("@/modules/feature/views/FeatureDetailView.vue").then((view) => {
|
|
28
|
+
resolve({ component: view.default });
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export default featureRoutes;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`await` inside that hook does not compile — the property is literally named `async`.
|
|
38
|
+
|
|
39
|
+
## Repository
|
|
40
|
+
|
|
41
|
+
`domains/<domain>/<domain>.repository.ts`
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import type { Kysely } from "kysely";
|
|
45
|
+
import { nowISO } from "@cavulsqa/mobile-db";
|
|
46
|
+
import type { Database } from "@/shared/database/schema";
|
|
47
|
+
|
|
48
|
+
export interface ThingRow {
|
|
49
|
+
id: number;
|
|
50
|
+
name: string;
|
|
51
|
+
totalCents: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Reads take the database as a parameter - that is what makes them testable. */
|
|
55
|
+
export function listThings(db: Kysely<Database>, term: string): Promise<ThingRow[]> {
|
|
56
|
+
let query = db.selectFrom("thing").select(["id", "name", "total_cents as totalCents"]);
|
|
57
|
+
if (term.trim()) query = query.where("name", "like", `%${term.trim()}%`);
|
|
58
|
+
return query.orderBy("id", "desc").limit(40).execute();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** All-or-nothing work goes in one transaction. */
|
|
62
|
+
export async function saveThing(
|
|
63
|
+
db: Kysely<Database>,
|
|
64
|
+
input: { name: string; lines: Array<{ productId: number; quantity: number }> },
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
await db.transaction().execute(async (trx) => {
|
|
67
|
+
const thing = await trx
|
|
68
|
+
.insertInto("thing")
|
|
69
|
+
.values({ created_at: nowISO(), name: input.name })
|
|
70
|
+
.returning("id")
|
|
71
|
+
.executeTakeFirstOrThrow();
|
|
72
|
+
|
|
73
|
+
for (const line of input.lines) {
|
|
74
|
+
await trx
|
|
75
|
+
.insertInto("thing_line")
|
|
76
|
+
.values({ thing_id: thing.id, ...line })
|
|
77
|
+
.execute();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
No `ref`, no lifecycle, no Framework7, no imports from `modules/`.
|
|
84
|
+
|
|
85
|
+
## Composable
|
|
86
|
+
|
|
87
|
+
`modules/<feature>/composables/useFeature.ts`
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { listThings, saveThing, type ThingRow } from "@/domains/thing/thing.repository";
|
|
91
|
+
import { getDatabase, rdb } from "@/shared/database/database";
|
|
92
|
+
import { useReactiveQuery } from "@/shared/database/queries";
|
|
93
|
+
|
|
94
|
+
export function useFeature() {
|
|
95
|
+
const term = ref("");
|
|
96
|
+
const busy = ref(false);
|
|
97
|
+
|
|
98
|
+
const query = useReactiveQuery(() => listThings(getDatabase().db, term.value), {
|
|
99
|
+
// Every table the SQL touches. A join means each joined table.
|
|
100
|
+
tables: ["thing"],
|
|
101
|
+
// The term is part of the identity, so the query re-runs (debounced) as it moves.
|
|
102
|
+
queryKey: ["feature:things", term],
|
|
103
|
+
debounce: 250,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const things = computed<ThingRow[]>(() => query.data.value ?? []);
|
|
107
|
+
|
|
108
|
+
// Writes go through `rdb`, which announces the tables they touched.
|
|
109
|
+
async function save(input: Parameters<typeof saveThing>[1]) {
|
|
110
|
+
busy.value = true;
|
|
111
|
+
try {
|
|
112
|
+
await saveThing(rdb, input);
|
|
113
|
+
} finally {
|
|
114
|
+
busy.value = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { term, things, loading: query.loading, busy, save };
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`ref`, `computed` and the composable itself are auto-imported. Repositories are not.
|
|
123
|
+
|
|
124
|
+
## View
|
|
125
|
+
|
|
126
|
+
`modules/<feature>/views/FeatureView.vue`
|
|
127
|
+
|
|
128
|
+
```vue
|
|
129
|
+
<template>
|
|
130
|
+
<AppPage :title="t('feature.title')" name="feature">
|
|
131
|
+
<SectionHeader :title="t('feature.section')" />
|
|
132
|
+
<M3List v-if="things.length" variant="segmented" inset>
|
|
133
|
+
<M3ListItem
|
|
134
|
+
v-for="thing in things"
|
|
135
|
+
:key="thing.id"
|
|
136
|
+
clickable
|
|
137
|
+
:headline="thing.name"
|
|
138
|
+
:supporting="formatMoney(thing.totalCents)"
|
|
139
|
+
@click="f7router.navigate(`/feature/${String(thing.id)}/`)"
|
|
140
|
+
>
|
|
141
|
+
<template #leading><i-ms-inventory-2-outline-rounded /></template>
|
|
142
|
+
</M3ListItem>
|
|
143
|
+
</M3List>
|
|
144
|
+
<EmptyState v-else :headline="t('feature.emptyTitle')" :text="t('feature.empty')" />
|
|
145
|
+
</AppPage>
|
|
146
|
+
</template>
|
|
147
|
+
|
|
148
|
+
<script setup lang="ts">
|
|
149
|
+
import type { Router } from "framework7/types";
|
|
150
|
+
import { useFeature } from "@/modules/feature/composables/useFeature";
|
|
151
|
+
|
|
152
|
+
defineProps<{ f7router: Router.Router }>();
|
|
153
|
+
const { t } = useI18n();
|
|
154
|
+
const { things } = useFeature();
|
|
155
|
+
</script>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Components, icons and composables resolve themselves; colours and type come from tokens. The
|
|
159
|
+
`m3-expressive` skill decides which component and which role.
|
|
160
|
+
|
|
161
|
+
## Pushed detail view
|
|
162
|
+
|
|
163
|
+
```vue
|
|
164
|
+
<AppPage :title="thing?.name ?? t('feature.title')" back>…</AppPage>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const props = defineProps<{ f7route: Router.Route; f7router: Router.Router }>();
|
|
169
|
+
const id = Number(props.f7route.params.id ?? 0);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`back` gives the small app bar with a back button and hides the navigation bar. A detail screen's
|
|
173
|
+
state changes go in an `M3DockedToolbar` in `AppPage`'s `#fixed` slot.
|
|
174
|
+
|
|
175
|
+
## Test
|
|
176
|
+
|
|
177
|
+
`tests/<domain>.repository.test.ts` — see the reactive-data skill's testing guide for the harness.
|
|
178
|
+
Assert the arithmetic, the empty state and the idempotence, not just that rows come back.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reactive-data
|
|
3
|
+
description: Reading and writing the local SQLite database in this app. Use whenever the task involves a query, a mutation, a repository, a migration, the schema, useReactiveQuery, the change bus, stale data, a screen not refreshing, query metrics, or testing data access. Covers the tables invalidation contract, why every write goes through rdb, and how to test SQL without a device.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Reactive data workflow
|
|
7
|
+
|
|
8
|
+
SQLite is the source of truth. A screen reads it through a reactive query; a write announces the
|
|
9
|
+
tables it touched and every query watching one of them refetches. Nothing calls refetch by hand.
|
|
10
|
+
|
|
11
|
+
## Adding a read
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const query = useReactiveQuery(() => searchOrders(getDatabase().db, term.value), {
|
|
15
|
+
tables: ["sales_order", "order_line", "customer"],
|
|
16
|
+
queryKey: ["demo:orders", term],
|
|
17
|
+
debounce: 250,
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
1. Put the SQL in a repository: `src/domains/<domain>/<domain>.repository.ts`, taking the database
|
|
22
|
+
as its first parameter. Never reach for the singleton inside a repository — that is what makes it
|
|
23
|
+
testable.
|
|
24
|
+
2. **List every table the SQL touches in `tables`.** Count them in the query, not from memory: a
|
|
25
|
+
join means each joined table. Under-list and the screen goes stale with no error; over-list and
|
|
26
|
+
an unrelated write re-runs an expensive query.
|
|
27
|
+
3. `queryKey` is an array of the values the query reads. Two mounted queries whose keys match
|
|
28
|
+
share one request, so anything that distinguishes them belongs in the key — a route param, a
|
|
29
|
+
filter ref. Refs are unwrapped and tracked: when one moves the query re-runs through its own
|
|
30
|
+
`debounce`, so never pair a key with a manual `refetch()`.
|
|
31
|
+
4. `debounce` so a burst of writes causes one refetch.
|
|
32
|
+
|
|
33
|
+
## Adding a write
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
await saveOrder(rdb, payload); // announces its tables
|
|
37
|
+
await saveOrder(getDatabase().db, …); // writes, and no screen notices
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`rdb` is the reactive wrapper. A raw write lands in SQLite silently and the UI keeps showing old
|
|
41
|
+
rows until something unrelated refetches — no error, nothing to see in review.
|
|
42
|
+
|
|
43
|
+
All-or-nothing work goes in one transaction. A helper anyone can press twice must survive being
|
|
44
|
+
pressed twice: insert-where-missing, or `onConflict(...).doNothing()`.
|
|
45
|
+
|
|
46
|
+
## Schema changes
|
|
47
|
+
|
|
48
|
+
`schema.ts` and `migrations.ts` are edited together — a field in one and not the other is a runtime
|
|
49
|
+
error the compiler cannot see. Migrations are numbered, never renamed, never edited after shipping.
|
|
50
|
+
Money is integer cents. Index what you filter and join by.
|
|
51
|
+
|
|
52
|
+
## Testing
|
|
53
|
+
|
|
54
|
+
Always. [testing.md](testing.md) has the harness: a Kysely on `createSqlJsDialect()`, migrate, then
|
|
55
|
+
assert against real rows — including the arithmetic. A total that type-checks can still be computed
|
|
56
|
+
wrong, and a device is not needed to catch that.
|
|
57
|
+
|
|
58
|
+
## Diagnosing "the screen did not update"
|
|
59
|
+
|
|
60
|
+
In order:
|
|
61
|
+
|
|
62
|
+
1. Did the write go through `rdb`?
|
|
63
|
+
2. Does the query's `tables` include the table that changed?
|
|
64
|
+
3. Is the query's `queryKey` shared with a different query? Check the console for the conflict
|
|
65
|
+
warning.
|
|
66
|
+
4. Is the page off-screen? `usePageVisibility` suppresses refetches for hidden pages by design.
|
|
67
|
+
5. Only then look at the packages.
|
|
68
|
+
|
|
69
|
+
## Proof obligations
|
|
70
|
+
|
|
71
|
+
Name the tables the SQL touches and confirm `tables` matches. Say whether a test covers it. Never
|
|
72
|
+
claim a data path works on the strength of a type-check.
|
|
73
|
+
|
|
74
|
+
## Inserting a parent and its children
|
|
75
|
+
|
|
76
|
+
The parent and its children go in one transaction, and the parent's id comes from `insertId`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
await db.transaction().execute(async (trx) => {
|
|
80
|
+
const inserted = await trx.insertInto("sales_order").values({ ... }).executeTakeFirstOrThrow();
|
|
81
|
+
const orderId = Number(inserted.insertId ?? 0);
|
|
82
|
+
if (!orderId) throw new Error("the order was written but the database reported no id for it");
|
|
83
|
+
...
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`.returning("id")` looks like the obvious way and is the wrong one: inside an open transaction the
|
|
88
|
+
plugin runs the statement through `query()` and discards its RETURNING rows, so kysely throws
|
|
89
|
+
`no result` from a write that succeeded. `insertId` comes from `last_insert_rowid()` and works on
|
|
90
|
+
both sides of the boundary. Full note in [database.md](../../rules/database.md).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Testing data access
|
|
2
|
+
|
|
3
|
+
The queries run against real SQLite — sql.js, the same dialect `@cavulsqa/mobile-db` uses for its
|
|
4
|
+
own tests. No device, no emulator, about a second for the suite.
|
|
5
|
+
|
|
6
|
+
## The harness
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { beforeEach, expect, test } from "vite-plus/test";
|
|
10
|
+
import { Kysely } from "kysely";
|
|
11
|
+
import { Migrator } from "kysely/migration";
|
|
12
|
+
import { createSqlJsDialect } from "@cavulsqa/mobile-db/testing";
|
|
13
|
+
import { migrations } from "../src/shared/database/migrations.js";
|
|
14
|
+
import type { Database } from "../src/shared/database/schema.js";
|
|
15
|
+
|
|
16
|
+
let db: Kysely<Database>;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
db = new Kysely<Database>({ dialect: await createSqlJsDialect() });
|
|
20
|
+
await new Migrator({
|
|
21
|
+
db,
|
|
22
|
+
provider: { getMigrations: () => Promise.resolve(migrations) },
|
|
23
|
+
}).migrateToLatest();
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A fresh in-memory database per test, with the app's real migrations applied. `Migrator` comes from
|
|
28
|
+
`kysely/migration` — the root export is a compile-time error in kysely 0.29.
|
|
29
|
+
|
|
30
|
+
## What to assert
|
|
31
|
+
|
|
32
|
+
Type-checking proves the query compiles. These are the things it cannot prove:
|
|
33
|
+
|
|
34
|
+
- **Arithmetic.** Two lines at quantity 1 and 2 × 1000 cents must total 3000. Write the number.
|
|
35
|
+
- **Empty state.** An aggregate over no rows returns `0`, not `null`. `coalesce` is easy to forget
|
|
36
|
+
and the screen shows a blank tile.
|
|
37
|
+
- **Joins.** Every joined row actually resolves — product names present, customer attached.
|
|
38
|
+
- **Filters.** A search matches on each field it claims to, and returns `[]` for no match.
|
|
39
|
+
- **State machines.** A status cycle lands where you expect at each step.
|
|
40
|
+
- **Idempotence.** Anything a person can press twice, pressed twice. `seedSampleData` threw
|
|
41
|
+
`UNIQUE constraint failed: tag.label` on the second press and only a test caught it.
|
|
42
|
+
- **Missing rows.** A lookup for an id that does not exist returns `null`, and an update against one
|
|
43
|
+
is a no-op rather than a throw.
|
|
44
|
+
|
|
45
|
+
## The icon test
|
|
46
|
+
|
|
47
|
+
`tests/icons.test.ts` guards a different silent failure: it reads the framework7-icons ttf and
|
|
48
|
+
asserts every name used in `src/` is a real ligature, plus that the five names this app has already
|
|
49
|
+
got wrong stay unresolvable. Extend the second list whenever a wrong name gets through.
|
|
50
|
+
|
|
51
|
+
## Running
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
vp test # from templates/m3e-app
|
|
55
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Which SQLite engine the app opens, and what it falls back to.
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to `.env` and edit. Everything here is baked in at build time - Vite substitutes
|
|
4
|
+
# `import.meta.env` when it bundles - so changing it means rebuilding, not restarting.
|
|
5
|
+
|
|
6
|
+
# The engine tried first. The rest of the chain in src/app/storage.config.ts still follows it as
|
|
7
|
+
# fallback, so this reorders rather than restricts.
|
|
8
|
+
#
|
|
9
|
+
# sqlite-wasm-opfs-sahpool official @sqlite.org/sqlite-wasm on an OPFS pool. Fastest measured, the SQLite team's own build, and the default.
|
|
10
|
+
# wa-sqlite-access-handle-pool wa-sqlite on an OPFS pool. Faster at joins and scans on measured hardware; a single-maintainer project.
|
|
11
|
+
# wa-sqlite-opfs-async wa-sqlite over OPFS without the pool, on the Asyncify build.
|
|
12
|
+
# wa-sqlite-idb-batch-atomic SQLite pages in IndexedDB. The slowest, and the only durable route on a WebView below Chromium 109.
|
|
13
|
+
# capacitor-sqlite the native plugin. Slower on every measured axis and it ships in the APK, but a read is not blocked by an in-flight write.
|
|
14
|
+
VITE_STORAGE_ENGINE=sqlite-wasm-opfs-sahpool
|
|
15
|
+
|
|
16
|
+
# PRAGMA profile. `safe` keeps SQLite's durability guarantee; `fast` trades it for speed by handing
|
|
17
|
+
# the flush decision to the OS, which is right for a benchmark and wrong for data a person would
|
|
18
|
+
# miss. Batched writes cost almost the same either way.
|
|
19
|
+
VITE_PRAGMA_PROFILE=safe
|
|
20
|
+
|
|
21
|
+
# Live reload, for development only. Put these in `.env.local` (git-ignored), run the dev server,
|
|
22
|
+
# forward its port to the device, then `npx cap sync android` and install the debug build once:
|
|
23
|
+
#
|
|
24
|
+
# adb reverse tcp:5173 tcp:5173
|
|
25
|
+
#
|
|
26
|
+
# From then on every edit reloads on the device. The dev server listens on VITE_LIVE_RELOAD_PORT
|
|
27
|
+
# too, so if another project already holds 5173, change it here and reverse that port instead.
|
|
28
|
+
# A build with VITE_ENVIRONMENT=staging or prod ignores these and always loads its bundled files.
|
|
29
|
+
# VITE_LIVE_RELOAD=true
|
|
30
|
+
# VITE_LIVE_RELOAD_PORT=5173
|
|
31
|
+
# VITE_LIVE_RELOAD_HOST=localhost
|
|
32
|
+
# VITE_LIVE_RELOAD_SCHEME=http
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# App instructions
|
|
2
|
+
|
|
3
|
+
An offline-first mobile app in **Material 3 Expressive**: Vue 3 + Capacitor over a local SQLite
|
|
4
|
+
database that screens read reactively. Framework7 runs underneath as the navigation engine only.
|
|
5
|
+
Generated from `@cavulsqa/template-m3e-app`.
|
|
6
|
+
|
|
7
|
+
**SQLite is the source of truth.** There is no server in this template. A screen reads the local
|
|
8
|
+
database and a write to the local database refreshes it. If you add a backend, it syncs _into_
|
|
9
|
+
SQLite — it does not become the thing screens read.
|
|
10
|
+
|
|
11
|
+
## Stack
|
|
12
|
+
|
|
13
|
+
Vue 3.5 `<script setup lang="ts">` · `@cavulsqa/m3e-vue` components on `@cavulsqa/m3e` tokens ·
|
|
14
|
+
Framework7 9 core (router, views, pages - nothing visual) · Capacitor 8 (Android first) · Kysely over
|
|
15
|
+
SQLite in an OPFS worker · vue-i18n · Tailwind 4 mapped to M3 tokens · Google Sans Flex · Material
|
|
16
|
+
Symbols · Vite+ (`vp`).
|
|
17
|
+
|
|
18
|
+
| Package | What it gives you |
|
|
19
|
+
| ------------------------ | -------------------------------------------------------------------- |
|
|
20
|
+
| `@cavulsqa/m3e` | 2025-spec colour, the 35 shapes, springs, wavy progress, every token |
|
|
21
|
+
| `@cavulsqa/m3e-vue` | The `M3*` components, overlay services, theme application |
|
|
22
|
+
| `@cavulsqa/mobile-db` | The OPFS worker dialect, migrations, transaction-aware writes |
|
|
23
|
+
| `@cavulsqa/reactive-db` | Change bus, result cache, visibility gate, query metrics |
|
|
24
|
+
| `@cavulsqa/reactive-vue` | `useReactiveQuery` and the Framework7 page-visibility adapter |
|
|
25
|
+
|
|
26
|
+
Do not vendor or fork them. If one is wrong, fix it there and bump the version.
|
|
27
|
+
|
|
28
|
+
## Layout
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
src/
|
|
32
|
+
├── app/ tabs.ts (destinations as data), theme.config.ts (seed, presets, extras)
|
|
33
|
+
├── router/ index.ts aggregates module routes; global/ holds the catch-all
|
|
34
|
+
├── domains/ <domain>/<domain>.repository.ts — SQL, no Vue
|
|
35
|
+
├── modules/ home, demo, gallery, settings — each {router/routes,views,components,composables}
|
|
36
|
+
├── shared/ components (AppPage, SectionHeader, EmptyState), composables (navigation, theme),
|
|
37
|
+
│ database/, utils/
|
|
38
|
+
├── plugins/ framework7 (engine), m3e (services + haptics), capacitor/, i18n, sqlite
|
|
39
|
+
├── assets/css/ app.css (layers), theme/, layout/ (shell, transitions), base.css
|
|
40
|
+
└── locales/ en.json, fr.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A module owns its routes, views, components and composables. A `domain` owns SQL and nothing else.
|
|
44
|
+
`shared` is for what two modules both use today.
|
|
45
|
+
|
|
46
|
+
## Skills and rules
|
|
47
|
+
|
|
48
|
+
| Skill | Reach for it when |
|
|
49
|
+
| --------------------- | ---------------------------------------------------------------------- |
|
|
50
|
+
| `m3-expressive` | Any screen, component, layout, motion, colour or icon work |
|
|
51
|
+
| `reactive-data` | Any query, mutation, repository, migration, or a screen not refreshing |
|
|
52
|
+
| `module-architecture` | Adding a feature, or deciding where a file belongs |
|
|
53
|
+
|
|
54
|
+
Rules in `.claude/rules/`. The short version, each earned by a real bug:
|
|
55
|
+
|
|
56
|
+
- **Everything visible is an `M3*` component; Framework7 is the engine.** Five `F7*` components
|
|
57
|
+
resolve (app, views, view, page, page content). See `.claude/rules/m3e-ui.md`.
|
|
58
|
+
- **Every routed screen is an `AppPage`.** Pushed pages pass `back`; that also hides the bar.
|
|
59
|
+
- **Tokens only.** Roles, the shape scale, `type-*` styles; Tailwind's own palette does not exist
|
|
60
|
+
here. Hex values live in `src/app/theme.config.ts`.
|
|
61
|
+
- **Overlays go through the M3 services** (`useSnackbar`, `useDialog`, `useActionSheet`,
|
|
62
|
+
`M3BottomSheet`, `M3Menu`) so Android back closes the top one first.
|
|
63
|
+
- **Icons are `<i-ms-*-rounded />` SVGs**; a wrong name fails the build, which is the point.
|
|
64
|
+
- **A reactive query's `tables` must list exactly what its function reads.** Under-list and the
|
|
65
|
+
screen goes stale with no error. See `.claude/rules/data-fetching.md`.
|
|
66
|
+
- **Every write goes through `rdb`**, never `getDatabase().db`.
|
|
67
|
+
- **`queryKey` is an identity built from arguments**, not a label: `["demo:order", orderId]`.
|
|
68
|
+
- **An inserted id comes from `insertId`, never `.returning(...)`.** See `.claude/rules/database.md`.
|
|
69
|
+
- **Money is integer cents.**
|
|
70
|
+
- **`f7route` and `f7router` are props**, not imports.
|
|
71
|
+
- **A route loads its component through `lazyRoute`**, never a bare `import().then(resolve)`.
|
|
72
|
+
- **In locale files, `@` and `|` are message syntax.** A literal `@` must be `{'@'}`.
|
|
73
|
+
- **No comments that narrate.** A comment explains a non-obvious _why_.
|
|
74
|
+
|
|
75
|
+
## Verification
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
vp check # format, lint, type-check
|
|
79
|
+
vp test # repository, migration and navigation tests
|
|
80
|
+
pnpm type-check # vue-tsc, the gate for anything touching .vue
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`components.d.ts` and `auto-imports.d.ts` are generated by the dev server and the build; they are
|
|
84
|
+
committed so `vue-tsc` works on a fresh clone.
|
|
85
|
+
|
|
86
|
+
**Type-checking is not verification for UI.** If you changed something visual, check light and dark,
|
|
87
|
+
a 360dp width and reduced motion, and say plainly whether you have seen it run on a device.
|
|
88
|
+
|
|
89
|
+
## What not to do
|
|
90
|
+
|
|
91
|
+
- Do not add a state library or a data-fetching library; see the notes in `@cavulsqa/reactive-vue`.
|
|
92
|
+
- Do not assume the toolchain. Capacitor 8 compiles against **JDK 21**.
|
|
93
|
+
- Do not build for the web. `vp dev` in a browser is for inspection; the target is a device.
|