@cavulsqa/create 2.9.3 → 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 (199) 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/auto-imports.d.ts +9 -0
  6. package/templates/f7-app/package.json +6 -3
  7. package/templates/f7-app/src/env.d.ts +1 -0
  8. package/templates/f7-app/src/main.ts +3 -0
  9. package/templates/f7-app/src/plugins/bootstrapError.ts +2 -0
  10. package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
  11. package/templates/f7-app/src/shared/recorder/capacitorSink.ts +124 -0
  12. package/templates/f7-app/vite.config.ts +1 -0
  13. package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
  14. package/templates/m3e-app/.claude/rules/database.md +106 -0
  15. package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
  16. package/templates/m3e-app/.claude/rules/modules.md +43 -0
  17. package/templates/m3e-app/.claude/rules/native.md +60 -0
  18. package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
  19. package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
  20. package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
  21. package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
  22. package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
  23. package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
  24. package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
  25. package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
  26. package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
  27. package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
  28. package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
  29. package/templates/m3e-app/.env.example +32 -0
  30. package/templates/m3e-app/CLAUDE.md +93 -0
  31. package/templates/m3e-app/auto-imports.d.ts +851 -0
  32. package/templates/m3e-app/capacitor.config.ts +44 -0
  33. package/templates/m3e-app/components.d.ts +252 -0
  34. package/templates/m3e-app/index.html +16 -0
  35. package/templates/m3e-app/package.json +107 -0
  36. package/templates/m3e-app/src/App.vue +129 -0
  37. package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
  38. package/templates/m3e-app/src/app/scroll.config.ts +20 -0
  39. package/templates/m3e-app/src/app/storage.config.ts +65 -0
  40. package/templates/m3e-app/src/app/tabs.ts +33 -0
  41. package/templates/m3e-app/src/app/theme.config.ts +51 -0
  42. package/templates/m3e-app/src/assets/css/app.css +18 -0
  43. package/templates/m3e-app/src/assets/css/base.css +53 -0
  44. package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
  45. package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
  46. package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
  47. package/templates/m3e-app/src/assets/css/m3e.css +6 -0
  48. package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
  49. package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
  50. package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
  51. package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
  52. package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
  53. package/templates/m3e-app/src/env.d.ts +40 -0
  54. package/templates/m3e-app/src/locales/ar.json +1260 -0
  55. package/templates/m3e-app/src/locales/en.json +1260 -0
  56. package/templates/m3e-app/src/locales/fr.json +1260 -0
  57. package/templates/m3e-app/src/main.ts +41 -0
  58. package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
  59. package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
  60. package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
  61. package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
  62. package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
  63. package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
  64. package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
  65. package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
  66. package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
  67. package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
  68. package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
  69. package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
  70. package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
  71. package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
  72. package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
  73. package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
  74. package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
  75. package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
  76. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
  77. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
  78. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
  79. package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
  80. package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
  81. package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
  82. package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
  83. package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
  84. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
  85. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
  86. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
  87. package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
  88. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
  89. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
  90. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
  91. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
  92. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
  93. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
  94. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
  95. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
  96. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
  97. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
  98. package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
  99. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
  100. package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
  101. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
  102. package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
  103. package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
  104. package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
  105. package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
  106. package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
  107. package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
  108. package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
  109. package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
  110. package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
  111. package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
  112. package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
  113. package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
  114. package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
  115. package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
  116. package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
  117. package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
  118. package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
  119. package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
  120. package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
  121. package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
  122. package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
  123. package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
  124. package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
  125. package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
  126. package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
  127. package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
  128. package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
  129. package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
  130. package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
  131. package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
  132. package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
  133. package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
  134. package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
  135. package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
  136. package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
  137. package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
  138. package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
  139. package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
  140. package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
  141. package/templates/m3e-app/src/modules/settings/types.ts +7 -0
  142. package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
  143. package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
  144. package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
  145. package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
  146. package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
  147. package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
  148. package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
  149. package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
  150. package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
  151. package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
  152. package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
  153. package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
  154. package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
  155. package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
  156. package/templates/m3e-app/src/router/index.ts +20 -0
  157. package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
  158. package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
  159. package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
  160. package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
  161. package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
  162. package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
  163. package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
  164. package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
  165. package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
  166. package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
  167. package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
  168. package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
  169. package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
  170. package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
  171. package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
  172. package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
  173. package/templates/m3e-app/src/shared/database/database.ts +219 -0
  174. package/templates/m3e-app/src/shared/database/index.ts +3 -0
  175. package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
  176. package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
  177. package/templates/m3e-app/src/shared/database/queries.ts +26 -0
  178. package/templates/m3e-app/src/shared/database/schema.ts +153 -0
  179. package/templates/m3e-app/src/shared/database/storage.ts +64 -0
  180. package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
  181. package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
  182. package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
  183. package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
  184. package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
  185. package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
  186. package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
  187. package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
  188. package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
  189. package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
  190. package/templates/m3e-app/tests/locales.test.ts +73 -0
  191. package/templates/m3e-app/tests/migrations.test.ts +74 -0
  192. package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
  193. package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
  194. package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
  195. package/templates/m3e-app/tests/storage.test.ts +117 -0
  196. package/templates/m3e-app/tests/textDirection.test.ts +11 -0
  197. package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
  198. package/templates/m3e-app/tsconfig.json +21 -0
  199. package/templates/m3e-app/vite.config.ts +144 -0
@@ -0,0 +1,106 @@
1
+ # Schema and migrations
2
+
3
+ `src/shared/database/schema.ts` is what Kysely type-checks every query against.
4
+ `src/shared/database/migrations.ts` is what actually creates the tables. The two are edited
5
+ together, always: a field in one and not the other is a runtime error the compiler cannot see.
6
+
7
+ ## Migrations
8
+
9
+ - Keys are ordered lexically and recorded once applied, so they are **numbered and never renamed**.
10
+ Renaming one makes it run again on a database that already has it.
11
+ - Never edit a migration that has shipped. Add the next one.
12
+ - A synced table is created with `createTableWithDefaults` when it carries the sync contract, or a
13
+ plain `createTable` when it does not. This template has no server, so plain tables are the norm.
14
+ - Declare foreign keys, and index the columns screens filter and join by. Without them every
15
+ dashboard aggregate is a full scan, which you will not notice until the table is large and the
16
+ device is slow.
17
+ - SQLite ignores foreign keys unless asked; `PRAGMA foreign_keys = ON` runs at the end of the
18
+ migration. It is per-connection, so a cascade is not something to rely on — `deleteOrder` removes
19
+ the lines explicitly.
20
+
21
+ ## Types
22
+
23
+ - Money is **integer cents**, named `*_cents`. A float total is a rounding bug waiting for a
24
+ large-enough order.
25
+ - Timestamps are ISO strings via `nowISO()`.
26
+ - A price copied onto an order line is copied deliberately, so a later catalogue change does not
27
+ rewrite history.
28
+
29
+ ## Getting an inserted id
30
+
31
+ Use `insertId`, never `.returning(...)`:
32
+
33
+ ```ts
34
+ const inserted = await trx.insertInto("sales_order").values({ ... }).executeTakeFirstOrThrow();
35
+ const orderId = Number(inserted.insertId ?? 0);
36
+ if (!orderId) throw new Error("the order was written but the database reported no id for it");
37
+ ```
38
+
39
+ `insertId` is the portable answer. Every engine reports it — the worker engines from
40
+ `last_insert_rowid()`, the sql.js test dialect the same way — so a repository written against it
41
+ behaves identically in tests and on a device. `.returning(...)` does work on the worker engines, but
42
+ it is the one thing that differs between them: the Capacitor plugin runs a statement issued inside an
43
+ open transaction through `query()`, which executes it and silently drops its RETURNING rows, so
44
+ `.returning("id").executeTakeFirstOrThrow()` threw `no result` from an insert that had in fact
45
+ succeeded. `@cavulsqa/mobile-db` now throws a message that says so instead.
46
+
47
+ ## The web path is the device path
48
+
49
+ Both run the same engine: SQLite compiled to WebAssembly, in a worker, with the database file in
50
+ OPFS. There is no Capacitor SQLite plugin in this template and no sql.js outside the tests. So:
51
+
52
+ - **Browser data survives a reload.** OPFS is durable storage, not memory. Clear it from
53
+ Diagnostics, or through the browser's site-data controls.
54
+ - A bug reproduced in the browser is very likely the same bug as on the device, which was not true
55
+ when the two ran different engines.
56
+ - `localStorage["app.storage.force"]` pins the chain to one engine id, for comparing them.
57
+
58
+ What still does not transfer is **timing**. A phone's storage and CPU are nothing like a laptop's,
59
+ and the worker is serial either way, so a ratio measured in a browser says nothing about the device.
60
+ Run the Diagnostics benchmark on hardware.
61
+
62
+ ## Proof obligations
63
+
64
+ A schema change needs a test in `tests/` that runs the migration and the affected queries against
65
+ sql.js. `tests/sales.repository.test.ts` is the pattern: build a Kysely on `createSqlJsDialect()`,
66
+ migrate, then assert on real rows — including the arithmetic. A total that type-checks can still be
67
+ computed wrong.
68
+
69
+ ## Writing a lot of rows
70
+
71
+ Measured on a phone, at 100k rows, per row written:
72
+
73
+ | how | per row |
74
+ | ------------------------------------------------ | -------------- |
75
+ | one insert per statement, each its own commit | 8-13 ms |
76
+ | one insert per statement, inside one transaction | ~0.45 ms |
77
+ | multi-row insert, ~150 rows per statement | 0.066-0.115 ms |
78
+
79
+ Roughly a hundredfold between the worst and best way to write the same row. SQLite caps parameters
80
+ per statement, so 150 rows of five columns is about the practical ceiling for one insert - chunk by
81
+ parameter budget, not by a round number.
82
+
83
+ ## Why a big write freezes the screen, and what to do
84
+
85
+ The database runs in one worker, and that worker is serial. A read cannot overtake a write already
86
+ in flight; it waits for everything queued ahead of it. So the cost to the UI is not how fast the
87
+ write is, it is **how much work the write committed to before the read arrived**.
88
+
89
+ Time a screen's read waits when it lands during a 1000-row write:
90
+
91
+ | write strategy | read waits |
92
+ | ---------------------------------------------- | ------------- |
93
+ | 1000 single inserts in one transaction | ~350-690 ms |
94
+ | 150 rows per statement, one transaction | ~36-53 ms |
95
+ | ten transactions of 100, awaited one at a time | **~21-32 ms** |
96
+
97
+ A naive loop stalls the screen for most of a second. Chunked transactions bring it under the
98
+ threshold anyone notices, and the reason is mechanical: awaiting each chunk means only one chunk is
99
+ ever queued, so an arriving read waits for 100 rows instead of 1000.
100
+
101
+ **So: write in chunks of about a hundred rows, use multi-row inserts inside each chunk, and await
102
+ each chunk before starting the next.** Do not wrap a thousand rows in one transaction to be fast -
103
+ it is faster in total and far worse for anyone looking at the screen while it runs.
104
+
105
+ The Diagnostics benchmark measures all three strategies, so this is checkable on any device rather
106
+ than taken on faith.
@@ -0,0 +1,48 @@
1
+ # UI: Material 3 Expressive on a Framework7 engine
2
+
3
+ The full workflow is the `m3-expressive` skill. These are the guardrails it rests on, each one a way
4
+ the screen breaks silently.
5
+
6
+ ## Framework7 is the engine, not the look
7
+
8
+ Only five Framework7 components resolve: `F7App`, `F7Views`, `F7View`, `F7Page`, `F7PageContent`
9
+ (the allowlist in `src/shared/utils/resolvers/resolvers.ts`). Routing, per-tab history and page
10
+ events come from them; everything visible is an `M3*` component. A visual `f7-*` component brings
11
+ Framework7's styling and modal stack back and fights the M3 overlay stack for Android back.
12
+
13
+ `f7`, `f7ready` are auto-imported; `f7route` / `f7router` arrive as props of a route component:
14
+ `defineProps<{ f7route: Router.Route; f7router: Router.Router }>()`. Inside shared components, reach
15
+ the router with `useViewRouter(el)`.
16
+
17
+ ## CSS is layered
18
+
19
+ `assets/css/app.css` declares `@layer framework7, theme, base, components, utilities`. Framework7's
20
+ CSS is the lowest layer and the M3 components sit below Tailwind's utilities, so a utility on a
21
+ component always wins and `!important` is never needed. New global CSS goes in a layer; component
22
+ CSS goes in the component's `<style scoped>`.
23
+
24
+ Layers only settle conflicts: a Framework7 rule nothing else contradicts still applies. Its core
25
+ sizes every bare `button` to `width: 100%` - the M3 components undo it for their own buttons, so a
26
+ hand-made `<button>` must set its width or use an `M3*` button.
27
+
28
+ In `<style scoped>`, wrap the whole selector: `:global(.parent .child)`. Vue compiles
29
+ `:global(.parent) .child` to `.parent` alone, silently styling the wrong element.
30
+
31
+ ## Tokens only
32
+
33
+ Tailwind's default palette, radii, shadows and easings are cleared in
34
+ `assets/css/theme/tailwind.css` and refilled from `--md-sys-*`. A class like `bg-red-500` does not
35
+ exist. Colours: roles. Corners: the shape scale. Type: `type-*`. Hex values live in
36
+ `src/app/theme.config.ts` only.
37
+
38
+ ## Icons are SVG components, checked at build time
39
+
40
+ `<i-ms-<name>-rounded />` from Material Symbols (outlined: `-outline-rounded`, filled: `-rounded`).
41
+ `autoInstall` is off, so a wrong name fails the build instead of rendering nothing. In TS:
42
+ `import Icon from "~icons/material-symbols/<name>"` and `markRaw` it before putting it in reactive
43
+ state.
44
+
45
+ ## Proof obligations
46
+
47
+ `vp check`, `pnpm type-check` and `vp test` pass. Say what you saw run - light and dark, compact width,
48
+ reduced motion - and say plainly when you have not seen it on a device.
@@ -0,0 +1,43 @@
1
+ # Modules, domains and shared
2
+
3
+ ```
4
+ modules/<feature>/router/routes/<feature>.routes.ts default export, Router.RouteParameters[]
5
+ modules/<feature>/views/<Name>View.vue thin, presentational
6
+ modules/<feature>/components/*.vue props in, emits out
7
+ modules/<feature>/composables/use*.ts the feature's state and actions
8
+ domains/<domain>/<domain>.repository.ts SQL only
9
+ shared/… what two modules genuinely both need
10
+ ```
11
+
12
+ ## The seam
13
+
14
+ A **view** wires a composable to components. If it holds business logic, that logic belongs in the
15
+ composable; if it holds SQL, that belongs in a repository.
16
+
17
+ A **composable** owns state, queries and actions for one feature. It may import repositories and
18
+ the reactive query helpers. It returns refs and functions, never markup.
19
+
20
+ A **repository** is plain functions over Kysely. No `ref`, no lifecycle, no Framework7, no imports
21
+ from `modules/`. It takes the database as a parameter — that is what makes it testable, and reaching
22
+ for the singleton instead is what made the first version of this template untestable.
23
+
24
+ A **component** takes props and emits events. It does not query the database.
25
+
26
+ ## Direction of dependencies
27
+
28
+ `modules → domains → shared → packages`. Never backwards. A repository importing from a module, or
29
+ `shared` importing from `modules`, means something is in the wrong place.
30
+
31
+ ## Adding a feature
32
+
33
+ 1. `modules/<feature>/` with the four folders.
34
+ 2. Its routes in `router/routes/<feature>.routes.ts`, registered in `src/router/index.ts` before the
35
+ global catch-all.
36
+ 3. A tab in `src/app/tabs.ts` only if it is a top-level section. Otherwise it is a pushed page, and
37
+ pushed pages are an `AppPage` with `back`, which hides the navigation bar.
38
+ 4. Strings in **both** `locales/en.json` and `locales/fr.json`. A missing key renders the key.
39
+
40
+ ## shared/ is not a junk drawer
41
+
42
+ Something goes in `shared/` when two modules import it today. Not when one module might later. The
43
+ test is: can you name the second caller? If not, it lives in the module that uses it.
@@ -0,0 +1,60 @@
1
+ # Capacitor and native behaviour
2
+
3
+ Every native call is guarded with `Capacitor.isNativePlatform()` or
4
+ `Capacitor.getPlatform() === "web"`. `vp dev` in a browser must keep working — it is how the app is
5
+ inspected — so a handler that assumes a device breaks the fastest feedback loop you have.
6
+
7
+ Native wiring that needs the Framework7 instance runs inside `f7ready`, not at module load.
8
+ `src/plugins/capacitor/index.ts` is the single entry point.
9
+
10
+ ## What is already handled, and why it is not simple
11
+
12
+ - **Back button** (`useAndroidBackButton`) first asks the M3 overlay stack to close its topmost
13
+ overlay (sheet, dialog, menu, FAB menu) - a persistent one swallows back instead. Then the current
14
+ tab's history, then from another tab's root the start destination, then it minimises rather than
15
+ exits. Overlays register themselves through `useOverlay`; a hand-built modal is invisible to back.
16
+ - **Keyboard** (`useKeyboard`) scrolls the focused input into view on _every_ phase of the
17
+ transition, not once — the layout is still settling at `keyboardWillShow` and only
18
+ `keyboardDidShow` sees the final height. It also hides the navigation bar, which otherwise steals a
19
+ row from the field being typed into.
20
+ - **Status bar** overlays the web view (edge to edge); the top app bar pads itself with the inset,
21
+ and the icon colour follows the theme's dark mode.
22
+ - **Splash** stays up until `hideSplashScreen()`, called on a frame boundary so there is no flash of
23
+ an unpainted shell.
24
+
25
+ Do not simplify these into a single listener. Each branch is there because of a specific device
26
+ behaviour, and the comments say which.
27
+
28
+ ## The bootstrap must never fail silently
29
+
30
+ `main.ts` opens the database before mounting, inside a `try`, and renders the failure on the page if
31
+ it throws. An earlier version awaited it at module top level: a rejection produced an empty `#app`
32
+ and a completely silent console, which is the worst possible failure for whoever generates from this
33
+ template. The timeout exists so a hang cannot masquerade as a blank screen either.
34
+
35
+ Anything else added to the bootstrap follows the same shape: guarded, and loud when it fails.
36
+
37
+ ## Fixed elements and the shell
38
+
39
+ The compact navigation bar floats over the bottom of the views; the shell publishes its height as
40
+ `--app-nav-offset` and `--app-bottom-inset` (offset plus the gesture area). So:
41
+
42
+ - Anything floating above content offsets by `--app-bottom-inset` - `AppPage`'s `#fab` slot, the
43
+ snackbar host and the floating toolbar already do. Never measure the bar.
44
+ - Page content is already padded by the same variable; do not add your own bottom spacer for it.
45
+ - The FAB menu opens upward, away from the bar.
46
+
47
+ ## Permissions live in a generated folder
48
+
49
+ `android/` is generated by `cap add android` and gitignored, so a permission the app needs is not
50
+ carried by the template. Add it to `android/app/src/main/AndroidManifest.xml` after generating:
51
+
52
+ - **Location** (the chat's _Location_ attachment): `ACCESS_COARSE_LOCATION` and
53
+ `ACCESS_FINE_LOCATION`. The web view's own `navigator.geolocation` does the lookup; Capacitor
54
+ asks the person at runtime only for a permission the manifest declares, so without these the
55
+ lookup fails as `denied` and the chat offers the depot instead.
56
+
57
+ ## Proof obligations
58
+
59
+ Say which platform you tested on. "Type-checks" is not a claim about a device, and neither is a
60
+ browser. If you have not run it on Android, say so.
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: m3-expressive
3
+ description: Material 3 Expressive UI in this app. Use for any screen, component, layout, animation, transition, sheet, dialog, menu, list, button, progress or loading state, colour or theme change, icon, or app-bar work - before writing markup.
4
+ ---
5
+
6
+ # Material 3 Expressive
7
+
8
+ This app is Material 3 Expressive end to end. Framework7 is only the navigation engine (router, tab
9
+ views, page lifecycle); everything visible is an `M3*` component from `@cavulsqa/m3e-vue`, styled by
10
+ `--md-sys-*` tokens from `@cavulsqa/m3e`. Expressive is not decoration: shape, spring and tonal
11
+ colour say _what is happening_ - a selection rounds off, a sheet springs, progress waves while work
12
+ moves. Every number comes from Google's Compose tokens; [sources.md](sources.md) links each spec.
13
+
14
+ ## Building a screen
15
+
16
+ 1. **Name the job in one sentence** and the nearest Google app that does it (Messages, Photos,
17
+ Settings, Files, Wallet). Open its pattern on m3.material.io from [sources.md](sources.md).
18
+ 2. **Pick the component** from [components.md](components.md). Every visible element is an `M3*`
19
+ component or Tailwind layout around one. Missing a component? It belongs in
20
+ `packages/m3e-vue`, built from its spec page - not a one-off in a module.
21
+ 3. **Wrap the route in `AppPage`** (`shared/components/page`): tab roots get the large flexible app
22
+ bar, pushed pages pass `back`. Layout rules: [layout.md](layout.md).
23
+ 4. **Choose shape, colour role and motion** - [shapes-type.md](shapes-type.md),
24
+ [color.md](color.md), [motion.md](motion.md). Each decision names its token.
25
+ 5. **Strings in both locales**, icons from Material Symbols (`<i-ms-name-rounded />`), filled
26
+ variant for a selected state.
27
+ 6. **Done means**: `vp check`, `pnpm type-check` and `vp test` pass; you checked light, dark, a 360dp
28
+ width, reduced motion, and RTL by reasoning or on screen; and you said plainly whether you saw it
29
+ run. Type-checking proves nothing about how a screen looks.
30
+
31
+ ## The rules every screen keeps
32
+
33
+ - **Tokens only.** Colour is a role (`bg-surface-container-low`, `text-on-primary-container`),
34
+ corners are the shape scale (`rounded-lg`, `rounded-xl`), type is a style (`type-title-medium`),
35
+ shadows are levels (`shadow-1`..`shadow-5`). Tailwind's own palette is deleted from the theme, so
36
+ only roles resolve; a hex value belongs in `src/app/theme.config.ts` and nowhere else.
37
+ - **Pairs travel together.** A container role always carries its `on-` role:
38
+ `bg-tertiary-container text-on-tertiary-container`. Text on a surface is `text-on-surface` or
39
+ `text-on-surface-variant`.
40
+ - **Surfaces stack by container tone**, not by shadow: page `surface`, grouped content
41
+ `surface-container-low`, raised panels `surface-container`/`-high`. Shadows only on what floats.
42
+ - **Overlays come from the services**: `useSnackbar`, `useDialog`, `useActionSheet` (auto-imported),
43
+ or `M3BottomSheet`/`M3Menu` with `v-model:open`. They register on the overlay stack, so Android
44
+ back and Escape close the top one. Never hand-build a modal.
45
+ - **One tap target per row.** A list row's own button lives in `M3ListItem`'s `#action` slot, never
46
+ inside a clickable row.
47
+ - **Motion is spatial or effects.** Position, size and shape use the spatial springs; colour and
48
+ opacity the effects springs. Never animate `width`/`top` of a large surface - `transform` and
49
+ `opacity`.
50
+ - **Reduced motion keeps meaning, drops travel**: morphs and fades stay, rotation, bounce and slide
51
+ go. The tokens collapse spatial durations to 1 ms for you; JS-driven motion reads
52
+ `useReducedMotion()`.
53
+ - **Every control is labelled**: icon buttons, FABs and switches take `label`; it becomes the
54
+ accessible name and tooltip.
55
+ - **Empty is a state, not a blank.** `EmptyState` (shape, one sentence, the one action that fills it).
56
+ Waits under ~5 s show `M3LoadingIndicator`; known progress shows the wavy indicators.
57
+
58
+ ## Where things live
59
+
60
+ | Need | Use |
61
+ | ----------------------------- | ---------------------------------------------------------------- |
62
+ | A screen | `AppPage` + modules/`<feature>`/views |
63
+ | Section label | `SectionHeader` |
64
+ | Theme state | `useThemeSettings()` - seed, variant, contrast, mode, motion |
65
+ | Shell navigation | `useActiveTab()` (`show`, `open(tab, path)`), `useWindowClass()` |
66
+ | Hide the bar on a pushed page | `AppPage back` does it; elsewhere `useHiddenNavigation()` |
67
+ | Decorative tone for a shape | `TONE_CLASSES` from `shared/utils/tone` |
68
+ | Live component examples | the Gallery tab - `modules/gallery/components/sections` |
@@ -0,0 +1,44 @@
1
+ # Colour
2
+
3
+ One seed (`BRAND_SEED` in `src/app/theme.config.ts`) generates every role in both modes through
4
+ material-color-utilities' 2025 (Expressive) spec. The user can change seed, variant and contrast in
5
+ the colour studio; `useThemeSettings()` persists it and repaints in one frame. Dark mode is a `.dark`
6
+ class on `<html>` - both modes are always in the stylesheet, so switching regenerates nothing.
7
+
8
+ ## Roles to reach for
9
+
10
+ | Purpose | Role |
11
+ | ----------------------------------------------------------- | -------------------------------------------------------------------- |
12
+ | Page background | `surface` |
13
+ | Grouped content, list segments, cards on a page | `surface-container-low` |
14
+ | Raised panels, the nav bar, bars over scrolled content | `surface-container` / `-high` |
15
+ | Text fields, inactive tracks | `surface-container-highest` |
16
+ | Main emphasis (FAB, hero, selected nav) | `primary-container` + `on-primary-container` |
17
+ | Strong action fill | `primary` + `on-primary` |
18
+ | Selected or active but quiet (chips, list rows, indicators) | `secondary-container` |
19
+ | Contrasting accent, decorative variety | `tertiary-container` |
20
+ | Snackbar, tooltips | `inverse-surface` + `inverse-on-surface`, action `inverse-primary` |
21
+ | Errors and destructive | `error`, `error-container` |
22
+ | Success, warning | custom groups `success-*`, `warning-*` (theme.config `EXTRA_COLORS`) |
23
+ | Hairlines | `outline-variant`; field borders `outline` |
24
+ | Secondary text, icons | `on-surface-variant` |
25
+
26
+ Disabled content is `on-surface` at 38%, disabled containers `on-surface` at 10-12% - the components
27
+ already do this.
28
+
29
+ ## Variants and the spec trap
30
+
31
+ Material applies the 2025 rules only to **tonalSpot, expressive, vibrant, neutral**. `brand`
32
+ (seed exact as `primary-container`, Theme Builder's "Match colour"), fidelity, content, monochrome,
33
+ rainbow and fruitSalad silently use 2021 rules - `effectiveSpec(variant)` says which. The studio shows
34
+ it to the user.
35
+
36
+ ## Rules
37
+
38
+ - A new semantic colour is a custom group in `EXTRA_COLORS` (harmonised to the seed), then a role
39
+ name in `assets/css/theme/tailwind.css` - never a hex in a component.
40
+ - Contrast is a user setting (standard / medium / high): never compensate a weak pair by hand-picking
41
+ a darker colour; fix the role choice.
42
+ - Status needs more than colour: pair it with a shape or glyph (see `STATUS_LOOK` in the demo module).
43
+
44
+ See [sources.md](sources.md) → Colour.