@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,82 @@
1
+ import type { PhotoItem } from "@cavulsqa/m3e-vue";
2
+
3
+ type SceneKind = "mountains" | "coast" | "dunes" | "city" | "forest" | "harbour";
4
+
5
+ interface Scene {
6
+ kind: SceneKind;
7
+ width: number;
8
+ height: number;
9
+ }
10
+
11
+ const SCENES: readonly Scene[] = [
12
+ { kind: "mountains", width: 1600, height: 1200 },
13
+ { kind: "coast", width: 1200, height: 1600 },
14
+ { kind: "dunes", width: 2400, height: 1000 },
15
+ { kind: "city", width: 1600, height: 1200 },
16
+ { kind: "forest", width: 1200, height: 1600 },
17
+ { kind: "harbour", width: 1600, height: 1200 },
18
+ ];
19
+
20
+ function token(name: string): string {
21
+ return getComputedStyle(document.documentElement)
22
+ .getPropertyValue(`--md-sys-color-${name}`)
23
+ .trim();
24
+ }
25
+
26
+ function ridge(width: number, height: number, base: number, peaks: number, seed: number): string {
27
+ const points = [`0,${height}`];
28
+ for (let i = 0; i <= peaks; i++) {
29
+ const x = (i / peaks) * width;
30
+ const wave = Math.sin(i * 1.7 + seed) * 0.5 + Math.sin(i * 0.6 + seed * 2) * 0.5;
31
+ points.push(`${x.toFixed(0)},${(height * base - wave * height * 0.12).toFixed(0)}`);
32
+ }
33
+ points.push(`${width},${height}`);
34
+ return points.join(" ");
35
+ }
36
+
37
+ function scene({ kind, width, height }: Scene): string {
38
+ const sky = token("primary-container");
39
+ const glow = token("tertiary-container");
40
+ const sun = token("tertiary");
41
+ const far = token("secondary");
42
+ const near = token("primary");
43
+ const deep = token("on-primary-container");
44
+ const seed = kind.length;
45
+ const shapes: Record<SceneKind, string> = {
46
+ mountains: `<polygon points="${ridge(width, height, 0.55, 7, seed)}" fill="${far}" opacity=".7"/><polygon points="${ridge(width, height, 0.72, 5, seed + 2)}" fill="${near}"/>`,
47
+ coast: `<rect y="${height * 0.6}" width="${width}" height="${height * 0.4}" fill="${far}"/><polygon points="${ridge(width, height, 0.78, 3, seed)}" fill="${near}"/>`,
48
+ dunes: `<polygon points="${ridge(width, height, 0.62, 4, seed)}" fill="${glow}"/><polygon points="${ridge(width, height, 0.78, 3, seed + 1)}" fill="${sun}" opacity=".8"/>`,
49
+ city: Array.from({ length: 14 }, (_, i) => {
50
+ const w = width / 14;
51
+ const h = height * (0.25 + ((i * 37) % 9) / 22);
52
+ return `<rect x="${i * w + 4}" y="${height - h}" width="${w - 8}" height="${h}" fill="${i % 2 ? near : far}"/>`;
53
+ }).join(""),
54
+ forest: Array.from({ length: 9 }, (_, i) => {
55
+ const x = (i + 0.5) * (width / 9);
56
+ const top = height * (0.45 + ((i * 13) % 5) / 20);
57
+ return `<polygon points="${x},${top} ${x - 90},${height} ${x + 90},${height}" fill="${i % 2 ? near : far}"/>`;
58
+ }).join(""),
59
+ harbour: `<rect y="${height * 0.65}" width="${width}" height="${height * 0.35}" fill="${far}"/><polygon points="${width * 0.42},${height * 0.62} ${width * 0.58},${height * 0.62} ${width * 0.55},${height * 0.68} ${width * 0.45},${height * 0.68}" fill="${deep}"/><polygon points="${width * 0.5},${height * 0.3} ${width * 0.5},${height * 0.6} ${width * 0.6},${height * 0.6}" fill="${glow}"/>`,
60
+ };
61
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><defs><linearGradient id="s" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${sky}"/><stop offset="1" stop-color="${glow}"/></linearGradient></defs><rect width="${width}" height="${height}" fill="url(#s)"/><circle cx="${width * 0.72}" cy="${height * 0.28}" r="${Math.min(width, height) * 0.09}" fill="${sun}"/>${shapes[kind]}</svg>`;
62
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
63
+ }
64
+
65
+ /**
66
+ * Demo photos drawn from the live theme, so the gallery needs no network and no bundled images:
67
+ * vector scenes at photo sizes - landscape, portrait and panorama - that stay sharp when zoomed.
68
+ */
69
+ export function usePhotoScenes(label: (kind: SceneKind) => string) {
70
+ const photos = shallowRef<PhotoItem[]>([]);
71
+ const build = () => {
72
+ photos.value = SCENES.map((item) => ({
73
+ src: scene(item),
74
+ alt: label(item.kind),
75
+ caption: label(item.kind),
76
+ width: item.width,
77
+ height: item.height,
78
+ }));
79
+ };
80
+ onMounted(build);
81
+ return { photos, rebuild: build };
82
+ }
@@ -0,0 +1,67 @@
1
+ import type { TreeNode } from "@cavulsqa/m3e-vue";
2
+
3
+ /** A catalogue and a permission set to show the treeview with, labelled in the current language. */
4
+ export function useTreeDemo() {
5
+ const { t } = useI18n();
6
+ const label = (key: string) => t(`gallery.tree.nodes.${key}`);
7
+
8
+ const categories = computed<TreeNode[]>(() => [
9
+ {
10
+ id: "drinks",
11
+ label: label("drinks"),
12
+ supporting: t("gallery.tree.products", { count: 42 }),
13
+ children: [
14
+ { id: "water", label: label("water") },
15
+ {
16
+ id: "juice",
17
+ label: label("juice"),
18
+ children: [
19
+ { id: "orange", label: label("orange") },
20
+ { id: "apple", label: label("apple") },
21
+ ],
22
+ },
23
+ { id: "soda", label: label("soda") },
24
+ ],
25
+ },
26
+ { id: "dairy", label: label("dairy"), supporting: t("gallery.tree.loadsLater"), lazy: true },
27
+ {
28
+ id: "grocery",
29
+ label: label("grocery"),
30
+ children: [
31
+ { id: "pasta", label: label("pasta") },
32
+ { id: "oil", label: label("oil") },
33
+ ],
34
+ },
35
+ ]);
36
+
37
+ const permissions = computed<TreeNode[]>(() => [
38
+ {
39
+ id: "sales",
40
+ label: label("sales"),
41
+ children: [
42
+ { id: "sales.read", label: label("read") },
43
+ { id: "sales.create", label: label("create") },
44
+ { id: "sales.cancel", label: label("cancel") },
45
+ ],
46
+ },
47
+ {
48
+ id: "stock",
49
+ label: label("stock"),
50
+ children: [
51
+ { id: "stock.read", label: label("read") },
52
+ { id: "stock.adjust", label: label("adjust") },
53
+ ],
54
+ },
55
+ ]);
56
+
57
+ async function loadChildren(): Promise<TreeNode[]> {
58
+ await new Promise((resolve) => setTimeout(resolve, 900));
59
+ return [
60
+ { id: "milk", label: label("milk") },
61
+ { id: "yoghurt", label: label("yoghurt") },
62
+ { id: "cheese", label: label("cheese") },
63
+ ];
64
+ }
65
+
66
+ return { categories, permissions, loadChildren };
67
+ }
@@ -0,0 +1,67 @@
1
+ /** Algeria's 58 wilayas in code order - a realistic list for the autocomplete demo. */
2
+ export const WILAYAS = [
3
+ "Adrar",
4
+ "Chlef",
5
+ "Laghouat",
6
+ "Oum El Bouaghi",
7
+ "Batna",
8
+ "Béjaïa",
9
+ "Biskra",
10
+ "Béchar",
11
+ "Blida",
12
+ "Bouira",
13
+ "Tamanrasset",
14
+ "Tébessa",
15
+ "Tlemcen",
16
+ "Tiaret",
17
+ "Tizi Ouzou",
18
+ "Alger",
19
+ "Djelfa",
20
+ "Jijel",
21
+ "Sétif",
22
+ "Saïda",
23
+ "Skikda",
24
+ "Sidi Bel Abbès",
25
+ "Annaba",
26
+ "Guelma",
27
+ "Constantine",
28
+ "Médéa",
29
+ "Mostaganem",
30
+ "M'Sila",
31
+ "Mascara",
32
+ "Ouargla",
33
+ "Oran",
34
+ "El Bayadh",
35
+ "Illizi",
36
+ "Bordj Bou Arréridj",
37
+ "Boumerdès",
38
+ "El Tarf",
39
+ "Tindouf",
40
+ "Tissemsilt",
41
+ "El Oued",
42
+ "Khenchela",
43
+ "Souk Ahras",
44
+ "Tipaza",
45
+ "Mila",
46
+ "Aïn Defla",
47
+ "Naâma",
48
+ "Aïn Témouchent",
49
+ "Ghardaïa",
50
+ "Relizane",
51
+ "Timimoun",
52
+ "Bordj Badji Mokhtar",
53
+ "Ouled Djellal",
54
+ "Béni Abbès",
55
+ "In Salah",
56
+ "In Guezzam",
57
+ "Touggourt",
58
+ "Djanet",
59
+ "El M'Ghair",
60
+ "El Meniaa",
61
+ ] as const;
62
+
63
+ export const WILAYA_OPTIONS = WILAYAS.map((label, index) => ({
64
+ value: index + 1,
65
+ label,
66
+ supporting: String(index + 1).padStart(2, "0"),
67
+ }));
@@ -0,0 +1,52 @@
1
+ import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
3
+
4
+ const galleryRoutes: Router.RouteParameters[] = [
5
+ {
6
+ name: "gallery",
7
+ path: "/gallery/",
8
+ async: lazyRoute(() => import("@/modules/gallery/views/GalleryView.vue")),
9
+ },
10
+ {
11
+ name: "gallery-tabs",
12
+ path: "/gallery/tabs/",
13
+ async: lazyRoute(() => import("@/modules/gallery/views/GalleryTabsView.vue")),
14
+ },
15
+ {
16
+ name: "gallery-contacts",
17
+ path: "/gallery/contacts/",
18
+ async: lazyRoute(() => import("@/modules/gallery/views/GalleryContactsView.vue")),
19
+ },
20
+ {
21
+ name: "gallery-chat",
22
+ path: "/gallery/chat/",
23
+ async: lazyRoute(() => import("@/modules/gallery/views/GalleryChatView.vue")),
24
+ },
25
+ {
26
+ name: "gallery-onboarding",
27
+ path: "/gallery/onboarding/",
28
+ async: lazyRoute(() => import("@/modules/gallery/views/GalleryOnboardingView.vue")),
29
+ },
30
+ {
31
+ name: "gallery-login",
32
+ path: "/gallery/login/",
33
+ async: lazyRoute(() => import("@/modules/gallery/views/GalleryLoginView.vue")),
34
+ },
35
+ {
36
+ name: "gallery-agenda",
37
+ path: "/gallery/agenda/",
38
+ async: lazyRoute(() => import("@/modules/gallery/views/GalleryAgendaView.vue")),
39
+ },
40
+ {
41
+ name: "gallery-featured",
42
+ path: "/gallery/featured/:id/",
43
+ async: lazyRoute(() => import("@/modules/gallery/views/GalleryFeaturedView.vue")),
44
+ },
45
+ {
46
+ name: "gallery-section",
47
+ path: "/gallery/:section/",
48
+ async: lazyRoute(() => import("@/modules/gallery/views/GallerySectionView.vue")),
49
+ },
50
+ ];
51
+
52
+ export default galleryRoutes;
@@ -0,0 +1,117 @@
1
+ <template>
2
+ <AppPage :title="t('gallery.agenda.title')" :subtitle="month" back variant="small">
3
+ <template #actions>
4
+ <M3IconButton
5
+ :label="t('gallery.agenda.today')"
6
+ :disabled="day === today"
7
+ @click="day = today"
8
+ >
9
+ <i-ms-today-outline-rounded />
10
+ </M3IconButton>
11
+ </template>
12
+ <template #bottom>
13
+ <div>
14
+ <M3ButtonGroup
15
+ variant="connected"
16
+ size="s"
17
+ class="agenda-switcher px-4 pb-2"
18
+ :label="t('gallery.agenda.view')"
19
+ >
20
+ <M3Button
21
+ v-for="option in AGENDA_VIEWS"
22
+ :key="option"
23
+ variant="tonal"
24
+ toggle
25
+ :selected="view === option"
26
+ @update:selected="view = option"
27
+ >
28
+ {{ t(`gallery.agenda.views.${option}`) }}
29
+ </M3Button>
30
+ </M3ButtonGroup>
31
+ <M3WeekStrip
32
+ v-if="view !== 'month'"
33
+ v-model="day"
34
+ class="pb-2"
35
+ :label="t('gallery.agenda.days')"
36
+ :locale="locale"
37
+ :marks="weekMarks"
38
+ :mark-label="visitsLabel"
39
+ />
40
+ </div>
41
+ </template>
42
+
43
+ <Transition name="agenda-through" mode="out-in" @enter="rewind">
44
+ <AgendaDay
45
+ v-if="view === 'day'"
46
+ key="day"
47
+ :day="day"
48
+ :today="today"
49
+ :heading="heading"
50
+ :visits="visits"
51
+ @step="day = addDays(day, $event)"
52
+ />
53
+ <AgendaMonth
54
+ v-else-if="view === 'month'"
55
+ key="month"
56
+ v-model="day"
57
+ :heading="heading"
58
+ :visits="visits"
59
+ :marks="monthMarks"
60
+ :mark-label="visitsLabel"
61
+ @month="shownMonth = $event"
62
+ />
63
+ <AgendaVisitList v-else key="week" :heading="heading" :visits="visits" />
64
+ </Transition>
65
+ </AppPage>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { addDays } from "@cavulsqa/m3e-vue";
70
+ import AgendaDay from "@/modules/gallery/components/agenda/AgendaDay.vue";
71
+ import AgendaMonth from "@/modules/gallery/components/agenda/AgendaMonth.vue";
72
+ import AgendaVisitList from "@/modules/gallery/components/agenda/AgendaVisitList.vue";
73
+ import { AGENDA_VIEWS, useAgenda } from "@/modules/gallery/composables/useAgenda";
74
+
75
+ const { t, locale } = useI18n();
76
+ const { today, day, view, visits, weekMarks, monthMarks, shownMonth, month, heading, visitsLabel } =
77
+ useAgenda();
78
+
79
+ function rewind(element: Element) {
80
+ element.closest(".page-content")?.scrollTo({ top: 0 });
81
+ }
82
+ </script>
83
+
84
+ <style scoped>
85
+ .agenda-switcher {
86
+ display: flex;
87
+ }
88
+
89
+ .agenda-switcher > :deep(*) {
90
+ flex: 1 1 0;
91
+ }
92
+
93
+ .agenda-through-leave-active {
94
+ transition: opacity 90ms var(--md-sys-motion-easing-emphasized-accelerate);
95
+ }
96
+
97
+ .agenda-through-enter-active {
98
+ transition:
99
+ opacity 210ms var(--md-sys-motion-easing-emphasized-decelerate),
100
+ transform 210ms var(--md-sys-motion-easing-emphasized-decelerate);
101
+ }
102
+
103
+ .agenda-through-leave-to,
104
+ .agenda-through-enter-from {
105
+ opacity: 0;
106
+ }
107
+
108
+ .agenda-through-enter-from {
109
+ transform: scale(0.96);
110
+ }
111
+
112
+ @media (prefers-reduced-motion: reduce) {
113
+ .agenda-through-enter-from {
114
+ transform: none;
115
+ }
116
+ }
117
+ </style>
@@ -0,0 +1,292 @@
1
+ <template>
2
+ <AppPage
3
+ :title="t('gallery.chat.title')"
4
+ :subtitle="t('gallery.chat.subtitle')"
5
+ back
6
+ variant="small"
7
+ class="gallery-chat"
8
+ >
9
+ <M3Messages
10
+ :messages="messages"
11
+ :label="t('gallery.chat.title')"
12
+ :locale="locale"
13
+ :typing="typing"
14
+ :you-label="t('gallery.chat.you')"
15
+ :retry-label="t('gallery.chat.retry')"
16
+ :latest-label="t('gallery.chat.latest')"
17
+ :status-labels="statusLabels"
18
+ :typing-label="typingLabel"
19
+ :unread-label="unreadLabel"
20
+ authors
21
+ :reactions="REACTIONS"
22
+ :actions="actions"
23
+ :react-label="t('gallery.chat.react')"
24
+ :menu-label="t('gallery.chat.messageActions')"
25
+ :reactions-label="reactionsLabel"
26
+ :reactions-title="t('gallery.chat.reactionsTitle')"
27
+ :all-reactions-label="t('gallery.chat.allReactions')"
28
+ :remove-reaction-label="t('gallery.chat.removeReaction')"
29
+ :others-label="othersLabel"
30
+ :card-labels="cardLabels"
31
+ @react="react"
32
+ @action="onAction"
33
+ @press="openMessage"
34
+ @retry="retry"
35
+ @vote="vote"
36
+ @rsvp="rsvp"
37
+ >
38
+ <template #before>
39
+ <M3InfiniteScroll
40
+ edge="start"
41
+ :load="loadOlder"
42
+ :loading-label="t('gallery.chat.loadingOlder')"
43
+ :error-text="t('gallery.chat.loadFailed')"
44
+ :retry-label="t('gallery.chat.retry')"
45
+ :end-text="t('gallery.chat.start')"
46
+ />
47
+ </template>
48
+ </M3Messages>
49
+
50
+ <template #fixed>
51
+ <M3MessageBar
52
+ v-model="draft"
53
+ :label="t('gallery.chat.compose')"
54
+ :send-label="t('gallery.chat.send')"
55
+ @send="send"
56
+ >
57
+ <template #leading>
58
+ <M3IconButton
59
+ :label="t('gallery.chat.attach')"
60
+ :aria-expanded="attachOpen"
61
+ @click="attachOpen = !attachOpen"
62
+ >
63
+ <i-ms-add-rounded class="chat-plus" :class="{ 'chat-plus--open': attachOpen }" />
64
+ </M3IconButton>
65
+ </template>
66
+ <template #trailing>
67
+ <M3IconButton :label="t('gallery.chat.thumbsUp')" @click="draft += '👍'">
68
+ <i-ms-add-reaction-outline-rounded />
69
+ </M3IconButton>
70
+ </template>
71
+ </M3MessageBar>
72
+
73
+ <ChatAttachSheet
74
+ v-model:open="attachOpen"
75
+ :photos="photos"
76
+ @image="sendImage"
77
+ @text="send"
78
+ @recent="sendPhoto"
79
+ @failed="onFailed"
80
+ @compose="compose"
81
+ />
82
+ <ChatPollComposer v-model:open="pollOpen" @send="sendPoll" />
83
+ <ChatInviteComposer v-model:open="inviteOpen" @send="sendInvite" />
84
+ <ChatContactPicker
85
+ v-model:open="contactOpen"
86
+ :customers="customers"
87
+ :loading="customersLoading"
88
+ :failed="customersFailed"
89
+ @pick="sendContact"
90
+ />
91
+
92
+ <M3PhotoBrowser
93
+ v-model:open="browserOpen"
94
+ v-model:index="photoIndex"
95
+ :photos="shared"
96
+ :label="t('gallery.chat.photos')"
97
+ :close-label="t('shell.dismiss')"
98
+ />
99
+ </template>
100
+ </AppPage>
101
+ </template>
102
+
103
+ <script setup lang="ts">
104
+ import type {
105
+ ChatMessage,
106
+ MessageAction,
107
+ MessageCardLabels,
108
+ MessageReaction,
109
+ MessageStatus,
110
+ PhotoItem,
111
+ } from "@cavulsqa/m3e-vue";
112
+ import type { Router } from "framework7/types";
113
+ import { markRaw } from "vue";
114
+ import CopyIcon from "~icons/material-symbols/content-copy-outline-rounded";
115
+ import DeleteIcon from "~icons/material-symbols/delete-outline-rounded";
116
+ import ChatAttachSheet from "@/modules/gallery/components/chat/ChatAttachSheet.vue";
117
+ import ChatContactPicker from "@/modules/gallery/components/chat/ChatContactPicker.vue";
118
+ import ChatInviteComposer from "@/modules/gallery/components/chat/ChatInviteComposer.vue";
119
+ import ChatPollComposer from "@/modules/gallery/components/chat/ChatPollComposer.vue";
120
+ import type { ComposeKind } from "@/modules/gallery/composables/composeKind";
121
+ import { mapsLink } from "@/modules/gallery/composables/currentPosition";
122
+ import { openExternal } from "@/modules/gallery/composables/openExternal";
123
+ import { useChatCustomers } from "@/modules/gallery/composables/useChatCustomers";
124
+ import { useChatDemo } from "@/modules/gallery/composables/useChatDemo";
125
+ import { useLocationShare } from "@/modules/gallery/composables/useLocationShare";
126
+ import { usePhotoScenes } from "@/modules/gallery/composables/usePhotoScenes";
127
+
128
+ const props = defineProps<{ f7router: Router.Router }>();
129
+ const { t, locale } = useI18n();
130
+ const snackbar = useSnackbar();
131
+ const { photos } = usePhotoScenes((kind) => t(`gallery.carousels.scenes.${kind}`));
132
+ const REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏", "🔥"] as const;
133
+
134
+ const {
135
+ messages,
136
+ typing,
137
+ draft,
138
+ send,
139
+ sendPhoto,
140
+ sendImage,
141
+ sendLocation,
142
+ sendContact,
143
+ sendPoll,
144
+ sendInvite,
145
+ vote,
146
+ rsvp,
147
+ react,
148
+ retry,
149
+ remove,
150
+ loadOlder,
151
+ } = useChatDemo((index) => {
152
+ const photo = photos.value[index % Math.max(1, photos.value.length)];
153
+ if (!photo?.width || !photo.height) return null;
154
+ return { src: photo.src, width: photo.width, height: photo.height, alt: photo.alt };
155
+ });
156
+
157
+ const browserOpen = ref(false);
158
+ const attachOpen = ref(false);
159
+ const pollOpen = ref(false);
160
+ const inviteOpen = ref(false);
161
+ const contactOpen = ref(false);
162
+
163
+ const {
164
+ customers,
165
+ loading: customersLoading,
166
+ failed: customersFailed,
167
+ } = useChatCustomers(contactOpen);
168
+ const { share: shareLocation } = useLocationShare(sendLocation);
169
+
170
+ const cardLabels = computed<MessageCardLabels>(() => ({
171
+ openLocation: t("gallery.chat.cards.openLocation"),
172
+ openContact: t("gallery.chat.cards.openContact"),
173
+ pollSingle: t("gallery.chat.cards.pollSingle"),
174
+ pollMultiple: t("gallery.chat.cards.pollMultiple"),
175
+ votes: (count: number) => t("gallery.chat.cards.votes", { count }, count),
176
+ going: t("gallery.chat.cards.going"),
177
+ maybe: t("gallery.chat.cards.maybe"),
178
+ no: t("gallery.chat.cards.no"),
179
+ }));
180
+
181
+ const actions = computed<MessageAction[]>(() => [
182
+ {
183
+ id: "copy",
184
+ label: t("gallery.chat.copy"),
185
+ icon: markRaw(CopyIcon),
186
+ when: (message) => Boolean(message.text),
187
+ },
188
+ {
189
+ id: "delete",
190
+ label: t("gallery.chat.delete"),
191
+ icon: markRaw(DeleteIcon),
192
+ tone: "destructive",
193
+ },
194
+ ]);
195
+ const photoIndex = ref(0);
196
+
197
+ const statusLabels = computed<Record<MessageStatus, string>>(() => ({
198
+ sending: t("gallery.chat.status.sending"),
199
+ sent: t("gallery.chat.status.sent"),
200
+ delivered: t("gallery.chat.status.delivered"),
201
+ read: t("gallery.chat.status.read"),
202
+ failed: t("gallery.chat.status.failed"),
203
+ }));
204
+
205
+ const shared = computed<PhotoItem[]>(() =>
206
+ messages.value.flatMap((message) =>
207
+ message.image
208
+ ? [{ ...message.image, alt: message.image.alt ?? "", caption: message.text }]
209
+ : [],
210
+ ),
211
+ );
212
+
213
+ function typingLabel(author: string | undefined) {
214
+ return t("gallery.chat.typing", { name: author ?? "" });
215
+ }
216
+
217
+ function unreadLabel(count: number) {
218
+ return t("gallery.chat.unread", { count }, count);
219
+ }
220
+
221
+ function compose(kind: ComposeKind) {
222
+ if (kind === "location") void shareLocation();
223
+ else if (kind === "contact") contactOpen.value = true;
224
+ else if (kind === "poll") pollOpen.value = true;
225
+ else inviteOpen.value = true;
226
+ }
227
+
228
+ function openMessage(message: ChatMessage) {
229
+ if (message.location) openExternal(mapsLink(message.location.lat, message.location.lng));
230
+ else if (message.contact)
231
+ props.f7router.navigate(`/demo/search/?q=${encodeURIComponent(message.contact.name)}`);
232
+ else if (message.image) openPhoto(message);
233
+ }
234
+
235
+ function openPhoto(message: ChatMessage) {
236
+ const index = shared.value.findIndex((photo) => photo.src === message.image?.src);
237
+ photoIndex.value = Math.max(0, index);
238
+ browserOpen.value = true;
239
+ }
240
+
241
+ function reactionsLabel(reactions: readonly MessageReaction[]) {
242
+ return reactions
243
+ .map((reaction) =>
244
+ t(
245
+ "gallery.chat.reactionCount",
246
+ { emoji: reaction.emoji, count: reaction.count ?? 1 },
247
+ reaction.count ?? 1,
248
+ ),
249
+ )
250
+ .join(", ");
251
+ }
252
+
253
+ function othersLabel(count: number) {
254
+ return t("gallery.chat.othersReacted", { count }, count);
255
+ }
256
+
257
+ function onFailed(name: string) {
258
+ void snackbar.show({ message: t("gallery.chat.attachFailed", { name }) });
259
+ }
260
+
261
+ async function onAction(message: ChatMessage, id: string) {
262
+ if (id === "delete") {
263
+ remove(message);
264
+ return;
265
+ }
266
+ if (id !== "copy" || !message.text) return;
267
+ try {
268
+ await navigator.clipboard.writeText(message.text);
269
+ void snackbar.show({ message: t("gallery.chat.copied") });
270
+ } catch {
271
+ void snackbar.show({ message: t("gallery.chat.copyFailed") });
272
+ }
273
+ }
274
+ </script>
275
+
276
+ <style scoped>
277
+ .chat-plus {
278
+ transition: rotate var(--md-sys-motion-spring-fast-spatial-duration)
279
+ var(--md-sys-motion-spring-fast-spatial);
280
+ }
281
+
282
+ .chat-plus--open {
283
+ rotate: 45deg;
284
+ }
285
+
286
+ :global(.gallery-chat .page-content) {
287
+ --m3-messages-min-height: calc(100% - 64px - env(safe-area-inset-top));
288
+
289
+ padding-bottom: 0;
290
+ scroll-padding-bottom: var(--m3-message-bar-height, 0px);
291
+ }
292
+ </style>