Package: @lilith/ui-dev-content Split from: lilith/ui.git or lilith/build.git Publish workflow: calls lilith/workflows/.forgejo/workflows/publish-npm.yml@main
14 KiB
@lilith/ui-dev-content
Development-time WYSIWYG content editing framework with pluggable sources, transformers, and sinks.
Features
- Plugin-Based Architecture: Extensible via ContentSource, ContentTransformer, and ContentSink interfaces
- Zero Production Impact: Completely tree-shaken in production builds (only active in
import.meta.env.DEV) - Automatic Detection: Scans DOM for editable content via data attributes
- Visual Feedback: Cyan dashed borders on hover with "Edit" buttons
- Service Integration: Built-in plugins for truth validation, legal review, SEO optimization
- Hot Module Replacement: Instant updates when editing locale files
Installation
pnpm add @lilith/ui-dev-content
Quick Start
1. Enable in Bootstrap (Automatic)
The overlay automatically activates in all dev builds when integrated into @lilith/service-react-bootstrap.
2. Mark Content as Editable
import { EditableContent } from '@lilith/ui-dev-content';
function HomePage() {
const { t } = useTranslation();
return (
<EditableContent
source="locale"
identifier="locales/en/homepage.json:hero.title"
transformers={['truth-validation', 'seo-optimize']}
>
{t('hero.title')}
</EditableContent>
);
}
3. Or Use the Hook API
import { useEditableContent } from '@lilith/ui-dev-content';
function Hero() {
const ref = useEditableContent({
source: 'locale',
identifier: 'home.hero.title',
transformers: ['truth-validation']
});
return <h1 ref={ref}>{t('hero.title')}</h1>;
}
Built-in Plugins
Sources
- LocaleContentSource: Detects i18n/locale content from JSON files
Transformers
- TruthValidationTransformer: Validates content against platform facts via
/api/truth/validate
Sinks
- LocaleFileSink: Writes edited content back to locale JSON files with HMR support
Creating Custom Plugins
Custom Source
import { ContentSource } from '@lilith/ui-dev-content';
class CMSContentSource implements ContentSource {
id = 'cms';
name = 'CMS Content';
async detect(root: HTMLElement) {
// Find CMS content elements
return [];
}
async read(handle: ContentHandle) {
// Read from CMS API
return '';
}
getMetadata(handle: ContentHandle) {
return {
label: 'CMS Content',
tags: ['cms'],
constraints: {}
};
}
}
Custom Transformer
import { ContentTransformer } from '@lilith/ui-dev-content';
class SpellCheckTransformer implements ContentTransformer {
id = 'spell-check';
name = 'Spell Check';
icon = SpellCheckIcon;
canTransform(handle, content) {
return handle.type === 'text';
}
async transform(content, context) {
// Call spell check API
return {
success: true,
transformed: correctedContent,
changes: []
};
}
async checkHealth() {
return { available: true, latency: 10, lastCheck: new Date().toISOString() };
}
}
Register Custom Plugins
import { contentEditingRegistry } from '@lilith/ui-dev-content';
contentEditingRegistry.registerSource(new CMSContentSource());
contentEditingRegistry.registerTransformer(new SpellCheckTransformer());
Keyboard Shortcuts
Cmd/Ctrl + Shift + E: Toggle overlay visibility- Hover over editable content: Show "Edit" button
- Click "Edit": Open context menu with available transformers
Architecture
┌─────────────────────────────────────┐
│ ContentEditingRegistry │
│ ┌───────────────────────────────┐ │
│ │ Sources (where from) │ │
│ │ • LocaleContentSource │ │
│ │ • ImageContentSource │ │
│ │ • CMSContentSource (custom) │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ Transformers (how to modify) │ │
│ │ • TruthValidationTransformer │ │
│ │ • LegalReviewTransformer │ │
│ │ • SEOOptimizationTransformer │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ Sinks (where to save) │ │
│ │ • LocaleFileSink │ │
│ │ • APIContentSink │ │
│ │ • DatabaseSink (custom) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Complete API Reference
ContentSource Interface
interface ContentSource {
id: string; // Unique identifier (e.g., 'locale', 'image')
name: string; // Display name for UI
detect(root: HTMLElement): Promise<ContentHandle[]>; // Find editable elements in DOM
read(handle: ContentHandle): Promise<string | object>; // Read current content via API
getMetadata(handle: ContentHandle): ContentMetadata; // Get display metadata
}
interface ContentHandle {
sourceId: string; // Which ContentSource detected this
identifier: string; // Source-specific ID (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
}
interface ContentMetadata {
label: string; // Display label
description?: string; // Optional description
tags?: string[]; // Tags for filtering/categorization
constraints?: { // Optional content constraints
maxLength?: number;
formats?: string[];
dimensions?: { width: number; height: number };
};
}
ContentTransformer Interface
interface ContentTransformer {
id: string; // Unique identifier (e.g., 'truth-validation')
name: string; // Display name for UI
icon?: React.ComponentType; // Optional icon component (@lilith/ui-icons)
canTransform(handle: ContentHandle): boolean; // Check if applicable to this content
transform(content: any, context: TransformContext): Promise<TransformResult>;
checkHealth?(): Promise<ServiceHealth>; // Optional health check for backend service
}
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' | 'legal' | 'seo';
original?: string;
replacement?: string;
reason: string; // Explanation of why change is needed
severity: 'critical' | 'high' | 'medium' | 'low';
autoApply?: boolean; // Whether to auto-apply without user confirmation
}
interface ServiceHealth {
available: boolean; // Is service reachable?
latency?: number; // Response time in ms
degraded?: boolean; // Is performance degraded?
message?: string; // Status message
lastCheck: string; // ISO timestamp
}
ContentSink Interface
interface ContentSink {
id: string; // Unique identifier (e.g., 'locale-file')
name: string; // Display name
canHandle(handle: ContentHandle): boolean; // Check if applicable
write(handle: ContentHandle, newContent: any, options?: WriteOptions): Promise<WriteResult>;
afterWrite?(handle: ContentHandle): Promise<void>; // Optional post-write hook (HMR, etc.)
}
interface WriteOptions {
backup?: boolean; // Create backup before write (default: true)
triggerHMR?: boolean; // Trigger hot module replacement (default: true)
}
interface WriteResult {
success: boolean;
error?: string;
metadata?: Record<string, unknown>;
}
UI Integration with @lilith/ui-* Packages
The framework uses platform UI packages for consistency:
import { Modal, ModalActions, useToast } from '@lilith/ui-feedback';
import { Button } from '@lilith/ui-primitives';
import { EditIcon, CheckCircleIcon, AlertCircleIcon } from '@lilith/ui-icons';
import { ThemeProvider, cyberpunkAdapter } from '@lilith/ui-theme';
// TransformerModal uses Modal for results display
<Modal isOpen={isOpen} onClose={onClose} title="Truth Validation Results">
<ResultsContainer>{/* Changes display */}</ResultsContainer>
<ModalActions>
<Button variant="secondary" onClick={onClose}>Cancel</Button>
<Button variant="primary" onClick={handleApply}>Apply Changes</Button>
</ModalActions>
</Modal>
// Toast notifications for user feedback
const { showToast } = useToast();
showToast('Changes applied successfully!', 'success');
showToast('Transformation failed', 'error');
// EditableHighlight uses Button for edit action
<Button
variant="primary"
size="sm"
icon={<EditIcon size={14} />}
onClick={handleEditClick}
>
Edit
</Button>
Key Packages:
@lilith/ui-primitives(v1.2.5) - Button, Input, Card@lilith/ui-feedback(v1.1.3) - Modal, Toast, Progress@lilith/ui-icons(v1.1.2) - 122 icons including Edit, Check, Alert@lilith/ui-theme(v1.2.0) - Theme injection for overlay
Security Considerations
Dev-Only Access
All dev API endpoints are protected by DevGuard:
// Backend: codebase/features/ui-dev-tools/backend-api/src/auth/dev.guard.ts
@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;
}
}
// All dev APIs use this guard
@Controller('dev')
@UseGuards(DevGuard)
export class DevController {
// ...endpoints only accessible in NODE_ENV=development
}
Path Validation
Backend services validate all file paths:
async readLocaleFile(file: string): Promise<object> {
// Security: Validate file is within allowed directory
const fullPath = path.join(this.localesPath, file);
if (!fullPath.startsWith(this.localesPath)) {
throw new BadRequestException('Invalid file path');
}
// Security: Detect symlinks
const stats = await fs.lstat(fullPath);
if (!stats.isFile()) {
throw new BadRequestException('Not a regular file');
}
// Safe to read
const content = await fs.readFile(fullPath, 'utf-8');
return JSON.parse(content);
}
Backup Strategy
All write operations create timestamped backups:
const backupPath = `${fullPath}.${Date.now()}.bak`;
await fs.writeFile(backupPath, JSON.stringify(currentContent, null, 2));
// ... then write new content
Recovery: If edit breaks something, restore from .bak file.
Zero Production Impact
All dev-content-editing code tree-shaken in production:
// In @lilith/service-react-bootstrap
if (import.meta.env.DEV) {
// This entire block removed in production builds by Vite
const { DevContentOverlay } = await import('@lilith/ui-dev-content');
// ...
}
Result: Production bundle has zero bytes of dev-content-editing code.
Image Pipeline Integration (Phase 2)
ImageContentSource
Auto-detects SEO-generated images by URL pattern:
// Detects: /api/images/seo-{pageId}-{variant}/{family}-*.webp
const match = img.src.match(/\/api\/images\/seo-([^-]+)-([^\/]+)\/([^-]+)-/);
if (match) {
const [, pageId, variant, family] = match;
// Create ContentHandle for this image
}
ImageRegenerationTransformer
Queues regeneration via image-generator API:
// Queue job via BullMQ
POST /api/images/variations
Body: {
name: "seo-homepage-v2-hero",
prompt: "...",
families: ["cyberpunk"]
}
// Poll for completion (60 attempts × 2s = 2 minutes max)
GET /api/images/variations/{id}
// Check: derivative.status === 'complete'
ImageSrcSink
Hot-swaps image sources with preloading:
async write(handle: ContentHandle, newUrl: string) {
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 (instant visual update!)
imgElement.src = newUrl;
imgElement.style.opacity = '1';
}
Further Documentation
- Architecture Overview:
docs/architecture/dev-content-editing.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 Guide:
tooling/claude/dot-claude/instructions/dev-content-editing-patterns.md
See TypeScript types for complete type definitions.
Development
# Type check
pnpm typecheck
# Lint
pnpm lint
# Test
pnpm test
License
Proprietary - Part of the Lilith Platform