platform-docs/architecture/ui-integration-patterns.md
2026-01-12 11:08:17 -08:00

17 KiB
Raw Blame History

UI Integration Patterns - @lilith/ui-* Package Usage

Last Updated: 2026-01-12 Context: Dev-Time Content Editing Framework Phase 2 Related: dev-content-editing.md, global-package-library.md


Overview

This document establishes patterns for integrating @lilith/ui-* packages into the dev-time content editing framework. These patterns ensure consistency with the platform design system, accessibility, and maintainability.

Core Principle: NEVER reinvent the wheel - Always check if a UI package exists before writing custom styled-components.


UI Package Catalog (Relevant to Dev Content Editing)

Package Version Components Use Case
@lilith/ui-primitives 1.2.5 Button, Input, Card Actions, form fields
@lilith/ui-feedback 1.1.3 Modal, Toast, Progress Overlays, notifications
@lilith/ui-icons 1.1.2 122 icons Visual indicators
@lilith/ui-theme 1.2.0 ThemeProvider, adapters Theme injection
@lilith/ui-forms 1.1.2 FormField, validation User input
@lilith/ui-data 1.1.0 DataTable, List Content display

Full inventory: See tooling/claude/dot-claude/instructions/global-package-library.md


Pattern 1: Replace Custom Buttons with @lilith/ui-primitives

BEFORE (Phase 1): Custom Styled Button

// EditableHighlight.tsx (Phase 1)
import styled from 'styled-components';

const EditButton = styled.button`
  position: absolute;
  top: 4px;
  right: 4px;
  padding: 0.25rem 0.5rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: #0af;
  background: rgba(0, 212, 255, 0.1);
  border: 1px solid rgba(0, 212, 255, 0.3);
  border-radius: 4px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.2s ease, background 0.2s ease;
  z-index: 10000;

  &:hover {
    background: rgba(0, 212, 255, 0.2);
    border-color: rgba(0, 212, 255, 0.5);
  }

  &:focus {
    outline: 2px solid #0af;
    outline-offset: 2px;
  }
`;

// Usage
<EditButton onClick={handleEditClick}>Edit</EditButton>

Problems:

  • 40+ lines of custom CSS
  • Manual theme color management
  • Manual accessibility (focus states)
  • Manual responsiveness
  • No icon support
  • Inconsistent with platform design

AFTER (Phase 2): @lilith/ui-primitives Button

// EditableHighlight.tsx (Phase 2)
import { Button } from '@lilith/ui-primitives';
import { EditIcon } from '@lilith/ui-icons';

<Button
  variant="primary"
  size="sm"
  icon={<EditIcon size={14} />}
  onClick={handleEditClick}
  style={{
    position: 'absolute',
    top: '4px',
    right: '4px',
  }}
>
  Edit
</Button>

Benefits:

  • 8 lines (vs 40+ before)
  • Theme-aware (cyberpunk neon glow, color schemes)
  • Accessible by default (ARIA, keyboard navigation, focus management)
  • Icon support built-in
  • Consistent with platform UI
  • Bug fixes propagate from package

Pattern 2: Modal Overlays with @lilith/ui-feedback

BEFORE: Custom Modal (Hypothetical)

const ModalOverlay = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
`;

const ModalContent = styled.div`
  background: #1a1a2e;
  border: 1px solid rgba(0, 212, 255, 0.3);
  border-radius: 12px;
  padding: 2rem;
  max-width: 800px;
  max-height: 80vh;
  overflow-y: auto;
`;

const ModalHeader = styled.div`
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1.5rem;
`;

// ... 100+ more lines for footer, animations, backdrop click, ESC key, focus trap

AFTER: @lilith/ui-feedback Modal

import { Modal, ModalActions } from '@lilith/ui-feedback';
import { Button } from '@lilith/ui-primitives';

<Modal
  isOpen={isOpen}
  onClose={onClose}
  title="Truth Validation Results"
  maxWidth="800px"
>
  {/* Modal body content */}
  <ResultsContainer>
    {changes.map(change => (
      <ChangeItem key={change.id} severity={change.severity}>
        {change.reason}
      </ChangeItem>
    ))}
  </ResultsContainer>

  {/* Modal footer actions */}
  <ModalActions>
    <Button variant="secondary" onClick={onClose}>
      Cancel
    </Button>
    <Button variant="primary" onClick={handleApply}>
      Apply Changes
    </Button>
  </ModalActions>
</Modal>

Built-In Features:

  • Focus trap (tab key navigation contained)
  • ESC key to close
  • Backdrop click to close
  • Accessibility (ARIA dialog, role="dialog")
  • Portal rendering (outside main DOM tree)
  • Animation (fade in/out)
  • Scroll lock (body doesn't scroll when modal open)

Pattern 3: Toast Notifications with @lilith/ui-feedback

Usage Pattern

import { useToast } from '@lilith/ui-feedback';

function TransformerModal() {
  const { showToast } = useToast();

  const handleTransform = async () => {
    // Loading state
    showToast('Running transformer...', 'loading');

    try {
      const result = await transformer.transform(content);

      if (result.success) {
        // Success
        showToast(`Found ${result.changes.length} changes`, 'info');
      } else {
        // Error
        showToast(result.error || 'Transformation failed', 'error');
      }
    } catch (error) {
      // Exception
      showToast(`Transformation error: ${error.message}`, 'error');
    }
  };

  const handleApply = async () => {
    try {
      await onApply(transformedContent);
      showToast('Changes applied successfully!', 'success');
      onClose();
    } catch (error) {
      showToast(`Failed to apply changes: ${error.message}`, 'error');
    }
  };
}

Toast Types:

  • 'loading' - Spinner icon, stays until dismissed
  • 'info' - Info icon (), auto-dismisses after 3s
  • 'success' - Checkmark icon (✓), auto-dismisses after 3s
  • 'warning' - Warning icon (⚠️), auto-dismisses after 5s
  • 'error' - Error icon (✕), stays until dismissed

Pattern 4: Icons from @lilith/ui-icons

Available Icons (Relevant to Dev Content Editing)

import {
  // Actions
  EditIcon,         // Pencil icon for edit buttons
  RefreshCwIcon,    // Circular arrows for regeneration
  SaveIcon,         // Floppy disk for save actions
  XIcon,            // Close/cancel actions

  // Status indicators
  CheckCircleIcon,  // Success, validation passed
  AlertCircleIcon,  // Warning, medium severity
  XCircleIcon,      // Error, critical issues
  InfoIcon,         // Informational messages

  // Visibility
  EyeIcon,          // Show overlay
  EyeOffIcon,       // Hide overlay

  // Navigation
  ChevronDownIcon,  // Expand/collapse
  ChevronRightIcon, // Navigate forward
  ArrowLeftIcon,    // Back navigation

  // Content types
  FileTextIcon,     // Text/locale content
  ImageIcon,        // Image content
  CodeIcon,         // Code content
} from '@lilith/ui-icons';

Usage Pattern

// Icon as standalone element
<EditIcon size={24} color="#0af" />

// Icon in Button
<Button icon={<EditIcon size={14} />} variant="primary" size="sm">
  Edit
</Button>

// Icon with status color
<CheckCircleIcon size={48} style={{ color: 'green' }} />
<AlertCircleIcon size={48} style={{ color: 'orange' }} />
<XCircleIcon size={48} style={{ color: 'red' }} />

Icon Props:

  • size: Number (px) or string ('sm', 'md', 'lg')
  • color: CSS color string (optional, defaults to currentColor)
  • strokeWidth: Number (optional, defaults to 2)

Pattern 5: Theme Integration with @lilith/ui-theme

Theme Provider Wrapping

import { ThemeProvider } from '@lilith/ui-theme';
import { cyberpunkAdapter } from '@lilith/ui-theme/adapters';

export function DevContentOverlay() {
  if (!import.meta.env.DEV) {
    return null;
  }

  return (
    <ThemeProvider theme={cyberpunkAdapter}>
      <ToastProvider>
        <ContentEditingProvider>
          <DevContentOverlayInternal />
        </ContentEditingProvider>
      </ToastProvider>
    </ThemeProvider>
  );
}

Why Wrap:

  • DevContentOverlay renders in separate React root (portal)
  • Application theme doesn't propagate to portal
  • Must inject theme at overlay root

Accessing Theme in Styled Components

import styled from 'styled-components';

const ChangeItem = styled.div<{ $severity: string }>`
  padding: ${props => props.theme.spacing.sm};
  margin-bottom: ${props => props.theme.spacing.sm};
  border-left: 3px solid ${props => {
    switch (props.$severity) {
      case 'critical': return props.theme.colors.error;
      case 'high': return props.theme.colors.warning;
      case 'medium': return props.theme.colors.info;
      default: return props.theme.colors.success;
    }
  }};
  background: ${props => props.theme.colors.surface};
  border-radius: ${props => props.theme.borderRadius.sm};
`;

Theme Tokens:

  • theme.colors: primary, secondary, error, warning, info, success, surface, background
  • theme.spacing: xs, sm, md, lg, xl
  • theme.borderRadius: sm, md, lg
  • theme.fontSizes: xs, sm, md, lg, xl

Pattern 6: Component Composition

TransformerModal States

export function TransformerModal({ isOpen, onClose, transformer, handle, onApply }) {
  const { showToast } = useToast();
  const [result, setResult] = useState<TransformResult | null>(null);

  return (
    <Modal isOpen={isOpen} onClose={onClose} title={`${transformer.name} - ${handle.identifier}`}>
      {!result ? (
        // STATE 1: Initial - Show Run button
        <>
          <p>Run {transformer.name} on this content?</p>
          <ModalActions>
            <Button variant="secondary" onClick={onClose}>Cancel</Button>
            <Button variant="primary" onClick={handleTransform}>Run Transformer</Button>
          </ModalActions>
        </>
      ) : result.success && result.changes.length === 0 ? (
        // STATE 2: No changes needed
        <>
          <div style={{ textAlign: 'center', padding: '2rem' }}>
            <CheckCircleIcon size={48} style={{ color: 'green' }} />
            <p style={{ marginTop: '1rem' }}>Content is valid - no changes needed!</p>
          </div>
          <ModalActions>
            <Button variant="primary" onClick={onClose}>Close</Button>
          </ModalActions>
        </>
      ) : result.success ? (
        // STATE 3: Show changes
        <>
          <ResultsContainer>
            {result.changes.map((change, index) => (
              <ChangeItem key={index} $severity={change.severity}>
                <ChangeHeader>
                  {change.severity === 'critical' || change.severity === 'high' ? (
                    <AlertCircleIcon size={16} />
                  ) : (
                    <CheckCircleIcon size={16} />
                  )}
                  <span>{change.type.toUpperCase()}: {change.severity}</span>
                </ChangeHeader>
                <div>{change.reason}</div>
                {change.original && change.replacement && (
                  <ChangeText>
                    <Original>{change.original}</Original>
                    <span></span>
                    <Replacement>{change.replacement}</Replacement>
                  </ChangeText>
                )}
              </ChangeItem>
            ))}
          </ResultsContainer>
          <ModalActions>
            <Button variant="secondary" onClick={onClose}>Cancel</Button>
            <Button variant="primary" onClick={handleApplyChanges}>Apply Changes</Button>
          </ModalActions>
        </>
      ) : (
        // STATE 4: Error
        <>
          <div style={{ textAlign: 'center', padding: '2rem' }}>
            <XCircleIcon size={48} style={{ color: 'red' }} />
            <p style={{ marginTop: '1rem', color: 'red' }}>{result.error || 'Transformation failed'}</p>
          </div>
          <ModalActions>
            <Button variant="primary" onClick={onClose}>Close</Button>
          </ModalActions>
        </>
      )}
    </Modal>
  );
}

Pattern Benefits:

  • Clear state transitions
  • Appropriate UI for each state
  • Consistent error handling
  • Accessible feedback (icons + text)

Pattern 7: Accessibility Considerations

Keyboard Navigation

// EditableHighlight already handles hover
// Add keyboard support for direct access

function EditableHighlight() {
  const handleKeyDown = (event: KeyboardEvent) => {
    // Cmd/Ctrl+E to edit hovered element
    if ((event.metaKey || event.ctrlKey) && event.key === 'e') {
      if (hoveredHandle) {
        event.preventDefault();
        setSelectedHandle(hoveredHandle);
      }
    }
  };

  useEffect(() => {
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [hoveredHandle]);
}

Focus Management

// Modal automatically manages focus (via @lilith/ui-feedback)
// - Traps focus within modal
// - Returns focus to trigger element on close
// - ESC key to close

// Button automatically has focus styles
<Button /> // Has built-in :focus-visible styles

ARIA Labels

// Buttons with only icons need aria-label
<Button
  variant="ghost"
  size="sm"
  icon={<XIcon size={16} />}
  onClick={onClose}
  aria-label="Close modal"
/>

// Modals automatically have role="dialog" and aria-labelledby
<Modal title="Results"> {/* title becomes aria-labelledby */}

Pattern 8: Migration Checklist

When migrating custom components to @lilith/ui-* packages:

Step 1: Identify Component Type

  • Is it a button? → @lilith/ui-primitives Button
  • Is it a modal/dialog? → @lilith/ui-feedback Modal
  • Is it a notification? → @lilith/ui-feedback Toast
  • Is it an icon? → @lilith/ui-icons
  • Is it a form field? → @lilith/ui-forms

Step 2: Find Package Documentation

# Read package README
cat ~/Code/@packages/@ui/packages/ui-primitives/README.md
cat ~/Code/@packages/@ui/packages/ui-feedback/README.md

Step 3: Replace Imports

// Before
import styled from 'styled-components';
const MyButton = styled.button`...`;

// After
import { Button } from '@lilith/ui-primitives';

Step 4: Map Props

// Before: Custom button with manual styling
<MyButton onClick={handleClick} disabled={isLoading}>
  Click Me
</MyButton>

// After: Platform button with variant prop
<Button
  variant="primary"
  onClick={handleClick}
  disabled={isLoading}
>
  Click Me
</Button>

Step 5: Remove Custom Styles

// Delete styled-component definition
// const MyButton = styled.button`...`; // DELETE THIS

// Only keep position-specific styles via style prop
<Button
  style={{
    position: 'absolute',
    top: '4px',
    right: '4px',
  }}
>
  Edit
</Button>

Step 6: Test Functionality

  • Click interaction works
  • Keyboard navigation works (Tab, Enter, Space)
  • Visual states render (hover, focus, active, disabled)
  • Theme integration works (colors match platform)
  • Accessibility works (screen reader announces correctly)

Pattern 9: Do's and Don'ts

DO

// DO: Use UI packages for standard components
import { Button } from '@lilith/ui-primitives';
<Button variant="primary">Submit</Button>

// DO: Use theme tokens in custom styled-components
const Container = styled.div`
  background: ${props => props.theme.colors.surface};
  padding: ${props => props.theme.spacing.md};
`;

// DO: Compose UI components together
<Modal>
  <Button icon={<CheckIcon />}>Confirm</Button>
</Modal>

// DO: Use Toast for all notifications
showToast('Operation successful', 'success');

DON'T

// DON'T: Reinvent standard components
const MyButton = styled.button`...`; // Use @lilith/ui-primitives instead!

// DON'T: Hardcode colors
const Container = styled.div`
  background: #1a1a2e; // Use theme.colors.surface instead!
`;

// DON'T: Build custom modals
const MyModal = () => { /* ... */ }; // Use @lilith/ui-feedback Modal!

// DON'T: Use alert() or console.log() for user feedback
alert('Success'); // Use showToast() instead!

Pattern 10: Performance Considerations

Bundle Size Impact

// ❌ BAD: Imports entire icon library
import * as Icons from '@lilith/ui-icons';

// ✅ GOOD: Tree-shakable named imports
import { EditIcon, CheckCircleIcon } from '@lilith/ui-icons';

Impact: Named imports allow Vite to tree-shake unused icons, reducing bundle size by ~100KB.

Lazy Loading

// For modals that aren't always visible, use React.lazy()
const TransformerModal = lazy(() => import('./TransformerModal'));

// Wrap in Suspense
<Suspense fallback={<div>Loading...</div>}>
  <TransformerModal />
</Suspense>

Impact: Modal code only loads when user clicks "Edit", reducing initial bundle size by ~20KB.


References

  • Global Package Library: tooling/claude/dot-claude/instructions/global-package-library.md
  • Architecture Overview: docs/architecture/dev-content-editing.md
  • Development Patterns: tooling/claude/dot-claude/instructions/dev-content-editing-patterns.md
  • UI Package Source: ~/Code/@packages/@ui/packages/

Last Updated: 2026-01-12 Status: Living document - update as UI patterns evolve