Skip to content
Alpha The designer is real and runs today. Persistence and publishing are not built yet — read exactly what is missing
How it works

The shape of the document.

Two co-equal editors is a structural claim, not a feature. It is true because of five decisions about how a page is represented — and each one of them was arrived at by shipping the alternative first.

Decisions this page defends
5
Arrived at by shipping the opposite
5
Values stored that could be derived
0
Wrapper elements added to your markup
0

The spine

Vertical is flow. Inside is free.

An earlier version was a flat artboard of absolutely positioned children, and it had a real hole: absolute geometry does not reflow. Adding content meant hand-moving everything below it, and there was no unit of the page big enough to reorder, reuse, or hand to somebody else.

Sections split the problem in two. Sections stack in order and each owns a height, so inserting one pushes everything below it down for free. Within a section, children are placed to the pixel — which is what a design tool is actually for.

  • Reorder, rename, lock, hide and duplicate operate on a section.
  • A container resolving to flex or grid lays out its own children instead.
  • Sections clip, so an overflow reads as a layout problem rather than a canvas fault.
The stack heights are authored
Navbar72px
Hero560px
Feature grid480px
Footer240px
Four rows, four heights, and no top anywhere. The next decision is why.

Derived, never stored

A section's top is a sum, not a number.

It is the total of the heights above it, computed on demand. Storing it would mean every insert, delete, reorder and resize has to rewrite every section below — and the first code path that forgets leaves a gap nobody can select.

This is also why the serialiser emits no top for a section. position: relative in source order reproduces exactly the same result in the browser, and writing the number down would be recording a value the next insert invalidates.

The general form

Anything that can be computed from something else is computed. A stored duplicate is a second source of truth that agrees with the first until exactly one code path forgets to update it — and that path is always found by a user, never by a test.

sectionTop()
top(Hero)     = 72
top(Features) = 72 + 560   = 632
top(Footer)   = 632 + 480  = 1112

insert a banner of 180 above Hero
→ every top below moves. No writes.
No writes is the whole result. An insert touches one array and nothing else in the document changes.

The cascade

Partial layers, narrowest wins.

Desktop is the base and carries no override layer. Laptop, Tablet and Phone each apply at their own width and below, in that order, so the narrowest matching layer is applied last and wins. It is a max-width media query, expressed as data.

Resolution at 390px one field at a time
Layer x y w h Applies at 390px?
base 80 96 520 180 Always — it is the document
tablet 380 Yes, 834 and below
phone 24 342 Yes, and applied last
resolved 24 96 342 180 y and h are still the base
A dash is a field the layer does not carry. y was never written at any width, so it still resolves from the base — and a later desktop change to it reaches the phone.
Storing the whole box

Every layer holds all four numbers

  • Simple to write and simple to read back.
  • Editing width at Phone silently pins x, y and height too.
  • A later Desktop change stops reaching the widths below it.
  • The bug everybody who has built one of these has shipped once.
Storing the difference

A layer holds only what you touched

  • Editing width at Phone writes width, and nothing else.
  • Untouched fields keep resolving from the base, forever.
  • Overridden fields are marked, so the blast radius is visible.
  • Reset drops the field and inheritance resumes.

The one rule

The model owns layout. CSS owns paint.

Learned the hard way, and now the rule every preset and every import is written against. The four tiles below are the whole episode: what broke, the fix that was rejected, the rule that replaced it, and what the rule costs.

01

What went wrong

Presets declared display: flex in their stylesheets. It rendered correctly in a browser and was wrong in the editor: the canvas decides whether to position a child absolutely by asking the model what its parent's display is, and a rule living in a stylesheet is invisible to the model. Every preset landed as a pile of overlapping boxes.

Rendered right in a browser · wrong in the tool
02

The rejected fix

Reading computed style back out of the DOM. It solves the symptom and costs a measurement pass on every render — and it makes the editor's behaviour depend on layout having already happened, which is the kind of coupling that produces bugs nobody can reproduce.

Correct output · unreproducible failures
03

The rule

Anything deciding where a child sits — display, direction, gap, padding, alignment — lives in the node's own style, where both the canvas and the inspector can see it. Stylesheets keep what they are genuinely good at: colour, type, borders, radii.

One sentence · applies to every preset and import
04

The consequence

Responsive layout changes go in the per-breakpoint override layers rather than in media queries — and they have to, because an inline style beats a media query anyway. at.tablet.style is the stacking rule, and the model knows about it.

The cascade above is the mechanism

Round-tripping

parse(serialize(doc)) is the contract.

If that did not hold, the code pane could only ever be read. So the two functions are written as a pair and must stay one — and the parser is strict on purpose, because round-tripping the app's own output is its entire job and silence about a mismatch would be a data-loss bug.

What it cannot represent, it reports. Comments, unknown attributes and unsupported tags are dropped and the panel lists them, so a paste that loses an attribute says so at the moment it happens rather than at deploy.

Allowlist, not blocklist

Attributes are admitted by name rather than excluded by name, which keeps onclick and onerror out by construction — a blocklist only keeps out the handlers somebody remembered to write down, and there are about a hundred of them.

On the wire
<section style="position:relative;height:560px">
  <h1 class="display"
      style="left:80px;top:96px;width:520px">
    Design and ship.
  </h1>
</section>
No wrapper, no section top, geometry inline. Three absences, and each one is a decision from further up this page showing through in the output.

All five exist so an agent can drive it.

One reducer, one action set, one history — decided before anything needed it. Read the agent plan

Read it, then go and break it.

The designer boots with a document already in memory. Every decision on this page is observable in it inside a minute — drag a section, and watch the ones below move without a single write.

Deployed with Astro and Comlumen