Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
20 KiB
SCRATCHPAD — Invoice Designer Native CSS Layout Engine
Context
Goal: remove bespoke geometry math in the invoice designer and rely on native browser layout (CSS flex/grid + box model). Replace custom drop-parent resolution with @dnd-kit/core DOM-driven collision detection.
Target Deletions
packages/billing/src/components/invoice-designer/utils/constraintSolver.tspackages/billing/src/components/invoice-designer/utils/constraints.tspackages/billing/src/components/invoice-designer/utils/dropParentResolution.tspackages/billing/src/components/invoice-designer/utils/aspectRatio.ts
Decisions
- Prefer CSS-first semantics even if it means removing some legacy constraint behaviors.
- Drag-drop should be based on DOM geometry via dnd-kit collision detection (not custom math).
- Use CSS
aspect-ratiofor images. Avoid JS measurement loops. - Scope decisions (2026-02-13):
- Layout modes: flex + grid.
- Resizing: enabled via CSS sizing props.
- Snapping: edge + grid snapping as discrete insertion behavior.
- Resize writes pixel values:
updateNodeSizerounds storednode.sizeto whole pixels and writesnode.style.width/heightasNpxstrings (avoids float drift between size and CSS).
Detailed Decisions
- Resizing:
- Drag handles:
image,section,stack. - No drag-resize for:
text,field,divider,totals,table,dynamic-table. - No table column resizing.
- Drag writes pixel sizing only (
px). Non-px sizing can be entered via the property panel. - Flex main-axis resize should prefer
flex-basisupdates.
- Drag handles:
- Nesting allowlist:
- Containers:
document,section,stack,grid(if a node/type exists). - Leaves:
text,field,image,divider,totals,table,dynamic-table.
- Containers:
- Drag-drop persistence:
- Persist only
targetContainerId+insertionIndexstate changes. No coordinate-based persistence.
- Persist only
- "Basic snapping" definition:
- Flex: snap to before/after sibling insertion indices.
- Grid: snap to deterministic cell/index insertion based on grid tracks, not arbitrary pixel coordinates.
Implemented
-
2026-02-13:
F001CSS-first layout model- New node model fields in
packages/billing/src/components/invoice-designer/state/designerStore.ts:node.layout:display: flex|grid+ flex/grid properties in CSS semantics.node.style: width/height/min/max, flex item props, media props (aspectRatio/objectFit).
- Removed constraint-solver state from the store snapshot and APIs (constraints are no longer part of
exportWorkspace()). - Updated presets to accept legacy layout shapes while mapping them into CSS layout at insertion time:
packages/billing/src/components/invoice-designer/constants/presets.tspackages/billing/src/components/invoice-designer/state/designerStore.ts
- Deleted the constraints inspector tests and removed constraints UI from
packages/billing/src/components/invoice-designer/DesignerShell.tsx.
- New node model fields in
-
2026-02-13:
F002Layout/style -> DOM style mapping for canvas- New mapping helpers:
packages/billing/src/components/invoice-designer/utils/cssLayout.ts
- Canvas applies mapped styles:
packages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx- outer node: uses
node.stylefor width/height/min/max/flex props, still absolute-positioned during cutover - container child wrapper: uses
node.layoutto setdisplay, flex/grid rules,gap,padding
- outer node: uses
- New mapping helpers:
-
2026-02-13:
F003Flex row/column container layout- Children of flex/grid containers are now rendered as flow items (no absolute
top/left), enabling native flex row/column layout:packages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx
- Child ordering uses
parent.childIdswhen parent isdisplay:flex|grid(stable authored order); legacy canvas containers remain position-sorted for now.
- Children of flex/grid containers are now rendered as flow items (no absolute
-
2026-02-13:
F004Spacing controls (gap/padding + border via existing presets)- Updated Inspector layout panel to edit
gapandpadding(px) and flex alignment using CSS semantics:packages/billing/src/components/invoice-designer/DesignerShell.tsx
resolveFlexPaddingupdated to support both legacy numeric padding and CSSpadding: \"Npx\"during cutover:packages/billing/src/components/invoice-designer/utils/layout.ts
- Updated Inspector layout panel to edit
-
2026-02-13:
F005Flex alignment controls (justify/align)- Layout inspector edits
align-itemsandjustify-contentfor containers and writes CSS values intonode.layout:packages/billing/src/components/invoice-designer/DesignerShell.tsx
- Canvas applies
justifyContent/alignItemsvia the container style mapping:packages/billing/src/components/invoice-designer/utils/cssLayout.tspackages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx
- Layout inspector edits
-
2026-02-13:
F006Item sizing controls (CSS width/height/min/max)- Added "Sizing (CSS)" inspector panel that edits
node.stylesizing strings (width,height,minWidth,minHeight,maxWidth,maxHeight):packages/billing/src/components/invoice-designer/DesignerShell.tsx
- Canvas already applies these values via
resolveNodeBoxStyle(node.style)(no geometry math).
- Added "Sizing (CSS)" inspector panel that edits
-
2026-02-13:
F007Flex item controls (grow/shrink/basis)- Added "Flex Item" inspector panel shown when the selected node is inside a
display:flexparent; editsflexGrow,flexShrink, andflexBasisonnode.style:packages/billing/src/components/invoice-designer/DesignerShell.tsx
- Canvas already applies these values via
resolveNodeBoxStyle(node.style).
- Added "Flex Item" inspector panel shown when the selected node is inside a
-
2026-02-13:
F008Grid container mode (CSS grid)- Layout inspector now supports switching a container between
display:flexanddisplay:grid. - When in grid mode, inspector exposes
grid-template-columns,grid-template-rows, andgrid-auto-flow(gap/padding remain shared):packages/billing/src/components/invoice-designer/DesignerShell.tsx
- Canvas already renders grid containers via
resolveContainerLayoutStyle(node.layout)+ flow children rendering.
- Layout inspector now supports switching a container between
-
2026-02-13:
F009dnd-kit collision detection + sortable plumbing (start of DOM-driven DnD)- Added dnd-kit collision detection strategy (pointer-within preferred; smallest rect under pointer approximates "deepest" target) with closest-center fallback and Always measuring:
packages/billing/src/components/invoice-designer/DesignerShell.tsx
- Introduced sortable contexts for flow-layout containers (flex/grid) and
useSortablefor flow children, enabling DOM-measured ordering targets (no coordinate math for flow drags):packages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx
- Added store action to move/reparent nodes by
parentId + insertionIndex, with allowlist enforcement and cycle prevention:packages/billing/src/components/invoice-designer/state/designerStore.ts
- Added dnd-kit collision detection strategy (pointer-within preferred; smallest rect under pointer approximates "deepest" target) with closest-center fallback and Always measuring:
-
2026-02-13:
F010Reorder within container (sortable)- Dragging a flow-layout node (inside a
display:flex|gridparent) now reorders by updating the parent'schildIdsusing dnd-kit sortableoverresolution (no position math).
- Dragging a flow-layout node (inside a
-
2026-02-13:
F011Cross-container moves (flow layout)- Flow-layout nodes can be dragged into another eligible container (or onto a child within it) and are reparented by updating
parentId+childIdsinsertion index (DOM-measuredovertarget).
- Flow-layout nodes can be dragged into another eligible container (or onto a child within it) and are reparented by updating
-
2026-02-13:
F012Nesting allowlist enforcement- Reparenting via drag-drop is validated with
canNestWithinParent(childType, parentType)and cycle prevention (no dropping into descendants):packages/billing/src/components/invoice-designer/state/designerStore.ts
- Reparenting via drag-drop is validated with
-
2026-02-13:
F013Drop indicators (DOM-derived)- Added a before/after insertion indicator line for flow-layout drags, derived from active/over DOM rect midpoints (no custom geometry solver):
packages/billing/src/components/invoice-designer/DesignerShell.tsxpackages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx
- Container drop targets are still highlighted via
useDroppable().isOverrings (DOM-measured).
- Added a before/after insertion indicator line for flow-layout drags, derived from active/over DOM rect midpoints (no custom geometry solver):
-
2026-02-13:
F014Invalid drop UX- Invalid drop targets now show a blocked visual state (red insertion line or red target ring) and a
not-allowedcursor on the drag overlay; drops do not mutate state:packages/billing/src/components/invoice-designer/DesignerShell.tsxpackages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx
- Drop end emits an error toast-style banner via existing
dropFeedbackmechanism when invalid.
- Invalid drop targets now show a blocked visual state (red insertion line or red target ring) and a
-
2026-02-13:
F015Image aspect ratio + object-fit (CSS)- Media nodes (
image,logo,qr) now render with CSSaspect-ratio(wrapper) andobject-fit(img) support, with inspector controls writingnode.style.aspectRatioandnode.style.objectFit:packages/billing/src/components/invoice-designer/DesignerShell.tsxpackages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx
- New media nodes default to
objectFit: contain(andqrdefaults toaspectRatio: 1 / 1) without any JS measurement loops:packages/billing/src/components/invoice-designer/state/designerStore.ts
- Media nodes (
-
2026-02-13:
F016Resize handles (limited types)- Resize handles are now limited to
image/logo/qrand container blocks (section,container) per PRD; resizing continues to write pixelwidth/heightintonode.styleviaupdateNodeSize(no constraint solver):packages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx
- Resize handles are now limited to
-
2026-02-13:
F016AProperty-panel sizing strings- Inspector "Sizing (CSS)" panel accepts arbitrary CSS strings (
%,rem,auto,calc(...), etc.) for size/min/max while drag-resize continues to write pixel values (px):packages/billing/src/components/invoice-designer/DesignerShell.tsxpackages/billing/src/components/invoice-designer/state/designerStore.ts
- Inspector "Sizing (CSS)" panel accepts arbitrary CSS strings (
-
2026-02-13:
F016BBasic snapping (discrete insertion)- Flex parents now resolve insertion as before/after based on DOM rect midpoint along the main axis; grid uses the sortable
overcell/index deterministically (no pixel snapping math):packages/billing/src/components/invoice-designer/DesignerShell.tsx
- Flex parents now resolve insertion as before/after based on DOM rect midpoint along the main axis; grid uses the sortable
-
2026-02-13:
F016CDrag-drop persistence (no coordinates)- Dragging nodes no longer writes
position.x/yduring drop; drag-drop mutations are limited totargetParentId + insertionIndexupdates (parentId+childIds):packages/billing/src/components/invoice-designer/DesignerShell.tsxpackages/billing/src/components/invoice-designer/state/designerStore.ts
- Dragging nodes no longer writes
-
2026-02-13:
F017Delete legacy geometry utilities- Deleted legacy solver + geometry/drop-parent modules and their tests (removed from runtime graph):
packages/billing/src/components/invoice-designer/utils/constraintSolver.tspackages/billing/src/components/invoice-designer/utils/constraints.tspackages/billing/src/components/invoice-designer/utils/dropParentResolution.tspackages/billing/src/components/invoice-designer/utils/aspectRatio.ts
- Simplified component insertion parent resolution in the designer to no longer depend on
dropParentResolution.
- Deleted legacy solver + geometry/drop-parent modules and their tests (removed from runtime graph):
-
2026-02-13:
F018Reduceutils/layout.tspackages/billing/src/components/invoice-designer/utils/layout.tsnow contains only lightweight helpers (e.g.resolveFlexPadding) and type exports; alignment/geometry helpers were removed from this module.- Legacy drag-move guide/preview logic in
DesignerShellwas pared back (no coordinate-based preview updates).
-
2026-02-13:
F019Persist CSS-like layout props in AST import/export- AST export writes CSS-like sizing, flex/grid container props, and media props (
aspectRatio,objectFit) intostyle.inline:packages/billing/src/components/invoice-designer/ast/workspaceAst.ts
- AST import now hydrates
DesignerNode.styleandDesignerNode.layoutfromstyle.inline(with safe defaults for containers) so export -> import roundtrips CSS semantics. - Import mapping now treats top-level AST
sectionnodes as designerpagenodes (avoids a duplicate designer-only page wrapper).
- AST export writes CSS-like sizing, flex/grid container props, and media props (
-
2026-02-13:
F020Preview uses authoritative renderer CSS semantics- Preview pipeline now passes only the
DesignerWorkspaceSnapshotfields (removed staleconstraintsplumbing that no longer exists in the store snapshot):packages/billing/src/components/invoice-designer/DesignerVisualWorkspace.tsx
- Preview iframe
srcDocis now a full HTML document withhtml, body { margin: 0; padding: 0; }so the renderer-scoped CSS (.invoice-template-root { ... }) behaves the same inside the iframe as it does in the app / PDF (avoids default iframe body margins affecting layout).
- Preview pipeline now passes only the
-
2026-02-13:
F021Audit imports of deleted geometry utilities- Confirmed no runtime imports/references remain for deleted modules:
packages/billing/src/components/invoice-designer/utils/constraintSolver.tspackages/billing/src/components/invoice-designer/utils/constraints.tspackages/billing/src/components/invoice-designer/utils/dropParentResolution.tspackages/billing/src/components/invoice-designer/utils/aspectRatio.ts
- Grep runbook:
rg -n "utils/(constraintSolver|constraints|dropParentResolution|aspectRatio)" -S .
- Confirmed no runtime imports/references remain for deleted modules:
Remaining Design Choices
- Collision strategy and snapping thresholds are intentionally selected to minimize custom geometry logic:
- Collision detection:
- Use
pointerWithinfirst (deepest eligible droppable under cursor). - Fallback to
closestCenterif pointer is not within any droppable. - Configure measuring to keep DOM rects fresh during drag.
- Use
- Sensors and overlay:
PointerSensorwith activation distance (e.g. 6px).DragOverlayto avoid layout reflow while dragging.
- Sorting strategies:
- Flex column:
verticalListSortingStrategy - Flex row:
horizontalListSortingStrategy - Grid:
rectSortingStrategy
- Flex column:
- Snapping threshold behavior:
- Flex uses midpoint rule on hovered rect (before/after) for insertion indicators.
- Grid uses sortable
overresolution directly (deterministic cell/index).
- Collision detection:
Developer Notes (DnD + Nesting)
-
Collision detection (why pointer-within first):
- Implemented in
packages/billing/src/components/invoice-designer/DesignerShell.tsxascollisionDetection. - We use
pointerWithin(args)and return the collisions sorted by smallest droppable rect area first, as a practical proxy for "deepest nested droppable under the cursor". - If no droppable contains the pointer (fast drag / overlay edges), we fall back to
closestCenter(args)so the drag never “loses” anovertarget. - Measuring is configured as
MeasuringStrategy.Alwayson theDndContextto avoid stale droppable rects in nested flex/grid layouts.
- Implemented in
-
Droppable id conventions (how
over.idmaps back to a node):- Container drop zones use
id: \droppable-${node.id}`inpackages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx`. - Flow items use
useSortable({ id: node.id, ... })(sortable id is the node id). - When debugging: if
over.idstarts withdroppable-, it represents “drop into this container”; otherwise it is a sortable item id representing “drop relative to this item”.
- Container drop zones use
-
Nesting rules (authoritative allowlist):
- Implemented in
packages/billing/src/components/invoice-designer/state/hierarchy.tsand enforced viacanNestWithinParent(...). - Store actions that move/reparent nodes must validate:
- type allowlist (
canNestWithinParent) - cycle prevention (no dropping into descendants)
- type allowlist (
- When adding a new node type, update
HIERARCHY_RULESand ensureallowedChildrenon container nodes matches the same intent.
- Implemented in
-
Where to tune UX:
- Activation threshold (avoid accidental drags):
PointerSensoractivationConstraint.distanceinpackages/billing/src/components/invoice-designer/DesignerShell.tsx. - Over-target selection behavior: the
collisionDetectioncallback inpackages/billing/src/components/invoice-designer/DesignerShell.tsx. - Sort strategy selection (row/column/grid):
SortableContextstrategyselection inpackages/billing/src/components/invoice-designer/canvas/DesignCanvas.tsx.
- Activation threshold (avoid accidental drags):
Implementation Sketch (Non-binding)
- Introduce a single "layout props -> style props" mapping function used by:
- designer canvas rendering
- preview rendering (if different)
- AST export (if relevant)
- Add dnd-kit:
- sensors: pointer + keyboard (optional)
- sortable contexts for sibling reordering
- collision detection tuned for nested containers
- Enforce nesting rules in drop handler (reject invalid parent).
- Remove legacy util usage, then delete files.
Useful Commands
- Search for legacy geometry imports:
rg -n \"constraintSolver|constraints|dropParentResolution|aspectRatio\" packages/billing/src/components/invoice-designer
- Run a single designer test under the server Vitest config:
cd server && npx vitest run ../packages/billing/src/components/invoice-designer/utils/cssLayout.test.tscd server && npx vitest run ../packages/billing/src/components/invoice-designer/canvas/DesignCanvas.flexColumn.integration.test.tsxcd server && npx vitest run ../packages/billing/src/components/invoice-designer/canvas/DesignCanvas.flexRow.integration.test.tsxcd server && npx vitest run ../packages/billing/src/components/invoice-designer/canvas/DesignCanvas.spacing.integration.test.tsxcd server && npx vitest run ../packages/billing/src/components/invoice-designer/canvas/DesignCanvas.alignment.integration.test.tsxcd server && npx vitest run ../packages/billing/src/components/invoice-designer/canvas/DesignCanvas.sizing.integration.test.tsxcd server && npx vitest run ../packages/billing/src/components/invoice-designer/canvas/DesignCanvas.flexItem.integration.test.tsxcd server && npx vitest run ../packages/billing/src/components/invoice-designer/canvas/DesignCanvas.grid.integration.test.tsxcd server && npx vitest run ../packages/billing/src/components/invoice-designer/utils/dndCollision.test.tscd server && npx vitest run ../packages/billing/src/components/invoice-designer/state/designerStore.flowDnd.test.tscd server && npx vitest run ../packages/billing/src/components/invoice-designer/utils/dropIndicator.test.tscd server && npx vitest run ../packages/billing/src/components/invoice-designer/canvas/DesignCanvas.mediaAspect.integration.test.tsx
Repo/Test Gotchas (Discovered 2026-02-13)
- Vitest + React tests were failing when
NODE_ENV=productionleaked into the test process (React test utils expect non-production builds).- Fix:
server/vitest.globalSetup.jsnow forcesprocess.env.NODE_ENV = 'test'.
- Fix:
- Package tests under
../packages/**were not being discovered when running Vitest fromserver/.- Fix:
server/vitest.config.tsnow includes../packages/**/*.{test,spec}.*explicitly.
- Fix:
- Coverage provider version must match Vitest major version.
server/is currently onvitest@3.2.4, so@vitest/coverage-v8@3.2.4is added toserver/package.jsonto avoid resolving the repo-root@vitest/coverage-v8@4.x.
- Running tests from repo root:
npm run test:localcurrently shells out to a systemdotenvbinary in some environments (CLI flag mismatch).- Reliable alternative:
cd server && npx vitest run <path-to-test>(usesserver/vitest.globalSetup.jsto load../.env.localtest).
Notes (Renderer Schema)
- Preview/renderer schema must allow the same safe CSS subset that the designer exports into
style.inline.- Added support for:
flexDirection, grid inline props (gridTemplateColumns,gridTemplateRows,gridAutoFlow), and media props (aspectRatio,objectFit) in:packages/billing/src/lib/invoice-template-ast/schema.ts
- Added support for: