@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
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @cavulsqa/create
2
+
3
+ Create a Vue 3 + Framework7 + Capacitor + SQLite app. One command, no clone.
4
+
5
+ ```bash
6
+ pnpm create @cavulsqa
7
+ ```
8
+
9
+ `npm create @cavulsqa`, `bun create @cavulsqa` and `vp create @cavulsqa` all work the same way. With
10
+ a terminal it asks four questions; with `--name` it asks none, which is what CI wants.
11
+
12
+ ```bash
13
+ pnpm create @cavulsqa --name caputa --app-id com.sig.caputa --yes
14
+ ```
15
+
16
+ | flag | default | |
17
+ | ------------ | ---------------- | ------------------------------------------------------ |
18
+ | `--name` | asked | package and directory name |
19
+ | `--template` | the only one | which bundled template |
20
+ | `--dir` | `./<name>` | where to write it |
21
+ | `--app-name` | `Name` | launcher name, window title, Settings screen |
22
+ | `--app-id` | `com.ayb.<name>` | Android application id |
23
+ | `--from` | — | a template directory on disk, instead of a bundled one |
24
+ | `--yes` | — | take the defaults, ask nothing |
25
+
26
+ ## What you get
27
+
28
+ The `f7-app` template: a tabbed shell, a `domains` / `modules` / `shared` layout, and a working
29
+ sales demo over a six-table schema — dashboard aggregates, search, an order sheet, swipe actions,
30
+ a detail screen — with tests against real SQLite.
31
+
32
+ Underneath it, the data layer this repository publishes: `@cavulsqa/mobile-db` for Capacitor SQLite
33
+ under Kysely, `@cavulsqa/reactive-db` for the change bus, and `@cavulsqa/reactive-vue` for
34
+ `useReactiveQuery`. A write announces the tables it touched and every query watching them refetches;
35
+ nothing in a screen asks for a refresh.
36
+
37
+ `CLAUDE.md`, `.claude/rules/` and `.claude/skills/` come with it, so an agent opening the generated
38
+ repository knows the architecture, the conventions, and the traps that have already cost someone a
39
+ day.
40
+
41
+ ## Versions
42
+
43
+ The template's dependencies are resolved and pinned when this package is published, so a given
44
+ version of `@cavulsqa/create` always generates the same app. Upgrading the data layer afterwards is
45
+ an ordinary `pnpm update`.
46
+
47
+ ## After generating
48
+
49
+ ```bash
50
+ pnpm install
51
+ pnpm dev
52
+ ```
53
+
54
+ `pnpm dev` runs the app in a browser against sql.js in memory — quick for UI work, and data does not
55
+ survive a reload. The real target is a device:
56
+
57
+ ```bash
58
+ npx cap add android
59
+ pnpm build && npx cap sync android && npx cap run android
60
+ ```
61
+
62
+ Android builds need **JDK 21**. An older one fails with `invalid source release: 21` from inside
63
+ `capacitor-android`, which reads like a Capacitor bug and is not one.
package/bin/create.mjs ADDED
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `pnpm create @cavulsqa` / `npm create @cavulsqa` / `vp create @cavulsqa`.
4
+ *
5
+ * Interactive when it can be, flag-driven when it cannot: a TTY gets asked, and CI passing
6
+ * `--name` gets no prompts at all. Anything still missing without a TTY is an error rather than a
7
+ * silent default, because a generated app named "my-app" in the wrong directory is worse than a
8
+ * failed command.
9
+ */
10
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
11
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
12
+ import { createInterface } from "node:readline/promises";
13
+ import { fileURLToPath } from "node:url";
14
+ import { scaffold } from "../lib/scaffold.mjs";
15
+
16
+ const PACKAGE = dirname(dirname(fileURLToPath(import.meta.url)));
17
+ const BUNDLED = join(PACKAGE, "templates");
18
+
19
+ function parseArgs(argv) {
20
+ const flags = new Map();
21
+ const positional = [];
22
+ for (let index = 0; index < argv.length; index++) {
23
+ const arg = argv[index];
24
+ if (!arg.startsWith("--")) {
25
+ positional.push(arg);
26
+ continue;
27
+ }
28
+ const [key, inline] = arg.slice(2).split("=");
29
+ if (inline !== undefined) flags.set(key, inline);
30
+ else if (argv[index + 1] && !argv[index + 1].startsWith("--")) flags.set(key, argv[++index]);
31
+ else flags.set(key, true);
32
+ }
33
+ return { flags, positional };
34
+ }
35
+
36
+ function usage() {
37
+ console.log(`Usage: create-cavulsqa [name] [options]
38
+
39
+ Options:
40
+ --name NAME package and directory name
41
+ --template NAME which template (default: the only one, or you are asked)
42
+ --dir PATH where to write it (default: ./<name>)
43
+ --app-name NAME launcher name and window title (default: Name)
44
+ --app-id ID android application id (default: com.ayb.<name>)
45
+ --from PATH use a template directory on disk instead of the bundled ones
46
+ --yes take the defaults, ask nothing
47
+ --help this
48
+
49
+ Templates bundled in this package:
50
+ ${listTemplates()
51
+ .map((entry) => ` ${entry.name} ${entry.description}`)
52
+ .join("\n")}`);
53
+ }
54
+
55
+ function listTemplates() {
56
+ const manifest = JSON.parse(readFileSync(join(PACKAGE, "package.json"), "utf8"));
57
+ const declared = manifest.createConfig?.templates ?? [];
58
+ if (!existsSync(BUNDLED)) return declared;
59
+
60
+ // The manifest is what `vp create` reads; the directory is what actually shipped. Trust the
61
+ // directory, and let the manifest supply the descriptions.
62
+ return readdirSync(BUNDLED).map((name) => ({
63
+ name,
64
+ description: declared.find((entry) => entry.name === name)?.description ?? "",
65
+ }));
66
+ }
67
+
68
+ /** npm package names: lowercase, no spaces, no leading dot or underscore. */
69
+ function validName(value) {
70
+ return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(value);
71
+ }
72
+
73
+ async function main() {
74
+ const { flags, positional } = parseArgs(process.argv.slice(2));
75
+ if (flags.has("help") || flags.has("h")) return usage();
76
+
77
+ const interactive = process.stdin.isTTY && !flags.has("yes");
78
+ const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
79
+
80
+ const ask = async (question, fallback) => {
81
+ if (!rl) return fallback;
82
+ const answer = (await rl.question(`${question}${fallback ? ` (${fallback})` : ""}: `)).trim();
83
+ return answer || fallback;
84
+ };
85
+
86
+ try {
87
+ let name = flags.get("name") ?? positional[0];
88
+ if (typeof name !== "string") name = await ask("App name", "my-app");
89
+ if (typeof name !== "string" || !validName(name)) {
90
+ throw new Error(`"${String(name)}" is not a usable package name`);
91
+ }
92
+
93
+ const templates = listTemplates();
94
+ if (!templates.length) throw new Error("this build of @cavulsqa/create bundles no templates");
95
+
96
+ let template = flags.get("template");
97
+ if (typeof template !== "string") {
98
+ template =
99
+ templates.length === 1
100
+ ? templates[0].name
101
+ : await ask(`Template [${templates.map((t) => t.name).join(", ")}]`, templates[0].name);
102
+ }
103
+ if (!templates.some((entry) => entry.name === template)) {
104
+ throw new Error(
105
+ `unknown template "${template}"; bundled: ${templates.map((t) => t.name).join(", ")}`,
106
+ );
107
+ }
108
+
109
+ const titled = name.charAt(0).toUpperCase() + name.slice(1);
110
+ const appName = flags.get("app-name") ?? (await ask("Display name", titled));
111
+ const bareName = name.replace(/[^a-z0-9]/gi, "").toLowerCase();
112
+ const appId =
113
+ flags.get("app-id") ?? (await ask("Android application id", `com.ayb.${bareName}`));
114
+
115
+ const dir = flags.get("dir") ?? (await ask("Directory", `./${name}`));
116
+ const out = isAbsolute(dir) ? dir : resolve(process.cwd(), dir);
117
+
118
+ const from = flags.get("from");
119
+ const templateDir =
120
+ typeof from === "string" ? resolve(process.cwd(), from) : join(BUNDLED, template);
121
+
122
+ scaffold({ templateDir, out, name, appId, appName: String(appName) });
123
+
124
+ console.log(`
125
+ ${appName} created in ${out}
126
+ template ${template}
127
+ appId ${appId}
128
+
129
+ cd ${basename(out)}
130
+ pnpm install
131
+ pnpm dev
132
+
133
+ Android needs JDK 21: npx cap add android, then pnpm build && npx cap sync android`);
134
+ } finally {
135
+ rl?.close();
136
+ }
137
+ }
138
+
139
+ main().catch((error) => {
140
+ console.error(`\n${error instanceof Error ? error.message : String(error)}`);
141
+ process.exitCode = 1;
142
+ });
@@ -0,0 +1,124 @@
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+
4
+ /** Files whose contents carry the app's identity and have to be rewritten, not copied. */
5
+ function personalise(entry, text, { name, appName }) {
6
+ if (entry === "capacitor.config.ts") {
7
+ return text
8
+ .replace(/appId: "[^"]*"/, `appId: "${name.appId}"`)
9
+ .replace(/appName: "[^"]*"/, `appName: "${appName}"`);
10
+ }
11
+ if (entry === "vite.config.ts") {
12
+ return text.replace(
13
+ /__APP_NAME__: JSON\.stringify\("[^"]*"\)/,
14
+ `__APP_NAME__: JSON.stringify("${appName}")`,
15
+ );
16
+ }
17
+ if (entry === "index.html") {
18
+ return text.replace(/<title>[^<]*<\/title>/, `<title>${appName}</title>`);
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function manifest(source, { name, appName, templateName }) {
24
+ const pkg = JSON.parse(source);
25
+ const { private: _private, ...rest } = pkg;
26
+ return `${JSON.stringify(
27
+ {
28
+ ...rest,
29
+ name: name.package,
30
+ version: "0.1.0",
31
+ private: true,
32
+ description: `${appName} - generated from @cavulsqa/create (${templateName}).`,
33
+ engines: { node: ">=22.18.0" },
34
+ },
35
+ null,
36
+ 2,
37
+ )}\n`;
38
+ }
39
+
40
+ function copyTree(from, to, transform) {
41
+ mkdirSync(to, { recursive: true });
42
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
43
+ const source = join(from, entry.name);
44
+ const target = join(to, entry.name);
45
+ if (entry.isDirectory()) {
46
+ copyTree(source, target, transform);
47
+ continue;
48
+ }
49
+ const rewritten = transform(entry.name, source);
50
+ if (rewritten === null) cpSync(source, target);
51
+ else writeFileSync(target, rewritten);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Writes a generated app to `out`.
57
+ *
58
+ * The template's dependencies are already concrete - `bundleTemplates.mjs` resolved them when the
59
+ * creator was packed - so nothing here has to know about workspaces or catalogs.
60
+ */
61
+ export function scaffold({ templateDir, out, name, appId, appName }) {
62
+ if (!existsSync(templateDir)) throw new Error(`no template at ${templateDir}`);
63
+ if (existsSync(out)) throw new Error(`${out} already exists`);
64
+
65
+ const templateName = basename(templateDir);
66
+ const identity = { package: name, appId };
67
+
68
+ copyTree(templateDir, out, (entry, source) => {
69
+ const text = readFileSync(source, "utf8");
70
+ if (entry === "package.json" && source === join(templateDir, "package.json")) {
71
+ return manifest(text, { name: identity, appName, templateName });
72
+ }
73
+ return personalise(entry, text, { name: identity, appName });
74
+ });
75
+
76
+ // pnpm will not finish an install while a dependency's build script is neither allowed nor
77
+ // denied, and vite-plus pulls esbuild in. Without this every generated app fails its first
78
+ // `pnpm install` with ERR_PNPM_IGNORED_BUILDS.
79
+ writeFileSync(join(out, "pnpm-workspace.yaml"), "allowBuilds:\n esbuild: true\n");
80
+
81
+ // Inside the monorepo the template leaned on the root for the ordinary ignores.
82
+ const ignore = join(out, ".gitignore");
83
+ const existing = existsSync(ignore) ? readFileSync(ignore, "utf8").trim() : "";
84
+ writeFileSync(
85
+ ignore,
86
+ [
87
+ "node_modules",
88
+ "dist",
89
+ "*.log",
90
+ ".env",
91
+ ".env.*",
92
+ "!.env.example",
93
+ ".DS_Store",
94
+ "",
95
+ "# Generated native projects. Recreate with `npx cap add android`.",
96
+ "android",
97
+ "ios",
98
+ "",
99
+ existing,
100
+ "",
101
+ ].join("\n"),
102
+ );
103
+
104
+ writeFileSync(
105
+ join(out, "README.md"),
106
+ `# ${appName}
107
+
108
+ Generated from \`@cavulsqa/create\` (${templateName}).
109
+
110
+ \`\`\`bash
111
+ pnpm install
112
+ pnpm dev # browser, sql.js in memory - data does not survive a reload
113
+ npx cap add android # once
114
+ pnpm build && npx cap sync android && npx cap run android
115
+ \`\`\`
116
+
117
+ Android builds need **JDK 21**; an older one fails with \`invalid source release: 21\`.
118
+
119
+ \`CLAUDE.md\` and \`.claude/\` carry the architecture an agent needs before editing anything here.
120
+ `,
121
+ );
122
+
123
+ return { templateName };
124
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@cavulsqa/create",
3
+ "version": "0.1.0",
4
+ "description": "Create a Vue + Framework7 + Capacitor + SQLite app from the cavulsqa templates.",
5
+ "keywords": [
6
+ "capacitor",
7
+ "create",
8
+ "framework7",
9
+ "scaffold",
10
+ "sqlite",
11
+ "template",
12
+ "vue"
13
+ ],
14
+ "homepage": "https://github.com/aybinv7/cavulsqa/tree/main/packages/create#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/aybinv7/cavulsqa/issues"
17
+ },
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/aybinv7/cavulsqa.git",
22
+ "directory": "packages/create"
23
+ },
24
+ "bin": {
25
+ "create-cavulsqa": "./bin/create.mjs"
26
+ },
27
+ "files": [
28
+ "bin",
29
+ "lib",
30
+ "templates"
31
+ ],
32
+ "type": "module",
33
+ "exports": {
34
+ "./package.json": "./package.json"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "devDependencies": {
40
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.3.0",
41
+ "vite-plus": "0.3.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=22.18.0"
45
+ },
46
+ "createConfig": {
47
+ "templates": [
48
+ {
49
+ "name": "f7-app",
50
+ "description": "Vue 3 + Framework7 + Capacitor + SQLite, tabbed shell, offline-first data layer",
51
+ "template": "./templates/f7-app"
52
+ }
53
+ ]
54
+ },
55
+ "scripts": {
56
+ "build": "node scripts/bundleTemplates.mjs",
57
+ "check": "vp check",
58
+ "test": "vp test"
59
+ }
60
+ }
@@ -0,0 +1,67 @@
1
+ # Reading and writing data
2
+
3
+ Every read is a reactive query or a deliberate on-demand call. Every write goes through `rdb`. A
4
+ third shape — a hand-rolled cache, or a raw write — produces "stale screen, no error, no symptom",
5
+ which is the one failure an offline app cannot afford: the user has no network to blame.
6
+
7
+ ## Reads
8
+
9
+ `useReactiveQuery(fn, { tables, queryKey, debounce })` from `@/shared/database/queries`.
10
+
11
+ - **`tables` is the invalidation contract.** It must list exactly the tables `fn` reads — no more,
12
+ no less. Under-list and a write to the missing table leaves the screen stale with no error.
13
+ Over-list and an unrelated write re-runs an expensive query for nothing.
14
+
15
+ A join means every joined table. `loadOrderDetail` reads the order, its lines, the customer, the
16
+ product names and the customer's tags, so it lists all five. Count the tables in the SQL, not the
17
+ ones you were thinking about.
18
+
19
+ - **`queryKey` is a process-wide identity, not a label.** Two mounted queries sharing a key await
20
+ one request and share its result — right for the same list rendered twice, wrong for two
21
+ different queries that happen to be named alike, and the second silently receives the first's
22
+ rows. Framework7 keeps pages mounted, so two instances of one screen genuinely coexist.
23
+
24
+ Default to `uniqueQueryKey("prefix")`. A stable literal is opt-in sharing, and anything
25
+ parameterised by a route param or a ref must never use one.
26
+
27
+ - **`debounce`** collapses a burst. A loop of twenty inserts should refetch once, not twenty times.
28
+
29
+ - Reads may take the database directly: `searchOrders(getDatabase().db, term)`. Repositories take
30
+ the database as a parameter rather than reaching for the singleton, which is what makes them
31
+ testable — `tests/sales.repository.test.ts` runs them against sql.js.
32
+
33
+ ## Writes
34
+
35
+ Always `rdb`, never `getDatabase().db`:
36
+
37
+ ```ts
38
+ await saveOrder(rdb, { customerId, reference, lines }); // announces sales_order, order_line
39
+ await saveOrder(getDatabase().db, …); // writes, and nothing notices
40
+ ```
41
+
42
+ `rdb` wraps Kysely so a mutation publishes the tables it touched on the change bus. A raw write
43
+ lands in SQLite and no query hears about it, so the screen keeps showing the old rows until
44
+ something unrelated triggers a refetch. There is no error and nothing to see in review.
45
+
46
+ Anything that must be all-or-nothing goes in one transaction. `saveOrder` writes the order and its
47
+ lines together because a half-written order is worse than no order.
48
+
49
+ ## Forbidden
50
+
51
+ A module-scope `Map` or `ref` holding fetched rows with neither a TTL nor a bus subscription. Once
52
+ populated it never refreshes for the session, and the next write is invisible until restart.
53
+ In-flight dedup (a `Map<key, Promise>` cleared in `finally`) is concurrency control, not a cache,
54
+ and is fine — `useReactiveQuery` already does it.
55
+
56
+ ## Seed and demo helpers
57
+
58
+ Anything a person can press twice must survive being pressed twice. `seedSampleData` inserts the
59
+ catalogue only where missing and upserts tags, because a plain insert threw
60
+ `UNIQUE constraint failed: tag.label` on the second press.
61
+
62
+ ## Proof obligations
63
+
64
+ - A new query: state which tables its SQL touches and that `tables` matches.
65
+ - A new write: state that it goes through `rdb`, and whether it needs a transaction.
66
+ - Either: a test in `tests/` exercising it against sql.js. Type-checking a query proves nothing
67
+ about what it returns.
@@ -0,0 +1,65 @@
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
+ The SQLite plugin runs a statement issued inside an open transaction through `query()`, which
40
+ executes it and drops its RETURNING rows. `.returning("id").executeTakeFirstOrThrow()` therefore
41
+ threw `no result` from an insert that had in fact succeeded — a message that sends you hunting for a
42
+ failed write. `@cavulsqa/mobile-db` now throws a message that says so, and fills `insertId` from
43
+ `last_insert_rowid()` on both sides of a transaction boundary. The sql.js test dialect reports it
44
+ too, so a repository written this way behaves the same in tests as on a device.
45
+
46
+ ## The web path is not the device path
47
+
48
+ On a device the database is a real file behind the Capacitor plugin. In a browser it is sql.js in
49
+ memory, through the dialect `@cavulsqa/mobile-db` ships for its own tests. Two consequences:
50
+
51
+ - Browser data does not survive a reload. That is expected, not a bug to fix.
52
+ - The plugin's own web mode is deliberately unused: it needs a `jeep-sqlite` element and a
53
+ `sql-wasm.wasm` whose build must match the glue jeep-sqlite bundles, a pairing outside this
54
+ template's control that fails as a `WebAssembly LinkError` on an upstream bump.
55
+
56
+ Measurements taken in a browser do not transfer. Concurrent reads are several times faster than
57
+ sequential ones **on a device**, because the native bridge pipelines them; in memory there is no
58
+ bridge and the ratio sits at 1.
59
+
60
+ ## Proof obligations
61
+
62
+ A schema change needs a test in `tests/` that runs the migration and the affected queries against
63
+ sql.js. `tests/sales.repository.test.ts` is the pattern: build a Kysely on `createSqlJsDialect()`,
64
+ migrate, then assert on real rows — including the arithmetic. A total that type-checks can still be
65
+ computed wrong.
@@ -0,0 +1,79 @@
1
+ # Framework7 UI
2
+
3
+ Framework7 owns the look. Your job is to compose its components, not to restyle them.
4
+
5
+ ## Components arrive by resolver
6
+
7
+ `Framework7VueResolver` (in `vite.config.ts`, implemented in
8
+ `src/shared/utils/resolvers/resolvers.ts`) imports each `f7-*` component where it is used. So:
9
+
10
+ - Never write `import { f7Page } from "framework7-vue"`.
11
+ - Never call `registerComponents`.
12
+ - Use PascalCase tags: `<F7Page>`, `<F7ListItem>`, `<F7BlockTitle>`.
13
+
14
+ If a component renders as an unknown element, its kebab name is missing from the resolver's list —
15
+ add it there. Do not add a name the installed `framework7-vue` does not export: `f7-toolbar-pane`
16
+ is a Framework7 9 CSS class with no Vue component in framework7-vue 8, and resolving it fails as a
17
+ runtime `SyntaxError`, not a warning. Check the package's exports before adding.
18
+
19
+ `f7`, `f7ready` and `theme` are auto-imported. `f7route` and `f7router` are **not** — Framework7
20
+ passes them to a route component as props:
21
+
22
+ ```ts
23
+ const props = defineProps<{ f7route: Router.Route; f7router: Router.Router }>();
24
+ ```
25
+
26
+ ## Write no CSS
27
+
28
+ No backgrounds, no heights, no safe-area padding, no font sizes for body text. Framework7's theme
29
+ provides all of it for both iOS and Material, light and dark. `app.css` is one line.
30
+
31
+ Tailwind is available for layout and spacing inside a component — flex, grid, gaps, a text size on
32
+ a number. The moment you reach for a colour or a background, stop: use a Framework7 component or a
33
+ theme variable.
34
+
35
+ ## Lists
36
+
37
+ `F7ListItem` renders `subtitle`, `text` and `#media` **only in a media list**. Outside one they are
38
+ dropped silently — the quantity line on the order detail vanished exactly this way.
39
+
40
+ - Short label + value: a plain `F7List` with `title` and `after`.
41
+ - Anything with a description: `F7List media-list`, value in `subtitle`, prose in `#text`.
42
+ - `item-footer` is sized for a few words. Long text in it overlaps the title, and a long `after`
43
+ squeezes the text into a one-word-per-line column.
44
+
45
+ ## Icons
46
+
47
+ framework7-icons is a **ligature font**: a wrong name renders nothing at all — no warning, no
48
+ fallback, no console message. This template has shipped invisible icons twice.
49
+
50
+ - Verify against the font, never from memory, another icon set, or a filename in the package. The
51
+ React components in `framework7-icons/react` are SVG wrappers whose names do **not** match the
52
+ ligatures — trusting them broke four working icons.
53
+ - The name keeps the underscore before a digit: `arrow_2_circlepath`, `square_grid_2x2_fill`.
54
+ - SF Symbols names are not F7 names. It is `search`, not `magnifyingglass`.
55
+ - `tests/icons.test.ts` checks every name in the app against the ttf. It is the authority.
56
+
57
+ Per-theme icons where the platform look matters: `icon-ios="f7:house_fill"` beside
58
+ `icon-md="material:home"`. Material glyphs need the bundled font in `assets/css/icons.css`, or they
59
+ render as the literal word.
60
+
61
+ ## Gestures
62
+
63
+ Swipeout inside swipeable tabs claims the same horizontal drag as the tabs. Put
64
+ `swiper-no-swiping` on the list, or one gesture does both.
65
+
66
+ ## Navigation
67
+
68
+ - Tabs are data in `src/app/tabs.ts`. Each tab is a view with its own history.
69
+ - A pushed page calls `useHiddenTabbar()`. The tab bar belongs to the tab roots; on a detail screen
70
+ it is navigation to somewhere you are not.
71
+ - A route's `async` is Framework7's hook, not an async function — resolve from a promise, `await`
72
+ inside it does not compile.
73
+ - Actions on a detail screen belong in an `F7Toolbar bottom`, which spaces links evenly and clears
74
+ the safe area. A hand-built fixed bar does neither.
75
+
76
+ ## Proof obligations
77
+
78
+ State that `vp test` passes (it includes the icon check), and say plainly whether you have seen the
79
+ screen render. A screen that type-checks can still be an empty box.
@@ -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 call `useHiddenTabbar()`.
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.