figma-code-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +133 -0
  2. package/bin/install.js +328 -0
  3. package/knowledge/README.md +62 -0
  4. package/knowledge/css-strategy.md +973 -0
  5. package/knowledge/design-to-code-assets.md +855 -0
  6. package/knowledge/design-to-code-layout.md +929 -0
  7. package/knowledge/design-to-code-semantic.md +1085 -0
  8. package/knowledge/design-to-code-typography.md +1003 -0
  9. package/knowledge/design-to-code-visual.md +1145 -0
  10. package/knowledge/design-tokens-variables.md +1261 -0
  11. package/knowledge/design-tokens.md +960 -0
  12. package/knowledge/figma-api-devmode.md +894 -0
  13. package/knowledge/figma-api-plugin.md +920 -0
  14. package/knowledge/figma-api-rest.md +742 -0
  15. package/knowledge/figma-api-variables.md +848 -0
  16. package/knowledge/figma-api-webhooks.md +876 -0
  17. package/knowledge/payload-blocks.md +1184 -0
  18. package/knowledge/payload-figma-mapping.md +1210 -0
  19. package/knowledge/payload-visual-builder.md +1004 -0
  20. package/knowledge/plugin-architecture.md +1176 -0
  21. package/knowledge/plugin-best-practices.md +1206 -0
  22. package/knowledge/plugin-codegen.md +1313 -0
  23. package/package.json +31 -0
  24. package/skills/README.md +103 -0
  25. package/skills/audit-plugin/SKILL.md +244 -0
  26. package/skills/build-codegen-plugin/SKILL.md +279 -0
  27. package/skills/build-importer/SKILL.md +320 -0
  28. package/skills/build-plugin/SKILL.md +199 -0
  29. package/skills/build-token-pipeline/SKILL.md +363 -0
  30. package/skills/ref-html/SKILL.md +290 -0
  31. package/skills/ref-layout/SKILL.md +150 -0
  32. package/skills/ref-payload-block/SKILL.md +415 -0
  33. package/skills/ref-react/SKILL.md +222 -0
  34. package/skills/ref-tokens/SKILL.md +347 -0
@@ -0,0 +1,1004 @@
1
+ # PayloadCMS Visual Builder Plugin Architecture
2
+
3
+ ## Purpose
4
+
5
+ Authoritative reference for the `@eab/payload-visual-builder` plugin architecture. Documents the complete plugin system including the two-entry-point pattern (server + client), configuration API, core components (VisualBuilder, Canvas, BlockWrapper, Inspector, BlockLayers, TabbedSidebar), the edit block registry, inline editing primitives (EditableText, EditableMedia, Lexical editors), container adapter pattern for block nesting, drag-and-drop via dnd-kit, undo/redo history context, debounced save queue, keyboard shortcuts, responsive viewport preview, CSS token architecture, and the thin wrapper pattern for consuming applications. Encodes production patterns from a PayloadCMS 3.x visual page builder implementation.
6
+
7
+ ## When to Use
8
+
9
+ Reference this module when you need to:
10
+
11
+ - Understand the visual builder plugin architecture and component hierarchy
12
+ - Build or extend visual editing capabilities for PayloadCMS blocks
13
+ - Add new edit block components to the registry
14
+ - Implement inline editing for new block types using EditableText, EditableMedia, or Lexical
15
+ - Work with the container adapter pattern for nested block structures
16
+ - Understand the drag-and-drop implementation across containers
17
+ - Debug the undo/redo history system or save queue behavior
18
+ - Add keyboard shortcuts or viewport preview presets
19
+ - Build a Figma-to-CMS importer that needs to understand the visual editing layer
20
+ - Customize the plugin CSS token system for a consuming application
21
+ - Mount the visual builder in a new PayloadCMS project
22
+
23
+ ---
24
+
25
+ ## Content
26
+
27
+ ### 1. Purpose and When to Use
28
+
29
+ The `@eab/payload-visual-builder` plugin adds a canvas-based visual editing experience to PayloadCMS collections that use the blocks field type. Instead of editing blocks through Payload's default form interface, editors get a WYSIWYG-like canvas where they can:
30
+
31
+ - See blocks rendered with frontend-like styling
32
+ - Edit text and media inline (without navigating to field forms)
33
+ - Drag and drop blocks to reorder them within and across containers
34
+ - Use an inspector panel for block properties and layout controls
35
+ - Navigate the block hierarchy through a layers panel
36
+ - Preview layouts at different viewport sizes
37
+ - Undo and redo changes with full history tracking
38
+
39
+ The plugin is designed as a portable package that can be installed into any PayloadCMS 3.x project with minimal configuration. It does NOT replace Payload's built-in editing -- it provides an additional "Visual Builder" tab on document views for collections that opt in.
40
+
41
+ **Key architectural decision:** The plugin uses `useDocumentInfo()` to read initial block data and direct API calls (`PATCH /api/{collection}/{id}`) to persist changes. It deliberately avoids Payload's form state hooks to prevent re-render cascades during visual editing. This is the most important architectural decision in the entire plugin.
42
+
43
+ ### 2. Plugin Architecture Overview
44
+
45
+ The plugin follows a **two-entry-point pattern** that separates server-safe code from browser-only React components.
46
+
47
+ #### Entry Points
48
+
49
+ ```
50
+ @eab/payload-visual-builder
51
+ ├── index.ts → Re-exports from plugin-entry.ts
52
+ ├── src/
53
+ │ ├── plugin-entry.ts → Server entry (Node-safe, no CSS)
54
+ │ ├── client-entry.ts → Client entry (React, CSS, browser-only)
55
+ │ ├── plugin.ts → Plugin factory function
56
+ │ ├── types/ → Shared type definitions
57
+ │ ├── components/ → React components (client-only)
58
+ │ ├── hooks/ → React hooks (client-only)
59
+ │ ├── context/ → React contexts (client-only)
60
+ │ ├── registry/ → Block registry (client-only)
61
+ │ ├── utils/ → Utility functions (shared)
62
+ │ └── styles/ → CSS Modules + tokens (client-only)
63
+ ```
64
+
65
+ #### Server Entry (`index.ts` / `plugin-entry.ts`)
66
+
67
+ The server entry is imported in `payload.config.ts`. It exports:
68
+
69
+ - `visualBuilderPlugin()` -- Plugin factory function
70
+ - `VisualBuilderPluginConfig`, `VisualBuilderFeatures`, `ResolvedVisualBuilderConfig` -- Types
71
+ - `defaultFeatures`, `resolveConfig` -- Configuration utilities
72
+ - `validatePluginConfig`, `validateCollectionsExist` -- Validation utilities
73
+
74
+ **Critical rule:** This entry must NEVER import React components or CSS files. It runs in Node.js during Payload startup.
75
+
76
+ #### Client Entry (`client-entry.ts`)
77
+
78
+ The client entry is imported in React components (browser context). It exports:
79
+
80
+ - All React components (VisualBuilder, Canvas, BlockWrapper, etc.)
81
+ - All hooks (useSaveQueue, useBlockKeyboardShortcuts, etc.)
82
+ - Registry functions (registerEditBlock, getEditBlock, etc.)
83
+ - Utility functions (createBlock, isContainer, findBlockById, etc.)
84
+ - Style utilities (cn, getCSSVar, setCSSVar, tokens, etc.)
85
+ - Type re-exports for consumer convenience
86
+
87
+ **Usage pattern:**
88
+ ```typescript
89
+ // In payload.config.ts (server)
90
+ import { visualBuilderPlugin } from '@eab/payload-visual-builder'
91
+
92
+ // In React components (client)
93
+ import { VisualBuilder } from '@eab/payload-visual-builder/client'
94
+ ```
95
+
96
+ #### Plugin Factory Pattern
97
+
98
+ The `visualBuilderPlugin()` function follows Payload's plugin convention -- it returns a config modifier function:
99
+
100
+ ```typescript
101
+ export const visualBuilderPlugin = (
102
+ pluginConfig: VisualBuilderPluginConfig
103
+ ): ((config: Config) => Config) => {
104
+ validatePluginConfig(pluginConfig)
105
+ const resolvedConfig = resolveConfig(pluginConfig)
106
+
107
+ return (config: Config): Config => {
108
+ validateCollectionsExist(pluginConfig, config)
109
+
110
+ const modifiedCollections = (config.collections ?? []).map((collection) => {
111
+ if (resolvedConfig.collections.includes(collection.slug)) {
112
+ return addVisualBuilderToCollection(collection, resolvedConfig)
113
+ }
114
+ return collection
115
+ })
116
+
117
+ return { ...config, collections: modifiedCollections }
118
+ }
119
+ }
120
+ ```
121
+
122
+ The factory validates configuration, resolves defaults, and modifies the target collections by injecting `admin.custom.visualBuilder` metadata with the plugin's settings.
123
+
124
+ ### 3. Configuration API
125
+
126
+ #### VisualBuilderPluginConfig
127
+
128
+ ```typescript
129
+ interface VisualBuilderPluginConfig {
130
+ /** Collections to enable Visual Builder on */
131
+ collections: string[]
132
+
133
+ /** Path to the blocks field (dot notation). Default: 'layout.blocks' */
134
+ blocksFieldPath?: string
135
+
136
+ /** Feature flags for optional capabilities */
137
+ features?: VisualBuilderFeatures
138
+
139
+ /** Custom block registry mapping slugs to edit components */
140
+ blockRegistry?: Record<string, ComponentType<unknown>>
141
+ }
142
+ ```
143
+
144
+ #### VisualBuilderFeatures
145
+
146
+ All features default to `true`:
147
+
148
+ ```typescript
149
+ interface VisualBuilderFeatures {
150
+ inlineEditing?: boolean // Inline text/media editing
151
+ dragAndDrop?: boolean // Block reordering via drag
152
+ inspector?: boolean // Side panel for properties
153
+ keyboardShortcuts?: boolean // Keyboard shortcuts
154
+ undoRedo?: boolean // Undo/redo history
155
+ responsivePreview?: boolean // Viewport size switching
156
+ }
157
+ ```
158
+
159
+ #### Configuration Flow
160
+
161
+ 1. Consumer calls `visualBuilderPlugin({ collections: ['pages'], blocksFieldPath: 'layout.blocks' })`
162
+ 2. `resolveConfig()` merges user config with `defaultFeatures`
163
+ 3. Plugin modifies each target collection's `admin.custom.visualBuilder` with resolved config
164
+ 4. At runtime, the VisualBuilder component reads this config to determine behavior
165
+
166
+ #### Example Usage
167
+
168
+ ```typescript
169
+ // payload.config.ts
170
+ import { buildConfig } from 'payload'
171
+ import { visualBuilderPlugin } from '@eab/payload-visual-builder'
172
+
173
+ export default buildConfig({
174
+ plugins: [
175
+ visualBuilderPlugin({
176
+ collections: ['pages'],
177
+ blocksFieldPath: 'layout.blocks',
178
+ features: {
179
+ responsivePreview: true,
180
+ undoRedo: true,
181
+ },
182
+ }),
183
+ ],
184
+ })
185
+ ```
186
+
187
+ ### 4. Core Components
188
+
189
+ #### VisualBuilder (`VisualBuilder.tsx`)
190
+
191
+ The top-level component. It wraps `VisualBuilderInner` with a `HistoryProvider` for undo/redo support.
192
+
193
+ **Data flow:**
194
+
195
+ 1. `useDocumentInfo()` provides `initialData` from Payload's document context
196
+ 2. Blocks are extracted from `initialData.layout.blocks` (configurable path)
197
+ 3. Local `useState` manages the blocks array -- this is the source of truth during editing
198
+ 4. Changes flow through `handleBlocksChange()` which: validates blocks, updates local state, records a history snapshot, queues a debounced API save, and marks the document as dirty
199
+
200
+ **Key state:**
201
+ - `blocks: Block[]` -- Current block tree
202
+ - `selectedBlockId: string | null` -- Currently selected block
203
+ - `hoveredBlockId: string | null` -- Block hovered in layers panel
204
+ - `activeTab: string` -- Current sidebar tab ('fields' | 'outline' | 'blocks')
205
+ - `showPreview: boolean` -- Preview modal visibility
206
+
207
+ **Critical behavior:**
208
+ - Initial data loads ONCE per document (tracked by `initialLoadDoneRef`)
209
+ - Collapses Payload's main nav on mount to maximize horizontal space
210
+ - Text edits are batched via `useTextEditHistory` (1s debounce) to avoid flooding history
211
+ - Undo/redo do NOT trigger saves -- they are exploratory. The user must explicitly continue editing to trigger a save.
212
+
213
+ **Component tree:**
214
+ ```
215
+ VisualBuilder
216
+ └── HistoryProvider
217
+ └── VisualBuilderInner
218
+ ├── DndContext (dnd-kit)
219
+ │ ├── TabbedSidebar
220
+ │ │ ├── Inspector (fields tab)
221
+ │ │ ├── BlockLayers (outline tab)
222
+ │ │ └── BlockLibrary (blocks tab)
223
+ │ ├── Canvas
224
+ │ │ └── VisualCanvas
225
+ │ │ └── SortableBlockCard[]
226
+ │ │ └── EditBlockRenderer
227
+ │ │ └── [EditBlock component]
228
+ │ └── DragOverlay
229
+ └── PreviewModal
230
+ ```
231
+
232
+ #### Canvas (`Canvas.tsx`)
233
+
234
+ Renders the block list with drag-and-drop support. Provides the `Block` type definition used throughout the plugin:
235
+
236
+ ```typescript
237
+ export type Block = {
238
+ id: string
239
+ blockType: string
240
+ [key: string]: unknown
241
+ }
242
+ ```
243
+
244
+ **Features:**
245
+ - `SortableContext` from dnd-kit for reordering
246
+ - `useDroppable` for canvas-level drop zone
247
+ - Viewport-constrained container with dashed edge indicators
248
+ - Empty state when no blocks exist
249
+ - Block inserter at footer and inline after each block
250
+ - Recursive rendering of nested container children via `SortableBlockCard`
251
+
252
+ **Block operations:** Remove, move up, move down, duplicate, insert after -- all implemented as immutable array operations using path-based tree updates.
253
+
254
+ #### BlockWrapper (`BlockWrapper.tsx`)
255
+
256
+ Provides the visual editing chrome around each block:
257
+
258
+ - **Selection outline:** 2px solid blue when selected, 1px dashed blue on hover, transparent otherwise
259
+ - **Top bar:** Positioned above the block (-32px), appears on hover/selection/menu-open
260
+ - Left: Block type label pill with drag handle (GripVertical icon)
261
+ - Right: BlockActionsMenu (move up/down, add below, duplicate, remove)
262
+ - **Click behavior:** Clicks on the wrapper select the block. Clicks on editable content (contenteditable, input, textarea) pass through without selecting.
263
+ - **Hover debounce:** 100ms delay on mouse leave to allow cursor travel from content to toolbar
264
+
265
+ #### Inspector (`Inspector.tsx`)
266
+
267
+ Side panel for editing selected block properties:
268
+
269
+ - **No selection state:** Shows empty state with "No Block Selected" message
270
+ - **Selected block:** Shows block type header, truncated block ID, "Edit Content" button (navigates to Payload's Edit tab), ContainerControls (for container blocks), and generic BlockControls (layoutMeta fields)
271
+ - Uses `onBlockUpdate(blockId, updates)` callback for all property changes
272
+
273
+ #### BlockLayers (`BlockLayers.tsx`)
274
+
275
+ Hierarchical tree view of the page block structure:
276
+
277
+ - Recursive `LayerNode` components with depth-based indentation (16px per level)
278
+ - Expand/collapse chevrons for containers
279
+ - Click-to-select (syncs with canvas selection)
280
+ - Mouse hover triggers `onHoverBlock` (syncs with canvas highlight)
281
+ - Exposed via `forwardRef` with `BlockLayersRef` interface for external expand/collapse all
282
+ - ARIA tree roles for accessibility (`role="tree"`, `role="treeitem"`, `aria-expanded`)
283
+
284
+ #### TabbedSidebar (`TabbedSidebar.tsx`)
285
+
286
+ Combines `IconRail` (vertical icon tab strip) and `TabPanel` (content area) into the sidebar:
287
+
288
+ - Default tabs: Fields (Sliders icon), Outline (Layers icon), Blocks (LayoutGrid icon)
289
+ - Position switching (left/right) persisted via `useSidebarPosition` hook to localStorage
290
+ - Configurable panel width (default 220px)
291
+ - Optional header actions (e.g., expand/collapse all for Outline tab)
292
+
293
+ ### 5. Edit Block System
294
+
295
+ Edit blocks are React components that render block content with inline editing capabilities. They are the visual representations of Payload blocks in the canvas.
296
+
297
+ #### EditBlockProps Interface
298
+
299
+ Every edit block receives these props:
300
+
301
+ ```typescript
302
+ interface EditBlockProps<T extends Block = Block> {
303
+ block: T // Block data from Payload
304
+ onChange: (updates: Partial<T>) => void // Partial update callback
305
+ isSelected: boolean // Whether this block is selected
306
+ onSelect: () => void // Selection callback
307
+ showControls?: boolean // Show editing controls
308
+ dragHandleProps?: HTMLAttributes // dnd-kit drag handle
309
+ onEditInPayload?: () => void // Navigate to Payload edit tab
310
+ onRemove?: () => void // Remove this block
311
+ onMoveUp?: () => void // Move block up
312
+ onMoveDown?: () => void // Move block down
313
+ onDuplicate?: () => void // Duplicate this block
314
+ onAddBelow?: () => void // Insert block below
315
+ selectedBlockId?: string | null // Currently selected block (for nested)
316
+ onBlockSelect?: (id: string | null) => void // Select any block (for nested)
317
+ hoveredBlockId?: string | null // Hovered block for highlight sync
318
+ parentContainerId?: string | null // Parent container for hierarchy
319
+ }
320
+ ```
321
+
322
+ #### Registry Pattern
323
+
324
+ Edit blocks are registered in a global `Map<string, EditBlockRegistryEntry>`:
325
+
326
+ ```typescript
327
+ // Registration
328
+ registerEditBlock('hero', HeroEdit, { label: 'Hero', icon: Star })
329
+
330
+ // Lookup
331
+ const EditComponent = getEditBlock('hero') // Returns HeroEdit or null
332
+
333
+ // Check
334
+ hasEditBlock('hero') // true
335
+
336
+ // List all
337
+ getRegisteredBlockTypes() // ['hero', 'richText', 'container', ...]
338
+ ```
339
+
340
+ The registry is initialized once via `initializeEditBlocks()`, called at module load time in `VisualBuilder.tsx`. This function is idempotent (safe to call multiple times).
341
+
342
+ #### Registered Block Types
343
+
344
+ Phase B (core):
345
+ - `richText` (RichTextEdit) -- Lexical rich text editor
346
+ - `hero` (HeroEdit) -- Hero section with image, rich text, buttons
347
+ - `media` (MediaEdit) -- Media/image block
348
+ - `button` (ButtonEdit) -- Button with editable label
349
+ - `container` (ContainerEdit) -- Recursive container with nested blocks
350
+
351
+ Phase C (content):
352
+ - `card` (CardEdit) -- Card with image, title, description
353
+ - `accordion` (AccordionEdit) -- Expandable accordion items
354
+ - `video` (VideoEdit) -- Video embed
355
+ - `testimonial` (TestimonialEdit) -- Quote with attribution
356
+ - `stats` (StatsEdit) -- Statistics display
357
+ - `callToAction` (CallToActionEdit) -- CTA section
358
+ - `stickyCTA` (StickyCTAEdit) -- Sticky CTA bar
359
+ - `subnavigation` (SubNavigationEdit) -- Sub-navigation links
360
+ - `formEmbed` (FormEmbedEdit) -- Form embed
361
+
362
+ #### Fallback for Unregistered Blocks
363
+
364
+ When `getEditBlock()` returns null for a block type, the `EditBlockRenderer` falls back to a generic card view showing the block type and basic metadata.
365
+
366
+ #### How Edit Blocks Use Inline Primitives
367
+
368
+ Each edit block composes inline editing primitives to make its content editable. For example, `HeroEdit`:
369
+
370
+ ```typescript
371
+ // HeroEdit composition
372
+ <BlockWrapper block={block} blockType="Hero" ...>
373
+ <div className={containerClass}>
374
+ <EditableMedia // Background image
375
+ value={imageData}
376
+ onChange={handleImageChange}
377
+ size="full"
378
+ />
379
+ <PayloadLexicalEditor // Rich text content
380
+ value={richTextData}
381
+ onChange={handleRichTextChange}
382
+ showToolbar={isSelected}
383
+ />
384
+ {buttons.map((btn, idx) => (
385
+ <EditableText // Button labels
386
+ value={btn.link?.label || ''}
387
+ onChange={(label) => handleButtonLabelChange(idx, label)}
388
+ tag="span"
389
+ />
390
+ ))}
391
+ </div>
392
+ </BlockWrapper>
393
+ ```
394
+
395
+ **Pattern:** Every edit block wraps its content in `BlockWrapper` for selection chrome, then uses `EditableText` for short text fields, `EditableMedia` for images, and `PayloadLexicalEditor` or `InlineLexicalEditor` for rich text areas. The `onChange` prop on each primitive calls the parent's `onChange` with a partial block update.
396
+
397
+ #### ContainerEdit: Recursive Rendering
398
+
399
+ `ContainerEdit` is unique because it recursively renders its children using `EditBlockRenderer`, creating nested sortable contexts:
400
+
401
+ ```
402
+ ContainerEdit
403
+ └── BlockWrapper
404
+ └── <Tag> (dynamic: div, section, article, etc.)
405
+ └── SortableContext (per container)
406
+ ├── SortableChildBlock → EditBlockRenderer → [EditBlock]
407
+ ├── SortableChildBlock → EditBlockRenderer → ContainerEdit (recursive)
408
+ │ └── SortableContext (nested)
409
+ │ └── ...
410
+ └── ContainerEndDropZone
411
+ ```
412
+
413
+ Container settings (layout direction, alignment, gap, grid columns, width) are rendered as CSS flexbox/grid properties directly on the container element.
414
+
415
+ ### 6. Inline Editing Components
416
+
417
+ These primitives enable in-place content editing within edit blocks.
418
+
419
+ #### EditableText (`EditableText.tsx`)
420
+
421
+ A `contenteditable` div component for inline text editing.
422
+
423
+ **Props:**
424
+ ```typescript
425
+ interface EditableTextProps {
426
+ value: string // Current text value
427
+ onChange: (newValue: string) => void // Fires on blur or Enter (single-line)
428
+ placeholder?: string // Placeholder when empty
429
+ tag?: 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'label'
430
+ className?: string
431
+ style?: React.CSSProperties
432
+ multiline?: boolean // Enter adds newline vs. submits
433
+ disabled?: boolean
434
+ enableFormatting?: boolean // Rich text mode (bold, italic, link)
435
+ htmlValue?: string // HTML value when formatting enabled
436
+ onHtmlChange?: (html: string) => void // HTML change callback
437
+ }
438
+ ```
439
+
440
+ **Behavior:**
441
+ - Renders the specified HTML tag with `contenteditable`
442
+ - Changes propagate on blur (always) or Enter (single-line mode)
443
+ - Escape reverts to last committed value
444
+ - Paste strips formatting in plain text mode, preserves in rich text mode
445
+ - When `enableFormatting` is true, shows a floating `FormatToolbar` on text selection
446
+ - Format shortcuts: Cmd+B (bold), Cmd+I (italic), Cmd+K (link)
447
+
448
+ **Use cases:** Hero headlines, card titles, button labels, any short text field.
449
+
450
+ #### EditableMedia (`EditableMedia.tsx`)
451
+
452
+ Clickable image component that opens Payload's media drawer.
453
+
454
+ **Props:**
455
+ ```typescript
456
+ interface EditableMediaProps {
457
+ value: Media | string | null // Full object, ID string, or null
458
+ onChange: (newMedia: Media | null) => void
459
+ alt?: string
460
+ placeholder?: string
461
+ aspectRatio?: string // CSS aspect-ratio (e.g., '16/9')
462
+ style?: React.CSSProperties
463
+ disabled?: boolean
464
+ size?: 'small' | 'medium' | 'large' | 'full'
465
+ }
466
+ ```
467
+
468
+ **Behavior:**
469
+ - Displays current image with hover overlay ("Click to change")
470
+ - Empty state shows dashed border with "Add image" placeholder
471
+ - Click opens `useListDrawer` from `@payloadcms/ui` for media selection
472
+ - Remove button (X) clears the media
473
+ - Resolves media by ID via `/api/media/{id}` with in-memory cache
474
+ - Keyboard accessible: Enter/Space to open drawer
475
+
476
+ **Use cases:** Hero background images, card images, media blocks, any image field.
477
+
478
+ #### PayloadLexicalEditor (`PayloadLexicalEditor.tsx`)
479
+
480
+ Full Lexical rich text editor with a Payload-styled toolbar.
481
+
482
+ **Features:**
483
+ - Full formatting: bold, italic, underline, strikethrough, code
484
+ - Headings: H1-H4 via dropdown selector
485
+ - Lists: bullet and numbered
486
+ - Block quotes
487
+ - Text alignment: left, center, right, justify (via dropdown)
488
+ - Links: insert and remove
489
+ - Undo/redo (Lexical-internal history)
490
+ - Toolbar appears on focus or when externally controlled
491
+
492
+ **Architecture:** Uses `LexicalComposer` with `HeadingNode`, `QuoteNode`, `ListNode`, `ListItemNode`, and `LinkNode`. The `InitialStatePlugin` hydrates the editor from serialized Lexical JSON on mount only (never resets during typing).
493
+
494
+ #### InlineLexicalEditor (`InlineLexicalEditor.tsx`)
495
+
496
+ Full-featured rich text editor with a dark-themed toolbar. Similar to PayloadLexicalEditor but with a different visual treatment.
497
+
498
+ **Key difference from PayloadLexicalEditor:** Uses a dark toolbar style (#1f2937 background) with icon-only buttons. Shows toolbar only when focused or externally controlled via `showToolbar` prop.
499
+
500
+ #### FormatToolbar (`FormatToolbar.tsx`)
501
+
502
+ Floating toolbar for inline text formatting, rendered via React portal.
503
+
504
+ **Features:**
505
+ - Appears above selected text
506
+ - Three buttons: Bold (Cmd+B), Italic (Cmd+I), Link (Cmd+K)
507
+ - Viewport-aware positioning (prevents going off-screen)
508
+ - Dark theme (#1f2937 background)
509
+ - Active state highlighting for current formats
510
+ - `onMouseDown={preventDefault}` to preserve text selection during clicks
511
+
512
+ ### 7. Container Adapter Pattern
513
+
514
+ All container nesting logic is abstracted through `containerAdapter.ts`. This is the single source of truth for how blocks contain children.
515
+
516
+ #### Two Container Schemas
517
+
518
+ The system supports two container data structures:
519
+
520
+ ```
521
+ Main Container (layout schema): block.layout.content.blocks
522
+ Nested Container (content schema): block.content.content.blocks
523
+ ```
524
+
525
+ Both share the same `blockType: 'container'` slug. The adapter determines which schema to use by checking for the presence of a `layout` property.
526
+
527
+ #### Adapter Functions
528
+
529
+ ```typescript
530
+ // Check if block is a container
531
+ isContainer(block: Block): boolean
532
+ // Returns true if blockType === 'container'
533
+
534
+ // Determine which schema is used
535
+ getContainerSchema(block: Block): 'layout' | 'content' | null
536
+ // Returns 'layout' if block has layout property, else 'content'
537
+
538
+ // Check for children
539
+ hasChildren(block: Block): boolean
540
+
541
+ // Get children array
542
+ getChildren(block: Block): Block[]
543
+ // Returns [] for non-containers or empty containers
544
+
545
+ // Set children (immutably)
546
+ setChildren(block: Block, children: Block[]): Block
547
+ // Returns new block with updated children at correct schema path
548
+
549
+ // Get path array for children (used by Canvas for deep updates)
550
+ getChildrenPath(block: Block): string[]
551
+ // Returns ['layout', 'content', 'blocks'] or ['content', 'content', 'blocks']
552
+
553
+ // Recursive block search
554
+ findBlockById(blocks: Block[], id: string): Block | null
555
+
556
+ // Find parent container of a block
557
+ findParentContainer(blocks: Block[], childId: string): Block | null
558
+
559
+ // Recursive immutable block update
560
+ updateBlockInTree(blocks: Block[], blockId: string, updates: Partial<Block>): Block[]
561
+
562
+ // Cycle detection for drag-and-drop
563
+ wouldCreateCycle(blocks: Block[], draggedId: string, targetId: string): boolean
564
+ ```
565
+
566
+ #### Why This Abstraction Matters
567
+
568
+ Without the adapter, every component that touches container children would need to know about both schema variants. The adapter centralizes this knowledge so that Canvas, ContainerEdit, BlockLayers, dnd handlers, and the save pipeline all work through a single interface.
569
+
570
+ **Rule:** Never access `block.layout.content.blocks` or `block.content.content.blocks` directly. Always use `getChildren()` and `setChildren()`.
571
+
572
+ #### Container Adapter in Practice
573
+
574
+ The adapter is used across the entire plugin:
575
+
576
+ | Consumer | Functions Used | Purpose |
577
+ |----------|---------------|---------|
578
+ | `Canvas.tsx` | `isContainer`, `getChildren`, `getChildrenPath` | Recursive block rendering, path-based updates |
579
+ | `ContainerEdit.tsx` | `getContainerSchema`, `getChildren`, `setChildren` | Child CRUD operations, layout rendering |
580
+ | `BlockLayers.tsx` | `isContainer`, `getChildren` | Tree view hierarchy |
581
+ | `VisualBuilder.tsx` | `findBlockById`, `updateBlockInTree`, `findParentContainer`, `isContainer`, `getChildren`, `setChildren` | Selection, updates, insert logic |
582
+ | `useDndHandlers.ts` | `isContainer`, `getChildren`, `setChildren`, `getChildrenPath`, `wouldCreateCycle` | Cross-container moves, cycle prevention |
583
+ | `useBlockKeyboardShortcuts.ts` | (via blockOperations) | Delete, duplicate, move operations |
584
+
585
+ #### Path-Based Deep Updates
586
+
587
+ The Canvas uses a path-based approach for deeply nested updates. A path like `['0', 'layout', 'content', 'blocks', '2', 'content', 'content', 'blocks']` describes how to reach a specific nested block array. The `updateBlocksAtPath()` function walks this path and applies an updater function at the target level, returning a new immutable tree.
588
+
589
+ ### 8. Drag and Drop (dnd-kit)
590
+
591
+ The plugin uses `@dnd-kit/core` and `@dnd-kit/sortable` for drag-and-drop.
592
+
593
+ #### Architecture
594
+
595
+ ```
596
+ DndContext (in VisualBuilder)
597
+ ├── sensors: [PointerSensor { distance: 8 }]
598
+ ├── collisionDetection: pointerWithin
599
+ ├── onDragStart → setActiveId
600
+ ├── onDragOver → cycle detection + visual feedback
601
+ └── onDragEnd → block reorder/move
602
+ ├── SortableContext (root blocks in Canvas)
603
+ │ └── SortableBlockCard[] (useSortable per block)
604
+ └── SortableContext (per container in ContainerEdit)
605
+ └── SortableChildBlock[] (useSortable per child)
606
+ ```
607
+
608
+ #### Key Implementation Details
609
+
610
+ **PointerSensor activation:** 8px movement threshold before drag activates. This allows regular clicks to work on blocks without accidentally triggering drags.
611
+
612
+ **Collision detection:** `pointerWithin` is used instead of `closestCenter` because it provides more accurate hit detection for nested containers.
613
+
614
+ **Cycle prevention:** Before any drop, `wouldCreateCycle()` checks whether dropping block A into block B would create a cycle (e.g., dropping a container into itself or its descendants). The check uses `isDescendant()` recursive traversal. Invalid drop targets get visual feedback (red highlight) via the `invalidDropTarget` state.
615
+
616
+ **Cross-container moves:** When moving a block between containers:
617
+ 1. Remove the block from its source container
618
+ 2. Recalculate the target container's children path (indices may have shifted)
619
+ 3. Insert the block at the target position
620
+
621
+ **New block drops from library:** The BlockLibrary supports dragging new blocks onto the canvas. New blocks have IDs prefixed with `library-{blockType}`. On drop, `createBlock(blockType)` generates a fresh block with a new UUID.
622
+
623
+ **DragOverlay:** Shows a `BlockCardPreview` of the dragged block floating under the cursor.
624
+
625
+ #### Container-Specific Drop Zones
626
+
627
+ Each container provides:
628
+ - `ContainerEmptyDropZone` -- Shown when container has no children (dashed border, "Add blocks" or "Drop block here")
629
+ - `ContainerEndDropZone` -- Shown at the end of children during drag (thin bar indicator)
630
+ - Per-child drop indicators -- Horizontal or vertical lines based on container layout direction
631
+
632
+ #### Layout-Aware Sorting
633
+
634
+ ContainerEdit uses layout-aware sorting strategies:
635
+ - `verticalListSortingStrategy` for column and grid layouts (default)
636
+ - `horizontalListSortingStrategy` for row layouts
637
+
638
+ Drop indicators match the layout direction:
639
+ - Column/grid layouts: horizontal line above the drop position
640
+ - Row layouts: vertical line to the left of the drop position
641
+
642
+ Both indicator types use a 4px thick blue (#3b82f6) bar with a glow effect (`box-shadow: 0 0 8px rgba(59, 130, 246, 0.6)`).
643
+
644
+ #### Block Library Drag Source
645
+
646
+ The `BlockLibrary` component allows dragging new block types from the sidebar onto the canvas. Library items are configured as drag sources with:
647
+ - `id: 'library-{blockType}'` -- Unique ID format for distinguishing from existing blocks
648
+ - `data: { type: 'new-block', blockType }` -- Data payload for the drop handler
649
+
650
+ On drop, the `handleDragEnd` in `useDndHandlers` detects the `new-block` type, creates a fresh block via `createBlock()`, and inserts it at the target position.
651
+
652
+ ### 9. History and Undo/Redo
653
+
654
+ #### HistoryContext (`HistoryContext.tsx`)
655
+
656
+ A React context providing undo/redo functionality with these safety measures:
657
+
658
+ **Deep cloning:** Uses `JSON.parse(JSON.stringify(blocks))` for deep cloning. Not `structuredClone`, because the blocks may contain non-cloneable references. The `safeCloneBlocks()` utility handles this with validation.
659
+
660
+ **Validation:** Every snapshot is validated before recording AND before restoring. If validation fails, the operation is rejected with a console error.
661
+
662
+ **Maximum entries:** 50 entries maximum. When exceeded, oldest entries are pruned.
663
+
664
+ **Cursor-based navigation:** The history maintains an array of entries and a cursor. Undo decrements the cursor, redo increments it. New edits truncate everything after the current cursor (destroying redo history).
665
+
666
+ #### HistoryEntry Structure
667
+
668
+ ```typescript
669
+ interface HistoryEntry {
670
+ id: string // crypto.randomUUID()
671
+ timestamp: number // Date.now()
672
+ blocks: Block[] // Deep-cloned block state
673
+ operation: OperationType // 'initial' | 'insert' | 'remove' | 'move' | 'duplicate' | 'update' | 'batch'
674
+ label: string // Human-readable label
675
+ }
676
+ ```
677
+
678
+ #### Text Edit Batching (`useTextEditHistory`)
679
+
680
+ Rapid keystrokes during text editing would create one history entry per keystroke. The `useTextEditHistory` hook batches them:
681
+
682
+ 1. First keystroke calls `startBatch()` -- clones current blocks as "before" state
683
+ 2. Subsequent keystrokes within 1000ms reset the debounce timer
684
+ 3. After 1000ms of inactivity OR explicit `flushPending()` -- records a single "Edit text" entry
685
+ 4. Non-text changes (structural operations) auto-flush any pending text batch first
686
+
687
+ **Flush triggers:**
688
+ - Debounce timeout (1000ms)
689
+ - Focus leaves the VisualBuilder container (onBlur)
690
+ - Non-text structural change (move, delete, insert)
691
+
692
+ #### Undo/Redo Behavior
693
+
694
+ **Undo** restores the previous state WITHOUT triggering a save. The rationale: undo is exploratory. The user may undo several steps just to look, then redo back. Only when the user makes a new edit from an undone state does a save get queued.
695
+
696
+ **Redo** similarly restores without saving.
697
+
698
+ **History-aware save status:** The `SaveStatusIndicator` compares the current history cursor with the last saved cursor to determine if the user is exploring history (viewing a state older than what was saved).
699
+
700
+ ### 10. Save Strategy
701
+
702
+ #### Direct API Approach
703
+
704
+ The visual builder does NOT use Payload's form state for writes. Instead:
705
+
706
+ ```typescript
707
+ // In VisualBuilder.tsx
708
+ const saveBlocks = async (blocksToSave: Block[]) => {
709
+ const response = await fetch(
710
+ `/api/${collectionSlug}/${documentId}?depth=0`,
711
+ {
712
+ method: 'PATCH',
713
+ headers: { 'Content-Type': 'application/json' },
714
+ body: JSON.stringify({ layout: { blocks: blocksToSave } }),
715
+ }
716
+ )
717
+ // ... error handling
718
+ }
719
+ ```
720
+
721
+ #### Why NOT Form State
722
+
723
+ Payload's form state hooks (`useField`, `useForm`) trigger React re-renders on every field change. In a visual editor with many blocks, each containing multiple fields, this creates render cascades that degrade performance. By managing local state and saving via direct API calls, the visual builder achieves:
724
+
725
+ - **Instant UI updates:** `setBlocks()` updates local state immediately
726
+ - **Debounced saves:** Only one API call per 500ms burst of changes
727
+ - **No form re-renders:** Block changes don't cascade through Payload's form system
728
+ - **Optimistic UI:** Changes appear instantly; save happens in the background
729
+
730
+ #### useSaveQueue Hook
731
+
732
+ ```typescript
733
+ const { status, queueSave, retry } = useSaveQueue(saveFn, {
734
+ debounceMs: 500, // Wait 500ms after last change
735
+ savedDisplayMs: 2000, // Show "Saved" status for 2s
736
+ onSuccess: () => { ... }, // Called after successful save
737
+ onError: (err) => { ... }, // Called on save failure
738
+ })
739
+ ```
740
+
741
+ **Save states:** `idle` -> `dirty` -> `saving` -> `saved` -> `idle` (or `error`)
742
+
743
+ **Queue behavior:**
744
+ 1. `queueSave(data)` stores the latest data and starts a debounce timer
745
+ 2. After 500ms of no new calls, `executeSave()` fires
746
+ 3. If a new `queueSave()` arrives during save, it stores as pending
747
+ 4. After save completes, if pending data exists, it saves again
748
+ 5. Only one request is in-flight at a time
749
+
750
+ **Error handling:** On save failure, the pending data is preserved for retry. The `retry()` function re-attempts the last failed save.
751
+
752
+ #### Unsaved Changes Warning
753
+
754
+ The `useUnsavedChanges` hook sets up a `beforeunload` event listener when the document has unsaved changes, warning users before navigating away.
755
+
756
+ ### 11. Keyboard Shortcuts
757
+
758
+ #### Block Operations (`useBlockKeyboardShortcuts`)
759
+
760
+ | Shortcut | Action | Condition |
761
+ |----------|--------|-----------|
762
+ | Delete / Backspace | Delete selected block | Block selected, not in editable |
763
+ | Cmd+D | Duplicate selected block | Block selected, not in editable |
764
+ | Cmd+ArrowUp | Move block up | Block selected, not in editable |
765
+ | Cmd+ArrowDown | Move block down | Block selected, not in editable |
766
+ | Escape | Clear selection | Any time |
767
+
768
+ **Skip conditions:** All shortcuts (except Escape) are disabled when focus is in an input, textarea, or contenteditable element to avoid interfering with text editing.
769
+
770
+ #### Global Shortcuts (in VisualBuilder)
771
+
772
+ | Shortcut | Action |
773
+ |----------|--------|
774
+ | Cmd+Z | Undo |
775
+ | Cmd+Shift+Z | Redo |
776
+ | Cmd+P | Open preview modal |
777
+
778
+ These global shortcuts also skip when focus is in input/textarea/contenteditable.
779
+
780
+ #### Inline Text Shortcuts (in EditableText)
781
+
782
+ | Shortcut | Action | Condition |
783
+ |----------|--------|-----------|
784
+ | Cmd+B | Bold | `enableFormatting` is true |
785
+ | Cmd+I | Italic | `enableFormatting` is true |
786
+ | Cmd+K | Insert/remove link | `enableFormatting` is true |
787
+ | Enter | Submit (blur) | Single-line mode |
788
+ | Cmd+Enter | Submit (blur) | Multiline mode |
789
+ | Escape | Revert and blur | Always |
790
+
791
+ ### 12. Responsive Preview
792
+
793
+ #### useViewportPreset Hook
794
+
795
+ ```typescript
796
+ const VIEWPORT_PRESETS = {
797
+ desktop: { width: 1280, label: 'Desktop', icon: 'Monitor' },
798
+ tablet: { width: 768, label: 'Tablet', icon: 'Tablet' },
799
+ mobile: { width: 375, label: 'Mobile', icon: 'Smartphone' },
800
+ full: { width: null, label: 'Full Width', icon: 'Maximize2' },
801
+ }
802
+
803
+ const { preset, setPreset, width } = useViewportPreset()
804
+ ```
805
+
806
+ - `width: null` means no constraint (full canvas width)
807
+ - Default preset is `desktop` (1280px)
808
+ - No persistence -- resets on unmount
809
+
810
+ #### Canvas Viewport Containment
811
+
812
+ When a viewport width is set, the Canvas applies:
813
+ ```css
814
+ max-width: ${viewportWidth}px;
815
+ margin: 0 auto;
816
+ transition: max-width 0.2s ease;
817
+ ```
818
+
819
+ Dashed blue edge indicators (opacity 0.6) show the viewport boundaries.
820
+
821
+ #### ViewportSwitcher Component
822
+
823
+ A segmented control in the toolbar that shows the available presets and allows switching between them. Each button shows the preset's icon (Monitor, Tablet, Smartphone, Maximize2).
824
+
825
+ #### Preview Modal
826
+
827
+ `PreviewModal` shows a full-screen overlay with the block tree rendered at selected viewport sizes. It provides separate viewport switching independent of the editor viewport.
828
+
829
+ **Preview viewport widths:**
830
+ ```typescript
831
+ const PREVIEW_VIEWPORT_WIDTHS = {
832
+ desktop: 1280,
833
+ tablet: 768,
834
+ mobile: 375,
835
+ }
836
+ ```
837
+
838
+ The preview modal renders blocks via `PreviewBlockRenderer`, which renders blocks without editing chrome (no BlockWrapper, no inline editing). This provides a clean preview of the final layout.
839
+
840
+ ### 13. CSS Architecture
841
+
842
+ #### Plugin CSS Variables (`--vb-*`)
843
+
844
+ The plugin defines its own CSS custom properties in `tokens.css`:
845
+
846
+ ```css
847
+ :root {
848
+ --vb-font-family: system-ui, ...;
849
+ --vb-font-size-base: 18px;
850
+ --vb-spacing-md: 16px;
851
+ --vb-color-primary: #3b82f6;
852
+ --vb-radius-md: 8px;
853
+ --vb-shadow-md: 0 4px 6px ...;
854
+ --vb-transition-fast: 150ms ease;
855
+ /* ... */
856
+ }
857
+ ```
858
+
859
+ **DEPRECATED:** The `tokens.css` file is deprecated in favor of the frontend's `--token-*` variables. The `VisualCanvas` component now aliases:
860
+ ```css
861
+ --vb-color-primary: var(--token-color-primary, #3b82f6);
862
+ ```
863
+
864
+ This means consuming projects that define `--token-*` variables get automatic parity between their frontend and the visual builder.
865
+
866
+ #### CSS Modules
867
+
868
+ Components use SCSS or CSS Modules for scoped styles:
869
+ - `visualBuilder.module.scss` -- Layout container, header, toolbar
870
+ - `tabbedSidebar.module.scss` -- Sidebar layout, icon rail, tab panel
871
+ - `blockLayers.module.scss` -- Tree view nodes, expand/collapse
872
+ - `blockLibrary.module.scss` -- Block library grid
873
+ - `visualCanvas.module.css` -- Canvas containment and token aliasing
874
+ - `viewportSwitcher.module.css` -- Preset buttons
875
+ - `previewModal.module.scss` -- Modal overlay
876
+
877
+ #### Block-Specific Styles
878
+
879
+ Each edit block has its own CSS Module exposed via `styles/blocks.ts`:
880
+ ```typescript
881
+ export {
882
+ heroStyles,
883
+ containerStyles,
884
+ cardStyles,
885
+ richTextStyles,
886
+ mediaStyles,
887
+ buttonStyles,
888
+ // ...
889
+ } from './blocks'
890
+ ```
891
+
892
+ These are CSS Module objects that edit blocks import for their visual treatment.
893
+
894
+ #### CSS Containment
895
+
896
+ The `VisualCanvas` component uses CSS containment to isolate the rendering context:
897
+ ```css
898
+ .visualCanvas {
899
+ /* Style isolation from Payload admin */
900
+ isolation: isolate;
901
+ contain: content;
902
+ }
903
+ ```
904
+
905
+ This prevents Payload's admin styles from bleeding into block rendering and vice versa.
906
+
907
+ #### Style Utilities
908
+
909
+ ```typescript
910
+ // Class name merging (falsy-safe)
911
+ cn('base', isActive && 'active', undefined) // => 'base active'
912
+
913
+ // Read CSS variable
914
+ getCSSVar('vb-color-primary', '#3b82f6')
915
+
916
+ // Set CSS variable on element
917
+ setCSSVar(element, 'vb-color-primary', '#2563eb')
918
+
919
+ // TypeScript token access
920
+ tokens.colors.primary // => '#3b82f6'
921
+ getToken('colors.primary') // => '#3b82f6'
922
+ ```
923
+
924
+ ### 14. Thin Wrapper Pattern
925
+
926
+ Consuming applications mount the Visual Builder through a thin wrapper pattern.
927
+
928
+ #### How It Works
929
+
930
+ 1. The plugin registers itself on target collections via `visualBuilderPlugin()`
931
+ 2. The consuming app creates a thin wrapper component that re-exports the Visual Builder:
932
+
933
+ ```typescript
934
+ // In consuming app (e.g., app/(payload)/admin/components/VisualBuilder.tsx)
935
+ export { VisualBuilder as default } from '@eab/payload-visual-builder/client'
936
+ ```
937
+
938
+ 3. Payload's import map resolution picks up this component for the custom document view tab
939
+
940
+ #### Plugin Import Map
941
+
942
+ The plugin uses Payload 3.x's import map system to inject the Visual Builder as a custom document view tab. When a user navigates to a document in an enabled collection, they see an additional "Visual Builder" tab alongside the default "Edit" tab.
943
+
944
+ #### Customization Points
945
+
946
+ - **Block registry:** Consumers can register additional edit blocks or override existing ones
947
+ - **CSS variables:** Setting `--token-*` variables in the admin context customizes the visual builder's appearance
948
+ - **Feature flags:** Individual features can be disabled via the config
949
+ - **Tab configuration:** The sidebar tabs can be customized (though defaults are provided)
950
+ - **Block categories:** The `BLOCK_CATEGORIES` and `getBlocksByCategory` utilities organize blocks in the library
951
+
952
+ ### 15. Figma Parallels
953
+
954
+ The Visual Builder's architecture mirrors Figma's design patterns, which is intentional -- it informs the design of a future Figma-to-CMS importer.
955
+
956
+ | Visual Builder | Figma | Notes |
957
+ |----------------|-------|-------|
958
+ | Canvas | Figma canvas | Root rendering surface, click-to-deselect |
959
+ | Block selection (blue outline) | Node selection (blue outline) | Both use outline + box-shadow |
960
+ | Inspector panel | Right panel (Design/Prototype/Inspect) | Properties for selected element |
961
+ | BlockLayers panel | Layers panel | Hierarchical tree with expand/collapse |
962
+ | TabbedSidebar (Fields/Outline/Blocks) | Left sidebar (Layers/Assets/Pages) | Icon rail + panel content |
963
+ | Container nesting | Auto Layout frames | Both support recursive nesting |
964
+ | ContainerEdit (flex/grid layout) | Auto Layout settings | Direction, gap, alignment, padding |
965
+ | BlockWrapper hover/select chrome | Node hover/select chrome | Label pill, action buttons |
966
+ | Drag-and-drop reorder | Layer drag reorder | Within and across containers |
967
+ | Inline text editing (EditableText) | Text layer double-click editing | Direct content manipulation |
968
+ | Undo/redo (HistoryContext) | Undo/redo (Cmd+Z / Cmd+Shift+Z) | Snapshot-based history |
969
+ | Viewport presets | Device frames | Desktop/tablet/mobile preview |
970
+ | DragOverlay (ghost preview) | Drag ghost | Visual feedback during drag |
971
+ | BlockLibrary | Assets panel | Catalog of available components |
972
+ | Block categories | Component sections | Organizational grouping |
973
+
974
+ #### Implications for Figma Importer
975
+
976
+ Understanding these parallels means a Figma importer can map:
977
+ - Figma page structure -> Visual Builder block tree
978
+ - Figma Auto Layout frames -> Container blocks with layout settings
979
+ - Figma component instances -> Registered edit blocks
980
+ - Figma text layers -> EditableText or Lexical content
981
+ - Figma image fills -> EditableMedia values
982
+ - Figma component properties -> Block field values
983
+
984
+ The mapping rules in `payload-figma-mapping.md` formalize these correspondences.
985
+
986
+ ### 16. Cross-References
987
+
988
+ This module connects to several other knowledge modules:
989
+
990
+ - **`payload-blocks.md`** -- Block configurations (slug, fields, tabs) that the edit block system renders. Every `EditBlockComponent` corresponds to a block config defined in this module. Container schemas (layout vs. content) documented here are the same schemas the container adapter handles.
991
+
992
+ - **`payload-figma-mapping.md`** -- Mapping rules that determine which Figma components become which block types. The visual builder's edit block registry mirrors the block catalog from this mapping. Container nesting rules translate between Figma Auto Layout and the Container block's flex/grid settings.
993
+
994
+ - **`design-to-code-layout.md`** -- Auto Layout to CSS Flexbox mapping. ContainerEdit renders the same CSS flexbox/grid properties documented in this module: flex-direction, align-items, justify-content, gap, grid-template-columns. The visual builder makes these properties editable via ContainerControls.
995
+
996
+ - **`css-strategy.md`** -- The three-layer CSS architecture (Tailwind + Custom Properties + CSS Modules). The visual builder's CSS architecture follows this strategy: `--vb-*` custom properties for tokens, CSS Modules for scoped component styles, and the `--token-*` variable bridge for frontend parity.
997
+
998
+ - **`design-tokens.md`** -- Token extraction pipeline and CSS rendering. The visual builder's `tokens.css` and `styles/utils.ts` implement the same token categories (colors, typography, spacing, radii, shadows) documented in this module. The deprecated `--vb-*` to `--token-*` migration reflects the tokens module's single-source-of-truth principle.
999
+
1000
+ - **`figma-api-plugin.md`** -- Figma plugin sandbox model and development patterns. The visual builder's plugin architecture (two-entry pattern, configuration factory) parallels Figma's plugin model. Both use message-passing (Figma: main/UI threads; Visual Builder: server/client entries) and registration patterns.
1001
+
1002
+ - **`design-to-code-semantic.md`** -- Semantic HTML generation. ContainerEdit supports dynamic HTML tags (section, article, div, nav, aside) matching the semantic element mapping documented in this module. The visual builder preserves semantic structure during visual editing.
1003
+
1004
+ - **`design-to-code-visual.md`** -- Visual property mapping (fills, strokes, effects). Edit blocks like HeroEdit apply visual properties (background images, overlays, shadows) that correspond to the Figma visual properties documented in this module.