Two drag-based editors: one for approval workflows, one for shop-floor forms. A line supervisor builds and changes both without writing code, and sees every change live. This came out of a real shop-floor project; the parts tied to that customer's domain were removed, so what's here is the part that isn't about any one shop floor.
Live: workflow designer · form designer
§ The problem
Shop floors run on paper. A form changes (a new field on a checklist, a different approver once a change goes over budget) and someone edits a spreadsheet, prints it, walks it around. Moving that onto a screen doesn't help if every change is still a code change. So treat the form and the flow as data: the supervisor who knows the process owns the configuration, IT owns the engine that runs it.
§ What's built
Two editors, three languages, all in the browser. Definitions are saved to localStorage and export as JSON. No backend.
Workflow designer. A node graph with six node types: start, approval, condition branch, parallel, completion event, end. Each has subtypes. An approval can be a single signer, all-of, any-of, or a quorum. A condition branches on a field value, a role, or the time of day. Edges are directed, drawn as SVG bezier curves; a condition labels its outgoing edges. Auto-layout walks the graph breadth-first from the start node and stacks it top to bottom.

Form designer. A 12-column grid, Bootstrap-style. Seventeen field types: text, number, single- and multi-select, radio groups, checkboxes, date / datetime / time, file upload, a signature pad, a range slider, a switch, section dividers, static labels. Each field has a column span from 1 to 12, picked from a 12-cell bar. Fields drag to reorder, and the canvas shows a live read-only preview of every one.

Both editors come with a sample built around the same scenario: an engineering-change-notice approval flow, and the form that requests it.
§ Engineering decisions
No react-flow, no dagre. The graph model is flat: { nodes: [{ id, type, x, y, label, ... }], edges: [{ from, to, condition_label? }] }. Edges are inline SVG paths. Auto-layout is a breadth-first depth assignment from the start node, about ninety lines. A library would have added pan, zoom, minimaps, and a much bigger bundle for things this prototype doesn't need.
Tradeoff: fewer canvas conveniences than a flow library gives you out of the box. If the editor ever grows real complexity (sub-flows, large graphs), pulling one in starts to make sense.
Tailwind v4 doesn't generate classes from runtime strings, so col-span-{n} won't work for a span the user picks. The field card sets style={{ '--fd-col-span': n }} and one fixed arbitrary-property class reads it. The span is picked from a 12-cell bar, not a dropdown. dnd-kit's sortable handles reordering. One thing that bit me: a disabled preview control inside a draggable card was eating the click that should select the card, so the preview region is pointer-events-none.
Tradeoff: how a field degrades on a phone (what a 6-of-12 field becomes on a small screen) is something you write, not something a grid library hands you. Here it collapses to one column under the sm breakpoint.
No backend. A workflow or form definition is JSON in localStorage, and an export button downloads it. The prototype is about the configuration, the thing a supervisor produces, and that's complete and inspectable here. A production version is a different shape: persistence and versioning behind an API, validation on the server, an execution engine that runs the chain and binds a form to a step. The editors barely change; everything under them does.
Tradeoff: you can design a flow here, but you can't run it. Running it is the engine's job, not the designer's.
Three locales through a React context and a t(key) hook, reading from three JSON bundles kept key-for-key in sync. No middleware, no route segments. The locale comes from a stored preference, then the browser language, then English; a switcher in the header changes it. One wrinkle: the detected locale is applied a microtask after mount (React 19 doesn't want a synchronous state set inside an effect), so anything that builds persistent content from the locale (the sample graph, the sample form) waits for a ready flag instead of reading English on the first pass.
Tradeoff: no locale-prefixed URLs, so it isn't set up for per-language SEO. If that matters later, switch to next-intl with routing.
No component library. Buttons, selects, switches, the signature pad, the range slider are plain HTML elements with Tailwind. cn is a six-line filter-and-join, not clsx plus tailwind-merge.
Tradeoff: you build the UI primitives yourself. In return there's less to learn, less to break, and the whole thing stays small.
§ Process
Any change above a small size goes through a written proposal first: a file under openspec/changes/ with the scope, what's out of scope, the UI spec, the test plan. It moves to openspec/archive/ once it lands. A few conventions are enforced by git hooks rather than memory: a build and a responsive-screenshot check before a push, English commit subjects, AI assistance noted in commits and docs.
§ Links