@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,41 @@
1
+ import Framework7 from "framework7/lite";
2
+ import Tabs from "framework7/components/tabs";
3
+ import Framework7Vue from "framework7-vue";
4
+
5
+ import "@fontsource-variable/google-sans-flex/rond.css";
6
+ import "./assets/css/app.css";
7
+ import "./assets/css/m3e.css";
8
+
9
+ import App from "./App.vue";
10
+ import { i18n } from "./plugins/i18n.plugin";
11
+ import { m3e } from "./plugins/m3e.plugin";
12
+ import { renderBootstrapError } from "./plugins/bootstrapError";
13
+ import { seedPlugin } from "./plugins/seed.plugin";
14
+ import { sqlitePlugin } from "./plugins/sqlite.plugin";
15
+ import { startTheme } from "./shared/composables/theme/useThemeSettings";
16
+
17
+ Framework7.use([Tabs]);
18
+ Framework7.use(Framework7Vue);
19
+
20
+ /**
21
+ * The theme starts first, so even the bootstrap error screen is drawn in the user's colours and
22
+ * mode. The database opens before mount so the first screen never renders against a missing
23
+ * schema; if it fails, `renderBootstrapError` takes the screen instead of leaving an empty `#app`.
24
+ */
25
+ async function bootstrap(): Promise<void> {
26
+ startTheme();
27
+ try {
28
+ await sqlitePlugin();
29
+ await seedPlugin();
30
+ } catch (error) {
31
+ renderBootstrapError(error);
32
+ return;
33
+ }
34
+
35
+ const app = createApp(App);
36
+ app.use(i18n);
37
+ app.use(m3e);
38
+ app.mount("#app");
39
+ }
40
+
41
+ void bootstrap();
@@ -0,0 +1,119 @@
1
+ <template>
2
+ <section class="mx-4 flex flex-col gap-4 rounded-lg bg-surface-container-low p-4">
3
+ <header class="flex flex-col gap-1">
4
+ <h3 class="type-title-medium-emphasized m-0">{{ t("bench.title") }}</h3>
5
+ <p class="type-body-small m-0 text-on-surface-variant">{{ t("bench.intro") }}</p>
6
+ </header>
7
+
8
+ <div class="flex flex-wrap gap-2">
9
+ <M3Chip :label="engineName">
10
+ <template #icon><i-ms-database-outline-rounded /></template>
11
+ </M3Chip>
12
+ <M3Chip v-if="baseline" :label="t('bench.against', { engine: baseline.engine })" />
13
+ <M3Chip :label="t('bench.pragmas', { profile })" />
14
+ </div>
15
+
16
+ <M3Button :disabled="running" size="m" @click="run">
17
+ <template #icon><i-ms-speed-rounded /></template>
18
+ {{ running ? t("bench.running") : t("bench.run") }}
19
+ </M3Button>
20
+
21
+ <div v-if="running" class="flex flex-col gap-2">
22
+ <M3LinearProgress :label="t('bench.running')" />
23
+ <span v-if="progress" class="type-body-small text-on-surface-variant">{{ progress }}</span>
24
+ </div>
25
+
26
+ <p
27
+ v-if="failure"
28
+ class="type-body-medium m-0 rounded-md bg-error-container p-3 text-on-error-container"
29
+ role="alert"
30
+ >
31
+ {{ failure }}
32
+ </p>
33
+
34
+ <div v-if="result" class="grid grid-cols-3 gap-0.5 overflow-hidden rounded-md text-center">
35
+ <div class="flex flex-col gap-1 bg-surface-container p-3">
36
+ <span class="type-label-medium text-on-surface-variant">{{ t("bench.cases") }}</span>
37
+ <span class="type-title-medium tabular-nums">{{ result.cases.length }}</span>
38
+ </div>
39
+ <div class="flex flex-col gap-1 bg-surface-container p-3">
40
+ <span class="type-label-medium text-on-surface-variant">{{ t("bench.rows") }}</span>
41
+ <span class="type-title-medium tabular-nums">{{ result.rowsSeeded }}</span>
42
+ </div>
43
+ <div class="flex flex-col gap-1 bg-surface-container p-3">
44
+ <span class="type-label-medium text-on-surface-variant">{{ t("bench.wall") }}</span>
45
+ <span class="type-title-medium tabular-nums">{{ Math.round(result.totalMs) }} ms</span>
46
+ </div>
47
+ </div>
48
+ </section>
49
+
50
+ <template v-for="group in groups" :key="group">
51
+ <template v-if="byGroup(group).length">
52
+ <SectionHeader :title="t(`bench.group.${group}`)" />
53
+ <M3List variant="segmented" inset>
54
+ <M3ListItem
55
+ v-for="entry in byGroup(group)"
56
+ :key="entry.name"
57
+ :headline="entry.name"
58
+ :supporting="`${entry.current.operations} ops · p50 ${entry.current.medianMs} ms · ${t('bench.worst')} ${entry.current.worstMs} ms`"
59
+ :trailing-text="`${entry.current.msPerOperation} ms`"
60
+ :overline="entry.speedup ? speedupText(entry.speedup) : entry.note"
61
+ >
62
+ <template v-if="entry.speedup" #leading>
63
+ <M3Shape
64
+ :shape="entry.speedup >= 1 ? 'sunny' : 'square'"
65
+ class="size-8"
66
+ :class="verdictClass(entry.speedup)"
67
+ />
68
+ </template>
69
+ </M3ListItem>
70
+ </M3List>
71
+ </template>
72
+ </template>
73
+
74
+ <div v-if="result" class="flex gap-2 px-4 pt-4">
75
+ <M3Button variant="outlined" class="flex-1" @click="copy">{{ t("bench.copy") }}</M3Button>
76
+ <M3Button variant="text" class="flex-1 text-error" @click="clear">{{
77
+ t("bench.clear")
78
+ }}</M3Button>
79
+ </div>
80
+ </template>
81
+
82
+ <script setup lang="ts">
83
+ import { pragmaProfile } from "@/app/pragmas.config";
84
+ import { useBenchmark, type CaseComparison } from "@/modules/demo/composables/useBenchmark";
85
+
86
+ const { t } = useI18n();
87
+ const snackbar = useSnackbar();
88
+ const { engineName, running, progress, failure, result, baseline, comparison, run, clear, asJson } =
89
+ useBenchmark();
90
+
91
+ const profile = pragmaProfile;
92
+ const groups = ["read", "join", "write", "transaction", "schema", "concurrency", "sync"] as const;
93
+
94
+ function byGroup(group: CaseComparison["group"]): CaseComparison[] {
95
+ return comparison.value.filter((entry) => entry.group === group);
96
+ }
97
+
98
+ /** Both directions read plainly; "0.4x faster" would not. */
99
+ function speedupText(speedup: number): string {
100
+ return speedup >= 1
101
+ ? t("bench.faster", { times: speedup.toFixed(2) })
102
+ : t("bench.slower", { times: (1 / speedup).toFixed(2) });
103
+ }
104
+
105
+ function verdictClass(speedup: number): string {
106
+ if (speedup >= 1.2) return "bg-success-container";
107
+ if (speedup <= 0.83) return "bg-error-container";
108
+ return "bg-surface-container-highest";
109
+ }
110
+
111
+ async function copy() {
112
+ try {
113
+ await navigator.clipboard.writeText(asJson());
114
+ void snackbar.show(t("bench.copied"));
115
+ } catch {
116
+ void snackbar.show({ message: t("bench.copyFailed"), duration: "long" });
117
+ }
118
+ }
119
+ </script>
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <section class="mx-4 flex flex-col gap-3 rounded-lg bg-surface-container-low p-4">
3
+ <header class="flex flex-col gap-0.5">
4
+ <h3 class="type-title-medium-emphasized m-0">{{ t("demo.bus") }}</h3>
5
+ <p class="type-body-small m-0 text-on-surface-variant">{{ t("demo.busHint") }}</p>
6
+ </header>
7
+ <TransitionGroup
8
+ v-if="entries.length"
9
+ tag="ul"
10
+ name="bus"
11
+ class="m-0 flex list-none flex-col gap-0.5 p-0"
12
+ >
13
+ <li
14
+ v-for="entry in entries"
15
+ :key="entry.id"
16
+ class="flex items-center justify-between rounded-xs bg-surface-container px-3 py-2 first:rounded-t-md last:rounded-b-md"
17
+ >
18
+ <span class="type-label-large"
19
+ >{{ entry.table }}<span class="text-on-surface-variant">.{{ entry.type }}</span></span
20
+ >
21
+ <span class="type-label-small tabular-nums text-on-surface-variant">{{
22
+ formatTime(entry.at)
23
+ }}</span>
24
+ </li>
25
+ </TransitionGroup>
26
+ <p v-else class="type-body-medium m-0 text-on-surface-variant">{{ t("demo.busEmpty") }}</p>
27
+ </section>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import type { BusEntry } from "@/modules/demo/composables/useReactiveDemo";
32
+
33
+ defineProps<{ entries: BusEntry[] }>();
34
+ const { t } = useI18n();
35
+
36
+ function formatTime(at: number): string {
37
+ return new Date(at).toLocaleTimeString(undefined, { hour12: false });
38
+ }
39
+ </script>
40
+
41
+ <style scoped>
42
+ .bus-enter-active {
43
+ transition:
44
+ transform var(--md-sys-motion-spring-fast-spatial-duration)
45
+ var(--md-sys-motion-spring-fast-spatial),
46
+ opacity var(--md-sys-motion-spring-fast-effects-duration)
47
+ var(--md-sys-motion-spring-fast-effects);
48
+ }
49
+
50
+ .bus-enter-from {
51
+ opacity: 0;
52
+ transform: translateY(-8px) scale(0.98);
53
+ }
54
+
55
+ .bus-move {
56
+ transition: transform var(--md-sys-motion-spring-default-spatial-duration)
57
+ var(--md-sys-motion-spring-default-spatial);
58
+ }
59
+
60
+ .bus-leave-active {
61
+ display: none;
62
+ }
63
+ </style>
@@ -0,0 +1,203 @@
1
+ <template>
2
+ <M3BottomSheet v-model:open="open" :title="t('demo.newOrder')" @opened="focusReference">
3
+ <div class="flex flex-col gap-2 px-4">
4
+ <M3TextField
5
+ ref="referenceField"
6
+ v-model="reference"
7
+ :label="t('demo.reference')"
8
+ autocomplete="off"
9
+ />
10
+ <M3List variant="segmented">
11
+ <M3ListItem
12
+ clickable
13
+ :overline="t('demo.customer')"
14
+ :headline="customerLabel"
15
+ @click="chooseCustomer"
16
+ >
17
+ <template #leading><i-ms-person-outline-rounded /></template>
18
+ <template #trailing><i-ms-chevron-right-rounded class="rtl:-scale-x-100" /></template>
19
+ </M3ListItem>
20
+ </M3List>
21
+ </div>
22
+
23
+ <SectionHeader :title="t('demo.products')" />
24
+ <M3List variant="segmented" inset :label="t('demo.products')">
25
+ <M3ListItem
26
+ v-for="product in products"
27
+ :key="product.id"
28
+ :headline="product.name"
29
+ :supporting="formatMoney(product.price_cents)"
30
+ :selected="quantityOf(product.id) > 0"
31
+ >
32
+ <template #action>
33
+ <div class="flex items-center gap-1">
34
+ <M3IconButton
35
+ :label="t('demo.less', { product: product.name })"
36
+ variant="tonal"
37
+ size="xs"
38
+ :disabled="quantityOf(product.id) === 0"
39
+ @click="step(product.id, -1)"
40
+ >
41
+ <i-ms-remove-rounded />
42
+ </M3IconButton>
43
+ <span
44
+ class="type-title-small-emphasized w-7 text-center tabular-nums"
45
+ aria-live="polite"
46
+ >
47
+ {{ quantityOf(product.id) }}
48
+ </span>
49
+ <M3IconButton
50
+ :label="t('demo.more', { product: product.name })"
51
+ variant="tonal"
52
+ size="xs"
53
+ @click="step(product.id, 1)"
54
+ >
55
+ <i-ms-add-rounded />
56
+ </M3IconButton>
57
+ </div>
58
+ </template>
59
+ </M3ListItem>
60
+ </M3List>
61
+
62
+ <template #footer>
63
+ <div class="flex items-center gap-4">
64
+ <div class="flex flex-1 flex-col">
65
+ <span class="type-label-medium text-on-surface-variant">{{ t("demo.total") }}</span>
66
+ <span class="type-title-large-emphasized tabular-nums">{{
67
+ formatMoney(totalCents)
68
+ }}</span>
69
+ </div>
70
+ <M3Button size="m" :disabled="!canSave || saving" @click="save">{{
71
+ t("demo.save")
72
+ }}</M3Button>
73
+ </div>
74
+ </template>
75
+ </M3BottomSheet>
76
+ </template>
77
+
78
+ <script setup lang="ts">
79
+ import type { ComponentPublicInstance } from "vue";
80
+ import {
81
+ listCustomers,
82
+ listProducts,
83
+ nextOrderReference,
84
+ type DraftLine,
85
+ type ProductRow,
86
+ } from "@/domains/sales/sales.repository";
87
+ import { formatMoney } from "@/modules/demo/composables/useOrderStatus";
88
+ import { getDatabase } from "@/shared/database/database";
89
+
90
+ /**
91
+ * The new-order form in a bottom sheet. Saving is handed in as an async prop rather than emitted, so
92
+ * the sheet stays open and says so when the write fails instead of closing on a lost order.
93
+ */
94
+ const open = defineModel<boolean>("open", { default: false });
95
+ const props = defineProps<{
96
+ save: (draft: { customerId: number; reference: string; lines: DraftLine[] }) => Promise<void>;
97
+ }>();
98
+ const { t } = useI18n();
99
+ const actionSheet = useActionSheet();
100
+ const snackbar = useSnackbar();
101
+
102
+ const customers = ref<Array<{ id: number; name: string; city: string }>>([]);
103
+ const products = ref<ProductRow[]>([]);
104
+ const quantities = ref<Record<number, number>>({});
105
+ const reference = ref("");
106
+ const customerId = ref(0);
107
+ const saving = ref(false);
108
+ const referenceField = useTemplateRef<ComponentPublicInstance>("referenceField");
109
+
110
+ const customerLabel = computed(() => {
111
+ const customer = customers.value.find((entry) => entry.id === customerId.value);
112
+ return customer ? `${customer.name} — ${customer.city}` : t("demo.chooseCustomer");
113
+ });
114
+
115
+ /** Loaded each time the sheet opens rather than on mount, so the reference follows the latest order. */
116
+ async function load() {
117
+ try {
118
+ const db = getDatabase().db;
119
+ const [loadedCustomers, loadedProducts, nextReference] = await Promise.all([
120
+ listCustomers(db),
121
+ listProducts(db),
122
+ nextOrderReference(db),
123
+ ]);
124
+ customers.value = loadedCustomers;
125
+ products.value = loadedProducts;
126
+ reference.value = nextReference;
127
+ customerId.value = loadedCustomers[0]?.id ?? 0;
128
+ quantities.value = {};
129
+ } catch (error) {
130
+ console.error("[demo] could not load the order form", error);
131
+ void snackbar.show({ message: t("demo.loadFailed"), duration: "long" });
132
+ }
133
+ }
134
+
135
+ watch(open, (isOpen) => {
136
+ if (isOpen) void load();
137
+ });
138
+
139
+ function focusReference() {
140
+ (referenceField.value?.$el as HTMLElement | undefined)
141
+ ?.querySelector("input")
142
+ ?.focus({ preventScroll: true });
143
+ }
144
+
145
+ const quantityOf = (productId: number) => quantities.value[productId] ?? 0;
146
+
147
+ function step(productId: number, delta: number) {
148
+ const next = Math.min(99, Math.max(0, quantityOf(productId) + delta));
149
+ quantities.value = { ...quantities.value, [productId]: next };
150
+ }
151
+
152
+ async function chooseCustomer() {
153
+ const choice = await actionSheet.open({
154
+ title: t("demo.customer"),
155
+ groups: [
156
+ {
157
+ items: customers.value.map((customer) => ({
158
+ id: String(customer.id),
159
+ label: customer.name,
160
+ supporting: customer.city,
161
+ selected: customer.id === customerId.value,
162
+ })),
163
+ },
164
+ ],
165
+ });
166
+ if (choice) customerId.value = Number(choice);
167
+ }
168
+
169
+ const lines = computed(() =>
170
+ products.value
171
+ .filter((product) => quantityOf(product.id) > 0)
172
+ .map((product) => ({
173
+ productId: product.id,
174
+ quantity: quantityOf(product.id),
175
+ unitPriceCents: product.price_cents,
176
+ })),
177
+ );
178
+
179
+ const totalCents = computed(() =>
180
+ lines.value.reduce((sum, line) => sum + line.quantity * line.unitPriceCents, 0),
181
+ );
182
+ const canSave = computed(
183
+ () => customerId.value > 0 && lines.value.length > 0 && reference.value.trim() !== "",
184
+ );
185
+
186
+ async function save() {
187
+ if (!canSave.value || saving.value) return;
188
+ saving.value = true;
189
+ try {
190
+ await props.save({
191
+ customerId: customerId.value,
192
+ reference: reference.value.trim(),
193
+ lines: lines.value,
194
+ });
195
+ open.value = false;
196
+ } catch (error) {
197
+ console.error("[demo] saving the order failed", error);
198
+ void snackbar.show({ message: t("demo.saveFailed"), duration: "long" });
199
+ } finally {
200
+ saving.value = false;
201
+ }
202
+ }
203
+ </script>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <M3BottomSheet v-model:open="open" :title="t('metrics.title')">
3
+ <div class="grid grid-cols-2 gap-2 px-4">
4
+ <div
5
+ v-for="tile in tiles"
6
+ :key="tile.labelKey"
7
+ class="flex flex-col gap-1 rounded-lg bg-surface-container p-4"
8
+ >
9
+ <span class="type-label-medium text-on-surface-variant">{{ t(tile.labelKey) }}</span>
10
+ <span class="type-title-large-emphasized tabular-nums">{{ tile.value }}</span>
11
+ </div>
12
+ </div>
13
+
14
+ <SectionHeader :title="t('metrics.refetchesTitle')" />
15
+ <M3List v-if="refetches.length" variant="segmented" inset>
16
+ <M3ListItem
17
+ v-for="[table, count] in refetches"
18
+ :key="table"
19
+ :headline="table"
20
+ :trailing-text="String(count)"
21
+ >
22
+ <template #leading><i-ms-sync-rounded /></template>
23
+ </M3ListItem>
24
+ </M3List>
25
+ <p v-else class="type-body-medium m-0 px-6 text-on-surface-variant">
26
+ {{ t("metrics.noRefetches") }}
27
+ </p>
28
+
29
+ <SectionHeader :title="t('metrics.slowest')" />
30
+ <M3List v-if="slowestQueries.length" variant="segmented" inset>
31
+ <M3ListItem
32
+ v-for="entry in slowestQueries"
33
+ :key="entry.key"
34
+ :headline="`${entry.avgTime.toFixed(1)} ms`"
35
+ :supporting="entry.key"
36
+ :trailing-text="t('metrics.calls', { count: entry.count }, entry.count)"
37
+ multiline
38
+ >
39
+ <template #leading><i-ms-speed-rounded /></template>
40
+ </M3ListItem>
41
+ </M3List>
42
+ <p v-else class="type-body-medium m-0 px-6 text-on-surface-variant">
43
+ {{ t("metrics.noQueries") }}
44
+ </p>
45
+
46
+ <template #footer>
47
+ <M3Button variant="outlined" class="w-full" @click="reset">{{ t("metrics.reset") }}</M3Button>
48
+ </template>
49
+ </M3BottomSheet>
50
+ </template>
51
+
52
+ <script setup lang="ts">
53
+ import { useQueryMetrics } from "@/shared/database/queries";
54
+
55
+ /** The query metrics, in a sheet over whichever screen asked - the numbers belong to no one page. */
56
+ const open = defineModel<boolean>("open", { default: false });
57
+ const { t } = useI18n();
58
+ const {
59
+ totalQueries,
60
+ avgQueryTime,
61
+ cacheHitRate,
62
+ activeListeners,
63
+ refetchesByTable,
64
+ slowestQueries,
65
+ reset,
66
+ } = useQueryMetrics();
67
+
68
+ const refetches = computed(() => Object.entries(refetchesByTable.value));
69
+ const tiles = computed(() => [
70
+ { labelKey: "metrics.totalQueries", value: String(totalQueries.value) },
71
+ { labelKey: "metrics.avgQueryTime", value: `${avgQueryTime.value.toFixed(1)} ms` },
72
+ { labelKey: "metrics.cacheHitRate", value: `${cacheHitRate.value.toFixed(0)} %` },
73
+ { labelKey: "metrics.activeListeners", value: String(activeListeners.value) },
74
+ ]);
75
+ </script>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <M3List variant="segmented" inset :label="t('demo.orders')">
3
+ <M3ListItem
4
+ v-for="order in orders"
5
+ :key="order.id"
6
+ v-model:swiped="swiped[order.id]"
7
+ clickable
8
+ swipe-full="end"
9
+ :headline="order.reference"
10
+ :supporting="`${order.customerName} · ${t('demo.lines', { count: order.lines }, order.lines)}`"
11
+ @click="emit('open', order, $event)"
12
+ >
13
+ <template #leading>
14
+ <M3Shape
15
+ :shape="statusLook(order.status).shape"
16
+ class="size-10"
17
+ :class="statusLook(order.status).classes"
18
+ >
19
+ <component :is="statusLook(order.status).icon" class="size-5" />
20
+ </M3Shape>
21
+ </template>
22
+ <template #trailing>
23
+ <span class="type-title-small-emphasized tabular-nums text-on-surface">{{
24
+ formatMoney(order.totalCents)
25
+ }}</span>
26
+ </template>
27
+ <template #swipe-end>
28
+ <M3SwipeAction
29
+ v-if="nextStep(order)"
30
+ tone="primary"
31
+ :label="nextStep(order) === 'confirm' ? t('demo.confirm') : t('demo.deliver')"
32
+ @click="emit('advance', order)"
33
+ >
34
+ <i-ms-local-shipping-outline-rounded v-if="nextStep(order) === 'confirm'" />
35
+ <i-ms-verified-outline-rounded v-else />
36
+ </M3SwipeAction>
37
+ <M3SwipeAction tone="error" :label="t('demo.delete')" @click="emit('delete', order)">
38
+ <i-ms-delete-outline-rounded />
39
+ </M3SwipeAction>
40
+ </template>
41
+ <template #action>
42
+ <M3IconButton
43
+ :label="t('demo.actionsFor', { reference: order.reference })"
44
+ size="s"
45
+ @click="emit('more', order)"
46
+ >
47
+ <i-ms-more-vert-rounded />
48
+ </M3IconButton>
49
+ </template>
50
+ </M3ListItem>
51
+ </M3List>
52
+ </template>
53
+
54
+ <script setup lang="ts">
55
+ import type { OrderRow } from "@/domains/sales/sales.repository";
56
+ import { formatMoney, statusLook } from "@/modules/demo/composables/useOrderStatus";
57
+
58
+ /**
59
+ * Tapping a row opens the order. Swiping it toward the start reveals its next step and Delete - a
60
+ * long swipe deletes - and the trailing button offers the same actions in a sheet, for anyone who
61
+ * never discovers the gesture.
62
+ */
63
+ defineProps<{ orders: OrderRow[] }>();
64
+ const emit = defineEmits<{
65
+ open: [order: OrderRow, event: MouseEvent];
66
+ more: [order: OrderRow];
67
+ advance: [order: OrderRow];
68
+ delete: [order: OrderRow];
69
+ }>();
70
+ const { t } = useI18n();
71
+ const swiped = reactive<Record<number, "start" | "end" | null>>({});
72
+
73
+ const nextStep = (order: OrderRow) =>
74
+ order.status === "draft" ? "confirm" : order.status === "confirmed" ? "deliver" : null;
75
+ </script>
@@ -0,0 +1,65 @@
1
+ <template>
2
+ <section class="mx-4 flex flex-col gap-4 rounded-lg bg-surface-container-low p-4">
3
+ <header class="flex flex-col gap-1">
4
+ <h3 class="type-title-medium-emphasized m-0">{{ t("demo.pipelining") }}</h3>
5
+ <p class="type-body-small m-0 text-on-surface-variant">
6
+ {{
7
+ isNative
8
+ ? t("demo.pipeliningNative", { count: readsPerRun })
9
+ : t("demo.pipeliningWeb", { count: readsPerRun })
10
+ }}
11
+ </p>
12
+ </header>
13
+
14
+ <div class="flex flex-wrap items-center gap-2">
15
+ <M3Chip :label="engineName">
16
+ <template #icon><i-ms-database-outline-rounded /></template>
17
+ </M3Chip>
18
+ <span class="type-body-small text-on-surface-variant">{{ t("demo.engineSwitch") }}</span>
19
+ </div>
20
+
21
+ <div class="flex items-center gap-3">
22
+ <M3Button :disabled="measuring" @click="emit('measure')">
23
+ <template #icon><i-ms-speed-rounded /></template>
24
+ {{ measuring ? t("demo.measuring") : t("demo.measure") }}
25
+ </M3Button>
26
+ <M3LoadingIndicator v-if="measuring" :size="40" :label="t('demo.measuring')" />
27
+ </div>
28
+
29
+ <template v-if="result">
30
+ <div class="grid grid-cols-3 gap-0.5 overflow-hidden rounded-md text-center">
31
+ <div class="flex flex-col gap-1 bg-surface-container p-3">
32
+ <span class="type-label-medium text-on-surface-variant">{{ t("demo.parallel") }}</span>
33
+ <span class="type-title-medium tabular-nums">{{ result.parallelMs }} ms</span>
34
+ </div>
35
+ <div class="flex flex-col gap-1 bg-surface-container p-3">
36
+ <span class="type-label-medium text-on-surface-variant">{{ t("demo.sequential") }}</span>
37
+ <span class="type-title-medium tabular-nums">{{ result.sequentialMs }} ms</span>
38
+ </div>
39
+ <div class="flex flex-col gap-1 bg-primary-container p-3 text-on-primary-container">
40
+ <span class="type-label-medium">{{ t("demo.ratio") }}</span>
41
+ <span class="type-title-medium-emphasized tabular-nums">{{ result.ratio }}&times;</span>
42
+ </div>
43
+ </div>
44
+ <p class="type-body-small m-0 text-on-surface-variant">
45
+ {{ t("demo.measuredOn", { engine: result.engine }) }} ·
46
+ {{ isNative ? t("demo.ratioNative") : t("demo.ratioWeb", { platform }) }}
47
+ </p>
48
+ </template>
49
+ </section>
50
+ </template>
51
+
52
+ <script setup lang="ts">
53
+ import type { PipelineResult } from "@/modules/demo/composables/useReactiveDemo";
54
+
55
+ defineProps<{
56
+ result: PipelineResult | null;
57
+ measuring: boolean;
58
+ readsPerRun: number;
59
+ isNative: boolean;
60
+ platform: string;
61
+ engineName: string;
62
+ }>();
63
+ const emit = defineEmits<{ measure: [] }>();
64
+ const { t } = useI18n();
65
+ </script>