@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,71 @@
1
+ <template>
2
+ <div class="grid grid-cols-2 gap-2 px-4">
3
+ <div
4
+ v-for="tile in tiles"
5
+ :key="tile.labelKey"
6
+ class="flex flex-col gap-2 rounded-lg p-4"
7
+ :class="tile.surface"
8
+ >
9
+ <div class="flex items-center gap-2">
10
+ <M3Shape :shape="tile.shape" class="size-8" :class="tile.badge">
11
+ <component :is="tile.icon" class="size-4" />
12
+ </M3Shape>
13
+ <span class="type-label-large opacity-80">{{ t(tile.labelKey) }}</span>
14
+ </div>
15
+ <span class="type-headline-small-emphasized tabular-nums">{{ tile.value }}</span>
16
+ <span v-if="tile.hint" class="type-body-small opacity-70">{{ tile.hint }}</span>
17
+ </div>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import type { DashboardStats } from "@/domains/sales/sales.repository";
23
+ import { STATUS_LOOK, formatMoney } from "@/modules/demo/composables/useOrderStatus";
24
+ import PaymentsIcon from "~icons/material-symbols/monitoring-rounded";
25
+
26
+ const props = defineProps<{ stats: DashboardStats }>();
27
+ const { t } = useI18n();
28
+
29
+ /**
30
+ * Status counts rather than plain totals, because they are what moves when you use the screen:
31
+ * confirm one order and three of these four change at once, without anything telling them to.
32
+ */
33
+ const tiles = computed(() => [
34
+ {
35
+ icon: STATUS_LOOK.draft.icon,
36
+ shape: STATUS_LOOK.draft.shape,
37
+ labelKey: "demo.tiles.draft",
38
+ surface: "bg-surface-container-low text-on-surface",
39
+ badge: STATUS_LOOK.draft.classes,
40
+ value: String(props.stats.draft),
41
+ hint: t("demo.tiles.ofOrders", { count: props.stats.orders }, props.stats.orders),
42
+ },
43
+ {
44
+ icon: STATUS_LOOK.confirmed.icon,
45
+ shape: STATUS_LOOK.confirmed.shape,
46
+ labelKey: "demo.tiles.confirmed",
47
+ surface: "bg-surface-container-low text-on-surface",
48
+ badge: STATUS_LOOK.confirmed.classes,
49
+ value: String(props.stats.confirmed),
50
+ hint: "",
51
+ },
52
+ {
53
+ icon: STATUS_LOOK.delivered.icon,
54
+ shape: STATUS_LOOK.delivered.shape,
55
+ labelKey: "demo.tiles.delivered",
56
+ surface: "bg-surface-container-low text-on-surface",
57
+ badge: STATUS_LOOK.delivered.classes,
58
+ value: String(props.stats.delivered),
59
+ hint: "",
60
+ },
61
+ {
62
+ labelKey: "demo.tiles.committed",
63
+ icon: PaymentsIcon,
64
+ shape: "sunny" as const,
65
+ surface: "bg-primary-container text-on-primary-container",
66
+ badge: "bg-primary text-on-primary",
67
+ value: formatMoney(props.stats.committedCents),
68
+ hint: t("demo.tiles.ofTotal", { total: formatMoney(props.stats.revenueCents) }),
69
+ },
70
+ ]);
71
+ </script>
@@ -0,0 +1,139 @@
1
+ import {
2
+ runBenchmark,
3
+ type CaseResult,
4
+ type SuiteResult,
5
+ } from "@/domains/benchmark/benchmark.suite";
6
+ import { activePragmas, activeStorage, activeStorageLabel, rdb } from "@/shared/database/database";
7
+ import type { StorageId } from "@/shared/database/candidates";
8
+
9
+ const STORAGE_KEY = "app.benchmark.results";
10
+
11
+ export interface CaseComparison {
12
+ name: string;
13
+ group: CaseResult["group"];
14
+ note?: string;
15
+ current: CaseResult;
16
+ other?: CaseResult;
17
+ /** How many times faster the current engine is at this case. Below 1 means slower. */
18
+ speedup?: number;
19
+ }
20
+
21
+ /**
22
+ * Results are kept per engine in localStorage, because the comparison is the point and an engine
23
+ * switch needs a restart - so the two halves can never be in memory at the same time.
24
+ */
25
+ function readStored(): Partial<Record<StorageId, SuiteResult>> {
26
+ try {
27
+ const raw = localStorage.getItem(STORAGE_KEY);
28
+ return raw ? (JSON.parse(raw) as Partial<Record<StorageId, SuiteResult>>) : {};
29
+ } catch {
30
+ return {};
31
+ }
32
+ }
33
+
34
+ function writeStored(all: Partial<Record<StorageId, SuiteResult>>): void {
35
+ try {
36
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
37
+ } catch {
38
+ // Out of quota or storage disabled: the run still displayed, it just will not be remembered.
39
+ }
40
+ }
41
+
42
+ export function useBenchmark() {
43
+ const engine = activeStorage().id;
44
+ const stored = ref(readStored());
45
+ const running = ref(false);
46
+ const progress = ref("");
47
+ const failure = ref<string | null>(null);
48
+
49
+ const result = computed<SuiteResult | null>(() => stored.value[engine] ?? null);
50
+
51
+ /** The most recent run on any other engine, which is what the current one is measured against. */
52
+ const baseline = computed<SuiteResult | null>(() => {
53
+ const others = Object.entries(stored.value)
54
+ .filter(([key]) => key !== engine)
55
+ .map(([, value]) => value)
56
+ .filter((value): value is SuiteResult => Boolean(value));
57
+ return others.sort((a, b) => b.at - a.at)[0] ?? null;
58
+ });
59
+
60
+ const comparison = computed<CaseComparison[]>(() => {
61
+ const current = result.value;
62
+ if (!current) return [];
63
+
64
+ return current.cases.map((entry) => {
65
+ const other = baseline.value?.cases.find((candidate) => candidate.name === entry.name);
66
+ const speedup =
67
+ other && entry.msPerOperation > 0
68
+ ? Number((other.msPerOperation / entry.msPerOperation).toFixed(2))
69
+ : undefined;
70
+ return {
71
+ name: entry.name,
72
+ group: entry.group,
73
+ note: entry.note,
74
+ current: entry,
75
+ other,
76
+ speedup,
77
+ };
78
+ });
79
+ });
80
+
81
+ async function run(): Promise<void> {
82
+ if (running.value) return;
83
+ running.value = true;
84
+ failure.value = null;
85
+ progress.value = "";
86
+
87
+ try {
88
+ /**
89
+ * Reads take the raw Kysely instance and writes go through `rdb`, exactly as the app does -
90
+ * measuring a write that skipped the change bus would measure something the app never runs.
91
+ * The suite's own tables are not watched by any query, so nothing refetches while it runs.
92
+ */
93
+ const finished = await runBenchmark(rdb, (name) => {
94
+ progress.value = name;
95
+ });
96
+
97
+ const next = {
98
+ ...stored.value,
99
+ [engine]: {
100
+ ...finished,
101
+ engine: activeStorageLabel(),
102
+ pragmas: activePragmas(),
103
+ at: Date.now(),
104
+ },
105
+ };
106
+ stored.value = next;
107
+ writeStored(next);
108
+ } catch (error) {
109
+ failure.value = error instanceof Error ? error.message : String(error);
110
+ } finally {
111
+ running.value = false;
112
+ progress.value = "";
113
+ }
114
+ }
115
+
116
+ function clear(): void {
117
+ stored.value = {};
118
+ writeStored({});
119
+ }
120
+
121
+ /** For pasting a run into an issue or a commit message rather than retyping numbers. */
122
+ function asJson(): string {
123
+ return JSON.stringify(stored.value, null, 2);
124
+ }
125
+
126
+ return {
127
+ engine,
128
+ engineName: activeStorageLabel(),
129
+ running,
130
+ progress,
131
+ failure,
132
+ result,
133
+ baseline,
134
+ comparison,
135
+ run,
136
+ clear,
137
+ asJson,
138
+ };
139
+ }
@@ -0,0 +1,64 @@
1
+ import type { MaterialShapeName } from "@cavulsqa/m3e";
2
+ import type { Component } from "vue";
3
+ import DraftIcon from "~icons/material-symbols/draft-outline-rounded";
4
+ import ShippingIcon from "~icons/material-symbols/local-shipping-outline-rounded";
5
+ import VerifiedIcon from "~icons/material-symbols/verified-outline-rounded";
6
+
7
+ export const ORDER_STATUSES = ["draft", "confirmed", "delivered"] as const;
8
+ export type OrderStatus = (typeof ORDER_STATUSES)[number];
9
+
10
+ export interface StatusLook {
11
+ labelKey: string;
12
+ icon: Component;
13
+ shape: MaterialShapeName;
14
+ /** Container and content roles; delivered uses the harmonised `success` custom colour. */
15
+ classes: string;
16
+ /** The container role as a text colour, for an SVG shape that fills with `currentColor`. */
17
+ fill: string;
18
+ /** The content role on that container. */
19
+ ink: string;
20
+ }
21
+
22
+ /**
23
+ * How a status reads at a glance: a role pair, a glyph and a shape, so three statuses stay apart for
24
+ * someone who cannot tell the colours apart - the shape and glyph carry it alone.
25
+ */
26
+ export const STATUS_LOOK: Readonly<Record<OrderStatus, StatusLook>> = {
27
+ draft: {
28
+ labelKey: "demo.statusDraft",
29
+ icon: DraftIcon,
30
+ shape: "square",
31
+ classes: "bg-surface-container-highest text-on-surface-variant",
32
+ fill: "text-surface-container-highest",
33
+ ink: "text-on-surface-variant",
34
+ },
35
+ confirmed: {
36
+ labelKey: "demo.statusConfirmed",
37
+ icon: ShippingIcon,
38
+ shape: "cookie4Sided",
39
+ classes: "bg-tertiary-container text-on-tertiary-container",
40
+ fill: "text-tertiary-container",
41
+ ink: "text-on-tertiary-container",
42
+ },
43
+ delivered: {
44
+ labelKey: "demo.statusDelivered",
45
+ icon: VerifiedIcon,
46
+ shape: "cookie9Sided",
47
+ classes: "bg-success-container text-on-success-container",
48
+ fill: "text-success-container",
49
+ ink: "text-on-success-container",
50
+ },
51
+ };
52
+
53
+ export function statusLook(status: string): StatusLook {
54
+ return STATUS_LOOK[
55
+ (ORDER_STATUSES as readonly string[]).includes(status) ? (status as OrderStatus) : "draft"
56
+ ];
57
+ }
58
+
59
+ const formatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 });
60
+
61
+ /** Money is integer cents everywhere; this is the only place it becomes a display string. */
62
+ export function formatMoney(cents: number): string {
63
+ return formatter.format(cents / 100);
64
+ }
@@ -0,0 +1,171 @@
1
+ import { Capacitor } from "@capacitor/core";
2
+ import type { TableChangeEvent } from "@cavulsqa/reactive-db";
3
+ import {
4
+ advanceOrderStatus,
5
+ clearAll,
6
+ deleteOrder,
7
+ ensureReferenceData,
8
+ loadDashboardStats,
9
+ saveOrder,
10
+ searchOrders,
11
+ seedSampleData,
12
+ setOrderStatus,
13
+ type DashboardStats,
14
+ type DraftLine,
15
+ type OrderRow,
16
+ } from "@/domains/sales/sales.repository";
17
+ import { activeStorageLabel, changeBus, getDatabase, rdb } from "@/shared/database/database";
18
+ import { useReactiveQuery } from "@/shared/database/queries";
19
+
20
+ export interface BusEntry {
21
+ /** Monotonic, because a millisecond timestamp is not unique - several events share one. */
22
+ id: number;
23
+ at: number;
24
+ table: string;
25
+ type: string;
26
+ }
27
+
28
+ export interface PipelineResult {
29
+ parallelMs: number;
30
+ sequentialMs: number;
31
+ ratio: number;
32
+ /** Recorded with the numbers, because a measurement without its engine is not comparable. */
33
+ engine: string;
34
+ }
35
+
36
+ const EVENT_LOG_LIMIT = 20;
37
+ const READS_PER_RUN = 5;
38
+ const EMPTY_STATS: DashboardStats = {
39
+ customers: 0,
40
+ products: 0,
41
+ orders: 0,
42
+ draft: 0,
43
+ confirmed: 0,
44
+ delivered: 0,
45
+ revenueCents: 0,
46
+ committedCents: 0,
47
+ };
48
+
49
+ /**
50
+ * Everything the Reactive screen shows, so the view stays presentational.
51
+ *
52
+ * Nothing here tells a query to refresh. Writes go through `rdb`, which announces the tables they
53
+ * touched, and both queries below watch those tables - which is why creating one order moves the
54
+ * revenue tile, the order count and the list at the same time.
55
+ */
56
+ export function useReactiveDemo() {
57
+ const busy = ref(false);
58
+ const busLog = ref<BusEntry[]>([]);
59
+ const pipeline = ref<PipelineResult | null>(null);
60
+ const measuring = ref(false);
61
+
62
+ const statsQuery = useReactiveQuery(() => loadDashboardStats(getDatabase().db), {
63
+ // Four tables, because the tiles aggregate across all of them.
64
+ tables: ["customer", "product", "sales_order", "order_line"],
65
+ queryKey: ["demo:stats"],
66
+ debounce: 250,
67
+ });
68
+
69
+ const ordersQuery = useReactiveQuery(() => searchOrders(getDatabase().db, ""), {
70
+ tables: ["sales_order", "order_line", "customer"],
71
+ queryKey: ["demo:orders"],
72
+ debounce: 250,
73
+ });
74
+
75
+ let busSequence = 0;
76
+ const stopWatching = changeBus.on(["*"], (event: TableChangeEvent) => {
77
+ busSequence += 1;
78
+ busLog.value = [
79
+ { id: busSequence, at: Date.now(), table: event.table, type: event.type },
80
+ ...busLog.value,
81
+ ].slice(0, EVENT_LOG_LIMIT);
82
+ });
83
+ onUnmounted(stopWatching);
84
+
85
+ const stats = computed<DashboardStats>(() => statsQuery.data.value ?? EMPTY_STATS);
86
+ const orders = computed<OrderRow[]>(() => ordersQuery.data.value ?? []);
87
+
88
+ async function withBusy(work: () => Promise<void>): Promise<void> {
89
+ busy.value = true;
90
+ try {
91
+ await work();
92
+ } finally {
93
+ busy.value = false;
94
+ }
95
+ }
96
+
97
+ const seed = () => withBusy(() => seedSampleData(rdb));
98
+
99
+ const advance = (orderId: number) => withBusy(() => advanceOrderStatus(rdb, orderId));
100
+
101
+ const setStatus = (orderId: number, status: "draft" | "confirmed" | "delivered") =>
102
+ withBusy(() => setOrderStatus(rdb, orderId, status));
103
+
104
+ const remove = (orderId: number) => withBusy(() => deleteOrder(rdb, orderId));
105
+
106
+ const save = (input: { customerId: number; reference: string; lines: DraftLine[] }) =>
107
+ withBusy(() => saveOrder(rdb, input));
108
+ /**
109
+ * Wipe the demo data, then put the catalogue back. Deleting the products and customers too would
110
+ * leave the app in a state where no order can be written and the only way out is a restart.
111
+ */
112
+ const clear = () =>
113
+ withBusy(async () => {
114
+ await clearAll(rdb);
115
+ await ensureReferenceData(rdb);
116
+ });
117
+
118
+ /**
119
+ * Reads issued together against reads awaited one by one.
120
+ *
121
+ * The worker executes statements one at a time, so this measures how much of a read is the message
122
+ * round trip rather than the query - it cannot show queries overlapping, and a ratio near 1x is
123
+ * the expected result rather than a regression.
124
+ */
125
+ async function measurePipelining(): Promise<void> {
126
+ measuring.value = true;
127
+ try {
128
+ const db = getDatabase().db;
129
+ const read = () => db.selectFrom("sales_order").select("id").limit(5).execute();
130
+ await read();
131
+
132
+ const parallelStart = performance.now();
133
+ await Promise.all(Array.from({ length: READS_PER_RUN }, read));
134
+ const parallelMs = performance.now() - parallelStart;
135
+
136
+ const sequentialStart = performance.now();
137
+ for (let index = 0; index < READS_PER_RUN; index++) await read();
138
+ const sequentialMs = performance.now() - sequentialStart;
139
+
140
+ pipeline.value = {
141
+ parallelMs: Number(parallelMs.toFixed(1)),
142
+ sequentialMs: Number(sequentialMs.toFixed(1)),
143
+ ratio: Number((sequentialMs / Math.max(parallelMs, 0.01)).toFixed(2)),
144
+ engine: activeStorageLabel(),
145
+ };
146
+ } finally {
147
+ measuring.value = false;
148
+ }
149
+ }
150
+
151
+ return {
152
+ stats,
153
+ orders,
154
+ loading: ordersQuery.loading,
155
+ busy,
156
+ busLog,
157
+ pipeline,
158
+ measuring,
159
+ readsPerRun: READS_PER_RUN,
160
+ isNative: Capacitor.isNativePlatform(),
161
+ engineName: activeStorageLabel(),
162
+ platform: Capacitor.getPlatform(),
163
+ seed,
164
+ advance,
165
+ setStatus,
166
+ remove,
167
+ save,
168
+ clear,
169
+ measurePipelining,
170
+ };
171
+ }
@@ -0,0 +1,22 @@
1
+ import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
3
+
4
+ const demoRoutes: Router.RouteParameters[] = [
5
+ {
6
+ name: "demo",
7
+ path: "/demo/",
8
+ async: lazyRoute(() => import("@/modules/demo/views/DemoView.vue")),
9
+ },
10
+ {
11
+ name: "demo-search",
12
+ path: "/demo/search/",
13
+ async: lazyRoute(() => import("@/modules/demo/views/OrderSearchView.vue")),
14
+ },
15
+ {
16
+ name: "demo-order",
17
+ path: "/demo/order/:id/",
18
+ async: lazyRoute(() => import("@/modules/demo/views/OrderDetailView.vue")),
19
+ },
20
+ ];
21
+
22
+ export default demoRoutes;
@@ -0,0 +1,210 @@
1
+ <template>
2
+ <AppPage :title="t('demo.title')" name="demo">
3
+ <template #actions>
4
+ <M3IconButton :label="t('demo.searchTitle')" @click="f7router.navigate('/demo/search/')">
5
+ <i-ms-search-rounded />
6
+ </M3IconButton>
7
+ <M3IconButton :label="t('metrics.title')" @click="metricsOpen = true">
8
+ <i-ms-monitoring-rounded />
9
+ </M3IconButton>
10
+ </template>
11
+
12
+ <M3Tabs
13
+ v-model="pane"
14
+ class="app-sticky-tabs sticky top-[calc(env(safe-area-inset-top)+64px)] z-[2] mb-3"
15
+ :label="t('demo.title')"
16
+ >
17
+ <M3Tab value="data" :label="t('demo.data')" />
18
+ <M3Tab value="diagnostics" :label="t('demo.diagnostics')" />
19
+ </M3Tabs>
20
+
21
+ <div v-show="pane === 'data'">
22
+ <DemoStatCards :stats="stats" />
23
+
24
+ <SectionHeader>
25
+ <span class="flex items-center gap-2">
26
+ {{ t("demo.orders") }}
27
+ <M3LoadingIndicator v-if="loading" :size="24" :label="t('demo.refetching')" />
28
+ </span>
29
+ </SectionHeader>
30
+
31
+ <DemoOrderList
32
+ v-if="orders.length"
33
+ :orders="orders"
34
+ @open="openOrder"
35
+ @more="orderActions"
36
+ @advance="(order) => run(() => advance(order.id))"
37
+ @delete="deleteOrder"
38
+ />
39
+ <EmptyState
40
+ v-else
41
+ shape="cookie6Sided"
42
+ :headline="t('demo.emptyTitle')"
43
+ :text="t('demo.empty')"
44
+ >
45
+ <template #icon><i-ms-receipt-long-outline-rounded class="size-10" /></template>
46
+ <M3Button variant="tonal" :disabled="busy" @click="run(seed)">
47
+ <template #icon><i-ms-wand-stars-rounded /></template>
48
+ {{ t("demo.seed") }}
49
+ </M3Button>
50
+ </EmptyState>
51
+ </div>
52
+
53
+ <div v-show="pane === 'diagnostics'" class="flex flex-col gap-3">
54
+ <DemoBusLog :entries="busLog" />
55
+ <DemoPipelineBenchmark
56
+ :result="pipeline"
57
+ :measuring="measuring"
58
+ :reads-per-run="readsPerRun"
59
+ :is-native="isNative"
60
+ :platform="platform"
61
+ :engine-name="engineName"
62
+ @measure="measurePipelining"
63
+ />
64
+ <DemoBenchmark />
65
+ </div>
66
+
67
+ <template v-if="pane === 'data'" #fab>
68
+ <M3FabMenu
69
+ v-model:open="fabOpen"
70
+ :label="t('demo.actions')"
71
+ :close-label="t('shell.dismiss')"
72
+ >
73
+ <template #icon><i-ms-add-rounded /></template>
74
+ <M3FabMenuItem :index="2" :label="t('demo.newOrder')" @click="createOpen = true">
75
+ <i-ms-edit-outline-rounded />
76
+ </M3FabMenuItem>
77
+ <M3FabMenuItem :index="1" :label="t('demo.seed')" @click="run(seed)">
78
+ <i-ms-wand-stars-rounded />
79
+ </M3FabMenuItem>
80
+ <M3FabMenuItem :index="0" :label="t('demo.clear')" @click="resetData">
81
+ <i-ms-delete-outline-rounded />
82
+ </M3FabMenuItem>
83
+ </M3FabMenu>
84
+ </template>
85
+
86
+ <template #fixed>
87
+ <DemoCreateOrderSheet v-model:open="createOpen" :save="saveOrder" />
88
+ <DemoMetricsSheet v-model:open="metricsOpen" />
89
+ </template>
90
+ </AppPage>
91
+ </template>
92
+
93
+ <script setup lang="ts">
94
+ import { markRaw } from "vue";
95
+ import type { ActionSheetGroup } from "@cavulsqa/m3e-vue";
96
+ import type { Router } from "framework7/types";
97
+ import DeleteIcon from "~icons/material-symbols/delete-outline-rounded";
98
+ import ShippingIcon from "~icons/material-symbols/local-shipping-outline-rounded";
99
+ import VerifiedIcon from "~icons/material-symbols/verified-outline-rounded";
100
+ import type { DraftLine, OrderRow } from "@/domains/sales/sales.repository";
101
+ import DemoBenchmark from "@/modules/demo/components/DemoBenchmark.vue";
102
+ import DemoBusLog from "@/modules/demo/components/DemoBusLog.vue";
103
+ import DemoCreateOrderSheet from "@/modules/demo/components/DemoCreateOrderSheet.vue";
104
+ import DemoMetricsSheet from "@/modules/demo/components/DemoMetricsSheet.vue";
105
+ import DemoOrderList from "@/modules/demo/components/DemoOrderList.vue";
106
+ import DemoPipelineBenchmark from "@/modules/demo/components/DemoPipelineBenchmark.vue";
107
+ import DemoStatCards from "@/modules/demo/components/DemoStatCards.vue";
108
+ import { useReactiveDemo } from "@/modules/demo/composables/useReactiveDemo";
109
+
110
+ const { t } = useI18n();
111
+ const props = defineProps<{ f7router: Router.Router }>();
112
+ const snackbar = useSnackbar();
113
+ const { open } = useContainerTransform();
114
+ const dialog = useDialog();
115
+ const actionSheet = useActionSheet();
116
+
117
+ const pane = ref<"data" | "diagnostics">("data");
118
+ const createOpen = ref(false);
119
+ const metricsOpen = ref(false);
120
+ const fabOpen = ref(false);
121
+
122
+ const {
123
+ stats,
124
+ orders,
125
+ loading,
126
+ busy,
127
+ busLog,
128
+ pipeline,
129
+ measuring,
130
+ readsPerRun,
131
+ isNative,
132
+ platform,
133
+ engineName,
134
+ seed,
135
+ advance,
136
+ remove,
137
+ save,
138
+ clear,
139
+ measurePipelining,
140
+ } = useReactiveDemo();
141
+
142
+ /** Every action reports failure the same way, so a broken write is never silent. */
143
+ async function run(action: () => Promise<void>): Promise<boolean> {
144
+ if (busy.value) return false;
145
+ try {
146
+ await action();
147
+ return true;
148
+ } catch (error) {
149
+ console.error("[demo] action failed", error);
150
+ void snackbar.show({ message: t("demo.actionFailed"), duration: "long" });
151
+ return false;
152
+ }
153
+ }
154
+
155
+ function openOrder(order: OrderRow, event: MouseEvent) {
156
+ open(props.f7router, event, `/demo/order/${String(order.id)}/`);
157
+ }
158
+
159
+ async function saveOrder(draft: { customerId: number; reference: string; lines: DraftLine[] }) {
160
+ await save(draft);
161
+ void snackbar.show(t("demo.saved", { reference: draft.reference }));
162
+ }
163
+
164
+ async function orderActions(order: OrderRow) {
165
+ const next =
166
+ order.status === "draft" ? "confirm" : order.status === "confirmed" ? "deliver" : null;
167
+ const groups: ActionSheetGroup[] = [
168
+ {
169
+ items: [
170
+ { id: "delete", label: t("demo.delete"), icon: markRaw(DeleteIcon), tone: "destructive" },
171
+ ],
172
+ },
173
+ ];
174
+ if (next) {
175
+ groups.unshift({
176
+ items: [
177
+ {
178
+ id: "advance",
179
+ label: next === "confirm" ? t("demo.confirm") : t("demo.deliver"),
180
+ icon: markRaw(next === "confirm" ? ShippingIcon : VerifiedIcon),
181
+ },
182
+ ],
183
+ });
184
+ }
185
+ const choice = await actionSheet.open({
186
+ title: order.reference,
187
+ supporting: order.customerName,
188
+ groups,
189
+ });
190
+ if (choice === "advance") await run(() => advance(order.id));
191
+ if (choice === "delete") await deleteOrder(order);
192
+ }
193
+
194
+ async function deleteOrder(order: OrderRow) {
195
+ if (await run(() => remove(order.id))) {
196
+ void snackbar.show(t("demo.deleted", { reference: order.reference }));
197
+ }
198
+ }
199
+
200
+ async function resetData() {
201
+ const confirmed = await dialog.confirm({
202
+ headline: t("demo.clearTitle"),
203
+ text: t("demo.clearText"),
204
+ confirmLabel: t("demo.clear"),
205
+ dismissLabel: t("demo.cancel"),
206
+ destructive: true,
207
+ });
208
+ if (confirmed) await run(clear);
209
+ }
210
+ </script>