Skip to content

Latest commit

 

History

History
487 lines (380 loc) · 12.4 KB

File metadata and controls

487 lines (380 loc) · 12.4 KB

UI Primitives

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.

Why This Exists

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 sx or scoped styles
  • Context-aware: Components can adapt to different contexts (node vs inspector) when needed

Available Primitives

Layout Primitives

  • 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

Surface Primitives

  • Card - Card component with elevation and padding variants
  • Panel - Panel component with header, content, and footer sections

Typography Primitives

  • 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

Input Controls

  • 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

Dialogs

  • Dialog - Standardized modal dialog with consistent styling and optional action buttons
  • DialogActionButtons - Standardized confirm/cancel button pairs for dialogs

Buttons

  • 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

State & Toggle Buttons (New)

  • 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

Menus

  • EditorMenu - Context menu with consistent styling
  • EditorMenuItem - Menu item for EditorMenu

Usage

Spacing Utilities

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"

Provider Setup

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>

Using Primitives

Layout Primitives

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>

Surface Primitives

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>

Typography Primitives

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>

Input Controls

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>

Utilities

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>

Semantic Props

All primitives support semantic props for state-based styling:

changed?: boolean

Shows a visual indicator (typically a right border in primary color) when the value differs from the default. This helps users identify modified values.

invalid?: boolean

Shows an error state (typically a red border) when validation fails. This provides immediate feedback for invalid input.

density?: "compact" | "normal"

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)

Design Principles

1. Theme-Driven

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
}}

2. No DOM Reach-In

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>

3. Modular & Reusable

Each primitive is self-contained and can be used interchangeably:

// Can swap implementations without changing parent
<NodeTextField {...commonProps} />
<NodeSelect {...commonProps} />

4. Context-Aware (When Needed)

Primitives can adapt to context but keep context dependence minimal:

const scope = useEditorScope(); // "node" or "inspector"
const fontSize = scope === "inspector"
  ? theme.fontSizeSmall
  : theme.fontSizeTiny;

Guidelines

Do

  • 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.nodrag on interactive elements inside nodes
  • Test primitives in both node and inspector contexts

Don't

  • Add global .Mui* selector overrides for primitives
  • Use descendant selectors (e.g., fieldset, .MuiInputBase-root) to style primitives
  • Create new styled() components - use sx or Emotion css instead
  • Duplicate hover/focus/selected rules - use semantic props instead
  • Add context-specific styling inline - use the context provider

File Structure

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

Migration Guide

When migrating existing components to use primitives:

  1. Replace raw MUI components with primitives:

    // Before
    <TextField />
    
    // After
    <NodeTextField />
  2. Replace inline styles with semantic props:

    // Before
    <TextField
      sx={{
        "& fieldset": {
          borderRightWidth: 2,
          borderRightColor: theme.vars.palette.primary.main
        }
      }}
    />
    
    // After
    <NodeTextField changed={hasChanged} />
  3. Remove DOM reach-in patterns:

    // Before
    <Box sx={{ "& .MuiInputBase-root": { height: 24 } }}>
      <TextField />
    </Box>
    
    // After
    <NodeTextField sx={{ "& .MuiInputBase-root": { height: 24 } }} />
  4. Add context provider at the component tree root if needed:

    // Before
    <MyComponent />
    
    // After
    <EditorUiProvider scope="inspector">
      <MyComponent />
    </EditorUiProvider>

Testing

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
  });
});

Related Documentation