umpordez 1.2.1 → 1.3.1

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 (30) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/README.md +28 -1
  3. package/cli.mjs +4 -2
  4. package/create.mjs +72 -6
  5. package/package.json +1 -1
  6. package/template/CLAUDE.md +1 -1
  7. package/template/misc/build-local.sh +82 -0
  8. package/template/mobile/{{PROJECT_SLUG}}/App.tsx +5 -2
  9. package/template/mobile/{{PROJECT_SLUG}}/CHANGELOG.md +28 -0
  10. package/template/mobile/{{PROJECT_SLUG}}/CLAUDE.md +10 -1
  11. package/template/mobile/{{PROJECT_SLUG}}/docs/releases.md +78 -0
  12. package/template/mobile/{{PROJECT_SLUG}}/docs/screenshots.md +84 -0
  13. package/template/mobile/{{PROJECT_SLUG}}/docs/store-copy.md +122 -0
  14. package/template/mobile/{{PROJECT_SLUG}}/docs/store-listing.md +107 -0
  15. package/template/mobile/{{PROJECT_SLUG}}/src/config.ts +6 -0
  16. package/template/scripts/README.md +18 -1
  17. package/template/scripts/appstore-resize.sh +70 -0
  18. package/template/scripts/frame-shots.mjs +246 -0
  19. package/template/scripts/gen-mockups.mjs +120 -0
  20. package/template/scripts/screenshots-capture.sh +50 -0
  21. package/template/scripts/screenshots.flow.yaml +54 -0
  22. package/template/ui/site/_variants/app/en.ts +131 -0
  23. package/template/ui/site/_variants/app/footer.ejs +46 -0
  24. package/template/ui/site/_variants/app/header.ejs +102 -0
  25. package/template/ui/site/_variants/app/home.ejs +237 -0
  26. package/template/ui/site/_variants/app/not-found.ejs +22 -0
  27. package/template/ui/site/_variants/app/pt-BR.ts +203 -0
  28. package/template/ui/site/tailwind.config.js +9 -1
  29. package/template-variables.mjs +34 -0
  30. package/update.mjs +30 -1
@@ -46,7 +46,9 @@
46
46
  "Bash(md5 /tmp/umptest/everything/mobile/testapp/assets/icon.png /Users/ligeiro/dev/tranqs/app/mobile/tranqs/assets/icon.png)",
47
47
  "Bash(git merge *)",
48
48
  "Bash(git tag *)",
49
- "Bash(npm pack *)"
49
+ "Bash(npm pack *)",
50
+ "Read(//Users/ligeiro/dev/dropa/**)",
51
+ "Read(//Users/ligeiro/dev/growerheleper-v2/app/**)"
50
52
  ]
51
53
  }
52
54
  }
package/README.md CHANGED
@@ -98,11 +98,19 @@ It asks **what to generate** first:
98
98
  |---|---|
99
99
  | everything | server + admin UI + site + mobile |
100
100
  | web-only | server + admin UI + site (no mobile) |
101
+ | mobile-and-site | mobile app + app-store landing page (no server, no admin) |
101
102
  | mobile-and-server | server + mobile (no UI) |
102
103
  | just-mobile | mobile only (assumes external API) |
103
104
 
105
+ The **public site** ships two landing flavors and the CLI picks by
106
+ preset: a **SaaS landing** (CTAs → admin signup) for server/admin
107
+ presets, and an **app-store landing** (App Store / Play buttons,
108
+ pricing, download) for `mobile-and-site`, branded from your primary
109
+ color.
110
+
104
111
  Then a few questions (name, domain, ports, primary color, mobile
105
- bundle ID if applicable). After scaffold:
112
+ bundle ID + **splash style** — instant show-up or animated — if
113
+ applicable). After scaffold:
106
114
 
107
115
  ```bash
108
116
  cd my-project
@@ -212,6 +220,25 @@ umpordez --version check version
212
220
  - **fetch-api** now supports PUT and PATCH methods plus safer
213
221
  URL parameter filtering (skips null/undefined values).
214
222
 
223
+ ## What's new in 1.3
224
+
225
+ Ship a mobile app + its marketing site in one shot:
226
+
227
+ - **`mobile-and-site` preset** — mobile app + app-store landing page,
228
+ no server or admin. The "indie mobile app" shape.
229
+ - **App-store landing** — App Store / Play buttons, phone mockup,
230
+ pricing, download CTAs; branded from your primary color. The CLI
231
+ picks SaaS vs app landing by preset (and `umpordez update` keeps
232
+ the right flavor).
233
+ - **Splash choice** — instant show-up (default; native splash hides on
234
+ font load) or the animated splash, chosen at scaffold time.
235
+ - **Store launch kit** (every mobile preset) — `CHANGELOG.md` +
236
+ `docs/store-listing.md`, `store-copy.md`, `releases.md`,
237
+ `screenshots.md`.
238
+ - **Screenshot + release automation** — `frame-shots.mjs`,
239
+ `gen-mockups.mjs`, `screenshots-capture.sh` + Maestro flow,
240
+ `appstore-resize.sh`, and `misc/build-local.sh`.
241
+
215
242
  ## What changed in 1.1
216
243
 
217
244
  The big one. v1.0.x was server + web only. v1.1 adds:
package/cli.mjs CHANGED
@@ -41,13 +41,15 @@ function showHelp() {
41
41
  console.log(` umpordez --version ${dim('Show version')}`);
42
42
  console.log('');
43
43
  console.log(green.bold('What you get:'));
44
+ console.log(` ${green('>')} Presets: everything / web-only / mobile+site / mobile+server / just-mobile`);
44
45
  console.log(` ${green('>')} Multi-tenant auth (cookie-based JWT)`);
45
46
  console.log(` ${green('>')} Admin API + Site API (Express)`);
46
47
  console.log(` ${green('>')} React admin panel (Vite + shadcn/ui)`);
47
48
  console.log(` ${green('>')} PostgreSQL + Knex.js migrations`);
48
49
  console.log(` ${green('>')} S3 file uploads with signed URLs`);
49
- console.log(` ${green('>')} EJS-based public site`);
50
- console.log(` ${green('>')} Console task runner`);
50
+ console.log(` ${green('>')} Public site — SaaS or app-store landing (EJS + i18n + SEO)`);
51
+ console.log(` ${green('>')} Mobile app (Expo + TS) — instant or animated splash`);
52
+ console.log(` ${green('>')} Store launch kit (listing, copy, changelog, screenshots)`);
51
53
  console.log(` ${green('>')} Role-based access (admin/owner/manager/user)`);
52
54
  console.log(` ${green('>')} Production build system with versioning`);
53
55
  console.log('');
package/create.mjs CHANGED
@@ -4,7 +4,11 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { execSync } from 'node:child_process';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { buildReplacements } from './template-variables.mjs';
7
+ import {
8
+ buildReplacements,
9
+ APP_LANDING_SWAPS,
10
+ SITE_VARIANT_DIR
11
+ } from './template-variables.mjs';
8
12
  import { generateIcons } from './icons.mjs';
9
13
  import { writeInitialManifest } from './update.mjs';
10
14
 
@@ -45,6 +49,17 @@ const PRESETS = {
45
49
  wantsAdminUi: true,
46
50
  wantsSite: true,
47
51
  },
52
+ 'mobile-and-site': {
53
+ label: 'Mobile app + landing page (no server, no admin)',
54
+ includes: ['mobile', 'ui/site', 'scripts', 'doc'],
55
+ wantsMobile: true,
56
+ wantsServer: false,
57
+ wantsAdminUi: false,
58
+ wantsSite: true,
59
+ // Swaps the SaaS landing for the app-marketing landing
60
+ // (App Store / Play buttons, pricing, download CTA).
61
+ appLanding: true,
62
+ },
48
63
  'mobile-and-server': {
49
64
  label: 'Mobile + server only (no admin UI, no site)',
50
65
  includes: ['server', 'mobile', 'misc', 'scripts', 'doc'],
@@ -70,6 +85,8 @@ const ROOT_FILES_BY_PRESET = {
70
85
  'web-only': ['CLAUDE.md', 'AGENTS.md', 'README.md',
71
86
  'architecture.md', 'code.md',
72
87
  'install.sh', 'seed.sh', 'dev.sh'],
88
+ 'mobile-and-site': ['CLAUDE.md', 'AGENTS.md', 'README.md',
89
+ 'code.md', 'install.sh', 'dev.sh'],
73
90
  'mobile-and-server': ['CLAUDE.md', 'AGENTS.md', 'README.md',
74
91
  'architecture.md', 'code.md',
75
92
  'install.sh', 'seed.sh', 'dev.sh'],
@@ -266,6 +283,16 @@ async function promptProjectDetails() {
266
283
  ? true
267
284
  : 'Lowercase alphanumeric, dashes allowed',
268
285
  },
286
+ {
287
+ type: 'list',
288
+ name: 'splashStyle',
289
+ message: 'Splash screen:',
290
+ choices: [
291
+ { name: 'Instant show-up (recommended)', value: 'instant' },
292
+ { name: 'Animated (glow + logo + pulse)', value: 'animated' },
293
+ ],
294
+ default: 'instant',
295
+ },
269
296
  ];
270
297
 
271
298
  const answers = await inquirer.prompt([
@@ -296,6 +323,7 @@ async function promptProjectDetails() {
296
323
  androidPackage: answers.androidPackage
297
324
  ?? toReverseDomain(answers.domain),
298
325
  mobileScheme: answers.mobileScheme ?? answers.slug,
326
+ splashStyle: answers.splashStyle ?? 'instant',
299
327
  ...answers,
300
328
  };
301
329
  }
@@ -331,17 +359,28 @@ function showSummary(details) {
331
359
  }
332
360
  console.log(` ${chalk.bold('Color:')} ${chalk.hex(details.primaryColor)('██')} ${details.primaryColor}`);
333
361
  console.log(` ${chalk.bold('Theme:')} ${details.defaultTheme}`);
362
+ if (presetDef.wantsSite) {
363
+ console.log(` ${chalk.bold('Landing:')} ${cyan(
364
+ presetDef.appLanding ? 'app store (download)' : 'SaaS (signup)'
365
+ )}`);
366
+ }
334
367
  if (presetDef.wantsMobile) {
335
368
  console.log(` ${chalk.bold('iOS:')} ${details.iosBundleId}`);
336
369
  console.log(` ${chalk.bold('Android:')} ${details.androidPackage}`);
337
370
  console.log(` ${chalk.bold('Scheme:')} ${details.mobileScheme}://`);
371
+ console.log(` ${chalk.bold('Splash:')} ${details.splashStyle === 'animated'
372
+ ? 'animated (glow + logo)'
373
+ : 'instant show-up'}`);
338
374
  }
339
375
  console.log('');
340
376
  console.log(dim(' Generates:'));
341
377
  if (presetDef.wantsServer) console.log(dim(` ${green('>')} Admin API + Site API (Express + TS)`));
342
378
  if (presetDef.wantsAdminUi) console.log(dim(` ${green('>')} React admin panel (Vite + shadcn/ui)`));
343
- if (presetDef.wantsSite) console.log(dim(` ${green('>')} Public site (EJS + Tailwind)`));
379
+ if (presetDef.wantsSite) console.log(dim(` ${green('>')} ${presetDef.appLanding
380
+ ? 'App landing page (store buttons + pricing)'
381
+ : 'Public site (EJS + Tailwind)'}`));
344
382
  if (presetDef.wantsMobile) console.log(dim(` ${green('>')} Mobile app (Expo + TS + auth + theme)`));
383
+ if (presetDef.wantsMobile) console.log(dim(` ${green('>')} Store launch kit (listing, copy, changelog, screenshots)`));
345
384
  if (presetDef.wantsServer) console.log(dim(` ${green('>')} PostgreSQL migrations + seed`));
346
385
  console.log('');
347
386
  }
@@ -383,7 +422,7 @@ function replacePlaceholders(content, replacements) {
383
422
  // config, git ignore) doesn't belong to any single sub-app.
384
423
  const ALWAYS_KEEP = new Set(['.claude']);
385
424
 
386
- async function copyTemplate(outputDir, preset) {
425
+ export async function copyTemplate(outputDir, preset) {
387
426
  const presetDef = PRESETS[preset];
388
427
  await fs.promises.cp(TEMPLATE_DIR, outputDir, { recursive: true });
389
428
 
@@ -442,6 +481,25 @@ async function copyTemplate(outputDir, preset) {
442
481
  await fs.promises.rm(path.join(outputDir, entry.name));
443
482
  }
444
483
  }
484
+
485
+ // Landing-page variant swap. App-focused presets replace the SaaS
486
+ // landing with the app-marketing one before placeholder replacement
487
+ // runs (so the swapped files get their placeholders filled too). The
488
+ // _variants staging dir is always removed — it never ships.
489
+ if (presetDef.appLanding) {
490
+ for (const [variant, canonical] of APP_LANDING_SWAPS) {
491
+ const from = path.join(outputDir, variant);
492
+ const to = path.join(outputDir, canonical);
493
+ if (fs.existsSync(from)) {
494
+ await fs.promises.rm(to, { force: true });
495
+ await fs.promises.rename(from, to);
496
+ }
497
+ }
498
+ }
499
+ await fs.promises.rm(
500
+ path.join(outputDir, SITE_VARIANT_DIR),
501
+ { recursive: true, force: true }
502
+ );
445
503
  }
446
504
 
447
505
  export async function createProject() {
@@ -565,9 +623,14 @@ export async function createProject() {
565
623
  'scripts/run-ios.sh',
566
624
  'scripts/release-android.sh',
567
625
  'scripts/generate-icons.sh',
568
- 'scripts/install-native.sh'
626
+ 'scripts/install-native.sh',
627
+ 'scripts/appstore-resize.sh',
628
+ 'scripts/screenshots-capture.sh'
569
629
  );
570
630
  }
631
+ if (details.presetDef.wantsServer) {
632
+ scripts.push('misc/build-local.sh');
633
+ }
571
634
 
572
635
  for (const script of scripts) {
573
636
  const scriptPath = path.join(outputDir, script);
@@ -638,8 +701,10 @@ function showNextSteps(details) {
638
701
  console.log(` ${green('3.')} ./seed.sh`);
639
702
  console.log(` ${green('4.')} ./dev.sh`);
640
703
  } else {
641
- console.log(` ${green('2.')} cd mobile/${details.slug} && npm install`);
642
- console.log(` ${green('3.')} npm run start`);
704
+ // No DB to seed, but install.sh + dev.sh still orchestrate the
705
+ // site + mobile (they guard on directory existence).
706
+ console.log(` ${green('2.')} ./install.sh`);
707
+ console.log(` ${green('3.')} ./dev.sh`);
643
708
  }
644
709
  console.log('');
645
710
  if (presetDef.wantsAdminUi) {
@@ -653,6 +718,7 @@ function showNextSteps(details) {
653
718
  }
654
719
  if (presetDef.wantsMobile) {
655
720
  console.log(dim(` Mobile: cd mobile/${details.slug} && npm run ios | android | web`));
721
+ console.log(dim(` Store kit: mobile/${details.slug}/docs/ (listing, copy, screenshots) + CHANGELOG.md`));
656
722
  }
657
723
  console.log('');
658
724
  console.log(dim(' Happy hacking! - ') + cyan('youtube.com/ligeiro'));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "umpordez",
3
3
  "type": "module",
4
- "version": "1.2.1",
4
+ "version": "1.3.1",
5
5
  "description": "SaaS starter kit generator — server, admin SPA, public site (i18n), mobile app (offline-first, IAP, native push)",
6
6
  "main": "cli.mjs",
7
7
  "scripts": {
@@ -58,7 +58,7 @@ URLs:
58
58
  Folders that the preset didn't ship are simply absent. `dev.sh`
59
59
  and `install.sh` guard on existence per directory, so the same
60
60
  script works for every preset (everything / web-only /
61
- mobile-and-server / just-mobile).
61
+ mobile-and-site / mobile-and-server / just-mobile).
62
62
 
63
63
  ## Multi-tenancy model
64
64
 
@@ -0,0 +1,82 @@
1
+ #!/bin/bash
2
+ #
3
+ # build-local — produce a production build of every web sub-project this
4
+ # repo ships (server, admin SPA, public site) into a sibling `builds/`
5
+ # directory, bump a build number and (optionally) commit + push it.
6
+ #
7
+ # Layout produced (matches CLAUDE.md → Deployment):
8
+ # ../builds/server/ server `npm run build` output + deps
9
+ # ../builds/ui/admin/dist/ Vite build (nginx serves this)
10
+ # ../builds/ui/site/ compiled site + views + public
11
+ #
12
+ # Usage (from the project root):
13
+ # ./misc/build-local.sh # build into ../builds
14
+ # ./misc/build-local.sh --push # also git commit + tag + push builds
15
+ #
16
+ # Each block is guarded on directory existence, so the same script works
17
+ # whatever preset generated this project.
18
+ set -euo pipefail
19
+
20
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
21
+ cd "$ROOT"
22
+
23
+ BUILD_DIR="${BUILD_DIR:-../builds}"
24
+ PUSH=0
25
+ for arg in "$@"; do
26
+ case "$arg" in
27
+ --push) PUSH=1 ;;
28
+ esac
29
+ done
30
+
31
+ mkdir -p "$BUILD_DIR"
32
+
33
+ # ── Build number ────────────────────────────────────────────────────
34
+ NUM_FILE="$BUILD_DIR/build-number"
35
+ number=$(cat "$NUM_FILE" 2>/dev/null || echo 0)
36
+ next=$((number + 1))
37
+
38
+ # ── Server ──────────────────────────────────────────────────────────
39
+ if [ -d server ]; then
40
+ echo "==> Building server…"
41
+ (cd server && npm run build)
42
+ mkdir -p "$BUILD_DIR/server"
43
+ rsync -a --delete \
44
+ --exclude node_modules --exclude .env \
45
+ server/dist/ "$BUILD_DIR/server/dist/" 2>/dev/null || true
46
+ cp server/package.json server/package-lock.json "$BUILD_DIR/server/" 2>/dev/null || true
47
+ fi
48
+
49
+ # ── Admin SPA ───────────────────────────────────────────────────────
50
+ if [ -d ui/admin ]; then
51
+ echo "==> Building admin SPA…"
52
+ (cd ui/admin && npm run build)
53
+ mkdir -p "$BUILD_DIR/ui/admin"
54
+ rsync -a --delete ui/admin/dist/ "$BUILD_DIR/ui/admin/dist/"
55
+ fi
56
+
57
+ # ── Public site ─────────────────────────────────────────────────────
58
+ if [ -d ui/site ]; then
59
+ echo "==> Building public site…"
60
+ (cd ui/site && npm run build)
61
+ mkdir -p "$BUILD_DIR/ui/site"
62
+ rsync -a --delete \
63
+ --exclude node_modules --exclude .env \
64
+ ui/site/build/ "$BUILD_DIR/ui/site/build/" 2>/dev/null || true
65
+ cp ui/site/package.json "$BUILD_DIR/ui/site/" 2>/dev/null || true
66
+ cp -R ui/site/views "$BUILD_DIR/ui/site/" 2>/dev/null || true
67
+ cp -R ui/site/public "$BUILD_DIR/ui/site/" 2>/dev/null || true
68
+ fi
69
+
70
+ echo "$next" > "$NUM_FILE"
71
+ echo ""
72
+ echo "Build #$next written to $BUILD_DIR"
73
+
74
+ # ── Optional: commit + push the builds repo ─────────────────────────
75
+ if [ "$PUSH" = "1" ] && [ -d "$BUILD_DIR/.git" ]; then
76
+ echo "==> Committing + pushing builds…"
77
+ (cd "$BUILD_DIR" \
78
+ && git add . \
79
+ && git commit -m "build: #$next" \
80
+ && git tag -a "$next" -m "#$next" \
81
+ && git push && git push origin --tags)
82
+ fi
@@ -15,13 +15,16 @@ import AnimatedSplash from './src/components/AnimatedSplash';
15
15
  import { toastConfig } from './src/components/ToastConfig';
16
16
  import { initDb } from './src/services/db';
17
17
  import { syncAllContent } from './src/services/content';
18
+ import { SPLASH_ANIMATED } from './src/config';
18
19
 
19
20
  // Hide the native splash screen so our AnimatedSplash takes over the
20
21
  // transition. Best-effort — if the call fails (rare), we just continue.
21
22
  SplashScreen.preventAutoHideAsync().catch(() => { /* best-effort */ });
22
23
 
23
24
  export default function App() {
24
- const [splashDone, setSplashDone] = useState<boolean>(false);
25
+ // Instant show-up (SPLASH_ANIMATED = false) starts already "done" so
26
+ // no overlay ever mounts — the app appears the moment fonts load.
27
+ const [splashDone, setSplashDone] = useState<boolean>(!SPLASH_ANIMATED);
25
28
 
26
29
  useEffect(() => {
27
30
  // Init runs in the background. App rendering doesn't wait on
@@ -79,7 +82,7 @@ export default function App() {
79
82
  </AuthProvider>
80
83
  </ThemeProvider>
81
84
 
82
- {!splashDone && (
85
+ {SPLASH_ANIMATED && !splashDone && (
83
86
  <AnimatedSplash onDone={() => setSplashDone(true)} />
84
87
  )}
85
88
  </SafeAreaProvider>
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to **{{PROJECT_NAME}}** (the mobile app). This is the
4
+ source of truth for what shipped; store "What's New" copy is derived from
5
+ it in [`docs/releases.md`](./docs/releases.md).
6
+
7
+ Format loosely follows [Keep a Changelog](https://keepachangelog.com);
8
+ versions match `app.json` (`version` / `ios.buildNumber` /
9
+ `android.versionCode`).
10
+
11
+ ## [Unreleased]
12
+
13
+ ### Added
14
+ - First public build.
15
+
16
+ ### Changed
17
+ -
18
+
19
+ ### Fixed
20
+ -
21
+
22
+ <!--
23
+ When you cut a release:
24
+ 1. `../../scripts/bump-version.sh patch|minor|major`
25
+ 2. Move the items above under a new `## [x.y.z] - YYYY-MM-DD` heading.
26
+ 3. Reset [Unreleased] to empty Added/Changed/Fixed.
27
+ 4. Translate the highlights into store copy in docs/releases.md.
28
+ -->
@@ -6,7 +6,7 @@ auth, 5-tab navigator, offline SQLite, custom-native modules.
6
6
  ```
7
7
  src/
8
8
  ├── App.tsx, index.ts
9
- ├── config.ts API base URL + provider client IDs
9
+ ├── config.ts API base URL + client IDs + SPLASH_ANIMATED
10
10
  ├── theme/colors.ts Palette generated from primary color
11
11
  ├── i18n/ pt-BR default + en (react-i18next)
12
12
  ├── contexts/
@@ -95,6 +95,15 @@ hasCompletedOnboarding
95
95
  Use `./scripts/prebuild.sh` (refuses --clean). To genuinely
96
96
  reset, move customizations into `plugins/` first.
97
97
 
98
+ ## Splash
99
+
100
+ `config.ts` exports `SPLASH_ANIMATED` (set at scaffold time). When
101
+ `false` (instant show-up, the default) the native splash hides the
102
+ moment fonts load and the app appears immediately — `AnimatedSplash`
103
+ never mounts. When `true`, `App.tsx` overlays `AnimatedSplash`
104
+ (glow + logo + pulse ring, ~2s) until it finishes. The component ships
105
+ either way; flip the flag to switch behavior.
106
+
98
107
  ## Adding a screen
99
108
 
100
109
  1. **Page** — `src/pages/<scope>/<Name>.tsx`. Typed via
@@ -0,0 +1,78 @@
1
+ # Store release notes
2
+
3
+ Copy-paste texts for publishing {{PROJECT_NAME}} updates to the **Apple
4
+ App Store** (App Store Connect → *What's New in This Version*) and **Google
5
+ Play** (Play Console → release → *Release notes*).
6
+
7
+ Character limits to respect:
8
+
9
+ - **Apple — Promotional Text**: max **170 chars** per language. Evergreen
10
+ blurb above the description; updatable without a build.
11
+ - **Apple — "What's New"**: max **4000 chars** per language. Roomy; use a
12
+ structured highlights + fixes block.
13
+ - **Google Play — release notes**: max **500 chars per language**. Paste
14
+ the whole locale-tagged block; Play parses the `<locale>` tags. Keep each
15
+ language tight and benefit-first.
16
+
17
+ Primary market is Brazil, so **pt-BR is the lead copy**; en mirrors it.
18
+ Source of truth for what shipped: [`../CHANGELOG.md`](../CHANGELOG.md).
19
+
20
+ House style: never a hyphen as a separator; use `;` `,` `.` instead.
21
+
22
+ ---
23
+
24
+ ## App Store — Promotional Text (≤170 chars / language)
25
+
26
+ Evergreen marketing blurb shown above the description. Not tied to a build
27
+ — update anytime. Order: pt-BR, en.
28
+
29
+ ```
30
+
31
+ ```
32
+
33
+ ```
34
+
35
+ ```
36
+
37
+ ---
38
+
39
+ ## App Store — "What's New" (≤4000 chars / language)
40
+
41
+ Per release. Lead with the biggest user-facing win. Order: pt-BR, en.
42
+
43
+ ```
44
+ Novidades desta versão:
45
+
46
+
47
+
48
+
49
+ Correções e melhorias de estabilidade.
50
+ ```
51
+
52
+ ```
53
+ What's new in this version:
54
+
55
+
56
+
57
+
58
+ Fixes and stability improvements.
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Google Play — Release notes (≤500 chars / language)
64
+
65
+ Paste the whole block; Play parses the locale tags.
66
+
67
+ ```
68
+ <pt-BR>
69
+
70
+
71
+ Correções e melhorias.
72
+ </pt-BR>
73
+ <en-US>
74
+
75
+
76
+ Fixes and improvements.
77
+ </en-US>
78
+ ```
@@ -0,0 +1,84 @@
1
+ # Store screenshots
2
+
3
+ Produce the full set — **N screens × 2 locales (pt-BR, en) × store sizes**
4
+ (iPhone, Android phone, iPad, Android tablet) — with a brand caption and no
5
+ design tool. Both routes below feed the same framer,
6
+ `scripts/frame-shots.mjs`.
7
+
8
+ > Apple accepts up to 10 screenshots; Google Play caps at 8. Produce 8 and
9
+ > they upload as-is to both.
10
+
11
+ Everything lands under `mobile/{{PROJECT_SLUG}}/screenshots/` (git-ignored):
12
+
13
+ ```
14
+ screenshots/raw/<locale>/NN-*.png phone source screens
15
+ screenshots/raw-tablet/<locale>/NN-*.png tablet source screens
16
+ screenshots/store/<preset>/<locale>/*.png framed, upload-ready
17
+ ```
18
+
19
+ Presets → sizes: `iphone` 1290×2796 · `android` 1080×2340 · `ipad`
20
+ 2048×2732 · `androidtablet` 1600×2560.
21
+
22
+ ---
23
+
24
+ ## Option A — Headless mockups (no simulator)
25
+
26
+ `scripts/gen-mockups.mjs` renders recreations of your screens straight to
27
+ PNGs — no simulator, runs anywhere in ~1s. Ships as a **starting point**:
28
+ edit the `DATA` block (copy shown) and the `SCREENS` list (which screens,
29
+ what layout) to match your app, then:
30
+
31
+ ```bash
32
+ node scripts/gen-mockups.mjs # → screenshots/raw + raw-tablet
33
+ node scripts/frame-shots.mjs # → screenshots/store/**
34
+ ```
35
+
36
+ Both stores allow marketing recreations of the UI. Dev deps:
37
+ `@resvg/resvg-js`, `lucide-static` (add them to the mobile app if missing).
38
+
39
+ ## Option B — Real device captures (Maestro)
40
+
41
+ Pixel-true captures of the live app on a booted simulator/emulator, driven
42
+ by [Maestro](https://maestro.mobile.dev/). Tune the screen labels in
43
+ `scripts/screenshots.flow.yaml` to your nav first (it matches tab labels in
44
+ both languages).
45
+
46
+ ```bash
47
+ ./scripts/run-ios.sh # or: cd mobile/{{PROJECT_SLUG}} && npm run android
48
+ ./scripts/screenshots-capture.sh phone # → screenshots/raw
49
+ # boot an iPad / Android tablet, then:
50
+ ./scripts/screenshots-capture.sh tablet # → screenshots/raw-tablet
51
+ node scripts/frame-shots.mjs # frame both sets
52
+ ```
53
+
54
+ The flow resets the app, gets past onboarding, switches the in-app language
55
+ per locale and captures each screen. Bundle IDs:
56
+
57
+ ```
58
+ iOS {{IOS_BUNDLE_ID}}
59
+ Android {{ANDROID_PACKAGE}} (pass --app-id {{ANDROID_PACKAGE}} on Android)
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Captions & order
65
+
66
+ Numeric filename prefixes (`00-…`, `01-…`) drive both the order **and** the
67
+ caption lookup in `frame-shots.mjs` (the `CAPTIONS` map). Keep them
68
+ numbered. Captions should mirror [`store-copy.md`](./store-copy.md); the
69
+ first and last carry the strongest download pitch (most visitors only see
70
+ the first 2–3 frames).
71
+
72
+ Preview the framing without any raw captures:
73
+
74
+ ```bash
75
+ node scripts/frame-shots.mjs --selftest
76
+ node scripts/frame-shots.mjs --preset=iphone --locale=en
77
+ ```
78
+
79
+ ## Store requirements
80
+
81
+ - **Apple** requires a 6.9"/6.5" iPhone set; add the iPad set when
82
+ `supportsTablet: true` in `app.json`.
83
+ - **Google Play** takes up to 8 phone + tablet screenshots, long edge
84
+ 320–3840px.