# mobile-previewer — package internals Consumer docs are in [README.md](./README.md). This file is for sessions working on the package itself. ## Working rules (read first) - **`npm run release` (and the `:minor` / `:major` variants) is pre-authorized.** Run it freely whenever a coherent change is ready to ship. `release` is `npm version patch`; the `postversion` hook then runs `git push --follow-tags`, which triggers GitHub Actions → npm publish in one shot. - **Other git ops (`git commit`, `git push` on non-release commits) still need explicit per-message authorization.** "Commit and push this" in chat counts; "you can keep editing" doesn't. - Editing files, running builds, running tests, running the dev server — all fine without asking. - The author can be assumed to know git/npm fundamentals. Skip basics; flag the non-obvious. ## Architecture - **Single component, no provider.** `` owns device state. No context, no store, no router. - **Inline render, no iframe.** Prototypes mount as React children inside `.mp-phone-screen`. They share the host React tree. Consequence: prototypes must use `100%`-relative sizing, not `100vw`/`100vh`. - **CSS-driven device morph.** Device dimensions are written as CSS custom properties on `.mp-phone-shell` (`--mp-screen-w`, `--mp-shell-px`, etc). The CSS transitions those properties on width / height / padding / border-radius. Switching devices is just a state update; no JS animation library. ## Conventions (read before editing) - **All component CSS lives under `.mp-root` or `.mp-responsive`.** No bare selectors, no resets. This is a library — its styles must not bleed into the consumer's prototype. - **Chrome uses `rem`, device dimensions use `px`.** Topbar / dropdown / fonts → `rem` (consumer can adjust by changing root font-size). Phone screen `width`/`height`/`border-radius` and shell padding/radius → `px` (those are physical phone specs and shouldn't scale). - **Font loading is a module side effect.** `src/loadFonts.ts` injects preconnect + Google Fonts `` tags into `` at lib-eval time (not in useEffect — that would be after first paint). Imported from `Previewer.tsx` so the side effect fires whenever the component is used. - **`localStorage` access guards.** All `window.localStorage` calls go through `typeof window !== 'undefined'` + try/catch — Safari private mode and disabled-storage envs throw on access. - **Device registry is just data.** `src/devices.ts` is the source of truth. To add a device, add an entry there; nothing else changes. ## Build `npm run build` does two things in sequence: 1. `tsc -p tsconfig.build.json` — type-checks the public surface (excludes `src/demo/`). 2. `vite build` — library mode (`vite.config.ts` checks `command === 'build'`), outputs `dist/mobile-previewer.{js,css}` + a rolled-up `index.d.ts`. React is externalized (peerDep). `npm run dev` runs Vite in regular HTML-entry mode against `index.html` → `src/demo/main.tsx`. The demo imports `Previewer` directly from the source, so changes hot-reload. ## Release pipeline (tag-triggered) `.github/workflows/release.yml` publishes to npm on `v*.*.*` tag push. Flow: - User locally: `npm version patch` → commits package.json + creates tag. - User locally: `git push --follow-tags` → tag arrives on GitHub. - CI: typecheck, build, **verify the tag matches `package.json` version** (refuses to publish otherwise), `npm publish --provenance`, create a GitHub Release. Required GitHub secrets / settings (one-time): `NPM_TOKEN` (npm Granular Access Token, 2FA bypass, write scope on `@fromluke/mobile-previewer`), Actions → workflow permissions = "Read and write". Provenance + signature: every published version is cryptographically linked to the exact public commit it was built from (SLSA v1). Visible on the npm package page and in `npm view @fromluke/mobile-previewer dist.attestations`. ## When CSS conflicts arise If a future change adds a global selector or unscoped utility, the prototype's styles can break. Always check the lib's compiled CSS doesn't contain anything matching `* { ... }`, `body { ... }`, etc. The only intentional global is the `@keyframes mp-fade-in` declaration — keyframe names are global by spec but namespaced with the `mp-` prefix. ## Demo content vs library content `src/demo/` is the local playground. It is **excluded from the library build** (see `tsconfig.build.json` and `vite-plugin-dts` exclude). Anything in `src/demo/` can use inline px, hardcoded fonts, whatever — it never ships.