# 🧭 WooNooW — Single Source of Truth (S.O.P.)

This document defines the **Standard Operating Procedure** for developing, maintaining, and collaborating on the **WooNooW** project — ensuring every AI Agent or human collaborator follows the same workflow and conventions.

---

## 1. 🎯 Project Intent

WooNooW modernizes WooCommerce **without migration**, delivering a Hybrid + SPA experience for both **storefront** and **admin**, while keeping compatibility with legacy WooCommerce addons.

> **Goal:** “Reimagine WooCommerce for now — faster, modern, reversible.”

---

## 1.1 📝 Documentation Standards

### Progress & Testing Documentation

**All progress notes and reports MUST be added to:**
- `PROGRESS_NOTE.md` - Consolidated progress tracking with timestamps

**All test checklists MUST be added to:**
- `TESTING_CHECKLIST.md` - Comprehensive testing requirements

**Feature-specific documentation:**
- Create dedicated `.md` files for major features (e.g., `PAYMENT_GATEWAY_INTEGRATION.md`)
- Link to these files from `PROGRESS_NOTE.md`
- Include implementation details, code examples, and testing steps

**API Routes documentation:**
- `API_ROUTES.md` - Complete registry of all REST API routes
- **MUST be updated** when adding new API endpoints
- Prevents route conflicts between modules
- Documents ownership and naming conventions

**Metabox & Custom Fields compatibility:**
- `METABOX_COMPAT.md` - 🔴 **CRITICAL** compatibility requirement
- Documents how to expose WordPress/WooCommerce metaboxes in SPA
- **Currently NOT implemented** - blocks production readiness
- Required for third-party plugin compatibility (Shipment Tracking, ACF, etc.)

**Documentation Rules:**
1. ✅ Update `PROGRESS_NOTE.md` after completing any major feature
2. ✅ Add test cases to `TESTING_CHECKLIST.md` before implementation
3. ✅ Use consistent formatting (emojis, headings, code blocks)
4. ✅ Include "Last synced" timestamp in GMT+7
5. ✅ Reference file paths and line numbers for code changes

---

## 2. 🧱 Core Principles

1. **Zero Data Migration** — All data remains in WooCommerce’s database schema.  
2. **Safe Activation/Deactivation** — Deactivating WooNooW restores vanilla Woo instantly.  
3. **HPOS-First Architecture** — Mandatory use of WooCommerce HPOS.  
4. **Hybrid by Default** — SSR + React SPA islands for Cart, Checkout, and My‑Account.  
5. **Full SPA Option** — Optional React-only mode for performance-critical sites.  
6. **Compat Layer** — HookBridge & SlotRenderer preserve legacy addon behavior.  
7. **Async System** — MailQueue & async actions replace blocking PHP tasks.

---

## 3. ⚙️ Tech Stack Reference

| Layer | Technology |
|-------|-------------|
| Backend | PHP 8.2+, WordPress, WooCommerce (HPOS), Action Scheduler |
| Frontend | React 18 + TypeScript, Vite, React Query, Tailwind CSS + Shadcn UI, Recharts |
| Architecture | Modular PSR‑4 autoload, REST‑driven logic, SPA hydration islands |
| Routing | Admin SPA: HashRouter, Customer SPA: HashRouter |
| Build | Composer + NPM + ESM scripts |
| Packaging | `scripts/package-zip.mjs` |
| Deployment | LocalWP for dev, Coolify for staging |

---

## 3.1 🔀 Customer SPA Routing Pattern

### HashRouter Implementation

**Why HashRouter?**

The Customer SPA uses **HashRouter** instead of BrowserRouter to avoid conflicts with WordPress routing:

```typescript
// customer-spa/src/App.tsx
import { HashRouter } from 'react-router-dom';

<HashRouter>
  <Routes>
    <Route path="/product/:slug" element={<Product />} />
    <Route path="/cart" element={<Cart />} />
    {/* ... */}
  </Routes>
</HashRouter>
```

**URL Format:**
```
Shop:     https://example.com/shop#/
Product:  https://example.com/shop#/product/product-slug
Cart:     https://example.com/shop#/cart
Checkout: https://example.com/shop#/checkout
Account:  https://example.com/shop#/my-account
```

**How It Works:**

1. **WordPress loads:** `/shop` (valid WordPress page)
2. **React takes over:** `#/product/product-slug` (client-side only)
3. **No conflicts:** Everything after `#` is invisible to WordPress

**Benefits:**

| Benefit | Description |
|---------|-------------|
| **Zero WordPress conflicts** | WordPress never sees routes after `#` |
| **Direct URL access** | Works from any source (email, social, QR codes) |
| **Shareable links** | Perfect for marketing campaigns |
| **No server config** | No .htaccess or rewrite rules needed |
| **Reliable** | No canonical redirects or 404 issues |
| **Consistent with Admin SPA** | Same routing approach |

**Use Cases:**

✅ **Email campaigns:** `https://example.com/shop#/product/special-offer`
✅ **Social media:** Share product links directly
✅ **QR codes:** Generate codes for products
✅ **Bookmarks:** Users can bookmark product pages
✅ **Direct access:** Type URL in browser

**Implementation Rules:**

1. ✅ **Always use HashRouter** for Customer SPA
2. ✅ **Use React Router Link** components (automatically use hash URLs)
3. ✅ **Test direct URL access** for all routes
4. ✅ **Document URL format** in user guides
5. ❌ **Never use BrowserRouter** (causes WordPress conflicts)
6. ❌ **Never try to override WordPress routes** (unreliable)

**Comparison: BrowserRouter vs HashRouter**

| Feature | BrowserRouter | HashRouter |
|---------|---------------|------------|
| **URL Format** | `/product/slug` | `#/product/slug` |
| **Clean URLs** | ✅ Yes | ❌ Has `#` |
| **SEO** | ✅ Better | ⚠️ Acceptable |
| **Direct Access** | ❌ Conflicts | ✅ Works |
| **WordPress Conflicts** | ❌ Many | ✅ None |
| **Sharing** | ❌ Unreliable | ✅ Reliable |
| **Email Links** | ❌ Breaks | ✅ Works |
| **Setup Complexity** | ❌ Complex | ✅ Simple |
| **Reliability** | ❌ Fragile | ✅ Solid |

**Winner:** HashRouter for Customer SPA ✅

**SEO Considerations:**

- WooCommerce product pages still exist for SEO
- Search engines index actual product URLs
- SPA provides better UX for users
- Canonical tags point to real products
- Best of both worlds approach

**Files:**
- `customer-spa/src/App.tsx` - HashRouter configuration
- `customer-spa/src/pages/*` - All page components use React Router

---

## 4. 🧩 Folder Structure

```
woonoow/
├─ woonoow.php              # main plugin file (WordPress entry)
├─ includes/                # PSR‑4 classes
│  ├─ Core/                 # Bootstrap, Datastores, Mail, Hooks
│  ├─ Api/                  # REST endpoints
│  ├─ Admin/                # Menus, asset loaders
│  ├─ Compat/               # Compatibility shims & hook mirrors
│  └─ …
├─ admin-spa/               # React admin interface
├─ customer-spa/            # React customer interface
├─ scripts/                 # automation scripts
│  └─ package-zip.mjs
├─ dist/                    # build output
├─ composer.json
├─ package.json
├─ README.md
└─ PROJECT_SOP.md           # this file
```

---

## 5. 🧰 Development Workflow

### 5.1 Environment Setup
1. Use **LocalWP** or **Docker** (PHP 8.2+, MySQL 8, Redis optional).  
2. Clone or mount `woonoow` folder into `/wp-content/plugins/`.  
3. Ensure WooCommerce is installed and active.  
4. Activate WooNooW in wp-admin → “Plugins.”

### 5.2 Build & Test Commands
```bash
npm run build       # build both admin & customer SPAs
npm run pack        # create woonoow.zip for release
composer dump-autoload
```

### 5.3 Plugin Packaging
- The release ZIP must contain only:
  ```
  woonoow.php
  includes/
  admin-spa/dist/
  customer-spa/dist/
  composer.json
  package.json
  phpcs.xml
  README.md
  ```
- Build ZIP using:
  ```bash
  node scripts/package-zip.mjs
  ```

### 5.4 Commit Convention
Use conventional commits:
```
feat(api): add checkout quote endpoint
fix(core): prevent duplicate email send on async queue
refactor(admin): improve SPA routing
```

### 5.5 Branching
- `main` — stable, production-ready
- `dev` — development staging
- `feature/*` — specific features or fixes

### 5.6 Admin SPA Template Pattern

The WooNooW Admin SPA follows a consistent layout structure ensuring a predictable UI across all routes:

**Structure**
```
Admin-SPA
├── App Bar [Branding | Version | Server Connectivity | Global Buttons (Fullscreen)]
├── Menu Bar (Main Menu) [Normal (Tabbed Overflow-X-Auto)] [Fullscreen (Sidebar)]
├── Submenu Bar (Tabbed Overflow-X-Auto, context-sensitive)
└── Page Template
    ├── Page Tool Bar (Page filters, CRUD buttons, Back button)
    └── Page Content (Data tables, cards, forms)
```

**Behavioral Notes**
- `App Bar`: Persistent across all routes; contains global controls (fullscreen, server, user menu).
- `Menu Bar`: Primary navigation for main sections (Dashboard, Orders, Products, etc.); sticky with overflow-x scroll.
- `Submenu Bar`: Context-sensitive secondary tabs under the main menu.
- `Page Tool Bar`: Contains functional filters and actions relevant to the current page.
- `Page Content`: Hosts the page body—tables, analytics, and CRUD forms.
- In Fullscreen mode, `Menu Bar` becomes a collapsible sidebar while all others remain visible.
- Sticky layout rules ensure `App Bar` and `Menu Bar` remain fixed while content scrolls independently.

### 5.7 CRUD Module Pattern (Standard Operating Procedure)

WooNooW enforces a **consistent CRUD pattern** for all entity management modules (Orders, Products, Customers, etc.) to ensure predictable UX and maintainability.

**Core Principle:** All CRUD modules MUST follow the submenu tab pattern with consistent toolbar structure.

#### UI Structure

**Submenu Tabs Pattern:**
```
[All {Entity}] [New] [Categories] [Tags] [Other Sections...]
```

**Toolbar Structure:**
```
[Bulk Actions] [Filters...] [Search]
```

**Examples:**
- **Products:** `All products | New | Categories | Tags | Attributes`
- **Orders:** `All orders | New | Drafts | Recurring`
- **Customers:** `All customers | New | Groups | Segments`

#### Implementation Rules

1. **✅ Use Submenu Tabs** for main sections
   - Primary action (New) is a tab, NOT a toolbar button
   - Tabs for related entities (Categories, Tags, etc.)
   - Consistent with WordPress/WooCommerce patterns

2. **✅ Toolbar for Actions & Filters**
   - Bulk actions (Delete, Export, etc.)
   - Filter dropdowns (Status, Type, Date, etc.)
   - Search input
   - NO primary CRUD buttons (New, Edit, etc.)

3. **❌ Don't Mix Patterns**
   - Don't put "New" button in toolbar if using submenu
   - Don't duplicate actions in both toolbar and submenu
   - Don't use different patterns for different modules

#### Why This Pattern?

**Industry Standard:**
- Shopify Admin uses submenu tabs
- WooCommerce uses submenu tabs
- WordPress core uses submenu tabs

**Benefits:**
- **Scalability:** Easy to add new sections
- **Consistency:** Users know where to find actions
- **Clarity:** Visual hierarchy between main actions and filters

#### Migration Checklist

When updating an existing module to follow this pattern:

- [ ] Move "New {Entity}" button from toolbar to submenu tab
- [ ] Add other relevant tabs (Drafts, Categories, etc.)
- [ ] Keep filters and bulk actions in toolbar
- [ ] Update navigation tree in `NavigationRegistry.php`
- [ ] Test mobile responsiveness (tabs scroll horizontally)

#### Code Example

**Navigation Tree (PHP):**
```php
'orders' => [
    'label' => __('Orders', 'woonoow'),
    'path' => '/orders',
    'icon' => 'ShoppingCart',
    'children' => [
        'all' => [
            'label' => __('All orders', 'woonoow'),
            'path' => '/orders',
        ],
        'new' => [
            'label' => __('New', 'woonoow'),
            'path' => '/orders/new',
        ],
        'drafts' => [
            'label' => __('Drafts', 'woonoow'),
            'path' => '/orders/drafts',
        ],
    ],
],
```

**Submenu Component (React):**
```typescript
<SubMenu>
  <SubMenuItem to="/orders" label={__('All orders')} />
  <SubMenuItem to="/orders/new" label={__('New')} />
  <SubMenuItem to="/orders/drafts" label={__('Drafts')} />
</SubMenu>
```

**Submenu Mobile Behavior:**

To reduce clutter on mobile detail/new/edit pages, submenu MUST be hidden on mobile for these pages:

```typescript
// In SubmenuBar.tsx
const isDetailPage = /\/(orders|products|coupons|customers)\/(?:new|\d+(?:\/edit)?)$/.test(pathname);
const hiddenOnMobile = isDetailPage ? 'hidden md:block' : '';

return (
  <div className={`border-b border-border bg-background ${hiddenOnMobile}`}>
    {/* Submenu items */}
  </div>
);
```

**Rules:**
1. ✅ **Hide on mobile** for detail/new/edit pages (has own tabs + back button)
2. ✅ **Show on desktop** for all pages (useful for quick navigation)
3. ✅ **Show on mobile** for index pages only (list views)
4. ✅ **Use regex pattern** to detect detail/new/edit pages
5. ❌ **Never hide on desktop** - always useful for navigation
6. ❌ **Never show on mobile detail pages** - causes clutter

**Behavior Matrix:**

| Page Type | Mobile | Desktop | Reason |
|-----------|--------|---------|--------|
| Index (`/orders`) | ✅ Show | ✅ Show | Main navigation |
| New (`/orders/new`) | ❌ Hide | ✅ Show | Has form tabs + back button |
| Edit (`/orders/123/edit`) | ❌ Hide | ✅ Show | Has form tabs + back button |
| Detail (`/orders/123`) | ❌ Hide | ✅ Show | Has detail tabs + back button |

**Toolbar (React):**
```typescript
<Toolbar>
  <BulkActions />
  <FilterDropdown options={statusOptions} />
  <SearchInput />
</Toolbar>
```

#### Toolbar Button Standards

All CRUD list pages MUST use consistent button styling in the toolbar:

**Button Types:**

| Button Type | Classes | Use Case |
|-------------|---------|----------|
| **Delete (Destructive)** | `border rounded-md px-3 py-2 text-sm bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 inline-flex items-center gap-2` | Bulk delete action |
| **Refresh (Required)** | `border rounded-md px-3 py-2 text-sm hover:bg-accent disabled:opacity-50 inline-flex items-center gap-2` | Refresh data (MUST exist in all CRUD lists) |
| **Reset Filters** | `text-sm text-muted-foreground hover:text-foreground underline` | Clear all active filters |
| **Export/Secondary** | `border rounded-md px-3 py-2 text-sm hover:bg-accent disabled:opacity-50 inline-flex items-center gap-2` | Other secondary actions |

**Button Structure:**
```tsx
<button 
  className="border rounded-md px-3 py-2 text-sm bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 inline-flex items-center gap-2"
  onClick={handleAction}
  disabled={condition}
>
  <IconComponent className="w-4 h-4" />
  {__('Button Label')}
</button>
```

**Rules:**
1. ✅ **Delete button** - Always use `bg-red-600` (NOT `bg-black`)
2. ✅ **Refresh button** - MUST exist in all CRUD list pages (mandatory)
3. ✅ **Reset filters** - Use text link style (NOT button with background)
4. ✅ **Icon placement** - Use `inline-flex items-center gap-2` (NOT `inline mr-2`)
5. ✅ **Destructive actions** - Only show when items selected (conditional render)
6. ✅ **Non-destructive actions** - Can be always visible (use `disabled` state)
7. ✅ **Consistent spacing** - Use `gap-2` between icon and text
8. ✅ **Hover states** - Destructive: `hover:bg-red-700`, Secondary: `hover:bg-accent`
9. ❌ **Never use `bg-black`** for delete buttons
10. ❌ **Never use `inline mr-2`** - use `inline-flex gap-2` instead
11. ❌ **Never use button style** for reset filters - use text link

**Toolbar Layout:**
```tsx
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-3">
  {/* Left: Bulk Actions */}
  <div className="flex gap-3">
    {/* Delete - Show only when items selected */}
    {selectedIds.length > 0 && (
      <button className="border rounded-md px-3 py-2 text-sm bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 inline-flex items-center gap-2">
        <Trash2 className="w-4 h-4" />
        {__('Delete')} ({selectedIds.length})
      </button>
    )}
    
    {/* Refresh - Always visible (REQUIRED) */}
    <button className="border rounded-md px-3 py-2 text-sm hover:bg-accent disabled:opacity-50 inline-flex items-center gap-2">
      <RefreshCw className="w-4 h-4" />
      {__('Refresh')}
    </button>
  </div>
  
  {/* Right: Filters */}
  <div className="flex gap-3 flex-wrap items-center">
    <Select>...</Select>
    <Select>...</Select>
    
    {/* Reset Filters - Text link style */}
    {activeFiltersCount > 0 && (
      <button className="text-sm text-muted-foreground hover:text-foreground underline">
        {__('Clear filters')}
      </button>
    )}
  </div>
</div>
```

#### Table/List UI Standards

All CRUD list pages MUST follow these consistent UI patterns:

**Table Structure:**
```tsx
<div className="hidden md:block rounded-lg border overflow-hidden">
  <table className="w-full">
    <thead className="bg-muted/50">
      <tr className="border-b">
        <th className="w-12 p-3">{/* Checkbox */}</th>
        <th className="text-left p-3 font-medium">{__('Column')}</th>
        {/* ... more columns */}
      </tr>
    </thead>
    <tbody>
      <tr className="border-b hover:bg-muted/30 last:border-0">
        <td className="p-3">{/* Cell content */}</td>
        {/* ... more cells */}
      </tr>
    </tbody>
  </table>
</div>
```

**Required Classes:**

| Element | Classes | Purpose |
|---------|---------|---------|
| **Container** | `rounded-lg border overflow-hidden` | Rounded corners, border, hide overflow |
| **Table** | `w-full` | Full width |
| **Header Row** | `bg-muted/50` + `border-b` | Light background, bottom border |
| **Header Cell** | `p-3 font-medium text-left` | Padding, bold, left-aligned |
| **Body Row** | `border-b hover:bg-muted/30 last:border-0` | Border, hover effect, remove last border |
| **Body Cell** | `p-3` | Consistent padding (NOT `px-3 py-2`) |
| **Checkbox Column** | `w-12 p-3` | Fixed width for checkbox |
| **Actions Column** | `text-right p-3` or `text-center p-3` | Right/center aligned |

**Empty State Pattern:**
```tsx
<tr>
  <td colSpan={columnCount} className="p-8 text-center text-muted-foreground">
    <IconComponent className="w-12 h-12 mx-auto mb-2 opacity-50" />
    {primaryMessage}
    {helperText && <p className="text-sm mt-1">{helperText}</p>}
  </td>
</tr>
```

**Mobile Card Pattern (Linkable):**

Mobile cards MUST be fully tappable (whole card is a link) for better UX:

```tsx
<div className="md:hidden space-y-3">
  {items.map(item => (
    <Link
      key={item.id}
      to={`/entity/${item.id}/edit`}
      className="block bg-card border border-border rounded-xl p-3 hover:bg-accent/50 transition-colors active:scale-[0.98] active:transition-transform shadow-sm"
    >
      <div className="flex items-center gap-3">
        {/* Checkbox with stopPropagation */}
        <div onClick={(e) => { e.preventDefault(); e.stopPropagation(); onSelect(item.id); }}>
          <Checkbox checked={selected} className="w-5 h-5" />
        </div>

        {/* Content */}
        <div className="flex-1 min-w-0">
          <h3 className="font-bold text-base leading-tight mb-1">{item.name}</h3>
          <div className="text-sm text-muted-foreground truncate mb-2">{item.description}</div>
          <div className="flex items-center gap-3 text-xs text-muted-foreground mb-1">
            <span>{item.stats}</span>
          </div>
          <div className="font-bold text-lg tabular-nums text-primary">{item.amount}</div>
        </div>

        {/* Chevron */}
        <ChevronRight className="w-5 h-5 text-muted-foreground flex-shrink-0" />
      </div>
    </Link>
  ))}
</div>
```

**Card Rules:**
1. ✅ **Whole card is Link** - Better mobile UX (single tap to view/edit)
2. ✅ **Use `space-y-3`** - Consistent spacing between cards
3. ✅ **Checkbox stopPropagation** - Prevent navigation when selecting
4. ✅ **ChevronRight icon** - Visual indicator card is tappable
5. ✅ **Active scale animation** - `active:scale-[0.98]` for tap feedback
6. ✅ **Hover effect** - `hover:bg-accent/50` for desktop hover
7. ✅ **Shadow** - `shadow-sm` for depth
8. ✅ **Rounded corners** - `rounded-xl` for modern look
9. ❌ **Never use separate edit button** - Whole card should be tappable
10. ❌ **Never use `space-y-2`** - Use `space-y-3` for consistency

**Table Rules:**
1. ✅ **Always use `p-3`** for table cells (NOT `px-3 py-2`)
2. ✅ **Always add `hover:bg-muted/30`** to body rows
3. ✅ **Always use `bg-muted/50`** for table headers
4. ✅ **Always use `font-medium`** for header cells
5. ✅ **Always use `last:border-0`** to remove last row border
6. ✅ **Always use `overflow-hidden`** on table container
7. ❌ **Never mix padding styles** between modules
8. ❌ **Never omit hover effects** on interactive rows

**Responsive Behavior:**
- Desktop: Show table with `hidden md:block`
- Mobile: Show cards with `md:hidden`
- Both views must support same actions (select, edit, delete)
- Cards must be linkable (whole card tappable)

#### Variable Product Handling in Order Forms

When adding products to orders, variable products MUST follow the Tokopedia/Shopee pattern:

**Responsive Modal Pattern:**
- **Desktop:** Use `Dialog` component (centered modal)
- **Mobile:** Use `Drawer` component (bottom sheet)
- **Detection:** Use `useMediaQuery("(min-width: 768px)")`

**Implementation:**
```tsx
const isDesktop = useMediaQuery("(min-width: 768px)");

{/* Desktop: Dialog */}
{selectedProduct && isDesktop && (
  <Dialog open={open} onOpenChange={setOpen}>
    <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
      <DialogHeader>
        <DialogTitle>{product.name}</DialogTitle>
      </DialogHeader>
      {/* Variation list */}
    </DialogContent>
  </Dialog>
)}

{/* Mobile: Drawer */}
{selectedProduct && !isDesktop && (
  <Drawer open={open} onOpenChange={setOpen}>
    <DrawerContent>
      <DrawerHeader>
        <DrawerTitle>{product.name}</DrawerTitle>
      </DrawerHeader>
      {/* Variation list */}
    </DrawerContent>
  </Drawer>
)}
```

**Desktop Pattern:**
```
[Search Product...]
↓
[Product Name - Variable Product]
  └─ [Select Variation ▼] → Dropdown: Red, Blue, Green
     [Add to Order]
```

**Mobile Pattern:**
```
[Search Product...]
↓
[Product Card]
  Product Name
  [Select Variation →] → Opens drawer with variation chips
  [Add]
```

**Cart Display (Each variation = separate row):**
```
✓ Anker Earbuds
  White                    Rp296,000  [-] 1 [+] [🗑️]

✓ Anker Earbuds  
  Black                    Rp296,000  [-] 1 [+] [🗑️]

**Rules:**
1. ✅ Each variation is a **separate line item**
2. ✅ Show variation name clearly next to product name
3. ✅ Allow adding same product multiple times with different variations
4. ✅ Mobile: Click variation to open drawer for selection
5. ❌ Don't auto-select first variation
6. ❌ Don't hide variation selector
7. ✅ **Duplicate Handling**: Same product + same variation = increment quantity (NOT new row)
8. ✅ **Empty Attribute Values**: Filter empty attribute values - Use `.filter()` to remove empty strings

**Implementation:**
- Product search shows variable products
- If variable, show variation selector (dropdown/drawer)
- User must select variation before adding
- Each selected variation becomes separate cart item
- Can repeat for different variations

### 5.8 Mobile Responsiveness & UI Controls

WooNooW enforces a mobile‑first responsive standard across all SPA interfaces to ensure usability on small screens.

**Control Sizing Standard (`.ui-ctrl`)**
- All interactive controls — input, select, button, and dropdown options — must include the `.ui-ctrl` class or equivalent utility for consistent sizing.
- Default height: `h-11` (mobile), `md:h-9` (desktop).
- This sizing improves tap area accessibility and maintains visual alignment between mobile and desktop.

**Responsive Layout Rules**
- On mobile view, even in fullscreen mode, the layout uses **Topbar navigation** instead of Sidebar for better reachability.
- The Sidebar layout is applied **only** in desktop fullscreen mode.
- Sticky top layers (`App Bar`, `Menu Bar`) remain visible while sub‑content scrolls independently.
- Tables and grids must support horizontal scroll (`overflow-x-auto`) and collapse to cards when screen width < 640px.

**Tokens & Global Styles**
- File: `admin-spa/src/ui/tokens.css` defines base CSS variables for control sizing.
- File: `admin-spa/src/index.css` imports `./ui/tokens.css` and applies the `.ui-ctrl` rules globally.

These rules ensure consistent UX across device classes while maintaining WooNooW's design hierarchy.

### 5.8 Dialog Behavior Pattern

WooNooW uses **Radix UI Dialog** with specific patterns for preventing accidental dismissal.

**Core Principle:** Prevent outside-click and escape-key dismissal for dialogs with unsaved changes or complex editing.

**Dialog Types:**

| Type | Outside Click | Escape Key | Use Case | Example |
|------|---------------|------------|----------|---------|
| **Informational** | ✅ Allow | ✅ Allow | Simple info, confirmations | Alert dialogs |
| **Quick Edit** | ✅ Allow | ✅ Allow | Single field edits | Rename, quick settings |
| **Heavy Edit** | ❌ Prevent | ❌ Prevent | Multi-field forms, rich content | Email builder, template editor |
| **Destructive** | ❌ Prevent | ❌ Prevent | Delete confirmations with input | Delete with confirmation text |

**Implementation:**

```typescript
// Heavy Edit Dialog - Prevent accidental dismissal
<Dialog open={isOpen} onOpenChange={setIsOpen}>
  <DialogContent
    onInteractOutside={(e) => e.preventDefault()}
    onEscapeKeyDown={(e) => e.preventDefault()}
  >
    {/* Dialog content */}
    <DialogFooter>
      <Button variant="outline" onClick={() => setIsOpen(false)}>
        {__('Cancel')}
      </Button>
      <Button onClick={handleSave}>
        {__('Save Changes')}
      </Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

// Quick Edit Dialog - Allow dismissal
<Dialog open={isOpen} onOpenChange={setIsOpen}>
  <DialogContent>
    {/* Simple content */}
  </DialogContent>
</Dialog>
```

**Rules:**

1. ✅ **Prevent dismissal** when:
   - Dialog contains unsaved form data
   - User is editing rich content (WYSIWYG, code editor)
   - Dialog has multiple steps or complex state
   - Action is destructive and requires confirmation

2. ✅ **Allow dismissal** when:
   - Dialog is purely informational
   - Single field with auto-save
   - No data loss risk
   - Quick actions (view, select)

3. ✅ **Always provide explicit close buttons**:
   - Cancel button to close without saving
   - Save button to commit changes
   - X button in header (Radix default)

**Examples:**

- ❌ Prevent: `admin-spa/src/components/EmailBuilder/EmailBuilder.tsx` - Block edit dialog
- ❌ Prevent: Template editor dialogs with rich content
- ✅ Allow: Simple confirmation dialogs
- ✅ Allow: View-only information dialogs

**Best Practice:**

When in doubt, **prevent dismissal** for editing dialogs. It's better to require explicit Cancel/Save than risk data loss.

**Responsive Dialog/Drawer Pattern:**

For settings pages and forms, use **ResponsiveDialog** component that automatically switches between Dialog (desktop) and Drawer (mobile):

```typescript
import { ResponsiveDialog } from '@/components/ui/responsive-dialog';

<ResponsiveDialog
  open={isOpen}
  onOpenChange={setIsOpen}
  title={__('Edit Settings')}
  description={__('Configure your settings')}
  footer={
    <div className="flex gap-2">
      <Button variant="outline" onClick={() => setIsOpen(false)}>
        {__('Cancel')}
      </Button>
      <Button onClick={handleSave}>
        {__('Save')}
      </Button>
    </div>
  }
>
  {/* Form content */}
</ResponsiveDialog>
```

**Behavior:**
- **Desktop (≥768px)**: Shows centered Dialog
- **Mobile (<768px)**: Shows bottom Drawer for better reachability

**Component:** `admin-spa/src/components/ui/responsive-dialog.tsx`

### 5.9 Settings Page Layout Pattern

WooNooW enforces a **consistent layout pattern** for all settings pages to ensure predictable UX and maintainability.

**Core Principle:** All settings pages MUST use `SettingsLayout` component with contextual header.

**Implementation Pattern:**

```typescript
import { SettingsLayout } from './components/SettingsLayout';

export default function MySettingsPage() {
  const [settings, setSettings] = useState({...});
  const [isLoading, setIsLoading] = useState(true);
  
  const handleSave = async () => {
    // Save logic
  };
  
  if (isLoading) {
    return (
      <SettingsLayout
        title={__('Page Title')}
        description={__('Page description')}
        isLoading={true}
      >
        <div className="animate-pulse h-64 bg-muted rounded-lg"></div>
      </SettingsLayout>
    );
  }
  
  return (
    <SettingsLayout
      title={__('Page Title')}
      description={__('Page description')}
      onSave={handleSave}
      saveLabel={__('Save Changes')}
    >
      {/* Settings content - automatically boxed with max-w-5xl */}
      <SettingsCard title={__('Section Title')}>
        {/* Form fields */}
      </SettingsCard>
    </SettingsLayout>
  );
}
```

**SettingsLayout Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `title` | `string \| ReactNode` | Yes | Page title shown in contextual header |
| `description` | `string` | No | Subtitle/description below title |
| `onSave` | `() => Promise<void>` | No | Save handler - shows Save button in header |
| `saveLabel` | `string` | No | Custom label for save button (default: "Save changes") |
| `isLoading` | `boolean` | No | Shows loading state |
| `action` | `ReactNode` | No | Custom action buttons (e.g., Back button) |

**Layout Behavior:**

1. **Contextual Header** (Mobile + Desktop)
   - Shows page title and description
   - Shows Save button if `onSave` provided
   - Shows custom actions if `action` provided
   - Sticky at top of page

2. **Content Area**
   - Automatically boxed with `max-w-5xl mx-auto`
   - Responsive padding and spacing
   - Consistent with other admin pages

3. **No Inline Header**
   - When using `onSave` or `action`, inline header is hidden
   - Title/description only appear in contextual header
   - Saves vertical space

**Rules for Settings Pages:**

1. ✅ **Always use SettingsLayout** - Never create custom layout
2. ✅ **Pass title/description to layout** - Don't render inline headers
3. ✅ **Use onSave for save actions** - Don't render save buttons in content
4. ✅ **Use SettingsCard for sections** - Consistent card styling
5. ✅ **Show loading state** - Use `isLoading` prop during data fetch
6. ❌ **Never use full-width layout** - Content is always boxed
7. ❌ **Never duplicate save buttons** - One save button in header only

**Examples:**

- ✅ Good: `admin-spa/src/routes/Settings/Customers.tsx`
- ✅ Good: `admin-spa/src/routes/Settings/Notifications/Staff.tsx`
- ✅ Good: `admin-spa/src/routes/Settings/Notifications/Customer.tsx`

**Files:**
- Layout component: `admin-spa/src/routes/Settings/components/SettingsLayout.tsx`
- Card component: `admin-spa/src/routes/Settings/components/SettingsCard.tsx`

### 5.9 Mobile Contextual Header Pattern

WooNooW implements a **dual-header system** for mobile-first UX, ensuring actionable pages have consistent navigation and action buttons.

**Concept: Two Headers on Mobile**

1. **Contextual Header** (Mobile + Desktop)
   - Common actions that work everywhere
   - Format: `[Back Button] Page Title [Primary Action]`
   - Always visible (sticky)
   - Examples: Back, Edit, Save, Create

2. **Page Header / Extra Actions** (Desktop Only)
   - Additional desktop-specific actions
   - Hidden on mobile (`hidden md:flex`)
   - Examples: Print, Invoice, Label, Export

**Implementation Pattern**

```typescript
import { usePageHeader } from '@/contexts/PageHeaderContext';
import { Button } from '@/components/ui/button';

export default function MyPage() {
  const { setPageHeader, clearPageHeader } = usePageHeader();
  const nav = useNavigate();
  
  // Set contextual header
  useEffect(() => {
    const actions = (
      <div className="flex gap-2">
        <Button size="sm" variant="ghost" onClick={() => nav('/parent')}>
          {__('Back')}
        </Button>
        <Button size="sm" onClick={handlePrimaryAction}>
          {__('Save')}
        </Button>
      </div>
    );
    
    setPageHeader(__('Page Title'), actions);
    return () => clearPageHeader();
  }, [dependencies]);
  
  return (
    <div>
      {/* Desktop-only extra actions */}
      <div className="hidden md:flex gap-2">
        <button onClick={printAction}>{__('Print')}</button>
        <button onClick={exportAction}>{__('Export')}</button>
      </div>
      
      {/* Page content */}
    </div>
  );
}
```

**Rules for CRUD Pages**

| Page Type | Contextual Header | Page Header |
|-----------|-------------------|-------------|
| **List** | None (list page) | Filters, Search |
| **Detail** | [Back] Title [Edit] | Print, Invoice, Label |
| **New** | [Back] Title [Create] | None |
| **Edit** | [Back] Title [Save] | None |

**Form Submit Pattern**

For New/Edit pages, move submit button to contextual header:

```typescript
// Use formRef to trigger submit from header
const formRef = useRef<HTMLFormElement>(null);

const actions = (
  <Button onClick={() => formRef.current?.requestSubmit()}>
    {__('Save')}
  </Button>
);

<OrderForm formRef={formRef} hideSubmitButton={true} />
```

**Best Practices**

1. **No Duplication** - If action is in contextual header, remove from page body
2. **Mobile First** - Contextual header shows essential actions only
3. **Desktop Enhancement** - Extra actions in page header (desktop only)
4. **Consistent Pattern** - All CRUD pages follow same structure
5. **Loading States** - Buttons show loading state during mutations

**Files**
- `admin-spa/src/contexts/PageHeaderContext.tsx` - Context provider
- `admin-spa/src/hooks/usePageHeader.ts` - Hook for setting headers
- `admin-spa/src/components/PageHeader.tsx` - Header component

### 5.8 Error Handling & User Notifications

WooNooW implements a centralized, user-friendly error handling system that ensures consistent UX across all features.

**Core Principles**
1. **Never expose technical details** to end users (no "API 500", stack traces, or raw error codes)
2. **Use appropriate notification types** based on context
3. **Provide actionable feedback** with clear next steps
4. **Maintain consistency** across all pages and features

**Notification Types**

| Context | Component | Use Case | Example |
|---------|-----------|----------|---------|
| **Page Load Errors** | `<ErrorCard>` | Query failures, data fetch errors | "Failed to load orders" with retry button |
| **Action Errors** | `toast.error()` | Mutation failures, form submissions | "Failed to create order. Please check all required fields." |
| **Action Success** | `toast.success()` | Successful mutations | "Order created successfully" |
| **Inline Validation** | `<ErrorMessage>` | Form field errors | "Email address is required" |

**Implementation**

```typescript
// For mutations (create, update, delete)
import { showErrorToast, showSuccessToast } from '@/lib/errorHandling';

const mutation = useMutation({
  mutationFn: OrdersApi.create,
  onSuccess: (data) => {
    showSuccessToast('Order created successfully', `Order #${data.number} created`);
  },
  onError: (error) => {
    showErrorToast(error); // Automatically extracts user-friendly message
  }
});

// For queries (page loads)
import { ErrorCard } from '@/components/ErrorCard';
import { getPageLoadErrorMessage } from '@/lib/errorHandling';

if (query.isError) {
  return <ErrorCard 
    title="Failed to load data"
    message={getPageLoadErrorMessage(query.error)}
    onRetry={() => query.refetch()}
  />;
}
```

**Error Message Mapping**

Backend errors are mapped to user-friendly messages in `lib/errorHandling.ts`:

```typescript
const friendlyMessages = {
  'no_items': 'Please add at least one product to the order',
  'create_failed': 'Failed to create order. Please check all required fields.',
  'update_failed': 'Failed to update order. Please check all fields.',
  'not_found': 'The requested item was not found',
  'forbidden': 'You do not have permission to perform this action',
};
```

**Toast Configuration**
- **Position:** Bottom-right
- **Duration:** 4s (success), 6s (errors)
- **Theme:** Light mode with colored backgrounds
- **Colors:** Green (success), Red (error), Amber (warning), Blue (info)

**Files**
- `admin-spa/src/lib/errorHandling.ts` — Centralized error utilities
- `admin-spa/src/components/ErrorCard.tsx` — Page load error component
- `admin-spa/src/components/ui/sonner.tsx` — Toast configuration

### 5.9 Data Validation & Required Fields

WooNooW enforces strict validation rules to ensure data integrity and provide clear feedback to users.

**Order Creation Validation**

All orders must include:

| Field | Requirement | Error Message |
|-------|-------------|---------------|
| **Products** | At least 1 product | "At least one product is required" |
| **Billing First Name** | Required | "Billing first name is required" |
| **Billing Last Name** | Required | "Billing last name is required" |
| **Billing Email** | Required & valid format | "Billing email is required" / "Billing email is not valid" |
| **Billing Address** | Required | "Billing address is required" |
| **Billing City** | Required | "Billing city is required" |
| **Billing Postcode** | Required | "Billing postcode is required" |
| **Billing Country** | Required | "Billing country is required" |

**Backend Validation Response**

When validation fails, the API returns:

```json
{
  "error": "validation_failed",
  "message": "Please complete all required fields",
  "fields": [
    "Billing first name is required",
    "Billing email is required",
    "Billing address is required"
  ]
}
```

**Frontend Display**

The error handling utility automatically formats field errors as a bulleted list:

```
❌ Please complete all required fields

• Billing first name is required
• Billing email is required
• Billing address is required
• Billing city is required
• Billing postcode is required
```

Each field error appears as a bullet point on its own line, making it easy for users to scan and see exactly what needs to be fixed.

**Implementation Location**
- Backend validation: `includes/Api/OrdersController.php` create() method
- Frontend handling: `admin-spa/src/lib/errorHandling.ts` getErrorMessage()

### 5.10 Internationalization (i18n)

WooNooW follows WordPress translation standards to ensure all user-facing strings are translatable.

**Text Domain:** `woonoow`

**Backend (PHP)**

Use WordPress translation functions:

```php
// Simple translation
__( 'Billing first name', 'woonoow' )

// Translation with sprintf
sprintf( __( '%s is required', 'woonoow' ), $field_label )

// Translators comment for context
/* translators: %s: field label */
sprintf( __( '%s is required', 'woonoow' ), $label )
```

**Frontend (TypeScript/React)**

Use the i18n utility wrapper:

```typescript
import { __, sprintf } from '@/lib/i18n';

// Simple translation
__('Failed to load data')

// Translation with sprintf (placeholders)
sprintf(__('Order #%s created'), orderNumber)
sprintf(__('Edit Order #%s'), orderId)

// In components
<button>{__('Try again')}</button>
<h2>{sprintf(__('Order #%s'), order.number)}</h2>

// In error messages
const title = __('Please complete all required fields');
const message = sprintf(__('Order #%s has been created'), data.number);
```

**Translation Files**

- Backend strings: Extracted to `languages/woonoow.pot`
- Frontend strings: Loaded via `wp.i18n` (WordPress handles this)
- Translation utilities: `admin-spa/src/lib/i18n.ts`

**Best Practices**

1. **Never hardcode user-facing strings** - Always use translation functions
2. **Use translators comments** for context when using placeholders
3. **Keep strings simple** - Avoid complex concatenation
4. **Test in English first** - Ensure strings make sense before translation

---

## 5.11 Loading States

WooNooW provides a **consistent loading UI system** across the application to ensure a polished user experience.

**Component:** `admin-spa/src/components/LoadingState.tsx`

### Loading Components

**1. LoadingState (Default)**a
```typescript
import { LoadingState } from '@/components/LoadingState';

// Default loading
<LoadingState />

// Custom message
<LoadingState message={__('Loading order...')} />

// Different sizes
<LoadingState size="sm" message={__('Saving...')} />
<LoadingState size="md" message={__('Loading...')} /> // default
<LoadingState size="lg" message={__('Processing...')} />

// Full screen overlay
<LoadingState fullScreen message={__('Loading...')} />
```

**2. PageLoadingState**
```typescript
import { PageLoadingState } from '@/components/LoadingState';

// For full page loads
if (isLoading) {
  return <PageLoadingState message={__('Loading order...')} />;
}
```

**3. InlineLoadingState**
```typescript
import { InlineLoadingState } from '@/components/LoadingState';

// For inline loading within components
{isLoading && <InlineLoadingState message={__('Loading...')} />}
```

**4. CardLoadingSkeleton**
```typescript
import { CardLoadingSkeleton } from '@/components/LoadingState';

// For loading card content
{isLoading && <CardLoadingSkeleton />}
```

**5. TableLoadingSkeleton**
```typescript
import { TableLoadingSkeleton } from '@/components/LoadingState';

// For loading table rows
{isLoading && <TableLoadingSkeleton rows={10} />}
```

### Usage Guidelines

**Page-Level Loading:**
```typescript
// ✅ Good - Use PageLoadingState for full page loads
if (orderQ.isLoading || countriesQ.isLoading) {
  return <PageLoadingState message={sprintf(__('Loading order #%s...'), orderId)} />;
}

// ❌ Bad - Don't use plain text
if (isLoading) {
  return <div>Loading...</div>;
}
```

**Inline Loading:**
```typescript
// ✅ Good - Use InlineLoadingState for partial loads
{q.isLoading && <InlineLoadingState message={__('Loading order...')} />}

// ❌ Bad - Don't use custom spinners
{q.isLoading && <div><Loader2 className="animate-spin" /> Loading...</div>}
```

**Table Loading:**
```typescript
// ✅ Good - Use TableLoadingSkeleton for tables
{q.isLoading && <TableLoadingSkeleton rows={10} />}

// ❌ Bad - Don't show empty state while loading
{q.isLoading && <div>Loading data...</div>}
```

### Best Practices

1. **Always use i18n** - All loading messages must be translatable
   ```typescript
   <LoadingState message={__('Loading...')} />
   ```

2. **Be specific** - Use descriptive messages
   ```typescript
   // ✅ Good
   <LoadingState message={sprintf(__('Loading order #%s...'), orderId)} />
   
   // ❌ Bad
   <LoadingState message="Loading..." />
   ```

3. **Choose appropriate size** - Match the context
   - `sm` - Inline, buttons, small components
   - `md` - Default, cards, sections
   - `lg` - Full page, important actions

4. **Use skeletons for lists** - Better UX than spinners
   ```typescript
   {isLoading ? <TableLoadingSkeleton rows={5} /> : <Table data={data} />}
   ```

5. **Responsive design** - Loading states work on all screen sizes
   - Mobile: Optimized spacing and sizing
   - Desktop: Full layout preserved

### Pattern Examples

**Order Edit Page:**
```typescript
export default function OrdersEdit() {
  const orderQ = useQuery({ ... });
  
  if (orderQ.isLoading) {
    return <LoadingState message={sprintf(__('Loading order #%s...'), orderId)} />;
  }
  
  return <OrderForm ... />;
}
```

**Order Detail Page:**
```typescript
export default function OrderDetail() {
  const q = useQuery({ ... });
  
  return (
    <div>
      <h1>{__('Order Details')}</h1>
      {q.isLoading && <InlineLoadingState message={__('Loading order...')} />}
      {q.data && <OrderContent order={q.data} />}
    </div>
  );
}
```

**Orders List:**
```typescript
export default function OrdersList() {
  const q = useQuery({ ... });
  
  return (
    <table>
      <thead>...</thead>
      <tbody>
        {q.isLoading && <TableLoadingSkeleton rows={10} />}
        {q.data?.map(order => <OrderRow key={order.id} order={order} />)}
      </tbody>
    </table>
  );
}
```

---

## 6. 🔌 Addon Development Standards

### 6.1 Addon Injection System

WooNooW provides a **filter-based addon injection system** that allows third-party plugins to integrate seamlessly with the SPA without modifying core files.

**Core Principle:** All modules that can accept external injection MUST provide filter hooks following the standard naming convention.

### 6.2 Hook Naming Convention

All WooNooW hooks follow this structure:

```
woonoow/{category}/{action}[/{subcategory}]
```

**Examples:**
- `woonoow/addon_registry` - Register addon metadata
- `woonoow/spa_routes` - Register SPA routes
- `woonoow/nav_tree` - Modify navigation tree
- `woonoow/nav_tree/products/children` - Inject into Products submenu
- `woonoow/dashboard/widgets` - Add dashboard widgets (future)
- `woonoow/order/detail/panels` - Add order detail panels (future)

**Rules:**
1. Always prefix with `woonoow/`
2. Use lowercase with underscores
3. Use singular nouns for registries (`addon_registry`, not `addons_registry`)
4. Use hierarchical structure for nested items
5. Use descriptive names that indicate purpose

### 6.3 Filter Template Pattern

When creating a new module that accepts external injection, follow this template:

#### **Backend (PHP)**

```php
<?php
namespace WooNooW\Compat;

class MyModuleRegistry {
    const OPTION_KEY = 'wnw_my_module_data';
    const VERSION = '1.0.0';
    
    public static function init() {
        add_action('plugins_loaded', [__CLASS__, 'collect_data'], 30);
        add_action('activated_plugin', [__CLASS__, 'flush']);
        add_action('deactivated_plugin', [__CLASS__, 'flush']);
    }
    
    public static function collect_data() {
        $data = [];
        
        /**
         * Filter: woonoow/my_module/items
         * 
         * Allows addons to register items with this module.
         * 
         * @param array $data Array of item configurations
         * 
         * Example:
         * add_filter('woonoow/my_module/items', function($data) {
         *     $data['my-item'] = [
         *         'id'    => 'my-item',
         *         'label' => 'My Item',
         *         'value' => 'something',
         *     ];
         *     return $data;
         * });
         */
        $data = apply_filters('woonoow/my_module/items', $data);
        
        // Validate and store
        $validated = self::validate_items($data);
        update_option(self::OPTION_KEY, [
            'version' => self::VERSION,
            'items'   => $validated,
            'updated' => time(),
        ], false);
    }
    
    private static function validate_items(array $items): array {
        // Validation logic
        return $items;
    }
    
    public static function get_items(): array {
        $data = get_option(self::OPTION_KEY, []);
        return $data['items'] ?? [];
    }
    
    public static function flush() {
        delete_option(self::OPTION_KEY);
    }
    
    public static function get_frontend_data(): array {
        // Return sanitized data for frontend
        return self::get_items();
    }
}
```

#### **Expose to Frontend (Assets.php)**

```php
// In localize_runtime() method
wp_localize_script($handle, 'WNW_MY_MODULE', MyModuleRegistry::get_frontend_data());
wp_add_inline_script($handle, 'window.WNW_MY_MODULE = window.WNW_MY_MODULE || WNW_MY_MODULE;', 'after');
```

#### **Frontend (TypeScript)**

```typescript
// Read from window
const moduleData = (window as any).WNW_MY_MODULE || [];

// Use in component
function MyComponent() {
  const items = (window as any).WNW_MY_MODULE || [];
  return (
    <div>
      {items.map(item => (
        <div key={item.id}>{item.label}</div>
      ))}
    </div>
  );
}
```

### 6.4 Documentation Requirements

When adding a new filter hook, you MUST:

1. **Add to Hook Registry** (see section 6.5)
2. **Document in code** with PHPDoc
3. **Add example** in ADDON_INJECTION_GUIDE.md
4. **Update** ADDONS_ADMIN_UI_REQUIREMENTS.md

### 6.5 Hook Registry

See `HOOKS_REGISTRY.md` for complete list of available hooks and filters.

### 6.6 Non-React Addon Development

**Question:** Can developers build addons without React?

**Answer:** **YES!** WooNooW supports multiple addon approaches:

#### **Approach 1: PHP + HTML/CSS/JS (No React)**

Traditional WordPress addon development works perfectly:

```php
<?php
/**
 * Plugin Name: My Traditional Addon
 */

// Register addon
add_filter('woonoow/addon_registry', function($addons) {
    $addons['my-addon'] = [
        'id'      => 'my-addon',
        'name'    => 'My Addon',
        'version' => '1.0.0',
    ];
    return $addons;
});

// Add navigation item that links to classic admin page
add_filter('woonoow/nav_tree', function($tree) {
    $tree[] = [
        'key'      => 'my-addon',
        'label'    => 'My Addon',
        'path'     => '/my-addon-classic', // Will redirect to admin page
        'icon'     => 'puzzle',
        'children' => [],
    ];
    return $tree;
});

// Register classic admin page
add_action('admin_menu', function() {
    add_menu_page(
        'My Addon',
        'My Addon',
        'manage_options',
        'my-addon-page',
        'my_addon_render_page',
        'dashicons-admin-generic',
        30
    );
});

function my_addon_render_page() {
    ?>
    <div class="wrap">
        <h1>My Traditional Addon</h1>
        <p>Built with PHP, HTML, CSS, and vanilla JS!</p>
        
        <script>
        // Vanilla JavaScript works fine
        document.addEventListener('DOMContentLoaded', function() {
            console.log('My addon loaded!');
        });
        </script>
    </div>
    <?php
}
```

**This approach:**
- ✅ Works with WooNooW navigation
- ✅ No React knowledge required
- ✅ Uses standard WordPress admin pages
- ✅ Can use WordPress admin styles
- ✅ Can enqueue own CSS/JS
- ⚠️ Opens in separate page (not SPA)

#### **Approach 2: Vanilla JS Component (No React)**

For developers who want SPA integration without React:

```javascript
// dist/MyAddon.js - Vanilla JS module
export default function MyAddonPage(props) {
    const container = document.createElement('div');
    container.className = 'p-6';
    container.innerHTML = `
        <div class="rounded-lg border border-border p-6 bg-card">
            <h2 class="text-xl font-semibold mb-2">My Addon</h2>
            <p class="text-sm opacity-70">Built with vanilla JavaScript!</p>
            <button id="my-button" class="px-4 py-2 bg-blue-500 text-white rounded">
                Click Me
            </button>
        </div>
    `;
    
    // Add event listeners
    setTimeout(() => {
        const button = container.querySelector('#my-button');
        button.addEventListener('click', () => {
            alert('Vanilla JS works!');
        });
    }, 0);
    
    return container;
}
```

**This approach:**
- ✅ Integrates with SPA
- ✅ No React required
- ✅ Can use Tailwind classes
- ✅ Can fetch from REST API
- ⚠️ Must return DOM element
- ⚠️ Manual state management

#### **Approach 3: React Component (Full SPA)**

For developers comfortable with React:

```typescript
// dist/MyAddon.tsx - React component
import React from 'react';

export default function MyAddonPage() {
    const [count, setCount] = React.useState(0);
    
    return (
        <div className="p-6">
            <div className="rounded-lg border border-border p-6 bg-card">
                <h2 className="text-xl font-semibold mb-2">My Addon</h2>
                <p className="text-sm opacity-70">Built with React!</p>
                <button 
                    onClick={() => setCount(count + 1)}
                    className="px-4 py-2 bg-blue-500 text-white rounded"
                >
                    Clicked {count} times
                </button>
            </div>
        </div>
    );
}
```

**This approach:**
- ✅ Full SPA integration
- ✅ React state management
- ✅ Can use React Query
- ✅ Can use WooNooW components
- ✅ Best UX
- ⚠️ Requires React knowledge

### 6.7 Addon Development Checklist

When creating a module that accepts addons:

- [ ] Create Registry class (e.g., `MyModuleRegistry.php`)
- [ ] Add filter hook with `woonoow/` prefix
- [ ] Document filter in PHPDoc with example
- [ ] Expose data to frontend via `Assets.php`
- [ ] Add to `HOOKS_REGISTRY.md`
- [ ] Add example to `ADDON_INJECTION_GUIDE.md`
- [ ] Test with example addon
- [ ] Update `ADDONS_ADMIN_UI_REQUIREMENTS.md`

### 6.8 Orders Module as Reference

The **Orders module** is the reference implementation:
- No external injection (by design)
- Clean route structure
- Type-safe components
- Proper error handling
- Mobile responsive
- i18n complete

Use Orders as the template for building new core modules.

---

## 6.9 CRUD Module Pattern (Standard Template)

**All CRUD modules (Orders, Products, Customers, Coupons, etc.) MUST follow this exact pattern for consistency.**

### 📁 File Structure

```
admin-spa/src/routes/{Module}/
├── index.tsx              # List view (table + filters)
├── New.tsx                # Create new item
├── Edit.tsx               # Edit existing item
├── Detail.tsx             # View item details (optional)
├── components/            # Module-specific components
│   ├── {Module}Card.tsx   # Mobile card view
│   ├── FilterBottomSheet.tsx  # Mobile filters
│   └── SearchBar.tsx      # Search component
└── partials/              # Shared form components
    └── {Module}Form.tsx   # Reusable form for create/edit
```

### 🎯 Backend API Pattern

**File:** `includes/Api/{Module}Controller.php`

```php
<?php
namespace WooNooW\Api;

class {Module}Controller {
    
    public static function register_routes() {
        // List
        register_rest_route('woonoow/v1', '/{module}', [
            'methods' => 'GET',
            'callback' => [__CLASS__, 'get_{module}'],
            'permission_callback' => [Permissions::class, 'check_admin'],
        ]);
        
        // Single
        register_rest_route('woonoow/v1', '/{module}/(?P<id>\d+)', [
            'methods' => 'GET',
            'callback' => [__CLASS__, 'get_{item}'],
            'permission_callback' => [Permissions::class, 'check_admin'],
        ]);
        
        // Create
        register_rest_route('woonoow/v1', '/{module}', [
            'methods' => 'POST',
            'callback' => [__CLASS__, 'create_{item}'],
            'permission_callback' => [Permissions::class, 'check_admin'],
        ]);
        
        // Update
        register_rest_route('woonoow/v1', '/{module}/(?P<id>\d+)', [
            'methods' => 'PUT',
            'callback' => [__CLASS__, 'update_{item}'],
            'permission_callback' => [Permissions::class, 'check_admin'],
        ]);
        
        // Delete
        register_rest_route('woonoow/v1', '/{module}/(?P<id>\d+)', [
            'methods' => 'DELETE',
            'callback' => [__CLASS__, 'delete_{item}'],
            'permission_callback' => [Permissions::class, 'check_admin'],
        ]);
    }
    
    // List with pagination & filters
    public static function get_{module}(WP_REST_Request $request) {
        $page = max(1, (int) $request->get_param('page'));
        $per_page = min(100, max(1, (int) ($request->get_param('per_page') ?: 20)));
        $search = $request->get_param('search');
        $status = $request->get_param('status');
        $orderby = $request->get_param('orderby') ?: 'date';
        $order = $request->get_param('order') ?: 'DESC';
        
        // Query logic here
        
        return new WP_REST_Response([
            'rows' => $items,
            'total' => $total,
            'page' => $page,
            'per_page' => $per_page,
            'pages' => $max_pages,
        ], 200);
    }
}
```

**Register in Routes.php:**
```php
use WooNooW\Api\{Module}Controller;

// In rest_api_init:
{Module}Controller::register_routes();
```

### 🎨 Frontend Index Page Pattern

**File:** `admin-spa/src/routes/{Module}/index.tsx`

```typescript
import React, { useState, useCallback } from 'react';
import { useQuery, useMutation, keepPreviousData } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { useFABConfig } from '@/hooks/useFABConfig';
import { setQuery, getQuery } from '@/lib/query-params';
import { __ } from '@/lib/i18n';

export default function {Module}Index() {
  useFABConfig('{module}'); // Enable FAB for create
  
  const initial = getQuery();
  const [page, setPage] = useState(Number(initial.page ?? 1) || 1);
  const [status, setStatus] = useState<string | undefined>(initial.status || undefined);
  const [searchQuery, setSearchQuery] = useState('');
  const [selectedIds, setSelectedIds] = useState<number[]>([]);
  const [showDeleteDialog, setShowDeleteDialog] = useState(false);
  const perPage = 20;

  // Sync URL params
  React.useEffect(() => {
    setQuery({ page, status });
  }, [page, status]);

  // Fetch data
  const q = useQuery({
    queryKey: ['{module}', { page, perPage, status }],
    queryFn: () => api.get('/{module}', {
      page, per_page: perPage, status
    }),
    placeholderData: keepPreviousData,
  });

  const data = q.data as undefined | { rows: any[]; total: number };

  // Filter by search
  const filteredItems = React.useMemo(() => {
    const rows = data?.rows;
    if (!rows) return [];
    if (!searchQuery.trim()) return rows;
    
    const query = searchQuery.toLowerCase();
    return rows.filter((item: any) =>
      item.name?.toLowerCase().includes(query) ||
      item.id?.toString().includes(query)
    );
  }, [data, searchQuery]);

  // Bulk delete
  const deleteMutation = useMutation({
    mutationFn: async (ids: number[]) => {
      const results = await Promise.allSettled(
        ids.map(id => api.del(`/{module}/${id}`))
      );
      const failed = results.filter(r => r.status === 'rejected').length;
      return { total: ids.length, failed };
    },
    onSuccess: (result) => {
      const { total, failed } = result;
      if (failed === 0) {
        toast.success(__('Items deleted successfully'));
      } else if (failed < total) {
        toast.warning(__(`${total - failed} deleted, ${failed} failed`));
      } else {
        toast.error(__('Failed to delete items'));
      }
      setSelectedIds([]);
      setShowDeleteDialog(false);
      q.refetch();
    },
  });

  // Checkbox handlers
  const allIds = filteredItems.map(r => r.id) || [];
  const allSelected = allIds.length > 0 && selectedIds.length === allIds.length;

  const toggleAll = () => {
    setSelectedIds(allSelected ? [] : allIds);
  };

  const toggleRow = (id: number) => {
    setSelectedIds(prev =>
      prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]
    );
  };

  return (
    <div className="space-y-4 w-full pb-4">
      {/* Desktop: Filters */}
      <div className="hidden md:block rounded-lg border p-4">
        {/* Filter controls */}
      </div>

      {/* Mobile: Search + Filter */}
      <div className="md:hidden">
        <SearchBar
          value={searchQuery}
          onChange={setSearchQuery}
          onFilterClick={() => setFilterSheetOpen(true)}
        />
      </div>

      {/* Desktop: Table */}
      <div className="hidden md:block">
        <table className="w-full">
          <thead>
            <tr>
              <th><Checkbox checked={allSelected} onCheckedChange={toggleAll} /></th>
              <th>{__('Name')}</th>
              <th>{__('Status')}</th>
              <th>{__('Actions')}</th>
            </tr>
          </thead>
          <tbody>
            {filteredItems.map(item => (
              <tr key={item.id}>
                <td><Checkbox checked={selectedIds.includes(item.id)} onCheckedChange={() => toggleRow(item.id)} /></td>
                <td>{item.name}</td>
                <td><StatusBadge value={item.status} /></td>
                <td><Link to={`/{module}/${item.id}`}>{__('View')}</Link></td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Mobile: Cards */}
      <div className="md:hidden space-y-2">
        {filteredItems.map(item => (
          <{Module}Card key={item.id} item={item} />
        ))}
      </div>

      {/* Delete Dialog */}
      <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
        {/* Dialog content */}
      </AlertDialog>
    </div>
  );
}
```

### 📝 Frontend Create Page Pattern

**File:** `admin-spa/src/routes/{Module}/New.tsx`

```typescript
import React, { useEffect, useRef } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { useNavigate } from 'react-router-dom';
import { usePageHeader } from '@/contexts/PageHeaderContext';
import { Button } from '@/components/ui/button';
import { useFABConfig } from '@/hooks/useFABConfig';
import { __ } from '@/lib/i18n';
import {Module}Form from './partials/{Module}Form';

export default function {Module}New() {
  const nav = useNavigate();
  const qc = useQueryClient();
  const { setPageHeader, clearPageHeader } = usePageHeader();
  const formRef = useRef<HTMLFormElement>(null);
  
  useFABConfig('none'); // Hide FAB on create page

  const mutate = useMutation({
    mutationFn: (data: any) => api.post('/{module}', data),
    onSuccess: (data) => {
      qc.invalidateQueries({ queryKey: ['{module}'] });
      showSuccessToast(__('Item created successfully'));
      nav('/{module}');
    },
    onError: (error: any) => {
      showErrorToast(error);
    },
  });

  // Set page header
  useEffect(() => {
    const actions = (
      <div className="flex gap-2">
        <Button size="sm" variant="ghost" onClick={() => nav('/{module}')}>
          {__('Back')}
        </Button>
        <Button 
          size="sm" 
          onClick={() => formRef.current?.requestSubmit()}
          disabled={mutate.isPending}
        >
          {mutate.isPending ? __('Creating...') : __('Create')}
        </Button>
      </div>
    );
    setPageHeader(__('New {Item}'), actions);
    return () => clearPageHeader();
  }, [mutate.isPending, setPageHeader, clearPageHeader, nav]);

  return (
    <div className="space-y-4">
      <{Module}Form
        mode="create"
        formRef={formRef}
        hideSubmitButton={true}
        onSubmit={(form) => mutate.mutate(form)}
      />
    </div>
  );
}
```

### ✏️ Frontend Edit Page Pattern

**File:** `admin-spa/src/routes/{Module}/Edit.tsx`

```typescript
import React, { useEffect, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { usePageHeader } from '@/contexts/PageHeaderContext';
import { Button } from '@/components/ui/button';
import { useFABConfig } from '@/hooks/useFABConfig';
import { __ } from '@/lib/i18n';
import {Module}Form from './partials/{Module}Form';

export default function {Module}Edit() {
  const { id } = useParams();
  const itemId = Number(id);
  const nav = useNavigate();
  const qc = useQueryClient();
  const { setPageHeader, clearPageHeader } = usePageHeader();
  const formRef = useRef<HTMLFormElement>(null);
  
  useFABConfig('none');

  const itemQ = useQuery({ 
    queryKey: ['{item}', itemId], 
    enabled: Number.isFinite(itemId), 
    queryFn: () => api.get(`/{module}/${itemId}`) 
  });

  const upd = useMutation({
    mutationFn: (payload: any) => api.put(`/{module}/${itemId}`, payload),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['{module}'] });
      qc.invalidateQueries({ queryKey: ['{item}', itemId] });
      showSuccessToast(__('Item updated successfully'));
      nav(`/{module}/${itemId}`);
    },
    onError: (error: any) => {
      showErrorToast(error);
    }
  });

  const item = itemQ.data || {};

  // Set page header
  useEffect(() => {
    const actions = (
      <div className="flex gap-2">
        <Button size="sm" variant="ghost" onClick={() => nav(`/{module}/${itemId}`)}>
          {__('Back')}
        </Button>
        <Button 
          size="sm" 
          onClick={() => formRef.current?.requestSubmit()}
          disabled={upd.isPending}
        >
          {upd.isPending ? __('Saving...') : __('Save')}
        </Button>
      </div>
    );
    setPageHeader(__('Edit {Item}'), actions);
    return () => clearPageHeader();
  }, [itemId, upd.isPending, setPageHeader, clearPageHeader, nav]);

  if (!Number.isFinite(itemId)) {
    return <div className="p-4 text-sm text-red-600">{__('Invalid ID')}</div>;
  }

  if (itemQ.isLoading) {
    return <LoadingState message={__('Loading...')} />;
  }

  if (itemQ.isError) {
    return <ErrorCard 
      title={__('Failed to load item')}
      message={getPageLoadErrorMessage(itemQ.error)}
      onRetry={() => itemQ.refetch()}
    />;
  }

  return (
    <div className="space-y-4">
      <{Module}Form
        mode="edit"
        initial={item}
        formRef={formRef}
        hideSubmitButton={true}
        onSubmit={(form) => upd.mutate(form)}
      />
    </div>
  );
}
```

### 📋 Checklist for New CRUD Module

**Backend:**
- [ ] Create `{Module}Controller.php` with all CRUD endpoints
- [ ] Register routes in `Routes.php`
- [ ] Add permission checks (`Permissions::check_admin`)
- [ ] Implement pagination, filters, search
- [ ] Return consistent response format
- [ ] Add i18n for all error messages

**Frontend:**
- [ ] Create `routes/{Module}/index.tsx` (list view)
- [ ] Create `routes/{Module}/New.tsx` (create)
- [ ] Create `routes/{Module}/Edit.tsx` (edit)
- [ ] Create `routes/{Module}/Detail.tsx` (optional view)
- [ ] Create `components/{Module}Card.tsx` (mobile)
- [ ] Create `partials/{Module}Form.tsx` (reusable form)
- [ ] Add to navigation tree (`nav/tree.ts`)
- [ ] Configure FAB (`useFABConfig`)
- [ ] Add all i18n strings
- [ ] Implement bulk delete
- [ ] Add filters (status, date, search)
- [ ] Add pagination
- [ ] Test mobile responsive
- [ ] Test error states
- [ ] Test loading states

**Testing:**
- [ ] Create item
- [ ] Edit item
- [ ] Delete item
- [ ] Bulk delete
- [ ] Search
- [ ] Filter by status
- [ ] Pagination
- [ ] Mobile view
- [ ] Error handling
- [ ] Permission checks

---

## 7. 🎨 Admin Interface Modes

WooNooW provides **three distinct admin interface modes** to accommodate different workflows and user preferences:

### **1. Normal Mode (wp-admin)**
- **Access:** `/wp-admin/admin.php?page=woonoow`
- **Layout:** Traditional WordPress admin with WooNooW SPA in content area
- **Use Case:** Standard WordPress admin workflow
- **Features:**
  - WordPress admin bar and sidebar visible
  - Full WordPress admin functionality
  - WooNooW SPA integrated seamlessly
  - Settings submenu hidden (use WooCommerce settings)
- **When to use:** When you need access to other WordPress admin features alongside WooNooW

### **2. Fullscreen Mode**
- **Access:** Toggle button in WooNooW header
- **Layout:** WooNooW SPA only (no WordPress chrome)
- **Use Case:** Focused work sessions, order processing
- **Features:**
  - Maximized workspace
  - Distraction-free interface
  - All WooNooW features accessible
  - Settings submenu hidden
- **When to use:** When you want to focus exclusively on WooNooW tasks

### **3. Standalone Mode** ✨
- **Access:** `https://yoursite.com/admin`
- **Layout:** Complete standalone application with custom login
- **Use Case:** Quick daily access, mobile-friendly, bookmark-able
- **Features:**
  - Custom login page (`/admin#/login`)
  - WordPress authentication integration
  - Settings submenu visible (SPA settings pages)
  - "WordPress" button to access wp-admin
  - "Logout" button in header
  - Admin bar link in wp-admin to standalone
  - Session persistence across modes
- **When to use:** As your primary WooNooW interface, especially on mobile or for quick access

### **Mode Switching**
- **From wp-admin to Standalone:** Click "WooNooW" in admin bar
- **From Standalone to wp-admin:** Click "WordPress" button in header
- **To Fullscreen:** Click fullscreen toggle in any mode
- **Session persistence:** Login state is shared across all modes

### **Settings Submenu Behavior**
- **Normal Mode:** No settings submenu (use WooCommerce settings in wp-admin)
- **Fullscreen Mode:** No settings submenu
- **Standalone Mode:** Full settings submenu visible with SPA pages

**Implementation:** Settings submenu uses dynamic getter in `admin-spa/src/nav/tree.ts`:
```typescript
get children() {
  const isStandalone = (window as any).WNW_CONFIG?.standaloneMode;
  if (!isStandalone) return [];
  return [ /* settings items */ ];
}
```

---

## 8. 🤖 AI Agent Collaboration Rules

When using an AI IDE agent (ChatGPT, Claude, etc.):

### Step 1: Context Injection
Always load:
- `README.md`
- `PROJECT_SOP.md`
- The specific file(s) being edited

### Step 2: Editing Rules
1. All AI edits must be **idempotent** — never break structure or naming conventions.  
2. Always follow PSR‑12 PHP standard and React code conventions.  
3. When unsure about a design decision, **refer back to this S.O.P.** before guessing.  
4. New files must be registered in the correct namespace path.  
5. When editing React components, ensure build compatibility with Vite.

### Step 3: Communication
AI agents must:
- Explain each patch clearly.  
- Never auto‑remove code without reason.  
- Maintain English for all code comments, Markdown for docs.

---

## 7. 📦 Release Steps

1. Run all builds:
   ```bash
   npm run build && npm run pack
   ```
2. Test in LocalWP with a sample Woo store.
3. Validate HPOS compatibility and order creation flow.
4. Push final `woonoow.zip` to release channel (Sejoli, member.dwindi.com, or manual upload).
5. Tag version using semantic versioning (e.g. `v0.2.0-beta`).

---

## 8. 🧭 Decision Hierarchy

| Category | Decision Reference |
|-----------|--------------------|
| Code Style | Follow PSR‑12 (PHP) & Airbnb/React rules |
| Architecture | PSR‑4 + modular single responsibility |
| UI/UX | Modern minimal style, standardized using Tailwind + Shadcn UI. Recharts for data visualization. |
| Icons | Use lucide-react via npm i lucide-react. Icons should match Shadcn UI guidelines. Always import directly (e.g. import { Package } from 'lucide-react'). Maintain consistent size (16–20px) and stroke width (1.5px). Use Tailwind classes for color states. |
| **Navigation Pattern** | **CRUD pages MUST follow consistent back button navigation: New Order: Index ← New. Edit Order: Index ← Detail ← Edit. Back button always goes to parent page, not index. Use ArrowLeft icon from lucide-react. Toolbar format: `<button onClick={() => nav('/parent/path')}><ArrowLeft /> Back</button> <h2>Page Title</h2>`** |
| Compatibility | Must preserve Woo hooks unless explicitly replaced |
| Performance | Async-first, no blocking mail or sync jobs |
| **Email Policy** | **ALL `wp_mail()` calls MUST be delayed by 15+ seconds using Action Scheduler or wp-cron. Never send emails synchronously during API requests (create, update, status change). Use `OrdersController::schedule_order_email()` pattern.** |
| Deployment | LocalWP → Coolify → Production |

---

## 9. 🧩 Future Extensions

- **Addon Manager** (JSON feed + licensing integration).  
- **Admin Insights** (charts, sales analytics with React).  
- **Storefront SPA Theme Override** (optional full React mode).  
- **Developer SDK** for 3rd-party addon compatibility.

---

## 10. 📜 License & Ownership

All rights reserved to **Dwindi (dewe.dev)**.  
The WooNooW project may include GPL-compatible code portions for WordPress compliance.  
Redistribution without written consent is prohibited outside official licensing channels.

---