npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

codetocanvas

v0.1.0

Published

See your React prototype whole: every screen, feature and iteration on one zoomable board, live and joined by user-flow connectors.

Downloads

288

Readme

codetocanvas

See your React prototype whole. Every screen, every feature, every iteration on one zoomable board, laid out as a user flow and joined by arrows labelled with what the user clicks. Not screenshots and not an export: your real components, running live, side by side.

<FlowCanvas />

That one component adds a Prototype / Canvas toggle — build in one, review the whole thing in the other. With no configuration it records screens at runtime as you click through. Flow files turn the board into a curated design source of truth: a section per feature, a lane per iteration (v1, v2…), in any of your prototype's interface languages.

Scope

The job is laying your prototype out as a flow. Two things it deliberately doesn't do yet:

  • In-progress screen states aren't supported. There's no way to mark a screen as a stub, half-built or work in progress — every screen on the board reads as equally finished. A flow describes what exists, not how far along it is.
  • Nothing is generated at build time. The board comes from runtime capture, or from flow files — written for you by /map-flow, or by hand. No routes are parsed and no map is produced by a build.

Transient states — loading, errors, mid-process — are shown by rendering your component in that state, with props you supply. That's ordinary code in your own flow file; the plugin has no fixture system of its own. How much of your prototype the board can reach depends on what your components let you set from outside: see Letting the canvas open a state.


Install

1. Add the package

npm install codetocanvas
# or
pnpm add codetocanvas
yarn add codetocanvas
bun add codetocanvas

It needs React 18 or newer (react and react-dom). There's no stylesheet to import; the styles come bundled.

2. Drop it next to your app

Render it once, anywhere, usually beside your app's root. It draws its own floating toggle and full-screen canvas, so it doesn't matter where it sits in your tree.

Vite

// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { FlowCanvas } from 'codetocanvas'
import App from './App'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
    {import.meta.env.DEV && <FlowCanvas />}
  </StrictMode>,
)

Next.js (App Router)

// app/layout.tsx
import { FlowCanvas } from 'codetocanvas' // already marked 'use client'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        {process.env.NODE_ENV === 'development' && <FlowCanvas />}
      </body>
    </html>
  )
}

Create React App / Webpack

{process.env.NODE_ENV === 'development' && <FlowCanvas />}

The DEV / development check keeps it out of production builds. Leave it off if you want the toggle on a shared demo link too.

Backing out. That check is also the answer to "how do I remove this?" — a production build drops the component and everything it renders, so nothing reaches your users either way. To detach properly, remove the <FlowCanvas /> line and its import, then the flows import, since flow files import defineFlow and would keep the dependency alive. What's left in your own code is plain React with nothing to unpick: the initial* props that let a screen open at a given state, and any data-flow-screen attributes. Both are worth keeping regardless.

3. Fill the board

Two ways. Take the first one — it reads your code, so it finds screens you'd have to hunt for by hand, including the states you can't just click into (loading, errors, mid-process).

Map it from your code (recommended)

npx codetocanvas install-skill   # copies /map-flow into .claude/skills/

Then run /map-flow in Claude Code. It reads your prototype, proposes the features and iterations it found, and on your say-so writes flows/<feature>/<iteration>.flow.tsx plus flows/index.ts. Pass them to the canvas and that's your board:

<FlowCanvas flows={flows} />

Installing the package doesn't install the skill — a dependency shouldn't write into your .claude/ folder uninvited — so it's this one extra command. See Keeping flows in sync for updating flows as the code changes.

Or click through it yourself

With no flows prop, the canvas records as you go. Useful for a first look, or when you'd rather not run a skill:

  1. Click through your prototype. Each screen is captured as you reach it.
  2. Click Canvas in the bottom-left corner.
  3. Every screen is laid out left to right in the order you reached them. Branches stack below, and the arrows are labelled with the button or link you clicked ("Continue", "Start call"…).

Captures live in localStorage and Clear starts them over. They're a sketch, not a source of truth: they only cover what you clicked, and they don't survive a refactor. Flow files are the ones you keep.

Either way: drag the background to pan, ⌘/Ctrl-scroll or pinch to zoom towards the pointer, −/+/Fit in the corner. Adding #canvas to the URL opens the canvas directly. Your app stays mounted underneath, so switching back loses no state.


Naming screens (optional)

By default, each URL counts as one screen. That's right for multi-page apps and router-based SPAs.

If your screens aren't separate URLs, like steps in a modal, tabs or a wizard, name them with a data-flow-screen attribute on each screen's outer element:

<div className="modal" data-flow-screen={`Step ${step}`}>…</div>
<main data-flow-screen="Agents page">…</main>
  • Each distinct name becomes its own screen on the canvas. You can include state in the name, e.g. "Checkout · error".
  • When several named screens are visible, like a modal over a page, both are captured. The one later in the page counts as the screen in front, for the arrows.

Flows: features and iterations

A flow is one iteration of a feature: a set of screens joined by arrows. Keep one file per iteration, and list them all in a plain flows/index.ts. It's an ordinary array, so it works with any bundler.

// flows/onboarding/v2.flow.tsx
import { defineFlow } from 'codetocanvas'

export default defineFlow({
  feature: 'Onboarding',
  iteration: 'v2 · Genie chat',
  notes: 'Type a sentence on Home, then Genie sets the agent up in a chat',
  source: 'src/agent/SetupChat.tsx',
  languages: ['en', 'hi'], // interface locales, optional
  screens: [
    { id: 'home', label: 'Home', render: () => <Home /> },
    { id: 'type', label: 'Confirm the type', width: 1280, height: 780, render: () => <AgentPage step="type" /> },
    { id: 'type-error', label: 'Nothing typed', row: 1, render: () => <AgentPage step="type" error /> },
    { id: 'voice', label: 'Pick a voice', render: () => <AgentPage step="voice" /> },
  ],
  edges: [
    { from: 'home', to: 'type', label: 'Enter' },
    { from: 'type', to: 'type-error', dashed: true },
    { from: 'type', to: 'voice', label: 'Looks right' },
  ],
})
// flows/index.ts
import onboardingV1 from './onboarding/v1.flow'
import onboardingV2 from './onboarding/v2.flow'
import settings from './settings/current.flow'

export const flows = [onboardingV1, onboardingV2, settings]
<FlowCanvas flows={flows} />
  • Sections and lanes. Flows with the same feature share a section titled with it, one lane per iteration. The lane header shows the iteration's name, notes, source and languages.
  • Layout. Each lane runs left to right in the order you list its screens. row: 1 (or 2…) stacks a screen under the one before it, for a branch or an error state. col pins a screen to a column.
  • Screen ids line up across iterations. Give the same step the same id in every iteration (welcome, pick-type…) and it sits in the same column in each lane, so v1 and v2 compare side by side.
  • Three kinds of screen:
    • render: (ctx) => <Screen />: live React. The default size is 520 × 600.
    • image: '/shots/welcome.png': a screenshot or reference, e.g. the shipped version. It shows at full height by default.
    • url: 'https://my-prototype.app/welcome': an iframe, e.g. a deployed prototype.
  • height: 'auto' lets a screen grow with its content instead of clipping at a fixed height.
  • sources lists the files and folders a flow was written from, and fingerprint is their hash (npx codetocanvas fingerprint). When they change, npx codetocanvas check --mark sets stale: true and the lane shows "May be out of date". See Keeping flows in sync.
  • Toolbar. The board's title and screen count sit top left. The controls sit top right, and stack on narrow screens: a feature filter (when there's more than one), the language menu, a Motion / Paused switch, and zoom. The board remembers these per browser.

Components you render in flows often need app state (the current user, a draft…). <FlowCanvas /> renders through a portal, so React context still reaches the screens: wrap it in your providers and read them from the screen components, which keeps the flow files static.


Letting the canvas open a state

The board can only show states your app lets something else set. A screen is a component rendered with props — so if a state exists only as useState inside a component, with no way in, the canvas can render that component's default and nothing more. This is the one thing that decides how much of your prototype the board can reach, so it's worth designing for.

The fix is plain React: optional props for the starting state, each defaulting to what happens today.

export default function App({ initialTasks = SEED, initialFilter = 'all', initialUser = null, initialOpenId = null }: Props = {}) {
  const [tasks, setTasks] = useState(initialTasks)
  const [filter, setFilter] = useState(initialFilter)
  // …
}

<App /> behaves exactly as before, and each screen opens a state directly:

{ id: 'tasks-done',  label: 'Filtered to done', render: () => <App initialFilter="done" /> },
{ id: 'tasks-empty', label: 'No tasks',         render: () => <App initialTasks={[]} /> },
{ id: 'task-detail', label: 'Task detail',      render: () => <App initialOpenId="t1" /> },

No plugin API and nothing to couple to — it's a component that accepts its own starting state, which is worth doing for tests and Storybook anyway. Three other routes to the same place:

  • Render the inner component instead of the root. A modal or panel that already takes props (<TaskModal task={…} />) needs nothing new.
  • State that's persisted — a useStoredState-style hook — can read a snapshot instead: see useCanvasState().
  • A state that can't be reached at all (mid-call, a failed request) needs a fixture: a frozen object satisfying the same type, with no-op functions.

Rendering a screen from saved state

When a prototype keeps its state in a storage hook (so a reload resumes where you left off), a screen can carry that state instead of props. Give the screen a state object and have your hook read it through useCanvasState():

import { useCanvasState } from 'codetocanvas'

export function useStoredState<T>(key: string, initial: T) {
  // Inside a frame this is the screen's `state`; outside the canvas it's null.
  const snapshot = useCanvasState()

  const [value, setValue] = useState<T>(() => {
    if (snapshot) return key in snapshot ? (snapshot[key] as T) : initial
    return readFromStorage(key, initial)
  })

  useEffect(() => {
    if (snapshot) return // a frame must never write to the app's own storage
    writeToStorage(key, value)
  }, [snapshot, key, value])

  return [value, setValue] as const
}

Then every screen is the same component at a different point in the flow:

{ id: 'step-2', label: 'Pick a voice', state: { 'app:step': 2, 'app:modal': true }, render: () => <App /> },

Two things to watch: render the root and it will render <FlowCanvas /> too, so guard that with useFlowCanvas().inCanvas; and a flow like this depends on your state keys, so it goes stale when those change rather than when a component's props do.

Keeping side effects out of the canvas

Every screen on the board is mounted at once, so a screen that plays audio, calls an API or pre-fetches on mount would do it many times over. useFlowCanvas() tells a component it's inside a canvas frame:

import { useFlowCanvas } from 'codetocanvas'

function VoicePreview({ text }) {
  const { inCanvas, locale, paused } = useFlowCanvas()
  useEffect(() => {
    if (inCanvas) return // don't spend credits just by being on the board
    prefetchAudio(text)
  }, [inCanvas, text])
  // …
}

It returns { inCanvas, locale, dir, paused }. Outside the canvas it's { inCanvas: false, locale: undefined, dir: 'ltr', paused: false }. The same object is passed to render(ctx).

  • paused follows the toolbar's Motion / Paused switch. CSS animations and transitions inside screens are frozen automatically; use paused for JS-driven motion and video.
  • Only screens near the viewport are mounted. Screens scrolled far away show a light placeholder and unmount, which keeps big boards light. (A screen's local state resets when it unmounts.)

Multilingual interface support

The language menu switches the interface language of the screens: your app's i18n locale, for every lane at once.

1. Declare the locales each iteration supports. The first one is its fallback.

defineFlow({ feature: 'Checkout', iteration: 'v2', languages: ['en', 'hi', 'ar'], … })

The menu lists every language any flow declares. Lanes show their languages, highlighting the current one. A lane that doesn't support the current locale says so ("Not in Arabic · showing English") and shows its fallback.

2. Wire your i18n library. React screens get the locale through render(ctx) and useFlowCanvas(). To apply it, pass a LocaleProvider that wraps each React screen:

// react-i18next: a clone of your instance per locale, so lanes can differ without switching the app
import { I18nextProvider } from 'react-i18next'
import { FlowCanvas, i18nextLocaleProvider } from 'codetocanvas'
import i18n from './i18n'

const CanvasLocale = i18nextLocaleProvider(i18n, I18nextProvider) // once, outside components

<FlowCanvas flows={flows} LocaleProvider={CanvasLocale} />
// next-intl, lingui, react-intl or your own context: map the locale onto the library's provider
<FlowCanvas
  flows={flows}
  LocaleProvider={({ locale, children }) => <IntlProvider locale={locale} messages={messages[locale]}>{children}</IntlProvider>}
/>

Define LocaleProvider outside the component (or memoise it), so the screens don't remount on every render.

3. The other screen kinds.

  • url screens get the locale as a query param, ?lang=hi by default. Set localeParam to rename it, urlLocale="path" for a /hi/… prefix, urlLocale="none" to leave URLs alone, or pass a function (url, locale) => string. A screen can override it with its own urlLocale.
  • image screens can't switch language. Give one image per locale (image: { en: '/en/welcome.png', hi: '/hi/welcome.png' }); where one is missing, the screen shows its first image with a "Not in Hindi" badge.
  • Right-to-left locales (Arabic, Hebrew, Urdu, Persian…, or any locale in an RTL script) get dir="rtl" on their screens, and every screen gets lang. Use height: 'auto' where longer translations need room.

defaultLocale picks the starting language; after that, the board remembers the last one chosen.


Keeping flows in sync: /map-flow

Writing flow files by hand works, but the package ships a Claude Code skill that does it for you, and a small CLI that keeps them honest.

npx codetocanvas install-skill   # copies the skill to .claude/skills/map-flow

Then, in Claude Code:

  • /map-flow reads your prototype, finds its features and their iterations (version folders, version switches, parallel components, a hand-made canvas), and proposes flow files. You confirm, it writes flows/<feature>/<iteration>.flow.tsx and flows/index.ts, with fixtures for states you can't just click into (loading, errors, mid-process). It never silently changes an iteration's notes.
  • /map-flow --check finds flows whose code changed and proposes updates.
  • /map-flow --import <screenshots or URLs> adds an iteration that isn't in the code, such as the shipped product's screenshots.

The CLI is the deterministic part (no AI, no network), and runs fine on its own:

| Command | | |---|---| | codetocanvas check [flows-dir] [--mark] | Lists flows whose sources changed since their fingerprint, or are missing. Exits 1 if any are stale, for CI. --mark writes stale: true into them (and removes it from fresh ones), so the canvas shows the warning. | | codetocanvas fingerprint [paths…] | Records the current hash of each flow's sources and clears stale. | | codetocanvas index [flows-dir] | Syncs flows/index.ts with the flow files on disk. It keeps the current import order (the board order) and appends new flows. | | codetocanvas install-skill [--force] | Copies the /map-flow skill into .claude/skills/map-flow. |

The flows folder defaults to src/flows, then flows. Run the commands from the project root: sources paths are relative to it. A handy setup:

// package.json
"scripts": {
  "predev": "codetocanvas check --mark || true" // flag stale flows on dev start, without blocking it
}

Keep sources a plain array of string literals: the CLI reads flow files as text. Leave fingerprint and stale to the CLI.


Placing screens by hand (optional)

For a single flow you can also pass frames and edges, placing each screen on a grid yourself. This is the simpler, older form of a flow. Frames render live React, so they stay interactive. lanes adds headings: { title, subtitle?, row }, with that lane's frames on the rows below it. languages declares their interface locales.

import { FlowCanvas, type FlowEdge, type FlowFrame } from 'codetocanvas'

const frames: FlowFrame[] = [
  { id: 'welcome', label: 'Welcome', col: 0, row: 0, render: () => <Welcome /> },
  { id: 'signup', label: 'Sign up', col: 1, row: 0, render: () => <SignUp /> },
  { id: 'sso', label: 'Sign in with SSO', col: 1, row: 1, render: () => <Sso /> },
  { id: 'home', label: 'Home', col: 2, row: 0, width: 1100, height: 700, render: () => <Home /> },
]

const edges: FlowEdge[] = [
  { from: 'welcome', to: 'signup', label: 'Get started' },
  { from: 'signup', to: 'sso', label: 'Use SSO', dashed: true },
  { from: 'signup', to: 'home', label: 'Create account' },
  { from: 'sso', to: 'home' },
]

<FlowCanvas title="Onboarding" frames={frames} edges={edges} />

API

<FlowCanvas />

| Prop | Default | | |---|---|---| | flows | | Features and their iterations: an array of defineFlow() objects. | | frames | | Screens placed by hand, as one flow. Leave out flows and frames to capture screens automatically. | | edges | | The arrows between frames: { from, to, label?, dashed? }. | | lanes | | Headings between frames: { title, subtitle?, row }. | | languages | | Interface locales of the frames. | | defaultLocale | first declared | The language the board starts in. | | LocaleProvider | | ({ locale, dir, children }) => …: applies the board's locale to React screens. See i18nextLocaleProvider(). | | urlLocale | 'query' | How url screens get the locale: 'query', 'path', 'none' or (url, locale) => string. | | localeParam | 'lang' | The query param for urlLocale: 'query'. | | title | document.title | Shown in the canvas toolbar. | | gap | 150 | The space between screens in px (room for the arrows and their labels). | | storageKey | 'flow-canvas' | Prefix for the saved view, toolbar settings and captured screens in localStorage. Set a distinct one per prototype if several share an origin. | | onModeChange | | (mode: 'prototype' \| 'canvas') => void. Use it to pause audio, video or timers in the app when the canvas opens. |

A screen is { id, label?, col?, row?, width?, height?, … } plus one of render, image or url. A FlowFrame is { id, label, col, row, width?, height?, render }. The default size is 520 × 600.

Also exported: defineFlow(), useFlowCanvas(), useCanvasState(), i18nextLocaleProvider(), and the helpers localizeUrl() and direction().

Lower-level pieces

For custom setups, these are exported too: FlowBoard (the board on its own), ViewToggle and useViewMode() (the switch and its state), and useFlowCapture() and layoutCapture() (the recorder and its layout).


How capture works

It's deterministic, with no AI and no network calls:

  • A MutationObserver watches the page. Once it settles for about 0.4s, the plugin takes a static copy of each visible screen: its HTML, with current form values written in. Screens are keyed by data-flow-screen, or by the URL if there's no marker.
  • When the screen in front changes, it records an arrow from the previous screen, labelled with the button or link clicked in the last few seconds.
  • The layout is breadth-first from the first screen you captured: each screen goes one column after the earliest screen that leads to it, and branches stack below.
  • Captures are kept per interface locale, read from <html lang>. Capture in two languages and the canvas shows one lane per language.
  • Everything is stored in localStorage in your browser. Clear wipes it.

Captured screens are static copies: they show what was on screen, styled by your app's CSS, but you can't click them. Style-only changes, like animations, don't trigger new captures.


Developing

npm install
npm run dev        # rebuilds dist/ on change
npm run typecheck
npm run build

To try it in a local app before publishing, run npm install ../flow-canvas (this folder) from that app. If the app uses Vite, also set resolve: { dedupe: ['react', 'react-dom'] }, so the app and this folder's dev copy of React don't both load.

Publishing

npm login
npm version patch   # or minor / major
npm publish         # runs typecheck + build first

Only dist/, skills/, this README and package.json are published. Check with npm pack --dry-run.