Your AI already built the website. ReFrame is where you visually finish it.
Click anything, make it look right — no prompt, no tokens, no regeneration, no fighting the code.
The source stays yours.
ReFrame is a manual editing layer for websites that already exist as React/Next.js code — including sites generated by AI tools (v0, Lovable, bolt.new, Cursor). Those tools are good at generating a first draft and bad at the mechanical follow-up ("move this 12px," "align these cards," "make the mobile nav 8px shorter") — the kind of edit that's fastest done by hand, on a canvas, not by prompting an LLM again.
ReFrame is explicitly not an AI product. There's no chat, no LLM calls, no token metering anywhere in the editing path. It's a parser + a deterministic mutation engine + a canvas — closer to a code-aware Wix/Webflow than to a coding assistant.
Current scope (v0): Next.js (App Router) + React (Server & Client Components) + Tailwind CSS. Not a universal website editor yet.
Existing direct-manipulation editors for React (Onlook is the closest one) operate on the rendered
DOM/className string, not on the source's actual structure. That works for a static
className="p-4" — but breaks down exactly where real component libraries live:
- a
classNamedriven by a prop (variant === "transparent" ? "..." : "...") gets silently dropped or flattened to whichever branch happened to be visible when the edit was made - editing a shared component doesn't clearly distinguish "change this one instance" from "change the shared definition," so overrides get clobbered
ReFrame's engine is built around the opposite bet: parse real component structure — props,
conditionals, shared-component usages, clsx/cn() calls — and refuse to guess when it doesn't
recognize the shape, rather than silently corrupting it. See Architecture below
for the specifics of what's currently supported.
A pnpm workspace with two packages:
packages/core/ Headless engine — parse, resolve, mutate, write. No UI, no server.
apps/dev/ Dev-time visual editor: HTTP proxy + host shell + browser-side click resolver.
parse.ts— source files → aComponentGraph(recast-wrapped Babel parser, so mutations reprint only the changed subtree instead of the whole file).class-ir.ts— a typed intermediate representation forclassNameexpressions. Every shape either matches a supported pattern or comes back as{ kind: "unsupported", reason }— there is no silent fallback.- Supported: a plain string;
prop === "value" ? A : B(direct comparison ternaries);clsx(...)/cn(...)calls whose args are each a string literal ortest && "...". - Explicitly refused (each with a distinct reason): calls to any other helper function;
template literals; ternaries on a derived variable (
const isX = variant === "x"; isX ? A : B— traced dataflow is deliberately out of scope for now); nested/chained ternaries; anything else unrecognized.
- Supported: a plain string;
resolve.ts— routes a DOM click (or a route + component name) to the right source target: the shared definition, or one specific call-site usage. Fails loudly ("can't disambiguate") rather than guessing when a component has multiple usages in one file that can't be told apart.mutate/— deterministic, targeted mutations:class.ts(set a ternary branch, without ever collapsing it to a flat string),prop.ts(per-instance prop overrides),move.ts(reorder JSX children as a whitespace-preserving chunk),tailwind.ts(token-exact utility class replacement —p-4→p-6never touchessm:p-4).tailwind-scale.ts/properties.ts— maps a visual property edit (e.g. height in px) onto the Tailwind spacing scale when it lands on an exact step, falling back to an arbitrary-value class (h-[72px]) rather than silently rounding.write.ts— recast-based reprinting: only the mutated subtree changes, so diffs stay small and human-reviewable.
proxy.ts— a reverse proxy in front of your already-runningnext dev. Injects one<script>tag into HTML responses (never touches RSC flight payloads), rewrites Host/Origin so Next's dev-origin checks stay happy, and passes the HMR WebSocket straight through. Your project's source is never modified — no config change, no injected script in your ownlayout.tsx— the injection happens at the HTTP layer, in front of the server.static/preload.js— runs in the target page; walks up from a clicked DOM node via React's fiber tree (_debugInfo, since React 19 removed_debugSource) to resolve the click to the owning component, including through Server Component nesting.host.ts/static/host.html— the editor shell: iframe of your app + a component list- a properties panel (height, padding, instance-vs-shared scope picker), talking to the proxy
and to
packages/core's mutation functions.
- a properties panel (height, padding, instance-vs-shared scope picker), talking to the proxy
and to
Requires Node.js and pnpm.
pnpm install
pnpm test # run packages/core's test suiteTo try the live editor against a target Next.js app:
# 1. In your target project, start Next's own dev server as usual
cd path/to/your-nextjs-app && npm run dev # e.g. listening on :3000
# 2. In this repo, point ReFrame's dev tool at it
cd apps/dev
pnpm start ../../path/to/your-nextjs-app 3000
# open http://localhost:4200apps/dev assumes next dev is already running on the target port — it doesn't spawn it for you
in this slice. There's a small bundled fixture app for trying this out immediately without a real
project: fixtures/onlook-validation-app.
Early and actively evolving — engine and dev-tool pieces above are implemented and tested;
the polished canvas UI (drag/resize wired into the visual editor, a git-diff review step) is not
built yet. Expect the supported-pattern boundary in class-ir.ts to expand over time rather than
all at once — patterns outside it fail with an explicit reason rather than being silently mishandled.
Not yet decided.