This folder contains reusable UI primitives that follow semantic design principles across the entire application, not just the editor. These primitives are the foundation for building consistent, maintainable, and accessible user interfaces.
The UI primitives provide:
- Semantic props: Explicit state communication (e.g.,
changed,invalid,density) - Theme-driven styling: All visuals reference
useTheme()for consistency - No DOM reach-in: Components manage their own styles without descendant selectors
- Reusable & modular: Each primitive handles its own visuals via
sxor scoped styles - Context-aware: Components can adapt to different contexts (node vs inspector) when needed
- FlexColumn - Vertical flex container with gap spacing
- FlexRow - Horizontal flex container with gap spacing
- Stack - Vertical stack with spacing between children
- Container - Standardized container with consistent padding
- Card - Card component with elevation and padding variants
- Panel - Panel component with header, content, and footer sections
- Text - Flexible text component with size, color, and weight variants
- Label - Semantic label for form fields with required indicator
- Caption - Small secondary text for hints and captions
- NodeTextField - Text input field with semantic props
- NodeSwitch - Boolean toggle switch
- NodeSelect - Dropdown select with options
- NodeMenuItem - Menu item for use with NodeSelect
- NodeSlider - Range slider with visual feedback
- Dialog - Standardized modal dialog with consistent styling and optional action buttons
- DialogActionButtons - Standardized confirm/cancel button pairs for dialogs
- EditorButton - Button with density variants
- ToolbarIconButton - Icon button with tooltip for toolbar actions
- NavButton - Navigation button with icon and optional label
- CreateFab - Extended FAB for create/add actions
- PlaybackButton - Audio/video playback controls (play/pause/stop)
- RunWorkflowButton - Run/stop workflow actions
- ExpandCollapseButton - Toggle button for expand/collapse content
- RefreshButton - Refresh/reset button with loading state
- SelectionControls - Bulk selection controls (select all/clear)
- ViewModeToggle - Toggle button group for view mode switching
- StateIconButton - Versatile icon button with loading, active, and disabled states
- LabeledToggle - Toggle button with icon, label, and expand/collapse indicator
- CircularActionButton - Circular action button for primary actions and FABs
- ActionButtonGroup - Flexible container for grouping action buttons with consistent spacing
- EditorMenu - Context menu with consistent styling
- EditorMenuItem - Menu item for EditorMenu
The spacing module provides consistent spacing constants and utilities:
import { SPACING, GAP, PADDING, getSpacingPx } from "../ui_primitives";
// Use predefined spacing constants
const mySpacing = SPACING.md; // 8px (2 * 4px)
const myGap = GAP.normal; // 8px
const myPadding = PADDING.comfortable; // 12px
// Convert to pixel string
const pixels = getSpacingPx(2); // "8px"Wrap your component tree with EditorUiProvider to set the styling scope:
import { EditorUiProvider } from "../ui_primitives";
// Default "node" scope (compact density)
<EditorUiProvider>
<YourComponent />
</EditorUiProvider>
// Inspector scope (normal density)
<EditorUiProvider scope="inspector">
<YourComponent />
</EditorUiProvider>import {
FlexColumn,
FlexRow,
Stack,
Container
} from "../ui_primitives";
// Vertical layout with gap
<FlexColumn gap={2} padding={3}>
<Typography>Item 1</Typography>
<Typography>Item 2</Typography>
</FlexColumn>
// Horizontal layout with alignment
<FlexRow gap={1.5} align="center" justify="space-between">
<Button>Left</Button>
<Button>Right</Button>
</FlexRow>
// Stack with divider
<Stack spacing={2} divider={<Divider />}>
<MenuItem>Option 1</MenuItem>
<MenuItem>Option 2</MenuItem>
</Stack>
// Padded container
<Container padding="comfortable" scrollable maxWidth={800}>
<Content />
</Container>import { Card, Panel } from "../ui_primitives";
// Card with hover effect
<Card variant="elevated" hoverable padding="normal">
<Typography>Card content</Typography>
</Card>
// Panel with header and footer
<Panel
title="Settings"
subtitle="Configure your preferences"
headerAction={<Button>Save</Button>}
footer={<Button>Apply</Button>}
>
<SettingsForm />
</Panel>import { Text, Label, Caption } from "../ui_primitives";
// Flexible text component
<Text size="big" color="primary" weight={600}>
Important text
</Text>
// Form label
<Label required htmlFor="email">Email Address</Label>
// Helper text
<Caption color="secondary" size="small">
Enter your email address
</Caption>import {
NodeTextField,
NodeSwitch,
NodeSelect,
NodeMenuItem,
NodeSlider,
EditorButton
} from "../ui_primitives";
// Text input with semantic props
<NodeTextField
value={value}
onChange={(e) => onChange(e.target.value)}
changed={hasChanged} // Shows visual indicator
invalid={hasError} // Shows error state
multiline
/>
// Boolean switch
<NodeSwitch
checked={isEnabled}
onChange={(e) => onChange(e.target.checked)}
changed={hasChanged}
/>
// Select dropdown
<NodeSelect
value={selected}
onChange={(e) => onChange(e.target.value)}
changed={hasChanged}
>
<NodeMenuItem value="opt1">Option 1</NodeMenuItem>
<NodeMenuItem value="opt2">Option 2</NodeMenuItem>
</NodeSelect>
// Slider
<NodeSlider
value={volume}
onChange={(e, val) => onChange(val)}
min={0}
max={100}
changed={hasChanged}
/>
// Button
<EditorButton
onClick={handleClick}
density="compact"
>
Click me
</EditorButton>
// Dialog with auto-generated action buttons
<Dialog
open={isOpen}
onClose={handleClose}
title="Confirm Action"
onConfirm={handleConfirm}
confirmText="Save"
cancelText="Cancel"
>
<DialogContent>
Are you sure you want to continue?
</DialogContent>
</Dialog>
// Dialog with manual content and no action buttons
<Dialog
open={isOpen}
onClose={handleClose}
title="Information"
>
<DialogContent>
<Typography>Custom dialog content</Typography>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
</DialogActions>
</Dialog>
// Dialog with destructive action
<Dialog
open={isOpen}
onClose={handleClose}
title="Delete Item"
onConfirm={handleDelete}
confirmText="Delete"
destructive={true}
>
<DialogContent>
This action cannot be undone.
</DialogContent>
</Dialog>For custom components, use the shared utilities:
import {
editorClassNames,
reactFlowClasses,
cn,
stopPropagationHandlers
} from "../ui_primitives";
// Prevent ReactFlow drag
<div className={cn(reactFlowClasses.nodrag, myClassName)}>
// Add nowheel when focused to prevent zoom capture
<textarea className={cn(reactFlowClasses.nodrag, isFocused && reactFlowClasses.nowheel)}>
// Stop event propagation for interactive containers
<div {...stopPropagationHandlers}>
<MyInput />
</div>All primitives support semantic props for state-based styling:
Shows a visual indicator (typically a right border in primary color) when the value differs from the default. This helps users identify modified values.
Shows an error state (typically a red border) when validation fails. This provides immediate feedback for invalid input.
Controls the size and spacing of the component:
"compact": Tighter spacing, smaller fonts (default for node scope)"normal": More generous spacing, larger fonts (default for inspector scope)
All visuals reference useTheme() for colors, typography, spacing, and dimensions:
const theme = useTheme();
sx={{
fontSize: theme.fontSizeSmall,
backgroundColor: theme.vars.palette.background.paper,
borderColor: theme.vars.palette.primary.main
}}Components manage their own styles without relying on descendant selectors or parent overrides:
// ✅ Good - component owns its styling
<NodeTextField
sx={{ height: 24 }}
changed={hasChanged}
/>
// ❌ Bad - parent reaching into component
<div sx={{ "& fieldset": { border: "none" } }}>
<TextField />
</div>Each primitive is self-contained and can be used interchangeably:
// Can swap implementations without changing parent
<NodeTextField {...commonProps} />
<NodeSelect {...commonProps} />Primitives can adapt to context but keep context dependence minimal:
const scope = useEditorScope(); // "node" or "inspector"
const fontSize = scope === "inspector"
? theme.fontSizeSmall
: theme.fontSizeTiny;- Use primitives for all input controls across the application
- Use semantic props (
changed,invalid,density) to communicate state - Reference
useTheme()for all visual values - Use
reactFlowClasses.nodragon interactive elements inside nodes - Test primitives in both node and inspector contexts
- Add global
.Mui*selector overrides for primitives - Use descendant selectors (e.g.,
fieldset,.MuiInputBase-root) to style primitives - Create new
styled()components - usesxor Emotioncssinstead - Duplicate hover/focus/selected rules - use semantic props instead
- Add context-specific styling inline - use the context provider
One file per primitive, plus the shared token and utility modules. An excerpt:
ui_primitives/
├── index.ts # Re-exports every primitive and utility
├── NodeSlider.tsx # Slider primitive
├── TextInput.tsx # Standalone text input
├── SelectField.tsx # Standalone select
├── tokens.ts # TYPOGRAPHY, MOTION, Z_INDEX, BORDER_RADIUS
├── spacing.ts # SPACING, GAP, PADDING, MARGIN
├── … # one file per remaining primitive
└── README.md # This file
editor_ui/ # Re-exported through ui_primitives/index.ts
├── NodeTextField.tsx # Text input primitive
├── NodeSwitch.tsx # Switch primitive
├── NodeSelect.tsx # NodeSelect and NodeMenuItem
└── EditorButton.tsx # Button primitive
When migrating existing components to use primitives:
-
Replace raw MUI components with primitives:
// Before <TextField /> // After <NodeTextField />
-
Replace inline styles with semantic props:
// Before <TextField sx={{ "& fieldset": { borderRightWidth: 2, borderRightColor: theme.vars.palette.primary.main } }} /> // After <NodeTextField changed={hasChanged} />
-
Remove DOM reach-in patterns:
// Before <Box sx={{ "& .MuiInputBase-root": { height: 24 } }}> <TextField /> </Box> // After <NodeTextField sx={{ "& .MuiInputBase-root": { height: 24 } }} />
-
Add context provider at the component tree root if needed:
// Before <MyComponent /> // After <EditorUiProvider scope="inspector"> <MyComponent /> </EditorUiProvider>
All primitives should have comprehensive tests:
import { render, screen } from "@testing-library/react";
import { NodeTextField } from "../ui_primitives";
describe("NodeTextField", () => {
it("shows changed indicator when changed prop is true", () => {
const { container } = render(
<NodeTextField value="test" changed={true} />
);
// Assert changed styling
});
});- Editor UI README - Editor-specific primitives
- Components AGENTS.md - Component architecture guide
- Stores AGENTS.md - State management patterns