ui/packages/ui-dev-content
2026-06-10 21:19:44 -07:00
..
.forgejo/workflows ci(workflows): 👷 Remove redundant build steps from publish workflows to improve efficiency 2026-04-20 01:16:37 -07:00
src chore(ui-dev-content): 🔧 Introduce EditableContent component, ContentEditingContext, and useEditableContent hook for reusable UI content editing via React context 2026-02-01 04:54:14 -08:00
.eslintrc.json chore(ui-components): 🚀 Clean up unnecessary files and update package.jsons 2026-01-13 09:12:15 -08:00
.gitignore chore(ui): 🔧 Standardize build artifact and environment file exclusion in all UI packages to enforce consistent .gitignore patterns 2026-04-20 01:16:37 -07:00
IMPLEMENTATION_SUMMARY.md chore(ui-components): 🚀 Clean up unnecessary files and update package.jsons 2026-01-13 09:12:15 -08:00
package.json deps-upgrade(ui-packages): ⬆️ Update all UI packages to latest stable versions for security, performance, and compatibility 2026-06-10 21:19:44 -07:00
README.md chore(ui-components): 🚀 Clean up unnecessary files and update package.jsons 2026-01-13 09:12:15 -08:00
tsconfig.json chore(ui-dev-content): 🔧 Update TypeScript build settings in tsconfig.json for stricter compilation (strict mode, target ES2023) 2026-01-20 21:09:34 -08:00

@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