@cavulsqa/create 2.9.2 → 2.10.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 (209) hide show
  1. package/README.md +12 -1
  2. package/bin/create.mjs +18 -25
  3. package/lib/templates.mjs +56 -0
  4. package/package.json +13 -5
  5. package/templates/f7-app/CLAUDE.md +3 -0
  6. package/templates/f7-app/auto-imports.d.ts +15 -0
  7. package/templates/f7-app/package.json +6 -3
  8. package/templates/f7-app/src/App.vue +1 -0
  9. package/templates/f7-app/src/env.d.ts +1 -0
  10. package/templates/f7-app/src/main.ts +3 -0
  11. package/templates/f7-app/src/modules/demo/router/routes/demo.routes.ts +4 -16
  12. package/templates/f7-app/src/modules/home/router/routes/home.routes.ts +3 -11
  13. package/templates/f7-app/src/modules/settings/router/routes/settings.routes.ts +2 -6
  14. package/templates/f7-app/src/plugins/bootstrapError.ts +2 -0
  15. package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
  16. package/templates/f7-app/src/router/global/global.routes.ts +2 -6
  17. package/templates/f7-app/src/shared/composables/useNavigationGuard.ts +48 -0
  18. package/templates/f7-app/src/shared/recorder/capacitorSink.ts +124 -0
  19. package/templates/f7-app/src/shared/utils/lazyRoute.ts +25 -0
  20. package/templates/f7-app/tests/lazyRoute.test.ts +30 -0
  21. package/templates/f7-app/tests/navigationGuard.test.ts +82 -0
  22. package/templates/f7-app/vite.config.ts +1 -0
  23. package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
  24. package/templates/m3e-app/.claude/rules/database.md +106 -0
  25. package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
  26. package/templates/m3e-app/.claude/rules/modules.md +43 -0
  27. package/templates/m3e-app/.claude/rules/native.md +60 -0
  28. package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
  29. package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
  30. package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
  31. package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
  32. package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
  33. package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
  34. package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
  35. package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
  36. package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
  37. package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
  38. package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
  39. package/templates/m3e-app/.env.example +32 -0
  40. package/templates/m3e-app/CLAUDE.md +93 -0
  41. package/templates/m3e-app/auto-imports.d.ts +851 -0
  42. package/templates/m3e-app/capacitor.config.ts +44 -0
  43. package/templates/m3e-app/components.d.ts +252 -0
  44. package/templates/m3e-app/index.html +16 -0
  45. package/templates/m3e-app/package.json +107 -0
  46. package/templates/m3e-app/src/App.vue +129 -0
  47. package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
  48. package/templates/m3e-app/src/app/scroll.config.ts +20 -0
  49. package/templates/m3e-app/src/app/storage.config.ts +65 -0
  50. package/templates/m3e-app/src/app/tabs.ts +33 -0
  51. package/templates/m3e-app/src/app/theme.config.ts +51 -0
  52. package/templates/m3e-app/src/assets/css/app.css +18 -0
  53. package/templates/m3e-app/src/assets/css/base.css +53 -0
  54. package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
  55. package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
  56. package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
  57. package/templates/m3e-app/src/assets/css/m3e.css +6 -0
  58. package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
  59. package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
  60. package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
  61. package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
  62. package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
  63. package/templates/m3e-app/src/env.d.ts +40 -0
  64. package/templates/m3e-app/src/locales/ar.json +1260 -0
  65. package/templates/m3e-app/src/locales/en.json +1260 -0
  66. package/templates/m3e-app/src/locales/fr.json +1260 -0
  67. package/templates/m3e-app/src/main.ts +41 -0
  68. package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
  69. package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
  70. package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
  71. package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
  72. package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
  73. package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
  74. package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
  75. package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
  76. package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
  77. package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
  78. package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
  79. package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
  80. package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
  81. package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
  82. package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
  83. package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
  84. package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
  85. package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
  86. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
  87. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
  88. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
  89. package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
  90. package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
  91. package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
  92. package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
  93. package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
  94. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
  95. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
  96. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
  97. package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
  98. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
  99. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
  100. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
  101. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
  102. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
  103. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
  104. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
  105. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
  106. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
  107. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
  108. package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
  109. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
  110. package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
  111. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
  112. package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
  113. package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
  114. package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
  115. package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
  116. package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
  117. package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
  118. package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
  119. package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
  120. package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
  121. package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
  122. package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
  123. package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
  124. package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
  125. package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
  126. package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
  127. package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
  128. package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
  129. package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
  130. package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
  131. package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
  132. package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
  133. package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
  134. package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
  135. package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
  136. package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
  137. package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
  138. package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
  139. package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
  140. package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
  141. package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
  142. package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
  143. package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
  144. package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
  145. package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
  146. package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
  147. package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
  148. package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
  149. package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
  150. package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
  151. package/templates/m3e-app/src/modules/settings/types.ts +7 -0
  152. package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
  153. package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
  154. package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
  155. package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
  156. package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
  157. package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
  158. package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
  159. package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
  160. package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
  161. package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
  162. package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
  163. package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
  164. package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
  165. package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
  166. package/templates/m3e-app/src/router/index.ts +20 -0
  167. package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
  168. package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
  169. package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
  170. package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
  171. package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
  172. package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
  173. package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
  174. package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
  175. package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
  176. package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
  177. package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
  178. package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
  179. package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
  180. package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
  181. package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
  182. package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
  183. package/templates/m3e-app/src/shared/database/database.ts +219 -0
  184. package/templates/m3e-app/src/shared/database/index.ts +3 -0
  185. package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
  186. package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
  187. package/templates/m3e-app/src/shared/database/queries.ts +26 -0
  188. package/templates/m3e-app/src/shared/database/schema.ts +153 -0
  189. package/templates/m3e-app/src/shared/database/storage.ts +64 -0
  190. package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
  191. package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
  192. package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
  193. package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
  194. package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
  195. package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
  196. package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
  197. package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
  198. package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
  199. package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
  200. package/templates/m3e-app/tests/locales.test.ts +73 -0
  201. package/templates/m3e-app/tests/migrations.test.ts +74 -0
  202. package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
  203. package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
  204. package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
  205. package/templates/m3e-app/tests/storage.test.ts +117 -0
  206. package/templates/m3e-app/tests/textDirection.test.ts +11 -0
  207. package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
  208. package/templates/m3e-app/tsconfig.json +21 -0
  209. package/templates/m3e-app/vite.config.ts +144 -0
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: module-architecture
3
+ description: Where code goes in this app and how to add a feature end to end. Use when creating a new screen, module, route, domain, repository, composable, or when deciding whether something belongs in modules, domains or shared. Covers the dependency direction, the file layout of a module, and the checklist for wiring a feature into the shell.
4
+ ---
5
+
6
+ # Module architecture
7
+
8
+ ```
9
+ src/
10
+ ├── app/tabs.ts the navigation destinations, as data
11
+ ├── router/index.ts aggregates module routes, catch-all last
12
+ ├── domains/<domain>/<x>.repository.ts SQL only
13
+ ├── modules/<feature>/
14
+ │ ├── router/routes/<feature>.routes.ts
15
+ │ ├── views/<Name>View.vue
16
+ │ ├── components/*.vue
17
+ │ └── composables/use*.ts
18
+ └── shared/ what two modules genuinely both need
19
+ ```
20
+
21
+ ## The seam
22
+
23
+ | Layer | Owns | Never contains |
24
+ | ---------- | --------------------------------------- | -------------------------------------------- |
25
+ | View | Wiring a composable to components | Business logic, SQL |
26
+ | Composable | State, queries, actions for one feature | Markup |
27
+ | Repository | Plain functions over Kysely | `ref`, lifecycle, Framework7, module imports |
28
+ | Component | Props in, emits out | Database access |
29
+
30
+ Dependencies run **modules → domains → shared → packages**, never backwards. A repository importing
31
+ from a module, or `shared` importing from `modules`, means something is in the wrong place.
32
+
33
+ ## Adding a feature
34
+
35
+ 1. `modules/<feature>/` with the four folders. Templates: [file-templates.md](file-templates.md).
36
+ 2. SQL in `domains/<domain>/<domain>.repository.ts`, taking the database as its first parameter.
37
+ 3. Routes in `modules/<feature>/router/routes/<feature>.routes.ts`, default-exported, registered in
38
+ `src/router/index.ts` **before** the global catch-all.
39
+ 4. A tab in `src/app/tabs.ts` only if it is a top-level section — with both `iconIos` and `iconMd`.
40
+ Otherwise it is a pushed page, and pushed pages are an `AppPage` with `back`, which hides the navigation bar.
41
+ 5. Strings in **both** `locales/en.json` and `locales/fr.json`. A missing key renders as the key.
42
+ Remember `@` and `|` are message syntax.
43
+ 6. A test in `tests/` for anything with SQL.
44
+ 7. `vp check`, `vp test`, `pnpm type-check`.
45
+
46
+ ## shared/ is not a junk drawer
47
+
48
+ Something moves to `shared/` when a second module imports it **today**. The test: name the second
49
+ caller. If you cannot, it lives in the module that uses it.
50
+
51
+ ## Auto-imports
52
+
53
+ `ref`, `computed`, `watch`, lifecycle hooks, `useI18n`, `@vueuse/core`, `f7`, `f7ready` and
54
+ everything under `shared/composables`, `shared/utils`, `plugins` and `modules/**/composables` are
55
+ auto-imported — no import line. Components under `shared/components` and `modules/**/{views,components}`
56
+ resolve the same way.
57
+
58
+ Two things are **not** auto-imported and must be declared:
59
+
60
+ - `f7route` / `f7router` — Framework7 passes them to a route component as props.
61
+ - Repositories and anything under `domains/` — imported explicitly, because a domain is a boundary
62
+ you should see being crossed.
63
+
64
+ `auto-imports.d.ts` and `components.d.ts` are generated. Never hand-edit them; if the editor
65
+ disagrees with the build, run the dev server once to regenerate.
@@ -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.