Work / Trunk / Wiki / Decisions
2026-10-17-drawn-elements
Decisioncanonicalverified 2026-10-18
DECISION.2026-10-17.DRAWN-ELEMENTSDrawn elements: a table of their own, changed like everything else
Approved by the owner on 2026-10-17 (T032).
Decision
- A drawn element is one row in a new table,
DrawnElement. The row holds only what never changes: which product, what kind it is, and where it lives (one page or one variant). Like an imported token or icon, it isCREATED: no ref sees it until aCREATEchange for it is on that ref. - Everything about it that can change is a change row, the same as any other edit: its parent, its place among its siblings, x, y, width, height, its text, its path, the variant it uses, and its CSS. So drafts, Review, sync, conflicts, merge, history and Verify work for it with no new rules.
- A new page is a page, not an element: a
Pagerow with originDESIGNED, made visible by aCREATEchange, as the schema already allows. New pages are made on Journey, from the right-click menu or with N (2026-10-18-editor-redesign). A Frame drawn on a page is an element of kindFRAME. - Two people can design the same path. The unique index on a page's path holds for captured pages only; a designed page's path is checked when a branch is merged, as one of the branch page's checks.
Why
- The owner chose a table of its own over JSON in
DraftChange(0017, decided 2026-10-17): an element has many properties, and they change one at a time. - Keeping the properties as change rows, rather than columns, means two people who move the same rectangle conflict only on what both changed, property by property, which is how every other conflict works (branch-verify-merge). Columns would make a drawn element the one thing in Trunk that cannot be synced or merged property by property.
- A row is never updated, so it can be shared by every ref: a branch that
never saw the
CREATEsimply does not see it.
Impact
Schema
One new enum, one new table, one new target kind, and a partial unique index.
// What a drawn element is. FRAME is a box that holds others; COMPONENT_USE
// places a use of a variant.
enum DrawnKind {
FRAME
RECTANGLE
TEXT
PATH
COMPONENT_USE
}
// An element someone drew. Only what never changes is here; its place, size,
// text, path, variant and styles are changes (targetKind DRAWN_ELEMENT), so a
// ref sees it, and sees it as it is, only through its changes. It lives on one
// page or in one variant.
// SQL check: exactly one of pageId and variantId.
model DrawnElement {
id String @id @default(uuid(7)) @db.Uuid
clientId String @db.Uuid
productId String @db.Uuid
kind DrawnKind
pageId String? @db.Uuid
variantId String? @db.Uuid
createdById String? @db.Uuid
createdAt DateTime @default(now()) @db.Timestamptz(3)
product Product @relation(fields: [productId, clientId], references: [id, clientId], onDelete: Cascade)
page Page? @relation(fields: [pageId, productId, clientId], references: [id, productId, clientId], onDelete: Cascade)
variant Variant? @relation(fields: [variantId, productId, clientId], references: [id, productId, clientId], onDelete: Cascade)
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
@@unique([id, productId, clientId])
@@index([pageId])
@@index([variantId])
@@index([productId])
@@index([createdById])
}
TargetKindgainsDRAWN_ELEMENT. Its properties, withselector"":property value parentthe selector of the element it sits in, in its page or variant, or the id of a drawn FRAME; "" for the topafterthe selector or drawn id it follows among its siblings; "" for first x,y,width,heightpixels from the parent's top left, at the size it was drawn texta TEXTelement's wordspatha PATHelement's SVG path datavarianta COMPONENT_USEelement's variant idany CSS property in EDITABLE_PROPERTIESas for a page's element Page,VariantandUsergain the back-relationdrawnElements.Migration SQL, at the bottom with the other checks:
DrawnElement: exactly one ofpageId,variantId; row-level security like every company table; noUPDATE(a trigger, as forCommit).- The change-target trigger learns
DRAWN_ELEMENT: the target is aDrawnElementof the same product. Page_productId_path_keybecomes a partial unique index,WHERE origin = 'CAPTURED'.
Rules the app keeps
- A
CREATEfor a drawn element is always written with itsparent,after,x,y,widthandheightin the same save. - A
DELETEof a drawnFRAMEdeletes what is inside it, in the same save. - When a capture changes a page so a
parentselector no longer matches, the element is drawn at the top of the page, and Analysis lists it as moved. - Merging a branch with a designed page whose path main already has is stopped by a check on the branch page, with a rename.
Code
packages/renderdraws a page's or a variant's drawn elements into its HTML, absolutely placed inside their parent, after the changes are laid over it.packages/versioningandpackages/historyneed no new rules: a drawn element is one more target kind.- Verify counts a drawn element as a difference, as it does any edit.
Not changed
- No
DraftChange,CommitChangeorRefValuecolumn changes. - The dev and test databases are dropped and reseeded once, as for every schema change before launch (CLAUDE.md, "MVP rule").
Status
Active. Approved 2026-10-17; the migration and the database rules are in place.
Sources
features/0017-canvas-tools.md, Decided, 2026-10-17.packages/db/prisma/schema.prisma:PrimitiveOrigin,TargetKind,ChangeOp.