monorepo/native/wordpress/maple-icons-wp/CLAUDE.MD
2026-02-02 14:17:16 -05:00

29 KiB
Raw Permalink Blame History

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.

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:

[
    '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.

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.

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

// 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:

{
    "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)

{
    "$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

// 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:

// The SVG itself uses currentColor for fill/stroke
// Additional stroke effect applied via CSS or inline style
<span
    className="wp-block-maple-icon__svg"
    style={{
        // Apply stroke via paint-order and stroke properties
        '--mi-stroke-width': strokeWidth ? `${strokeWidth}px` : undefined,
        '--mi-stroke-color': strokeColor || undefined,
    }}
    dangerouslySetInnerHTML={{ __html: iconSVG }}
/>

With 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)

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 (
        <span {...blockProps}>
            <span
                dangerouslySetInnerHTML={{ __html: iconSVG }}
                role={label ? 'img' : 'presentation'}
                aria-label={label || undefined}
                aria-hidden={!label ? 'true' : undefined}
            />
        </span>
    );
}

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)
// 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.

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\b[^>]*>.*?<\/script>/is', '', $svg);
    $svg = preg_replace('/\son\w+\s*=/i', ' data-removed=', $svg);

    return wp_kses($svg, $allowed_tags);
}

SVG Normalization

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

  1. includes/class-mi-downloader.php — CDN fetch, normalize, store
  2. includes/class-mi-icon-registry.php — Downloaded set management
  3. includes/class-mi-ajax-handler.php — Download/delete/activate endpoints
  4. Create manifest files: presets/*.json

Phase 3: Admin Interface

  1. includes/class-mi-admin-page.php — Settings page
  2. assets/admin.css — Settings page styles
  3. assets/admin.js — Download progress, AJAX handlers

Phase 4: Gutenberg Block

  1. src/block.json — Block metadata
  2. src/index.js — Block registration
  3. src/edit.js — Editor component
  4. src/save.js — Save component
  5. src/icon-picker.js — Modal component
  6. src/editor.scss — Editor styles
  7. package.json + build

Phase 5: Cleanup & Polish

  1. uninstall.php — Clean removal
  2. readme.txt — WordPress.org readme
  3. Testing

Constants

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

// 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):

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