More Projects
59 in totalRunnable Visual Editor
2025
Meant to let you paste a React component, preview it live in a sandboxed iframe compiled by Babel standalone, then click any element to edit its text, colour, size and weight, with edits persisted through a small Express store (POST /component, GET /preview/:id, PUT /component/:id). The editing half is written; the compile path is not. It appends a second default export to whatever you paste, Babel rejects it, and the effect that writes the iframe returns early — so the preview never paints and nothing downstream of it is exercisable.
Project Details
2025
Tool · DevTools
The target was a design-tool loop for React: paste a component, click the heading in the live preview, change its text and colour, get the component source back with those changes in it. The editing half is built. The round trip is where the prototype stops, and the reason it stops is the most useful thing in the repo.
This was written to a brief rather than for myself — the README signs off with two Runable addresses — which is why the scope is drawn where it is. Two hundred and sixty-nine lines across seven files, a Vite frontend on 5173 and an Express store on 4000, no database and no test suite.
What it does
- Compiles pasted source in the browser with
@babel/standaloneunder thereactpreset, re-running the transform on every keystroke in the editor pane and holding the last good output when the parse fails. - Mounts the result in a sandboxed iframe, written in through
document.writeso the preview gets a clean document rather than inheriting the host page's styles. - Selects elements by click using a capture-phase listener on the iframe document, which
intercepts the event before any handler inside the component can call
preventDefaultor navigate away. - Outlines the selection by injecting a
.__selected__ { outline: 2px solid #ff9800 }rule into the iframe head once and toggling the class, instead of writing inline styles it would later have to strip back out. - Reads live values, not source values. The inspector populates from
getComputedStyleon the selected node, so it shows the colour and font size the element actually resolved to, including anything inherited. - Edits text, colour, font size and weight, applied straight to the DOM node on blur so the preview updates without a recompile.
- Persists through a three-route Express service: POST to store a component and get a uuid, GET to read it back, PUT to update. One JSON file per component on disk, no database.
- Ships a seeded example component so the editor has something clickable the moment it loads.
Why an iframe and a capture listener
Both of those are load-bearing and neither is obvious. Rendering the pasted component into a
div on the host page would be less code, and it would also mean the host page's stylesheet
cascades into the preview, so getComputedStyle reports colours the user never wrote. Writing
a fresh document through document.write gives the component a blank cascade, which is the
only way the inspector's readings mean anything.
The capture-phase listener solves the other half. Pasted components contain buttons, links and
their own click handlers, and any of them can swallow the event or navigate the frame away.
Registering on the iframe document with capture: true means the picker sees the click on its
way down, before it reaches whatever the component installed on the target. Selection stops
being something the previewed code can opt out of.
The one-time style injection follows the same instinct. Setting element.style.outline
directly would mean writing an inline style the serializer would later have to distinguish
from a style the user actually asked for. A class plus a rule in the iframe head keeps the
highlight entirely outside the data.
The wall
Going the other direction, from an edited DOM node back to the JSX expression that produced
it, needs a source-position map linking each rendered node to its origin in the input. Babel
can emit that; this editor never asked for it, and reconstructing JSX by walking the DOM
loses everything that was an expression rather than a literal. A heading rendered from
{title} comes back as the string it happened to hold on this render, and the prop is gone.
So save() does not pretend. It takes the root's innerHTML and appends it to the stored
source inside a comment block delimited by __SERIALIZED_DOM_START__ and
__SERIALIZED_DOM_END__ markers, and the comment above the function says exactly why. That is
the honest boundary of the prototype: the editing experience is designed and written, and the
round trip needs a compiler pass that was never started.
The mount script does not run
Reading it back with fresh eyes, the boundary sits earlier than the comment claims, and it
starts with the compile call. It appends the literal string "\nexport default Card;" to
whatever was pasted, which hardcodes the component's name and, on the seeded example that
already declares export default function Card(), produces two default exports in one module.
Babel refuses that, the catch sets the compiled output to null, and the effect that writes the
iframe returns early. So the preview is empty on first load, before anything else gets a
chance to go wrong.
Suppose it compiled. The module script that document.write injects still cannot execute, for
three stacked reasons. It resolves the component with (typeof default === 'function') ? default : (window.Card || default), and default is a reserved word, so that line is a parse
error that kills the whole script. Under it, React is imported from the bare specifier
'react', which a browser module cannot resolve without an import map, and createRoot is
pulled as a named export from a UMD build URL, which does not export names.
None of this is visible while working on the file, because the entire block lives inside a template literal written into another document, and the failure surfaces only in the iframe's own console.
What the shape of it was right about
The parts worth keeping are the parts that describe an approach rather than an implementation: a clean document for the preview so computed styles are honest, capture-phase selection so the picker outranks the component, highlight state kept out of the data, and a save path that refuses to fabricate a round trip it cannot perform. Getting the preview to actually paint is a conditional on the appended export, a rewritten mount script and an import map, which is an afternoon. The round trip is a Babel plugin that stamps each JSX element with its source position, plus a serializer that edits the original source at those offsets rather than regenerating it, and that is a genuinely different project.
Project Details
2025
Tool · DevTools