# SCRATCHPAD — Invoice Designer Unified Component AST ## Context Goal: collapse designer state into a unified generic node tree so adding new properties (for example `borderRadius`) is mostly schema work, not store plumbing. This plan intentionally continues the simplification arc: 1. Templates are data (`templateAst` JSON) not code/WASM. 2. Layout math is delegated to the DOM/CSS engine (flex/grid + dnd-kit). 3. Designer state becomes a single generic AST with schema-driven editing. ## Current State (Starting Point) - Designer store uses `DesignerNode[]` with typed fields: - `position`, `size`, `parentId`, `childIds`, `allowedChildren`, `layout`, `style`, `metadata`, etc. - Property editing is largely hardcoded in `packages/billing/src/components/invoice-designer/DesignerShell.tsx`. - Nesting rules are defined in `packages/billing/src/components/invoice-designer/state/hierarchy.ts`. - Palette metadata defaults live in `packages/billing/src/components/invoice-designer/constants/componentCatalog.ts`. ## Decisions (Implementation Choices That Minimize Trouble) - Source of truth for hierarchy is `children` arrays only. - Do not persist `parentId` (redundant and easy to desync). - Parent lookup is derived when needed (tree sizes are small enough for O(n) searches). - Patch API uses dot-notation paths (for example `style.width`, `metadata.bindingKey`) rather than JSON Pointer to keep call sites readable. - Internally, patch operations should be applied immutably (structural sharing). - This patch API is a hard requirement for the strategy: without it, the refactor cost is paid without getting the “new prop = schema change” velocity gain. - Component schema becomes the single source of truth for: - palette labels/descriptions/categories - defaults (initial props) - editable props schema (inspector UI) - allowed parents/children (nesting rules) - Inspector will support a small set of core field widgets plus opt-in custom widgets for complex objects/arrays (tables). ## Notes / Gotchas - Ensure history/undo remains stable: - Either snapshot `nodesById + rootId` per commit, or store patches and replay. - Snapshotting is simpler and aligns with current approach; patch replay is smaller but more error-prone. - Some existing code expects `allowedChildren` to be present on nodes; that must become a derived selector from schema. - Export/import to invoice-template AST needs a single authoritative mapping; avoid letting schema defaults leak into exported templates unexpectedly. ## File Targets (Likely Touchpoints) - Store: - `packages/billing/src/components/invoice-designer/state/designerStore.ts` - Hierarchy rules removal: - `packages/billing/src/components/invoice-designer/state/hierarchy.ts` (delete) - Component schema/catalog: - `packages/billing/src/components/invoice-designer/constants/componentCatalog.ts` (likely merged into or replaced by schema module) - Inspector refactor: - `packages/billing/src/components/invoice-designer/DesignerShell.tsx` - Canvas rendering: - `packages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx` - Workspace AST mapping: - `packages/billing/src/components/invoice-designer/ast/workspaceAst.ts` ## Validation Runbook (When Implementing) - Tree invariants: - every child id exists in `nodesById` - no cycles (walk from root and track visited) - all nodes are reachable from root (or explicitly allow unattached nodes, but prefer not to) - Quick grep for legacy references: - `rg -n "parentId|allowedChildren|updateNodeStyle\\(|updateNodeLayout\\(|updateNodeMetadata\\(" packages/billing/src/components/invoice-designer -S` ## Developer Guide (Adding Props / Components) ### Component Schema Source Of Truth - File: `packages/billing/src/components/invoice-designer/schema/componentSchema.ts` - Each component defines: - `label`/`description`/`category` (palette + inspector display metadata) - `defaults`: initial `name`, `layout`, `style`, `metadata`, `size` - `hierarchy`: `allowedParents` + `allowedChildren` - `inspector`: panels/fields (schema-driven Inspector) - Helpers now live on the schema module: - `getAllowedChildrenForType(type)` - `getAllowedParentsForType(type)` - `canNestWithinParent(childType, parentType)` ### Inspector Schema Format - Files: - `packages/billing/src/components/invoice-designer/schema/inspectorSchema.ts` (field/panel types) - `packages/billing/src/components/invoice-designer/inspector/DesignerSchemaInspector.tsx` (renderer) - Field kinds supported: - primitives: `string`, `number`, `boolean`, `enum` - CSS-ish: `css-length`, `css-color` - complex: `widget` (custom React editor for component-specific metadata) - Visibility rules: - `visibleWhen` supports `nodeIsContainer`, `pathEquals`, `parentPathEquals` - Widgets: - Implement widgets in `packages/billing/src/components/invoice-designer/inspector/widgets/` - Register by referencing the widget `id` in schema (example: `table-editor` for `metadata.columns`) ### Patch API (How To Mutate State) - File: `packages/billing/src/components/invoice-designer/state/designerStore.ts` - Only supported mutation surface (for anything persistent/undoable): - `setNodeProp(nodeId, path, value, commit?)` - `unsetNodeProp(nodeId, path, commit?)` - `insertChild(parentId, childId, index)` - `removeChild(parentId, childId)` - `moveNode(nodeId, nextParentId, nextIndex)` - `deleteNode(nodeId)` - Dot-path conventions (recommended): - `name` - `style.*` (sizing/typography/media CSS-like props) - `layout.*` (container layout props) - `metadata.*` (component-specific config) - History/undo semantics: - `commit` defaults to `true` - For multi-step edits that should be a single undo step: use `commit=false` for intermediate writes and `commit=true` for the final write. ### Persistence Rules (What Gets Serialized) - Persisted workspace snapshot is `{ rootId, nodesById, canvas settings }`. - `exportWorkspace()` sanitizes node `props` to drop runtime/editor-only keys: - `position`, `size`, `baseSize`, `layoutPresetId` - If a prop should survive reload and affect output, it should live under: - `props.style`, `props.layout`, or `props.metadata` (and be surfaced via schema). ### Conventions For Adding A New Property 1. Add/extend the schema field in `componentSchema.ts` (usually via `COMMON_INSPECTOR` or component-specific `inspector`). 2. Provide a default in `defaults.style` / `defaults.layout` / `defaults.metadata` only if it must exist on insertion. 3. Avoid adding store actions/reducers: use `setNodeProp`/`unsetNodeProp` from inspector and other UI. ### Conventions For Adding A New Component 1. Add a `DesignerComponentSchema` entry in `DESIGNER_COMPONENT_SCHEMAS`. 2. Define `hierarchy.allowedParents` and `hierarchy.allowedChildren` (the only authority for nesting rules). 3. Define `defaults` and `inspector` panels/fields. 4. Canvas/palette/outline should not need bespoke wiring if schema is complete. ## Progress Log - 2026-02-13: Implemented unified AST type definitions in `packages/billing/src/components/invoice-designer/state/designerAst.ts`: - `DesignerAstNode` with `{ id, type, props, children }` - `DesignerAstWorkspace` with `{ rootId, nodesById }` - Stable document root id constant: `DESIGNER_AST_DOCUMENT_ID = 'designer-document-root'` - 2026-02-13: Added canonical indexing to the designer store state in `packages/billing/src/components/invoice-designer/state/designerStore.ts`: - Store state now includes `rootId` and `nodesById` kept in sync with the existing `nodes` array via a `setWithIndex` wrapper. - This is an incremental cutover step so downstream UI/tests can migrate off `nodes` progressively. - 2026-02-13: Implemented generic patch operations in `packages/billing/src/components/invoice-designer/state/patchOps.ts` and exposed them on the store: - `setNodeProp` / `unsetNodeProp` for dot-path updates (immutable deep updates with empty-object cleanup). - `insertChild` / `removeChild` / `moveNode` / `deleteNode` for hierarchy mutations (cycle prevention in `moveNode`). - Renamed legacy coordinate nudge API to `moveNodeByDelta` to free `moveNode` for tree moves. - 2026-02-13: Refined undo/redo history behavior in `packages/billing/src/components/invoice-designer/state/designerStore.ts`: - Store now initializes history with a baseline snapshot so the first committed mutation can be undone. - `setNodeProp` / `unsetNodeProp` now commit to history by default to match existing property-edit behavior. - 2026-02-13: Introduced component schema definitions in `packages/billing/src/components/invoice-designer/schema/componentSchema.ts`: - Defines per-component label/description/category, defaults (size/layout/metadata), and hierarchy allowlists. - `packages/billing/src/components/invoice-designer/constants/componentCatalog.ts` now derives palette definitions from schema (schema is the new source of truth for palette metadata/defaults). - 2026-02-13: Hierarchy allowlists are now resolved via schema: - `packages/billing/src/components/invoice-designer/state/hierarchy.ts` is now a thin wrapper over `getComponentSchema(type).hierarchy`. - 2026-02-13: Palette insertion now uses schema defaults and generic tree ops: - `packages/billing/src/components/invoice-designer/state/designerStore.ts` `addNodeFromPalette` now pulls size/layout/style/metadata defaults from `getComponentSchema(type).defaults` and attaches nodes using `insertChild` semantics (`patchOps.insertChild`). - Default metadata normalization for repeated insertions (table columns, attachment items) moved into the store (`normalizeDefaultMetadataForNewNode`), removing hardcoded defaults from `DesignerShell.tsx`. - 2026-02-13: Outline + breadcrumbs now traverse the tree via children arrays: - `packages/billing/src/components/invoice-designer/palette/OutlineView.tsx` now renders from `rootId` and `nodesById`, deriving parent lookup only for expand behavior. - `packages/billing/src/components/invoice-designer/DesignerShell.tsx` breadcrumbs now derive parent links from `childIds` instead of relying on persisted `parentId`. - 2026-02-13: Selection/hover state now validates ids against `nodesById`: - `packages/billing/src/components/invoice-designer/state/designerStore.ts` `selectNode`/`setHoverNode` clear invalid ids and `deleteNode` clears selection/hover if the referenced node is removed as part of a subtree delete. - 2026-02-13: Canvas rendering now resolves from unified node props: - Added `packages/billing/src/components/invoice-designer/utils/nodeProps.ts` as the canonical accessor for `props.name`, `props.layout`, `props.style`, `props.metadata` (with temporary legacy fallbacks during cutover). - `packages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx` and `packages/billing/src/components/invoice-designer/canvas/previewScaffolds.ts` now render using `props.*` accessors. - Store mutations keep `props` in sync with legacy fields for now (`packages/billing/src/components/invoice-designer/state/designerStore.ts`). - 2026-02-13: Drag-drop reparent/reorder now uses generic tree ops: - `packages/billing/src/components/invoice-designer/DesignerShell.tsx` now calls `store.moveNode(...)` (tree move) instead of `moveNodeToParentAtIndex`. - `packages/billing/src/components/invoice-designer/state/designerStore.flowDnd.test.ts` updated to exercise `moveNode(...)` directly. - 2026-02-13: Resizing now writes through the generic patch API: - Removed `updateNodeSize` from `packages/billing/src/components/invoice-designer/state/designerStore.ts`. - `packages/billing/src/components/invoice-designer/DesignerShell.tsx` now implements `resizeNode(...)` using `setNodeProp` writes (`size.*`, `baseSize.*`, `style.*`) with a single history commit on mouse-up. - `setNodeProp`/`unsetNodeProp` now mirror `name`/`style.*`/`layout.*`/`metadata.*` updates into both legacy fields and `props.*` during cutover. - 2026-02-13: Started schema-driven Inspector cutover: - Added a minimal inspector schema format in `packages/billing/src/components/invoice-designer/schema/inspectorSchema.ts` and attached schemas to component definitions in `packages/billing/src/components/invoice-designer/schema/componentSchema.ts`. - Implemented `packages/billing/src/components/invoice-designer/inspector/DesignerSchemaInspector.tsx` which renders panels/fields from the selected node's component schema and writes edits via `setNodeProp`/`unsetNodeProp` (commit-on-blur for text fields). - Updated `packages/billing/src/components/invoice-designer/DesignerShell.tsx` to render the schema-driven inspector for supported metadata panels while leaving complex editors (tables/attachments/media) on the legacy path for now. - 2026-02-13: Expanded schema-driven Inspector field types and conditional panels: - `packages/billing/src/components/invoice-designer/schema/inspectorSchema.ts` now supports `number`, `css-length`, `css-color` field kinds plus `visibleWhen` rules (`nodeIsContainer`, `pathEquals`, `parentPathEquals`). - `packages/billing/src/components/invoice-designer/schema/componentSchema.ts` now defines a `COMMON_INSPECTOR` (Layout, Sizing, Flex Item) and merges it into all component schemas, so layout/style edits are schema-defined rather than hardcoded in the shell. - `packages/billing/src/components/invoice-designer/DesignerShell.tsx` removed the hardcoded Layout/Sizing/Flex Item inspector blocks and relies on `DesignerSchemaInspector` for those panels; legacy metadata editors remain only for complex types (table columns, attachment items, media). - 2026-02-13: Added first complex schema widget for metadata: - Implemented `packages/billing/src/components/invoice-designer/inspector/widgets/TableEditorWidget.tsx` and wired it into the schema via a `widget` inspector field (`table-editor`). - `packages/billing/src/components/invoice-designer/schema/componentSchema.ts` now attaches the table editor widget to both `table` and `dynamic-table` schemas. - Removed the hardcoded table/dynamic-table inspector block from `packages/billing/src/components/invoice-designer/DesignerShell.tsx` (tables now render their metadata editor via schema widget). - 2026-02-13: Updated designer <-> invoice-template AST mapping to prefer unified props + children: - `packages/billing/src/components/invoice-designer/ast/workspaceAst.ts` now reads `name/layout/style/metadata` via `packages/billing/src/components/invoice-designer/utils/nodeProps.ts` helpers and traverses `children` (fallback to `childIds` for legacy nodes). - Import now materializes `props` and `children` on generated nodes and keeps them in sync with legacy fields during cutover. - Updated `packages/billing/src/components/invoice-designer/ast/workspaceAst.test.ts` fixtures to set `props` + `children` so export/import exercises the unified tree. - 2026-02-13: Added undo/redo history regression test in `packages/billing/src/components/invoice-designer/state/designerStore.undoRedo.test.ts`: - Verifies tree state returns exactly to prior snapshots after a `moveNode` followed by `deleteNode`, via sequential `undo()` and `redo()`. - 2026-02-13: Added schema invariants test in `packages/billing/src/components/invoice-designer/schema/componentSchema.test.ts`: - Ensures every component type has defaults, an inspector schema, and reciprocal nesting allowlists (parent allowedChildren aligns with child allowedParents). - 2026-02-13: Added repo/unit guard ensuring `packages/billing/src/components/invoice-designer/state/hierarchy.ts` stays deleted and is not imported anywhere in the invoice designer code. - 2026-02-13: Added palette insertion integration test in `packages/billing/src/components/invoice-designer/state/designerStore.addNodeFromPalette.test.ts`: - Verifies `addNodeFromPalette` pulls `layout/metadata/size` defaults from the component schema and attaches the new node id into the parent `children` array. - 2026-02-13: Added outline rendering integration test in `packages/billing/src/components/invoice-designer/palette/OutlineView.integration.test.tsx`: - Verifies outline order matches `childIds` ordering and selection state drives highlight styling. - 2026-02-13: Added breadcrumbs hierarchy integration coverage in `packages/billing/src/components/invoice-designer/DesignerShell.breadcrumbs.test.ts`: - Validates breadcrumb path is derived from `childIds` (children-only hierarchy) and does not depend on persisted `parentId`. - 2026-02-13: Added unified props rendering integration test in `packages/billing/src/components/invoice-designer/canvas/DesignCanvas.unifiedProps.integration.test.tsx`: - Ensures `node.props.layout` and `node.props.style` are sufficient to drive DOM styles (without relying on `node.layout` / `node.style`). - 2026-02-13: Added drag-drop reorder integration test in `packages/billing/src/components/invoice-designer/state/designerStore.dragDropReorder.integration.test.ts`: - Uses `loadWorkspace({ rootId, nodesById })` to validate `moveNode(...)` reorders `children` for flow (flex) containers in the unified snapshot format. - 2026-02-13: Added drag-drop reparent integration test in `packages/billing/src/components/invoice-designer/state/designerStore.dragDropMoveAcross.integration.test.ts`: - Validates moving across containers updates only the unified `children` arrays in the exported workspace snapshot and rejects invalid parent targets via schema nesting rules. - 2026-02-13: Added resize-to-DOM integration test in `packages/billing/src/components/invoice-designer/canvas/DesignCanvas.resizeProps.integration.test.tsx`: - Simulates resize writes via `store.setNodeProp('style.width'/'style.height', ...)` and verifies the rendered canvas node element updates its inline sizing. - 2026-02-13: Persisted workspace snapshots now omit runtime geometry/editor-only props: - `packages/billing/src/components/invoice-designer/state/designerStore.ts` `exportWorkspace()` sanitizes `node.props` to drop `position`, `size`, `baseSize`, and `layoutPresetId` (new saves are unified-only). - Updated `packages/billing/src/actions/invoicePreviewPdfParity.integration.test.ts` workspace fixture to use `{ rootId, nodesById }` (no `nodes` / `constraints`). - Updated `packages/billing/src/components/invoice-designer/state/designerStore.constraints.test.ts` assertions to validate unified snapshots. - 2026-02-13: Removed legacy hierarchy module: - Deleted `packages/billing/src/components/invoice-designer/state/hierarchy.ts`. - Moved `getAllowedChildrenForType` / `getAllowedParentsForType` / `canNestWithinParent` helpers into `packages/billing/src/components/invoice-designer/schema/componentSchema.ts` and updated call sites to import from schema. - 2026-02-13: Removed per-property store actions in favor of patch ops: - Deleted legacy store APIs: `updateNodeName`, `updateNodeMetadata`, `updateNodeLayout`, `updateNodeStyle`, `setNodePosition`, `moveNodeByDelta`, `moveNodeToParentAtIndex`. - Kept label text behavior by normalizing label `name` <-> `metadata.text` changes inside `setNodeProp`/`unsetNodeProp` (only when mutating `name` or `metadata.*`). - Fixed `rootId` indexing so `exportWorkspace()` snapshots can be round-tripped via `loadWorkspace()` even when legacy fixtures use non-canonical document ids. - Updated affected tests and UI call sites to use `setNodeProp`/`unsetNodeProp` exclusively. - 2026-02-13: Added deterministic unified-tree traversal helper and tests: - `packages/billing/src/components/invoice-designer/state/designerAst.ts` now exports `traverseDesignerAstNodeIds`. - Added `packages/billing/src/components/invoice-designer/state/designerAst.test.ts`. - 2026-02-13: Added patch ops unit coverage: - Added `packages/billing/src/components/invoice-designer/state/patchOps.setNodeProp.test.ts` to validate immutable deep dot-path updates. - Added `packages/billing/src/components/invoice-designer/state/patchOps.unsetNodeProp.test.ts` to validate cleanup behavior for empty objects. - Added `packages/billing/src/components/invoice-designer/state/patchOps.insertChild.test.ts` to validate deterministic child insertion ordering. - Added `packages/billing/src/components/invoice-designer/state/patchOps.moveNode.test.ts` to validate reorder/reparent semantics + cycle prevention. - Added `packages/billing/src/components/invoice-designer/state/patchOps.deleteNode.test.ts` to validate subtree deletion behavior. - 2026-02-13: Added schema-driven inspector integration coverage: - Added `packages/billing/src/components/invoice-designer/inspector/DesignerSchemaInspector.integration.test.tsx` to assert the inspector renders fields from schema and writes unified `props.*` via generic patch operations. - Testing gotcha: our shared `` doesn’t reliably forward `id` to the native ``, so tests query controls by placeholder rather than label association. - 2026-02-13: Extracted Inspector input normalizers and added unit coverage: - Added `packages/billing/src/components/invoice-designer/inspector/normalizers.ts` (+ `normalizeString`, `normalizeCssLength`, `normalizeCssColor`, `normalizeNumber`) and wired `DesignerSchemaInspector` to use them. - Added `packages/billing/src/components/invoice-designer/inspector/normalizers.test.ts` to validate canonicalization behavior (for example unitless CSS lengths become `px`, empty strings unset). - 2026-02-13: Added table editor schema widget integration coverage: - Added `packages/billing/src/components/invoice-designer/inspector/TableEditorWidget.integration.test.tsx` to assert the `table-editor` widget updates `metadata.columns` and the canvas preview header reacts to the updated columns list. - 2026-02-13: Made workspace <-> template AST export/import deterministic: - Updated `packages/billing/src/components/invoice-designer/ast/workspaceAst.ts` import to treat a single top-level AST `section` wrapper as the designer `page` node (avoids nested page->section wrapping on roundtrip). - Added `export -> import -> export` deterministic roundtrip assertion in `packages/billing/src/components/invoice-designer/ast/workspaceAst.test.ts`. - 2026-02-13: Added persisted workspace snapshot format coverage: - Added `packages/billing/src/components/invoice-designer/state/designerStore.exportWorkspace.test.ts` to ensure `exportWorkspace()` returns only `{ rootId, nodesById, snapToGrid, gridSize, showGuides, showRulers, canvasScale }` and strips runtime/editor-only `props` keys (`position`, `size`, `baseSize`, `layoutPresetId`). - 2026-02-13: Added repo guards for refactor completeness: - Added `packages/billing/src/components/invoice-designer/state/noLegacyApis.guard.test.ts` to enforce removed per-property store action names stay gone, and to keep hierarchy traversal using `children` (no `childIds` usage outside `state/`). - Updated `packages/billing/src/components/invoice-designer/palette/OutlineView.tsx`, `packages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx`, and `packages/billing/src/components/invoice-designer/DesignerShell.tsx` to traverse `children` rather than `childIds`.