The displaySchema useMemo was placed after conditional early returns,
violating React's Rules of Hooks and causing hook order mismatch errors
when the component toggled between rendering null and its content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rapid mouse movement across the canvas fires multiple mouseout events
per frame, each triggering ~150 Zustand selector evaluations. This
coalesces hover updates into one per animation frame and skips them
entirely during canvas panning/dragging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap InlineParameterPanel in React.memo
- Simplify handleToggleParams in all 5 generate nodes
- Use local isParamsExpanded instead of re-querying store
- Eliminates O(n) nodes.find() on every toggle click
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap ModelParameters and ParameterInput in React.memo
- Add useMemo on sortedSchema and displaySchema computations
- RAF-debounce ResizeObserver with redundancy guard
- Stabilize onChange callback (pass name as prop, not closure)
- Eliminates re-renders during canvas pan/zoom/drag
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The capabilities prop doesn't exist on ModelSearchDialogProps. Use the
correct initialCapabilityFilter="image" prop instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap the ResizeObserver callback in requestAnimationFrame with
cancellation to coalesce rapid resize events into one setNodes call
per frame. Cleanup cancels any pending RAF on unmount.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
useViewport() re-renders on every pan AND zoom. The narrow selector
useStore((s) => s.transform[2]) only fires when zoom changes, eliminating
unnecessary group overlay re-renders during panning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When dimmedNodeIds is empty and no node has a stale switch-dimmed class,
return the nodes array directly (same reference, zero iteration). The
.map() + regex only runs when dimming is actually active, which is the
uncommon case for most workflows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip Zustand set() when hoveredNodeId already matches the incoming value.
Prevents redundant store updates when the mouse stays on the same node
or when onMouseLeave fires with null when already null.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The `.react-flow__node:not(.switch-dimmed)` rule applied a transition to
every node DOM element unnecessarily. The `.switch-dimmed` rule already
handles the transition for dimming. Removing this makes undimming instant
(snappier) and eliminates a transition declaration from every node.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace underline tab bar with pill-style segmented tabs. Replace radio
button lists in Canvas tab with compact segmented button groups. Add
backdrop blur, click-outside-to-close, rounded-lg inputs, and improved
text contrast (neutral-400 instead of neutral-500 for descriptions).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restructure floating group label to use bottom-anchored positioning so
inverse-zoom scaling grows upward instead of overlapping group content.
Switch from useReactFlow().getViewport() to useViewport() hook for
proper reactive zoom updates. Change group name to double-click to edit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add beforeunload handler that prompts the user when they try to close
the tab or navigate away while there are unsaved changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Increase resize handle hit area from 8px to 16px (edge handles) and
from 3px to 5px (corner handles) so they're easier to grab.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace subscribing to the full nodes array with a Zustand selector that
returns only the single selected node. The selector runs the filter
internally but only causes a re-render when the selected node reference
changes, rather than on every store mutation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split the monolithic useWorkflowStore() destructure into useShallow for
data fields (nodes, edges, groups, dimmedNodeIds, etc.) and individual
selectors for action references. Actions are stable in Zustand and don't
trigger re-renders, while useShallow ensures the component only re-renders
when data values actually change rather than on every store mutation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace inline closure callbacks (onCustomTitleChange, onCommentChange,
onRun, onExpand) with stable handler patterns that pass nodeId. Replace
titlePrefix ReactNode prop with primitive provider string. Replace data
object prop with individual primitive fields (isInLockedGroup, isExecuting,
focusedCommentNodeId). This prevents N unnecessary re-renders per frame
during canvas panning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove solid color header bar from groups, replace with floating rounded-rectangle label at top-left
- Add floating controls toolbar (color picker, lock, delete) at top-right with bg-neutral-800/80 backdrop
- Both label and controls use transform: scale(1/zoom) for constant screen size regardless of canvas zoom
- Remove headerHeight offset from createGroup in workflowStore (no longer reserves vertical space for header)
- Keep all existing group interactions: rename, recolor, lock, delete, drag, resize
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When pasting nodes with inline parameters expanded, the copied
style.height (which included settings panel contribution) was preserved,
causing BaseNode's ResizeObserver to add panel height again on top of the
already-inflated height. Now pasteNodes() resets height to type defaults
while preserving user-set width.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add pannable prop to MiniMap for click-to-pan viewport navigation
- Add zoomable prop to MiniMap for scroll-to-zoom on minimap
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaced checkbox inputs with toggle switches for image embedding and
inline parameters settings. Renamed "Inline Parameters" to "Show model
settings on nodes".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The hidden file input was outside the inner dialog div (which has
stopPropagation) but inside the backdrop div (which calls onClose).
When the OS file picker closed, the backdrop click handler fired,
unmounting the modal before FileReader could complete.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
loadWorkflow now sets showQuickstart: false so the modal doesn't
persist over a loaded project. Previously the isCanvasEmpty gate
handled this implicitly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When opening the welcome modal over an existing project and choosing
"New Project", the canvas now clears before showing the project setup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add click handler to Header.tsx logo/title that calls setShowQuickstart(true)
- Wrap logo and title in button element with hover feedback
- Add onClick to WelcomeModal backdrop that calls onClose
- Add stopPropagation to inner dialog to prevent backdrop close on modal clicks
- Remove isCanvasEmpty gate from WelcomeModal rendering in WorkflowCanvas.tsx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change outer wrapper bg from #2a2a2a to bg-neutral-800 to match node body,
eliminating visible color mismatch at rounded corners
- Remove overflow-hidden from InlineParameterPanel (was clipping fields)
- Use min(180px, 100%) in grid minmax so columns shrink to fit narrow nodes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add bg-[#2a2a2a] rounded-lg to outer wrapper when settings expanded to
fill gap behind rounded corners at node/panel junction
- Reduce grid min column width from 240px to 180px so fields fit in narrow nodes
- Add overflow-hidden to InlineParameterPanel content div as safety net
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert trackedSettingsHeight from useState to useRef to eliminate a
race condition between two useLayoutEffect hooks. On expand, both the
rAF-based measure (Effect 1) and the ResizeObserver (Effect 2) were
adding the panel height — a double-add. On collapse only one subtracted,
leaving residual height each cycle.
The fix removes the expand branch from Effect 1 (now collapse-only) and
lets the ResizeObserver solely handle initial and dynamic measurement.
Using a ref also prevents the ResizeObserver from being torn down and
recreated on every height change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
calculateAspectFitSize was receiving the total node height (content +
settings panel), producing incorrect aspect-fit dimensions when settings
were expanded. Now subtracts trackedSettingsHeight before calculating
and re-adds it when applying the final height.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch BaseNode wrapper from `display: contents` to flex layout when
settings are expanded so NodeResizer handles span the full visual height.
Height management is now centralized in BaseNode via useLayoutEffect +
ResizeObserver instead of per-node useEffects that compounded height.
- BaseNode: track settings panel height, adjust node dimensions on
expand/collapse, observe dynamic content changes
- InlineParameterPanel: remove animation (instant show/hide), remove
selection ring logic (now handled by BaseNode outer container)
- GenerateImageNode/GenerateVideoNode: remove buggy height management
useEffects and parameterPanelRef
- All 5 node types: remove `selected` prop from InlineParameterPanel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move InlineParameterPanel from BaseNode children to a settingsPanel prop
rendered as a sibling of the bordered div. This fixes:
- Notch between preview and settings (width mismatch from border inset)
- Selection ring overlap/gap at the junction (clip-path on separate overlay)
- Bottom border and rounding when settings expanded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add settingsExpanded prop to BaseNode. When true + selected, the node
div uses rounded-t-lg instead of rounded-lg so its ring has no bottom
rounding. Combined with InlineParameterPanel's overlapping ring with
rounded-b-lg, this creates a seamless continuous selection outline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Revert min-h-full back to h-full (which broke preview layout). Instead,
InlineParameterPanel uses -mt-[3px] negative margin when expanded+selected
to overlap with BaseNode's ring bottom edge, creating a seamless continuous
blue outline around both preview and settings areas.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change BaseNode inner div from h-full to min-h-full so it grows to
encompass the settings panel. The ring now wraps the entire area as
one continuous outline instead of two separate rings with a gap.
Removed the now-unnecessary selected prop from InlineParameterPanel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
InlineParameterPanel now accepts a selected prop and draws a matching
ring-2 ring-blue-500/40 around the settings area when the node is
selected and parameters are expanded. Passed from all five node types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Settings area uses lighter grey (#2a2a2a), field inputs use darker
grey (#1a1a1a) with no borders, matching the reference design.
Applied across InlineParameterPanel, GenerateImageNode, LLMGenerateNode,
and ModelParameters.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Chevron toggle now shows "Settings" text and has no background when
collapsed so it floats below the node edge. Solid bg only appears on
the expanded content area. Preview container switches from rounded-lg
to rounded-t-lg when parameters are expanded so the settings area
connects seamlessly to the image above.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Solid dark background attached to image preview, labels on same line
as fields, slightly larger text (11px), darker rounded inputs matching
the reference design. Applied consistently across Gemini controls,
LLM controls, and ModelParameters (external provider fields).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Center max-width container, clip rounded bottom corners, improve
chevron visibility with subtle background, remove duplicate model
selector from image/video nodes (already in FloatingNodeHeader),
and unify control sizing across Gemini and LLM selects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Apply max-w-[280px] to parameter fields container
- Prevents fields from stretching too wide on large nodes
- Maintains compact, readable parameter inputs
- Replace useState with useSyncExternalStore for shared reactive state
- Add subscriber system to notify all hook consumers when value changes
- Fix bug where toggling setting required page refresh to see updates
- All generation nodes now update immediately when setting changes
- Import useInlineParameters hook and InlineParameterPanel component in all 3 nodes
- Add LLM_PROVIDERS and LLM_MODELS constants to LLMGenerateNode
- Add collapse state computation and toggle handlers for all 3 nodes
- LLMGenerateNode: render inline controls for provider, model, temperature, max tokens with compact styling
- GenerateAudioNode: render model selector and ModelParameters inline
- Generate3DNode: render model selector and ModelParameters inline
- Hide top ModelParameters when inline enabled (prevents duplication)
- All nodes use unified compact styling matching GenerateImageNode Gemini controls
- Import useInlineParameters hook and InlineParameterPanel component
- Add collapse state computation and toggle handler for both nodes
- Render InlineParameterPanel after content area when inline enabled
- GenerateImageNode includes Gemini-specific controls (model, aspect ratio, resolution, search toggles) with unified styling
- GenerateImageNode includes ModelParameters for external providers
- GenerateVideoNode includes model selector and ModelParameters
- Add node height management effects that measure parameter panel and resize nodes
- Conditional rendering: inline params only show when global setting enabled
- Import useInlineParameters hook in ControlPanel and ProjectSetupModal
- Define GENERATION_NODE_TYPES constant for conditional hiding logic
- Hide ControlPanel for generation nodes when inline parameters enabled
- ControlPanel still shows for easeCurve and conditionalSwitch regardless of setting
- Add inline parameters toggle to ProjectSetupModal Project tab
- Toggle labeled 'Inline Parameters' with descriptive help text
- Setting persists to localStorage and applies immediately
- Add parametersExpanded field to 5 generation node data types (NanoBanana, GenerateVideo, Generate3D, GenerateAudio, LLMGenerate)
- Create useInlineParameters hook for localStorage-backed global setting (default: false)
- Create InlineParameterPanel component with collapsible UI and chevron toggle
- All TypeScript compiles without new errors