Use this when building a mobile-first study app where the card is the product.
react-swipeabledist/sw.js and manifest.jsonseenGestureHint flag in localStorage so the toast is not permanent.For mobile React/Vite onboarding, account setup, OAuth/device-code, or first-run flows, keep the mobile chrome and conversion steps deliberately simple:
- Ensure there is exactly one visible primary menu trigger on mobile. If the brand title is needed, use a separate inert centered label rather than squeezing it into the same action row.
- Hide secondary topbar actions at small widths when they crowd the brand or signup CTA; keep those actions in the drawer/sidebar instead.
- Treat device codes as hero content: large centered monospace text, high-contrast block, tappable-to-copy, plus a smaller explicit copy button with temporary feedback.
- Make the external authorization URL the primary CTA, and keep the final “complete setup” button disabled until the auth CTA has been opened/clicked; use precondition text like “Open authorization first.”
- For iOS/mobile form inputs and autocomplete lists, set color, -webkit-text-fill-color, caret-color, suggestion background, suggestion text, and focus/hover colors explicitly.
Reference: references/mobile-onboarding-polish.md captures the Astral Hermes session-derived pattern for mobile header cleanup, device-code CTA sequencing, and birth-location autocomplete contrast.
For mobile React/Vite apps with review/detail modals (draft review, creative preview, record detail, etc.), treat the modal as a bottom sheet rather than a scaled-down desktop dialog:
- At max-width: 760px, align the modal overlay to the bottom, remove side padding, and give the sheet full width with rounded top corners (border-radius: 28px 28px 0 0) and max-height: ~94dvh.
- Add a small non-interactive grab handle via :before; position it absolute so it does not break the modal's grid rows.
- Make the modal header sticky, compact, and translucent/blurred. Clamp long titles/subtitles/status pills so they do not crowd the close button.
- Collapse desktop two-column preview/copy layouts to one column. Let the main media/card preview become full width, with min-height around 180–210px and max-height around 42dvhso it feels substantial without hiding the actions.
- Put destructive/approval/push actions in a sticky bottom action bar with safe-area padding, blur, and a subtle top border/shadow. Emphasize the primary action by spanning it across columns or ordering it first.
- Make schedule/date inputs and action buttons at least ~42–44px tall for thumb taps.
- Add a narrower breakpoint (max-width: 390px`) to collapse two-column action/copy grids further on small phones.
current/total)Reshuffle pill instead of a full control traySupport multiple orderings with a single createStudyDeck() function.
Recommended modes:
- canonical / King Wen
- user-provided custom sequence (e.g. Human Design)
- binary / structural
- random
For a custom sequence supplied as an explicit 64-number array:
1. Store the array as a constant.
2. Build const CUSTOM_INDEX: Map<number, number> = new Map(order.map((n, i) => [n, i] as const))
3. Sort with the map lookup instead of hardcoding many ifs.
4. Add a test that verifies:
- first several values match exactly
- last value matches exactly
- all 64 entries are unique
This avoids mistakes when users hand you a specific sacred/study order they care about.
This was the important deploy requirement.
sw.jsregistration.update() on loadregistration.update() every ~60scontrollerchange and reload once when a new SW takes control, but only if the page already had a controller before registration. Capture const hadController = Boolean(navigator.serviceWorker.controller) before register(), then in controllerchange do nothing on the first install. This avoids iOS standalone PWA first-launch reload loops/white screens.Update ready pill/button when updatefound firespublic/sw.jsself.skipWaiting() in installself.clients.claim() in activate/api/ responses as static app shellServe these with no-cache headers BEFORE express.static(distDir):
- /sw.js
- /manifest.json
Example headers:
- Cache-Control: no-cache, no-store, must-revalidate
- Service-Worker-Allowed: / for sw.js
When the user wants an LLM/API token in the app but not exposed to the browser:
1. Load env on the server with import 'dotenv/config'
2. Keep the token in .env only
3. Never surface the raw key to the frontend
4. If useful, expose only a boolean status endpoint like /api/config-status -> { hasOpenRouter: true }
If another app on the same VPS already uses OpenRouter:
- Check that app’s .env or PM2 ecosystem config
- Copy the key into the new app’s .env
- Keep .env gitignored
- Add .env.example with placeholder values only
For datasets that need generated study text (e.g. 64 cards):
- Write a standalone script under scripts/
- Use OpenRouter chat completions with a strict JSON prompt
- Request fields like:
- overview
- judgment
- image
- mnemonic
- Save incrementally after each item so long runs can recover
- Strip fenced code blocks before JSON.parse() because models may still return ```json wrappers
- Merge generated JSON into the main dataset at import time rather than hardcoding everything in one file
src/data/hexagrams.tssrc/data/...generated.jsontsc -b || true && vite builddist/assets/index-*.js modify timestamp changednpm testnpm run buildcurl http://127.0.0.1:PORT/api/healthcurl -I http://127.0.0.1:PORT/sw.js and confirm no-cache headersUse --update-env to update environment variables, restarting alone won’t refresh changed env vars.