npm.io
0.1.3 • Published 1 week ago

@fromluke/mobile-previewer

Licence
MIT
Version
0.1.3
Deps
0
Size
94 kB
Vulns
0
Weekly
0

@fromluke/mobile-previewer

Mobile device shell for previewing React prototypes to clients. Drop your prototype inside <Previewer>, push to GitHub, share the Vercel link.

import { Previewer } from '@fromluke/mobile-previewer'
import '@fromluke/mobile-previewer/styles.css'
import Prototype from './prototype'

export default function App() {
  return (
    <Previewer client="Clean Simple Eats" title="PDP Optimizations • Rd 1">
      <Prototype />
    </Previewer>
  )
}
  • ≥ 789px: black canvas, top bar (client + title left, device dropdown right), centered phone shell. Smooth CSS-transition morph between iPhone 15 Pro / 15 Pro Max / SE. Selected device persists in localStorage.
  • < 789px (including mobile landscape): shell hides, your prototype renders full-bleed responsive.

API

<Previewer
  client="Client Name"             // small text, top-left
  title="Project Name"              // big text, top-left
  defaultDeviceId="iphone-15-pro"   // optional
  devices={DEVICES}                 // optional — override the device list
  shellBreakpoint={789}             // optional — viewport width below which the shell hides
  storageKey="mp:device"            // optional — localStorage key; pass null to disable persistence
>
  {/* your prototype */}
</Previewer>

Built-in devices: iphone-15-pro, iphone-15-pro-max, iphone-se. Add more by passing your own Device[]:

import { Previewer, DEVICES, type Device } from '@fromluke/mobile-previewer'

const PIXEL: Device = {
  id: 'pixel-8',
  label: 'Pixel 8',
  screen: { width: 412, height: 915, radius: 36 },
  shell: { paddingX: 12, paddingY: 12, radius: 48 },
}

<Previewer devices={[...DEVICES, PIXEL]} ... />

Authoring prototypes — constraints

The prototype renders inline inside a fixed-size container (no iframe). So:

  • Use 100%, not 100vw/100vh for sizing. Container is 393×852 for 15 Pro, etc.
  • Scroll the inside, not the page. Set overflow: auto on a wrapper inside your prototype.
  • Anything fixed (position: fixed) anchors to the viewport, not the phone screen. Use position: sticky or position: absolute parented to your prototype root.

Develop the shell

npm install
npm run dev         # demo playground at http://localhost:5173
npm run build       # library bundle → dist/
npm run typecheck

The demo lives in src/demo/. The library entry is src/index.ts.

See CLAUDE.md for package-internal architecture notes.


Publishing flow

Once set up, the only command you ever run to ship a change is npm version patch && git push --follow-tags. GitHub Actions does the rest.

How the pieces fit together

local edits
   │
   │  npm version patch          ← bumps package.json + creates git tag v0.1.1
   │  git push --follow-tags     ← pushes commits AND the tag
   ▼
GitHub
   │
   │  .github/workflows/release.yml  fires on `v*.*.*` tags
   │    1. checkout + npm ci
   │    2. typecheck + build
   │    3. verify tag matches package.json version
   │    4. npm publish --provenance
   │    5. create GitHub Release with auto-generated notes
   ▼
npm registry + GitHub Releases  (both updated from a single push)

Source of truth split:

  • GitHub = the source code. Commits, history, issues, releases.
  • npm = the published artifact (just dist/). Consumers install from here.

You don't push to both. You push to GitHub; npm publishes from CI.

One-time setup

Do these once, in order.

1. Create the npm account + Granular Access Token
  1. Sign up / log in at https://www.npmjs.com.
  2. (Publishing under @fromluke?) Make sure you own the fromluke username, or create an npm org with that name at https://www.npmjs.com/org/create. To use a different scope, search/replace @fromluke across this repo + any consumers.
  3. Go to https://www.npmjs.com/settings/[your-username]/tokens/new and fill out the Granular Access Token form:
    • Token name: github-actions-mobile-previewer
    • Bypass two-factor authentication (2FA): check it (CI can't do interactive 2FA)
    • Allowed IP ranges: leave blank (GitHub Actions IPs are dynamic)
    • Packages and scopes → Permissions: Read and write
      • Then below, pick "Only select packages and scopes" → add @fromluke/mobile-previewer (scope narrowly — a leaked token only affects this one package)
    • Organizations → Permissions: No access
    • Expiration: 365 days (set a calendar reminder to rotate)
    • Click Generate token, copy it — it's only shown once.
2. Create the GitHub repo
  1. https://github.com/new
  2. Owner fromluke, name mobile-previewer, visibility Public (npm provenance requires a public repo; private works if you remove "provenance": true from package.json).
  3. Don't initialize with README / license / gitignore — you have them locally.
  4. Create repository.
3. Push your local code
cd ~/repos/mobile-previewer
git add .
git status                     # sanity check
git commit -m "Initial mobile-previewer scaffold"
git remote add origin https://github.com/fromluke/mobile-previewer.git
git branch -M main
git push -u origin main
4. Add the token + workflow permissions on GitHub

In the new repo:

  • Settings → Secrets and variables → Actions → New repository secret
    • Name: NPM_TOKEN
    • Value: the token from step 1
  • Settings → Actions → General → Workflow permissions → "Read and write permissions" → Save (lets the release workflow create GitHub Releases)
5. Verify CI ran green

Actions tab on the repo → CI workflow on your initial push should be green. If red, fix before tagging.

6. (Optional) first manual publish — only if 0.1.0 isn't on npm yet
npm login
npm publish --access public

From here on, every publish goes through CI — never manual.

Day-to-day: shipping an update

# edit code...
git commit -am "Tighten dropdown caret"

# One command — bumps version, builds, commits, tags, pushes everything.
# The `postversion` script pushes the tag, which fires release.yml.
npm run release            # 0.1.0 → 0.1.1
# or: npm run release:minor / npm run release:major

Under the hood npm run release runs:

  1. npm version patch — bumps package.json, runs prepublishOnly (build), creates a commit + tag.
  2. postversiongit push --follow-tags, pushes the commit + tag.
  3. GitHub Actions sees the v*.*.* tag → publishes to npm with provenance.

Watch the workflow run at https://github.com/fromluke/mobile-previewer/actions. When it goes green, the new version is live on npm.

In each prototype:

npm install @fromluke/mobile-previewer@latest
git commit -am "Bump mobile-previewer"
git push                   # Vercel redeploys automatically

Or pin to a range like "^0.1.0" so npm update keeps prototypes current without per-version commits.

CI on every push

.github/workflows/ci.yml runs on every push and PR:

  • npm ci
  • npm run typecheck
  • npm run build
  • npm pack --dry-run (validates the publish manifest before tag time)
  • npm audit on production deps, fails on high/critical

Catches regressions before they can be tagged.


Spinning up a prototype

For each client + project (e.g. clean-simple-eats/homepage):

cd ~/repos/clean-simple-eats
npm create vite@latest homepage -- --template react-ts
cd homepage
npm install
npm install @fromluke/mobile-previewer

Replace src/main.tsx:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Previewer } from '@fromluke/mobile-previewer'
import '@fromluke/mobile-previewer/styles.css'
import { Homepage } from './prototype/Homepage'
import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <Previewer client="Clean Simple Eats" title="Homepage • Rd 1">
      <Homepage />
    </Previewer>
  </StrictMode>,
)

Delete the default Vite styles in src/index.css (the previewer brings its own — keep only html, body, #root { margin: 0; padding: 0; } and a body { background: #000 }). Then:

git init
git add .
git commit -m "Initial prototype"
# Create the repo at https://github.com/new (e.g. "cse-homepage")
git remote add origin https://github.com/fromluke/cse-homepage.git
git branch -M main
git push -u origin main

Then on Vercel:

  1. https://vercel.com/new → import the GitHub repo.
  2. Framework preset: Vite. Defaults are fine.
  3. Deploy → you get a URL like cse-homepage.vercel.app to share with the client.

Every git push to main redeploys.


Security posture

  • No runtime network access from the lib code. The only outbound request is the Google Fonts stylesheet for Inter + Roboto Mono (preconnect + <link> injection in src/loadFonts.ts). URLs are hardcoded constants.
  • No dangerouslySetInnerHTML, no eval, no Function() constructor. All rendered text passes through React's standard escaping.
  • localStorage access is guarded (typeof window check + try/catch). Safe in Safari private mode, disabled-storage environments, and SSR.
  • CSS is scoped under .mp-root / .mp-responsive — can't bleed into the consumer's prototype DOM.
  • files: ["dist"] restricts npm tarball to compiled output only. Source files, configs, .claude/, .github/ never ship to consumers.
  • npm provenance is enabled via publishConfig.provenance: true + the release workflow's OIDC token, so each published version is cryptographically linked to the exact public commit it was built from. Visible on the package page on npmjs.com.
  • Automation token in CI bypasses 2FA but is tied to a single npm account; rotate it from npmjs.com → tokens if compromised.
  • npm audit runs in CI on production deps, fails the build at --audit-level=high.