# CLAUDE.md — Maple Icons WordPress Plugin ## Quick Reference | Document | When to Reference | |----------|-------------------| | **CLAUDE.md** (this file) | Architecture, file structure, build order | | **SECURITY.md** | Writing ANY PHP code (same patterns as Maple Local Fonts) | --- ## Project Overview Build a WordPress plugin called **Maple Icons** that: 1. Fetches open-source icon sets from CDN and stores them locally 2. Provides preset icon sets: Heroicons, Lucide, Feather, Phosphor, Material 3. Only ONE icon set can be active at a time (though multiple can be downloaded) 4. Offers a Gutenberg block "Maple Icons" for inserting icons 5. Icons use `currentColor` to inherit text color from Global Styles **Key principle:** Download once, serve locally. Zero runtime external requests after initial fetch. --- ## Requirements - **Minimum PHP:** 7.4 - **Minimum WordPress:** 6.5 (for block.json apiVersion 3 and Global Styles) - **License:** GPL-2.0-or-later --- ## Preset Icon Sets All icons fetched from jsdelivr CDN with pinned versions: | Set | Package | Version | Styles | ~Icons | viewBox | |-----|---------|---------|--------|--------|---------| | Heroicons | `heroicons` | 2.1.1 | outline, solid, mini | 290 | 24×24 | | Lucide | `lucide-static` | 0.303.0 | icons | 1,400 | 24×24 | | Feather | `feather-icons` | 4.29.1 | icons | 280 | 24×24 | | Phosphor | `@phosphor-icons/core` | 2.1.1 | regular, bold, light, thin, fill, duotone | 1,200× styles | 256×256 (normalize) | | Material | `@material-design-icons/svg` | 0.14.13 | filled, outlined, round, sharp, two-tone | 2,500 | 24×24 | ### CDN URL Patterns ``` Heroicons: https://cdn.jsdelivr.net/npm/heroicons@2.1.1/24/outline/{name}.svg Lucide: https://cdn.jsdelivr.net/npm/lucide-static@0.303.0/icons/{name}.svg Feather: https://cdn.jsdelivr.net/npm/feather-icons@4.29.1/dist/icons/{name}.svg Phosphor: https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2.1.1/assets/{style}/{name}.svg Material: https://cdn.jsdelivr.net/npm/@material-design-icons/svg@0.14.13/{style}/{name}.svg ``` ### SVG Normalization Required | Set | Issue | Fix | |-----|-------|-----| | Phosphor | viewBox="0 0 256 256" | Normalize to 24×24 | | Material | `fill="#000000"` hardcoded | Replace with `currentColor` | | All | May have XML declarations, comments | Strip during save | --- ## User Flow ``` Admin visits Settings → Maple Icons (or plugin action link) ↓ Sees list of preset icon sets with Download/Delete buttons ↓ Clicks "Download" on Heroicons → progress bar → icons saved locally ↓ Clicks "Set Active" to make Heroicons the active set ↓ User inserts "Maple Icons" block in Gutenberg editor ↓ Block shows + button → click → icon picker modal with search ↓ User searches/filters → selects icon → SVG inserted inline ↓ SVG uses currentColor → inherits from Global Styles ↓ All icons served from local storage (wp-content/maple-icons/) ``` --- ## File Structure ``` maple-icons-wp/ ├── maple-icons.php # Main plugin file ├── index.php # Silence is golden ├── uninstall.php # Clean removal ├── readme.txt # WordPress.org readme ├── package.json # Block build config ├── includes/ │ ├── index.php │ ├── class-mi-icon-sets.php # Preset icon set definitions │ ├── class-mi-icon-registry.php # Icon management & search │ ├── class-mi-downloader.php # CDN fetch & local storage │ ├── class-mi-admin-page.php # Settings page │ └── class-mi-ajax-handler.php # AJAX handlers ├── presets/ # Bundled manifests (icon names, tags) │ ├── index.php │ ├── heroicons.json │ ├── lucide.json │ ├── feather.json │ ├── phosphor.json │ └── material.json ├── build/ # Compiled block assets (generated) │ ├── index.php │ ├── index.js │ ├── index.asset.php │ └── style-index.css ├── src/ # Block source (for development) │ ├── index.js # Block registration │ ├── edit.js # Editor component │ ├── save.js # Save component (inline SVG) │ ├── icon-picker.js # Icon selection modal │ ├── editor.scss # Editor-only styles │ └── block.json # Block metadata ├── assets/ │ ├── index.php │ ├── admin.css # Settings page styles │ └── admin.js # Settings page JS (download progress, etc.) └── languages/ ├── index.php └── maple-icons.pot ``` ### Local Icon Storage Icons are stored in: ``` wp-content/maple-icons/{set-slug}/{style}/{icon-name}.svg ``` Example: ``` wp-content/maple-icons/heroicons/outline/academic-cap.svg wp-content/maple-icons/heroicons/solid/academic-cap.svg wp-content/maple-icons/lucide/icons/activity.svg wp-content/maple-icons/phosphor/regular/airplane.svg ``` Using `wp-content/maple-icons/` (not uploads) to avoid cleanup plugin interference. --- ## Class Responsibilities ### MI_Icon_Sets Static definitions of all preset icon sets. ```php class MI_Icon_Sets { public static function get_all(): array; public static function get(string $slug): ?array; public static function get_cdn_url(string $slug, string $style, string $name): string; public static function get_manifest_path(string $slug): string; } ``` Returns structure: ```php [ 'slug' => 'heroicons', 'name' => 'Heroicons', 'version' => '2.1.1', 'license' => 'MIT', 'url' => 'https://heroicons.com', 'cdn_base' => 'https://cdn.jsdelivr.net/npm/heroicons@2.1.1/', 'styles' => [ 'outline' => ['path' => '24/outline', 'label' => 'Outline'], 'solid' => ['path' => '24/solid', 'label' => 'Solid'], 'mini' => ['path' => '20/solid', 'label' => 'Mini'], ], 'default_style' => 'outline', 'viewbox' => '0 0 24 24', 'normalize' => false, // true for Phosphor (256→24) 'color_fix' => false, // true for Material (replace hardcoded colors) ] ``` ### MI_Icon_Registry Manages downloaded sets and provides icon search/retrieval. ```php class MI_Icon_Registry { public function get_downloaded_sets(): array; public function get_active_set(): ?string; public function set_active(string $slug): bool; public function is_downloaded(string $slug): bool; public function get_icons_for_set(string $slug, string $style): array; public function search_icons(string $query, int $limit = 50): array; public function get_icon_svg(string $slug, string $style, string $name): string|WP_Error; public function delete_set(string $slug): bool; } ``` ### MI_Downloader Handles fetching icons from CDN and storing locally. ```php class MI_Downloader { public function download_set(string $slug, callable $progress_callback = null): array|WP_Error; public function download_icon(string $slug, string $style, string $name): string|WP_Error; private function normalize_svg(string $svg, array $set_config): string; private function get_local_path(string $slug, string $style, string $name): string; } ``` ### MI_Admin_Page Settings page under Settings → Maple Icons. - List all preset icon sets - Show download status (downloaded/not downloaded) - Download button with progress indicator - Delete button for downloaded sets - Radio buttons to select active set - Preview sample icons from each downloaded set ### MI_Ajax_Handler AJAX endpoints: - `mi_download_set` — Download an icon set from CDN - `mi_delete_set` — Delete a downloaded icon set - `mi_set_active` — Set the active icon set - `mi_search_icons` — Search icons in active set (for block picker) - `mi_get_icon_svg` — Get specific icon SVG (for block) --- ## Settings Storage ```php // Option name: maple_icons_settings [ 'active_set' => 'heroicons', // Slug of active set, or empty string 'downloaded_sets' => [ 'heroicons' => [ 'version' => '2.1.1', 'downloaded_at' => '2024-01-15 10:30:00', 'icon_count' => 876, // Total across all styles ], 'lucide' => [ 'version' => '0.303.0', 'downloaded_at' => '2024-01-15 11:00:00', 'icon_count' => 1400, ], ], ] ``` --- ## Manifest Format (presets/*.json) Each preset ships with a manifest listing all icons: ```json { "slug": "heroicons", "name": "Heroicons", "version": "2.1.1", "icons": [ { "name": "academic-cap", "tags": ["education", "graduation", "school", "hat"], "category": "objects", "styles": ["outline", "solid", "mini"] }, { "name": "adjustments-horizontal", "tags": ["settings", "controls", "sliders"], "category": "ui", "styles": ["outline", "solid", "mini"] } ] } ``` Manifests are bundled with the plugin to avoid runtime dependency on jsdelivr API. --- ## Gutenberg Block Architecture ### Block Registration (block.json) ```json { "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "jetrails/maple-icons", "version": "1.0.0", "title": "Maple Icons", "category": "design", "icon": "star-filled", "description": "Insert an icon from your downloaded icon sets.", "keywords": ["icon", "svg", "symbol", "maple"], "textdomain": "maple-icons", "attributes": { "iconSet": { "type": "string", "default": "" }, "iconStyle": { "type": "string", "default": "" }, "iconName": { "type": "string", "default": "" }, "iconSVG": { "type": "string", "default": "" }, "size": { "type": "number", "default": 24 }, "label": { "type": "string", "default": "" }, "strokeWidth": { "type": "number", "default": 0 }, "strokeColor": { "type": "string", "default": "" } }, "supports": { "html": false, "align": ["left", "center", "right"], "color": { "text": true, "background": true, "gradients": true }, "spacing": { "margin": true, "padding": true }, "shadow": true }, "editorScript": "file:./index.js", "editorStyle": "file:./style-index.css" } ``` ### Block Style Controls (Inspector Panel) The block sidebar will include these controls: **Icon Settings:** - Icon picker button (change icon) - Size slider (8-256px, default 24) - Accessibility label text input **Color Settings (via WordPress supports):** - Icon color (text color) — inherited by SVG via `currentColor` - Background color - Gradient support **Stroke Settings (custom panel):** - Stroke width slider (0-10px) - Stroke color picker **Spacing (via WordPress supports):** - Padding controls - Margin controls **Effects:** - Drop shadow presets (via WordPress `shadow` support) ### Style Application ```jsx // In edit.js and save.js, compute styles: const iconStyles = { width: `${size}px`, height: `${size}px`, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', }; // Stroke is applied via CSS filter or SVG manipulation const svgStyles = strokeWidth > 0 ? { filter: `drop-shadow(0 0 0 ${strokeColor})`, WebkitTextStroke: `${strokeWidth}px ${strokeColor}`, } : {}; // Note: For true SVG stroke, we modify the SVG's stroke attribute // This is handled in the SVG wrapper, not CSS ``` ### SVG Stroke Handling For stroke on SVG icons, we wrap the SVG output: ```jsx // The SVG itself uses currentColor for fill/stroke // Additional stroke effect applied via CSS or inline style ``` With CSS: ```css .wp-block-maple-icon__svg svg { stroke: var(--mi-stroke-color, currentColor); stroke-width: var(--mi-stroke-width, 0); paint-order: stroke fill; } ``` ### Block Behavior **Initial State (no icon selected):** - Shows + button placeholder - Click opens icon picker modal **With icon selected:** - Displays inline SVG - Sidebar shows: size control, accessibility label, change icon button **Icon Picker Modal:** - Search input (filters as you type) - Style selector dropdown (if set has multiple styles) - Grid of icon thumbnails - Click to select → SVG fetched and stored in attributes ### Save Output (save.js) ```jsx export default function save({ attributes }) { const { iconSVG, size, label } = attributes; if (!iconSVG) { return null; } const blockProps = useBlockProps.save({ className: 'wp-block-maple-icon', style: { width: `${size}px`, height: `${size}px`, display: 'inline-flex', }, }); return ( ); } ``` --- ## Download Process ### Batch Download Flow 1. User clicks "Download" for an icon set 2. Frontend disables button, shows progress bar 3. AJAX request to `mi_download_set` 4. Backend: a. Load manifest from `presets/{slug}.json` b. Create local directory structure c. For each icon in manifest: - Fetch SVG from CDN - Normalize (viewBox, currentColor) - Save to local filesystem d. Update settings with download info 5. Return success with icon count 6. Frontend updates UI to show "Downloaded" state ### Batching Strategy To avoid timeouts and memory issues: - Process icons in batches of 50 - Use streaming/chunked approach - Allow resume on failure (track progress in transient) ```php // Download in batches $batch_size = 50; $icons = $manifest['icons']; $total = count($icons); for ($i = 0; $i < $total; $i += $batch_size) { $batch = array_slice($icons, $i, $batch_size); foreach ($batch as $icon) { // Download each icon in batch } // Update progress transient set_transient('mi_download_progress_' . $slug, [ 'completed' => min($i + $batch_size, $total), 'total' => $total, ], HOUR_IN_SECONDS); } ``` --- ## SVG Sanitization Same approach as original CLAUDE.md — strict allowlist of SVG elements and attributes. ```php function mi_sanitize_svg($svg) { $allowed_tags = [ 'svg' => ['xmlns', 'viewbox', 'width', 'height', 'fill', 'stroke', ...], 'path' => ['d', 'fill', 'stroke', 'stroke-width', ...], 'circle' => ['cx', 'cy', 'r', 'fill', 'stroke', ...], 'rect' => ['x', 'y', 'width', 'height', 'rx', 'ry', ...], 'line' => ['x1', 'y1', 'x2', 'y2', ...], 'polyline' => ['points', ...], 'polygon' => ['points', ...], 'ellipse' => ['cx', 'cy', 'rx', 'ry', ...], 'g' => ['fill', 'stroke', 'transform', ...], ]; // Remove dangerous content $svg = preg_replace('/]*>.*?<\/script>/is', '', $svg); $svg = preg_replace('/\son\w+\s*=/i', ' data-removed=', $svg); return wp_kses($svg, $allowed_tags); } ``` ### SVG Normalization ```php function mi_normalize_svg($svg, $set_config) { // 1. Strip XML declaration $svg = preg_replace('/<\?xml[^>]*\?>/i', '', $svg); // 2. Strip comments $svg = preg_replace('//s', '', $svg); // 3. Normalize viewBox for Phosphor (256 → 24) if ($set_config['normalize']) { $svg = preg_replace('/viewBox=["\']0 0 256 256["\']/', 'viewBox="0 0 24 24"', $svg); } // 4. Fix hardcoded colors for Material if ($set_config['color_fix']) { $svg = preg_replace('/fill=["\']#[0-9a-fA-F]{3,6}["\']/', 'fill="currentColor"', $svg); $svg = preg_replace('/fill=["\']black["\']/', 'fill="currentColor"', $svg); } // 5. Ensure currentColor is used // Most sets already use currentColor, but double-check // 6. Remove width/height attributes (let CSS control) $svg = preg_replace('/\s(width|height)=["\'][^"\']*["\']/', '', $svg); return trim($svg); } ``` --- ## Admin Settings Page UI ``` ┌─────────────────────────────────────────────────────────────────┐ │ Maple Icons │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ICON SETS │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ ◉ Heroicons ✓ Downloaded │ │ │ │ 290 icons · Outline, Solid, Mini · MIT │ │ │ │ [Preview: ⚙ 🏠 👤 ✉ 🔔 ⭐] │ │ │ │ [Delete] │ │ │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ○ Lucide ✓ Downloaded │ │ │ │ 1,400 icons · Icons · ISC │ │ │ │ [Preview: ⚙ 📁 📄 💾 🖨 ⬇] │ │ │ │ [Delete] │ │ │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ○ Feather Not downloaded │ │ │ │ 280 icons · Icons · MIT │ │ │ │ [Download] │ │ │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ○ Phosphor Not downloaded │ │ │ │ 7,200 icons · 6 styles · MIT │ │ │ │ [Download] │ │ │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ○ Material Not downloaded │ │ │ │ 2,500 icons · 5 styles · Apache 2.0 │ │ │ │ [Download] │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ ◉ = Active icon set (used in Maple Icons block) │ │ │ │ ─────────────────────────────────────────────────────────────── │ │ │ │ USAGE │ │ 1. Download one or more icon sets above │ │ 2. Select which set should be active (radio button) │ │ 3. Insert icons using the "Maple Icons" block in the editor │ │ 4. Icons inherit your theme's text color automatically │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Icon Picker Modal (Block Editor) ``` ┌─────────────────────────────────────────────────────────────────┐ │ Select Icon [X] │ ├─────────────────────────────────────────────────────────────────┤ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 🔍 Search icons... │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ Style: [Outline ▼] │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │ │ │ │ ⚙ │ │ 🏠 │ │ 👤 │ │ ✉ │ │ 🔔 │ │ ⭐ │ │ ❤ │ │ 🔍 │ │ │ │ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │ │ │ cog home user mail bell star heart search │ │ │ │ │ │ │ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │ │ │ │ + │ │ ✓ │ │ ✕ │ │ ⬆ │ │ ⬇ │ │ ◀ │ │ ▶ │ │ ↻ │ │ │ │ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │ │ │ plus check x up down left right refresh │ │ │ │ │ │ │ │ [Load More...] │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ Selected: arrow-right │ │ │ │ [Cancel] [Insert Icon] │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Build Order ### Phase 1: Foundation 1. `maple-icons.php` — Plugin header, constants, autoloader, activation hook 2. `index.php` files in ALL directories 3. `includes/class-mi-icon-sets.php` — Static preset definitions ### Phase 2: Download Infrastructure 4. `includes/class-mi-downloader.php` — CDN fetch, normalize, store 5. `includes/class-mi-icon-registry.php` — Downloaded set management 6. `includes/class-mi-ajax-handler.php` — Download/delete/activate endpoints 7. Create manifest files: `presets/*.json` ### Phase 3: Admin Interface 8. `includes/class-mi-admin-page.php` — Settings page 9. `assets/admin.css` — Settings page styles 10. `assets/admin.js` — Download progress, AJAX handlers ### Phase 4: Gutenberg Block 11. `src/block.json` — Block metadata 12. `src/index.js` — Block registration 13. `src/edit.js` — Editor component 14. `src/save.js` — Save component 15. `src/icon-picker.js` — Modal component 16. `src/editor.scss` — Editor styles 17. `package.json` + build ### Phase 5: Cleanup & Polish 18. `uninstall.php` — Clean removal 19. `readme.txt` — WordPress.org readme 20. Testing --- ## Constants ```php define('MI_VERSION', '1.0.0'); define('MI_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('MI_PLUGIN_URL', plugin_dir_url(__FILE__)); define('MI_PLUGIN_BASENAME', plugin_basename(__FILE__)); define('MI_ICONS_DIR', WP_CONTENT_DIR . '/maple-icons/'); define('MI_ICONS_URL', content_url('/maple-icons/')); define('MI_PRESETS_DIR', MI_PLUGIN_DIR . 'presets/'); // Limits define('MI_DOWNLOAD_BATCH_SIZE', 50); define('MI_SEARCH_LIMIT', 50); define('MI_DOWNLOAD_TIMEOUT', 10); // Per-icon timeout in seconds ``` --- ## Security Checklist Reference SECURITY.md for full details. Key points: - [ ] ABSPATH check on every PHP file - [ ] index.php in every directory - [ ] Nonce verification FIRST in every AJAX handler - [ ] Capability check SECOND (`manage_options` for settings, `edit_posts` for block) - [ ] Validate icon set slugs against preset allowlist - [ ] Validate style slugs against set's defined styles - [ ] Sanitize all SVG content before storage - [ ] Validate file paths (prevent path traversal) - [ ] Use `wp_remote_get()` for CDN requests - [ ] Escape all output --- ## WordPress Hooks Used ```php // Activation/Deactivation register_activation_hook(__FILE__, 'mi_activate'); register_deactivation_hook(__FILE__, 'mi_deactivate'); // Block registration add_action('init', 'mi_register_block'); // Admin menu add_action('admin_menu', 'mi_register_settings_page'); // Admin assets (settings page only) add_action('admin_enqueue_scripts', 'mi_enqueue_admin_assets'); // Plugin action links (Settings link in plugin list) add_filter('plugin_action_links_' . MI_PLUGIN_BASENAME, 'mi_add_action_links'); // AJAX handlers (admin only) add_action('wp_ajax_mi_download_set', 'mi_ajax_download_set'); add_action('wp_ajax_mi_delete_set', 'mi_ajax_delete_set'); add_action('wp_ajax_mi_set_active', 'mi_ajax_set_active'); add_action('wp_ajax_mi_search_icons', 'mi_ajax_search_icons'); add_action('wp_ajax_mi_get_icon_svg', 'mi_ajax_get_icon_svg'); // WooCommerce HPOS compatibility add_action('before_woocommerce_init', 'mi_declare_hpos_compatibility'); ``` --- ## Compatibility Notes ### WooCommerce HPOS Declare compatibility (we don't touch orders): ```php add_action('before_woocommerce_init', function() { if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); } }); ``` ### Caching Plugins Icons stored locally means no cache issues. SVG is inline in post_content. ### Wordfence CDN requests only happen during admin download action, using standard `wp_remote_get()`. ### LearnDash / WPForms No interference — we only add a block, no frontend hooks. --- ## Testing Checklist ### Download Flow - [ ] Download Heroicons → all icons saved locally - [ ] Download progress shows correctly - [ ] Download Lucide (large set) → doesn't timeout - [ ] Delete downloaded set → files removed - [ ] Switch active set → setting persists ### Block Editor - [ ] Insert Maple Icons block → shows placeholder - [ ] Click + → opens icon picker - [ ] Search filters icons correctly - [ ] Style dropdown works (for sets with multiple styles) - [ ] Select icon → SVG appears in editor - [ ] Save post → icon persists - [ ] Frontend → icon displays correctly - [ ] Change text color in Global Styles → icon color changes ### Security - [ ] Download without nonce → 403 - [ ] Download as non-admin → 403 - [ ] Invalid set slug → rejected - [ ] Path traversal attempt → rejected ### Edge Cases - [ ] No sets downloaded → block shows helpful message - [ ] Active set deleted → block handles gracefully - [ ] CDN unreachable during download → appropriate error - [ ] Partial download failure → can retry/resume