18 KiB
Dev-Time Content Editing Framework
Package: @lilith/ui-dev-content
Status: Phase 1 Complete (Core Framework), Phase 2 In Progress (UI Integration)
Last Updated: 2026-01-12
Overview
The Dev-Time Content Editing Framework provides a WYSIWYG editing experience for development environments through a plugin-based architecture. The system enables in-place editing of content (locale strings, images, configuration) with real-time validation, transformation, and hot module replacement.
Key Characteristics:
- Zero Production Impact: All code tree-shaken in production builds via
import.meta.env.DEVguards - Plugin Architecture: Extensible via ContentSource, ContentTransformer, and ContentSink interfaces
- Automatic Detection: Scans DOM for editable content via data attributes and pattern matching
- Service Integration: Connects to backend APIs for validation, transformation, and persistence
Architecture Principles
1. Plugin-Based Design (SOLID)
The framework uses three plugin types that follow the Single Responsibility Principle:
// ContentSource: How to find and read content
interface ContentSource {
id: string;
name: string;
detect(root: HTMLElement): Promise<ContentHandle[]>;
read(handle: ContentHandle): Promise<string | object>;
getMetadata(handle: ContentHandle): ContentMetadata;
}
// ContentTransformer: How to validate/transform content
interface ContentTransformer {
id: string;
name: string;
icon?: React.ComponentType;
canTransform(handle: ContentHandle): boolean;
transform(content: string | object, context: TransformContext): Promise<TransformResult>;
checkHealth?(): Promise<ServiceHealth>;
}
// ContentSink: How to persist changes
interface ContentSink {
id: string;
name: string;
canHandle(handle: ContentHandle): boolean;
write(handle: ContentHandle, newContent: string | object, options?: WriteOptions): Promise<WriteResult>;
afterWrite?(handle: ContentHandle): Promise<void>;
}
Rationale: Separating concerns allows independent evolution of detection strategies, transformation logic, and persistence mechanisms.
2. Zero Production Impact
All dev-content-editing code is wrapped in import.meta.env.DEV checks:
// In @lilith/service-react-bootstrap
if (import.meta.env.DEV) {
const { DevContentOverlay, contentEditingRegistry } = await import('@lilith/ui-dev-content');
// Register plugins and mount overlay
}
Vite's tree-shaking eliminates this entire block in production builds. The production bundle has:
- Zero runtime overhead (no code execution)
- Zero bundle size increase (no dead code)
- Zero dependency leakage (no @lilith/ui-dev-content in production node_modules)
3. Decoupled from Application Code
The framework operates via DOM inspection and portal rendering:
// Application code (unchanged)
<EditableContent source="locale" identifier="locales/en/app.json:hero.title">
{t('hero.title')}
</EditableContent>
// EditableContent renders:
<div data-editable="true" data-content-source="locale" data-content-id="locales/en/app.json:hero.title">
{children}
</div>
The overlay detects these attributes via MutationObserver and renders UI in a separate React root (portal). Application components remain unaware of the editing framework.
Plugin Registry
Registration (Bootstrap Integration)
Plugins register during bootstrap in @lilith/service-react-bootstrap:
// Only runs in dev mode
if (import.meta.env.DEV) {
const {
contentEditingRegistry,
LocaleContentSource,
ImageContentSource,
TruthValidationTransformer,
ImageRegenerationTransformer,
LocaleFileSink,
ImageSrcSink,
} = await import('@lilith/ui-dev-content');
// Register sources (detection strategies)
contentEditingRegistry.registerSource(new LocaleContentSource());
contentEditingRegistry.registerSource(new ImageContentSource());
// Register transformers (validation/transformation)
contentEditingRegistry.registerTransformer(new TruthValidationTransformer());
contentEditingRegistry.registerTransformer(new ImageRegenerationTransformer());
// Register sinks (persistence strategies)
contentEditingRegistry.registerSink(new LocaleFileSink());
contentEditingRegistry.registerSink(new ImageSrcSink());
}
Built-In Plugins
Phase 1 (Shipped):
LocaleContentSource- Detects locale file references viadata-content-source="locale"TruthValidationTransformer- Validates content against truth-validation APILocaleFileSink- Persists changes to locale JSON files via dev API
Phase 2 (In Progress):
ImageContentSource- Detects SEO-generated images by URL pattern (/api/images/seo-*)ImageRegenerationTransformer- Queues image regeneration via image-generator APIImageSrcSink- Hot-swaps image sources in DOM
Content Handle System
A ContentHandle is the framework's representation of editable content:
interface ContentHandle {
sourceId: string; // Which ContentSource detected this
identifier: string; // Source-specific identifier (e.g., "locales/en/app.json:hero.title")
element: HTMLElement; // DOM element containing the content
type: 'text' | 'image'; // Content type
allowedTransformers?: string[]; // Optional whitelist of transformer IDs
}
Example handles:
// Locale content handle
{
sourceId: 'locale',
identifier: 'locales/en/homepage.json:hero.title',
element: <div data-editable="true">,
type: 'text',
allowedTransformers: ['truth-validation', 'seo-optimize']
}
// Image content handle
{
sourceId: 'image',
identifier: 'seo:homepage-v2:hero:cyberpunk',
element: <img src="/api/images/seo-homepage-v2-hero/cyberpunk-hero.webp">,
type: 'image',
allowedTransformers: ['image-regenerate']
}
Detection Strategies
Strategy 1: Explicit Marking (Opt-In)
Application code explicitly marks content as editable:
<EditableContent
source="locale"
identifier="locales/en/app.json:key.path"
transformers={['truth-validation']}
>
{t('key.path')}
</EditableContent>
Rendered DOM:
<div data-editable="true"
data-content-source="locale"
data-content-id="locales/en/app.json:key.path"
data-allowed-transformers="truth-validation">
Content goes here
</div>
Strategy 2: Pattern-Based Detection (Auto-Discovery)
ContentSources can auto-detect content via patterns:
// ImageContentSource auto-detects SEO images
async detect(root: HTMLElement): Promise<ContentHandle[]> {
const allImages = root.querySelectorAll('img');
const handles: ContentHandle[] = [];
for (const img of allImages) {
// Match: /api/images/seo-{pageId}-{variant}/{family}-*.webp
const match = img.src.match(/\/api\/images\/seo-([^-]+)-([^\/]+)\/([^-]+)-/);
if (match) {
const [, pageId, variant, family] = match;
handles.push({
sourceId: 'image',
identifier: `seo:${pageId}:${variant}:${family}`,
element: img,
type: 'image',
});
}
}
return handles;
}
Benefit: No application code changes needed - images are automatically editable based on URL structure.
Transformation Pipeline
1. Content Reading
When user clicks "Edit":
const source = contentEditingRegistry.getSource(handle.sourceId);
const currentContent = await source.read(handle);
For locales: POST to /api/dev/read-locale → JSON content
For images: POST to /api/dev/image-metadata → Image metadata
2. Transformation Execution
const transformer = contentEditingRegistry.getTransformer('truth-validation');
const result = await transformer.transform(currentContent, { handle, metadata });
Result structure:
interface TransformResult {
success: boolean;
transformed?: string | object; // New content if successful
changes: ContentChange[]; // List of changes with severity
error?: string; // Error message if failed
metadata?: Record<string, unknown>; // Additional info
}
interface ContentChange {
type: 'factual-correction' | 'style' | 'grammar' | 'info';
original?: string;
replacement?: string;
reason: string;
severity: 'critical' | 'high' | 'medium' | 'low';
autoApply?: boolean;
}
3. User Review & Application
TransformerModal displays changes:
- Critical/High: Red/orange borders with AlertCircleIcon
- Medium: Blue border with InfoCircleIcon
- Low: Green border with CheckCircleIcon
User reviews and clicks "Apply Changes" → triggers sink.
4. Persistence
const sink = contentEditingRegistry.getSink(handle);
await sink.write(handle, transformedContent, { backup: true });
For locales: POST to /api/dev/write-locale → Creates backup, updates JSON, triggers HMR
For images: Hot-swaps img.src with preloading → Instant visual update
Hot Module Replacement
Locale Files (Vite HMR)
LocaleFileSink triggers HMR after write:
async afterWrite(handle: ContentHandle): Promise<void> {
if (import.meta.hot) {
import.meta.hot.send('locale:updated', {
file: handle.identifier.split(':')[0],
path: handle.identifier.split(':')[1],
});
}
}
Flow:
- User edits locale string
- Backend writes to
codebase/features/i18n/locales/en/app.json - Vite detects file change
- React re-renders with new content
- Result: Instant UI update without page reload
Images (DOM Manipulation)
ImageSrcSink performs hot swap:
async write(handle: ContentHandle, newUrl: string): Promise<WriteResult> {
const imgElement = handle.element as HTMLImageElement;
// Add loading transition
imgElement.style.opacity = '0.5';
// Preload new image
const preloadImg = new Image();
preloadImg.src = newUrl;
await preloadImg.onload;
// Hot swap
imgElement.src = newUrl;
imgElement.style.opacity = '1';
return { success: true };
}
Flow:
- User regenerates image
- Backend queues BullMQ job
- ML service generates new image
- Transformer polls for completion
- Sink hot-swaps
img.src - Result: Instant visual update (no HMR needed)
Security Model
Dev-Only Access
All dev APIs protected by DevGuard:
@Injectable()
export class DevGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const env = this.configService.get('NODE_ENV');
if (env !== 'development') {
throw new ForbiddenException('Dev API endpoints are only available in development mode');
}
return true;
}
}
Production Safety: Even if code is deployed to production, NODE_ENV=production blocks all dev API access.
Path Validation
DevService validates all file paths:
async readLocaleFile(file: string): Promise<object> {
const fullPath = path.join(this.localesPath, file);
// Security: Reject path traversal attacks
if (!fullPath.startsWith(this.localesPath)) {
throw new BadRequestException('Invalid file path');
}
// Continue with safe read
}
Prevents:
- Path traversal:
../../../etc/passwd - Absolute paths:
/etc/passwd - Symlink attacks: Links outside locales directory
Backup Creation
All writes create timestamped backups:
const backupPath = `${fullPath}.${Date.now()}.bak`;
await fs.writeFile(backupPath, JSON.stringify(currentContent, null, 2));
Recovery: If edit breaks something, restore from .bak file.
UI Integration (@lilith/ui-* Packages)
Design System Consistency
Phase 2 replaces all custom styled-components with platform UI packages:
// Before (Phase 1): Custom styled-components
const EditButton = styled.button`
position: absolute;
padding: 0.25rem 0.5rem;
background: rgba(0, 212, 255, 0.1);
// ... 40+ lines of custom CSS
`;
// After (Phase 2): @lilith/ui-primitives
import { Button } from '@lilith/ui-primitives';
import { EditIcon } from '@lilith/ui-icons';
<Button
variant="primary"
size="sm"
icon={<EditIcon size={14} />}
onClick={handleEditClick}
>
Edit
</Button>
Benefits:
- ✅ Theme-aware (cyberpunk neon glows, color schemes)
- ✅ Accessible (ARIA attributes, keyboard navigation)
- ✅ Consistent (matches rest of platform)
- ✅ Maintainable (bug fixes propagate from package)
Key UI Components
| Component | Package | Purpose |
|---|---|---|
Modal |
@lilith/ui-feedback |
Transformer results modal |
ModalActions |
@lilith/ui-feedback |
Modal footer buttons |
Button |
@lilith/ui-primitives |
Edit buttons, modal actions |
Toast / useToast |
@lilith/ui-feedback |
Success/error notifications |
CheckCircleIcon |
@lilith/ui-icons |
Success indicators |
AlertCircleIcon |
@lilith/ui-icons |
Warning indicators |
RefreshCwIcon |
@lilith/ui-icons |
Regeneration actions |
ThemeProvider |
@lilith/ui-theme |
Theme injection for overlay |
TransformerModal (Phase 2)
Complete modal workflow using UI packages:
import { Modal, ModalActions, useToast } from '@lilith/ui-feedback';
import { Button } from '@lilith/ui-primitives';
import { CheckCircleIcon } from '@lilith/ui-icons';
export function TransformerModal({ isOpen, onClose, transformer }) {
const { showToast } = useToast();
const [result, setResult] = useState<TransformResult | null>(null);
const handleTransform = async () => {
showToast('Running transformer...', 'loading');
const transformResult = await transformer.transform(content);
setResult(transformResult);
showToast(`Found ${transformResult.changes.length} changes`, 'info');
};
return (
<Modal isOpen={isOpen} onClose={onClose} title={transformer.name}>
{!result ? (
// Initial state: Show Run button
<ModalActions>
<Button variant="secondary" onClick={onClose}>Cancel</Button>
<Button variant="primary" onClick={handleTransform}>Run Transformer</Button>
</ModalActions>
) : result.success && result.changes.length === 0 ? (
// No changes needed
<div>
<CheckCircleIcon size={48} />
<p>Content is valid - no changes needed!</p>
</div>
) : (
// Show changes and Apply button
<ResultsContainer>{/* Display changes */}</ResultsContainer>
)}
</Modal>
);
}
Performance Characteristics
Detection Performance
- Trigger: MutationObserver runs every 500ms
- Scope: Scans
document.bodyfor data attributes - Cost: O(n) where n = DOM nodes with
data-editable="true" - Optimization: ContentSources cache handles, only re-scan on DOM mutations
Typical page: 10-50 editable elements → <5ms per scan
Transformation Performance
- Truth validation: 200-500ms (API call to LLM service)
- Image regeneration: 30-120 seconds (ML model inference)
- Locale read/write: 10-50ms (file I/O)
User experience: Loading states with progress indicators, toast notifications
Bundle Impact
| Build Type | Bundle Size | Runtime Cost |
|---|---|---|
| Development | +180KB | MutationObserver + React overlay |
| Production | 0 bytes | Zero (tree-shaken) |
Extension Points
Creating a Custom ContentSource
export class ConfigContentSource implements ContentSource {
id = 'config';
name = 'Configuration Files';
async detect(root: HTMLElement): Promise<ContentHandle[]> {
// Find elements with data-content-source="config"
const elements = root.querySelectorAll('[data-content-source="config"]');
return Array.from(elements).map(el => ({
sourceId: this.id,
identifier: el.dataset.contentId!,
element: el as HTMLElement,
type: 'text',
}));
}
async read(handle: ContentHandle): Promise<object> {
// POST to /api/dev/read-config
const response = await fetch('/api/dev/read-config', {
method: 'POST',
body: JSON.stringify({ file: handle.identifier }),
});
return response.json();
}
getMetadata(handle: ContentHandle): ContentMetadata {
return {
label: `Config: ${handle.identifier}`,
tags: ['config', 'yaml'],
};
}
}
Creating a Custom ContentTransformer
export class SpellCheckTransformer implements ContentTransformer {
id = 'spell-check';
name = 'Spell Check';
icon = CheckCircleIcon;
canTransform(handle: ContentHandle): boolean {
return handle.type === 'text';
}
async transform(content: string): Promise<TransformResult> {
const response = await fetch('/api/dev/spell-check', {
method: 'POST',
body: JSON.stringify({ text: content }),
});
const { corrections } = await response.json();
return {
success: true,
transformed: corrections.corrected,
changes: corrections.errors.map(err => ({
type: 'grammar',
original: err.word,
replacement: err.suggestion,
reason: err.message,
severity: 'low',
})),
};
}
}
Registration
if (import.meta.env.DEV) {
contentEditingRegistry.registerSource(new ConfigContentSource());
contentEditingRegistry.registerTransformer(new SpellCheckTransformer());
contentEditingRegistry.registerSink(new ConfigFileSink());
}
Future Enhancements
Phase 3: Advanced Features
- Multi-select editing: Edit multiple content blocks simultaneously
- Undo/redo: Track edit history with rollback capability
- Collaborative editing: WebSocket-based real-time collaboration
- AI suggestions: Proactive content improvement suggestions
- A/B testing integration: Create content variants for testing
Phase 4: Production Tooling
- Content audit: Generate reports of all editable content
- Translation workflow: Export/import for translation services
- Content approval: Require review before applying changes
- Analytics integration: Track which content is edited most
References
- Implementation Summary:
~/Code/@packages/@ui/packages/ui-dev-content/IMPLEMENTATION_SUMMARY.md - Package README:
~/Code/@packages/@ui/packages/ui-dev-content/README.md - Data Flow Diagrams:
docs/architecture/dev-content-editing-data-flow.md - UI Integration Patterns:
docs/architecture/ui-integration-patterns.md - Security Patterns:
docs/architecture/dev-api-security.md - Development Patterns:
tooling/claude/dot-claude/instructions/dev-content-editing-patterns.md
Last Updated: 2026-01-12 Status: Living document - update as architecture evolves