17 KiB
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,backgroundtheme.spacing:xs,sm,md,lg,xltheme.borderRadius:sm,md,lgtheme.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-primitivesButton - Is it a modal/dialog? →
@lilith/ui-feedbackModal - Is it a notification? →
@lilith/ui-feedbackToast - 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