@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.
- package/README.md +12 -1
- package/bin/create.mjs +18 -25
- package/lib/templates.mjs +56 -0
- package/package.json +13 -5
- package/templates/f7-app/auto-imports.d.ts +9 -0
- package/templates/f7-app/package.json +6 -3
- package/templates/f7-app/src/env.d.ts +1 -0
- package/templates/f7-app/src/main.ts +3 -0
- package/templates/f7-app/src/plugins/bootstrapError.ts +2 -0
- package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
- package/templates/f7-app/src/shared/recorder/capacitorSink.ts +124 -0
- package/templates/f7-app/vite.config.ts +1 -0
- package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
- package/templates/m3e-app/.claude/rules/database.md +106 -0
- package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
- package/templates/m3e-app/.claude/rules/modules.md +43 -0
- package/templates/m3e-app/.claude/rules/native.md +60 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
- package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
- package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
- package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
- package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
- package/templates/m3e-app/.env.example +32 -0
- package/templates/m3e-app/CLAUDE.md +93 -0
- package/templates/m3e-app/auto-imports.d.ts +851 -0
- package/templates/m3e-app/capacitor.config.ts +44 -0
- package/templates/m3e-app/components.d.ts +252 -0
- package/templates/m3e-app/index.html +16 -0
- package/templates/m3e-app/package.json +107 -0
- package/templates/m3e-app/src/App.vue +129 -0
- package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
- package/templates/m3e-app/src/app/scroll.config.ts +20 -0
- package/templates/m3e-app/src/app/storage.config.ts +65 -0
- package/templates/m3e-app/src/app/tabs.ts +33 -0
- package/templates/m3e-app/src/app/theme.config.ts +51 -0
- package/templates/m3e-app/src/assets/css/app.css +18 -0
- package/templates/m3e-app/src/assets/css/base.css +53 -0
- package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
- package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
- package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
- package/templates/m3e-app/src/assets/css/m3e.css +6 -0
- package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
- package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
- package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
- package/templates/m3e-app/src/env.d.ts +40 -0
- package/templates/m3e-app/src/locales/ar.json +1260 -0
- package/templates/m3e-app/src/locales/en.json +1260 -0
- package/templates/m3e-app/src/locales/fr.json +1260 -0
- package/templates/m3e-app/src/main.ts +41 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
- package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
- package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
- package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
- package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
- package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
- package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
- package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
- package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
- package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
- package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
- package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
- package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
- package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
- package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
- package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
- package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
- package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
- package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
- package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
- package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
- package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
- package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
- package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
- package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
- package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
- package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
- package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
- package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
- package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
- package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
- package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
- package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
- package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
- package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
- package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
- package/templates/m3e-app/src/modules/settings/types.ts +7 -0
- package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
- package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
- package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
- package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
- package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
- package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
- package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
- package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
- package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
- package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
- package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
- package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
- package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
- package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
- package/templates/m3e-app/src/router/index.ts +20 -0
- package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
- package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
- package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
- package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
- package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
- package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
- package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
- package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
- package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
- package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
- package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
- package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
- package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
- package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
- package/templates/m3e-app/src/shared/database/database.ts +219 -0
- package/templates/m3e-app/src/shared/database/index.ts +3 -0
- package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
- package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
- package/templates/m3e-app/src/shared/database/queries.ts +26 -0
- package/templates/m3e-app/src/shared/database/schema.ts +153 -0
- package/templates/m3e-app/src/shared/database/storage.ts +64 -0
- package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
- package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
- package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
- package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
- package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
- package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
- package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
- package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
- package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
- package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
- package/templates/m3e-app/tests/locales.test.ts +73 -0
- package/templates/m3e-app/tests/migrations.test.ts +74 -0
- package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
- package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
- package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
- package/templates/m3e-app/tests/storage.test.ts +117 -0
- package/templates/m3e-app/tests/textDirection.test.ts +11 -0
- package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
- package/templates/m3e-app/tsconfig.json +21 -0
- package/templates/m3e-app/vite.config.ts +144 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Schema and migrations
|
|
2
|
+
|
|
3
|
+
`src/shared/database/schema.ts` is what Kysely type-checks every query against.
|
|
4
|
+
`src/shared/database/migrations.ts` is what actually creates the tables. The two are edited
|
|
5
|
+
together, always: a field in one and not the other is a runtime error the compiler cannot see.
|
|
6
|
+
|
|
7
|
+
## Migrations
|
|
8
|
+
|
|
9
|
+
- Keys are ordered lexically and recorded once applied, so they are **numbered and never renamed**.
|
|
10
|
+
Renaming one makes it run again on a database that already has it.
|
|
11
|
+
- Never edit a migration that has shipped. Add the next one.
|
|
12
|
+
- A synced table is created with `createTableWithDefaults` when it carries the sync contract, or a
|
|
13
|
+
plain `createTable` when it does not. This template has no server, so plain tables are the norm.
|
|
14
|
+
- Declare foreign keys, and index the columns screens filter and join by. Without them every
|
|
15
|
+
dashboard aggregate is a full scan, which you will not notice until the table is large and the
|
|
16
|
+
device is slow.
|
|
17
|
+
- SQLite ignores foreign keys unless asked; `PRAGMA foreign_keys = ON` runs at the end of the
|
|
18
|
+
migration. It is per-connection, so a cascade is not something to rely on — `deleteOrder` removes
|
|
19
|
+
the lines explicitly.
|
|
20
|
+
|
|
21
|
+
## Types
|
|
22
|
+
|
|
23
|
+
- Money is **integer cents**, named `*_cents`. A float total is a rounding bug waiting for a
|
|
24
|
+
large-enough order.
|
|
25
|
+
- Timestamps are ISO strings via `nowISO()`.
|
|
26
|
+
- A price copied onto an order line is copied deliberately, so a later catalogue change does not
|
|
27
|
+
rewrite history.
|
|
28
|
+
|
|
29
|
+
## Getting an inserted id
|
|
30
|
+
|
|
31
|
+
Use `insertId`, never `.returning(...)`:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const inserted = await trx.insertInto("sales_order").values({ ... }).executeTakeFirstOrThrow();
|
|
35
|
+
const orderId = Number(inserted.insertId ?? 0);
|
|
36
|
+
if (!orderId) throw new Error("the order was written but the database reported no id for it");
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`insertId` is the portable answer. Every engine reports it — the worker engines from
|
|
40
|
+
`last_insert_rowid()`, the sql.js test dialect the same way — so a repository written against it
|
|
41
|
+
behaves identically in tests and on a device. `.returning(...)` does work on the worker engines, but
|
|
42
|
+
it is the one thing that differs between them: the Capacitor plugin runs a statement issued inside an
|
|
43
|
+
open transaction through `query()`, which executes it and silently drops its RETURNING rows, so
|
|
44
|
+
`.returning("id").executeTakeFirstOrThrow()` threw `no result` from an insert that had in fact
|
|
45
|
+
succeeded. `@cavulsqa/mobile-db` now throws a message that says so instead.
|
|
46
|
+
|
|
47
|
+
## The web path is the device path
|
|
48
|
+
|
|
49
|
+
Both run the same engine: SQLite compiled to WebAssembly, in a worker, with the database file in
|
|
50
|
+
OPFS. There is no Capacitor SQLite plugin in this template and no sql.js outside the tests. So:
|
|
51
|
+
|
|
52
|
+
- **Browser data survives a reload.** OPFS is durable storage, not memory. Clear it from
|
|
53
|
+
Diagnostics, or through the browser's site-data controls.
|
|
54
|
+
- A bug reproduced in the browser is very likely the same bug as on the device, which was not true
|
|
55
|
+
when the two ran different engines.
|
|
56
|
+
- `localStorage["app.storage.force"]` pins the chain to one engine id, for comparing them.
|
|
57
|
+
|
|
58
|
+
What still does not transfer is **timing**. A phone's storage and CPU are nothing like a laptop's,
|
|
59
|
+
and the worker is serial either way, so a ratio measured in a browser says nothing about the device.
|
|
60
|
+
Run the Diagnostics benchmark on hardware.
|
|
61
|
+
|
|
62
|
+
## Proof obligations
|
|
63
|
+
|
|
64
|
+
A schema change needs a test in `tests/` that runs the migration and the affected queries against
|
|
65
|
+
sql.js. `tests/sales.repository.test.ts` is the pattern: build a Kysely on `createSqlJsDialect()`,
|
|
66
|
+
migrate, then assert on real rows — including the arithmetic. A total that type-checks can still be
|
|
67
|
+
computed wrong.
|
|
68
|
+
|
|
69
|
+
## Writing a lot of rows
|
|
70
|
+
|
|
71
|
+
Measured on a phone, at 100k rows, per row written:
|
|
72
|
+
|
|
73
|
+
| how | per row |
|
|
74
|
+
| ------------------------------------------------ | -------------- |
|
|
75
|
+
| one insert per statement, each its own commit | 8-13 ms |
|
|
76
|
+
| one insert per statement, inside one transaction | ~0.45 ms |
|
|
77
|
+
| multi-row insert, ~150 rows per statement | 0.066-0.115 ms |
|
|
78
|
+
|
|
79
|
+
Roughly a hundredfold between the worst and best way to write the same row. SQLite caps parameters
|
|
80
|
+
per statement, so 150 rows of five columns is about the practical ceiling for one insert - chunk by
|
|
81
|
+
parameter budget, not by a round number.
|
|
82
|
+
|
|
83
|
+
## Why a big write freezes the screen, and what to do
|
|
84
|
+
|
|
85
|
+
The database runs in one worker, and that worker is serial. A read cannot overtake a write already
|
|
86
|
+
in flight; it waits for everything queued ahead of it. So the cost to the UI is not how fast the
|
|
87
|
+
write is, it is **how much work the write committed to before the read arrived**.
|
|
88
|
+
|
|
89
|
+
Time a screen's read waits when it lands during a 1000-row write:
|
|
90
|
+
|
|
91
|
+
| write strategy | read waits |
|
|
92
|
+
| ---------------------------------------------- | ------------- |
|
|
93
|
+
| 1000 single inserts in one transaction | ~350-690 ms |
|
|
94
|
+
| 150 rows per statement, one transaction | ~36-53 ms |
|
|
95
|
+
| ten transactions of 100, awaited one at a time | **~21-32 ms** |
|
|
96
|
+
|
|
97
|
+
A naive loop stalls the screen for most of a second. Chunked transactions bring it under the
|
|
98
|
+
threshold anyone notices, and the reason is mechanical: awaiting each chunk means only one chunk is
|
|
99
|
+
ever queued, so an arriving read waits for 100 rows instead of 1000.
|
|
100
|
+
|
|
101
|
+
**So: write in chunks of about a hundred rows, use multi-row inserts inside each chunk, and await
|
|
102
|
+
each chunk before starting the next.** Do not wrap a thousand rows in one transaction to be fast -
|
|
103
|
+
it is faster in total and far worse for anyone looking at the screen while it runs.
|
|
104
|
+
|
|
105
|
+
The Diagnostics benchmark measures all three strategies, so this is checkable on any device rather
|
|
106
|
+
than taken on faith.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# UI: Material 3 Expressive on a Framework7 engine
|
|
2
|
+
|
|
3
|
+
The full workflow is the `m3-expressive` skill. These are the guardrails it rests on, each one a way
|
|
4
|
+
the screen breaks silently.
|
|
5
|
+
|
|
6
|
+
## Framework7 is the engine, not the look
|
|
7
|
+
|
|
8
|
+
Only five Framework7 components resolve: `F7App`, `F7Views`, `F7View`, `F7Page`, `F7PageContent`
|
|
9
|
+
(the allowlist in `src/shared/utils/resolvers/resolvers.ts`). Routing, per-tab history and page
|
|
10
|
+
events come from them; everything visible is an `M3*` component. A visual `f7-*` component brings
|
|
11
|
+
Framework7's styling and modal stack back and fights the M3 overlay stack for Android back.
|
|
12
|
+
|
|
13
|
+
`f7`, `f7ready` are auto-imported; `f7route` / `f7router` arrive as props of a route component:
|
|
14
|
+
`defineProps<{ f7route: Router.Route; f7router: Router.Router }>()`. Inside shared components, reach
|
|
15
|
+
the router with `useViewRouter(el)`.
|
|
16
|
+
|
|
17
|
+
## CSS is layered
|
|
18
|
+
|
|
19
|
+
`assets/css/app.css` declares `@layer framework7, theme, base, components, utilities`. Framework7's
|
|
20
|
+
CSS is the lowest layer and the M3 components sit below Tailwind's utilities, so a utility on a
|
|
21
|
+
component always wins and `!important` is never needed. New global CSS goes in a layer; component
|
|
22
|
+
CSS goes in the component's `<style scoped>`.
|
|
23
|
+
|
|
24
|
+
Layers only settle conflicts: a Framework7 rule nothing else contradicts still applies. Its core
|
|
25
|
+
sizes every bare `button` to `width: 100%` - the M3 components undo it for their own buttons, so a
|
|
26
|
+
hand-made `<button>` must set its width or use an `M3*` button.
|
|
27
|
+
|
|
28
|
+
In `<style scoped>`, wrap the whole selector: `:global(.parent .child)`. Vue compiles
|
|
29
|
+
`:global(.parent) .child` to `.parent` alone, silently styling the wrong element.
|
|
30
|
+
|
|
31
|
+
## Tokens only
|
|
32
|
+
|
|
33
|
+
Tailwind's default palette, radii, shadows and easings are cleared in
|
|
34
|
+
`assets/css/theme/tailwind.css` and refilled from `--md-sys-*`. A class like `bg-red-500` does not
|
|
35
|
+
exist. Colours: roles. Corners: the shape scale. Type: `type-*`. Hex values live in
|
|
36
|
+
`src/app/theme.config.ts` only.
|
|
37
|
+
|
|
38
|
+
## Icons are SVG components, checked at build time
|
|
39
|
+
|
|
40
|
+
`<i-ms-<name>-rounded />` from Material Symbols (outlined: `-outline-rounded`, filled: `-rounded`).
|
|
41
|
+
`autoInstall` is off, so a wrong name fails the build instead of rendering nothing. In TS:
|
|
42
|
+
`import Icon from "~icons/material-symbols/<name>"` and `markRaw` it before putting it in reactive
|
|
43
|
+
state.
|
|
44
|
+
|
|
45
|
+
## Proof obligations
|
|
46
|
+
|
|
47
|
+
`vp check`, `pnpm type-check` and `vp test` pass. Say what you saw run - light and dark, compact width,
|
|
48
|
+
reduced motion - and say plainly when you have not seen it on a device.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Modules, domains and shared
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
modules/<feature>/router/routes/<feature>.routes.ts default export, Router.RouteParameters[]
|
|
5
|
+
modules/<feature>/views/<Name>View.vue thin, presentational
|
|
6
|
+
modules/<feature>/components/*.vue props in, emits out
|
|
7
|
+
modules/<feature>/composables/use*.ts the feature's state and actions
|
|
8
|
+
domains/<domain>/<domain>.repository.ts SQL only
|
|
9
|
+
shared/… what two modules genuinely both need
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## The seam
|
|
13
|
+
|
|
14
|
+
A **view** wires a composable to components. If it holds business logic, that logic belongs in the
|
|
15
|
+
composable; if it holds SQL, that belongs in a repository.
|
|
16
|
+
|
|
17
|
+
A **composable** owns state, queries and actions for one feature. It may import repositories and
|
|
18
|
+
the reactive query helpers. It returns refs and functions, never markup.
|
|
19
|
+
|
|
20
|
+
A **repository** is plain functions over Kysely. No `ref`, no lifecycle, no Framework7, no imports
|
|
21
|
+
from `modules/`. It takes the database as a parameter — that is what makes it testable, and reaching
|
|
22
|
+
for the singleton instead is what made the first version of this template untestable.
|
|
23
|
+
|
|
24
|
+
A **component** takes props and emits events. It does not query the database.
|
|
25
|
+
|
|
26
|
+
## Direction of dependencies
|
|
27
|
+
|
|
28
|
+
`modules → domains → shared → packages`. Never backwards. A repository importing from a module, or
|
|
29
|
+
`shared` importing from `modules`, means something is in the wrong place.
|
|
30
|
+
|
|
31
|
+
## Adding a feature
|
|
32
|
+
|
|
33
|
+
1. `modules/<feature>/` with the four folders.
|
|
34
|
+
2. Its routes in `router/routes/<feature>.routes.ts`, registered in `src/router/index.ts` before the
|
|
35
|
+
global catch-all.
|
|
36
|
+
3. A tab in `src/app/tabs.ts` only if it is a top-level section. Otherwise it is a pushed page, and
|
|
37
|
+
pushed pages are an `AppPage` with `back`, which hides the navigation bar.
|
|
38
|
+
4. Strings in **both** `locales/en.json` and `locales/fr.json`. A missing key renders the key.
|
|
39
|
+
|
|
40
|
+
## shared/ is not a junk drawer
|
|
41
|
+
|
|
42
|
+
Something goes in `shared/` when two modules import it today. Not when one module might later. The
|
|
43
|
+
test is: can you name the second caller? If not, it lives in the module that uses it.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Capacitor and native behaviour
|
|
2
|
+
|
|
3
|
+
Every native call is guarded with `Capacitor.isNativePlatform()` or
|
|
4
|
+
`Capacitor.getPlatform() === "web"`. `vp dev` in a browser must keep working — it is how the app is
|
|
5
|
+
inspected — so a handler that assumes a device breaks the fastest feedback loop you have.
|
|
6
|
+
|
|
7
|
+
Native wiring that needs the Framework7 instance runs inside `f7ready`, not at module load.
|
|
8
|
+
`src/plugins/capacitor/index.ts` is the single entry point.
|
|
9
|
+
|
|
10
|
+
## What is already handled, and why it is not simple
|
|
11
|
+
|
|
12
|
+
- **Back button** (`useAndroidBackButton`) first asks the M3 overlay stack to close its topmost
|
|
13
|
+
overlay (sheet, dialog, menu, FAB menu) - a persistent one swallows back instead. Then the current
|
|
14
|
+
tab's history, then from another tab's root the start destination, then it minimises rather than
|
|
15
|
+
exits. Overlays register themselves through `useOverlay`; a hand-built modal is invisible to back.
|
|
16
|
+
- **Keyboard** (`useKeyboard`) scrolls the focused input into view on _every_ phase of the
|
|
17
|
+
transition, not once — the layout is still settling at `keyboardWillShow` and only
|
|
18
|
+
`keyboardDidShow` sees the final height. It also hides the navigation bar, which otherwise steals a
|
|
19
|
+
row from the field being typed into.
|
|
20
|
+
- **Status bar** overlays the web view (edge to edge); the top app bar pads itself with the inset,
|
|
21
|
+
and the icon colour follows the theme's dark mode.
|
|
22
|
+
- **Splash** stays up until `hideSplashScreen()`, called on a frame boundary so there is no flash of
|
|
23
|
+
an unpainted shell.
|
|
24
|
+
|
|
25
|
+
Do not simplify these into a single listener. Each branch is there because of a specific device
|
|
26
|
+
behaviour, and the comments say which.
|
|
27
|
+
|
|
28
|
+
## The bootstrap must never fail silently
|
|
29
|
+
|
|
30
|
+
`main.ts` opens the database before mounting, inside a `try`, and renders the failure on the page if
|
|
31
|
+
it throws. An earlier version awaited it at module top level: a rejection produced an empty `#app`
|
|
32
|
+
and a completely silent console, which is the worst possible failure for whoever generates from this
|
|
33
|
+
template. The timeout exists so a hang cannot masquerade as a blank screen either.
|
|
34
|
+
|
|
35
|
+
Anything else added to the bootstrap follows the same shape: guarded, and loud when it fails.
|
|
36
|
+
|
|
37
|
+
## Fixed elements and the shell
|
|
38
|
+
|
|
39
|
+
The compact navigation bar floats over the bottom of the views; the shell publishes its height as
|
|
40
|
+
`--app-nav-offset` and `--app-bottom-inset` (offset plus the gesture area). So:
|
|
41
|
+
|
|
42
|
+
- Anything floating above content offsets by `--app-bottom-inset` - `AppPage`'s `#fab` slot, the
|
|
43
|
+
snackbar host and the floating toolbar already do. Never measure the bar.
|
|
44
|
+
- Page content is already padded by the same variable; do not add your own bottom spacer for it.
|
|
45
|
+
- The FAB menu opens upward, away from the bar.
|
|
46
|
+
|
|
47
|
+
## Permissions live in a generated folder
|
|
48
|
+
|
|
49
|
+
`android/` is generated by `cap add android` and gitignored, so a permission the app needs is not
|
|
50
|
+
carried by the template. Add it to `android/app/src/main/AndroidManifest.xml` after generating:
|
|
51
|
+
|
|
52
|
+
- **Location** (the chat's _Location_ attachment): `ACCESS_COARSE_LOCATION` and
|
|
53
|
+
`ACCESS_FINE_LOCATION`. The web view's own `navigator.geolocation` does the lookup; Capacitor
|
|
54
|
+
asks the person at runtime only for a permission the manifest declares, so without these the
|
|
55
|
+
lookup fails as `denied` and the chat offers the depot instead.
|
|
56
|
+
|
|
57
|
+
## Proof obligations
|
|
58
|
+
|
|
59
|
+
Say which platform you tested on. "Type-checks" is not a claim about a device, and neither is a
|
|
60
|
+
browser. If you have not run it on Android, say so.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: m3-expressive
|
|
3
|
+
description: Material 3 Expressive UI in this app. Use for any screen, component, layout, animation, transition, sheet, dialog, menu, list, button, progress or loading state, colour or theme change, icon, or app-bar work - before writing markup.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Material 3 Expressive
|
|
7
|
+
|
|
8
|
+
This app is Material 3 Expressive end to end. Framework7 is only the navigation engine (router, tab
|
|
9
|
+
views, page lifecycle); everything visible is an `M3*` component from `@cavulsqa/m3e-vue`, styled by
|
|
10
|
+
`--md-sys-*` tokens from `@cavulsqa/m3e`. Expressive is not decoration: shape, spring and tonal
|
|
11
|
+
colour say _what is happening_ - a selection rounds off, a sheet springs, progress waves while work
|
|
12
|
+
moves. Every number comes from Google's Compose tokens; [sources.md](sources.md) links each spec.
|
|
13
|
+
|
|
14
|
+
## Building a screen
|
|
15
|
+
|
|
16
|
+
1. **Name the job in one sentence** and the nearest Google app that does it (Messages, Photos,
|
|
17
|
+
Settings, Files, Wallet). Open its pattern on m3.material.io from [sources.md](sources.md).
|
|
18
|
+
2. **Pick the component** from [components.md](components.md). Every visible element is an `M3*`
|
|
19
|
+
component or Tailwind layout around one. Missing a component? It belongs in
|
|
20
|
+
`packages/m3e-vue`, built from its spec page - not a one-off in a module.
|
|
21
|
+
3. **Wrap the route in `AppPage`** (`shared/components/page`): tab roots get the large flexible app
|
|
22
|
+
bar, pushed pages pass `back`. Layout rules: [layout.md](layout.md).
|
|
23
|
+
4. **Choose shape, colour role and motion** - [shapes-type.md](shapes-type.md),
|
|
24
|
+
[color.md](color.md), [motion.md](motion.md). Each decision names its token.
|
|
25
|
+
5. **Strings in both locales**, icons from Material Symbols (`<i-ms-name-rounded />`), filled
|
|
26
|
+
variant for a selected state.
|
|
27
|
+
6. **Done means**: `vp check`, `pnpm type-check` and `vp test` pass; you checked light, dark, a 360dp
|
|
28
|
+
width, reduced motion, and RTL by reasoning or on screen; and you said plainly whether you saw it
|
|
29
|
+
run. Type-checking proves nothing about how a screen looks.
|
|
30
|
+
|
|
31
|
+
## The rules every screen keeps
|
|
32
|
+
|
|
33
|
+
- **Tokens only.** Colour is a role (`bg-surface-container-low`, `text-on-primary-container`),
|
|
34
|
+
corners are the shape scale (`rounded-lg`, `rounded-xl`), type is a style (`type-title-medium`),
|
|
35
|
+
shadows are levels (`shadow-1`..`shadow-5`). Tailwind's own palette is deleted from the theme, so
|
|
36
|
+
only roles resolve; a hex value belongs in `src/app/theme.config.ts` and nowhere else.
|
|
37
|
+
- **Pairs travel together.** A container role always carries its `on-` role:
|
|
38
|
+
`bg-tertiary-container text-on-tertiary-container`. Text on a surface is `text-on-surface` or
|
|
39
|
+
`text-on-surface-variant`.
|
|
40
|
+
- **Surfaces stack by container tone**, not by shadow: page `surface`, grouped content
|
|
41
|
+
`surface-container-low`, raised panels `surface-container`/`-high`. Shadows only on what floats.
|
|
42
|
+
- **Overlays come from the services**: `useSnackbar`, `useDialog`, `useActionSheet` (auto-imported),
|
|
43
|
+
or `M3BottomSheet`/`M3Menu` with `v-model:open`. They register on the overlay stack, so Android
|
|
44
|
+
back and Escape close the top one. Never hand-build a modal.
|
|
45
|
+
- **One tap target per row.** A list row's own button lives in `M3ListItem`'s `#action` slot, never
|
|
46
|
+
inside a clickable row.
|
|
47
|
+
- **Motion is spatial or effects.** Position, size and shape use the spatial springs; colour and
|
|
48
|
+
opacity the effects springs. Never animate `width`/`top` of a large surface - `transform` and
|
|
49
|
+
`opacity`.
|
|
50
|
+
- **Reduced motion keeps meaning, drops travel**: morphs and fades stay, rotation, bounce and slide
|
|
51
|
+
go. The tokens collapse spatial durations to 1 ms for you; JS-driven motion reads
|
|
52
|
+
`useReducedMotion()`.
|
|
53
|
+
- **Every control is labelled**: icon buttons, FABs and switches take `label`; it becomes the
|
|
54
|
+
accessible name and tooltip.
|
|
55
|
+
- **Empty is a state, not a blank.** `EmptyState` (shape, one sentence, the one action that fills it).
|
|
56
|
+
Waits under ~5 s show `M3LoadingIndicator`; known progress shows the wavy indicators.
|
|
57
|
+
|
|
58
|
+
## Where things live
|
|
59
|
+
|
|
60
|
+
| Need | Use |
|
|
61
|
+
| ----------------------------- | ---------------------------------------------------------------- |
|
|
62
|
+
| A screen | `AppPage` + modules/`<feature>`/views |
|
|
63
|
+
| Section label | `SectionHeader` |
|
|
64
|
+
| Theme state | `useThemeSettings()` - seed, variant, contrast, mode, motion |
|
|
65
|
+
| Shell navigation | `useActiveTab()` (`show`, `open(tab, path)`), `useWindowClass()` |
|
|
66
|
+
| Hide the bar on a pushed page | `AppPage back` does it; elsewhere `useHiddenNavigation()` |
|
|
67
|
+
| Decorative tone for a shape | `TONE_CLASSES` from `shared/utils/tone` |
|
|
68
|
+
| Live component examples | the Gallery tab - `modules/gallery/components/sections` |
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Colour
|
|
2
|
+
|
|
3
|
+
One seed (`BRAND_SEED` in `src/app/theme.config.ts`) generates every role in both modes through
|
|
4
|
+
material-color-utilities' 2025 (Expressive) spec. The user can change seed, variant and contrast in
|
|
5
|
+
the colour studio; `useThemeSettings()` persists it and repaints in one frame. Dark mode is a `.dark`
|
|
6
|
+
class on `<html>` - both modes are always in the stylesheet, so switching regenerates nothing.
|
|
7
|
+
|
|
8
|
+
## Roles to reach for
|
|
9
|
+
|
|
10
|
+
| Purpose | Role |
|
|
11
|
+
| ----------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
12
|
+
| Page background | `surface` |
|
|
13
|
+
| Grouped content, list segments, cards on a page | `surface-container-low` |
|
|
14
|
+
| Raised panels, the nav bar, bars over scrolled content | `surface-container` / `-high` |
|
|
15
|
+
| Text fields, inactive tracks | `surface-container-highest` |
|
|
16
|
+
| Main emphasis (FAB, hero, selected nav) | `primary-container` + `on-primary-container` |
|
|
17
|
+
| Strong action fill | `primary` + `on-primary` |
|
|
18
|
+
| Selected or active but quiet (chips, list rows, indicators) | `secondary-container` |
|
|
19
|
+
| Contrasting accent, decorative variety | `tertiary-container` |
|
|
20
|
+
| Snackbar, tooltips | `inverse-surface` + `inverse-on-surface`, action `inverse-primary` |
|
|
21
|
+
| Errors and destructive | `error`, `error-container` |
|
|
22
|
+
| Success, warning | custom groups `success-*`, `warning-*` (theme.config `EXTRA_COLORS`) |
|
|
23
|
+
| Hairlines | `outline-variant`; field borders `outline` |
|
|
24
|
+
| Secondary text, icons | `on-surface-variant` |
|
|
25
|
+
|
|
26
|
+
Disabled content is `on-surface` at 38%, disabled containers `on-surface` at 10-12% - the components
|
|
27
|
+
already do this.
|
|
28
|
+
|
|
29
|
+
## Variants and the spec trap
|
|
30
|
+
|
|
31
|
+
Material applies the 2025 rules only to **tonalSpot, expressive, vibrant, neutral**. `brand`
|
|
32
|
+
(seed exact as `primary-container`, Theme Builder's "Match colour"), fidelity, content, monochrome,
|
|
33
|
+
rainbow and fruitSalad silently use 2021 rules - `effectiveSpec(variant)` says which. The studio shows
|
|
34
|
+
it to the user.
|
|
35
|
+
|
|
36
|
+
## Rules
|
|
37
|
+
|
|
38
|
+
- A new semantic colour is a custom group in `EXTRA_COLORS` (harmonised to the seed), then a role
|
|
39
|
+
name in `assets/css/theme/tailwind.css` - never a hex in a component.
|
|
40
|
+
- Contrast is a user setting (standard / medium / high): never compensate a weak pair by hand-picking
|
|
41
|
+
a darker colour; fix the role choice.
|
|
42
|
+
- Status needs more than colour: pair it with a shape or glyph (see `STATUS_LOOK` in the demo module).
|
|
43
|
+
|
|
44
|
+
See [sources.md](sources.md) → Colour.
|