Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
8.8 KiB
PRD — Invoice Designer Native CSS Layout Engine (Flex/Grid + dnd-kit)
- Slug:
invoice-designer-native-css-layout-engine - Date:
2026-02-13 - Status: Planned
- Depends on:
ee/docs/plans/2026-02-12-invoice-template-json-ast-renderer-cutover/
Summary
Replace the invoice designer's custom geometry and constraint-math layout engine with native browser layout (CSS Flexbox/Grid + standard CSS features like aspect-ratio). Replace custom drop-parent resolution math with @dnd-kit/core collision detection based on DOM measurements.
The designer should map layout intent to CSS rules, and let the browser compute sizes, alignment, and bounding boxes.
Problem
Current designer behavior relies on a custom coordinate/constraint system for:
- drop target resolution and nesting decisions
- spatial constraints and edge case math
- aspect ratio enforcement
- manual layout calculations
This creates high maintenance cost, hard-to-debug geometry bugs, and divergence from how invoices render in the real HTML/CSS world.
Insight
All spatial constraints, aspect ratios, and grid layouts are special cases of the browser's native layout engine.
Goals
- Represent layout properties in the designer as native CSS semantics (flex/grid/box model).
- Compute all layout and bounding boxes using DOM layout, not custom math.
- Use
@dnd-kit/corecollision detection for drop placement and nesting. - Preserve or improve current designer UX for:
- dragging nodes
- reordering nodes in a container
- moving nodes across containers
- resizing nodes (where supported)
- aspect ratio for images and fixed-ratio blocks
- Keep designer preview output consistent with invoice rendering output (HTML/CSS).
- Delete the custom geometry/constraint layers listed below.
Non-goals
- Building a full “Figma-like” layout engine (guides, autolayout inference, advanced snapping).
- Supporting arbitrary custom physics/geometry behavior in drag-drop.
- Reworking unrelated invoice-template AST or billing flows beyond the designer canvas/layout editing.
Users and Primary Flows
Primary personas:
- Billing admins composing invoice layout in the visual designer.
- Implementers maintaining the designer and renderer.
Primary flows:
- Drag a node into a container
- Reorder nodes within a container
- Move a node between containers
- Adjust container layout mode (column/row/grid) and spacing
- Set sizing constraints using CSS primitives (width, min/max, flex, grid)
- Enforce aspect ratio for images (and optionally other nodes)
- Preview invoice output using the same HTML/CSS semantics used for rendering/PDF
UX / UI Notes
- Replace “constraint” concepts with a “Layout” panel that edits CSS-like properties.
- Layout modes:
- Stack (flex column/row)
- Grid (CSS grid)
- Freeform/absolute positioning is out of scope
- Expose a small, safe subset of properties first:
- container: direction, gap, align, justify, padding, border
- item: width/height, min/max, flex grow/shrink/basis
- grid: columns (template), rows (template), auto-flow, gap
- image:
aspect-ratioandobject-fit
- Drop indicators should be based on DOM geometry (closest edges, insertion index).
- Snapping:
- edge snapping within a container (before/after insertion positions)
- grid snapping when a parent container is in grid mode
Interaction Semantics
- Resizable node types (by drag handles):
image- container blocks:
section,stack
- Non-resizable by drag handles (size naturally; configurable via spacing/layout):
text,field,divider,totals,table,dynamic-table- No table column resizing.
- Resize units:
- Drag handles update pixel values only (e.g.
width: 320px,height: 180px,flex-basis: 240px). - The property panel may accept CSS strings (e.g.
%,rem,auto) for advanced control. - For flex children, resizing along the main axis should prefer
flex-basisoverwidth/height.
- Drag handles update pixel values only (e.g.
- Nesting model:
- Containers (droppable-into):
document,section,stack,grid(if represented as a node/type). - Leaf nodes (droppable-between only):
text,field,image,divider,totals,table,dynamic-table.
- Containers (droppable-into):
- Drag-drop is discrete, not coordinate-based:
- Drop resolves to
targetContainerId+insertionIndex(no "drop at x,y" semantics).
- Drop resolves to
- Basic snapping definition:
- Flex: snap to before/after sibling insertion indices.
- Grid: snap to a deterministic cell/index insertion derived from grid tracks, not arbitrary pixel snapping.
Requirements
Functional Requirements
- Canvas layout should be computed by CSS (flex/grid), not custom math utilities.
- Each designer node type that participates in layout must have a deterministic mapping:
- Designer layout state -> DOM element style -> rendered geometry
- Drag/drop must use
@dnd-kit/corewith collision detection derived from DOM measurements. - Drop-parent resolution must support:
- inserting before/after siblings
- nesting into eligible containers
- rejecting invalid drop targets with clear UX
- Resizing (if present today) should rely on DOM measurement + CSS properties (not constraint solving).
- Aspect ratio enforcement must use CSS
aspect-ratio(and/or intrinsic image sizing), not JS math. - Snapping must be discrete (insertion/cell selection), not freeform coordinate snapping.
- Drag-drop implementation guidance:
- Prefer
@dnd-kit/sortablefor within-container ordering and insertion-index snapping. - Collision detection:
- Use
pointerWithinfirst (select the deepest eligible droppable under the cursor). - Fallback to
closestCenterwhen the pointer is not within any droppable (fast drags / overlays). - Use dnd-kit measuring configured to keep rects fresh during drag (avoid stale geometry in nested layouts).
- Use
- Sensors and overlays:
- Use
PointerSensorwith an activation distance (e.g. 6px) to avoid accidental drags. - Use
DragOverlayso dragging does not reflow layout.
- Use
- Sorting strategies:
- Flex column:
verticalListSortingStrategy - Flex row:
horizontalListSortingStrategy - Grid:
rectSortingStrategy
- Flex column:
- Snapping thresholds:
- Flex insertion uses the midpoint rule on the hovered item's rect (before/after); avoid additional pixel snapping math.
- Grid insertion uses the sortable
overresolution directly (deterministic cell/index selection).
- Prefer
- The delete list modules must be removed from the runtime dependency graph:
utils/constraintSolver.tsutils/constraints.tsutils/dropParentResolution.tsutils/aspectRatio.ts
utils/layout.tsshould be reduced to lightweight mapping/helpers, not geometry solvers.
Non-functional Requirements
- Behavior should remain stable across preview and PDF (headless Chromium) since both are HTML/CSS.
- Drag-drop interactions should remain responsive for typical template sizes (tens to low hundreds of nodes).
- Errors should be surfaced as structured, actionable messages (invalid container, invalid nesting, etc.).
Deleted / Eliminated Layers
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
And eliminate “thousands of lines” of bespoke coordinate math and geometry edge cases.
Rollout / Migration
- No tenant templates require migration (no custom templates in production yet).
- Internal-only cutover is acceptable as long as parity holds for the “standard templates” designer output.
Risks
- CSS semantics may not exactly match legacy constraint behavior for edge cases.
- Drag-drop collision strategies can change perceived drop target behavior; needs UX tuning.
- Print/PDF layout differences vs. in-app preview if the preview container differs (scale, margins).
Acceptance Criteria (Definition of Done)
- Designer canvas uses DOM layout (flex/grid) for geometry; no constraint solver usage.
- Drag/drop uses
@dnd-kit/coreand supports reorder + cross-container move + nesting. - Aspect ratio uses CSS
aspect-ratiofor images (and any other nodes requiring it). - Resizing works using CSS sizing props (width/height/min/max/flex/grid), with no constraint solving.
- Snapping works (edge snapping for insertion/reorder and grid snapping for grid containers).
- Nesting rules are enforced via an explicit allowlist and covered by automated tests.
- Drag-drop resolves to container + insertion index only (no coordinate persistence).
- Removed modules are deleted and no longer referenced.
- Visual designer output renders consistently with the invoice renderer (HTML/CSS preview parity).
- Automated tests cover drag-drop behaviors, nesting rules, and deletion of legacy utilities.