@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,47 @@
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`) closes the topmost layer before it navigates, in order:
13
+ actions, dialog, sheet, popover, popup, login screen, panel. Then a popup's own view history, then
14
+ the router, then it minimises rather than exits. The order matters: an actions sheet over a popup
15
+ must close first or back dismisses the popup underneath and orphans the sheet.
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 tab bar, which otherwise steals a row
19
+ from the field being typed into, and leaves a message bar's accessory row alone.
20
+ - **Status bar** overlays the web view; the page owns the inset.
21
+ - **Splash** stays up until `hideSplashScreen()`, called on a frame boundary so there is no flash of
22
+ an unpainted shell.
23
+
24
+ Do not simplify these into a single listener. Each branch is there because of a specific device
25
+ behaviour, and the comments say which.
26
+
27
+ ## The bootstrap must never fail silently
28
+
29
+ `main.ts` opens the database before mounting, inside a `try`, and renders the failure on the page if
30
+ it throws. An earlier version awaited it at module top level: a rejection produced an empty `#app`
31
+ and a completely silent console, which is the worst possible failure for whoever generates from this
32
+ template. The timeout exists so a hang cannot masquerade as a blank screen either.
33
+
34
+ Anything else added to the bootstrap follows the same shape: guarded, and loud when it fails.
35
+
36
+ ## Fixed elements and the shell
37
+
38
+ The tab bar lives in the shell, outside every page, inside `.views.tabs`. So:
39
+
40
+ - A page already ends above it — do not add a bottom offset to a FAB to "clear" it. That counts it
41
+ twice.
42
+ - FAB buttons open upward (`position="top"`), or they land behind it.
43
+
44
+ ## Proof obligations
45
+
46
+ Say which platform you tested on. "Type-checks" is not a claim about a device, and neither is a
47
+ browser. If you have not run it on Android, say so.
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: f7-design
3
+ description: Framework7 UI work in this app. Use whenever the task is to build or change a screen, view, page, component, layout, navbar, tabbar, toolbar, list, sheet, popup, panel, FAB, card, chip, searchbar, swipeout, or any mobile UI. Covers the component-first rule, the resolver allowlist you must update before using a new component, the icon ligature trap, and why this app writes no CSS.
4
+ ---
5
+
6
+ # Framework7 UI workflow
7
+
8
+ Mobile-first, Android-first, native-feeling on both themes. Reach for a Framework7 component before
9
+ building anything by hand — it already handles the theme, dark mode, safe areas and the gestures.
10
+
11
+ ## Before building a screen
12
+
13
+ 1. Check whether Framework7 has the component: https://framework7.io/vue/. The catalogue wired into
14
+ this app is [components.md](components.md).
15
+ 2. In the allowlist → use `<F7Xxx>` directly. **No import** — `Framework7VueResolver` resolves it.
16
+ 3. Not in the allowlist → add the kebab name to `framework7Components` in
17
+ `src/shared/utils/resolvers/resolvers.ts` **first**. Skip this and the component silently fails
18
+ to resolve.
19
+ 4. Before adding a name, confirm the installed `framework7-vue` actually exports it. `f7-toolbar-pane`
20
+ is a Framework7 9 CSS class with no Vue component in framework7-vue 8 — resolving it throws a
21
+ runtime `SyntaxError`, not a warning. Use the class on a plain `div` in that case.
22
+ 5. Hand-build only when Framework7 has nothing suitable.
23
+
24
+ ## Layout idiom
25
+
26
+ ```
27
+ F7Page > F7Navbar > [F7NavRight, F7Subnavbar]
28
+ F7BlockTitle section heading
29
+ F7Block strong inset prose or a stat surface
30
+ F7List strong inset dividers rows
31
+ ```
32
+
33
+ - Rounded surfaces: `class="rounded-2xl!"`. The `!` matters — Tailwind loads after the F7 bundle.
34
+ - `F7ListItem` renders `subtitle`, `text` and `#media` **only in a media list**. Outside one they
35
+ are dropped with no warning.
36
+ - Long descriptive text goes in `#text` in a media list, never `#footer` — footers are sized for a
37
+ few words and long text overlaps the title.
38
+ - Detail-screen actions go in `F7Toolbar bottom`, which spaces links evenly and clears the safe
39
+ area. A hand-built fixed bar does neither.
40
+
41
+ ## Write no CSS
42
+
43
+ No backgrounds, heights, safe-area padding or body font sizes. The theme owns them, for iOS and
44
+ Material, light and dark. `assets/css/app.css` is one line.
45
+
46
+ Tailwind is for layout inside a component — flex, grid, gaps, a size on a number. The moment you
47
+ reach for a colour, use a Framework7 component or a theme variable instead. Theme colour is set once
48
+ via `colors.primary` in `src/plugins/framework7.plugin.ts`, never as a CSS override: Framework7
49
+ derives tints, shades, ripples and the dark variants from that value.
50
+
51
+ ## Icons
52
+
53
+ Read [icons.md](icons.md) before typing an icon name. framework7-icons is a **ligature font**: a
54
+ wrong name renders nothing at all, with no warning. This app has shipped invisible icons twice.
55
+
56
+ ## Navigation and gestures
57
+
58
+ - Tabs are data in `src/app/tabs.ts`; each is a view with its own history.
59
+ - A pushed page calls `useHiddenTabbar()`.
60
+ - Swipeout inside swipeable tabs needs `swiper-no-swiping` on the list, or one drag does both.
61
+ - A route's `async` is Framework7's hook, not an async function — resolve from a promise.
62
+ - `f7route` / `f7router` are props: `defineProps<{ f7route: Router.Route }>()`.
63
+
64
+ ## Before saying it works
65
+
66
+ `vp test` (includes the icon check) and `pnpm type-check`. Then say whether you have actually seen
67
+ the screen. A screen that compiles can still be an empty box.
@@ -0,0 +1,49 @@
1
+ # Component catalogue
2
+
3
+ The allowlist is the `framework7Components` array in
4
+ `src/shared/utils/resolvers/resolvers.ts`. Only names in it resolve; anything else renders as an
5
+ unknown element.
6
+
7
+ ## Wired
8
+
9
+ Shell and navigation
10
+ : `f7-app`, `f7-view`, `f7-views`, `f7-page`, `f7-page-content`, `f7-navbar`, `f7-nav-left`,
11
+ `f7-nav-right`, `f7-nav-title`, `f7-nav-title-large`, `f7-toolbar`, `f7-subnavbar`, `f7-link`,
12
+ `f7-tabs`, `f7-tab`, `f7-panel`, `f7-searchbar`
13
+
14
+ Lists and inputs
15
+ : `f7-list`, `f7-list-group`, `f7-list-item`, `f7-list-item-row`, `f7-list-item-cell`,
16
+ `f7-list-item-content`, `f7-list-button`, `f7-list-input`, `f7-checkbox`, `f7-toggle`,
17
+ `f7-stepper`
18
+
19
+ Actions and surfaces
20
+ : `f7-button`, `f7-segmented`, `f7-fab`, `f7-fab-button`, `f7-fab-buttons`, `f7-card`,
21
+ `f7-card-header`, `f7-card-content`, `f7-card-footer`, `f7-popup`, `f7-popover`, `f7-actions`,
22
+ `f7-actions-group`, `f7-actions-button`, `f7-actions-label`, `f7-sheet`, `f7-login-screen`
23
+
24
+ Content
25
+ : `f7-block`, `f7-block-title`, `f7-block-header`, `f7-block-footer`, `f7-icon`, `f7-chip`,
26
+ `f7-badge`, `f7-preloader`, `f7-progressbar`, `f7-skeleton-block`, `f7-skeleton-text`,
27
+ `f7-accordion*`, `f7-swipeout*`, `f7-messages*`, `f7-messagebar*`, `f7-photo-browser`,
28
+ `f7-virtual-list`, `f7-list-index`, `f7-row`, `f7-col`
29
+
30
+ ## Deliberately absent
31
+
32
+ `f7-toolbar-pane`
33
+ : Framework7 9 ships the CSS class; framework7-vue 8 exports no component. Resolving it fails as a
34
+ runtime `SyntaxError`. Use `<div class="toolbar-pane">`.
35
+
36
+ ## Adding one
37
+
38
+ 1. Confirm the installed `framework7-vue` exports it — check its entry, not the docs, since the Vue
39
+ bindings lag the core.
40
+ 2. Add the kebab name to the array.
41
+ 3. Use the PascalCase tag with no import.
42
+
43
+ ## Programmatic APIs
44
+
45
+ `f7` is auto-imported. `f7.dialog.*`, `f7.toast.*`, `f7.sheet.*`, `f7.actions.*`, `f7.fab.*`,
46
+ `f7.tab.show()`.
47
+
48
+ Check the option names against `framework7/components/<name>/<name>.d.ts` rather than from memory —
49
+ an action-sheet heading is `{ text, label: true }`, and emphasis is `strong`, not `bold`.
@@ -0,0 +1,52 @@
1
+ # Icons
2
+
3
+ Three systems. Pick by context.
4
+
5
+ ## 1. Framework7 icon font — chrome and list media
6
+
7
+ ```vue
8
+ <F7Icon f7="checkmark_seal_fill" color="green" />
9
+ <F7Icon ios="f7:house_fill" md="material:home" />
10
+ <!-- per-theme, for the tab bar -->
11
+ ```
12
+
13
+ **The trap.** framework7-icons is a ligature font. A name it does not carry renders _nothing_ — no
14
+ warning, no fallback, no console message. Invisible in review, invisible in a screenshot you did not
15
+ look closely at.
16
+
17
+ Rules that came from getting this wrong twice:
18
+
19
+ - **Verify against the font, never from memory.** `tests/icons.test.ts` checks every name used in
20
+ the app against `Framework7Icons-Regular.ttf`. It is the authority; run it.
21
+ - The name **keeps the underscore before a digit**: `arrow_2_circlepath`, `square_grid_2x2_fill`,
22
+ `square_stack_3d_down_right_fill`, `rectangle_3_offgrid_fill`.
23
+ - **Do not derive names from `framework7-icons/react/*`.** Those files are SVG wrappers whose names
24
+ do not match the ligatures. Trusting them turned four working icons into fragments of other
25
+ glyphs.
26
+ - **SF Symbols names are not Framework7 names.** It is `search`, not `magnifyingglass`.
27
+ - Browse real names at https://framework7.io/icons/.
28
+
29
+ ## 2. Material icons — the `md` theme
30
+
31
+ `icon-md="material:home"` needs the Material font, which is bundled in `src/assets/css/icons.css`
32
+ with a `font-feature-settings: "liga"` rule. Without it the icon renders as the literal word
33
+ "home". The font is self-hosted, not from a CDN, because an offline-first app should not lose its
34
+ icons with the network.
35
+
36
+ ## 3. unplugin-icons SVGs — feature UI
37
+
38
+ ```vue
39
+ <ILucideZap class="text-yellow-500" />
40
+ ```
41
+
42
+ Resolved by `IconsResolver` with the `framework7`, `material-symbols` and `lucide` collections
43
+ enabled, and `autoInstall: true`. Unlike the font, a wrong name here **fails at build time**, which
44
+ makes it the safer choice for anything decorative or one-off.
45
+
46
+ ## Choosing
47
+
48
+ | Context | Use |
49
+ | ------------------------------ | ---------------------------- |
50
+ | Tab bar, navbar, native chrome | `F7Icon` with `ios=` / `md=` |
51
+ | List row media, status glyphs | `F7Icon f7="…" color="…"` |
52
+ | Feature illustration, one-offs | `<ILucide… />` |
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: module-architecture
3
+ description: Where code goes in this app and how to add a feature end to end. Use when creating a new screen, module, route, domain, repository, composable, or when deciding whether something belongs in modules, domains or shared. Covers the dependency direction, the file layout of a module, and the checklist for wiring a feature into the shell.
4
+ ---
5
+
6
+ # Module architecture
7
+
8
+ ```
9
+ src/
10
+ ├── app/tabs.ts the tab bar, as data
11
+ ├── router/index.ts aggregates module routes, catch-all last
12
+ ├── domains/<domain>/<x>.repository.ts SQL only
13
+ ├── modules/<feature>/
14
+ │ ├── router/routes/<feature>.routes.ts
15
+ │ ├── views/<Name>View.vue
16
+ │ ├── components/*.vue
17
+ │ └── composables/use*.ts
18
+ └── shared/ what two modules genuinely both need
19
+ ```
20
+
21
+ ## The seam
22
+
23
+ | Layer | Owns | Never contains |
24
+ | ---------- | --------------------------------------- | -------------------------------------------- |
25
+ | View | Wiring a composable to components | Business logic, SQL |
26
+ | Composable | State, queries, actions for one feature | Markup |
27
+ | Repository | Plain functions over Kysely | `ref`, lifecycle, Framework7, module imports |
28
+ | Component | Props in, emits out | Database access |
29
+
30
+ Dependencies run **modules → domains → shared → packages**, never backwards. A repository importing
31
+ from a module, or `shared` importing from `modules`, means something is in the wrong place.
32
+
33
+ ## Adding a feature
34
+
35
+ 1. `modules/<feature>/` with the four folders. Templates: [file-templates.md](file-templates.md).
36
+ 2. SQL in `domains/<domain>/<domain>.repository.ts`, taking the database as its first parameter.
37
+ 3. Routes in `modules/<feature>/router/routes/<feature>.routes.ts`, default-exported, registered in
38
+ `src/router/index.ts` **before** the global catch-all.
39
+ 4. A tab in `src/app/tabs.ts` only if it is a top-level section — with both `iconIos` and `iconMd`.
40
+ Otherwise it is a pushed page, and pushed pages call `useHiddenTabbar()`.
41
+ 5. Strings in **both** `locales/en.json` and `locales/fr.json`. A missing key renders as the key.
42
+ Remember `@` and `|` are message syntax.
43
+ 6. A test in `tests/` for anything with SQL.
44
+ 7. `vp check`, `vp test`, `pnpm type-check`.
45
+
46
+ ## shared/ is not a junk drawer
47
+
48
+ Something moves to `shared/` when a second module imports it **today**. The test: name the second
49
+ caller. If you cannot, it lives in the module that uses it.
50
+
51
+ ## Auto-imports
52
+
53
+ `ref`, `computed`, `watch`, lifecycle hooks, `useI18n`, `@vueuse/core`, `f7`, `f7ready` and
54
+ everything under `shared/composables`, `shared/utils`, `plugins` and `modules/**/composables` are
55
+ auto-imported — no import line. Components under `shared/components` and `modules/**/{views,components}`
56
+ resolve the same way.
57
+
58
+ Two things are **not** auto-imported and must be declared:
59
+
60
+ - `f7route` / `f7router` — Framework7 passes them to a route component as props.
61
+ - Repositories and anything under `domains/` — imported explicitly, because a domain is a boundary
62
+ you should see being crossed.
63
+
64
+ `auto-imports.d.ts` and `components.d.ts` are generated. Never hand-edit them; if the editor
65
+ disagrees with the build, run the dev server once to regenerate.
@@ -0,0 +1,183 @@
1
+ # File templates
2
+
3
+ Copy these shapes. They encode decisions that are easy to get wrong once and then repeat everywhere.
4
+
5
+ ## Route file
6
+
7
+ `modules/<feature>/router/routes/<feature>.routes.ts`
8
+
9
+ ```ts
10
+ import type { Router } from "framework7/types";
11
+
12
+ const featureRoutes: Router.RouteParameters[] = [
13
+ {
14
+ name: "feature",
15
+ path: "/feature/",
16
+ // `async` is Framework7's route hook, not an async function - resolve from the promise.
17
+ async({ resolve }) {
18
+ void import("@/modules/feature/views/FeatureView.vue").then((view) => {
19
+ resolve({ component: view.default });
20
+ });
21
+ },
22
+ },
23
+ {
24
+ name: "feature-detail",
25
+ path: "/feature/:id/",
26
+ async({ resolve }) {
27
+ void import("@/modules/feature/views/FeatureDetailView.vue").then((view) => {
28
+ resolve({ component: view.default });
29
+ });
30
+ },
31
+ },
32
+ ];
33
+
34
+ export default featureRoutes;
35
+ ```
36
+
37
+ `await` inside that hook does not compile — the property is literally named `async`.
38
+
39
+ ## Repository
40
+
41
+ `domains/<domain>/<domain>.repository.ts`
42
+
43
+ ```ts
44
+ import type { Kysely } from "kysely";
45
+ import { nowISO } from "@cavulsqa/mobile-db";
46
+ import type { Database } from "@/shared/database/schema";
47
+
48
+ export interface ThingRow {
49
+ id: number;
50
+ name: string;
51
+ totalCents: number;
52
+ }
53
+
54
+ /** Reads take the database as a parameter - that is what makes them testable. */
55
+ export function listThings(db: Kysely<Database>, term: string): Promise<ThingRow[]> {
56
+ let query = db.selectFrom("thing").select(["id", "name", "total_cents as totalCents"]);
57
+ if (term.trim()) query = query.where("name", "like", `%${term.trim()}%`);
58
+ return query.orderBy("id", "desc").limit(40).execute();
59
+ }
60
+
61
+ /** All-or-nothing work goes in one transaction. */
62
+ export async function saveThing(
63
+ db: Kysely<Database>,
64
+ input: { name: string; lines: Array<{ productId: number; quantity: number }> },
65
+ ): Promise<void> {
66
+ await db.transaction().execute(async (trx) => {
67
+ const thing = await trx
68
+ .insertInto("thing")
69
+ .values({ created_at: nowISO(), name: input.name })
70
+ .returning("id")
71
+ .executeTakeFirstOrThrow();
72
+
73
+ for (const line of input.lines) {
74
+ await trx
75
+ .insertInto("thing_line")
76
+ .values({ thing_id: thing.id, ...line })
77
+ .execute();
78
+ }
79
+ });
80
+ }
81
+ ```
82
+
83
+ No `ref`, no lifecycle, no Framework7, no imports from `modules/`.
84
+
85
+ ## Composable
86
+
87
+ `modules/<feature>/composables/useFeature.ts`
88
+
89
+ ```ts
90
+ import { listThings, saveThing, type ThingRow } from "@/domains/thing/thing.repository";
91
+ import { getDatabase, rdb } from "@/shared/database/database";
92
+ import { uniqueQueryKey, useReactiveQuery } from "@/shared/database/queries";
93
+
94
+ export function useFeature() {
95
+ const term = ref("");
96
+ const busy = ref(false);
97
+
98
+ const query = useReactiveQuery(() => listThings(getDatabase().db, term.value), {
99
+ // Every table the SQL touches. A join means each joined table.
100
+ tables: ["thing"],
101
+ queryKey: uniqueQueryKey("feature:things"),
102
+ debounce: 250,
103
+ });
104
+
105
+ const things = computed<ThingRow[]>(() => query.data.value ?? []);
106
+
107
+ // Writes go through `rdb`, which announces the tables they touched.
108
+ async function save(input: Parameters<typeof saveThing>[1]) {
109
+ busy.value = true;
110
+ try {
111
+ await saveThing(rdb, input);
112
+ } finally {
113
+ busy.value = false;
114
+ }
115
+ }
116
+
117
+ return { term, things, loading: query.loading, busy, save };
118
+ }
119
+ ```
120
+
121
+ `ref`, `computed` and the composable itself are auto-imported. Repositories are not.
122
+
123
+ ## View
124
+
125
+ `modules/<feature>/views/FeatureView.vue`
126
+
127
+ ```vue
128
+ <template>
129
+ <F7Page>
130
+ <F7Navbar :title="t('feature.title')" large :sliding="true" />
131
+
132
+ <F7BlockTitle>{{ t("feature.section") }}</F7BlockTitle>
133
+ <F7List v-if="things.length" media-list strong inset dividers class="rounded-2xl!">
134
+ <F7ListItem
135
+ v-for="thing in things"
136
+ :key="thing.id"
137
+ :title="thing.name"
138
+ :link="`/feature/${String(thing.id)}/`"
139
+ >
140
+ <template #media><F7Icon f7="cube_box_fill" color="blue" /></template>
141
+ <template #subtitle>
142
+ <span class="text-[13px] opacity-60">{{ thing.totalCents }}</span>
143
+ </template>
144
+ </F7ListItem>
145
+ </F7List>
146
+ <F7Block v-else strong inset class="rounded-2xl!">
147
+ <p class="m-0 text-sm opacity-70">{{ t("feature.empty") }}</p>
148
+ </F7Block>
149
+ </F7Page>
150
+ </template>
151
+
152
+ <script setup lang="ts">
153
+ import { useFeature } from "@/modules/feature/composables/useFeature";
154
+
155
+ const { t } = useI18n();
156
+ const { things } = useFeature();
157
+ </script>
158
+ ```
159
+
160
+ No `f7-*` imports, no `<style>`. `subtitle` needs the media list.
161
+
162
+ ## Pushed detail view
163
+
164
+ Same shape, plus:
165
+
166
+ ```ts
167
+ import { useHiddenTabbar } from "@/shared/composables/useTabbarVisibility";
168
+
169
+ const { t } = useI18n();
170
+
171
+ // A pushed page owns the whole screen; the tab bar belongs to the tab roots.
172
+ useHiddenTabbar();
173
+
174
+ const props = defineProps<{ f7route: Router.Route; f7router: Router.Router }>();
175
+ const id = Number(props.f7route.params.id ?? 0);
176
+ ```
177
+
178
+ Actions on a detail screen go in `F7Toolbar bottom`.
179
+
180
+ ## Test
181
+
182
+ `tests/<domain>.repository.test.ts` — see the reactive-data skill's testing guide for the harness.
183
+ Assert the arithmetic, the empty state and the idempotence, not just that rows come back.
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: reactive-data
3
+ description: Reading and writing the local SQLite database in this app. Use whenever the task involves a query, a mutation, a repository, a migration, the schema, useReactiveQuery, the change bus, stale data, a screen not refreshing, query metrics, or testing data access. Covers the tables invalidation contract, why every write goes through rdb, and how to test SQL without a device.
4
+ ---
5
+
6
+ # Reactive data workflow
7
+
8
+ SQLite is the source of truth. A screen reads it through a reactive query; a write announces the
9
+ tables it touched and every query watching one of them refetches. Nothing calls refetch by hand.
10
+
11
+ ## Adding a read
12
+
13
+ ```ts
14
+ const query = useReactiveQuery(() => searchOrders(getDatabase().db, term.value), {
15
+ tables: ["sales_order", "order_line", "customer"],
16
+ queryKey: uniqueQueryKey("demo:orders"),
17
+ debounce: 250,
18
+ });
19
+ ```
20
+
21
+ 1. Put the SQL in a repository: `src/domains/<domain>/<domain>.repository.ts`, taking the database
22
+ as its first parameter. Never reach for the singleton inside a repository — that is what makes it
23
+ testable.
24
+ 2. **List every table the SQL touches in `tables`.** Count them in the query, not from memory: a
25
+ join means each joined table. Under-list and the screen goes stale with no error; over-list and
26
+ an unrelated write re-runs an expensive query.
27
+ 3. `queryKey`: default to `uniqueQueryKey("prefix")`. A stable literal means "share this result with
28
+ any other query using the same key", which is right for one list rendered twice and wrong for
29
+ anything parameterised.
30
+ 4. `debounce` so a burst of writes causes one refetch.
31
+
32
+ ## Adding a write
33
+
34
+ ```ts
35
+ await saveOrder(rdb, payload); // announces its tables
36
+ await saveOrder(getDatabase().db, …); // writes, and no screen notices
37
+ ```
38
+
39
+ `rdb` is the reactive wrapper. A raw write lands in SQLite silently and the UI keeps showing old
40
+ rows until something unrelated refetches — no error, nothing to see in review.
41
+
42
+ All-or-nothing work goes in one transaction. A helper anyone can press twice must survive being
43
+ pressed twice: insert-where-missing, or `onConflict(...).doNothing()`.
44
+
45
+ ## Schema changes
46
+
47
+ `schema.ts` and `migrations.ts` are edited together — a field in one and not the other is a runtime
48
+ error the compiler cannot see. Migrations are numbered, never renamed, never edited after shipping.
49
+ Money is integer cents. Index what you filter and join by.
50
+
51
+ ## Testing
52
+
53
+ Always. [testing.md](testing.md) has the harness: a Kysely on `createSqlJsDialect()`, migrate, then
54
+ assert against real rows — including the arithmetic. A total that type-checks can still be computed
55
+ wrong, and a device is not needed to catch that.
56
+
57
+ ## Diagnosing "the screen did not update"
58
+
59
+ In order:
60
+
61
+ 1. Did the write go through `rdb`?
62
+ 2. Does the query's `tables` include the table that changed?
63
+ 3. Is the query's `queryKey` shared with a different query? Check the console for the conflict
64
+ warning.
65
+ 4. Is the page off-screen? `usePageVisibility` suppresses refetches for hidden pages by design.
66
+ 5. Only then look at the packages.
67
+
68
+ ## Proof obligations
69
+
70
+ Name the tables the SQL touches and confirm `tables` matches. Say whether a test covers it. Never
71
+ claim a data path works on the strength of a type-check.
72
+
73
+ ## Inserting a parent and its children
74
+
75
+ The parent and its children go in one transaction, and the parent's id comes from `insertId`:
76
+
77
+ ```ts
78
+ await db.transaction().execute(async (trx) => {
79
+ const inserted = await trx.insertInto("sales_order").values({ ... }).executeTakeFirstOrThrow();
80
+ const orderId = Number(inserted.insertId ?? 0);
81
+ if (!orderId) throw new Error("the order was written but the database reported no id for it");
82
+ ...
83
+ });
84
+ ```
85
+
86
+ `.returning("id")` looks like the obvious way and is the wrong one: inside an open transaction the
87
+ plugin runs the statement through `query()` and discards its RETURNING rows, so kysely throws
88
+ `no result` from a write that succeeded. `insertId` comes from `last_insert_rowid()` and works on
89
+ both sides of the boundary. Full note in [database.md](../../rules/database.md).
@@ -0,0 +1,55 @@
1
+ # Testing data access
2
+
3
+ The queries run against real SQLite — sql.js, the same dialect `@cavulsqa/mobile-db` uses for its
4
+ own tests. No device, no emulator, about a second for the suite.
5
+
6
+ ## The harness
7
+
8
+ ```ts
9
+ import { beforeEach, expect, test } from "vite-plus/test";
10
+ import { Kysely } from "kysely";
11
+ import { Migrator } from "kysely/migration";
12
+ import { createSqlJsDialect } from "@cavulsqa/mobile-db/testing";
13
+ import { migrations } from "../src/shared/database/migrations.js";
14
+ import type { Database } from "../src/shared/database/schema.js";
15
+
16
+ let db: Kysely<Database>;
17
+
18
+ beforeEach(async () => {
19
+ db = new Kysely<Database>({ dialect: await createSqlJsDialect() });
20
+ await new Migrator({
21
+ db,
22
+ provider: { getMigrations: () => Promise.resolve(migrations) },
23
+ }).migrateToLatest();
24
+ });
25
+ ```
26
+
27
+ A fresh in-memory database per test, with the app's real migrations applied. `Migrator` comes from
28
+ `kysely/migration` — the root export is a compile-time error in kysely 0.29.
29
+
30
+ ## What to assert
31
+
32
+ Type-checking proves the query compiles. These are the things it cannot prove:
33
+
34
+ - **Arithmetic.** Two lines at quantity 1 and 2 × 1000 cents must total 3000. Write the number.
35
+ - **Empty state.** An aggregate over no rows returns `0`, not `null`. `coalesce` is easy to forget
36
+ and the screen shows a blank tile.
37
+ - **Joins.** Every joined row actually resolves — product names present, customer attached.
38
+ - **Filters.** A search matches on each field it claims to, and returns `[]` for no match.
39
+ - **State machines.** A status cycle lands where you expect at each step.
40
+ - **Idempotence.** Anything a person can press twice, pressed twice. `seedSampleData` threw
41
+ `UNIQUE constraint failed: tag.label` on the second press and only a test caught it.
42
+ - **Missing rows.** A lookup for an id that does not exist returns `null`, and an update against one
43
+ is a no-op rather than a throw.
44
+
45
+ ## The icon test
46
+
47
+ `tests/icons.test.ts` guards a different silent failure: it reads the framework7-icons ttf and
48
+ asserts every name used in `src/` is a real ligature, plus that the five names this app has already
49
+ got wrong stay unresolvable. Extend the second list whenever a wrong name gets through.
50
+
51
+ ## Running
52
+
53
+ ```bash
54
+ vp test # from templates/f7-app
55
+ ```