@cavulsqa/create 0.1.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 (75) hide show
  1. package/README.md +63 -0
  2. package/bin/create.mjs +142 -0
  3. package/lib/scaffold.mjs +124 -0
  4. package/package.json +60 -0
  5. package/templates/f7-app/.claude/rules/data-fetching.md +67 -0
  6. package/templates/f7-app/.claude/rules/database.md +65 -0
  7. package/templates/f7-app/.claude/rules/framework7-ui.md +79 -0
  8. package/templates/f7-app/.claude/rules/modules.md +43 -0
  9. package/templates/f7-app/.claude/rules/native.md +47 -0
  10. package/templates/f7-app/.claude/skills/f7-design/SKILL.md +67 -0
  11. package/templates/f7-app/.claude/skills/f7-design/components.md +49 -0
  12. package/templates/f7-app/.claude/skills/f7-design/icons.md +52 -0
  13. package/templates/f7-app/.claude/skills/module-architecture/SKILL.md +65 -0
  14. package/templates/f7-app/.claude/skills/module-architecture/file-templates.md +183 -0
  15. package/templates/f7-app/.claude/skills/reactive-data/SKILL.md +89 -0
  16. package/templates/f7-app/.claude/skills/reactive-data/testing.md +55 -0
  17. package/templates/f7-app/CLAUDE.md +105 -0
  18. package/templates/f7-app/auto-imports.d.ts +667 -0
  19. package/templates/f7-app/capacitor.config.ts +14 -0
  20. package/templates/f7-app/components.d.ts +59 -0
  21. package/templates/f7-app/index.html +15 -0
  22. package/templates/f7-app/package.json +51 -0
  23. package/templates/f7-app/src/App.vue +72 -0
  24. package/templates/f7-app/src/app/tabs.ts +25 -0
  25. package/templates/f7-app/src/assets/css/app.css +1 -0
  26. package/templates/f7-app/src/assets/css/icons.css +50 -0
  27. package/templates/f7-app/src/assets/fonts/material-icons-outlined.woff2 +0 -0
  28. package/templates/f7-app/src/assets/fonts/material-icons-round.woff2 +0 -0
  29. package/templates/f7-app/src/domains/sales/sales.repository.ts +458 -0
  30. package/templates/f7-app/src/env.d.ts +23 -0
  31. package/templates/f7-app/src/locales/en.json +179 -0
  32. package/templates/f7-app/src/locales/fr.json +179 -0
  33. package/templates/f7-app/src/main.ts +50 -0
  34. package/templates/f7-app/src/modules/demo/components/DemoBusLog.vue +26 -0
  35. package/templates/f7-app/src/modules/demo/components/DemoCreateOrderSheet.vue +160 -0
  36. package/templates/f7-app/src/modules/demo/components/DemoOrderList.vue +73 -0
  37. package/templates/f7-app/src/modules/demo/components/DemoPipelineBenchmark.vue +52 -0
  38. package/templates/f7-app/src/modules/demo/components/DemoStatCards.vue +63 -0
  39. package/templates/f7-app/src/modules/demo/composables/useReactiveDemo.ts +168 -0
  40. package/templates/f7-app/src/modules/demo/router/routes/demo.routes.ts +34 -0
  41. package/templates/f7-app/src/modules/demo/views/DemoView.vue +126 -0
  42. package/templates/f7-app/src/modules/demo/views/OrderDetailView.vue +131 -0
  43. package/templates/f7-app/src/modules/demo/views/OrderSearchView.vue +106 -0
  44. package/templates/f7-app/src/modules/home/composables/useHomeFeatures.ts +133 -0
  45. package/templates/f7-app/src/modules/home/router/routes/home.routes.ts +29 -0
  46. package/templates/f7-app/src/modules/home/views/FeatureDetailView.vue +108 -0
  47. package/templates/f7-app/src/modules/home/views/HomeView.vue +53 -0
  48. package/templates/f7-app/src/modules/settings/router/routes/settings.routes.ts +16 -0
  49. package/templates/f7-app/src/modules/settings/views/SettingsView.vue +126 -0
  50. package/templates/f7-app/src/plugins/capacitor/index.ts +14 -0
  51. package/templates/f7-app/src/plugins/capacitor/useAndroidBackButton.ts +68 -0
  52. package/templates/f7-app/src/plugins/capacitor/useKeyboard.ts +61 -0
  53. package/templates/f7-app/src/plugins/capacitor/useSplashScreen.ts +11 -0
  54. package/templates/f7-app/src/plugins/capacitor/useStatusBar.ts +15 -0
  55. package/templates/f7-app/src/plugins/framework7.plugin.ts +41 -0
  56. package/templates/f7-app/src/plugins/i18n.plugin.ts +10 -0
  57. package/templates/f7-app/src/plugins/seed.plugin.ts +17 -0
  58. package/templates/f7-app/src/plugins/sqlite.plugin.ts +21 -0
  59. package/templates/f7-app/src/router/global/global.routes.ts +16 -0
  60. package/templates/f7-app/src/router/index.ts +18 -0
  61. package/templates/f7-app/src/shared/components/error/404.vue +12 -0
  62. package/templates/f7-app/src/shared/components/metrics/MetricsPanel.vue +79 -0
  63. package/templates/f7-app/src/shared/composables/theme/useAppTheme.ts +68 -0
  64. package/templates/f7-app/src/shared/composables/useTabbarVisibility.ts +50 -0
  65. package/templates/f7-app/src/shared/database/database.ts +86 -0
  66. package/templates/f7-app/src/shared/database/index.ts +3 -0
  67. package/templates/f7-app/src/shared/database/migrations.ts +81 -0
  68. package/templates/f7-app/src/shared/database/queries.ts +22 -0
  69. package/templates/f7-app/src/shared/database/schema.ts +59 -0
  70. package/templates/f7-app/src/shared/utils/resolvers/resolvers.ts +152 -0
  71. package/templates/f7-app/tests/icons.test.ts +81 -0
  72. package/templates/f7-app/tests/sales.repository.test.ts +329 -0
  73. package/templates/f7-app/tsconfig.json +21 -0
  74. package/templates/f7-app/tsconfig.node.json +14 -0
  75. package/templates/f7-app/vite.config.ts +95 -0
@@ -0,0 +1,179 @@
1
+ {
2
+ "tabs": {
3
+ "home": "Aperçu",
4
+ "demo": "Réactif",
5
+ "settings": "Réglages"
6
+ },
7
+ "home": {
8
+ "title": "Aperçu",
9
+ "intro": "est un modèle pour applications mobiles hors-ligne : Vue 3, Framework7 et Capacitor sur une base SQLite locale lue de façon réactive.",
10
+ "included": "Fonctionnalités",
11
+ "next": "Voir le fonctionnement",
12
+ "welcome": "Bienvenue",
13
+ "tapAny": "Touchez une fonctionnalité — chacune s'ouvre dans cet onglet avec sa transition et son style de navbar."
14
+ },
15
+ "demo": {
16
+ "title": "Réactif",
17
+ "intro": "Un petit schéma relationnel — clients, produits, commandes, lignes et étiquettes — lu par jointures et agrégats. Rien sur cet écran ne demande de rafraîchir.",
18
+ "clear": "Réinitialiser les données",
19
+ "refetching": "rechargement…",
20
+ "empty": "Aucune commande. Utilisez le bouton en bas à droite.",
21
+ "pipelining": "Lectures en parallèle",
22
+ "measure": "Mesurer",
23
+ "measuring": "Mesure…",
24
+ "parallel": "Ensemble",
25
+ "sequential": "Une par une",
26
+ "ratio": "Plus rapide de",
27
+ "pipeliningNative": "{count} lectures lancées ensemble, puis les mêmes {count} une par une. Les lectures restent hors du verrou d'écriture et le pont natif les parallélise : « ensemble » devrait gagner largement.",
28
+ "pipeliningWeb": "{count} lectures lancées ensemble, puis les mêmes {count} une par une. Dans un navigateur, cette mesure n'a pas de sens — voir la note sous le résultat.",
29
+ "ratioNative": "Au-dessus de 1x, les lectures se chevauchent réellement sur le pont natif. Proche de 1x signifierait que quelque chose les sérialise.",
30
+ "ratioWeb": "Attendu sur {platform} : le chemin web est sql.js en mémoire, sans pont à paralléliser — les lancer ensemble n'apporte rien. Le gain se mesure sur un appareil.",
31
+ "data": "Données",
32
+ "diagnostics": "Diagnostics",
33
+ "searchPlaceholder": "Chercher une commande ou un client",
34
+ "orders": "Commandes",
35
+ "lines": "{count} lignes",
36
+ "seed": "Générer des données",
37
+ "tiles": {
38
+ "draft": "Brouillon",
39
+ "confirmed": "Confirmées",
40
+ "delivered": "Livrées",
41
+ "committed": "Engagé",
42
+ "ofOrders": "sur {count} commandes",
43
+ "ofTotal": "sur {total} au total"
44
+ },
45
+ "bus": "Bus de changements",
46
+ "busHint": "Chaque événement publié par une écriture. Les requêtes écoutent le même flux.",
47
+ "busEmpty": "Aucun événement.",
48
+ "swipeHint": "Glissez une ligne pour l'action rapide, touchez-la pour toutes.",
49
+ "newOrder": "Nouvelle commande",
50
+ "reference": "Référence",
51
+ "customer": "Client",
52
+ "products": "Produits",
53
+ "total": "Total",
54
+ "save": "Enregistrer",
55
+ "cancel": "Annuler",
56
+ "confirm": "Confirmer",
57
+ "deliver": "Marquer livrée",
58
+ "backToDraft": "Remettre en brouillon",
59
+ "delete": "Supprimer",
60
+ "searchHint": "Tapez pour chercher par référence ou client.",
61
+ "noMatches": "Aucun résultat.",
62
+ "order": "Commande",
63
+ "orderGone": "Cette commande n'existe plus.",
64
+ "lineItems": "Lignes",
65
+ "searchTitle": "Rechercher",
66
+ "statusDraft": "Brouillon",
67
+ "statusConfirmed": "Confirmée",
68
+ "statusDelivered": "Livrée",
69
+ "deleteConfirm": "Supprimer cette commande et ses lignes ?",
70
+ "tapHint": "Touchez une commande pour l'ouvrir.",
71
+ "allOrders": "Commandes récentes",
72
+ "matches": "{count} résultats"
73
+ },
74
+ "metrics": {
75
+ "totalQueries": "Requêtes exécutées",
76
+ "avgQueryTime": "Durée moyenne",
77
+ "cacheHitRate": "Taux de cache",
78
+ "activeListeners": "Écouteurs actifs",
79
+ "refetches": "Rechargements : {table}",
80
+ "title": "Métriques",
81
+ "summary": "Résumé",
82
+ "refetchesTitle": "Rechargements par table",
83
+ "noRefetches": "Aucun rechargement.",
84
+ "slowest": "Requêtes les plus lentes",
85
+ "noQueries": "Aucune requête enregistrée.",
86
+ "calls": "{count} appels",
87
+ "reset": "Réinitialiser"
88
+ },
89
+ "settings": {
90
+ "title": "Réglages",
91
+ "locale": "Langue",
92
+ "queryMetrics": "Métriques de requêtes",
93
+ "slowest": "Requêtes les plus lentes",
94
+ "noQueries": "Aucune requête enregistrée.",
95
+ "resetMetrics": "Réinitialiser",
96
+ "appearance": "Apparence",
97
+ "theme": "Thème",
98
+ "themeAuto": "Auto (appareil)",
99
+ "darkMode": "Mode sombre",
100
+ "language": "Langue",
101
+ "tryIt": "À essayer",
102
+ "testToast": "Afficher un toast",
103
+ "testDialog": "Afficher un dialogue",
104
+ "testSheet": "Ouvrir une feuille",
105
+ "about": "À propos",
106
+ "appName": "Nom",
107
+ "version": "Version",
108
+ "platform": "Plateforme",
109
+ "aboutUs": "Qui sommes-nous",
110
+ "toastText": "Ceci est un toast Framework7.",
111
+ "dialogTitle": "Dialogue",
112
+ "dialogText": "Dialogues, feuilles et toasts viennent de Framework7 - sans dépendance supplémentaire.",
113
+ "sheetText": "Une feuille modale. Le bouton retour Android la ferme avant de naviguer.",
114
+ "aboutText": "Construit sur {'@'}cavulsqa/mobile-db, reactive-db et reactive-vue. Remplacez ce texte par le vôtre.",
115
+ "footer": "Modifiez ces écrans dans src/modules/settings/."
116
+ },
117
+ "errors": {
118
+ "notFoundTitle": "Introuvable",
119
+ "notFound": "Cet écran n'existe pas."
120
+ },
121
+ "features": {
122
+ "tabs": {
123
+ "title": "Shell à onglets",
124
+ "subtitle": "Onglets déclarés en données",
125
+ "text": "Chaque onglet a sa propre vue et son historique, chargé à la demande."
126
+ },
127
+ "f7": {
128
+ "title": "Framework7",
129
+ "subtitle": "iOS et Material natifs",
130
+ "text": "Les deux thèmes sont fournis ; les réglages en fixent un ou suivent l'appareil."
131
+ },
132
+ "sqlite": {
133
+ "title": "SQLite",
134
+ "subtitle": "Kysely, hors-ligne d'abord",
135
+ "text": "Les écritures se relaient sur la connexion native, les lectures restent parallèles."
136
+ },
137
+ "reactive": {
138
+ "title": "Requêtes réactives",
139
+ "subtitle": "Sans invalidation manuelle",
140
+ "text": "Une écriture annonce ses tables et chaque requête concernée se relance."
141
+ },
142
+ "back": {
143
+ "title": "Bouton retour Android",
144
+ "subtitle": "Ferme la couche du dessus",
145
+ "text": "Actions, dialogue, feuille, popover, popup, panneau - puis l'historique."
146
+ },
147
+ "keyboard": {
148
+ "title": "Gestion du clavier",
149
+ "subtitle": "Le champ reste visible",
150
+ "text": "Défilement à chaque phase de la transition, barre d'onglets escamotée."
151
+ },
152
+ "metrics": {
153
+ "title": "Métriques",
154
+ "subtitle": "Intégrées",
155
+ "text": "Durées, cache et rechargements par table, dans le panneau de droite."
156
+ },
157
+ "dx": {
158
+ "title": "Zéro boilerplate",
159
+ "subtitle": "Auto-imports et resolvers",
160
+ "text": "API de composition, i18n, composants et icônes sans ligne d'import."
161
+ }
162
+ },
163
+ "detail": {
164
+ "what": "Quoi",
165
+ "how": "Comment",
166
+ "howText": "Cette page est une route enfant de la vue de l'onglet Accueil. La barre d'onglets ne bouge pas, le retour ne déroule que cet onglet, et les autres gardent leur écran.",
167
+ "thisScreen": "Ce que cet écran démontre",
168
+ "transition": "Transition",
169
+ "transitionNote": "Passée à chaque navigation : deux lignes peuvent ouvrir la même route différemment.",
170
+ "navbar": "Navbar",
171
+ "navbarNote": "large se replie au défilement, transparent apparaît sur le contenu, hide-on-scroll s'escamote, subnavbar reste en place.",
172
+ "nesting": "Vue imbriquée",
173
+ "nestingAfter": "Onglet Accueil",
174
+ "nestingNote": "Empilée dans l'historique de cet onglet, pas celui de l'application.",
175
+ "scrollMe": "Défilez pour voir la navbar réagir",
176
+ "row": "Ligne",
177
+ "next": "Ouvrir la suivante"
178
+ }
179
+ }
@@ -0,0 +1,50 @@
1
+ import Framework7 from "framework7/lite-bundle";
2
+ import Framework7Vue from "framework7-vue";
3
+
4
+ import "framework7/css/bundle";
5
+ import "framework7-icons";
6
+ import "./assets/css/icons.css";
7
+ import "./assets/css/app.css";
8
+
9
+ import App from "./App.vue";
10
+ import { i18n } from "./plugins/i18n.plugin";
11
+ import { seedPlugin } from "./plugins/seed.plugin";
12
+ import { sqlitePlugin } from "./plugins/sqlite.plugin";
13
+
14
+ Framework7.use(Framework7Vue);
15
+
16
+ function reportFatal(error: unknown): void {
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ console.error("[bootstrap] the app could not start:", error);
19
+
20
+ const root = document.getElementById("app");
21
+ if (!root) return;
22
+ const box = document.createElement("div");
23
+ box.setAttribute("style", "font:14px/1.5 system-ui;padding:24px;color:#b00020");
24
+ box.textContent = `The app could not start: ${message}`;
25
+ root.replaceChildren(box);
26
+ }
27
+
28
+ /**
29
+ * The database opens before mount so the first screen never renders against a missing schema.
30
+ *
31
+ * If it fails, the failure is shown. Awaiting this at the top level of the module and letting it
32
+ * reject renders an empty `#app` with an unhandled rejection nobody reads - which is exactly what
33
+ * this template did the first time it was opened in a browser.
34
+ */
35
+ async function bootstrap(): Promise<void> {
36
+ try {
37
+ await sqlitePlugin();
38
+ await seedPlugin();
39
+ } catch (error) {
40
+ reportFatal(error);
41
+ return;
42
+ }
43
+
44
+ // Components arrive through Framework7VueResolver, so nothing is registered by hand.
45
+ const app = createApp(App);
46
+ app.use(i18n);
47
+ app.mount("#app");
48
+ }
49
+
50
+ void bootstrap();
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <F7BlockTitle>{{ t("demo.bus") }}</F7BlockTitle>
3
+ <F7Block strong inset class="rounded-2xl!">
4
+ <p class="mt-0 text-sm opacity-70">{{ t("demo.busHint") }}</p>
5
+
6
+ <ul v-if="entries.length" class="m-0 list-none p-0 font-mono text-xs">
7
+ <li v-for="entry in entries" :key="entry.id" class="flex justify-between py-0.5">
8
+ <span>{{ entry.table }}.{{ entry.type }}</span>
9
+ <span class="opacity-60">{{ formatTime(entry.at) }}</span>
10
+ </li>
11
+ </ul>
12
+ <p v-else class="text-sm opacity-60">{{ t("demo.busEmpty") }}</p>
13
+ </F7Block>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import type { BusEntry } from "@/modules/demo/composables/useReactiveDemo";
18
+
19
+ defineProps<{ entries: BusEntry[] }>();
20
+
21
+ const { t } = useI18n();
22
+
23
+ function formatTime(at: number): string {
24
+ return new Date(at).toLocaleTimeString(undefined, { hour12: false });
25
+ }
26
+ </script>
@@ -0,0 +1,160 @@
1
+ <template>
2
+ <F7Sheet
3
+ class="demo-create-sheet"
4
+ :opened="opened"
5
+ swipe-to-close
6
+ backdrop
7
+ @sheet:closed="$emit('close')"
8
+ @sheet:open="load"
9
+ >
10
+ <F7Toolbar>
11
+ <div class="left px-3 font-medium">{{ t("demo.newOrder") }}</div>
12
+ <div class="right">
13
+ <F7Link sheet-close>{{ t("demo.cancel") }}</F7Link>
14
+ </div>
15
+ </F7Toolbar>
16
+
17
+ <F7PageContent class="pb-6!">
18
+ <F7List strong inset dividers class="rounded-2xl! mt-3!">
19
+ <F7ListInput
20
+ :label="t('demo.reference')"
21
+ type="text"
22
+ :value="reference"
23
+ @input="reference = ($event.target as HTMLInputElement).value"
24
+ />
25
+ <F7ListItem
26
+ :title="t('demo.customer')"
27
+ smart-select
28
+ :smart-select-params="{ openIn: 'sheet', closeOnSelect: true }"
29
+ >
30
+ <select :value="String(customerId)" @change="onCustomer">
31
+ <option v-for="customer in customers" :key="customer.id" :value="String(customer.id)">
32
+ {{ customer.name }} — {{ customer.city }}
33
+ </option>
34
+ </select>
35
+ </F7ListItem>
36
+ </F7List>
37
+
38
+ <F7BlockTitle>{{ t("demo.products") }}</F7BlockTitle>
39
+ <F7List strong inset dividers class="rounded-2xl!">
40
+ <F7ListItem v-for="product in products" :key="product.id" :title="product.name">
41
+ <template #footer>{{ money(product.price_cents) }}</template>
42
+ <template #after>
43
+ <F7Stepper
44
+ small
45
+ raised
46
+ :value="quantityOf(product.id)"
47
+ :min="0"
48
+ :max="99"
49
+ @stepper:change="(value: number) => setQuantity(product.id, value)"
50
+ />
51
+ </template>
52
+ </F7ListItem>
53
+ </F7List>
54
+
55
+ <F7Block class="flex items-center justify-between">
56
+ <span class="text-sm opacity-60">{{ t("demo.total") }}</span>
57
+ <span class="text-lg font-semibold tabular-nums">{{ money(totalCents) }}</span>
58
+ </F7Block>
59
+
60
+ <F7Block>
61
+ <F7Button large fill :disabled="!canSave" @click="save">{{ t("demo.save") }}</F7Button>
62
+ </F7Block>
63
+ </F7PageContent>
64
+ </F7Sheet>
65
+ </template>
66
+
67
+ <script setup lang="ts">
68
+ import {
69
+ listCustomers,
70
+ listProducts,
71
+ nextOrderReference,
72
+ type ProductRow,
73
+ } from "@/domains/sales/sales.repository";
74
+ import { getDatabase } from "@/shared/database/database";
75
+
76
+ const props = defineProps<{ opened: boolean }>();
77
+ const emit = defineEmits<{
78
+ close: [];
79
+ save: [
80
+ payload: {
81
+ customerId: number;
82
+ reference: string;
83
+ lines: Array<{ productId: number; quantity: number; unitPriceCents: number }>;
84
+ },
85
+ ];
86
+ }>();
87
+
88
+ const { t } = useI18n();
89
+
90
+ const customers = ref<Array<{ id: number; name: string; city: string }>>([]);
91
+ const products = ref<ProductRow[]>([]);
92
+ const quantities = ref<Record<number, number>>({});
93
+ const reference = ref("");
94
+ const customerId = ref(0);
95
+
96
+ const formatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 });
97
+ const money = (cents: number) => formatter.format(cents / 100);
98
+
99
+ /** Loaded when the sheet opens rather than on mount, so the reference reflects the latest order. */
100
+ async function load() {
101
+ const db = getDatabase().db;
102
+ const [loadedCustomers, loadedProducts, nextReference] = await Promise.all([
103
+ listCustomers(db),
104
+ listProducts(db),
105
+ nextOrderReference(db),
106
+ ]);
107
+
108
+ customers.value = loadedCustomers;
109
+ products.value = loadedProducts;
110
+ reference.value = nextReference;
111
+ customerId.value = loadedCustomers[0]?.id ?? 0;
112
+ quantities.value = {};
113
+ }
114
+
115
+ watch(
116
+ () => props.opened,
117
+ (isOpen) => {
118
+ if (isOpen) void load();
119
+ },
120
+ );
121
+
122
+ const quantityOf = (productId: number) => quantities.value[productId] ?? 0;
123
+
124
+ function setQuantity(productId: number, value: number) {
125
+ quantities.value = { ...quantities.value, [productId]: value };
126
+ }
127
+
128
+ function onCustomer(event: Event) {
129
+ customerId.value = Number((event.target as HTMLSelectElement).value);
130
+ }
131
+
132
+ const lines = computed(() =>
133
+ products.value
134
+ .filter((product) => quantityOf(product.id) > 0)
135
+ .map((product) => ({
136
+ productId: product.id,
137
+ quantity: quantityOf(product.id),
138
+ unitPriceCents: product.price_cents,
139
+ })),
140
+ );
141
+
142
+ const totalCents = computed(() =>
143
+ lines.value.reduce((sum, line) => sum + line.quantity * line.unitPriceCents, 0),
144
+ );
145
+
146
+ const canSave = computed(() => customerId.value > 0 && lines.value.length > 0);
147
+
148
+ function save() {
149
+ if (!canSave.value) return;
150
+ emit("save", { customerId: customerId.value, reference: reference.value, lines: lines.value });
151
+ }
152
+ </script>
153
+
154
+ <style scoped>
155
+ .demo-create-sheet {
156
+ height: auto;
157
+ max-height: 85vh;
158
+ overflow: auto;
159
+ }
160
+ </style>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <!--
3
+ `swiper-no-swiping` keeps the swipeable tabs out of this list: without it a horizontal drag
4
+ revealed the swipeout actions and moved the tab pane at the same time.
5
+ -->
6
+ <F7List strong inset dividers class="rounded-2xl! swiper-no-swiping">
7
+ <!--
8
+ Swipe is the fast path and the tap opens the full action sheet, so a status change costs one
9
+ gesture instead of a trip through a menu.
10
+ -->
11
+ <F7ListItem
12
+ v-for="order in orders"
13
+ :key="order.id"
14
+ swipeout
15
+ link="#"
16
+ @click="$emit('open', order)"
17
+ >
18
+ <template #media>
19
+ <F7Icon :f7="statusIcon(order.status)" :color="statusColor(order.status)" size="22" />
20
+ </template>
21
+
22
+ <template #title>
23
+ <span class="font-medium">{{ order.reference }}</span>
24
+ </template>
25
+
26
+ <template #after>
27
+ <span class="text-[15px] font-semibold tabular-nums">{{ money(order.totalCents) }}</span>
28
+ </template>
29
+
30
+ <template #subtitle>
31
+ <span class="text-[13px] opacity-60">
32
+ {{ order.customerName }} · {{ t("demo.lines", { count: order.lines }) }}
33
+ </span>
34
+ </template>
35
+
36
+ <F7SwipeoutActions right>
37
+ <F7SwipeoutButton
38
+ v-if="order.status !== 'delivered'"
39
+ color="orange"
40
+ close
41
+ @click="$emit('advance', order.id)"
42
+ >
43
+ {{ order.status === "draft" ? t("demo.confirm") : t("demo.deliver") }}
44
+ </F7SwipeoutButton>
45
+ <F7SwipeoutButton color="red" close @click="$emit('remove', order.id)">
46
+ {{ t("demo.delete") }}
47
+ </F7SwipeoutButton>
48
+ </F7SwipeoutActions>
49
+ </F7ListItem>
50
+ </F7List>
51
+ </template>
52
+
53
+ <script setup lang="ts">
54
+ import type { OrderRow } from "@/domains/sales/sales.repository";
55
+
56
+ defineProps<{ orders: OrderRow[] }>();
57
+ defineEmits<{ open: [order: OrderRow]; advance: [orderId: number]; remove: [orderId: number] }>();
58
+
59
+ const { t } = useI18n();
60
+
61
+ const formatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 });
62
+ const money = (cents: number) => formatter.format(cents / 100);
63
+
64
+ const statusColor = (status: string) =>
65
+ status === "delivered" ? "green" : status === "confirmed" ? "orange" : "gray";
66
+
67
+ const statusIcon = (status: string) =>
68
+ status === "delivered"
69
+ ? "checkmark_seal_fill"
70
+ : status === "confirmed"
71
+ ? "shippingbox_fill"
72
+ : "doc_plaintext";
73
+ </script>
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <F7BlockTitle>{{ t("demo.pipelining") }}</F7BlockTitle>
3
+ <F7Block strong inset class="rounded-2xl!">
4
+ <p class="mt-0 text-sm opacity-70">
5
+ {{
6
+ isNative
7
+ ? t("demo.pipeliningNative", { count: readsPerRun })
8
+ : t("demo.pipeliningWeb", { count: readsPerRun })
9
+ }}
10
+ </p>
11
+
12
+ <F7Button fill :disabled="measuring" @click="$emit('measure')">
13
+ {{ measuring ? t("demo.measuring") : t("demo.measure") }}
14
+ </F7Button>
15
+
16
+ <div v-if="result" class="mt-3">
17
+ <div class="grid grid-cols-3 gap-2 text-center">
18
+ <div>
19
+ <div class="text-xs opacity-60">{{ t("demo.parallel") }}</div>
20
+ <div class="font-mono text-base">{{ result.parallelMs }} ms</div>
21
+ </div>
22
+ <div>
23
+ <div class="text-xs opacity-60">{{ t("demo.sequential") }}</div>
24
+ <div class="font-mono text-base">{{ result.sequentialMs }} ms</div>
25
+ </div>
26
+ <div>
27
+ <div class="text-xs opacity-60">{{ t("demo.ratio") }}</div>
28
+ <div class="font-mono text-base font-bold">{{ result.ratio }}&times;</div>
29
+ </div>
30
+ </div>
31
+
32
+ <p class="mb-0 mt-3 text-xs opacity-70">
33
+ {{ isNative ? t("demo.ratioNative") : t("demo.ratioWeb", { platform }) }}
34
+ </p>
35
+ </div>
36
+ </F7Block>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import type { PipelineResult } from "@/modules/demo/composables/useReactiveDemo";
41
+
42
+ defineProps<{
43
+ result: PipelineResult | null;
44
+ measuring: boolean;
45
+ readsPerRun: number;
46
+ isNative: boolean;
47
+ platform: string;
48
+ }>();
49
+ defineEmits<{ measure: [] }>();
50
+
51
+ const { t } = useI18n();
52
+ </script>
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <div class="grid grid-cols-2 gap-2 px-3">
3
+ <div
4
+ v-for="tile in tiles"
5
+ :key="tile.labelKey"
6
+ class="rounded-2xl px-3 py-2.5"
7
+ :style="{ background: 'var(--f7-list-bg-color, rgb(128 128 128 / 10%))' }"
8
+ >
9
+ <div class="flex items-center gap-1.5">
10
+ <F7Icon :f7="tile.icon" :color="tile.color" size="16" />
11
+ <span class="text-[11px] uppercase tracking-wide opacity-55">{{ t(tile.labelKey) }}</span>
12
+ </div>
13
+ <div class="mt-0.5 text-xl font-semibold tabular-nums leading-tight">{{ tile.value }}</div>
14
+ <div v-if="tile.hint" class="text-[11px] opacity-45">{{ tile.hint }}</div>
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import type { DashboardStats } from "@/domains/sales/sales.repository";
21
+
22
+ const props = defineProps<{ stats: DashboardStats }>();
23
+ const { t } = useI18n();
24
+
25
+ const formatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 });
26
+ const money = (cents: number) => formatter.format(cents / 100);
27
+
28
+ /**
29
+ * Status counts rather than plain totals, because they are what actually moves when you use the
30
+ * screen: confirm one order and three of these four change at once, without anything telling them
31
+ * to. That is the whole point being demonstrated.
32
+ */
33
+ const tiles = computed(() => [
34
+ {
35
+ labelKey: "demo.tiles.draft",
36
+ icon: "doc_plaintext",
37
+ color: "gray",
38
+ value: String(props.stats.draft),
39
+ hint: t("demo.tiles.ofOrders", { count: props.stats.orders }),
40
+ },
41
+ {
42
+ labelKey: "demo.tiles.confirmed",
43
+ icon: "shippingbox_fill",
44
+ color: "orange",
45
+ value: String(props.stats.confirmed),
46
+ hint: "",
47
+ },
48
+ {
49
+ labelKey: "demo.tiles.delivered",
50
+ icon: "checkmark_seal_fill",
51
+ color: "green",
52
+ value: String(props.stats.delivered),
53
+ hint: "",
54
+ },
55
+ {
56
+ labelKey: "demo.tiles.committed",
57
+ icon: "money_dollar_circle_fill",
58
+ color: "teal",
59
+ value: money(props.stats.committedCents),
60
+ hint: t("demo.tiles.ofTotal", { total: money(props.stats.revenueCents) }),
61
+ },
62
+ ]);
63
+ </script>