1065 lines
33 KiB
Markdown
1065 lines
33 KiB
Markdown
# WooNooW Page Editor - Enhanced UI/UX Brief (v4)
|
|
|
|
## Overview
|
|
|
|
Redesign the Page Editor from a **card-list-based interface** to a **live-canvas-based interface** that shows actual rendered content in real-time. Leverage JSON-based rendering (no iframe/MutationObserver overhead) for faster, simpler implementation.
|
|
|
|
---
|
|
|
|
## Core Philosophy
|
|
|
|
**One Mental Model: What You See Is What You Get (WYSIWYG)**
|
|
|
|
- Canvas shows the **actual rendered output** merchants will see on the front-end
|
|
- No iframe isolation → Direct component rendering → Instant visual feedback
|
|
- Edit sections on the canvas → Settings panel updates → Preview updates in real-time
|
|
- No context switching between "list view" and "edit mode"
|
|
|
|
---
|
|
|
|
## New Layout Structure
|
|
|
|
### Full-Screen Layout (No iframe, Direct React Rendering)
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
│ [☰] WooNooW Appearance > Pages > [About Us] [D/M] [Save] │
|
|
├─────────────────┬────────────────────────┬──────────────────────┤
|
|
│ │ │ │
|
|
│ LEFT SIDEBAR │ MAIN CANVAS │ RIGHT INSPECTOR │
|
|
│ (180px) │ (Dynamic, ~60-70%) │ (Collapse ◄) │
|
|
│ │ │ (200-280px) │
|
|
│ 📄 PAGES │ ╔════════════════════╗ │ │
|
|
│ ├─ Home │ ║ LIVE PAGE RENDER ║ │ ┌─────────────────┐ │
|
|
│ ├─ About ✓ │ ║ ║ │ │ Section Config │ │
|
|
│ └─ Contact │ ║ ┌──────────────┐ ║ │ └─────────────────┘ │
|
|
│ │ ║ │ HERO │ ║ │ │
|
|
│ 📋 TEMPLATES │ ║ │ Welcome │←──╋─┼─→ [Hero section selected]
|
|
│ ├─ Post │ ║ │ [Image] │ ║ │ │
|
|
│ └─ Portfolio │ ║ │ Join us │ ║ │ Layout: │
|
|
│ │ ║ └──────────────┘ ║ │ [Hero-Left ▼] │
|
|
│ [+ Create] │ ║ ║ │ │
|
|
│ │ ║ ┌──────────────┐ ║ │ Title: │
|
|
│ │ ║ │ IMAGE+TEXT │ ║ │ [Welcome______] │
|
|
│ │ ║ │ [IMG] [TEXT] │ ║ │ │
|
|
│ │ ║ └──────────────┘ ║ │ Image: │
|
|
│ │ ║ ║ │ [Choose Image ▼] │
|
|
│ │ ║ ┌──────────────┐ ║ │ │
|
|
│ │ ║ │ CTA │ ║ │ [Delete Section] │
|
|
│ │ ║ │ "Shop Now" │ ║ │ │
|
|
│ │ ║ └──────────────┘ ║ │ [Hide Inspector] │
|
|
│ │ ║ ║ │ │
|
|
│ │ ║ [+ Add section] ║ │ │
|
|
│ │ ╚════════════════════╝ │ │
|
|
│ │ │ │
|
|
│ │ Responsive toggle: │ │
|
|
│ │ [Desktop] [Mobile] │ │
|
|
│ │ │ │
|
|
└─────────────────┴────────────────────────┴──────────────────────┘
|
|
│ [Discard Changes] [Save Changes] [Preview in New Tab] │
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Key Differences from v3
|
|
|
|
| Aspect | v3 (Card List) | v4 (Live Canvas) |
|
|
|--------|---|---|
|
|
| **Main Column** | Card list (read-only) | Live rendered sections |
|
|
| **Rendering** | Screenshot/preview | Actual React components |
|
|
| **Interactivity** | Click card → settings | Click section → settings |
|
|
| **Visual Feedback** | Static cards | Actual page appearance |
|
|
| **Preview Size** | Small box on right | Full mobile/desktop |
|
|
| **Edit Speed** | Click → settings → change | See change instantly |
|
|
| **Context** | List + Settings + Preview | One coherent interface |
|
|
|
|
---
|
|
|
|
## Canvas Rendering System (JSON → React Components)
|
|
|
|
### Architecture: Zero-Iframe, Direct Component Rendering
|
|
|
|
```
|
|
┌──────────────────────────────────────────┐
|
|
│ Page Editor State (Redux/Zustand) │
|
|
│ { │
|
|
│ sections: [ │
|
|
│ { type: "hero", props: {...} }, │
|
|
│ { type: "content", props: {...} } │
|
|
│ ], │
|
|
│ selectedSectionId: "sec-1" │
|
|
│ } │
|
|
└──────────────────────────────────────────┘
|
|
↓ (Real-time)
|
|
┌──────────────────────────────────────────┐
|
|
│ CanvasRenderer Component │
|
|
│ (Renders JSON → React components) │
|
|
│ │
|
|
│ { sections.map((sec) => │
|
|
│ <SectionComponent │
|
|
│ data={sec} │
|
|
│ isSelected={...} │
|
|
│ onSelect={...} │
|
|
│ /> │
|
|
│ )} │
|
|
└──────────────────────────────────────────┘
|
|
↓
|
|
┌──────────────────────────────────────────┐
|
|
│ Live Canvas Preview │
|
|
│ (Shows exact front-end appearance) │
|
|
│ │
|
|
│ ╔═══════════════════════════════════╗ │
|
|
│ ║ HERO SECTION ║ │
|
|
│ ║ (Selected, blue border) ║ │
|
|
│ ╚═══════════════════════════════════╝ │
|
|
│ │
|
|
│ ┌───────────────────────────────────┐ │
|
|
│ │ IMAGE + TEXT │ │
|
|
│ │ (Not selected, gray border) │ │
|
|
│ └───────────────────────────────────┘ │
|
|
└──────────────────────────────────────────┘
|
|
```
|
|
|
|
### Why This Is Simpler Than Page Builder Plugins
|
|
|
|
**Page Builder Plugins (e.g., Elementor, Beaver):**
|
|
- Use iframes to sandbox editing environment
|
|
- Must observe DOM mutations to track changes
|
|
- Heavy JavaScript overhead
|
|
- Slow preview updates
|
|
|
|
**WooNooW Approach:**
|
|
- ✅ JSON state → React components (unidirectional)
|
|
- ✅ No iframe = no sandbox overhead
|
|
- ✅ No mutation observers = simple state management
|
|
- ✅ Change state → re-render → instant preview
|
|
- ✅ Single component tree (not isolated)
|
|
|
|
---
|
|
|
|
## Canvas Interaction Model
|
|
|
|
### Section States
|
|
|
|
**At Rest (Not Selected):**
|
|
```
|
|
┌─────────────────────────────────┐
|
|
│ Hero Section │ ← Subtle border (gray)
|
|
│ [Actual rendered content] │ ← Hover shows subtle highlight
|
|
│ "Welcome to our store" │ ← Cursor change on hover
|
|
│ [Featured image] │
|
|
└─────────────────────────────────┘
|
|
```
|
|
|
|
**On Hover:**
|
|
```
|
|
╔════════════════════════════════╗
|
|
║ Hero Section ║ ← Blue border appears
|
|
║ [Actual rendered content] ║ ← Handles show: [↑][↓][◆]
|
|
║ "Welcome to our store" ║ ← "Click to edit" hint
|
|
║ [Featured image] ║
|
|
║ Drag handle: [:::] ║ ← Reorder handle visible
|
|
╚════════════════════════════════╝
|
|
```
|
|
|
|
**On Click (Selected):**
|
|
```
|
|
╔════════════════════════════════╗
|
|
║ Hero Section ║ ← Bold blue border
|
|
║ [Actual rendered content] ║ ← Selection handles
|
|
║ "Welcome to our store" ║ ← Full edit mode
|
|
║ [Featured image] ║ ← Right panel populated
|
|
║ [≣ Move ↑ ↓] [⊕ Duplicate] [✕]║ ← Context menu
|
|
╚════════════════════════════════╝
|
|
↓ Right panel updates:
|
|
Section Config
|
|
├─ Layout: [Hero-Left ▼]
|
|
├─ Color: [Primary ▼]
|
|
├─ Title: [Welcome__________]
|
|
└─ Image: [Choose Image ▼]
|
|
```
|
|
|
|
### Interaction Handlers
|
|
|
|
```typescript
|
|
// Canvas Section Component
|
|
<CanvasSection
|
|
section={section}
|
|
isSelected={selectedId === section.id}
|
|
|
|
// Selection
|
|
onClick={() => setSelectedSection(section.id)}
|
|
onMouseEnter={() => setHoveredId(section.id)}
|
|
onMouseLeave={() => setHoveredId(null)}
|
|
|
|
// Reordering
|
|
onDragStart={() => initDragReorder(section.id)}
|
|
onDragOver={(e) => e.preventDefault()}
|
|
onDrop={() => performDragReorder(section.id)}
|
|
|
|
// Quick actions
|
|
onDuplicate={() => duplicateSection(section.id)}
|
|
onDelete={() => deleteSection(section.id)}
|
|
onMoveUp={() => moveSection(section.id, 'up')}
|
|
onMoveDown={() => moveSection(section.id, 'down')}
|
|
/>
|
|
```
|
|
|
|
---
|
|
|
|
## Inspector Panel (Right Side)
|
|
|
|
### Inspector Behavior
|
|
|
|
**Collapsible:**
|
|
```
|
|
┌──────────────────┐
|
|
│ [Inspector] [◄] │ ← Click to collapse (inspector slides out)
|
|
│ │
|
|
│ Section Config │
|
|
│ ├─ Layout: [▼] │
|
|
│ ├─ Title: [____] │
|
|
│ └─ Image: [▼] │
|
|
└──────────────────┘
|
|
|
|
When collapsed:
|
|
[Canvas takes 70% width instead of 60%]
|
|
```
|
|
|
|
**Responsive to Selection:**
|
|
```
|
|
No section selected:
|
|
┌──────────────────┐
|
|
│ Page Settings │
|
|
├─ Title: [About] │
|
|
├─ Slug: [/about] │
|
|
└─ SEO: [Manage→] │
|
|
└──────────────────┘
|
|
|
|
Hero section selected:
|
|
┌──────────────────┐
|
|
│ Section Config │
|
|
│ Type: Hero │
|
|
├─ Layout: [▼] │
|
|
├─ Title: [____] │
|
|
└─ Image: [▼] │
|
|
└──────────────────┘
|
|
|
|
Content section selected:
|
|
┌──────────────────┐
|
|
│ Section Config │
|
|
│ Type: Content │
|
|
├─ Source: [▼] │ ← Dynamic: post_content
|
|
│ ○ Static text │
|
|
│ ◉ Post Body │
|
|
└─ [Dynamic ◆] │
|
|
└──────────────────┘
|
|
```
|
|
|
|
### Inspector Field Types
|
|
|
|
**For Static Fields:**
|
|
```
|
|
Title Field
|
|
[Text input____________]
|
|
|
|
Description
|
|
[Large textarea___
|
|
_________________
|
|
_________________]
|
|
|
|
Image
|
|
[Choose from Media] or [Upload new]
|
|
[Preview thumbnail]
|
|
```
|
|
|
|
**For Dynamic Fields (CPT Templates):**
|
|
```
|
|
Title Source
|
|
○ Static text: [__________]
|
|
◉ Dynamic: [Post Title ▼]
|
|
• Post Title
|
|
• Post Excerpt
|
|
• Custom Field X
|
|
|
|
[◆ Dynamic] indicator shown
|
|
```
|
|
|
|
**Layout Controls:**
|
|
```
|
|
Layout Variant
|
|
[Hero-Left ▼]
|
|
├─ Hero-Left
|
|
├─ Hero-Right
|
|
├─ Hero-Center
|
|
└─ Hero-Full
|
|
|
|
Color Scheme
|
|
[Primary ▼]
|
|
├─ Primary
|
|
├─ Secondary
|
|
├─ Accent
|
|
└─ Dark
|
|
```
|
|
|
|
---
|
|
|
|
## Canvas UI Features
|
|
|
|
### Section Card Appearance
|
|
|
|
**In Canvas:**
|
|
```
|
|
Rendered like the actual front-end:
|
|
|
|
╔═══════════════════════════════════╗
|
|
║ HERO SECTION ║ ← Looks like real page
|
|
║ ║
|
|
║ Welcome to our store ║ ← Actual text
|
|
║ ┌─────────────────────────────┐ ║
|
|
║ │ [Featured Image] │ ║ ← Actual image
|
|
║ │ (500x300px) │ ║
|
|
║ └─────────────────────────────┘ ║
|
|
║ ║
|
|
║ Join our community today ║ ← Button
|
|
║ [SHOP NOW →] ║
|
|
║ ║
|
|
╚═══════════════════════════════════╝
|
|
```
|
|
|
|
NOT like current cards:
|
|
```
|
|
❌ WRONG (Current):
|
|
┌─────────────────────────┐
|
|
│ Hero [default] │
|
|
│ [↑] [↓] [x] │
|
|
│ ─────────────────────── │
|
|
```
|
|
|
|
### Add Section UI
|
|
|
|
**Floating Button Below Sections:**
|
|
```
|
|
Canvas shows sections:
|
|
|
|
╔═══════════════════════════════════╗
|
|
║ HERO SECTION ║
|
|
╚═══════════════════════════════════╝
|
|
|
|
[+ Add section] ← Click to open section picker
|
|
├─ Hero
|
|
├─ Image + Text
|
|
├─ Feature Grid
|
|
├─ Related Items
|
|
├─ CTA Banner
|
|
└─ Content
|
|
```
|
|
|
|
Or inline:
|
|
|
|
```
|
|
╔═══════════════════════════════════╗
|
|
║ CTA BANNER ║
|
|
╚═══════════════════════════════════╝
|
|
|
|
[+ Add section below] ← Click here
|
|
```
|
|
|
|
### Responsive Preview Toggle
|
|
|
|
**Canvas shows device toggle:**
|
|
```
|
|
[Desktop] [Mobile] ← Toggle between previews
|
|
|
|
Desktop (full width, e.g., 1024px)
|
|
╔═══════════════════════════════════╗
|
|
║ Page at desktop width ║
|
|
╚═══════════════════════════════════╝
|
|
|
|
Mobile (narrow width, e.g., 375px)
|
|
╔════════════╗
|
|
║ Page at ║
|
|
║ mobile ║
|
|
║ width ║
|
|
╚════════════╝
|
|
```
|
|
|
|
---
|
|
|
|
## Data Flow & State Management
|
|
|
|
### Single Source of Truth
|
|
|
|
```
|
|
┌─────────────────────────────────┐
|
|
│ Editor State (Zustand/Redux) │
|
|
│ │
|
|
│ { │
|
|
│ pageId: "about", │
|
|
│ pageType: "page", │
|
|
│ title: "About Us", │
|
|
│ slug: "about", │
|
|
│ sections: [ │
|
|
│ { │
|
|
│ id: "sec-1", │
|
|
│ type: "hero", │
|
|
│ layoutVariant: "left", │
|
|
│ props: { │
|
|
│ title: { │
|
|
│ type: "static", │
|
|
│ value: "Welcome" │
|
|
│ }, │
|
|
│ image: { │
|
|
│ type: "static", │
|
|
│ value: "/img.jpg" │
|
|
│ } │
|
|
│ } │
|
|
│ } │
|
|
│ ], │
|
|
│ selectedSectionId: "sec-1", │
|
|
│ hasUnsavedChanges: true │
|
|
│ } │
|
|
└─────────────────────────────────┘
|
|
↓ (Consumed by):
|
|
|
|
├─ CanvasRenderer
|
|
│ └─ Renders sections to UI
|
|
│
|
|
├─ InspectorPanel
|
|
│ └─ Shows/edits selected section
|
|
│
|
|
├─ PreviewToggle
|
|
│ └─ Desktop/Mobile responsive
|
|
│
|
|
└─ SaveButton
|
|
└─ POST to /wp-json/woonoow/v1/pages/{slug}
|
|
```
|
|
|
|
### Real-Time Updates
|
|
|
|
```
|
|
User changes field in Inspector:
|
|
[Title input] → onChange event
|
|
↓
|
|
Store update:
|
|
sections[0].props.title.value = "New Title"
|
|
↓
|
|
React re-renders:
|
|
<CanvasRenderer sections={sections} />
|
|
↓
|
|
Canvas shows updated title instantly
|
|
(No save needed, live preview)
|
|
```
|
|
|
|
---
|
|
|
|
## Component Structure
|
|
|
|
### Main Components
|
|
|
|
```
|
|
<PageEditor> ← Top container
|
|
├─ <Sidebar> ← Left navigation
|
|
│ ├─ <PagesList> ← Structural pages
|
|
│ └─ <TemplatesList> ← CPT templates
|
|
│
|
|
├─ <Canvas> ← Main workspace
|
|
│ ├─ <ResponsiveToggle> ← Desktop/Mobile
|
|
│ └─ <CanvasRenderer> ← Renders sections
|
|
│ ├─ <CanvasSection> ← Each section
|
|
│ │ ├─ <HeroRenderer> ← Section-specific
|
|
│ │ ├─ <ContentRenderer>
|
|
│ │ ├─ <FeatureGridRenderer>
|
|
│ │ └─ ...
|
|
│ └─ <AddSectionButton>
|
|
│
|
|
├─ <Inspector> ← Right panel
|
|
│ ├─ <PageSettings> ← When no section selected
|
|
│ ├─ <SectionSettings> ← When section selected
|
|
│ └─ <PreviewToggle> ← Collapse button
|
|
│
|
|
└─ <BottomBar> ← Actions
|
|
├─ <DiscardButton>
|
|
├─ <SaveButton>
|
|
└─ <PreviewButton>
|
|
```
|
|
|
|
### Canvas Section Component Pattern
|
|
|
|
```tsx
|
|
// Each section type follows same pattern:
|
|
|
|
function HeroSectionRenderer({ section, isSelected, onSelect }) {
|
|
return (
|
|
<CanvasSection
|
|
isSelected={isSelected}
|
|
onClick={onSelect}
|
|
className="section-hero"
|
|
>
|
|
{/* Renders actual content */}
|
|
<h1>{section.props.title.value}</h1>
|
|
<img src={section.props.image.value} />
|
|
<p>{section.props.subtitle.value}</p>
|
|
|
|
{/* Selection UI overlays */}
|
|
{isSelected && (
|
|
<SelectionOverlay>
|
|
<DragHandle />
|
|
<ContextMenu />
|
|
</SelectionOverlay>
|
|
)}
|
|
</CanvasSection>
|
|
);
|
|
}
|
|
|
|
// Same for Content, FeatureGrid, CTA, etc.
|
|
```
|
|
|
|
---
|
|
|
|
## Selection & Interaction Feedback
|
|
|
|
### Visual Selection Indicators
|
|
|
|
**Unselected:**
|
|
```
|
|
┌─────────────────────────────┐
|
|
│ Hero Section │ ← Gray border (1px)
|
|
│ [Content] │ ← Normal opacity
|
|
└─────────────────────────────┘
|
|
```
|
|
|
|
**Hovered:**
|
|
```
|
|
┌─────────────────────────────┐
|
|
│ Hero Section │ ← Light blue border (1px)
|
|
│ [Content] │ ← Subtle shadow
|
|
│ [↑ ↓] │ ← Move handles appear
|
|
└─────────────────────────────┘
|
|
```
|
|
|
|
**Selected:**
|
|
```
|
|
╔═════════════════════════════╗
|
|
║ Hero Section ║ ← Bold blue border (2px)
|
|
║ [Content] ║ ← Highlight background
|
|
║ ║
|
|
║ [≣] [↑↓] [⊕] [✕] ║ ← Full action menu
|
|
╚═════════════════════════════╝
|
|
```
|
|
|
|
### Drag & Drop Reordering
|
|
|
|
```
|
|
// User drags section up:
|
|
|
|
Before:
|
|
╔═══════════════════╗
|
|
║ HERO (drag me) ║ ← Dragging
|
|
║ [↑ ↓] ║
|
|
╚═══════════════════╝
|
|
|
|
╔═══════════════════╗
|
|
║ IMAGE+TEXT ║ ← Drop zone appears
|
|
║ [↑ ↓] ║
|
|
╚═══════════════════╝
|
|
|
|
After drop:
|
|
╔═══════════════════╗
|
|
║ HERO ║
|
|
╚═══════════════════╝
|
|
|
|
╔═══════════════════╗
|
|
║ IMAGE+TEXT ║
|
|
╚═══════════════════╝
|
|
```
|
|
|
|
---
|
|
|
|
## Save & Persistence Flow
|
|
|
|
### Auto-Save vs Manual Save
|
|
|
|
**Current approach: Manual Save**
|
|
```
|
|
User edits → Real-time canvas update (no server call)
|
|
[Save Changes] button → POST to API → Save to wp_options/postmeta
|
|
[Discard Changes] button → Reset to last saved state
|
|
```
|
|
|
|
**Optional: Auto-Save (Future)**
|
|
```
|
|
User edits → Real-time canvas update
|
|
[Unsaved changes indicator] appears
|
|
Auto-save after 3 seconds of inactivity
|
|
Or manual save with [Save] button
|
|
```
|
|
|
|
### Save Endpoints
|
|
|
|
```
|
|
POST /wp-json/woonoow/v1/pages/about
|
|
{
|
|
"sections": [...]
|
|
}
|
|
→ Saves to wp_postmeta (_wn_page_structure)
|
|
|
|
POST /wp-json/woonoow/v1/templates/post
|
|
{
|
|
"sections": [...]
|
|
}
|
|
→ Saves to wp_options (wn_template_post)
|
|
```
|
|
|
|
---
|
|
|
|
## Responsive Canvas Behavior
|
|
|
|
### Desktop View (Default)
|
|
|
|
```
|
|
Full-width canvas showing page at 1024px width
|
|
|
|
┌─────────────────────────────────┐
|
|
│ [Desktop] [Mobile] │
|
|
│ │
|
|
│ Canvas width: 1024px │
|
|
│ Shows full page layout │
|
|
└─────────────────────────────────┘
|
|
```
|
|
|
|
### Mobile View
|
|
|
|
```
|
|
Canvas showing page at 375px width
|
|
|
|
┌──────────┐
|
|
│[D][M] │
|
|
│ │
|
|
│ Canvas │
|
|
│ width: │
|
|
│ 375px │
|
|
│ │
|
|
│ Shows │
|
|
│ mobile │
|
|
│ layout │
|
|
│ │
|
|
└──────────┘
|
|
```
|
|
|
|
### Scroll Behavior
|
|
|
|
- Canvas is scrollable (overflow-y: auto)
|
|
- Sections stack vertically
|
|
- Inspector panel stays fixed (top-right)
|
|
- Bottom action bar always visible
|
|
|
|
---
|
|
|
|
## Merchant Experience Flow
|
|
|
|
### Typical Edit Session
|
|
|
|
1. **Select page from sidebar**
|
|
```
|
|
Sidebar: [About Us] ← Click
|
|
Canvas loads page structure
|
|
Inspector shows page settings
|
|
```
|
|
|
|
2. **Click section to edit**
|
|
```
|
|
Canvas: [Hero section] ← Click
|
|
Section highlights (blue border)
|
|
Inspector updates: Section Config for Hero
|
|
```
|
|
|
|
3. **Change fields in Inspector**
|
|
```
|
|
Inspector: Title = [Welcome_____] ← Type "Welcome to us"
|
|
Canvas updates instantly
|
|
No server call needed yet
|
|
```
|
|
|
|
4. **Reorder sections (optional)**
|
|
```
|
|
Canvas: Drag [Hero] above [Image+Text]
|
|
Sections reorder instantly
|
|
```
|
|
|
|
5. **Add new section**
|
|
```
|
|
Canvas: [+ Add section] ← Click
|
|
Section picker popup
|
|
Select "CTA Banner"
|
|
New section appears at bottom
|
|
Inspector shows CTA settings
|
|
```
|
|
|
|
6. **Save all changes**
|
|
```
|
|
Bottom bar: [Save Changes] ← Click
|
|
POST to /wp-json/woonoow/v1/pages/about
|
|
Sections saved to database
|
|
"Page saved" toast notification
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Phases (Enhanced)
|
|
|
|
### Phase 1: Canvas Rendering System (Priority)
|
|
- [ ] Refactor state from Redux/Zustand (unified store)
|
|
- [ ] Build `CanvasRenderer` component
|
|
- Maps sections array to React components
|
|
- Zero-iframe approach (direct rendering)
|
|
- Responsive device simulation (CSS media queries)
|
|
- [ ] Build section renderers (Hero, Content, Feature Grid, CTA, Related)
|
|
- Render actual content, not placeholder cards
|
|
- Show dynamic placeholders with [◆] badges
|
|
- [ ] Add responsive toggle (Desktop/Mobile)
|
|
- [ ] Test real-time re-rendering on state changes
|
|
|
|
### Phase 2: Canvas Interaction (Click, Hover, Select)
|
|
- [ ] Click handler → Set selected section in state
|
|
- [ ] Hover handler → Show selection handles
|
|
- [ ] Visual feedback (borders, shadows, highlights)
|
|
- [ ] Context menu (Move up, Down, Duplicate, Delete)
|
|
- [ ] Add section button (opens section picker)
|
|
|
|
### Phase 3: Inspector Panel (Settings)
|
|
- [ ] Collapse/expand button
|
|
- [ ] Page settings vs Section settings
|
|
- [ ] Dynamic field type detection
|
|
- Static: text input, textarea, image picker
|
|
- Dynamic: dropdown with available sources
|
|
- [ ] Real-time sync (change field → canvas updates)
|
|
|
|
### Phase 4: Drag & Drop Reordering
|
|
- [ ] Drag start/over/drop handlers
|
|
- [ ] Visual drag feedback (opacity, shadow)
|
|
- [ ] Reorder state updates
|
|
- [ ] Test with multiple sections
|
|
|
|
### Phase 5: Polish & Launch
|
|
- [ ] Save/Discard buttons
|
|
- [ ] Unsaved changes warning
|
|
- [ ] Toast notifications (saved, error)
|
|
- [ ] Keyboard shortcuts (ESC to deselect, etc.)
|
|
- [ ] Mobile-friendly inspector (for tablet editing)
|
|
- [ ] Performance optimization (memoization, lazy rendering)
|
|
|
|
---
|
|
|
|
## Code Example: Canvas Component Structure
|
|
|
|
```tsx
|
|
// pages/editor/PageEditor.tsx
|
|
export function PageEditor() {
|
|
const [pageState, dispatch] = useReducer(pageReducer, initialState);
|
|
const [selectedSectionId, setSelectedSectionId] = useState(null);
|
|
const [deviceMode, setDeviceMode] = useState('desktop');
|
|
|
|
const handleSectionSelect = (sectionId) => {
|
|
setSelectedSectionId(sectionId);
|
|
};
|
|
|
|
const handleSectionDelete = (sectionId) => {
|
|
dispatch({
|
|
type: 'DELETE_SECTION',
|
|
payload: { sectionId }
|
|
});
|
|
};
|
|
|
|
const handleSectionDuplicate = (sectionId) => {
|
|
dispatch({
|
|
type: 'DUPLICATE_SECTION',
|
|
payload: { sectionId }
|
|
});
|
|
};
|
|
|
|
const handleFieldChange = (sectionId, fieldPath, value) => {
|
|
dispatch({
|
|
type: 'UPDATE_SECTION_PROP',
|
|
payload: { sectionId, fieldPath, value }
|
|
});
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
const response = await fetch(`/wp-json/woonoow/v1/pages/${pageState.slug}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ sections: pageState.sections })
|
|
});
|
|
// Show toast notification
|
|
};
|
|
|
|
return (
|
|
<div className="page-editor">
|
|
<Sidebar
|
|
pages={pageState.pages}
|
|
selectedPageId={pageState.id}
|
|
/>
|
|
|
|
<Canvas
|
|
sections={pageState.sections}
|
|
selectedSectionId={selectedSectionId}
|
|
deviceMode={deviceMode}
|
|
onSelectSection={handleSectionSelect}
|
|
onDeleteSection={handleSectionDelete}
|
|
onDuplicateSection={handleSectionDuplicate}
|
|
onFieldChange={handleFieldChange}
|
|
/>
|
|
|
|
<Inspector
|
|
pageState={pageState}
|
|
selectedSectionId={selectedSectionId}
|
|
onFieldChange={handleFieldChange}
|
|
onDeviceChange={setDeviceMode}
|
|
/>
|
|
|
|
<BottomBar
|
|
onSave={handleSave}
|
|
onDiscard={() => location.reload()}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// components/Canvas.tsx
|
|
export function Canvas({
|
|
sections,
|
|
selectedSectionId,
|
|
deviceMode,
|
|
onSelectSection,
|
|
onFieldChange,
|
|
...handlers
|
|
}) {
|
|
return (
|
|
<div className="canvas-container">
|
|
<DeviceToggle mode={deviceMode} onChange={setDeviceMode} />
|
|
|
|
<div className={`canvas-viewport canvas-${deviceMode}`}>
|
|
{sections.map((section) => (
|
|
<CanvasSection
|
|
key={section.id}
|
|
section={section}
|
|
isSelected={selectedSectionId === section.id}
|
|
onClick={() => onSelectSection(section.id)}
|
|
{...handlers}
|
|
>
|
|
<SectionRenderer
|
|
section={section}
|
|
onFieldChange={(field, value) =>
|
|
onFieldChange(section.id, field, value)
|
|
}
|
|
/>
|
|
</CanvasSection>
|
|
))}
|
|
|
|
<AddSectionButton onClick={openSectionPicker} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// components/CanvasSection.tsx
|
|
export function CanvasSection({ section, isSelected, children, ...handlers }) {
|
|
return (
|
|
<div
|
|
className={`canvas-section ${isSelected ? 'selected' : ''}`}
|
|
onClick={handlers.onClick}
|
|
onDragStart={handlers.onDragStart}
|
|
onDragOver={handlers.onDragOver}
|
|
onDrop={handlers.onDrop}
|
|
>
|
|
{/* Actual rendered content */}
|
|
{children}
|
|
|
|
{/* Selection overlay */}
|
|
{isSelected && (
|
|
<SelectionOverlay>
|
|
<DragHandle />
|
|
<ContextMenu
|
|
onDuplicate={() => handlers.onDuplicate(section.id)}
|
|
onDelete={() => handlers.onDelete(section.id)}
|
|
/>
|
|
</SelectionOverlay>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// components/Inspector.tsx
|
|
export function Inspector({ pageState, selectedSectionId, onFieldChange }) {
|
|
if (!selectedSectionId) {
|
|
return <PageSettings pageState={pageState} />;
|
|
}
|
|
|
|
const section = pageState.sections.find(s => s.id === selectedSectionId);
|
|
|
|
return (
|
|
<div className="inspector-panel">
|
|
<div className="section-config">
|
|
<h3>{section.type}</h3>
|
|
|
|
{Object.entries(section.props).map(([fieldName, fieldData]) => (
|
|
<InspectorField
|
|
key={fieldName}
|
|
fieldName={fieldName}
|
|
fieldData={fieldData}
|
|
onChange={(value) =>
|
|
onFieldChange(selectedSectionId, `props.${fieldName}.value`, value)
|
|
}
|
|
/>
|
|
))}
|
|
|
|
<button onClick={() => handleDelete(selectedSectionId)}>
|
|
Delete Section
|
|
</button>
|
|
</div>
|
|
|
|
<Canvas preview />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// components/InspectorField.tsx
|
|
export function InspectorField({ fieldName, fieldData, onChange }) {
|
|
if (fieldData.type === 'static') {
|
|
return (
|
|
<div className="inspector-field">
|
|
<label>{fieldName}</label>
|
|
<input
|
|
type="text"
|
|
value={fieldData.value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (fieldData.type === 'dynamic') {
|
|
return (
|
|
<div className="inspector-field">
|
|
<label>{fieldName} <span className="dynamic-badge">[◆]</span></label>
|
|
<select value={fieldData.source} onChange={(e) => onChange(e.target.value)}>
|
|
<option value="post_title">Post Title</option>
|
|
<option value="post_content">Post Content</option>
|
|
<option value="post_featured_image">Featured Image</option>
|
|
{/* More options */}
|
|
</select>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## CSS Utilities for Canvas
|
|
|
|
```css
|
|
/* Canvas container */
|
|
.canvas-container {
|
|
flex: 1;
|
|
background: #f5f5f5;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
/* Device modes */
|
|
.canvas-viewport {
|
|
background: white;
|
|
margin: 0 auto;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.canvas-desktop {
|
|
max-width: 1024px;
|
|
}
|
|
|
|
.canvas-mobile {
|
|
max-width: 375px;
|
|
}
|
|
|
|
/* Sections */
|
|
.canvas-section {
|
|
position: relative;
|
|
padding: 0;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
margin-bottom: 16px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.canvas-section:hover {
|
|
border-color: #90caf9;
|
|
box-shadow: 0 2px 8px rgba(144, 202, 249, 0.3);
|
|
}
|
|
|
|
.canvas-section.selected {
|
|
border-color: #1976d2;
|
|
border-width: 2px;
|
|
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
|
|
}
|
|
|
|
/* Selection overlay */
|
|
.selection-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
display: flex;
|
|
gap: 8px;
|
|
padding: 8px;
|
|
background: rgba(25, 118, 210, 0.05);
|
|
border-radius: 4px;
|
|
opacity: 0;
|
|
}
|
|
|
|
.canvas-section.selected .selection-overlay {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Dynamic badge */
|
|
.dynamic-badge {
|
|
display: inline-block;
|
|
width: 14px;
|
|
height: 14px;
|
|
background: #ff9800;
|
|
color: white;
|
|
border-radius: 50%;
|
|
text-align: center;
|
|
font-size: 10px;
|
|
line-height: 14px;
|
|
margin-left: 4px;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Benefits of This Approach
|
|
|
|
| Benefit | Explanation |
|
|
|---------|---|
|
|
| **WYSIWYG** | See actual page appearance while editing |
|
|
| **No iframe overhead** | Direct component rendering = faster updates |
|
|
| **No MutationObserver** | JSON → React = clean state management |
|
|
| **Instant feedback** | Change field → canvas updates in milliseconds |
|
|
| **Simpler code** | No iframe communication, no sandbox complexity |
|
|
| **Better mobile edit** | Responsive canvas shows mobile view directly |
|
|
| **Easier debugging** | Single React component tree, standard dev tools |
|
|
| **Faster iteration** | Less complexity = faster implementation |
|
|
| **Merchant friendly** | See exactly what customers will see |
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. ✅ Refactor state management (unified store)
|
|
2. ✅ Build CanvasRenderer component
|
|
3. ✅ Implement section click/hover interactions
|
|
4. ✅ Connect Inspector panel to canvas
|
|
5. ✅ Add drag-and-drop reordering
|
|
6. ✅ Test responsive device modes
|
|
7. ✅ Implement save/discard flow
|
|
8. ✅ Polish UX (animations, feedback)
|