@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,77 @@
1
+ <template>
2
+ <AppPage :title="t('gallery.contacts.title')" back variant="small">
3
+ <div class="pe-6">
4
+ <M3ListGroup v-for="group in groups" :key="group.key" :title="group.key">
5
+ <M3ListItem
6
+ v-for="customer in group.customers"
7
+ :key="customer.name"
8
+ clickable
9
+ :headline="customer.name"
10
+ :supporting="customer.city"
11
+ >
12
+ <template #leading>
13
+ <span
14
+ class="type-title-medium grid size-10 place-items-center rounded-full bg-secondary-container text-on-secondary-container"
15
+ >{{ customer.name.charAt(0) }}</span
16
+ >
17
+ </template>
18
+ </M3ListItem>
19
+ </M3ListGroup>
20
+ </div>
21
+
22
+ <template #fixed>
23
+ <M3ListIndex :keys="keys" :label="t('gallery.contacts.jump')" />
24
+ </template>
25
+ </AppPage>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { groupKey } from "@cavulsqa/m3e-vue";
30
+ import { WILAYAS } from "@/modules/gallery/composables/wilayas";
31
+
32
+ const NAMES = [
33
+ "Amine",
34
+ "Bachir",
35
+ "Chérif",
36
+ "Djamel",
37
+ "Elias",
38
+ "Farid",
39
+ "Ghani",
40
+ "Hakim",
41
+ "Idir",
42
+ "Jamel",
43
+ "Karim",
44
+ "Lotfi",
45
+ "Mourad",
46
+ "Nabil",
47
+ "Omar",
48
+ "Rachid",
49
+ "Samir",
50
+ "Tarek",
51
+ "Walid",
52
+ "Yacine",
53
+ "Zinedine",
54
+ ];
55
+ const TRADES = ["Alimentation", "Supérette"];
56
+
57
+ const { t } = useI18n();
58
+
59
+ const customers = NAMES.flatMap((name, index) =>
60
+ TRADES.map((trade, offset) => ({
61
+ name: `${name} ${trade}`,
62
+ city: WILAYAS[(index * 7 + offset * 13) % WILAYAS.length]!,
63
+ })),
64
+ ).sort((a, b) => a.name.localeCompare(b.name, "fr"));
65
+
66
+ const groups = [
67
+ ...customers
68
+ .reduce((byKey, customer) => {
69
+ const key = groupKey(customer.name);
70
+ byKey.set(key, [...(byKey.get(key) ?? []), customer]);
71
+ return byKey;
72
+ }, new Map<string, typeof customers>())
73
+ .entries(),
74
+ ].map(([key, members]) => ({ key, customers: members }));
75
+
76
+ const keys = groups.map((group) => group.key);
77
+ </script>
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <AppPage
3
+ :title="story ? t(`gallery.featured.${story.id}.title`) : t('errors.notFoundTitle')"
4
+ variant="small"
5
+ back
6
+ >
7
+ <template v-if="story">
8
+ <section
9
+ class="mx-4 flex flex-col gap-4 rounded-xl p-6"
10
+ :class="TONE_CLASSES[story.tone]"
11
+ :aria-label="t(`gallery.featured.${story.id}.title`)"
12
+ >
13
+ <M3Shape :shape="story.shape" class="size-20 bg-surface/60">
14
+ <component :is="story.icon" class="size-10" />
15
+ </M3Shape>
16
+ <h2 class="type-headline-small m-0">{{ t(`gallery.featured.${story.id}.title`) }}</h2>
17
+ <p class="type-body-large m-0 opacity-80">{{ t(`gallery.featured.${story.id}.text`) }}</p>
18
+ </section>
19
+
20
+ <p class="type-body-large m-0 px-6 pt-6 text-on-surface">
21
+ {{ t(`gallery.featured.${story.id}.body`) }}
22
+ </p>
23
+
24
+ <SectionHeader :title="t('gallery.featured.glance')" />
25
+ <M3List variant="segmented" inset :label="t('gallery.featured.glance')">
26
+ <M3ListItem
27
+ v-for="fact in story.facts"
28
+ :key="fact.id"
29
+ :headline="t(`gallery.featured.${story.id}.facts.${fact.id}`)"
30
+ :trailing-text="number.format(fact.value)"
31
+ >
32
+ <template #leading><component :is="fact.icon" /></template>
33
+ </M3ListItem>
34
+ </M3List>
35
+
36
+ <p class="type-body-small m-0 px-8 pt-4 text-on-surface-variant">
37
+ {{ t("gallery.featured.note") }}
38
+ </p>
39
+ </template>
40
+
41
+ <EmptyState
42
+ v-else
43
+ shape="ghostish"
44
+ :headline="t('errors.notFoundTitle')"
45
+ :text="t('errors.notFound')"
46
+ />
47
+ </AppPage>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import type { Router } from "framework7/types";
52
+ import { findStory } from "@/modules/gallery/composables/featuredStories";
53
+ import { TONE_CLASSES } from "@/shared/utils/tone";
54
+
55
+ const props = defineProps<{ f7route: Router.Route; f7router: Router.Router }>();
56
+ const { t, locale } = useI18n();
57
+
58
+ const story = computed(() => findStory(String(props.f7route.params.id ?? "")));
59
+ const number = computed(() => new Intl.NumberFormat(locale.value));
60
+ </script>
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <AppPage :title="t('gallery.login.title')" back variant="small">
3
+ <form
4
+ class="mx-auto flex w-full max-w-md flex-col gap-5 px-6 pt-6"
5
+ novalidate
6
+ @submit.prevent="signIn"
7
+ >
8
+ <div class="flex flex-col items-center gap-4 pb-2 text-center">
9
+ <M3Shape
10
+ shape="cookie12Sided"
11
+ class="size-24 bg-primary-container text-on-primary-container"
12
+ >
13
+ <i-ms-storefront-outline-rounded class="size-10" />
14
+ </M3Shape>
15
+ <h2 class="type-headline-small-emphasized m-0 text-on-surface">
16
+ {{ t("gallery.login.heading") }}
17
+ </h2>
18
+ <p class="type-body-medium m-0 text-on-surface-variant">
19
+ {{ t("gallery.login.subtitle") }}
20
+ </p>
21
+ </div>
22
+
23
+ <M3TextField
24
+ v-model="account.email"
25
+ :label="t('gallery.login.email')"
26
+ :error="emailError"
27
+ :supporting="restored ? t('gallery.login.restored') : undefined"
28
+ type="email"
29
+ inputmode="email"
30
+ autocomplete="username"
31
+ autocapitalize="off"
32
+ spellcheck="false"
33
+ enterkeyhint="next"
34
+ :disabled="busy"
35
+ @blur="touched.email = true"
36
+ >
37
+ <template #leading><i-ms-mail-outline-rounded /></template>
38
+ </M3TextField>
39
+
40
+ <M3TextField
41
+ v-model="password"
42
+ :label="t('gallery.login.password')"
43
+ :error="passwordError"
44
+ :type="reveal ? 'text' : 'password'"
45
+ autocomplete="current-password"
46
+ enterkeyhint="go"
47
+ :disabled="busy"
48
+ @blur="touched.password = true"
49
+ >
50
+ <template #leading><i-ms-lock-outline-rounded /></template>
51
+ <template #trailing>
52
+ <M3IconButton
53
+ toggle
54
+ :selected="reveal"
55
+ :label="reveal ? t('gallery.login.hide') : t('gallery.login.show')"
56
+ @update:selected="reveal = $event"
57
+ >
58
+ <i-ms-visibility-off-outline-rounded v-if="reveal" />
59
+ <i-ms-visibility-outline-rounded v-else />
60
+ </M3IconButton>
61
+ </template>
62
+ </M3TextField>
63
+
64
+ <div class="flex justify-end">
65
+ <M3Button variant="text" type="button" @click="forgot">{{
66
+ t("gallery.login.forgot")
67
+ }}</M3Button>
68
+ </div>
69
+
70
+ <M3Button type="submit" size="m" :disabled="busy">
71
+ <template v-if="busy" #leading
72
+ ><M3LoadingIndicator :size="24" :label="t('gallery.login.signingIn')"
73
+ /></template>
74
+ {{ busy ? t("gallery.login.signingIn") : t("gallery.login.signIn") }}
75
+ </M3Button>
76
+
77
+ <p class="type-body-small m-0 text-center text-on-surface-variant">
78
+ {{ t("gallery.login.hint") }}
79
+ </p>
80
+ </form>
81
+ </AppPage>
82
+ </template>
83
+
84
+ <script setup lang="ts">
85
+ import type { Router } from "framework7/types";
86
+ import { useFormDraft } from "@cavulsqa/m3e-vue";
87
+
88
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
89
+
90
+ const props = defineProps<{ f7router: Router.Router }>();
91
+ const { t } = useI18n();
92
+ const snackbar = useSnackbar();
93
+
94
+ const account = reactive({ email: "" });
95
+ const password = ref("");
96
+ const reveal = ref(false);
97
+ const busy = ref(false);
98
+ const rejected = ref(false);
99
+ const touched = reactive({ email: false, password: false });
100
+ const { restored, clear } = useFormDraft("gallery.login", account);
101
+
102
+ const emailError = computed(() =>
103
+ touched.email && !EMAIL.test(account.email.trim()) ? t("gallery.login.emailInvalid") : undefined,
104
+ );
105
+ const passwordError = computed(() => {
106
+ if (rejected.value) return t("gallery.login.rejected");
107
+ return touched.password && !password.value ? t("gallery.login.passwordMissing") : undefined;
108
+ });
109
+
110
+ watch(password, () => (rejected.value = false));
111
+
112
+ async function signIn() {
113
+ touched.email = true;
114
+ touched.password = true;
115
+ if (emailError.value || !password.value || busy.value) return;
116
+ busy.value = true;
117
+ try {
118
+ await new Promise((resolve) => setTimeout(resolve, 900));
119
+ if (password.value !== "demo") {
120
+ rejected.value = true;
121
+ return;
122
+ }
123
+ await clear();
124
+ password.value = "";
125
+ void snackbar.show(t("gallery.login.welcome", { email: account.email.trim() }));
126
+ props.f7router.back();
127
+ } finally {
128
+ busy.value = false;
129
+ }
130
+ }
131
+
132
+ function forgot() {
133
+ void snackbar.show(t("gallery.login.forgotSent"));
134
+ }
135
+ </script>
@@ -0,0 +1,116 @@
1
+ <template>
2
+ <AppPage :title="t('gallery.onboarding.title')" back variant="small" class="gallery-onboarding">
3
+ <M3Pager
4
+ v-model:page="page"
5
+ :label="t('gallery.onboarding.title')"
6
+ class="gallery-onboarding__pager"
7
+ >
8
+ <M3PagerPage
9
+ v-for="slide in SLIDES"
10
+ :key="slide.id"
11
+ :label="t(`gallery.onboarding.pages.${slide.id}.title`)"
12
+ >
13
+ <div class="flex h-full flex-col items-center justify-center gap-6 px-8 text-center">
14
+ <M3Shape :shape="slide.shape" class="size-48" :class="slide.tone">
15
+ <component :is="slide.icon" class="size-20" />
16
+ </M3Shape>
17
+ <h2 class="type-headline-medium-emphasized m-0 text-on-surface">
18
+ {{ t(`gallery.onboarding.pages.${slide.id}.title`) }}
19
+ </h2>
20
+ <p class="type-body-large m-0 max-w-80 text-on-surface-variant">
21
+ {{ t(`gallery.onboarding.pages.${slide.id}.text`) }}
22
+ </p>
23
+ </div>
24
+ </M3PagerPage>
25
+
26
+ <template #footer="{ page: current, count, progress, go, next }">
27
+ <div class="flex items-center justify-between gap-2 px-4 pb-6 pt-2">
28
+ <M3Button
29
+ variant="text"
30
+ :class="{ invisible: current === count - 1 }"
31
+ @click="go(count - 1)"
32
+ >
33
+ {{ t("gallery.onboarding.skip") }}
34
+ </M3Button>
35
+ <M3PageIndicator
36
+ :count="count"
37
+ :progress="progress"
38
+ :label="t('gallery.onboarding.title')"
39
+ :page-label="pageLabel"
40
+ @select="go"
41
+ />
42
+ <M3Button v-if="current < count - 1" @click="next">
43
+ {{ t("gallery.onboarding.next") }}
44
+ </M3Button>
45
+ <M3Button v-else @click="finish">{{ t("gallery.onboarding.start") }}</M3Button>
46
+ </div>
47
+ </template>
48
+ </M3Pager>
49
+ </AppPage>
50
+ </template>
51
+
52
+ <script setup lang="ts">
53
+ import type { MaterialShapeName } from "@cavulsqa/m3e";
54
+ import type { Router } from "framework7/types";
55
+ import { markRaw, type Component } from "vue";
56
+ import OfflineIcon from "~icons/material-symbols/cloud-off-outline-rounded";
57
+ import RouteIcon from "~icons/material-symbols/route-outline-rounded";
58
+ import ReceiptIcon from "~icons/material-symbols/receipt-long-outline-rounded";
59
+
60
+ interface Slide {
61
+ id: string;
62
+ shape: MaterialShapeName;
63
+ tone: string;
64
+ icon: Component;
65
+ }
66
+
67
+ const SLIDES: readonly Slide[] = [
68
+ {
69
+ id: "route",
70
+ shape: "cookie9Sided",
71
+ tone: "bg-primary-container text-on-primary-container",
72
+ icon: markRaw(RouteIcon),
73
+ },
74
+ {
75
+ id: "orders",
76
+ shape: "sunny",
77
+ tone: "bg-tertiary-container text-on-tertiary-container",
78
+ icon: markRaw(ReceiptIcon),
79
+ },
80
+ {
81
+ id: "offline",
82
+ shape: "clover4Leaf",
83
+ tone: "bg-secondary-container text-on-secondary-container",
84
+ icon: markRaw(OfflineIcon),
85
+ },
86
+ ];
87
+
88
+ const props = defineProps<{ f7router: Router.Router }>();
89
+ const { t } = useI18n();
90
+ const page = ref(0);
91
+
92
+ function pageLabel(n: number) {
93
+ return t("gallery.onboarding.page", { n });
94
+ }
95
+
96
+ function finish() {
97
+ props.f7router.back();
98
+ }
99
+ </script>
100
+
101
+ <style scoped>
102
+ :global(.gallery-onboarding .page-content) {
103
+ display: flex;
104
+ flex-direction: column;
105
+ padding-bottom: env(safe-area-inset-bottom);
106
+ }
107
+
108
+ .gallery-onboarding__pager {
109
+ flex: 1;
110
+ min-height: 0;
111
+ }
112
+
113
+ .gallery-onboarding__pager :deep(.m3-pager__track) {
114
+ flex: 1;
115
+ }
116
+ </style>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <AppPage :title="entry ? t(entry.titleKey) : t('errors.notFoundTitle')" variant="medium" back>
3
+ <Suspense v-if="entry">
4
+ <component :is="entry.component" />
5
+ <template #fallback>
6
+ <div class="grid place-items-center py-16">
7
+ <M3LoadingIndicator :label="t('gallery.loading')" />
8
+ </div>
9
+ </template>
10
+ </Suspense>
11
+ <EmptyState
12
+ v-else
13
+ shape="ghostish"
14
+ :headline="t('errors.notFoundTitle')"
15
+ :text="t('errors.notFound')"
16
+ />
17
+ </AppPage>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import type { Router } from "framework7/types";
22
+ import { findSection } from "@/modules/gallery/composables/useGallerySections";
23
+
24
+ const props = defineProps<{ f7route: Router.Route }>();
25
+ const { t } = useI18n();
26
+ const entry = computed(() => findSection(String(props.f7route.params.section ?? "")));
27
+ </script>
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <AppPage :title="t('gallery.tabs.title')" back variant="small">
3
+ <template #bottom>
4
+ <M3Tabs v-model="tab" :pager="pager" :label="t('gallery.tabs.title')">
5
+ <M3Tab v-for="item in TABS" :key="item" :value="item" :label="t(`gallery.tabs.${item}`)" />
6
+ </M3Tabs>
7
+ </template>
8
+
9
+ <M3TabPanels v-model="tab" :pager="pager" class="gallery-tabs__panels">
10
+ <M3TabPanel value="orders" :label="t('gallery.tabs.orders')">
11
+ <p class="type-body-medium m-0 px-6 py-3 text-on-surface-variant">
12
+ {{ t("gallery.tabs.ordersNote") }}
13
+ </p>
14
+ <M3List variant="segmented" inset>
15
+ <M3ListItem
16
+ v-for="row in 18"
17
+ :key="row"
18
+ :headline="t('gallery.tabs.order', { n: 1000 + row })"
19
+ :supporting="t('gallery.tabs.orderLines', { count: (row % 5) + 1 }, (row % 5) + 1)"
20
+ >
21
+ <template #leading><i-ms-receipt-long-outline-rounded /></template>
22
+ <template #swipe-end>
23
+ <M3SwipeAction :label="t('gallery.tabs.archive')" tone="secondary">
24
+ <i-ms-archive-outline-rounded />
25
+ </M3SwipeAction>
26
+ </template>
27
+ </M3ListItem>
28
+ </M3List>
29
+ </M3TabPanel>
30
+
31
+ <M3TabPanel value="invoices" :label="t('gallery.tabs.invoices')">
32
+ <M3List variant="segmented" inset class="py-3">
33
+ <M3ListItem
34
+ v-for="row in 24"
35
+ :key="row"
36
+ :headline="t('gallery.tabs.invoice', { n: 2000 + row })"
37
+ :trailing-text="t('gallery.tabs.amount', { value: (row * 3150).toLocaleString() })"
38
+ >
39
+ <template #leading><i-ms-request-quote-outline-rounded /></template>
40
+ </M3ListItem>
41
+ </M3List>
42
+ </M3TabPanel>
43
+
44
+ <M3TabPanel value="payments" :label="t('gallery.tabs.payments')">
45
+ <p class="type-body-medium m-0 px-6 py-3 text-on-surface-variant">
46
+ {{ t("gallery.tabs.paymentsNote") }}
47
+ </p>
48
+ <M3Carousel
49
+ :items="METHODS"
50
+ :item-key="methodKey"
51
+ :label="t('gallery.tabs.payments')"
52
+ :height="160"
53
+ >
54
+ <template #default="{ item }">
55
+ <div
56
+ class="type-title-medium-emphasized grid size-full place-items-center bg-secondary-container text-on-secondary-container"
57
+ >
58
+ {{ t(`gallery.tabs.methods.${item}`) }}
59
+ </div>
60
+ </template>
61
+ </M3Carousel>
62
+ </M3TabPanel>
63
+
64
+ <M3TabPanel value="returns" :label="t('gallery.tabs.returns')">
65
+ <EmptyState
66
+ shape="ghostish"
67
+ :headline="t('gallery.tabs.noReturns')"
68
+ :text="t('gallery.tabs.noReturnsText')"
69
+ />
70
+ </M3TabPanel>
71
+ </M3TabPanels>
72
+
73
+ <template #fab>
74
+ <M3FabMorph
75
+ v-model:open="actionsOpen"
76
+ variant="toolbar"
77
+ :label="t('gallery.tabs.quickActions')"
78
+ >
79
+ <template #icon><i-ms-bolt-outline-rounded /></template>
80
+ <template #default="{ close }">
81
+ <M3IconButton
82
+ v-for="action in ACTIONS"
83
+ :key="action.id"
84
+ :label="t(`gallery.tabs.actions.${action.id}`)"
85
+ @click="run(action.id, close)"
86
+ >
87
+ <component :is="action.icon" />
88
+ </M3IconButton>
89
+ </template>
90
+ </M3FabMorph>
91
+ </template>
92
+ </AppPage>
93
+ </template>
94
+
95
+ <script setup lang="ts">
96
+ import { createTabPager } from "@cavulsqa/m3e-vue";
97
+ import { markRaw } from "vue";
98
+ import AddIcon from "~icons/material-symbols/add-shopping-cart-rounded";
99
+ import CallIcon from "~icons/material-symbols/call-outline-rounded";
100
+ import CloseIcon from "~icons/material-symbols/close-rounded";
101
+ import MapIcon from "~icons/material-symbols/map-outline-rounded";
102
+ import ScanIcon from "~icons/material-symbols/barcode-scanner-rounded";
103
+
104
+ const TABS = ["orders", "invoices", "payments", "returns"] as const;
105
+ const METHODS = ["cash", "cheque", "transfer", "credit"] as const;
106
+
107
+ const { t } = useI18n();
108
+ const tab = ref<string>("orders");
109
+ const pager = createTabPager();
110
+ const snackbar = useSnackbar();
111
+ const actionsOpen = ref(false);
112
+
113
+ const ACTIONS = [
114
+ { id: "order", icon: markRaw(AddIcon) },
115
+ { id: "scan", icon: markRaw(ScanIcon) },
116
+ { id: "call", icon: markRaw(CallIcon) },
117
+ { id: "map", icon: markRaw(MapIcon) },
118
+ { id: "close", icon: markRaw(CloseIcon) },
119
+ ] as const;
120
+
121
+ function run(id: (typeof ACTIONS)[number]["id"], close: () => void) {
122
+ close();
123
+ if (id !== "close") void snackbar.show(t(`gallery.tabs.actions.${id}`));
124
+ }
125
+
126
+ function methodKey(method: (typeof METHODS)[number]) {
127
+ return method;
128
+ }
129
+ </script>
130
+
131
+ <style scoped>
132
+ .gallery-tabs__panels {
133
+ height: calc(100dvh - env(safe-area-inset-top) - 112px);
134
+ }
135
+ </style>
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <AppPage :title="t('gallery.title')" name="gallery">
3
+ <p class="type-body-large m-0 px-6 pb-2 text-on-surface-variant">{{ t("gallery.intro") }}</p>
4
+ <M3List variant="segmented" inset :label="t('gallery.title')">
5
+ <M3ListItem
6
+ v-for="entry in entries"
7
+ :key="entry.id"
8
+ clickable
9
+ :headline="t(entry.titleKey)"
10
+ :supporting="t(entry.subtitleKey)"
11
+ @click="f7router.navigate(entry.path ?? `/gallery/${entry.id}/`)"
12
+ >
13
+ <template #leading>
14
+ <M3Shape :shape="entry.shape" class="size-10" :class="TONE_CLASSES[entry.tone]">
15
+ <component :is="entry.icon" class="size-5" />
16
+ </M3Shape>
17
+ </template>
18
+ <template #trailing><i-ms-chevron-right-rounded class="rtl:-scale-x-100" /></template>
19
+ </M3ListItem>
20
+ </M3List>
21
+ </AppPage>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import type { Router } from "framework7/types";
26
+ import {
27
+ AGENDA_SECTION,
28
+ CHAT_SECTION,
29
+ CONTACTS_SECTION,
30
+ LOGIN_SECTION,
31
+ ONBOARDING_SECTION,
32
+ TABS_SECTION,
33
+ sections,
34
+ } from "@/modules/gallery/composables/useGallerySections";
35
+ import { TONE_CLASSES } from "@/shared/utils/tone";
36
+
37
+ defineProps<{ f7router: Router.Router }>();
38
+ const { t } = useI18n();
39
+ const entries = [
40
+ ...sections.slice(0, 5),
41
+ TABS_SECTION,
42
+ ...sections.slice(5, 9),
43
+ CONTACTS_SECTION,
44
+ CHAT_SECTION,
45
+ ONBOARDING_SECTION,
46
+ LOGIN_SECTION,
47
+ AGENDA_SECTION,
48
+ ...sections.slice(9),
49
+ ];
50
+ </script>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <section
3
+ class="mx-4 mt-2 grid grid-cols-[1fr_auto] items-center gap-4 rounded-xl bg-primary-container p-6 text-on-primary-container"
4
+ >
5
+ <div class="flex min-w-0 flex-col gap-2">
6
+ <span class="type-label-large text-on-primary-container/80">{{ t("home.overline") }}</span>
7
+ <h2 class="type-headline-medium-emphasized font-rounded m-0">{{ t("home.headline") }}</h2>
8
+ <p class="type-body-medium m-0 text-on-primary-container/80">
9
+ {{ t("home.intro", { app: appName }) }}
10
+ </p>
11
+ </div>
12
+ <button
13
+ type="button"
14
+ class="m3-focus-ring grid size-28 place-items-center rounded-full text-primary"
15
+ :aria-label="t('home.morph')"
16
+ @click="next"
17
+ >
18
+ <M3ShapeMorph :shape="shapes[index]!" :rotate="index * 45" class="size-28">
19
+ <i-ms-auto-awesome-outline-rounded class="size-10 text-on-primary" />
20
+ </M3ShapeMorph>
21
+ </button>
22
+ <M3ButtonGroup
23
+ class="col-span-2 mt-2 flex-wrap max-[600px]:[&>.m3-button]:grow"
24
+ size="m"
25
+ :label="t('home.actions')"
26
+ >
27
+ <M3Button @click="emit('explore')">{{ t("home.explore") }}</M3Button>
28
+ <M3Button variant="tonal" @click="emit('personalise')">{{ t("home.personalise") }}</M3Button>
29
+ </M3ButtonGroup>
30
+ </section>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import type { MaterialShapeName } from "@cavulsqa/m3e";
35
+
36
+ /**
37
+ * The first thing the app shows: what it is in one line, a shape that morphs when touched - the
38
+ * Expressive idea in one gesture - and the two places worth going next.
39
+ */
40
+ const emit = defineEmits<{ explore: []; personalise: [] }>();
41
+ const { t } = useI18n();
42
+ const appName = __APP_NAME__;
43
+
44
+ const shapes: readonly MaterialShapeName[] = [
45
+ "cookie9Sided",
46
+ "clover4Leaf",
47
+ "sunny",
48
+ "pentagon",
49
+ "softBurst",
50
+ "flower",
51
+ ];
52
+ const index = ref(0);
53
+
54
+ function next() {
55
+ index.value = (index.value + 1) % shapes.length;
56
+ }
57
+ </script>