# Admin Panel Expert Agent

> **Specialized agent for Translate Press Zone admin panel development**
> Full-stack expertise: Vanilla JS + Webpack + CSS + PHP REST API

---

## Identity & Scope

**Name:** `admin-panel-expert`
**Domain:** Full-stack admin panel (frontend + backend)
**Primary Files:**
- `admin/src-vanilla/**` - Frontend source
- `admin/build/**` - Compiled output
- `includes/Api/RestAdmin.php` - Admin REST endpoints
- `includes/Api/RestBase.php` - Base REST class
- `includes/class-tpz-service-registrar.php` - WPML service registration
- `includes/class-tpz-job-sender.php` - Translation job sender
- `includes/class-tpz-job-receiver.php` - Translation callback handler

---

## Tech Stack

### Frontend
| Technology | Details |
|------------|---------|
| **JavaScript** | Vanilla ES6+ (NO framework - no React/Vue/Angular) |
| **Bundler** | Webpack 5 |
| **CSS** | SCSS (compiles to CSS) |
| **Routing** | Hash-based SPA (`#/dashboard`, `#/settings`, etc.) |
| **Entry Point** | `admin/src-vanilla/admin.js` |
| **Output** | `admin/build/admin.js` + `admin/build/admin.css` |

### Backend
| Technology | Details |
|------------|---------|
| **PHP** | 8.0+ with strict types |
| **Framework** | WordPress REST API |
| **Namespace** | `translate-press-zone/v1` |
| **Base Class** | `RestBase` → `RestAdmin` |

---

## Critical Rules

### JavaScript Naming (WordPress.org Compliance)

```javascript
// 4+ character descriptive names required
const TranslatePresszoneApp = {};
window.translatePresszoneAdmin = { apiUrl, nonce };
```

### SCSS Variable Prefix

```scss
// CORRECT - Use SCSS variables for ALL styling
$translate-presszone-text: #0f172a;
$translate-presszone-surface: #ffffff;

.translate-presszone-card {
    background: $translate-presszone-surface;
    color: $translate-presszone-text;
    
    .dark-mode & {
        background: $translate-presszone-surface-dark;
        color: $translate-presszone-text-dark;
    }
}
```

> **Note:** Use SCSS variables ($) for ALL styling. NO CSS custom properties (--variables).
> For dynamic styling, use explicit CSS classes with SCSS variables.

### CSS Class Naming (BEM)

```css
/* Block */
.translate-presszone-btn { }

/* Element */
.translate-presszone-btn__icon { }

/* Modifier */
.translate-presszone-btn--primary { }
.translate-presszone-btn--disabled { }
```

### Settings Classes (Padding/Styling)

Use classes for Design page settings with SCSS variables:
```scss
/* Padding classes */
.translate-presszone-padding--minimal {
    .translate-presszone-item { padding: $translate-presszone-spacing-sm; }
}

.translate-presszone-padding--wide {
    .translate-presszone-item { padding: $translate-presszone-spacing-xl; }
}

/* Styling classes */
.presszone-comments-styling--square {
    .presszone-comments-item { border-radius: 0; }
}

.presszone-comments-styling--pill {
    .presszone-comments-item { border-radius: $presszone-comments-radius-xl; }
}
```

### NO Hardcoded Colors

```scss
/* CORRECT */
.presszone-comments-card {
    background: $presszone-comments-surface;
    
    .dark-mode & {
        background: $presszone-comments-surface-dark;
    }
}
}

/* WRONG - Never hardcode colors */
.dark-mode .presszone-comments-card {
    background: #282a2c;  /* NEVER hardcode */
}
```

### Dynamic Styling

Use explicit CSS classes instead of inline styles or CSS custom properties:

```scss
// CORRECT - Stagger animations with classes
.presszone-comments-stagger-0 { animation-delay: 0ms; }
.presszone-comments-stagger-1 { animation-delay: 60ms; }
.presszone-comments-stagger-2 { animation-delay: 120ms; }

// CORRECT - Widget color variants with classes
.presszone-comments-widget--primary { border-inline-start-color: $presszone-comments-primary; }
.presszone-comments-widget--success { border-inline-start-color: $presszone-comments-success; }
.presszone-comments-widget--warning { border-inline-start-color: $presszone-comments-warning; }
```

```javascript
// WRONG - Don't use inline styles or CSS custom properties
element.style.setProperty('--stagger-index', index);
element.style = `--widget-color: ${color}`;

// CORRECT - Use CSS classes
element.classList.add(`presszone-comments-stagger-${index}`);
element.classList.add('presszone-comments-widget--success');
```

### Build Requirements

| Change Type | Command | Directory |
|-------------|---------|-----------|
| Admin JS/SCSS | `npm run build` | `admin/` |
| Frontend SCSS | `npm run build:css` | Plugin root |

**ALWAYS rebuild after changes. Forgetting = changes won't appear.**

---

## Directory Structure

```
admin/
├── src-vanilla/
│   ├── admin.js                 # Entry point, router, init
│   ├── css/                     # SCSS source files
│   │   ├── main.scss            # Main entry point
│   │   ├── _variables.scss      # SCSS variables
│   │   ├── _mixins.scss         # Reusable mixins
│   │   ├── _animations.scss     # Animation keyframes
│   │   ├── _components.scss     # Component styles
│   │   ├── _forms.scss          # Form styles
│   │   └── ...
│   ├── components/              # 23 reusable UI components
│   │   ├── AnimatedItem.js
│   │   ├── Button.js
│   │   ├── Card.js
│   │   ├── ColorField.js
│   │   ├── ColorPickerModal.js
│   │   ├── ErrorState.js
│   │   ├── FormField.js
│   │   ├── GridTable.js
│   │   ├── Modal.js
│   │   ├── Placeholders.js
│   │   ├── Select.js
│   │   ├── Skeleton.js
│   │   ├── Spinner.js
│   │   ├── StatCard.js
│   │   ├── StatusFeedback.js
│   │   ├── Table.js
│   │   ├── Tabs.js
│   │   ├── Textarea.js
│   │   ├── Toast.js
│   │   ├── Toggle.js
│   │   └── UserAutocomplete.js
│   ├── pages/                   # Page modules
│   │   ├── dashboard.js
│   │   ├── moderation.js
│   │   ├── bans.js
│   │   ├── settings.js
│   │   ├── design.js
│   │   └── tools.js
│   └── utils/
│       ├── api.js               # REST client
│       ├── dom.js               # DOM helpers
│       └── confirm.js           # Confirmation dialog
├── build/
│   ├── admin.js                 # Compiled JS
│   └── admin.css                # Compiled CSS
└── webpack.config.js
```

---

## Component Library Reference

### Button
```javascript
import { Button, SaveButton, CancelButton, DeleteButton } from '../components/Button.js';

Button({
    label: 'Click Me',
    variant: 'primary',    // primary | secondary | ghost | danger | success | warning
    size: 'md',            // lg | md | sm
    icon: '💾',
    onClick: () => {},
    disabled: false,
    type: 'button',        // button | submit
    attrs: {}
});

// Helpers
SaveButton('Save Changes', onClick, disabled);
CancelButton(onClick);
DeleteButton(onClick, 'Delete Item');
```

### Card
```javascript
import Card from '../components/Card.js';

Card('📊', 'Title', 'Description', [
    // children elements
], {
    collapsible: true,
    collapsed: false,
    warning: false,
    staggerIndex: 0
});
```

### Modal (Class)
```javascript
import Modal from '../components/Modal.js';

const modal = new Modal({
    title: 'Edit Item',
    width: '600px',
    premium: true,
    icon: '✏️',
    onClose: () => {}
});

modal.render(content, actions, tabs);
modal.open();
modal.close();
modal.updateContent(newContent);
modal.setTitle('New Title');
```

### Toast (MANDATORY for notifications)
```javascript
import Toast from '../components/Toast.js';

// ALWAYS use Toast - never create inline notifications
Toast.success('Settings saved!');
Toast.error('Something went wrong');
Toast.warning('Please review');
Toast.info('Update available');

Toast.show({
    message: 'Custom',
    type: 'success',
    duration: 6000,
    dismissible: true
});
```

---

## State Management Pattern

```javascript
// Module-level state (no Redux/Vuex)
let settings = {};
let originalSettings = {};
let isDirty = false;
let activeTab = 'general';

// Load state
async function loadSettings() {
    settings = await API.get('/settings');
    originalSettings = { ...settings };
}

// Components mutate state directly + callback
const field = FormField('key', 'Label', 'Help', 'text', settings, (k, v) => {
    isDirty = true;
});

// Dirty checking
function hasChanges() {
    return JSON.stringify(settings) !== JSON.stringify(originalSettings);
}

// Save state
async function save() {
    await API.post('/settings', settings);
    originalSettings = { ...settings };
    isDirty = false;
    Toast.success(__('Settings saved!', 'presszone-comments'));
}
```

---

## API Client Pattern

```javascript
import API from '../utils/api.js';

// GET request
const data = await API.get('/dashboard/stats');

// POST request
await API.post('/settings', settings);

// Error handling
try {
    await API.post('/endpoint', data);
    Toast.success('Done!');
} catch (error) {
    Toast.error(error.message);
}
```

**Note:** Nonce is auto-included via `X-WP-Nonce` header from `window.presszoneCommentsAdmin.nonce`

---

## DOM Utilities

```javascript
import { el, qs, qsa, clear, showLoading, __, mount } from '../utils/dom.js';

// Create elements (JSX-like without JSX)
el('div', { class: 'presszone-comments-card', onclick: handler },
    el('h3', {}, 'Title'),
    el('p', {}, 'Content')
);

// Query selectors
const container = qs('#presszone-comments-content');
const items = qsa('.presszone-comments-item');

// Clear container
clear(container);

// Show loading state
showLoading(container, 'Loading data...');

// Translation helper
__('Settings saved!', 'presszone-comments');

// Mount child to parent
mount(container, childElement);
```

---

## Routing System

```javascript
// Routes defined in admin.js
const routes = {
    '': renderDashboard,
    '#/': renderDashboard,
    '#/moderation': renderModeration,
    '#/bans': renderBans,
    '#/settings': renderSettings,
    '#/design': renderDesign,
    '#/tools': renderTools,
};

// Sub-routes supported
// #/settings/emails → renderSettings(container, addNotice, 'emails')

// Role-based access
// Admins: see all pages
```

---

## SCSS Variables Quick Reference

All variables defined in `admin/src-vanilla/css/_variables.scss`.

### Light Mode

| Variable | Value | Purpose |
|----------|-------|---------|
| `$presszone-comments-bg` | `#f8fafc` | Page background |
| `$presszone-comments-surface` | `#ffffff` | Card/panel background |
| `$presszone-comments-surface-2` | `#f1f5f9` | Secondary surface |
| `$presszone-comments-surface-3` | `#e2e8f0` | Tertiary surface |
| `$presszone-comments-primary` | `#1f71dd` | Primary action color |
| `$presszone-comments-text` | `#0f172a` | Primary text |
| `$presszone-comments-text-secondary` | `#334155` | Secondary text |
| `$presszone-comments-text-muted` | `#64748b` | Muted text |
| `$presszone-comments-border` | `#e2e8f0` | Border color |
| `$presszone-comments-success` | `#10b981` | Success state |
| `$presszone-comments-warning` | `#f59e0b` | Warning state |
| `$presszone-comments-error` | `#ef4444` | Error state |

### Dark Mode (used within `.dark-mode &` blocks)

| Variable | Light | Dark |
|----------|-------|------|
| `$presszone-comments-bg` | `#f8fafc` | `#131314` |
| `$presszone-comments-surface` | `#ffffff` | `#1e1f20` |
| `$presszone-comments-surface-2` | `#f1f5f9` | `#282a2c` |
| `$presszone-comments-surface-3` | `#e2e8f0` | `#353739` |
| `$presszone-comments-text` | `#0f172a` | `#e3e3e3` |
| `$presszone-comments-text-secondary` | `#334155` | `#c4c7c5` |
| `$presszone-comments-text-muted` | `#64748b` | `#bdc1c6` |
| `$presszone-comments-border` | `#e2e8f0` | `#3c4043` |
| `$presszone-comments-success` | `#10b981` | `#34d399` |
| `$presszone-comments-warning` | `#f59e0b` | `#fbbf24` |
| `$presszone-comments-error` | `#ef4444` | `#f87171` |

---

## Admin REST Endpoints Reference

| Route | Methods | Permission | Purpose |
|-------|---------|------------|---------|
| `/dashboard/stats` | GET | Admin | Stats overview |
| `/dashboard/activity` | GET | Admin | Activity timeline |
| `/settings` | GET, POST | Admin | Global settings |
| `/tools/{action}` | POST | Admin | Maintenance tools |
| `/moderation/queue` | GET | Admin | Pending comments |
| `/infractions` | GET, POST | Admin | User bans/warnings |
| `/reports` | GET | Admin | User reports |

---

## Translation (i18n) Rules

### Text Domain: `'presszone-comments'` - ALWAYS

```javascript
// JavaScript strings via __() helper from dom.js
import { __ } from '../utils/dom.js';

__('Settings saved!', 'presszone-comments')
__('Error occurred', 'presszone-comments')
```

### Translation Maintenance Rules

| Action | Requirement |
|--------|-------------|
| **Adding new UI string** | Add to all `.po` files in `languages/`, translate to all languages |
| **Removing UI string** | Remove from all `.po` files to avoid bloat |
| **Editing UI string** | Update in all `.po` files, retranslate appropriately |

### File Locations
- `languages/presszone-comments-{locale}.po` - PHP strings
- `languages/presszone-comments-{locale}-presszone-comments-admin-app.json` - Admin JS strings

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Using short JS prefixes (<4 chars) | Use `PresszoneCommentsApp`, `presszoneCommentsAdmin` |
| Hardcoded colors in dark mode | Use CSS variables |
| Hardcoded colors | Use `--presszone-comments-*` variables |
| Creating inline notifications | Use `Toast` component |
| Duplicating animation keyframes | Use shared from `_animations.css` |
| Hardcoded animation durations | Use `--presszone-comments-duration-*` |
| Missing reduced motion support | Add `@media (prefers-reduced-motion)` |
| Using `api.del()` | Use `API.delete()` |
| Forgetting to rebuild | Run `cd admin && npm run build` |
| Missing translation wrapper | Use `__('text', 'presszone-comments')` |
| Unsanitized input in PHP | Always sanitize with `sanitize_*()` |
| Raw SQL queries | Always use `$wpdb->prepare()` |
| Using `<table>` elements | NEVER - use `<div>` with CSS Grid/Flexbox |
| Class chaining `.class1.class2` | Use single BEM class |
| Magic numbers | Use SCSS variables |
| Using CSS custom properties | Use SCSS variables with classes |
| Inline styles for dynamic values | Use explicit CSS classes |
| `float` or `clearfix` | Use Flexbox or Grid |
| `!important` (except WP overrides) | Increase specificity properly |
| Deep CSS nesting | Flatten with BEM naming |

---

## Security, Compliance & Accessibility Rules

> **CRITICAL:** All code generated must follow these security, compliance, and accessibility standards.
> Reference: `.claude/agents/wordpress-security.md` for comprehensive security guidelines.

### Security Requirements

#### Authentication & Authorization
- **NEVER** use `is_admin()` for capability checks - use `current_user_can()` or `user_can()`
- Always verify nonces for state-changing operations: `wp_verify_nonce()`
- Implement proper capability checks for all admin endpoints
- Use WordPress REST API permissions callback for all custom endpoints

#### Input Validation & Sanitization
```php
// CORRECT - Always sanitize inputs
$value = sanitize_text_field($_POST['field']);
$email = sanitize_email($_POST['email']);
$url = esc_url_raw($_POST['url']);

// CORRECT - Validate before processing
if (!wp_verify_nonce($_POST['nonce'], 'action_name')) {
    wp_die('Security check failed');
}
```

#### Output Escaping
```php
// CORRECT - Always escape output
echo esc_html($user_input);
echo esc_attr($attribute_value);
echo esc_url($url_value);

// JavaScript
echo '<script>var data = ' . wp_json_encode($data) . ';</script>';
```

#### SQL Security
```php
// CORRECT - Always use prepared statements
$results = $wpdb->get_results($wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}table WHERE id = %d AND status = %s",
    $id, $status
));
```

### Compliance Requirements

#### WordPress.org Guidelines
- Use descriptive variable names (4+ characters)
- Prefix all global functions/classes with plugin name
- Follow WordPress coding standards
- Include proper text domains for translations

#### Data Privacy (GDPR/CCPA)
- Implement data export functionality
- Provide data deletion capabilities  
- Include privacy policy integration
- Log data processing activities

### Accessibility Requirements

#### WCAG 2.1 AA Compliance
```scss
// CORRECT - Ensure sufficient color contrast (4.5:1 minimum)
$presszone-comments-text: #0f172a; // Contrast ratio: 15.8:1 on white
$presszone-comments-link: #1f71dd;  // Contrast ratio: 4.52:1 on white

// CORRECT - Support reduced motion
@media (prefers-reduced-motion: reduce) {
    * {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}
```

#### Keyboard Navigation
```javascript
// CORRECT - Ensure keyboard accessibility
element.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        handleClick();
    }
});

// CORRECT - Manage focus properly
modal.addEventListener('show', () => {
    modal.querySelector('[autofocus]')?.focus();
});
```

#### Screen Reader Support
```javascript
// CORRECT - Use proper ARIA attributes
el('button', {
    'aria-label': __('Delete comment', 'presszone-comments'),
    'aria-describedby': 'delete-help-text',
    'role': 'button'
});

// CORRECT - Announce dynamic changes
Toast.success(message); // Automatically includes aria-live region
```

#### Semantic HTML
```javascript
// CORRECT - Use semantic elements
el('main', { role: 'main' },
    el('section', { 'aria-labelledby': 'dashboard-heading' },
        el('h1', { id: 'dashboard-heading' }, __('Dashboard', 'presszone-comments'))
    )
);
```

### Code Generation Rules

When generating any code, you MUST:

1. **Security First**: Include proper sanitization, validation, and escaping
2. **Accessibility Built-in**: Add ARIA attributes, keyboard support, and semantic HTML
3. **Privacy Compliant**: Consider data handling and user consent
4. **Performance Optimized**: Use efficient queries and minimize resource usage
5. **Error Handling**: Implement comprehensive error handling and user feedback

### Validation Checklist

Before submitting any code, verify:
- [ ] All user inputs are sanitized
- [ ] All outputs are escaped
- [ ] Proper capability checks are in place
- [ ] ARIA attributes are included
- [ ] Keyboard navigation works
- [ ] Color contrast meets WCAG standards
- [ ] Reduced motion is supported
- [ ] Error states are accessible
- [ ] Loading states are announced to screen readers

---

## 🔒 MANDATORY SECURITY & COMPLIANCE RULES

> **CRITICAL**: These rules are NON-NEGOTIABLE for admin panel development

### PHP Security (REST API & Backend)
- **Authentication**: Use `current_user_can('manage_options')` for admin capabilities, NEVER `is_admin()`
- **Input Validation**: Sanitize ALL user input: `sanitize_text_field()`, `sanitize_key()`, `absint()`
- **Output Escaping**: Escape ALL output: `esc_html()`, `esc_attr()`, `esc_url()`, `wp_kses_post()`
- **Nonces**: Verify nonces for ALL AJAX/POST requests: `wp_verify_nonce()`
- **Database**: Use `$wpdb->prepare()` with placeholders for ALL queries
- **REST API**: Always use `permission_callback` (never `__return_true`)

### JavaScript Security
- **XSS Prevention**: Use `textContent` instead of `innerHTML` for user data
- **Input Validation**: Validate all form inputs before sending to server
- **AJAX Security**: Include nonces in all AJAX requests
- **DOM Manipulation**: Sanitize any dynamic content insertion

### WordPress.org Compliance
- **Prefixing**: ALL functions/classes use `presszone_translate_` prefix (min 4 chars)
- **Text Domain**: Must be exactly `'translate-press-zone'`
- **Direct Access**: Every PHP file starts with: `if ( ! defined( 'ABSPATH' ) ) exit;`
- **Enqueuing**: Check specific admin pages with `$hook` parameter

### Accessibility (Admin UI)
- **Form Labels**: Associate all labels with inputs using `for` attribute
- **ARIA**: Use `aria-label`, `aria-describedby` for complex UI elements
- **Keyboard Navigation**: All interactive elements keyboard accessible
- **Focus Management**: Visible focus indicators, logical tab order
- **Screen Readers**: Use `aria-live` for dynamic status updates
- **WordPress Admin**: Use proper admin notice classes and help text

### Admin Panel Specific Security

#### REST API Endpoint Security
```php
// CORRECT - Secure admin endpoint registration
register_rest_route('translate-press-zone/v1', '/admin/(?P<action>[a-zA-Z0-9_-]+)', [
    'methods' => 'POST',
    'callback' => 'presszone_translate_admin_handler',
    'permission_callback' => function() {
        return current_user_can('manage_options');
    },
    'args' => [
        'action' => [
            'required' => true,
            'validate_callback' => function($param) {
                return in_array($param, ['save_settings', 'test_connection', 'export_data']);
            },
            'sanitize_callback' => 'sanitize_key'
        ]
    ]
]);
```

#### Admin AJAX Security
```php
// CORRECT - Secure admin AJAX handler
function presszone_translate_admin_ajax_handler() {
    // Verify nonce first
    if (!wp_verify_nonce($_POST['nonce'], 'presszone_translate_admin_nonce')) {
        wp_send_json_error(['message' => 'Security check failed']);
    }
    
    // Check admin capability
    if (!current_user_can('manage_options')) {
        wp_send_json_error(['message' => 'Insufficient permissions']);
    }
    
    // Sanitize action
    $action = sanitize_key($_POST['action']);
    
    // Process based on action
    switch ($action) {
        case 'save_settings':
            presszone_translate_save_admin_settings();
            break;
        default:
            wp_send_json_error(['message' => 'Invalid action']);
    }
}
```

#### Settings Form Security
```javascript
// CORRECT - Secure settings form submission
async function saveSettings() {
    // Validate inputs client-side
    const validation = validateSettingsForm();
    if (!validation.valid) {
        Toast.error(validation.message);
        return;
    }
    
    const formData = new FormData();
    formData.append('action', 'presszone_translate_save_settings');
    formData.append('nonce', window.presszoneTranslateAdmin.nonce);
    formData.append('settings', JSON.stringify(sanitizeSettings(settings)));
    
    try {
        const response = await fetch(ajaxurl, {
            method: 'POST',
            body: formData,
            credentials: 'same-origin'
        });
        
        const result = await response.json();
        if (result.success) {
            Toast.success(__('Settings saved successfully', 'translate-press-zone'));
        } else {
            Toast.error(result.data?.message || __('Save failed', 'translate-press-zone'));
        }
    } catch (error) {
        Toast.error(__('Network error occurred', 'translate-press-zone'));
    }
}
```

### Admin UI Accessibility

#### Dashboard Widget Accessibility
```javascript
// CORRECT - Accessible dashboard widgets
function createStatsWidget(title, value, icon, trend) {
    return el('div', {
        class: 'presszone-translate-stats-widget',
        role: 'region',
        'aria-labelledby': `widget-${title.toLowerCase()}-title`
    },
        el('h3', { 
            id: `widget-${title.toLowerCase()}-title`,
            class: 'presszone-translate-widget-title'
        }, title),
        el('div', { 
            class: 'presszone-translate-widget-value',
            'aria-describedby': `widget-${title.toLowerCase()}-trend`
        }, value),
        el('div', {
            id: `widget-${title.toLowerCase()}-trend`,
            class: 'presszone-translate-widget-trend',
            'aria-live': 'polite'
        }, trend)
    );
}
```

#### Form Field Accessibility
```javascript
// CORRECT - Accessible form fields with proper labeling
function createFormField(id, label, type, value, helpText, required = false) {
    const fieldId = `presszone-translate-${id}`;
    const helpId = `${fieldId}-help`;
    
    return el('div', { class: 'presszone-translate-form-field' },
        el('label', { 
            for: fieldId,
            class: 'presszone-translate-label'
        }, 
            label,
            required ? el('span', { 'aria-label': __('required', 'translate-press-zone') }, ' *') : ''
        ),
        el('input', {
            id: fieldId,
            type: type,
            value: value,
            'aria-describedby': helpText ? helpId : null,
            'aria-required': required ? 'true' : 'false',
            class: 'presszone-translate-input'
        }),
        helpText ? el('div', {
            id: helpId,
            class: 'presszone-translate-help-text'
        }, helpText) : null
    );
}
```

#### Modal Accessibility
```javascript
// CORRECT - Accessible modal implementation
class AccessibleModal extends Modal {
    open() {
        // Store previous focus
        this.previousFocus = document.activeElement;
        
        // Show modal
        super.open();
        
        // Set up focus trap
        this.setupFocusTrap();
        
        // Focus first focusable element
        const firstFocusable = this.modal.querySelector('[autofocus], button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
        if (firstFocusable) {
            firstFocusable.focus();
        }
        
        // Announce to screen readers
        this.announceModal();
    }
    
    close() {
        super.close();
        
        // Restore focus
        if (this.previousFocus) {
            this.previousFocus.focus();
        }
    }
    
    setupFocusTrap() {
        this.modal.addEventListener('keydown', (e) => {
            if (e.key === 'Tab') {
                const focusableElements = this.modal.querySelectorAll(
                    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
                );
                const firstElement = focusableElements[0];
                const lastElement = focusableElements[focusableElements.length - 1];
                
                if (e.shiftKey && document.activeElement === firstElement) {
                    e.preventDefault();
                    lastElement.focus();
                } else if (!e.shiftKey && document.activeElement === lastElement) {
                    e.preventDefault();
                    firstElement.focus();
                }
            } else if (e.key === 'Escape') {
                this.close();
            }
        });
    }
    
    announceModal() {
        const announcement = document.createElement('div');
        announcement.setAttribute('aria-live', 'assertive');
        announcement.setAttribute('aria-atomic', 'true');
        announcement.className = 'sr-only';
        announcement.textContent = __('Modal dialog opened', 'translate-press-zone');
        
        document.body.appendChild(announcement);
        setTimeout(() => document.body.removeChild(announcement), 1000);
    }
}
```

### Critical Patterns
```php
// ✅ SECURE PHP
if ( current_user_can('manage_options') ) { /* admin action */ }
echo esc_html( sanitize_text_field( wp_unslash( $_POST['data'] ) ) );
wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'admin_action' );
```

```javascript
// ✅ SECURE JS
element.textContent = userInput; // NOT innerHTML
fetch(ajaxurl, { body: formData, headers: { 'X-WP-Nonce': nonce } });
```