Skip to main content

App Store / Play Store screenshot generator

A dev-only tool that renders the tracking app's marketing screenshots — framed, branded, pixel-exact store tiles — straight from the real app UI, so the store listings never drift from what ships. It lives at the route /debug/screenshots and is excluded from the production / native build (see Build exclusion).

Each tile is a marketing composition: the enode PRO lockup, a category label + a headline, the enode dot-cloud texture, and one or more real device frames with the actual app showing through. Pick a screen, pick a theme, and download each tile as a pixel-exact JPEG at the exact store resolution.

Using it

Open /debug/screenshots in the running dev app (it needs the debug area enabled — it is on by default in npm run dev). Then:

  • Pick a screen from the tab row (Bar path tracking, Velocity-based training, Performance insights, Video review, Multi-station, One platform).
  • Pick a theme with the Dark / Light toggle — the whole tile (background, chrome, and the app itself) renders in that theme.
  • Download JPEG per tile, or Download all devices for the current screen. Files are named <screen>-<device>-<theme>-<W>x<H>.jpg.

The on-screen previews are shrunk; the capture always shoots the tile at its full export resolution, so the downloaded file is pixel-exact for the store.

The screens

Every screen renders the real app components driven by a fixture session (no mock-up markup where a real component exists — see Real components).

ScreenCategoryApp view
techniqueBar path trackingInsights → Technique (Sagittal/Frontal bar path)
set-trackingVelocity-based trainingActive tracking (set list + coaching cards)
insightsPerformance insightsInsights → Overview (stat cards, positive trends)
video-reviewVideo reviewFull-screen set review (video + timeline + charts)
stationsMulti-stationThe station shell (tablet-only)
ecosystemOne platformComposite hero — three devices at once

stations is tablet-only (padOnly) — the tile grid hides the phone targets for it. ecosystem is a hero (hero) — a single composite tile that draws its own device frames (see Hero composition).

Device targets

Store resolutions and the frame each target uses live in devices.ts:

TargetExport pxFrame
iPhone 6.9"1320 × 2868iPhone
iPad 13" (landscape)2732 × 2048iPad
Android phone1080 × 1920iPhone (reused)
Android tablet (landscape)1920 × 1200iPad (reused)
Mac (Studio Display)2880 × 1800Mac
Ecosystem (landscape)2880 × 1920— (hero draws its own)
Ecosystem (iPhone)1320 × 2868— (hero draws its own)

Android reuses the Apple frames (there is no Android frame). Each target's screenW × screenH (the app's logical render size) is sized to match its frame cutout's aspect so the app fills the cutout without stretch.

Architecture

The pieces, under apps/tracking/src/app/debug/screenshots/:

  • page.screenshot.tsx — the route. Owns the screen tabs, the Dark/Light toggle, the tile grid, and the per-tile + "download all" export flows. The .screenshot.tsx extension is what gates the whole subtree out of prod (see Build exclusion).
  • devices.ts — the export targets (resolution, logical size, frame) + the frameless hero targets.
  • screens.tsx — the SCREENS registry and every screen's render. Each screen composes real app components for the target's device class.
  • marketing-tile.tsx — the tile shell: dot-cloud background, branding + headline, theme, and the device region (fits a single frame to the space, or hands off to a hero).
  • device-mock.tsx — one framed device: the app screen behind a real device-frame PNG, aligned to the frame's transparent cutout. Reads the theme from context.
  • frames.ts — the device-frame descriptors (image + cutout rect + aspect).
  • fixture-session.ts — the deterministic clean-pull WorkoutSession that drives the real screens.
  • app-chrome.tsx — the small header/nav chrome the app doesn't export as standalone components, reproduced (top bar, "Insights" header, bottom-nav pills).
  • sample-data.ts — the clean bar-path curve, the set-timeline reps, and the set-list rows.
  • export.ts — the pixel-exact JPEG capture (dynamic-imports modern-screenshot).
  • assets/ — the dot-cloud SVG and the video-mock placeholder. frames/ — the three device-frame PNGs.

Real components, fixture data

The screens mount the actual app route componentsTechniqueTab, PerformanceTab, SessionStats, the coaching cards, the station shell, etc. — not look-alikes. They are driven by one fixture WorkoutSession from fixture-session.ts, so the charts are exactly what the app renders. No app code was changed to support this.

Only where the app has no reusable component (route-level headers baked into InsightsSheet / ExerciseDetailView, the workouts list's store-coupled exercise names, the SetReviewView layout that relies on viewport media queries) is the chrome reproduced in app-chrome.tsx / screens.tsx.

Three fixture facts worth knowing (all in fixture-session.ts):

  • The load metric is selected by dimension === "mass" — the other loading metric is flywheel inertia (g·m²), which would render "60000 g·m²".
  • Power / force are stored per-kgpowerPeak / forcePeak are loadingFactor metrics, so measurementDisplayValue multiplies the stored value by the set load. Storing absolute W/N shows load× too high.
  • The bar path is derived from raw strength samples ({ t, s, a, v, q }) by the real chart service — the fixture authors the samples, not a 2-D path.

To use a real captured session instead of the synthetic one, replace cleanSessionFixture()'s return with a real WorkoutSession; nothing else changes.

Device frames

The frames are Apple design-mockup PNGs in frames/: a transparent screen cutout + transparent exterior, with the opaque bezel/notch/stand between. device-mock.tsx places the app screen behind the frame, aligned to the cutout, and paints the frame <img> on top; the frame's rounded bezel masks the app's square corners.

Each frame's cutout rectangle (in frames.ts) was measured from the PNG's alpha channel — flood-fill the transparent screen region, take its bounding box, express it as fractions of the image.

Dot-cloud background

The faint enode dot-cloud texture behind every tile is assets/dot-cloud.svg — the brand dot field, recolored to white dots on a transparent background. marketing-tile.tsx lays it full-bleed behind the content at low opacity. In light mode it is filter: invert(1) (dark dots on white).

Themes

The Dark / Light toggle swaps the whole tile. MarketingTile provides the theme through ScreenshotThemeContext; every DeviceMock (including the hero's nested ones) reads it and pins .theme-dark / .theme-light on the app subtree, so the app renders in that exact theme regardless of the viewer's prefers-color-scheme — exports are deterministic. The tile background, branding text, and dot-cloud recolor with the theme; the export background is forced white in light mode.

Hero composition

The ecosystem screen is a frameless hero (hero: true): the tile skips the single-device wrapper and the screen (HeroEcosystem) draws its own device frames — an iMac (Studio Display frame) showing Insights, an iPhone showing velocity-based training, and an iPad showing technique. It renders two tiles (landscape + iPhone portrait); the portrait branch is a layered scene that bleeds off the tile edges. To bleed past the tile's content padding, the hero drops the device region's overflow-hidden (the tile still clips) and the branding sits above the composition (z-20).

The "iMac" is currently the Studio Display frame (a monitor on a stand); there is no dedicated iMac frame. Drop one into frames/ and wire it in frames.ts + devices.ts to swap it.

Build exclusion

Nothing here ships in the app. The route file is page.screenshot.tsx, and next.config.ts only adds the screenshot.tsx page extension when the debug area is enabled. In a normal prod / native next build the flag is unset, so page.screenshot.tsx is not a page — nothing imports it or its subtree, so the whole thing (including modern-screenshot, the frame PNGs, the dot-cloud SVG, and the video mock) is tree-shaken out of out/ and the Capacitor apps.

This is why every asset lives under the screenshots subtree (imported as an ES module, never in public/). public/ is copied verbatim into out/; an imported asset is only emitted if reachable code imports it.

To verify after adding assets:

npm run build tracking
# 0 matches each = excluded:
find apps/tracking/out -iname '*frame-*' -o -iname '*dot-cloud*' -o -iname '*video-mock*'

Extending

  • Add a screen: add an entry to SCREENS in screens.tsx with a render(target) that composes the app for target.deviceClass. Set padOnly to hide phone tiles, or hero for a frameless composite tile.
  • Replace the video still: overwrite assets/video-mock.png (any aspect — it's object-cover cropped). If the filename/type changes, update the one import videoMock from … line in screens.tsx.
  • Add / replace a device frame: drop the PNG into frames/, measure its cutout (flood-fill the alpha), add a descriptor in frames.ts, and reference it from a target in devices.ts (size screenW/H to the cutout aspect).
  • Change a store resolution: edit the target's exportW/exportH in devices.ts — the tile fits itself, nothing else changes.