@cavulsqa/create 2.9.2 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +12 -1
  2. package/bin/create.mjs +18 -25
  3. package/lib/templates.mjs +56 -0
  4. package/package.json +13 -5
  5. package/templates/f7-app/CLAUDE.md +3 -0
  6. package/templates/f7-app/auto-imports.d.ts +15 -0
  7. package/templates/f7-app/package.json +6 -3
  8. package/templates/f7-app/src/App.vue +1 -0
  9. package/templates/f7-app/src/env.d.ts +1 -0
  10. package/templates/f7-app/src/main.ts +3 -0
  11. package/templates/f7-app/src/modules/demo/router/routes/demo.routes.ts +4 -16
  12. package/templates/f7-app/src/modules/home/router/routes/home.routes.ts +3 -11
  13. package/templates/f7-app/src/modules/settings/router/routes/settings.routes.ts +2 -6
  14. package/templates/f7-app/src/plugins/bootstrapError.ts +2 -0
  15. package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
  16. package/templates/f7-app/src/router/global/global.routes.ts +2 -6
  17. package/templates/f7-app/src/shared/composables/useNavigationGuard.ts +48 -0
  18. package/templates/f7-app/src/shared/recorder/capacitorSink.ts +124 -0
  19. package/templates/f7-app/src/shared/utils/lazyRoute.ts +25 -0
  20. package/templates/f7-app/tests/lazyRoute.test.ts +30 -0
  21. package/templates/f7-app/tests/navigationGuard.test.ts +82 -0
  22. package/templates/f7-app/vite.config.ts +1 -0
  23. package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
  24. package/templates/m3e-app/.claude/rules/database.md +106 -0
  25. package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
  26. package/templates/m3e-app/.claude/rules/modules.md +43 -0
  27. package/templates/m3e-app/.claude/rules/native.md +60 -0
  28. package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
  29. package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
  30. package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
  31. package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
  32. package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
  33. package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
  34. package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
  35. package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
  36. package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
  37. package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
  38. package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
  39. package/templates/m3e-app/.env.example +32 -0
  40. package/templates/m3e-app/CLAUDE.md +93 -0
  41. package/templates/m3e-app/auto-imports.d.ts +851 -0
  42. package/templates/m3e-app/capacitor.config.ts +44 -0
  43. package/templates/m3e-app/components.d.ts +252 -0
  44. package/templates/m3e-app/index.html +16 -0
  45. package/templates/m3e-app/package.json +107 -0
  46. package/templates/m3e-app/src/App.vue +129 -0
  47. package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
  48. package/templates/m3e-app/src/app/scroll.config.ts +20 -0
  49. package/templates/m3e-app/src/app/storage.config.ts +65 -0
  50. package/templates/m3e-app/src/app/tabs.ts +33 -0
  51. package/templates/m3e-app/src/app/theme.config.ts +51 -0
  52. package/templates/m3e-app/src/assets/css/app.css +18 -0
  53. package/templates/m3e-app/src/assets/css/base.css +53 -0
  54. package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
  55. package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
  56. package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
  57. package/templates/m3e-app/src/assets/css/m3e.css +6 -0
  58. package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
  59. package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
  60. package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
  61. package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
  62. package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
  63. package/templates/m3e-app/src/env.d.ts +40 -0
  64. package/templates/m3e-app/src/locales/ar.json +1260 -0
  65. package/templates/m3e-app/src/locales/en.json +1260 -0
  66. package/templates/m3e-app/src/locales/fr.json +1260 -0
  67. package/templates/m3e-app/src/main.ts +41 -0
  68. package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
  69. package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
  70. package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
  71. package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
  72. package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
  73. package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
  74. package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
  75. package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
  76. package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
  77. package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
  78. package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
  79. package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
  80. package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
  81. package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
  82. package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
  83. package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
  84. package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
  85. package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
  86. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
  87. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
  88. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
  89. package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
  90. package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
  91. package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
  92. package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
  93. package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
  94. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
  95. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
  96. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
  97. package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
  98. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
  99. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
  100. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
  101. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
  102. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
  103. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
  104. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
  105. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
  106. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
  107. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
  108. package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
  109. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
  110. package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
  111. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
  112. package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
  113. package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
  114. package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
  115. package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
  116. package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
  117. package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
  118. package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
  119. package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
  120. package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
  121. package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
  122. package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
  123. package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
  124. package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
  125. package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
  126. package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
  127. package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
  128. package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
  129. package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
  130. package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
  131. package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
  132. package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
  133. package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
  134. package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
  135. package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
  136. package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
  137. package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
  138. package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
  139. package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
  140. package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
  141. package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
  142. package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
  143. package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
  144. package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
  145. package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
  146. package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
  147. package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
  148. package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
  149. package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
  150. package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
  151. package/templates/m3e-app/src/modules/settings/types.ts +7 -0
  152. package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
  153. package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
  154. package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
  155. package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
  156. package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
  157. package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
  158. package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
  159. package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
  160. package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
  161. package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
  162. package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
  163. package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
  164. package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
  165. package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
  166. package/templates/m3e-app/src/router/index.ts +20 -0
  167. package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
  168. package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
  169. package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
  170. package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
  171. package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
  172. package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
  173. package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
  174. package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
  175. package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
  176. package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
  177. package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
  178. package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
  179. package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
  180. package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
  181. package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
  182. package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
  183. package/templates/m3e-app/src/shared/database/database.ts +219 -0
  184. package/templates/m3e-app/src/shared/database/index.ts +3 -0
  185. package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
  186. package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
  187. package/templates/m3e-app/src/shared/database/queries.ts +26 -0
  188. package/templates/m3e-app/src/shared/database/schema.ts +153 -0
  189. package/templates/m3e-app/src/shared/database/storage.ts +64 -0
  190. package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
  191. package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
  192. package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
  193. package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
  194. package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
  195. package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
  196. package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
  197. package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
  198. package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
  199. package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
  200. package/templates/m3e-app/tests/locales.test.ts +73 -0
  201. package/templates/m3e-app/tests/migrations.test.ts +74 -0
  202. package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
  203. package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
  204. package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
  205. package/templates/m3e-app/tests/storage.test.ts +117 -0
  206. package/templates/m3e-app/tests/textDirection.test.ts +11 -0
  207. package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
  208. package/templates/m3e-app/tsconfig.json +21 -0
  209. package/templates/m3e-app/vite.config.ts +144 -0
@@ -0,0 +1,128 @@
1
+ import type { MaterialShapeName } from "@cavulsqa/m3e";
2
+ import type { Component } from "vue";
3
+ import BackIcon from "~icons/material-symbols/arrow-back-rounded";
4
+ import BoltIcon from "~icons/material-symbols/bolt-rounded";
5
+ import DatabaseIcon from "~icons/material-symbols/database-outline-rounded";
6
+ import KeyboardIcon from "~icons/material-symbols/keyboard-outline-rounded";
7
+ import MonitoringIcon from "~icons/material-symbols/monitoring-rounded";
8
+ import PaletteIcon from "~icons/material-symbols/palette-outline-rounded";
9
+ import ShapesIcon from "~icons/material-symbols/shapes-outline-rounded";
10
+ import WidgetsIcon from "~icons/material-symbols/widgets-outline-rounded";
11
+ import type { Tone } from "@/shared/utils/tone";
12
+
13
+ /**
14
+ * Framework7's own page transitions, one per feature so each push looks different. They ship in
15
+ * Framework7's core stylesheet, and Framework7 replays the one a page opened with when it goes back.
16
+ */
17
+ export type PageTransition =
18
+ | "f7-circle"
19
+ | "f7-cover"
20
+ | "f7-cover-v"
21
+ | "f7-dive"
22
+ | "f7-fade"
23
+ | "f7-flip"
24
+ | "f7-parallax"
25
+ | "f7-push";
26
+
27
+ export interface HomeFeature {
28
+ id: string;
29
+ icon: Component;
30
+ shape: MaterialShapeName;
31
+ tone: Tone;
32
+ titleKey: string;
33
+ subtitleKey: string;
34
+ textKey: string;
35
+ transition: PageTransition;
36
+ }
37
+
38
+ /**
39
+ * The template's capabilities as data, so removing a layer means removing an entry and the screen
40
+ * never advertises something the generator left out. Each one opens inside the Home tab's own
41
+ * view, which is the nesting being demonstrated: the push stays in this tab's history.
42
+ */
43
+ export const features: readonly HomeFeature[] = [
44
+ {
45
+ id: "expressive",
46
+ icon: ShapesIcon,
47
+ shape: "cookie9Sided",
48
+ tone: "primary",
49
+ titleKey: "features.expressive.title",
50
+ subtitleKey: "features.expressive.subtitle",
51
+ textKey: "features.expressive.text",
52
+ transition: "f7-cover",
53
+ },
54
+ {
55
+ id: "color",
56
+ icon: PaletteIcon,
57
+ shape: "clover4Leaf",
58
+ tone: "tertiary",
59
+ titleKey: "features.color.title",
60
+ subtitleKey: "features.color.subtitle",
61
+ textKey: "features.color.text",
62
+ transition: "f7-circle",
63
+ },
64
+ {
65
+ id: "shell",
66
+ icon: WidgetsIcon,
67
+ shape: "pentagon",
68
+ tone: "secondary",
69
+ titleKey: "features.shell.title",
70
+ subtitleKey: "features.shell.subtitle",
71
+ textKey: "features.shell.text",
72
+ transition: "f7-parallax",
73
+ },
74
+ {
75
+ id: "sqlite",
76
+ icon: DatabaseIcon,
77
+ shape: "cookie4Sided",
78
+ tone: "primary",
79
+ titleKey: "features.sqlite.title",
80
+ subtitleKey: "features.sqlite.subtitle",
81
+ textKey: "features.sqlite.text",
82
+ transition: "f7-push",
83
+ },
84
+ {
85
+ id: "reactive",
86
+ icon: BoltIcon,
87
+ shape: "sunny",
88
+ tone: "tertiary",
89
+ titleKey: "features.reactive.title",
90
+ subtitleKey: "features.reactive.subtitle",
91
+ textKey: "features.reactive.text",
92
+ transition: "f7-dive",
93
+ },
94
+ {
95
+ id: "back",
96
+ icon: BackIcon,
97
+ shape: "pill",
98
+ tone: "secondary",
99
+ titleKey: "features.back.title",
100
+ subtitleKey: "features.back.subtitle",
101
+ textKey: "features.back.text",
102
+ transition: "f7-flip",
103
+ },
104
+ {
105
+ id: "keyboard",
106
+ icon: KeyboardIcon,
107
+ shape: "gem",
108
+ tone: "primary",
109
+ titleKey: "features.keyboard.title",
110
+ subtitleKey: "features.keyboard.subtitle",
111
+ textKey: "features.keyboard.text",
112
+ transition: "f7-fade",
113
+ },
114
+ {
115
+ id: "metrics",
116
+ icon: MonitoringIcon,
117
+ shape: "flower",
118
+ tone: "tertiary",
119
+ titleKey: "features.metrics.title",
120
+ subtitleKey: "features.metrics.subtitle",
121
+ textKey: "features.metrics.text",
122
+ transition: "f7-cover-v",
123
+ },
124
+ ];
125
+
126
+ export function findFeature(id: string): HomeFeature | undefined {
127
+ return features.find((feature) => feature.id === id);
128
+ }
@@ -0,0 +1,22 @@
1
+ import type { Router } from "framework7/types";
2
+ import type { HomeFeature } from "@/modules/home/composables/useHomeFeatures";
3
+
4
+ /** The transition's name without Framework7's prefix, as the rows and the detail page show it. */
5
+ export function transitionName(feature: HomeFeature): string {
6
+ return feature.transition.replace(/^f7-/, "");
7
+ }
8
+
9
+ /**
10
+ * Opens a feature's page inside the Home tab with that feature's own transition. Under reduced
11
+ * motion it falls back to the app's default, which is a cross-fade there: a flip or a circle reveal
12
+ * is exactly the movement reduced motion asks to remove.
13
+ */
14
+ export function useOpenFeature() {
15
+ const reduced = useReducedMotion();
16
+
17
+ return (router: Router.Router, feature: HomeFeature) =>
18
+ router.navigate(
19
+ `/home/feature/${feature.id}/`,
20
+ reduced.value ? {} : { transition: feature.transition },
21
+ );
22
+ }
@@ -0,0 +1,21 @@
1
+ import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
3
+
4
+ /**
5
+ * The detail route is a child of the tab's own view, which is what keeps the tab bar in place and
6
+ * the back gesture scoped to this tab's history.
7
+ */
8
+ const homeRoutes: Router.RouteParameters[] = [
9
+ {
10
+ name: "home",
11
+ path: "/home/",
12
+ async: lazyRoute(() => import("@/modules/home/views/HomeView.vue")),
13
+ },
14
+ {
15
+ name: "home-feature",
16
+ path: "/home/feature/:id/",
17
+ async: lazyRoute(() => import("@/modules/home/views/FeatureDetailView.vue")),
18
+ },
19
+ ];
20
+
21
+ export default homeRoutes;
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <AppPage :title="feature ? t(feature.titleKey) : t('errors.notFoundTitle')" variant="medium" back>
3
+ <template v-if="feature">
4
+ <section class="flex flex-col items-center gap-4 px-6 pt-4 pb-2 text-center">
5
+ <M3Shape :shape="feature.shape" class="size-32" :class="TONE_CLASSES[feature.tone]">
6
+ <component :is="feature.icon" class="size-12" />
7
+ </M3Shape>
8
+ <p class="type-title-medium m-0 text-on-surface-variant">{{ t(feature.subtitleKey) }}</p>
9
+ <p class="type-body-large m-0 max-w-[36rem] text-on-surface">{{ t(feature.textKey) }}</p>
10
+ </section>
11
+
12
+ <SectionHeader :title="t('detail.thisScreen')" />
13
+ <M3List variant="segmented" inset>
14
+ <M3ListItem
15
+ :headline="t('detail.transition')"
16
+ :supporting="t('detail.transitionNote', { name: transitionName(feature) })"
17
+ :trailing-text="transitionName(feature)"
18
+ multiline
19
+ >
20
+ <template #leading><i-ms-swipe-rounded /></template>
21
+ </M3ListItem>
22
+ <M3ListItem :headline="t('detail.appBar')" :supporting="t('detail.appBarNote')" multiline>
23
+ <template #leading><i-ms-web-rounded /></template>
24
+ </M3ListItem>
25
+ <M3ListItem :headline="t('detail.nesting')" :supporting="t('detail.nestingNote')" multiline>
26
+ <template #leading><i-ms-widgets-outline-rounded /></template>
27
+ </M3ListItem>
28
+ </M3List>
29
+
30
+ <div class="flex justify-end px-4 pt-6">
31
+ <M3Button variant="tonal" size="m" @click="openNext">
32
+ {{ t("detail.next") }}
33
+ <template #trailing><i-ms-chevron-right-rounded class="rtl:-scale-x-100" /></template>
34
+ </M3Button>
35
+ </div>
36
+ </template>
37
+
38
+ <EmptyState
39
+ v-else
40
+ shape="ghostish"
41
+ :headline="t('errors.notFoundTitle')"
42
+ :text="t('errors.notFound')"
43
+ />
44
+ </AppPage>
45
+ </template>
46
+
47
+ <script setup lang="ts">
48
+ import type { Router } from "framework7/types";
49
+ import { features, findFeature } from "@/modules/home/composables/useHomeFeatures";
50
+ import { transitionName, useOpenFeature } from "@/modules/home/composables/useOpenFeature";
51
+ import { TONE_CLASSES } from "@/shared/utils/tone";
52
+
53
+ const { t } = useI18n();
54
+ const props = defineProps<{ f7route: Router.Route; f7router: Router.Router }>();
55
+
56
+ const openFeature = useOpenFeature();
57
+
58
+ const feature = computed(() => findFeature(String(props.f7route.params.id ?? "")));
59
+
60
+ /** Pushes the next feature onto this tab's own history, so the stack grows inside the tab. */
61
+ function openNext() {
62
+ const index = features.findIndex((entry) => entry.id === feature.value?.id);
63
+ const next = features[(index + 1) % features.length];
64
+ if (next) openFeature(props.f7router, next);
65
+ }
66
+ </script>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <AppPage :title="t('home.title')" name="home">
3
+ <template #actions>
4
+ <M3IconButton :label="t('home.studio')" @click="openStudio">
5
+ <i-ms-palette-outline-rounded />
6
+ </M3IconButton>
7
+ </template>
8
+
9
+ <HomeHero @explore="tabs.show('gallery')" @personalise="openStudio" />
10
+
11
+ <SectionHeader :title="t('home.included')" />
12
+ <M3List variant="segmented" inset :label="t('home.included')">
13
+ <M3ListItem
14
+ v-for="feature in features"
15
+ :key="feature.id"
16
+ clickable
17
+ :headline="t(feature.titleKey)"
18
+ :supporting="t(feature.subtitleKey)"
19
+ :trailing-text="transitionName(feature)"
20
+ @click="openFeature(f7router, feature)"
21
+ >
22
+ <template #leading>
23
+ <M3Shape :shape="feature.shape" class="size-10" :class="TONE_CLASSES[feature.tone]">
24
+ <component :is="feature.icon" class="size-5" />
25
+ </M3Shape>
26
+ </template>
27
+ <template #trailing><i-ms-chevron-right-rounded class="rtl:-scale-x-100" /></template>
28
+ </M3ListItem>
29
+ </M3List>
30
+ </AppPage>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import type { Router } from "framework7/types";
35
+ import HomeHero from "@/modules/home/components/HomeHero.vue";
36
+ import { features } from "@/modules/home/composables/useHomeFeatures";
37
+ import { transitionName, useOpenFeature } from "@/modules/home/composables/useOpenFeature";
38
+ import { TONE_CLASSES } from "@/shared/utils/tone";
39
+
40
+ const { t } = useI18n();
41
+ defineProps<{ f7router: Router.Router }>();
42
+ const tabs = useActiveTab();
43
+ const openFeature = useOpenFeature();
44
+ function openStudio() {
45
+ tabs.open("settings", "/settings/studio/");
46
+ }
47
+ </script>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-2 px-4">
3
+ <div
4
+ v-for="role in ROLES"
5
+ :key="role.name"
6
+ class="flex min-h-18 flex-col justify-between rounded-md p-3"
7
+ :style="{
8
+ background: `var(--md-sys-color-${role.name})`,
9
+ color: `var(--md-sys-color-${role.on})`,
10
+ }"
11
+ >
12
+ <span class="type-label-large">{{ role.name }}</span>
13
+ <span class="type-label-small opacity-80">{{ role.on }}</span>
14
+ </div>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ /**
20
+ * The generated roles in pairs - each container with the content colour that must stay legible on
21
+ * it. A pair that reads poorly here reads poorly everywhere; raise the contrast level, not the
22
+ * individual colour.
23
+ */
24
+ const ROLES = [
25
+ { name: "primary", on: "on-primary" },
26
+ { name: "primary-container", on: "on-primary-container" },
27
+ { name: "secondary", on: "on-secondary" },
28
+ { name: "secondary-container", on: "on-secondary-container" },
29
+ { name: "tertiary", on: "on-tertiary" },
30
+ { name: "tertiary-container", on: "on-tertiary-container" },
31
+ { name: "error-container", on: "on-error-container" },
32
+ { name: "success-container", on: "on-success-container" },
33
+ { name: "surface-container-low", on: "on-surface" },
34
+ { name: "surface-container-highest", on: "on-surface-variant" },
35
+ { name: "inverse-surface", on: "inverse-on-surface" },
36
+ { name: "primary-fixed", on: "on-primary-fixed" },
37
+ ] as const;
38
+ </script>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <div class="grid grid-cols-5 gap-3 px-4" role="radiogroup" :aria-label="t('studio.seed')">
3
+ <button
4
+ v-for="preset in SEED_PRESETS"
5
+ :key="preset.id"
6
+ type="button"
7
+ role="radio"
8
+ class="m3-focus-ring grid aspect-square place-items-center rounded-full"
9
+ :aria-checked="isSelected(preset.seed)"
10
+ :aria-label="t(preset.labelKey)"
11
+ :style="{ color: preset.seed }"
12
+ @click="emit('select', preset.seed)"
13
+ >
14
+ <M3ShapeMorph
15
+ :shape="isSelected(preset.seed) ? 'cookie9Sided' : 'circle'"
16
+ :rotate="isSelected(preset.seed) ? 30 : 0"
17
+ class="size-full max-w-14"
18
+ >
19
+ <i-ms-check-rounded
20
+ class="size-6 text-white transition-opacity duration-spring-fast ease-fast-effects"
21
+ :class="isSelected(preset.seed) ? 'opacity-100' : 'opacity-0'"
22
+ />
23
+ </M3ShapeMorph>
24
+ </button>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { SEED_PRESETS } from "@/app/theme.config";
30
+
31
+ /**
32
+ * The preset seeds as shapes that morph from a circle into a cookie when chosen. The swatch is the
33
+ * raw seed, the one place a hex value reaches the screen, because the point is to show the input
34
+ * before Material turns it into a scheme.
35
+ */
36
+ const props = defineProps<{ selected: string }>();
37
+ const emit = defineEmits<{ select: [seed: string] }>();
38
+ const { t } = useI18n();
39
+
40
+ const isSelected = (seed: string) => seed.toLowerCase() === props.selected.toLowerCase();
41
+ </script>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <div
3
+ class="flex flex-col gap-3 rounded-xs bg-surface-container-low px-4 py-4 first:rounded-t-lg last:rounded-b-lg"
4
+ >
5
+ <div class="flex items-center gap-3">
6
+ <span class="text-on-surface-variant [&>svg]:size-6"><slot name="icon" /></span>
7
+ <div class="flex min-w-0 flex-col">
8
+ <span class="type-body-large text-on-surface">{{ props.label }}</span>
9
+ <span v-if="props.supporting" class="type-body-medium text-on-surface-variant">{{
10
+ props.supporting
11
+ }}</span>
12
+ </div>
13
+ </div>
14
+ <M3ButtonGroup variant="connected" size="s" :label="props.label">
15
+ <M3Button
16
+ v-for="option in props.options"
17
+ :key="option.value"
18
+ variant="tonal"
19
+ toggle
20
+ :selected="model === option.value"
21
+ @update:selected="model = option.value"
22
+ >
23
+ <template v-if="option.icon" #icon><component :is="option.icon" /></template>
24
+ {{ option.label }}
25
+ </M3Button>
26
+ </M3ButtonGroup>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup lang="ts" generic="T extends string">
31
+ import type { ChoiceOption } from "@/modules/settings/types";
32
+
33
+ /**
34
+ * One setting with a few exclusive choices: a label and a connected button group, the M3
35
+ * Expressive replacement for segmented buttons. Sits in a segmented stack with its siblings.
36
+ */
37
+ const props = defineProps<{
38
+ label: string;
39
+ supporting?: string;
40
+ options: readonly ChoiceOption<T>[];
41
+ }>();
42
+ const model = defineModel<T>({ required: true });
43
+ </script>
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <section
3
+ class="mx-4 flex flex-col gap-4 rounded-xl bg-surface-container-low p-5 text-on-surface shadow-1"
4
+ :aria-label="t('studio.preview')"
5
+ >
6
+ <header class="flex items-center gap-3">
7
+ <M3Shape shape="clover4Leaf" class="size-12 bg-tertiary-container text-on-tertiary-container">
8
+ <i-ms-auto-awesome-outline-rounded class="size-6" />
9
+ </M3Shape>
10
+ <div class="flex min-w-0 flex-1 flex-col">
11
+ <span class="type-title-medium-emphasized">{{ t("studio.previewTitle") }}</span>
12
+ <span class="type-body-medium text-on-surface-variant">{{ t("studio.previewText") }}</span>
13
+ </div>
14
+ <M3Switch v-model="on" :label="t('studio.previewSwitch')" />
15
+ </header>
16
+
17
+ <M3LinearProgress :value="0.62" :label="t('studio.previewProgress')" />
18
+
19
+ <div class="flex flex-wrap items-center gap-2">
20
+ <M3Chip
21
+ kind="filter"
22
+ :label="t('studio.previewChip')"
23
+ :selected="on"
24
+ @update:selected="on = $event"
25
+ />
26
+ <M3Chip :label="t('studio.previewAssist')">
27
+ <template #icon><i-ms-bolt-rounded /></template>
28
+ </M3Chip>
29
+ </div>
30
+
31
+ <div class="flex items-center gap-2">
32
+ <M3ButtonGroup size="s" :label="t('studio.preview')">
33
+ <M3Button>{{ t("studio.previewPrimary") }}</M3Button>
34
+ <M3Button variant="tonal">{{ t("studio.previewTonal") }}</M3Button>
35
+ </M3ButtonGroup>
36
+ <span class="flex-1" />
37
+ <M3LoadingIndicator contained :size="40" :label="t('studio.previewLoading')" />
38
+ <M3Fab size="small" :label="t('studio.previewFab')" color="tertiary-container">
39
+ <i-ms-add-rounded />
40
+ </M3Fab>
41
+ </div>
42
+ </section>
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ /**
47
+ * A handful of live components in the generated scheme, so a seed or variant change is judged on
48
+ * real surfaces - containers, an active wave, a selected chip - rather than on swatches alone.
49
+ */
50
+ const { t } = useI18n();
51
+ const on = ref(true);
52
+ </script>
@@ -0,0 +1,17 @@
1
+ import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
3
+
4
+ const settingsRoutes: Router.RouteParameters[] = [
5
+ {
6
+ name: "settings",
7
+ path: "/settings/",
8
+ async: lazyRoute(() => import("@/modules/settings/views/SettingsView.vue")),
9
+ },
10
+ {
11
+ name: "settings-studio",
12
+ path: "/settings/studio/",
13
+ async: lazyRoute(() => import("@/modules/settings/views/ColorStudioView.vue")),
14
+ },
15
+ ];
16
+
17
+ export default settingsRoutes;
@@ -0,0 +1,7 @@
1
+ import type { Component } from "vue";
2
+
3
+ export interface ChoiceOption<V extends string> {
4
+ value: V;
5
+ label: string;
6
+ icon?: Component;
7
+ }
@@ -0,0 +1,166 @@
1
+ <template>
2
+ <AppPage :title="t('studio.title')" variant="medium" back>
3
+ <template #actions>
4
+ <M3IconButton :label="t('studio.reset')" @click="reset">
5
+ <i-ms-restart-alt-rounded />
6
+ </M3IconButton>
7
+ </template>
8
+
9
+ <StudioPreview />
10
+
11
+ <SectionHeader :title="t('studio.seed')" />
12
+ <SeedSwatches :selected="settings.seed" @select="(seed) => theme.update({ seed })" />
13
+
14
+ <div class="flex items-start gap-2 px-4 pt-4">
15
+ <M3TextField
16
+ v-model="hex"
17
+ class="flex-1"
18
+ :label="t('studio.custom')"
19
+ :error="hexError"
20
+ :supporting="t('studio.customHint')"
21
+ autocapitalize="off"
22
+ autocomplete="off"
23
+ spellcheck="false"
24
+ >
25
+ <template #leading>
26
+ <span
27
+ class="block size-6 rounded-full"
28
+ :style="{ background: hexValid ? normalizedHex : 'transparent' }"
29
+ />
30
+ </template>
31
+ </M3TextField>
32
+ <M3IconButton
33
+ variant="tonal"
34
+ size="m"
35
+ class="mt-2"
36
+ :label="t('studio.pick')"
37
+ @click="openPicker"
38
+ >
39
+ <i-ms-tune-rounded />
40
+ </M3IconButton>
41
+ </div>
42
+
43
+ <template #fixed>
44
+ <M3BottomSheet v-model:open="pickerOpen" :title="t('studio.pick')">
45
+ <div class="flex flex-col gap-6 px-6 pb-6">
46
+ <M3ColorPicker
47
+ v-model="draftSeed"
48
+ :label="t('studio.pick')"
49
+ :hue-label="t('studio.hue')"
50
+ :chroma-label="t('studio.chroma')"
51
+ :tone-label="t('studio.tone')"
52
+ :hex-label="t('studio.hex')"
53
+ :invalid-text="t('studio.customInvalid')"
54
+ />
55
+ <p class="type-body-small m-0 text-on-surface-variant">{{ t("studio.pickNote") }}</p>
56
+ <div class="flex justify-end gap-2">
57
+ <M3Button variant="text" @click="pickerOpen = false">{{ t("studio.cancel") }}</M3Button>
58
+ <M3Button @click="applyPicked">{{ t("studio.apply") }}</M3Button>
59
+ </div>
60
+ </div>
61
+ </M3BottomSheet>
62
+ </template>
63
+
64
+ <SectionHeader :title="t('studio.variant')" />
65
+ <div class="flex flex-wrap gap-2 px-4" role="radiogroup" :aria-label="t('studio.variant')">
66
+ <M3Chip
67
+ v-for="variant in STUDIO_VARIANTS"
68
+ :key="variant"
69
+ kind="filter"
70
+ :label="t(`studio.variants.${variant}`)"
71
+ :selected="settings.variant === variant"
72
+ @update:selected="theme.update({ variant })"
73
+ />
74
+ </div>
75
+ <p class="type-body-small m-0 px-6 pt-2 text-on-surface-variant">
76
+ {{ effectiveSpec(settings.variant) === "2025" ? t("studio.spec2025") : t("studio.spec2021") }}
77
+ </p>
78
+
79
+ <div class="mx-4 mt-6">
80
+ <SettingsChoice
81
+ :model-value="settings.contrast"
82
+ :label="t('studio.contrast')"
83
+ :supporting="t('studio.contrastNote')"
84
+ :options="contrastOptions"
85
+ @update:model-value="(contrast) => theme.update({ contrast })"
86
+ >
87
+ <template #icon><i-ms-contrast-rounded /></template>
88
+ </SettingsChoice>
89
+ </div>
90
+
91
+ <SectionHeader :title="t('studio.roles')" />
92
+ <RolePalette />
93
+ </AppPage>
94
+ </template>
95
+
96
+ <script setup lang="ts">
97
+ import { effectiveSpec, isHexColor } from "@cavulsqa/m3e";
98
+ import { STUDIO_VARIANTS } from "@/app/theme.config";
99
+ import RolePalette from "@/modules/settings/components/RolePalette.vue";
100
+ import SeedSwatches from "@/modules/settings/components/SeedSwatches.vue";
101
+ import SettingsChoice from "@/modules/settings/components/SettingsChoice.vue";
102
+ import StudioPreview from "@/modules/settings/components/StudioPreview.vue";
103
+ import type { ChoiceOption } from "@/modules/settings/types";
104
+ import type { ContrastChoice } from "@/shared/utils/theme/themeSettings";
105
+
106
+ /**
107
+ * The colour studio: one seed, a scheme variant and a contrast level, and Material's 2025 colour
108
+ * system generates every role from them in both modes. Changes apply live and persist.
109
+ *
110
+ * @see https://m3.material.io/styles/color/choosing-a-scheme
111
+ */
112
+ const { t } = useI18n();
113
+ const theme = useThemeSettings();
114
+ const settings = computed(() => theme.settings.value);
115
+ const snackbar = useSnackbar();
116
+
117
+ const hex = ref(settings.value.seed);
118
+ const normalizedHex = computed(() => (hex.value.startsWith("#") ? hex.value : `#${hex.value}`));
119
+ const hexValid = computed(() => isHexColor(hex.value));
120
+ const hexError = computed(() =>
121
+ hex.value.length >= 4 && !hexValid.value ? t("studio.customInvalid") : undefined,
122
+ );
123
+
124
+ watch(normalizedHex, (value) => {
125
+ if (hexValid.value && value.toLowerCase() !== settings.value.seed.toLowerCase()) {
126
+ theme.update({ seed: value });
127
+ }
128
+ });
129
+
130
+ watch(
131
+ () => settings.value.seed,
132
+ (seed) => {
133
+ if (seed.toLowerCase() !== normalizedHex.value.toLowerCase()) hex.value = seed;
134
+ },
135
+ );
136
+
137
+ const contrastOptions = computed<ChoiceOption<ContrastChoice>[]>(() => [
138
+ { value: "standard", label: t("studio.contrastStandard") },
139
+ { value: "medium", label: t("studio.contrastMedium") },
140
+ { value: "high", label: t("studio.contrastHigh") },
141
+ ]);
142
+
143
+ const pickerOpen = ref(false);
144
+ const draftSeed = ref(settings.value.seed);
145
+
146
+ function openPicker() {
147
+ draftSeed.value = settings.value.seed;
148
+ pickerOpen.value = true;
149
+ }
150
+
151
+ function applyPicked() {
152
+ hex.value = draftSeed.value;
153
+ pickerOpen.value = false;
154
+ }
155
+
156
+ async function reset() {
157
+ const previous = { ...settings.value };
158
+ theme.reset();
159
+ const result = await snackbar.show({
160
+ message: t("studio.resetDone"),
161
+ action: t("studio.undo"),
162
+ duration: "long",
163
+ });
164
+ if (result === "action") theme.update(previous);
165
+ }
166
+ </script>