@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
package/README.md CHANGED
@@ -29,7 +29,7 @@ pnpm create @cavulsqa --name caputa --app-id com.example.caputa --yes
29
29
  | flag | default | |
30
30
  | ------------ | ---------------- | ------------------------------------------------------- |
31
31
  | `--name` | asked | package and directory name |
32
- | `--template` | the only one | which bundled template |
32
+ | `--template` | `f7-app` | `f7-app` or `m3e-app`; asked from a numbered menu |
33
33
  | `--dir` | `./<name>` | where to write it |
34
34
  | `--app-name` | `Name` | launcher name, window title, Settings screen |
35
35
  | `--app-id` | `com.ayb.<name>` | Android application id |
@@ -40,6 +40,13 @@ pnpm create @cavulsqa --name caputa --app-id com.example.caputa --yes
40
40
 
41
41
  ## What you get
42
42
 
43
+ Two templates, one data layer:
44
+
45
+ | Template | Look and feel |
46
+ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
47
+ | `f7-app` | Framework7's own components, native on iOS and on Material (`theme: auto`) |
48
+ | `m3e-app` | Material 3 Expressive: `@cavulsqa/m3e-vue` components on 2025 dynamic colour, Google Sans Flex, spring motion, a colour studio and a component gallery. Framework7 stays underneath as the navigation engine only, so it is Android-first by design |
49
+
43
50
  The `f7-app` template: a tabbed shell, a `domains` / `modules` / `shared` layout, and a working
44
51
  sales demo over a six-table schema — dashboard aggregates, search, an order sheet, swipe actions,
45
52
  a detail screen — with tests against real SQLite.
@@ -49,6 +56,10 @@ under Kysely, `@cavulsqa/reactive-db` for the change bus, and `@cavulsqa/reactiv
49
56
  `useReactiveQuery`. A write announces the tables it touched and every query watching them refetches;
50
57
  nothing in a screen asks for a refresh.
51
58
 
59
+ `m3e-app` has the same layout and data layer, with every screen built from `M3*` components and an
60
+ `m3-expressive` skill that carries the design system: which component, which colour role, which
61
+ spring, and a link to the spec page for each.
62
+
52
63
  `CLAUDE.md`, `.claude/rules/` and `.claude/skills/` come with it, so an agent opening the generated
53
64
  repository knows the architecture, the conventions, and the traps that have already cost someone a
54
65
  day.
package/bin/create.mjs CHANGED
@@ -7,11 +7,17 @@
7
7
  * silent default, because a generated app named "my-app" in the wrong directory is worse than a
8
8
  * failed command.
9
9
  */
10
- import { existsSync, readdirSync, readFileSync } from "node:fs";
10
+ import { readFileSync } from "node:fs";
11
11
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
12
12
  import { createInterface } from "node:readline/promises";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { scaffold } from "../lib/scaffold.mjs";
15
+ import {
16
+ defaultTemplate,
17
+ listTemplates,
18
+ resolveTemplateChoice,
19
+ templateMenu,
20
+ } from "../lib/templates.mjs";
15
21
 
16
22
  const PACKAGE = dirname(dirname(fileURLToPath(import.meta.url)));
17
23
  const BUNDLED = join(PACKAGE, "templates");
@@ -38,7 +44,7 @@ function usage() {
38
44
 
39
45
  Options:
40
46
  --name NAME package and directory name
41
- --template NAME which template (default: the only one, or you are asked)
47
+ --template NAME which template (default: the manifest's default, or you are asked)
42
48
  --dir PATH where to write it (default: ./<name>)
43
49
  --app-name NAME launcher name and window title (default: Name)
44
50
  --app-id ID android application id (default: com.ayb.<name>)
@@ -49,24 +55,7 @@ Options:
49
55
  --help this
50
56
 
51
57
  Templates bundled in this package:
52
- ${listTemplates()
53
- .map((entry) => ` ${entry.name} ${entry.description}`)
54
- .join("\n")}`);
55
- }
56
-
57
- function listTemplates() {
58
- const manifest = JSON.parse(readFileSync(join(PACKAGE, "package.json"), "utf8"));
59
- const declared = manifest.createConfig?.templates ?? [];
60
- if (!existsSync(BUNDLED)) return declared;
61
-
62
- // The manifest is what `vp create` reads; the directory is what actually shipped. Trust the
63
- // directory, and let the manifest supply the descriptions.
64
- return readdirSync(BUNDLED, { withFileTypes: true })
65
- .filter((entry) => entry.isDirectory())
66
- .map((entry) => ({
67
- name: entry.name,
68
- description: declared.find((listed) => listed.name === entry.name)?.description ?? "",
69
- }));
58
+ ${templateMenu(listTemplates(PACKAGE))}`);
70
59
  }
71
60
 
72
61
  /**
@@ -109,15 +98,19 @@ async function main() {
109
98
  throw new Error(`"${String(name)}" is not a usable package name`);
110
99
  }
111
100
 
112
- const templates = listTemplates();
101
+ const templates = listTemplates(PACKAGE);
113
102
  if (!templates.length) throw new Error("this build of @cavulsqa/create bundles no templates");
114
103
 
115
104
  let template = flags.get("template");
116
105
  if (typeof template !== "string") {
117
- template =
118
- templates.length === 1
119
- ? templates[0].name
120
- : await ask(`Template [${templates.map((t) => t.name).join(", ")}]`, templates[0].name);
106
+ const fallback = defaultTemplate(templates);
107
+ if (templates.length === 1 || !rl) {
108
+ template = fallback.name;
109
+ } else {
110
+ console.log(`\nTemplates:\n${templateMenu(templates)}\n`);
111
+ const answer = await ask("Template", String(templates.indexOf(fallback) + 1));
112
+ template = resolveTemplateChoice(answer, templates) ?? answer;
113
+ }
121
114
  }
122
115
  if (!templates.some((entry) => entry.name === template)) {
123
116
  throw new Error(
@@ -0,0 +1,56 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ /**
5
+ * The templates this build offers, in the manifest's order, with the manifest's descriptions.
6
+ *
7
+ * The manifest is what `vp create` reads; the directory is what actually shipped. A template is
8
+ * offered only when both agree, and a shipped directory the manifest forgot still appears - last,
9
+ * without a description - rather than silently vanishing.
10
+ */
11
+ export function listTemplates(packageDir) {
12
+ const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
13
+ const declared = manifest.createConfig?.templates ?? [];
14
+ const bundled = join(packageDir, "templates");
15
+ if (!existsSync(bundled)) return declared.map(toEntry);
16
+
17
+ const shipped = new Set(
18
+ readdirSync(bundled, { withFileTypes: true })
19
+ .filter((entry) => entry.isDirectory())
20
+ .map((entry) => entry.name),
21
+ );
22
+ const listed = declared.filter((entry) => shipped.has(entry.name)).map(toEntry);
23
+ const unlisted = [...shipped]
24
+ .filter((name) => !declared.some((entry) => entry.name === name))
25
+ .sort((a, b) => a.localeCompare(b))
26
+ .map((name) => toEntry({ name }));
27
+ return [...listed, ...unlisted];
28
+ }
29
+
30
+ function toEntry(entry) {
31
+ return {
32
+ name: entry.name,
33
+ description: entry.description ?? "",
34
+ isDefault: entry.default === true,
35
+ };
36
+ }
37
+
38
+ /** The one marked `default` in the manifest, else the first - never whatever readdir returns first. */
39
+ export function defaultTemplate(templates) {
40
+ return templates.find((entry) => entry.isDefault) ?? templates[0];
41
+ }
42
+
43
+ /** A typed answer as a template: its number in the list, or its name. Null when it is neither. */
44
+ export function resolveTemplateChoice(answer, templates) {
45
+ const value = String(answer).trim();
46
+ if (/^\d+$/.test(value)) return templates[Number(value) - 1]?.name ?? null;
47
+ return templates.some((entry) => entry.name === value) ? value : null;
48
+ }
49
+
50
+ /** The numbered menu the interactive prompt prints. */
51
+ export function templateMenu(templates) {
52
+ const width = Math.max(...templates.map((entry) => entry.name.length));
53
+ return templates
54
+ .map((entry, index) => ` ${index + 1}) ${entry.name.padEnd(width)} ${entry.description}`)
55
+ .join("\n");
56
+ }
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@cavulsqa/create",
3
- "version": "2.9.2",
4
- "description": "Create a Vue + Framework7 + Capacitor + SQLite app from the cavulsqa templates.",
3
+ "version": "2.10.0",
4
+ "description": "Create a Vue + Capacitor + SQLite app from the cavulsqa templates: Framework7 (iOS and Material) or Material 3 Expressive.",
5
5
  "keywords": [
6
6
  "capacitor",
7
7
  "create",
8
8
  "framework7",
9
+ "material-3",
10
+ "material-expressive",
9
11
  "scaffold",
10
12
  "sqlite",
11
13
  "template",
@@ -47,12 +49,18 @@
47
49
  "templates": [
48
50
  {
49
51
  "name": "f7-app",
50
- "description": "Vue 3 + Framework7 + Capacitor + SQLite, tabbed shell, offline-first data layer",
51
- "template": "./templates/f7-app"
52
+ "description": "Vue 3 + Framework7 + Capacitor + SQLite, tabbed shell, iOS and Material themes, offline-first data layer",
53
+ "template": "./templates/f7-app",
54
+ "default": true
55
+ },
56
+ {
57
+ "name": "m3e-app",
58
+ "description": "Material 3 Expressive: m3e-vue components, 2025 dynamic colour with a colour studio, adaptive bar/rail shell, offline-first data layer",
59
+ "template": "./templates/m3e-app"
52
60
  }
53
61
  ]
54
62
  },
55
- "templatesFingerprint": "sha256-w5ItqHFEFJCJwe7V3W0FEhJf7foJkanjYLY85j/Wbd8=",
63
+ "templatesFingerprint": "sha256-3YSsClBPuShazZVtnS9LabHQ330E0wPZiedhN6vIadU=",
56
64
  "scripts": {
57
65
  "build": "node scripts/bundleTemplates.mjs",
58
66
  "check": "vp check",
@@ -71,6 +71,9 @@ The short version of the rules, each earned by a real bug:
71
71
  kysely reports `no result`. See `.claude/rules/database.md`.
72
72
  - **Money is integer cents.** A float total is a rounding bug waiting to happen.
73
73
  - **`f7route` and `f7router` are props**, not imports: `defineProps<{ f7route: Router.Route }>()`.
74
+ - **A route loads its component through `lazyRoute`**, never a bare `import().then(resolve)`.
75
+ Framework7 locks the router before calling the hook and only unlocks it from `resolve` or
76
+ `reject`; a hook that settles neither — one failed chunk — freezes that view for the session.
74
77
  - **In locale files, `@` and `|` are message syntax.** `@` starts a linked message and `|` a plural
75
78
  branch. A literal `@` must be `{'@'}` or the whole locale file fails to compile.
76
79
  - **No comments that narrate.** A comment explains a non-obvious _why_ — a platform quirk, an
@@ -58,12 +58,14 @@ declare global {
58
58
  const initCapacitor: typeof import('./src/plugins/capacitor/index').initCapacitor
59
59
  const inject: typeof import('vue').inject
60
60
  const injectLocal: typeof import('@vueuse/core').injectLocal
61
+ const installNavigationGuard: typeof import('./src/shared/composables/useNavigationGuard').installNavigationGuard
61
62
  const isDefined: typeof import('@vueuse/core').isDefined
62
63
  const isProxy: typeof import('vue').isProxy
63
64
  const isReactive: typeof import('vue').isReactive
64
65
  const isReadonly: typeof import('vue').isReadonly
65
66
  const isRef: typeof import('vue').isRef
66
67
  const isShallow: typeof import('vue').isShallow
68
+ const lazyRoute: typeof import('./src/shared/utils/lazyRoute').lazyRoute
67
69
  const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
68
70
  const markRaw: typeof import('vue').markRaw
69
71
  const nextTick: typeof import('vue').nextTick
@@ -96,6 +98,8 @@ declare global {
96
98
  const reactiveOmit: typeof import('@vueuse/core').reactiveOmit
97
99
  const reactivePick: typeof import('@vueuse/core').reactivePick
98
100
  const readonly: typeof import('vue').readonly
101
+ const recorderDiagnostics: typeof import('./src/plugins/recorder.plugin').recorderDiagnostics
102
+ const recorderPlugin: typeof import('./src/plugins/recorder.plugin').recorderPlugin
99
103
  const ref: typeof import('vue').ref
100
104
  const refAutoReset: typeof import('@vueuse/core').refAutoReset
101
105
  const refDebounced: typeof import('@vueuse/core').refDebounced
@@ -104,6 +108,7 @@ declare global {
104
108
  const refThrottled: typeof import('@vueuse/core').refThrottled
105
109
  const refWithControl: typeof import('@vueuse/core').refWithControl
106
110
  const renderBootstrapError: typeof import('./src/plugins/bootstrapError').renderBootstrapError
111
+ const reportProblem: typeof import('./src/plugins/recorder.plugin').reportProblem
107
112
  const request: typeof import('framework7/lite').request
108
113
  const resolveComponent: typeof import('vue').resolveComponent
109
114
  const seedPlugin: typeof import('./src/plugins/seed.plugin').seedPlugin
@@ -230,6 +235,7 @@ declare global {
230
235
  const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
231
236
  const useMousePressed: typeof import('@vueuse/core').useMousePressed
232
237
  const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
238
+ const useNavigationGuard: typeof import('./src/shared/composables/useNavigationGuard').useNavigationGuard
233
239
  const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
234
240
  const useNetwork: typeof import('@vueuse/core').useNetwork
235
241
  const useNow: typeof import('@vueuse/core').useNow
@@ -338,6 +344,9 @@ declare global {
338
344
  export type { TabbarVisibility } from './src/shared/composables/useTabbarVisibility'
339
345
  import('./src/shared/composables/useTabbarVisibility')
340
346
  // @ts-ignore
347
+ export type { RecorderBackend, RecorderDiagnostics } from './src/plugins/recorder.plugin'
348
+ import('./src/plugins/recorder.plugin')
349
+ // @ts-ignore
341
350
  export type { CaseComparison } from './src/modules/demo/composables/useBenchmark'
342
351
  import('./src/modules/demo/composables/useBenchmark')
343
352
  // @ts-ignore
@@ -403,12 +412,14 @@ declare module 'vue' {
403
412
  readonly initCapacitor: UnwrapRef<typeof import('./src/plugins/capacitor/index')['initCapacitor']>
404
413
  readonly inject: UnwrapRef<typeof import('vue')['inject']>
405
414
  readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
415
+ readonly installNavigationGuard: UnwrapRef<typeof import('./src/shared/composables/useNavigationGuard')['installNavigationGuard']>
406
416
  readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
407
417
  readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
408
418
  readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
409
419
  readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
410
420
  readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
411
421
  readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
422
+ readonly lazyRoute: UnwrapRef<typeof import('./src/shared/utils/lazyRoute')['lazyRoute']>
412
423
  readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
413
424
  readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
414
425
  readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
@@ -441,6 +452,8 @@ declare module 'vue' {
441
452
  readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
442
453
  readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
443
454
  readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
455
+ readonly recorderDiagnostics: UnwrapRef<typeof import('./src/plugins/recorder.plugin')['recorderDiagnostics']>
456
+ readonly recorderPlugin: UnwrapRef<typeof import('./src/plugins/recorder.plugin')['recorderPlugin']>
444
457
  readonly ref: UnwrapRef<typeof import('vue')['ref']>
445
458
  readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
446
459
  readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
@@ -449,6 +462,7 @@ declare module 'vue' {
449
462
  readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
450
463
  readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
451
464
  readonly renderBootstrapError: UnwrapRef<typeof import('./src/plugins/bootstrapError')['renderBootstrapError']>
465
+ readonly reportProblem: UnwrapRef<typeof import('./src/plugins/recorder.plugin')['reportProblem']>
452
466
  readonly request: UnwrapRef<typeof import('framework7/lite')['request']>
453
467
  readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
454
468
  readonly seedPlugin: UnwrapRef<typeof import('./src/plugins/seed.plugin')['seedPlugin']>
@@ -575,6 +589,7 @@ declare module 'vue' {
575
589
  readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
576
590
  readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
577
591
  readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
592
+ readonly useNavigationGuard: UnwrapRef<typeof import('./src/shared/composables/useNavigationGuard')['useNavigationGuard']>
578
593
  readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
579
594
  readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
580
595
  readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
@@ -16,13 +16,16 @@
16
16
  "@capacitor/android": "^8.5.0",
17
17
  "@capacitor/app": "^8.0.0",
18
18
  "@capacitor/core": "^8.0.0",
19
+ "@capacitor/filesystem": "^8.0.0",
19
20
  "@capacitor/keyboard": "^8.0.0",
20
21
  "@capacitor/preferences": "^8.0.0",
22
+ "@capacitor/share": "^8.0.0",
21
23
  "@capacitor/splash-screen": "^8.0.0",
22
24
  "@capacitor/status-bar": "^8.0.0",
23
- "@cavulsqa/mobile-db": "^1.2.0",
24
- "@cavulsqa/reactive-db": "^1.2.0",
25
- "@cavulsqa/reactive-vue": "^1.2.0",
25
+ "@cavulsqa/mobile-db": "^1.4.0",
26
+ "@cavulsqa/reactive-db": "^1.4.0",
27
+ "@cavulsqa/reactive-vue": "^1.4.0",
28
+ "@cavulsqa/recorder": "^1.4.0",
26
29
  "@sqlite.org/sqlite-wasm": "3.53.0-build1",
27
30
  "@tailwindcss/vite": "^4.3.3",
28
31
  "@vueuse/core": "^14.4.0",
@@ -57,6 +57,7 @@ const parameters = framework7Parameters(appTheme.value.theme, appTheme.value.dar
57
57
  onMounted(() => {
58
58
  // The native handlers need the Framework7 instance, which exists only once the shell is mounted.
59
59
  f7ready(async (instance) => {
60
+ useNavigationGuard();
60
61
  await initCapacitor(instance);
61
62
  hideSplashScreen();
62
63
  });
@@ -35,6 +35,7 @@ declare const __APP_VERSION__: string;
35
35
  interface ImportMetaEnv {
36
36
  readonly VITE_STORAGE_ENGINE?: string;
37
37
  readonly VITE_PRAGMA_PROFILE?: "safe" | "fast";
38
+ readonly VITE_FIELD_RECORDER?: "on" | "off";
38
39
  }
39
40
 
40
41
  interface ImportMeta {
@@ -9,6 +9,7 @@ import "./assets/css/app.css";
9
9
  import App from "./App.vue";
10
10
  import { i18n } from "./plugins/i18n.plugin";
11
11
  import { renderBootstrapError } from "./plugins/bootstrapError";
12
+ import { recorderPlugin } from "./plugins/recorder.plugin";
12
13
  import { seedPlugin } from "./plugins/seed.plugin";
13
14
  import { sqlitePlugin } from "./plugins/sqlite.plugin";
14
15
 
@@ -31,6 +32,8 @@ async function bootstrap(): Promise<void> {
31
32
  return;
32
33
  }
33
34
 
35
+ void recorderPlugin();
36
+
34
37
  // Components arrive through Framework7VueResolver, so nothing is registered by hand.
35
38
  const app = createApp(App);
36
39
  app.use(i18n);
@@ -1,33 +1,21 @@
1
1
  import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
2
3
 
3
4
  const demoRoutes: Router.RouteParameters[] = [
4
5
  {
5
6
  name: "demo",
6
7
  path: "/demo/",
7
- // `async` is Framework7's route hook, not an async function - resolve from the promise.
8
- async({ resolve }) {
9
- void import("@/modules/demo/views/DemoView.vue").then((view) => {
10
- resolve({ component: view.default });
11
- });
12
- },
8
+ async: lazyRoute(() => import("@/modules/demo/views/DemoView.vue")),
13
9
  },
14
10
  {
15
11
  name: "demo-search",
16
12
  path: "/demo/search/",
17
- async({ resolve }) {
18
- void import("@/modules/demo/views/OrderSearchView.vue").then((view) => {
19
- resolve({ component: view.default });
20
- });
21
- },
13
+ async: lazyRoute(() => import("@/modules/demo/views/OrderSearchView.vue")),
22
14
  },
23
15
  {
24
16
  name: "demo-order",
25
17
  path: "/demo/order/:id/",
26
- async({ resolve }) {
27
- void import("@/modules/demo/views/OrderDetailView.vue").then((view) => {
28
- resolve({ component: view.default });
29
- });
30
- },
18
+ async: lazyRoute(() => import("@/modules/demo/views/OrderDetailView.vue")),
31
19
  },
32
20
  ];
33
21
 
@@ -1,4 +1,5 @@
1
1
  import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
2
3
 
3
4
  /**
4
5
  * The detail route is a child of the tab's own view, which is what keeps the tab bar in place and
@@ -8,21 +9,12 @@ const homeRoutes: Router.RouteParameters[] = [
8
9
  {
9
10
  name: "home",
10
11
  path: "/home/",
11
- // `async` is Framework7's route hook, not an async function - resolve from the promise.
12
- async({ resolve }) {
13
- void import("@/modules/home/views/HomeView.vue").then((view) => {
14
- resolve({ component: view.default });
15
- });
16
- },
12
+ async: lazyRoute(() => import("@/modules/home/views/HomeView.vue")),
17
13
  },
18
14
  {
19
15
  name: "home-feature",
20
16
  path: "/home/feature/:id/",
21
- async({ resolve }) {
22
- void import("@/modules/home/views/FeatureDetailView.vue").then((view) => {
23
- resolve({ component: view.default });
24
- });
25
- },
17
+ async: lazyRoute(() => import("@/modules/home/views/FeatureDetailView.vue")),
26
18
  },
27
19
  ];
28
20
 
@@ -1,15 +1,11 @@
1
1
  import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
2
3
 
3
4
  const settingsRoutes: Router.RouteParameters[] = [
4
5
  {
5
6
  name: "settings",
6
7
  path: "/settings/",
7
- // `async` is Framework7's route hook, not an async function - resolve from the promise.
8
- async({ resolve }) {
9
- void import("@/modules/settings/views/SettingsView.vue").then((view) => {
10
- resolve({ component: view.default });
11
- });
12
- },
8
+ async: lazyRoute(() => import("@/modules/settings/views/SettingsView.vue")),
13
9
  },
14
10
  ];
15
11
 
@@ -3,6 +3,7 @@ import {
3
3
  webviewLikelyTooOld,
4
4
  webviewVersion,
5
5
  } from "@/shared/database/storage";
6
+ import { hideSplashScreen } from "@/plugins/capacitor/useSplashScreen";
6
7
 
7
8
  /**
8
9
  * The screen shown when the app cannot start, which in practice means the database did not open.
@@ -15,6 +16,7 @@ import {
15
16
  * nothing they could act on.
16
17
  */
17
18
  export function renderBootstrapError(error: unknown): void {
19
+ hideSplashScreen();
18
20
  const root = document.getElementById("app");
19
21
  if (!root) return;
20
22
 
@@ -0,0 +1,155 @@
1
+ import {
2
+ createConsoleTrack,
3
+ createDatabasesTrack,
4
+ createNetworkTrack,
5
+ createPerfTrack,
6
+ createRecorder,
7
+ createRrwebTrack,
8
+ type Recorder,
9
+ type TrackContext,
10
+ type TrackRecorder,
11
+ } from "@cavulsqa/recorder";
12
+ import { changeBus } from "@/shared/database/database";
13
+ import { createCapacitorSink } from "@/shared/recorder/capacitorSink";
14
+
15
+ const RRWEB_VERIFY_WINDOW_MS = 2000;
16
+
17
+ export type RecorderBackend = "memory" | "opfs" | null;
18
+
19
+ export interface RecorderDiagnostics {
20
+ enabled: boolean;
21
+ recording: boolean;
22
+ backend: RecorderBackend;
23
+ rrwebAvailable: boolean;
24
+ lastError: string | null;
25
+ }
26
+
27
+ export const recorderDiagnostics = ref<RecorderDiagnostics>({
28
+ enabled: false,
29
+ recording: false,
30
+ backend: null,
31
+ rrwebAvailable: true,
32
+ lastError: null,
33
+ });
34
+
35
+ let recorder: Recorder | null = null;
36
+
37
+ function resolveFieldRecorderMode(): "on" | "off" {
38
+ const raw = import.meta.env.VITE_FIELD_RECORDER;
39
+ if (raw === "on" || raw === "off") return raw;
40
+ return import.meta.env.DEV ? "on" : "off";
41
+ }
42
+
43
+ function isCapuError(data: unknown): boolean {
44
+ return typeof data === "object" && data !== null && "__capuError" in data;
45
+ }
46
+
47
+ /**
48
+ * Wraps `createRrwebTrack` with a runtime check: rrweb's published package resolves to an empty
49
+ * module outside a bundler, and a `record()` call against that module fails silently inside the
50
+ * track's own try/catch, so the recorder would otherwise report a healthy `rrweb` track that never
51
+ * emitted a frame. This watches the first events pushed and flips `rrwebAvailable` to `false` if
52
+ * nothing but a `__capuError` payload arrives within {@link RRWEB_VERIFY_WINDOW_MS}.
53
+ */
54
+ function createVerifiedRrwebTrack(): TrackRecorder {
55
+ const inner = createRrwebTrack();
56
+ let sawRealFrame = false;
57
+
58
+ return {
59
+ name: "rrweb",
60
+ async start(ctx: TrackContext<unknown>) {
61
+ const wrapped: TrackContext<unknown> = {
62
+ ...ctx,
63
+ push(data, wallMs) {
64
+ if (!sawRealFrame && !isCapuError(data)) sawRealFrame = true;
65
+ ctx.push(data, wallMs);
66
+ },
67
+ };
68
+ await inner.start(wrapped);
69
+ setTimeout(() => {
70
+ recorderDiagnostics.value = { ...recorderDiagnostics.value, rrwebAvailable: sawRealFrame };
71
+ if (!sawRealFrame) {
72
+ console.error(
73
+ "[recorder] rrweb produced no frames; recording is running without screen replay.",
74
+ );
75
+ }
76
+ }, RRWEB_VERIFY_WINDOW_MS);
77
+ },
78
+ async stop() {
79
+ await inner.stop();
80
+ },
81
+ };
82
+ }
83
+
84
+ function buildTracks(): TrackRecorder[] {
85
+ return [
86
+ createVerifiedRrwebTrack(),
87
+ createConsoleTrack(),
88
+ createNetworkTrack(),
89
+ createPerfTrack(),
90
+ createDatabasesTrack({ changeBus }),
91
+ ];
92
+ }
93
+
94
+ function syncDiagnostics(): void {
95
+ if (!recorder) return;
96
+ const status = recorder.status;
97
+ recorderDiagnostics.value = {
98
+ ...recorderDiagnostics.value,
99
+ recording: status.recording,
100
+ backend: status.backend,
101
+ lastError: status.lastError,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Builds and starts the field recorder, gated by `VITE_FIELD_RECORDER` (`"on"` or `"off"`,
107
+ * defaulting to `on` in dev and `off` in a production build). Recovers a leftover session from a
108
+ * previous run before starting a new one. Never throws: every failure lands in
109
+ * {@link recorderDiagnostics}.`lastError` instead, so a broken recorder can never block `main.ts`
110
+ * from mounting the app.
111
+ */
112
+ export async function recorderPlugin(): Promise<void> {
113
+ const mode = resolveFieldRecorderMode();
114
+ recorderDiagnostics.value = { ...recorderDiagnostics.value, enabled: mode === "on" };
115
+ if (mode === "off") return;
116
+
117
+ try {
118
+ recorder = createRecorder({
119
+ tracks: buildTracks(),
120
+ sink: createCapacitorSink(),
121
+ label: "field recording",
122
+ appId: __APP_NAME__,
123
+ appVersion: __APP_VERSION__,
124
+ });
125
+
126
+ await recorder.recover();
127
+ await recorder.start();
128
+ syncDiagnostics();
129
+ } catch (error) {
130
+ recorderDiagnostics.value = {
131
+ ...recorderDiagnostics.value,
132
+ lastError: error instanceof Error ? error.message : String(error),
133
+ };
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Finalizes the current buffer into a `.capu` and hands it to the sink without stopping the
139
+ * recorder, for the Settings "Report a problem" action. Returns `true` when the capture reached
140
+ * the sink with no error.
141
+ */
142
+ export async function reportProblem(): Promise<boolean> {
143
+ if (!recorder) return false;
144
+ try {
145
+ await recorder.capture("user report");
146
+ } catch (error) {
147
+ recorderDiagnostics.value = {
148
+ ...recorderDiagnostics.value,
149
+ lastError: error instanceof Error ? error.message : String(error),
150
+ };
151
+ return false;
152
+ }
153
+ syncDiagnostics();
154
+ return recorderDiagnostics.value.lastError === null;
155
+ }
@@ -1,15 +1,11 @@
1
1
  import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
2
3
 
3
4
  const globalRoutes: Router.RouteParameters[] = [
4
5
  {
5
6
  name: "not-found",
6
7
  path: "(.*)",
7
- // `async` is Framework7's route hook, not an async function - resolve from the promise.
8
- async({ resolve }) {
9
- void import("@/shared/components/error/404.vue").then((view) => {
10
- resolve({ component: view.default });
11
- });
12
- },
8
+ async: lazyRoute(() => import("@/shared/components/error/404.vue")),
13
9
  },
14
10
  ];
15
11