@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,106 @@
1
+ <template>
2
+ <M3AttachSheet
3
+ v-model:open="open"
4
+ :options="options"
5
+ :label="t('gallery.chat.attachTitle')"
6
+ :title="t('gallery.chat.attachTitle')"
7
+ @select="choose"
8
+ >
9
+ <template #recent>
10
+ <ChatRecentPhotos
11
+ v-if="photos.length"
12
+ :photos="photos"
13
+ :label="t('gallery.chat.recent')"
14
+ :send-label="(alt) => t('gallery.chat.sendPhoto', { name: alt })"
15
+ @pick="pickRecent"
16
+ />
17
+ </template>
18
+ </M3AttachSheet>
19
+ <input ref="gallery" type="file" accept="image/*" multiple hidden @change="receive" />
20
+ <input ref="camera" type="file" accept="image/*" capture="environment" hidden @change="receive" />
21
+ <input ref="file" type="file" hidden @change="receive" />
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import type { AttachOption, MessageImage, PhotoItem } from "@cavulsqa/m3e-vue";
26
+ import { markRaw } from "vue";
27
+ import BallotIcon from "~icons/material-symbols/ballot-outline-rounded";
28
+ import CameraIcon from "~icons/material-symbols/photo-camera-outline-rounded";
29
+ import ContactIcon from "~icons/material-symbols/person-outline-rounded";
30
+ import EventIcon from "~icons/material-symbols/event-outline-rounded";
31
+ import FileIcon from "~icons/material-symbols/description-outline-rounded";
32
+ import GalleryIcon from "~icons/material-symbols/photo-library-outline-rounded";
33
+ import LocationIcon from "~icons/material-symbols/location-on-outline-rounded";
34
+ import ChatRecentPhotos from "@/modules/gallery/components/chat/ChatRecentPhotos.vue";
35
+ import type { ComposeKind } from "@/modules/gallery/composables/composeKind";
36
+ import { useLocalAttachments } from "@/modules/gallery/composables/useLocalAttachments";
37
+
38
+ defineProps<{ photos: readonly PhotoItem[] }>();
39
+
40
+ const open = defineModel<boolean>("open", { default: false });
41
+ const emit = defineEmits<{
42
+ image: [image: MessageImage];
43
+ text: [text: string];
44
+ recent: [index: number];
45
+ failed: [name: string];
46
+ compose: [kind: ComposeKind];
47
+ }>();
48
+
49
+ const { t, locale } = useI18n();
50
+ const { toImage, describe } = useLocalAttachments();
51
+ const pickers = {
52
+ gallery: useTemplateRef<HTMLInputElement>("gallery"),
53
+ camera: useTemplateRef<HTMLInputElement>("camera"),
54
+ file: useTemplateRef<HTMLInputElement>("file"),
55
+ };
56
+
57
+ const OPTIONS = [
58
+ { id: "gallery", icon: GalleryIcon, tone: "primary", shape: "cookie9Sided" },
59
+ { id: "camera", icon: CameraIcon, tone: "tertiary", shape: "clover4Leaf" },
60
+ { id: "file", icon: FileIcon, tone: "secondary", shape: "gem" },
61
+ { id: "location", icon: LocationIcon, tone: "primary", shape: "softBurst" },
62
+ { id: "contact", icon: ContactIcon, tone: "secondary", shape: "puffy" },
63
+ { id: "poll", icon: BallotIcon, tone: "tertiary", shape: "pentagon" },
64
+ { id: "event", icon: EventIcon, tone: "primary", shape: "sunny" },
65
+ ] as const;
66
+
67
+ const options = computed<AttachOption[]>(() =>
68
+ OPTIONS.map((option) => ({
69
+ ...option,
70
+ icon: markRaw(option.icon),
71
+ label: t(`gallery.chat.attachOptions.${option.id}`),
72
+ })),
73
+ );
74
+
75
+ function choose(id: string) {
76
+ if (id === "gallery" || id === "camera" || id === "file") {
77
+ const input = pickers[id].value;
78
+ if (input) {
79
+ input.value = "";
80
+ input.click();
81
+ }
82
+ return;
83
+ }
84
+ if (id === "location" || id === "contact" || id === "poll" || id === "event") emit("compose", id);
85
+ }
86
+
87
+ function pickRecent(index: number) {
88
+ open.value = false;
89
+ emit("recent", index);
90
+ }
91
+
92
+ async function receive(event: Event) {
93
+ const input = event.target as HTMLInputElement;
94
+ const files = [...(input.files ?? [])];
95
+ input.value = "";
96
+ for (const file of files) {
97
+ if (file.type.startsWith("image/")) {
98
+ const image = await toImage(file);
99
+ if (image) emit("image", image);
100
+ else emit("failed", file.name);
101
+ } else {
102
+ emit("text", describe(file, locale.value));
103
+ }
104
+ }
105
+ }
106
+ </script>
@@ -0,0 +1,85 @@
1
+ <template>
2
+ <M3BottomSheet v-model:open="open" :title="t('gallery.chat.contact.title')" @closed="filter = ''">
3
+ <div class="px-4 pb-2">
4
+ <M3SearchBar
5
+ v-model="filter"
6
+ :placeholder="t('gallery.chat.contact.search')"
7
+ :clear-label="t('gallery.inputs.clear')"
8
+ />
9
+ </div>
10
+ <M3List variant="standard" :label="t('gallery.chat.contact.title')">
11
+ <M3ListItem
12
+ v-for="customer in matches"
13
+ :key="customer.id"
14
+ clickable
15
+ :headline="customer.name"
16
+ :supporting="customer.city"
17
+ @click="pick(customer)"
18
+ >
19
+ <template #leading>
20
+ <span
21
+ class="type-title-medium grid size-10 place-items-center rounded-full bg-secondary-container text-on-secondary-container"
22
+ aria-hidden="true"
23
+ >
24
+ {{ initial(customer.name) }}
25
+ </span>
26
+ </template>
27
+ </M3ListItem>
28
+ </M3List>
29
+ <p
30
+ v-if="!loading && matches.length === 0"
31
+ class="type-body-medium m-0 px-6 py-8 text-center text-on-surface-variant"
32
+ role="status"
33
+ >
34
+ {{ emptyText }}
35
+ </p>
36
+ </M3BottomSheet>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import type { MessageContact } from "@cavulsqa/m3e-vue";
41
+
42
+ interface Customer {
43
+ id: number;
44
+ name: string;
45
+ city: string;
46
+ }
47
+
48
+ const props = defineProps<{
49
+ customers: readonly Customer[];
50
+ loading?: boolean;
51
+ failed?: boolean;
52
+ }>();
53
+ const open = defineModel<boolean>("open", { default: false });
54
+ const emit = defineEmits<{ pick: [contact: MessageContact] }>();
55
+ const { t, locale } = useI18n();
56
+
57
+ const filter = ref("");
58
+ const needle = computed(() => filter.value.trim().toLocaleLowerCase(locale.value));
59
+
60
+ const matches = computed(() =>
61
+ needle.value
62
+ ? props.customers.filter((customer) =>
63
+ `${customer.name} ${customer.city}`.toLocaleLowerCase(locale.value).includes(needle.value),
64
+ )
65
+ : props.customers,
66
+ );
67
+
68
+ const emptyText = computed(() => {
69
+ if (props.failed) return t("gallery.chat.contact.failed");
70
+ return needle.value ? t("gallery.inputs.noMatches") : t("gallery.chat.contact.empty");
71
+ });
72
+
73
+ /** First and last word's first letter, so "Customer 2" and "Customer 8" do not share a monogram. */
74
+ function initial(name: string) {
75
+ const words = name.trim().split(/\s+/).filter(Boolean);
76
+ const first = [...(words[0] ?? "")][0] ?? "";
77
+ const last = words.length > 1 ? ([...(words.at(-1) ?? "")][0] ?? "") : "";
78
+ return `${first}${last}`.toLocaleUpperCase(locale.value);
79
+ }
80
+
81
+ function pick(customer: Customer) {
82
+ emit("pick", { name: customer.name, detail: customer.city });
83
+ open.value = false;
84
+ }
85
+ </script>
@@ -0,0 +1,131 @@
1
+ <template>
2
+ <M3BottomSheet v-model:open="open" :title="t('gallery.chat.invite.title')">
3
+ <div class="flex flex-col gap-3 px-4 pb-2">
4
+ <M3TextField
5
+ v-model="title"
6
+ :label="t('gallery.chat.invite.name')"
7
+ :maxlength="TITLE_MAX"
8
+ autocomplete="off"
9
+ />
10
+ <M3List variant="segmented">
11
+ <M3ListItem
12
+ clickable
13
+ :overline="t('gallery.chat.invite.date')"
14
+ :headline="dateLabel"
15
+ @click="dateOpen = true"
16
+ >
17
+ <template #leading><i-ms-calendar-today-outline-rounded /></template>
18
+ </M3ListItem>
19
+ <M3ListItem
20
+ clickable
21
+ :overline="t('gallery.chat.invite.time')"
22
+ :headline="timeLabel"
23
+ @click="timeOpen = true"
24
+ >
25
+ <template #leading><i-ms-schedule-outline-rounded /></template>
26
+ </M3ListItem>
27
+ </M3List>
28
+ <M3TextField
29
+ v-model="place"
30
+ :label="t('gallery.chat.invite.place')"
31
+ :maxlength="PLACE_MAX"
32
+ autocomplete="off"
33
+ />
34
+ </div>
35
+
36
+ <template #footer>
37
+ <div class="flex justify-end">
38
+ <M3Button size="m" :disabled="!ready" @click="submit">
39
+ <template #icon><i-ms-send-rounded class="rtl:-scale-x-100" /></template>
40
+ {{ t("gallery.chat.send") }}
41
+ </M3Button>
42
+ </div>
43
+ </template>
44
+ </M3BottomSheet>
45
+
46
+ <M3DatePicker
47
+ v-model="date"
48
+ v-model:open="dateOpen"
49
+ :min="today"
50
+ :locale="locale"
51
+ :title="t('gallery.pickers.selectDate')"
52
+ :empty-headline="t('gallery.pickers.selectedDate')"
53
+ :confirm-label="t('gallery.pickers.ok')"
54
+ :dismiss-label="t('gallery.overlays.cancel')"
55
+ :input-label="t('gallery.pickers.typeDate')"
56
+ :calendar-label="t('gallery.pickers.useCalendar')"
57
+ :invalid-label="t('gallery.pickers.invalid')"
58
+ />
59
+ <M3TimePicker
60
+ v-model="time"
61
+ v-model:open="timeOpen"
62
+ :minute-step="15"
63
+ :locale="locale"
64
+ :title="t('gallery.pickers.selectTime')"
65
+ :confirm-label="t('gallery.pickers.ok')"
66
+ :dismiss-label="t('gallery.overlays.cancel')"
67
+ :dial-label="t('gallery.pickers.useDial')"
68
+ :input-label="t('gallery.pickers.useKeyboard')"
69
+ />
70
+ </template>
71
+
72
+ <script setup lang="ts">
73
+ import {
74
+ addDays,
75
+ formatIso,
76
+ formatIsoTime,
77
+ todayIso,
78
+ type IsoDate,
79
+ type IsoTime,
80
+ type MessageInvite,
81
+ } from "@cavulsqa/m3e-vue";
82
+
83
+ const TITLE_MAX = 80;
84
+ const PLACE_MAX = 80;
85
+ const DEFAULT_TIME: IsoTime = "09:00";
86
+
87
+ const open = defineModel<boolean>("open", { default: false });
88
+ const emit = defineEmits<{ send: [invite: MessageInvite] }>();
89
+ const { t, locale } = useI18n();
90
+
91
+ const today = todayIso();
92
+ const title = ref("");
93
+ const place = ref("");
94
+ const date = ref<IsoDate | null>(addDays(today, 1));
95
+ const time = ref<IsoTime | null>(DEFAULT_TIME);
96
+ const dateOpen = ref(false);
97
+ const timeOpen = ref(false);
98
+
99
+ const dateLabel = computed(() =>
100
+ date.value
101
+ ? formatIso(date.value, locale.value, { weekday: "long", day: "numeric", month: "long" })
102
+ : t("gallery.pickers.pick"),
103
+ );
104
+ const timeLabel = computed(() =>
105
+ time.value ? formatIsoTime(time.value, locale.value) : t("gallery.pickers.pickTime"),
106
+ );
107
+
108
+ const start = computed(() => {
109
+ if (!date.value || !time.value) return null;
110
+ const at = new Date(`${date.value}T${time.value}:00`);
111
+ return Number.isNaN(at.getTime()) ? null : at;
112
+ });
113
+
114
+ const ready = computed(() => Boolean(title.value.trim()) && start.value !== null);
115
+
116
+ function submit() {
117
+ if (!ready.value || !start.value) return;
118
+ const where = place.value.trim();
119
+ emit("send", {
120
+ title: title.value.trim(),
121
+ start: start.value,
122
+ ...(where ? { place: where } : {}),
123
+ answers: { going: 0, maybe: 0, no: 0 },
124
+ });
125
+ title.value = "";
126
+ place.value = "";
127
+ date.value = addDays(todayIso(), 1);
128
+ time.value = DEFAULT_TIME;
129
+ open.value = false;
130
+ }
131
+ </script>
@@ -0,0 +1,148 @@
1
+ <template>
2
+ <M3BottomSheet v-model:open="open" :title="t('gallery.chat.poll.title')">
3
+ <div class="flex flex-col gap-3 px-4 pb-2">
4
+ <M3TextField
5
+ v-model="question"
6
+ :label="t('gallery.chat.poll.question')"
7
+ :maxlength="QUESTION_MAX"
8
+ autocomplete="off"
9
+ />
10
+ <TransitionGroup tag="div" name="poll-option" class="relative flex flex-col gap-3">
11
+ <div v-for="(option, index) in options" :key="option.key" class="flex items-center gap-1">
12
+ <div class="min-w-0 flex-1">
13
+ <M3TextField
14
+ v-model="option.label"
15
+ :label="t('gallery.chat.poll.option', { number: index + 1 })"
16
+ :error="repeated.has(index) ? t('gallery.chat.poll.repeated') : undefined"
17
+ autocomplete="off"
18
+ />
19
+ </div>
20
+ <M3IconButton
21
+ v-if="options.length > MIN_OPTIONS"
22
+ :label="t('gallery.chat.poll.remove', { number: index + 1 })"
23
+ @click="remove(index)"
24
+ >
25
+ <i-ms-close-rounded />
26
+ </M3IconButton>
27
+ </div>
28
+ </TransitionGroup>
29
+ <M3Button v-if="options.length < MAX_OPTIONS" variant="text" class="self-start" @click="add">
30
+ <template #icon><i-ms-add-rounded /></template>
31
+ {{ t("gallery.chat.poll.add") }}
32
+ </M3Button>
33
+ <div class="flex items-center gap-4 py-2">
34
+ <div class="flex min-w-0 flex-1 flex-col">
35
+ <span class="type-body-large text-on-surface">{{ t("gallery.chat.poll.multiple") }}</span>
36
+ <span class="type-body-medium text-on-surface-variant">
37
+ {{ t("gallery.chat.poll.multipleHint") }}
38
+ </span>
39
+ </div>
40
+ <M3Switch v-model="multiple" :label="t('gallery.chat.poll.multiple')" />
41
+ </div>
42
+ </div>
43
+
44
+ <template #footer>
45
+ <div class="flex justify-end">
46
+ <M3Button size="m" :disabled="!ready" @click="submit">
47
+ <template #icon><i-ms-send-rounded class="rtl:-scale-x-100" /></template>
48
+ {{ t("gallery.chat.send") }}
49
+ </M3Button>
50
+ </div>
51
+ </template>
52
+ </M3BottomSheet>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import type { MessagePoll } from "@cavulsqa/m3e-vue";
57
+
58
+ const MIN_OPTIONS = 2;
59
+ const MAX_OPTIONS = 6;
60
+ const QUESTION_MAX = 140;
61
+
62
+ const open = defineModel<boolean>("open", { default: false });
63
+ const emit = defineEmits<{ send: [poll: MessagePoll] }>();
64
+ const { t } = useI18n();
65
+
66
+ let keys = 0;
67
+ const blankOption = () => ({ key: (keys += 1), label: "" });
68
+
69
+ const question = ref("");
70
+ const options = ref([blankOption(), blankOption()]);
71
+ const multiple = ref(false);
72
+
73
+ const normalised = computed(() =>
74
+ options.value.map((option) => option.label.trim().toLocaleLowerCase()),
75
+ );
76
+
77
+ /** Indexes of answers that repeat an earlier one: two identical answers split one opinion in two. */
78
+ const repeated = computed(() => {
79
+ const seen = new Set<string>();
80
+ const indexes = new Set<number>();
81
+ normalised.value.forEach((label, index) => {
82
+ if (!label) return;
83
+ if (seen.has(label)) indexes.add(index);
84
+ seen.add(label);
85
+ });
86
+ return indexes;
87
+ });
88
+
89
+ const filled = computed(() => options.value.filter((option) => option.label.trim()));
90
+ const ready = computed(
91
+ () =>
92
+ Boolean(question.value.trim()) && filled.value.length >= MIN_OPTIONS && !repeated.value.size,
93
+ );
94
+
95
+ function add() {
96
+ if (options.value.length < MAX_OPTIONS) options.value.push(blankOption());
97
+ }
98
+
99
+ function remove(index: number) {
100
+ if (options.value.length > MIN_OPTIONS) options.value.splice(index, 1);
101
+ }
102
+
103
+ function submit() {
104
+ if (!ready.value) return;
105
+ emit("send", {
106
+ question: question.value.trim(),
107
+ multiple: multiple.value,
108
+ options: filled.value.map((option, index) => ({
109
+ id: `o${index + 1}`,
110
+ label: option.label.trim(),
111
+ votes: 0,
112
+ })),
113
+ });
114
+ question.value = "";
115
+ options.value = [blankOption(), blankOption()];
116
+ multiple.value = false;
117
+ open.value = false;
118
+ }
119
+ </script>
120
+
121
+ <style scoped>
122
+ .poll-option-enter-active,
123
+ .poll-option-leave-active {
124
+ transition:
125
+ opacity var(--md-sys-motion-spring-fast-effects-duration)
126
+ var(--md-sys-motion-spring-fast-effects),
127
+ translate var(--md-sys-motion-spring-fast-spatial-duration)
128
+ var(--md-sys-motion-spring-fast-spatial);
129
+ }
130
+
131
+ .poll-option-enter-from,
132
+ .poll-option-leave-to {
133
+ opacity: 0;
134
+ translate: 0 -8px;
135
+ }
136
+
137
+ .poll-option-leave-active {
138
+ position: absolute;
139
+ inset-inline: 0;
140
+ }
141
+
142
+ @media (prefers-reduced-motion: reduce) {
143
+ .poll-option-enter-active,
144
+ .poll-option-leave-active {
145
+ transition: none;
146
+ }
147
+ }
148
+ </style>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <section :aria-label="label">
3
+ <h3 class="type-title-small m-0 px-1 pb-2 text-on-surface-variant">{{ label }}</h3>
4
+ <ul class="chat-recent m-0 flex list-none gap-2 overflow-x-auto p-0">
5
+ <li v-for="(photo, index) in photos" :key="photo.src" class="shrink-0">
6
+ <button
7
+ type="button"
8
+ class="chat-recent__photo m3-focus-ring rounded-lg"
9
+ :aria-label="sendLabel(photo.alt)"
10
+ @click="emit('pick', index)"
11
+ >
12
+ <img
13
+ :src="photo.src"
14
+ :alt="photo.alt"
15
+ class="size-full object-cover"
16
+ loading="lazy"
17
+ decoding="async"
18
+ draggable="false"
19
+ />
20
+ </button>
21
+ </li>
22
+ </ul>
23
+ </section>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import type { PhotoItem } from "@cavulsqa/m3e-vue";
28
+
29
+ defineProps<{
30
+ photos: readonly PhotoItem[];
31
+ label: string;
32
+ sendLabel: (alt: string) => string;
33
+ }>();
34
+
35
+ const emit = defineEmits<{ pick: [index: number] }>();
36
+ </script>
37
+
38
+ <style scoped>
39
+ .chat-recent {
40
+ scrollbar-width: none;
41
+ overscroll-behavior-x: contain;
42
+ scroll-snap-type: x proximity;
43
+ }
44
+
45
+ .chat-recent__photo {
46
+ display: block;
47
+ width: 88px;
48
+ height: 88px;
49
+ padding: 0;
50
+ overflow: hidden;
51
+ border: 0;
52
+ background: var(--md-sys-color-surface-container-highest);
53
+ scroll-snap-align: start;
54
+ cursor: pointer;
55
+ transition: scale var(--md-sys-motion-spring-fast-spatial-duration)
56
+ var(--md-sys-motion-spring-fast-spatial);
57
+ }
58
+
59
+ .chat-recent__photo:active {
60
+ scale: 0.94;
61
+ }
62
+ </style>
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <GalleryBlock :title="t('gallery.inputs.account')" :note="t('gallery.inputs.accountNote')" stack>
3
+ <div class="flex flex-col gap-2">
4
+ <M3TextField
5
+ v-model="password"
6
+ variant="outlined"
7
+ :label="t('gallery.inputs.password')"
8
+ :type="revealed ? 'text' : 'password'"
9
+ autocomplete="new-password"
10
+ autocapitalize="off"
11
+ spellcheck="false"
12
+ >
13
+ <template #leading><i-ms-lock-outline-rounded /></template>
14
+ <template #trailing>
15
+ <M3IconButton
16
+ :label="t(revealed ? 'gallery.inputs.hidePassword' : 'gallery.inputs.showPassword')"
17
+ :aria-pressed="revealed"
18
+ @pointerdown.prevent
19
+ @click="revealed = !revealed"
20
+ >
21
+ <i-ms-visibility-off-outline-rounded v-if="revealed" />
22
+ <i-ms-visibility-outline-rounded v-else />
23
+ </M3IconButton>
24
+ </template>
25
+ </M3TextField>
26
+ <PasswordStrength
27
+ :score="score"
28
+ :label="score ? t(`gallery.inputs.strength.${STRENGTH[score]}`) : ''"
29
+ class="px-1"
30
+ />
31
+ </div>
32
+ <M3TextField
33
+ v-model="phone"
34
+ variant="outlined"
35
+ :label="t('gallery.inputs.phone')"
36
+ prefix="+213"
37
+ inputmode="tel"
38
+ autocomplete="tel-national"
39
+ :supporting="t('gallery.inputs.phoneHint')"
40
+ :error="phoneError"
41
+ @blur="touched = true"
42
+ >
43
+ <template #leading><i-ms-call-outline-rounded /></template>
44
+ </M3TextField>
45
+ </GalleryBlock>
46
+ </template>
47
+
48
+ <script setup lang="ts">
49
+ import GalleryBlock from "@/modules/gallery/components/GalleryBlock.vue";
50
+ import PasswordStrength from "@/modules/gallery/components/inputs/PasswordStrength.vue";
51
+ import {
52
+ formatMobile,
53
+ isMobile,
54
+ mobileDigits,
55
+ passwordScore,
56
+ } from "@/modules/gallery/composables/fieldFormats";
57
+
58
+ const STRENGTH = [null, "weak", "fair", "good", "strong"] as const;
59
+
60
+ const { t } = useI18n();
61
+ const password = ref("");
62
+ const revealed = ref(false);
63
+ const digits = ref("");
64
+ const touched = ref(false);
65
+
66
+ const score = computed(() => (password.value ? passwordScore(password.value) : 0));
67
+ const phone = computed({
68
+ get: () => formatMobile(digits.value),
69
+ set: (raw: string) => {
70
+ digits.value = mobileDigits(raw);
71
+ },
72
+ });
73
+ const phoneError = computed(() => {
74
+ const complete = digits.value.length === 9;
75
+ if (complete && !isMobile(digits.value)) return t("gallery.inputs.phoneInvalid");
76
+ if (touched.value && digits.value.length > 0 && !complete) return t("gallery.inputs.phoneShort");
77
+ return undefined;
78
+ });
79
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <GalleryBlock :title="t('gallery.inputs.tags')" :note="t('gallery.inputs.tagsNote')" stack>
3
+ <M3ChipField
4
+ v-model="tags"
5
+ :label="t('gallery.inputs.tagsLabel')"
6
+ :placeholder="t('gallery.inputs.tagsPlaceholder')"
7
+ :supporting="t('gallery.inputs.tagsHint')"
8
+ :max="MAX"
9
+ :validate="valid"
10
+ :remove-label="(tag: string) => t('gallery.inputs.removeTag', { tag })"
11
+ />
12
+ <div
13
+ v-if="suggestions.length"
14
+ class="flex flex-wrap gap-2"
15
+ role="group"
16
+ :aria-label="t('gallery.inputs.suggested')"
17
+ >
18
+ <M3Chip
19
+ v-for="suggestion in suggestions"
20
+ :key="suggestion"
21
+ kind="suggestion"
22
+ :label="suggestion"
23
+ @click="add(suggestion)"
24
+ />
25
+ </div>
26
+ </GalleryBlock>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import { hasEntry } from "@cavulsqa/m3e-vue";
31
+ import GalleryBlock from "@/modules/gallery/components/GalleryBlock.vue";
32
+
33
+ const MAX = 6;
34
+
35
+ const { t } = useI18n();
36
+ const tags = ref<string[]>(["Wholesale", "Oran"]);
37
+ const SUGGESTED = computed(() =>
38
+ ["nightDelivery", "pharmacy", "cashOnly", "keyAccount"].map((key) =>
39
+ t(`gallery.inputs.suggestedTags.${key}`),
40
+ ),
41
+ );
42
+ const suggestions = computed(() =>
43
+ tags.value.length >= MAX ? [] : SUGGESTED.value.filter((tag) => !hasEntry(tags.value, tag)),
44
+ );
45
+
46
+ function valid(value: string) {
47
+ return value.length >= 2 && value.length <= 24;
48
+ }
49
+
50
+ function add(tag: string) {
51
+ if (tags.value.length < MAX && !hasEntry(tags.value, tag)) tags.value = [...tags.value, tag];
52
+ }
53
+ </script>