@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,458 @@
1
+ import { sql, type Kysely } from "kysely";
2
+ import { nowISO } from "@cavulsqa/mobile-db";
3
+ import type { Database } from "@/shared/database/schema";
4
+
5
+ export interface DashboardStats {
6
+ customers: number;
7
+ products: number;
8
+ orders: number;
9
+ draft: number;
10
+ confirmed: number;
11
+ delivered: number;
12
+ revenueCents: number;
13
+ /** Only confirmed and delivered orders count as money you will actually see. */
14
+ committedCents: number;
15
+ }
16
+
17
+ export interface OrderRow {
18
+ id: number;
19
+ reference: string;
20
+ status: string;
21
+ createdAt: string;
22
+ customerName: string;
23
+ city: string;
24
+ lines: number;
25
+ totalCents: number;
26
+ }
27
+
28
+ const CITIES = ["Algiers", "Oran", "Constantine", "Annaba", "Blida", "Sétif"];
29
+ const TAGS = ["wholesale", "retail", "priority", "new"];
30
+ const PRODUCTS: Array<[string, number]> = [
31
+ ["Espresso 1kg", 189000],
32
+ ["Green tea 500g", 74000],
33
+ ["Ceramic mug", 32000],
34
+ ["Filter papers", 9500],
35
+ ["Cold brew bottle", 45000],
36
+ ];
37
+
38
+ /** Reads go straight to the database; only writes need the reactive wrapper. */
39
+ export function loadDashboardStats(db: Kysely<Database>): Promise<DashboardStats> {
40
+ return db
41
+ .selectNoFrom((eb) => [
42
+ eb
43
+ .selectFrom("customer")
44
+ .select((e) => e.fn.countAll<number>().as("n"))
45
+ .as("customers"),
46
+ eb
47
+ .selectFrom("product")
48
+ .select((e) => e.fn.countAll<number>().as("n"))
49
+ .as("products"),
50
+ eb
51
+ .selectFrom("sales_order")
52
+ .select((e) => e.fn.countAll<number>().as("n"))
53
+ .as("orders"),
54
+ eb
55
+ .selectFrom("order_line")
56
+ .select(sql<number>`coalesce(sum(quantity * unit_price_cents), 0)`.as("n"))
57
+ .as("revenue"),
58
+ eb
59
+ .selectFrom("sales_order")
60
+ .select((e) => e.fn.countAll<number>().as("n"))
61
+ .where("status", "=", "draft")
62
+ .as("draft"),
63
+ eb
64
+ .selectFrom("sales_order")
65
+ .select((e) => e.fn.countAll<number>().as("n"))
66
+ .where("status", "=", "confirmed")
67
+ .as("confirmed"),
68
+ eb
69
+ .selectFrom("sales_order")
70
+ .select((e) => e.fn.countAll<number>().as("n"))
71
+ .where("status", "=", "delivered")
72
+ .as("delivered"),
73
+ eb
74
+ .selectFrom("order_line")
75
+ .innerJoin("sales_order", "sales_order.id", "order_line.order_id")
76
+ .select(
77
+ sql<number>`coalesce(sum(order_line.quantity * order_line.unit_price_cents), 0)`.as("n"),
78
+ )
79
+ .where("sales_order.status", "in", ["confirmed", "delivered"])
80
+ .as("committed"),
81
+ ])
82
+ .executeTakeFirstOrThrow()
83
+ .then((row) => ({
84
+ customers: Number(row.customers ?? 0),
85
+ products: Number(row.products ?? 0),
86
+ orders: Number(row.orders ?? 0),
87
+ draft: Number(row.draft ?? 0),
88
+ confirmed: Number(row.confirmed ?? 0),
89
+ delivered: Number(row.delivered ?? 0),
90
+ revenueCents: Number(row.revenue ?? 0),
91
+ committedCents: Number(row.committed ?? 0),
92
+ }));
93
+ }
94
+
95
+ /**
96
+ * The join is the point: this reads four tables, so a write to any of them has to refresh it. The
97
+ * search term filters on the customer and the reference together.
98
+ */
99
+ export function searchOrders(db: Kysely<Database>, term: string): Promise<OrderRow[]> {
100
+ const like = `%${term.trim()}%`;
101
+
102
+ let query = db
103
+ .selectFrom("sales_order")
104
+ .innerJoin("customer", "customer.id", "sales_order.customer_id")
105
+ .leftJoin("order_line", "order_line.order_id", "sales_order.id")
106
+ .select(({ fn }) => [
107
+ "sales_order.id as id",
108
+ "sales_order.reference as reference",
109
+ "sales_order.status as status",
110
+ "sales_order.created_at as createdAt",
111
+ "customer.name as customerName",
112
+ "customer.city as city",
113
+ fn.count<number>("order_line.id").as("lines"),
114
+ sql<number>`coalesce(sum(order_line.quantity * order_line.unit_price_cents), 0)`.as(
115
+ "totalCents",
116
+ ),
117
+ ])
118
+ .groupBy(["sales_order.id", "customer.name", "customer.city"])
119
+ .orderBy("sales_order.id", "desc")
120
+ .limit(40);
121
+
122
+ if (term.trim()) {
123
+ query = query.where((eb) =>
124
+ eb.or([eb("customer.name", "like", like), eb("sales_order.reference", "like", like)]),
125
+ );
126
+ }
127
+
128
+ return query.execute().then((rows) =>
129
+ rows.map((row) => ({
130
+ ...row,
131
+ lines: Number(row.lines),
132
+ totalCents: Number(row.totalCents),
133
+ })),
134
+ );
135
+ }
136
+
137
+ export function listCustomers(
138
+ db: Kysely<Database>,
139
+ ): Promise<Array<{ id: number; name: string; city: string }>> {
140
+ return db.selectFrom("customer").select(["id", "name", "city"]).orderBy("name").execute();
141
+ }
142
+
143
+ const REFERENCE_CUSTOMERS = 6;
144
+
145
+ /**
146
+ * The catalogue and a few customers - what the app needs before anyone can write an order at all.
147
+ * Run at startup, because a first launch that makes you press "seed" before the New order sheet has
148
+ * anything to pick is not a first launch anyone should have.
149
+ *
150
+ * Idempotent by table rather than by row: these are fixtures, so if they are there this has run.
151
+ */
152
+ export async function ensureReferenceData(db: Kysely<Database>): Promise<void> {
153
+ const at = nowISO();
154
+
155
+ const existing = await db.selectFrom("product").select("name").execute();
156
+ const known = new Set(existing.map((product) => product.name));
157
+
158
+ for (const [name, price] of PRODUCTS) {
159
+ if (known.has(name)) continue;
160
+ await db.insertInto("product").values({ created_at: at, name, price_cents: price }).execute();
161
+ }
162
+
163
+ // `tag.label` is unique, and a plain insert threw "UNIQUE constraint failed" on the second run.
164
+ for (const label of TAGS) {
165
+ await db
166
+ .insertInto("tag")
167
+ .values({ label })
168
+ .onConflict((oc) => oc.column("label").doNothing())
169
+ .execute();
170
+ }
171
+
172
+ const customers = await db.selectFrom("customer").select("id").execute();
173
+ if (customers.length) return;
174
+
175
+ const tags = await db.selectFrom("tag").select("id").execute();
176
+
177
+ for (let index = 0; index < REFERENCE_CUSTOMERS; index++) {
178
+ const customerId = await insertCustomer(db, at);
179
+ const tag = tags[index % tags.length];
180
+ if (tag) {
181
+ await db
182
+ .insertInto("customer_tag")
183
+ .values({ customer_id: customerId, tag_id: tag.id })
184
+ .execute();
185
+ }
186
+ }
187
+ }
188
+
189
+ /**
190
+ * The name is numbered from how many customers exist right now, so it stays unique across seeds -
191
+ * counted per insert rather than once per batch, because adding a loop index to a count that the
192
+ * loop is itself growing produces collisions.
193
+ *
194
+ * `insertId` rather than `.returning("id")` for the same reason as `saveOrder`: it is the one way
195
+ * that works on both sides of a transaction boundary.
196
+ */
197
+ async function insertCustomer(db: Kysely<Database>, at: string): Promise<number> {
198
+ const existing = (await db.selectFrom("customer").select("id").execute()).length;
199
+
200
+ const inserted = await db
201
+ .insertInto("customer")
202
+ .values({
203
+ created_at: at,
204
+ name: `Customer ${String(existing + 1)}`,
205
+ city: CITIES[existing % CITIES.length] ?? "Algiers",
206
+ })
207
+ .executeTakeFirstOrThrow();
208
+
209
+ const id = Number(inserted.insertId ?? 0);
210
+ if (!id) throw new Error("the customer was written but the database reported no id for it");
211
+ return id;
212
+ }
213
+
214
+ /**
215
+ * Demo orders on top of the reference data. Every write goes through `rdb`, so each announces the
216
+ * tables it touched.
217
+ *
218
+ * This is a button people press more than once, so it adds to the data rather than assuming an
219
+ * empty database.
220
+ */
221
+ export async function seedSampleData(db: Kysely<Database>, customers = 3): Promise<void> {
222
+ await ensureReferenceData(db);
223
+
224
+ const at = nowISO();
225
+ const products = await db.selectFrom("product").select(["id", "price_cents"]).execute();
226
+ const tags = await db.selectFrom("tag").select("id").execute();
227
+
228
+ for (let index = 0; index < customers; index++) {
229
+ const customerId = await insertCustomer(db, at);
230
+
231
+ const tag = tags[index % tags.length];
232
+ if (tag) {
233
+ await db
234
+ .insertInto("customer_tag")
235
+ .values({ customer_id: customerId, tag_id: tag.id })
236
+ .execute();
237
+ }
238
+
239
+ for (let orderIndex = 0; orderIndex < 2; orderIndex++) {
240
+ await createOrder(db, customerId, products.slice(0, 2 + ((index + orderIndex) % 3)));
241
+ }
242
+ }
243
+ }
244
+
245
+ export async function createOrder(
246
+ db: Kysely<Database>,
247
+ customerId: number,
248
+ lines: Array<{ id: number; price_cents: number }>,
249
+ ): Promise<void> {
250
+ const at = nowISO();
251
+ const reference = `SO-${String(Date.now() % 1000000).padStart(6, "0")}`;
252
+
253
+ const inserted = await db
254
+ .insertInto("sales_order")
255
+ .values({ created_at: at, customer_id: customerId, reference, status: "draft" })
256
+ .executeTakeFirstOrThrow();
257
+
258
+ const orderId = Number(inserted.insertId ?? 0);
259
+ if (!orderId) throw new Error("the order was written but the database reported no id for it");
260
+
261
+ for (const [index, line] of lines.entries()) {
262
+ await db
263
+ .insertInto("order_line")
264
+ .values({
265
+ order_id: orderId,
266
+ product_id: line.id,
267
+ quantity: index + 1,
268
+ unit_price_cents: line.price_cents,
269
+ })
270
+ .execute();
271
+ }
272
+ }
273
+
274
+ export async function advanceOrderStatus(db: Kysely<Database>, orderId: number): Promise<void> {
275
+ const next = { draft: "confirmed", confirmed: "delivered", delivered: "draft" } as const;
276
+ const current = await db
277
+ .selectFrom("sales_order")
278
+ .select("status")
279
+ .where("id", "=", orderId)
280
+ .executeTakeFirst();
281
+ if (!current) return;
282
+
283
+ await db
284
+ .updateTable("sales_order")
285
+ .set({ status: next[current.status] })
286
+ .where("id", "=", orderId)
287
+ .execute();
288
+ }
289
+
290
+ /** Order lines go with the order through the cascade, so only the parents are deleted here. */
291
+ export async function clearAll(db: Kysely<Database>): Promise<void> {
292
+ await db.deleteFrom("customer").execute();
293
+ await db.deleteFrom("sales_order").execute();
294
+ await db.deleteFrom("order_line").execute();
295
+ await db.deleteFrom("customer_tag").execute();
296
+ await db.deleteFrom("product").execute();
297
+ await db.deleteFrom("tag").execute();
298
+ }
299
+
300
+ export interface ProductRow {
301
+ id: number;
302
+ name: string;
303
+ price_cents: number;
304
+ }
305
+
306
+ export function listProducts(db: Kysely<Database>): Promise<ProductRow[]> {
307
+ return db.selectFrom("product").select(["id", "name", "price_cents"]).orderBy("name").execute();
308
+ }
309
+
310
+ /**
311
+ * Sequential rather than random, because a reference a person reads should count up. Derived from
312
+ * the highest existing number so it survives deletions without reusing a reference.
313
+ */
314
+ export async function nextOrderReference(db: Kysely<Database>): Promise<string> {
315
+ const row = await db
316
+ .selectFrom("sales_order")
317
+ .select(sql<number>`coalesce(max(cast(substr(reference, 4) as integer)), 0)`.as("highest"))
318
+ .executeTakeFirst();
319
+
320
+ return `SO-${String(Number(row?.highest ?? 0) + 1).padStart(4, "0")}`;
321
+ }
322
+
323
+ export interface DraftLine {
324
+ productId: number;
325
+ quantity: number;
326
+ unitPriceCents: number;
327
+ }
328
+
329
+ /** The whole order in one transaction: a half-written order is worse than no order. */
330
+ export async function saveOrder(
331
+ db: Kysely<Database>,
332
+ input: { customerId: number; reference: string; lines: DraftLine[] },
333
+ ): Promise<void> {
334
+ await db.transaction().execute(async (trx) => {
335
+ /**
336
+ * `insertId`, not `.returning("id")`: the SQLite plugin runs a statement issued inside an open
337
+ * transaction through `query()`, which executes it but drops its RETURNING rows - so the insert
338
+ * succeeded and kysely still threw "no result". `insertId` comes from `last_insert_rowid()`.
339
+ */
340
+ const inserted = await trx
341
+ .insertInto("sales_order")
342
+ .values({
343
+ created_at: nowISO(),
344
+ customer_id: input.customerId,
345
+ reference: input.reference,
346
+ status: "draft",
347
+ })
348
+ .executeTakeFirstOrThrow();
349
+
350
+ const orderId = Number(inserted.insertId ?? 0);
351
+ if (!orderId) throw new Error("the order was written but the database reported no id for it");
352
+
353
+ for (const line of input.lines) {
354
+ await trx
355
+ .insertInto("order_line")
356
+ .values({
357
+ order_id: orderId,
358
+ product_id: line.productId,
359
+ quantity: line.quantity,
360
+ unit_price_cents: line.unitPriceCents,
361
+ })
362
+ .execute();
363
+ }
364
+ });
365
+ }
366
+
367
+ export async function deleteOrder(db: Kysely<Database>, orderId: number): Promise<void> {
368
+ // The cascade only fires with foreign keys on, which the plugin does not guarantee per connection.
369
+ await db.deleteFrom("order_line").where("order_id", "=", orderId).execute();
370
+ await db.deleteFrom("sales_order").where("id", "=", orderId).execute();
371
+ }
372
+
373
+ export async function setOrderStatus(
374
+ db: Kysely<Database>,
375
+ orderId: number,
376
+ status: "draft" | "confirmed" | "delivered",
377
+ ): Promise<void> {
378
+ await db.updateTable("sales_order").set({ status }).where("id", "=", orderId).execute();
379
+ }
380
+
381
+ export interface OrderLineRow {
382
+ id: number;
383
+ productName: string;
384
+ quantity: number;
385
+ unitPriceCents: number;
386
+ lineTotalCents: number;
387
+ }
388
+
389
+ export interface OrderDetail {
390
+ id: number;
391
+ reference: string;
392
+ status: "draft" | "confirmed" | "delivered";
393
+ createdAt: string;
394
+ customerName: string;
395
+ city: string;
396
+ tags: string[];
397
+ lines: OrderLineRow[];
398
+ totalCents: number;
399
+ }
400
+
401
+ /**
402
+ * The detail read touches five of the six tables - order, customer, line, product and the tag
403
+ * junction - which is what makes it a fair demonstration: any write in that set refreshes it.
404
+ */
405
+ export async function loadOrderDetail(
406
+ db: Kysely<Database>,
407
+ orderId: number,
408
+ ): Promise<OrderDetail | null> {
409
+ const header = await db
410
+ .selectFrom("sales_order")
411
+ .innerJoin("customer", "customer.id", "sales_order.customer_id")
412
+ .select([
413
+ "sales_order.id as id",
414
+ "sales_order.reference as reference",
415
+ "sales_order.status as status",
416
+ "sales_order.created_at as createdAt",
417
+ "customer.id as customerId",
418
+ "customer.name as customerName",
419
+ "customer.city as city",
420
+ ])
421
+ .where("sales_order.id", "=", orderId)
422
+ .executeTakeFirst();
423
+
424
+ if (!header) return null;
425
+
426
+ const [lines, tags] = await Promise.all([
427
+ db
428
+ .selectFrom("order_line")
429
+ .innerJoin("product", "product.id", "order_line.product_id")
430
+ .select([
431
+ "order_line.id as id",
432
+ "product.name as productName",
433
+ "order_line.quantity as quantity",
434
+ "order_line.unit_price_cents as unitPriceCents",
435
+ sql<number>`order_line.quantity * order_line.unit_price_cents`.as("lineTotalCents"),
436
+ ])
437
+ .where("order_line.order_id", "=", orderId)
438
+ .execute(),
439
+ db
440
+ .selectFrom("customer_tag")
441
+ .innerJoin("tag", "tag.id", "customer_tag.tag_id")
442
+ .select("tag.label as label")
443
+ .where("customer_tag.customer_id", "=", header.customerId)
444
+ .execute(),
445
+ ]);
446
+
447
+ return {
448
+ id: header.id,
449
+ reference: header.reference,
450
+ status: header.status,
451
+ createdAt: header.createdAt,
452
+ customerName: header.customerName,
453
+ city: header.city,
454
+ tags: tags.map((row) => row.label),
455
+ lines: lines.map((line) => ({ ...line, lineTotalCents: Number(line.lineTotalCents) })),
456
+ totalCents: lines.reduce((sum, line) => sum + Number(line.lineTotalCents), 0),
457
+ };
458
+ }
@@ -0,0 +1,23 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ /**
4
+ * For the checkers that do not parse single-file components. `vue-tsc` resolves the real SFC and
5
+ * ignores this; it exists so `vp check`'s type-aware pass can follow a `.vue` import instead of
6
+ * reporting every one as a missing module.
7
+ *
8
+ * Safe here because every `.vue` import in this template is a lazily loaded route component, whose
9
+ * props are never checked at the import site.
10
+ */
11
+ declare module "*.vue" {
12
+ import type { DefineComponent } from "vue";
13
+
14
+ const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>;
15
+ export default component;
16
+ }
17
+
18
+ /** Icon font, imported for its side effect only. */
19
+ declare module "framework7-icons";
20
+
21
+ /** Injected by vite `define`; see vite.config.ts. */
22
+ declare const __APP_NAME__: string;
23
+ declare const __APP_VERSION__: string;
@@ -0,0 +1,179 @@
1
+ {
2
+ "tabs": {
3
+ "home": "Overview",
4
+ "demo": "Reactive",
5
+ "settings": "Settings"
6
+ },
7
+ "home": {
8
+ "title": "Overview",
9
+ "intro": "is a starter for offline-first mobile apps: Vue 3, Framework7 and Capacitor over a local SQLite database that screens read reactively.",
10
+ "included": "Core features",
11
+ "next": "See it work",
12
+ "welcome": "Welcome",
13
+ "tapAny": "Tap any feature — each opens inside this tab with its own transition and navbar style."
14
+ },
15
+ "demo": {
16
+ "title": "Reactive",
17
+ "intro": "A small relational schema — customers, products, orders, order lines and tags — read through joins and aggregates. Nothing on this screen asks a query to refresh.",
18
+ "clear": "Reset data",
19
+ "refetching": "refetching…",
20
+ "empty": "No orders yet. Use the button, bottom right.",
21
+ "pipelining": "Read pipelining",
22
+ "measure": "Measure",
23
+ "measuring": "Measuring…",
24
+ "parallel": "Together",
25
+ "sequential": "One by one",
26
+ "ratio": "Faster by",
27
+ "pipeliningNative": "{count} reads issued together, then the same {count} awaited one at a time. Reads stay outside the write lock and the native bridge pipelines them, so together should win by several times.",
28
+ "pipeliningWeb": "{count} reads issued together, then the same {count} awaited one at a time. In a browser this measures nothing useful — see the note under the result.",
29
+ "ratioNative": "Above 1x means concurrent reads are genuinely overlapping on the native bridge. Near 1x would mean something is serialising them.",
30
+ "ratioWeb": "Expected on {platform}: the web path is sql.js in memory with no bridge to pipeline, so issuing reads together buys nothing and adds a little overhead. The several-times gain is a device property — measure it there.",
31
+ "data": "Data",
32
+ "diagnostics": "Diagnostics",
33
+ "searchPlaceholder": "Search orders or customers",
34
+ "orders": "Orders",
35
+ "lines": "{count} lines",
36
+ "seed": "Seed sample data",
37
+ "tiles": {
38
+ "draft": "Draft",
39
+ "confirmed": "Confirmed",
40
+ "delivered": "Delivered",
41
+ "committed": "Committed",
42
+ "ofOrders": "of {count} orders",
43
+ "ofTotal": "of {total} total"
44
+ },
45
+ "bus": "Change bus",
46
+ "busHint": "Every event a write published. The queries are subscribed to the same stream.",
47
+ "busEmpty": "No events yet.",
48
+ "swipeHint": "Swipe a row for the quick action, tap it for all of them.",
49
+ "newOrder": "New order",
50
+ "reference": "Reference",
51
+ "customer": "Customer",
52
+ "products": "Products",
53
+ "total": "Total",
54
+ "save": "Save order",
55
+ "cancel": "Cancel",
56
+ "confirm": "Confirm",
57
+ "deliver": "Mark delivered",
58
+ "backToDraft": "Back to draft",
59
+ "delete": "Delete",
60
+ "searchHint": "Type to search orders by reference or customer.",
61
+ "noMatches": "Nothing matches that search.",
62
+ "order": "Order",
63
+ "orderGone": "That order no longer exists.",
64
+ "lineItems": "Line items",
65
+ "searchTitle": "Search orders",
66
+ "statusDraft": "Draft",
67
+ "statusConfirmed": "Confirmed",
68
+ "statusDelivered": "Delivered",
69
+ "deleteConfirm": "Delete this order and its lines?",
70
+ "tapHint": "Tap an order to open it.",
71
+ "allOrders": "Recent orders",
72
+ "matches": "{count} matches"
73
+ },
74
+ "metrics": {
75
+ "totalQueries": "Queries run",
76
+ "avgQueryTime": "Average duration",
77
+ "cacheHitRate": "Cache hit rate",
78
+ "activeListeners": "Active listeners",
79
+ "refetches": "Refetches: {table}",
80
+ "title": "Metrics",
81
+ "summary": "Summary",
82
+ "refetchesTitle": "Refetches per table",
83
+ "noRefetches": "No refetches yet.",
84
+ "slowest": "Slowest queries",
85
+ "noQueries": "No queries recorded yet.",
86
+ "calls": "{count} calls",
87
+ "reset": "Reset metrics"
88
+ },
89
+ "settings": {
90
+ "title": "Settings",
91
+ "locale": "Language",
92
+ "queryMetrics": "Query metrics",
93
+ "slowest": "Slowest queries",
94
+ "noQueries": "No queries recorded yet.",
95
+ "resetMetrics": "Reset metrics",
96
+ "appearance": "Appearance",
97
+ "theme": "Theme",
98
+ "themeAuto": "Auto (device)",
99
+ "darkMode": "Dark mode",
100
+ "language": "Language",
101
+ "tryIt": "Try it out",
102
+ "testToast": "Show a toast",
103
+ "testDialog": "Show a dialog",
104
+ "testSheet": "Open a sheet",
105
+ "about": "About",
106
+ "appName": "Name",
107
+ "version": "Version",
108
+ "platform": "Platform",
109
+ "aboutUs": "About us",
110
+ "toastText": "This is a Framework7 toast.",
111
+ "dialogTitle": "Dialog",
112
+ "dialogText": "Dialogs, sheets and toasts all come from Framework7 - no extra dependency.",
113
+ "sheetText": "A sheet modal. The Android back button closes this before it navigates.",
114
+ "aboutText": "Built on {'@'}cavulsqa/mobile-db, reactive-db and reactive-vue. Replace this text with your own.",
115
+ "footer": "Change these screens in src/modules/settings/."
116
+ },
117
+ "errors": {
118
+ "notFoundTitle": "Not found",
119
+ "notFound": "That screen does not exist."
120
+ },
121
+ "features": {
122
+ "tabs": {
123
+ "title": "Tabbed shell",
124
+ "subtitle": "Tabs as data",
125
+ "text": "Each tab is its own view with its own history, lazily loaded. Add a section by adding one entry."
126
+ },
127
+ "f7": {
128
+ "title": "Framework7",
129
+ "subtitle": "Native iOS and Material",
130
+ "text": "Both themes ship; Settings pins either one or follows the device."
131
+ },
132
+ "sqlite": {
133
+ "title": "SQLite",
134
+ "subtitle": "Kysely, offline-first",
135
+ "text": "Writes take turns on the one native connection while reads run in parallel."
136
+ },
137
+ "reactive": {
138
+ "title": "Reactive queries",
139
+ "subtitle": "No manual invalidation",
140
+ "text": "A write announces its tables and every query watching one refetches, debounced."
141
+ },
142
+ "back": {
143
+ "title": "Android back button",
144
+ "subtitle": "Closes the top layer first",
145
+ "text": "Actions, dialog, sheet, popover, popup, panel - then history, then minimise."
146
+ },
147
+ "keyboard": {
148
+ "title": "Keyboard handling",
149
+ "subtitle": "Field stays visible",
150
+ "text": "Scroll-into-view on every phase of the transition, tab bar out of the way."
151
+ },
152
+ "metrics": {
153
+ "title": "Query metrics",
154
+ "subtitle": "Built in",
155
+ "text": "Durations, cache hits and refetches per table, in the right-hand panel."
156
+ },
157
+ "dx": {
158
+ "title": "Zero-boilerplate DX",
159
+ "subtitle": "Auto-imports and resolvers",
160
+ "text": "Composition API, i18n, Framework7 components and icons resolve without import lines."
161
+ }
162
+ },
163
+ "detail": {
164
+ "what": "What",
165
+ "how": "How",
166
+ "howText": "This page is a child route of the Home tab's own view. The tab bar never moves, the back gesture unwinds only this tab, and the other tabs keep whatever screen they were on.",
167
+ "thisScreen": "What this screen demonstrates",
168
+ "transition": "Transition",
169
+ "transitionNote": "Passed per navigation, so two rows can open the same route differently.",
170
+ "navbar": "Navbar",
171
+ "navbarNote": "large collapses on scroll, transparent fades in over content, hide-on-scroll gets out of the way, subnavbar stays while the title collapses.",
172
+ "nesting": "Nested view",
173
+ "nestingAfter": "Home tab",
174
+ "nestingNote": "Pushed onto this tab's history, not the app's, which is why the tab bar stays put.",
175
+ "scrollMe": "Scroll to see the navbar react",
176
+ "row": "Row",
177
+ "next": "Open the next one"
178
+ }
179
+ }