The canvas and list add-node menus both listed all 10 node types in
one flat dropdown. As the type list grows — especially for forks
adding their own node types (see #312) — a flat list gets hard to
scan. Bucket the types into Messaging / Logic & data / Flow control,
driven by a new `category` field on NODE_META so a new type can't go
uncategorized, and a `groupNodeTypesByCategory` helper shared by both
menus so they stay in lockstep.
No triggers/events category: in wacrm a flow is triggered by
flow-level config (trigger_type), not a canvas node, so `start` lives
under Flow control.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworks the flow editor (/flows/[id]) to the Flow Builder design
handoff, adapted to wacrm's existing tokens, components, and
light+dark theming. Keeps all behaviour — React Flow canvas,
drag-to-connect, typed config forms, validation, save/activate/delete,
per-browser view persistence, mobile-forces-list.
- shared.tsx: per-node-type color system (nodeColors) + one-line
blurbs, and a shared NodeIconChip component. Label color blends
toward --foreground via color-mix so it stays readable in both modes.
- header.tsx: compact single-row toolbar (back / icon / inline name /
status chip / edited dot · Runs / Delete / Activate / Save) plus a
subtle inline description note.
- flow-editor-shell.tsx: app-like full-height layout (toolbar → mode
row → stage → validation bar) with a segmented Canvas/List control
and a complete node-type legend derived from NODE_META.
- flow-canvas.tsx: per-type colored node cards, branch ports, dot-grid
background, rounded controls/minimap, top-left primary Add-node menu,
colored inspector header.
- flow-builder.tsx: list rows tied to the same per-type color language.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce a light/dark MODE axis orthogonal to the existing accent
themes. Neutral surface tokens now live in html[data-mode="light|dark"]
blocks; accent tokens (--primary*, --ring, --chart-1, --sidebar-primary*)
stay in html[data-theme] blocks, so any accent works in either mode.
- globals.css: split neutrals (mode) from accent (theme); add light
palette; dark values unchanged (dark stays the default).
- lib/themes.ts: Mode type, DEFAULT_MODE=dark, MODE_STORAGE_KEY, isMode.
- use-theme.tsx: provider owns mode/setMode/toggleMode + cross-tab sync.
- layout.tsx: boot script applies persisted mode before first paint
(no flash); colorScheme "dark light".
- mode-toggle.tsx (header sun/moon) + themed-toaster.tsx (mode-aware).
- appearance-panel.tsx: light/dark control + updated copy.
- Tokenize hardcoded neutral colors (slate/white/gray) -> semantic
tokens across the app so every surface flips with mode. Flow canvas
and message-bubble timestamps made mode-aware too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the trilogy. Three high-value gaps from PRs 2a/2b addressed:
1. Canvas users couldn't save. The Header (with Save / Activate /
Pause / Delete buttons) lived inside FlowBuilder so canvas-only
users had to toggle to List to persist any edit. Lifted into a
new components/flows/header.tsx that consumes useFlowEditor()
and is mounted ONCE in FlowEditorShell, above both views.
2. Validator issues weren't clickable from canvas. The
ValidationPanel was also FlowBuilder-internal. Lifted into
components/flows/validation-panel.tsx + mounted in the shell,
pinned sticky-bottom in either view. Clicks now go through the
editor context's new requestFlash(key) — both views subscribe:
- List view derives an "expanded with flash" set + an effect
scrolls the row into view. The flash class is rendered when
the card's key matches context.flashKey.
- Canvas view's pan-on-flash effect calls
reactFlow.setCenter() to bring the node into the viewport,
and the card renders an amber ring while flashed.
The flash auto-clears in the context after 1600ms; rapid
clicks on different issues cancel the previous timeout.
3. Canvas was unusable on phones — handles are ~10px and live
finger-drags between them aren't a practical workflow. Added
a useMatchMedia('(max-width: 767px)') guard in the shell that
forces list view and hides the toggle below the breakpoint.
The user's preferred view is preserved (just shadowed) so it
comes back when they widen.
Plus minor cleanup: the canvas's empty-state used to direct users
to switch to List view to add a node, but the floating "+" button
shipped in PR 2b made that stale — now shows the same Add-node
dropdown inline.
flow-builder.tsx shrinks by another ~370 lines (Header + StatusBadge
+ ValidationPanel + IssueLine + the local flashedKey + jumpToNode
all moved or replaced by context subscription).
No new tests — the change is structural + UI-only. Existing 319
still pass; typecheck, lint, build all clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the editing-affordance gap from PR 2a. The canvas is now in
full parity with the list view for everything that fits in a node
graph: connections, deletes, and adding nodes all work on-canvas.
Adds:
- Per-slot Handle components on the custom node renderer. Single-
outgoing types get one handle on the right edge; condition gets
`true` / `false` handles; send_buttons and send_list render slot
rows inside the card with one handle each, so the user can SEE
which button or list row they're wiring. Every non-start node
gets a target handle on the left.
- Drag-to-connect (`onConnect`). Looks up the source node, dispatches
the right config patch via the new `applyEdgeConnection(node,
sourceHandle, target)` helper, and routes through updateNodeConfig.
Rejects self-loops silently (a button whose target is its own node
= infinite reprompt).
- Keyboard delete on nodes (Backspace + Delete, both for cross-OS
parity). Routes through removeNode, which now ALSO unlinks every
inbound reference via the new `unlinkNodeReferences(nodes, key)`
helper. No more dangling arrows after a delete — the validator
previously flagged these as errors and the user had to chase them
down manually.
- Edge delete. Selecting an edge + Delete clears just that slot's
next_node_key (rather than removing anything), via the same
applyEdgeConnection helper passing "" as the target.
- Floating "+ Add node" button (React-Flow Panel, bottom-right).
Same dropdown as the list view; drops the new node at the visible
viewport center via reactFlow.screenToFlowPosition.
New pure helpers in src/lib/flows/edges.ts (no React, fully unit-
tested — 18 new tests, 319 total passing):
- outgoingSlots(node) — list of {id, label} per outgoing slot.
- applyEdgeConnection(node, sourceHandle, targetKey) — config patch
for a drag-to-connect, or null for unrecognised handles.
- unlinkNodeReferences(nodes, deletedKey) — clears every direct +
nested reference to deletedKey. Returns nodes-by-identity when
no reference matches to avoid needless re-renders.
The hook's removeNode is the only consumer of unlinkNodeReferences
right now, which means LIST-view deletes also stop leaving dangling
refs — strict improvement for both views.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds on PR 1's read-only canvas. This PR brings the canvas to ~75%
of list-view parity: drag-to-reposition (persisted) and click-to-edit
via a side panel that mounts the same per-node forms the list view
uses. Drag-to-connect + add/delete from canvas land in PR 2b.
Architecture (the bulk of the diff):
- Extracted the per-node forms (SendButtonsForm, SendListForm,
SendMediaForm, ConditionForm, SetTagForm + inline forms for the
smaller types) plus the shared field components (TextRow,
NextNodeRow, NodeKeySelect) from flow-builder.tsx into
src/components/flows/forms/. Both views now mount the SAME
NodeConfigForm dispatcher so behaviour can't drift between
"what the list editor shows" and "what the canvas side panel
shows".
- Lifted all editor state into a context/hook
(src/components/flows/flow-editor-state.tsx) with a
FlowEditorProvider that wraps both views in flow-editor-shell.
Single source of truth: toggling Canvas ⇄ List never resets
unsaved edits, and a drag on the canvas updates the same nodes
array the list view reads.
- flow-builder.tsx shrinks from ~2400 lines to ~900 (extracted
forms) and stops owning state (now consumes via useFlowEditor()).
List-only UI state — expanded cards, scroll refs, jump-to-node
flash — stays internal.
Canvas additions:
- onNodeDragStop writes the new position to context via
updateNodePosition (flips dirty; serialized in the existing PUT
/api/flows/[id] body — the route already accepts position_x /
position_y per migration 010).
- onNodeClick opens a controlled shadcn Sheet (right-side panel)
with the per-node form, plus "Set as entry" and "Delete node"
buttons. Esc / overlay / close button all close.
What's intentionally still NOT in this PR (PR 2b):
- Per-slot Handle components on the custom node renderer.
- Drag-to-connect handler that maps target → updated
next_node_key for the right slot.
- Floating "+" button to add nodes from canvas.
- Keyboard / context-menu delete.
Tests: 9 new pure-helper tests (uniqueNodeKey + defaultConfigFor
self-consistency). Hook integration tests are deferred — would
require @testing-library/react + jsdom infra. Existing 271 tests
still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The second half of the user feedback that drove send_media: the
linear-list editor reads as a checklist, not a branching flow, which
made conversational structures hard to understand at a glance. This
introduces a canvas view alongside the list — boxes + arrows, drag-
zoom-pan, labeled edges per branch slot. Canvas is the new default;
the list stays as a fallback toggle (per-browser preference,
persisted to localStorage).
PR 1 scope is intentionally read-only: render, don't edit. The
foundation is testable in isolation before adding drag-to-persist,
side-panel editing, and drag-to-connect (PR 2) and polish like
keyboard shortcuts, mobile fallback, and validator integration
(PR 3).
Pieces:
- New @xyflow/react (~30 KB gz) + @dagrejs/dagre (auto-layout for
pre-existing flows whose position_x / position_y are all zero —
flow_nodes already reserved those columns in migration 010
precisely for this).
- Shared module src/components/flows/shared.tsx lifts NodeType,
BuilderNode, NODE_META, summarizeNode, truncate so both views
render nodes identically.
- Pure helpers (no React, fully unit-tested):
src/lib/flows/edges.ts — derive labeled canvas edges per
outgoing slot (button, list row,
condition branch)
src/lib/flows/layout.ts — dagre wrapper + shouldAutoLayout
guard so manually-positioned flows
never get rearranged on open
- FlowCanvas — client component, custom node renderer matching the
list view's collapsed-card styling, MiniMap + Controls, attribution
hidden.
- FlowEditorShell — owns the Canvas / List toggle, conditionally
mounts the right view.
21 new unit tests (layout + edges); existing 271 still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>