From 423b9a25fb44e3f33290d4b8955ee7eb3f3d6316 Mon Sep 17 00:00:00 2001 From: rodolfomartinez Date: Mon, 2 Feb 2026 14:17:16 -0500 Subject: [PATCH] initial commit --- native/wordpress/maple-icons-wp/CLAUDE.MD | 827 ++++++++++++++++++ native/wordpress/maple-icons-wp/SECURITY.md | 621 +++++++++++++ .../maple-icons-wp/WORDPRESS_COMPATIBILITY.md | 560 ++++++++++++ .../wordpress/maple-icons-wp/assets/admin.css | 294 +++++++ .../wordpress/maple-icons-wp/assets/admin.js | 255 ++++++ .../wordpress/maple-icons-wp/assets/index.php | 5 + .../includes/class-mi-admin-page.php | 224 +++++ .../includes/class-mi-ajax-handler.php | 376 ++++++++ .../includes/class-mi-downloader.php | 505 +++++++++++ .../includes/class-mi-icon-registry.php | 416 +++++++++ .../includes/class-mi-icon-sets.php | 328 +++++++ .../maple-icons-wp/includes/index.php | 5 + native/wordpress/maple-icons-wp/index.php | 5 + .../maple-icons-wp/languages/index.php | 5 + .../wordpress/maple-icons-wp/maple-icons.php | 230 +++++ .../maple-icons-wp/presets/feather.json | 293 +++++++ .../maple-icons-wp/presets/heroicons.json | 295 +++++++ .../maple-icons-wp/presets/index.php | 5 + .../maple-icons-wp/presets/lucide.json | 357 ++++++++ .../maple-icons-wp/presets/material.json | 455 ++++++++++ .../maple-icons-wp/presets/phosphor.json | 418 +++++++++ native/wordpress/maple-icons-wp/readme.txt | 124 +++ native/wordpress/maple-icons-wp/src/index.php | 5 + native/wordpress/maple-icons-wp/uninstall.php | 62 ++ 24 files changed, 6670 insertions(+) create mode 100644 native/wordpress/maple-icons-wp/CLAUDE.MD create mode 100644 native/wordpress/maple-icons-wp/SECURITY.md create mode 100644 native/wordpress/maple-icons-wp/WORDPRESS_COMPATIBILITY.md create mode 100644 native/wordpress/maple-icons-wp/assets/admin.css create mode 100644 native/wordpress/maple-icons-wp/assets/admin.js create mode 100644 native/wordpress/maple-icons-wp/assets/index.php create mode 100644 native/wordpress/maple-icons-wp/includes/class-mi-admin-page.php create mode 100644 native/wordpress/maple-icons-wp/includes/class-mi-ajax-handler.php create mode 100644 native/wordpress/maple-icons-wp/includes/class-mi-downloader.php create mode 100644 native/wordpress/maple-icons-wp/includes/class-mi-icon-registry.php create mode 100644 native/wordpress/maple-icons-wp/includes/class-mi-icon-sets.php create mode 100644 native/wordpress/maple-icons-wp/includes/index.php create mode 100644 native/wordpress/maple-icons-wp/index.php create mode 100644 native/wordpress/maple-icons-wp/languages/index.php create mode 100644 native/wordpress/maple-icons-wp/maple-icons.php create mode 100644 native/wordpress/maple-icons-wp/presets/feather.json create mode 100644 native/wordpress/maple-icons-wp/presets/heroicons.json create mode 100644 native/wordpress/maple-icons-wp/presets/index.php create mode 100644 native/wordpress/maple-icons-wp/presets/lucide.json create mode 100644 native/wordpress/maple-icons-wp/presets/material.json create mode 100644 native/wordpress/maple-icons-wp/presets/phosphor.json create mode 100644 native/wordpress/maple-icons-wp/readme.txt create mode 100644 native/wordpress/maple-icons-wp/src/index.php create mode 100644 native/wordpress/maple-icons-wp/uninstall.php diff --git a/native/wordpress/maple-icons-wp/CLAUDE.MD b/native/wordpress/maple-icons-wp/CLAUDE.MD new file mode 100644 index 0000000..00e1b07 --- /dev/null +++ b/native/wordpress/maple-icons-wp/CLAUDE.MD @@ -0,0 +1,827 @@ +# 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 diff --git a/native/wordpress/maple-icons-wp/SECURITY.md b/native/wordpress/maple-icons-wp/SECURITY.md new file mode 100644 index 0000000..3016412 --- /dev/null +++ b/native/wordpress/maple-icons-wp/SECURITY.md @@ -0,0 +1,621 @@ +# SECURITY.md — Maple Local Fonts Security Requirements + +## Overview + +This document covers all security requirements for the Maple Local Fonts plugin. Reference this when writing ANY PHP code. + +--- + +## ABSPATH Check (Every PHP File) + +Every PHP file MUST start with this check. No exceptions. + +```php +query("SELECT * FROM table WHERE id = " . $_POST['id']); + +// ALWAYS do this +$wpdb->get_results($wpdb->prepare( + "SELECT * FROM %i WHERE id = %d", + $table_name, + absint($_POST['id']) +)); +``` + +**Note:** This plugin should rarely need direct SQL. Use WordPress APIs (`get_posts`, `wp_insert_post`, etc.) which handle escaping internally. + +### A2 - Authentication + +All admin actions require capability check: + +```php +if (!current_user_can('edit_theme_options')) { + wp_die('Unauthorized', 'Error', ['response' => 403]); +} +``` + +### A3 - Sensitive Data + +- No API keys (Google Fonts CSS2 API is public) +- No user credentials stored +- No PII collected + +### A5 - Broken Access Control + +**Order of checks for ALL AJAX handlers:** + +```php +public function handle_ajax_action() { + // 1. Nonce verification FIRST + if (!check_ajax_referer('mlf_action_name', 'nonce', false)) { + wp_send_json_error(['message' => 'Security check failed'], 403); + } + + // 2. Capability check SECOND + if (!current_user_can('edit_theme_options')) { + wp_send_json_error(['message' => 'Unauthorized'], 403); + } + + // 3. Input validation THIRD + // ... validate all inputs ... + + // 4. Process request + // ... actual logic ... +} +``` + +### A7 - Cross-Site Scripting (XSS) + +**Escape ALL output:** + +```php +// HTML content +echo esc_html($font_name); + +// HTML attributes +echo ''; + +// URLs +echo ''; + +// JavaScript data +wp_localize_script('mlf-admin', 'mlfData', [ + 'fontName' => esc_js($font_name), // Or let wp_localize_script handle it +]); + +// Translatable strings with variables +printf( + esc_html__('Installed: %s', 'maple-local-fonts'), + esc_html($font_name) +); +``` + +**Never trust input for output:** +```php +// WRONG - XSS vulnerability +echo '
' . $_POST['font_name'] . '
'; + +// RIGHT - sanitize input, escape output +$font_name = sanitize_text_field($_POST['font_name']); +echo '
' . esc_html($font_name) . '
'; +``` + +### A8 - Insecure Deserialization + +```php +// NEVER use unserialize() on external data +$data = unserialize($_POST['data']); // DANGEROUS + +// Use JSON instead +$data = json_decode(sanitize_text_field($_POST['data']), true); +if (json_last_error() !== JSON_ERROR_NONE) { + wp_send_json_error(['message' => 'Invalid data format']); +} +``` + +### A9 - Vulnerable Components + +- No external PHP libraries +- Use only WordPress core functions +- Keep dependencies to zero + +--- + +## Nonce Implementation + +### Creating Nonces + +**In admin page form:** +```php +wp_nonce_field('mlf_download_font', 'mlf_nonce'); +``` + +**For AJAX (via wp_localize_script):** +```php +wp_localize_script('mlf-admin', 'mlfData', [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('mlf_download_font'), +]); +``` + +### Verifying Nonces + +**AJAX handler:** +```php +// Returns false on failure, doesn't die (we handle response ourselves) +if (!check_ajax_referer('mlf_download_font', 'nonce', false)) { + wp_send_json_error(['message' => 'Security check failed'], 403); +} +``` + +**Form submission:** +```php +if (!wp_verify_nonce($_POST['mlf_nonce'], 'mlf_download_font')) { + wp_die('Security check failed'); +} +``` + +### Nonce Names + +Use consistent, descriptive nonce action names: + +| Action | Nonce Name | +|--------|------------| +| Download font | `mlf_download_font` | +| Delete font | `mlf_delete_font` | +| Update settings | `mlf_update_settings` | + +--- + +## Input Validation + +### Font Name Validation + +```php +$font_name = isset($_POST['font_name']) ? sanitize_text_field($_POST['font_name']) : ''; + +// Strict allowlist pattern - alphanumeric, spaces, hyphens only +if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) { + wp_send_json_error(['message' => 'Invalid font name: only letters, numbers, spaces, and hyphens allowed']); +} + +// Length limit +if (strlen($font_name) > 100) { + wp_send_json_error(['message' => 'Font name too long']); +} + +// Not empty +if (empty($font_name)) { + wp_send_json_error(['message' => 'Font name required']); +} +``` + +### Weight Validation + +```php +$weights = isset($_POST['weights']) ? (array) $_POST['weights'] : []; + +// Convert to integers +$weights = array_map('absint', $weights); + +// Strict allowlist +$allowed_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900]; +$weights = array_intersect($weights, $allowed_weights); + +// Must have at least one +if (empty($weights)) { + wp_send_json_error(['message' => 'At least one weight required']); +} +``` + +### Style Validation + +```php +$styles = isset($_POST['styles']) ? (array) $_POST['styles'] : []; + +// Strict allowlist - only these two values ever +$allowed_styles = ['normal', 'italic']; +$styles = array_filter($styles, function($style) use ($allowed_styles) { + return in_array($style, $allowed_styles, true); +}); + +// Must have at least one +if (empty($styles)) { + wp_send_json_error(['message' => 'At least one style required']); +} +``` + +### Font Family ID Validation (for delete) + +```php +$font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0; + +if ($font_id < 1) { + wp_send_json_error(['message' => 'Invalid font ID']); +} + +// Verify it exists and is a font family +$font = get_post($font_id); +if (!$font || $font->post_type !== 'wp_font_family') { + wp_send_json_error(['message' => 'Font not found']); +} + +// Verify it's one we imported (not a theme font) +if (get_post_meta($font_id, '_mlf_imported', true) !== '1') { + wp_send_json_error(['message' => 'Cannot delete theme fonts']); +} +``` + +--- + +## File Operation Security + +### Path Traversal Prevention + +```php +/** + * Validate that a path is within the WordPress fonts directory. + * Prevents path traversal attacks. + * + * @param string $path Full path to validate + * @return bool True if path is safe, false otherwise + */ +function mlf_validate_font_path($path) { + $font_dir = wp_get_font_dir(); + $fonts_path = wp_normalize_path(trailingslashit($font_dir['path'])); + + // Resolve to real path (handles ../ etc) + $real_path = realpath($path); + + // If realpath fails, file doesn't exist yet - validate the directory + if ($real_path === false) { + $dir = dirname($path); + $real_dir = realpath($dir); + if ($real_dir === false) { + return false; + } + $real_path = wp_normalize_path($real_dir . '/' . basename($path)); + } else { + $real_path = wp_normalize_path($real_path); + } + + // Must be within fonts directory + return strpos($real_path, $fonts_path) === 0; +} +``` + +### Filename Sanitization + +```php +/** + * Sanitize and validate a font filename. + * + * @param string $filename The filename to validate + * @return string|false Sanitized filename or false if invalid + */ +function mlf_sanitize_font_filename($filename) { + // WordPress sanitization first + $filename = sanitize_file_name($filename); + + // Must have .woff2 extension + if (pathinfo($filename, PATHINFO_EXTENSION) !== 'woff2') { + return false; + } + + // No path components + if ($filename !== basename($filename)) { + return false; + } + + // Reasonable length + if (strlen($filename) > 200) { + return false; + } + + return $filename; +} +``` + +### Safe File Writing + +```php +/** + * Safely write a font file to the fonts directory. + * + * @param string $filename Sanitized filename + * @param string $content File content + * @return string|WP_Error File path on success, WP_Error on failure + */ +function mlf_write_font_file($filename, $content) { + // Validate filename + $safe_filename = mlf_sanitize_font_filename($filename); + if ($safe_filename === false) { + return new WP_Error('invalid_filename', 'Invalid filename'); + } + + // Get fonts directory + $font_dir = wp_get_font_dir(); + $destination = trailingslashit($font_dir['path']) . $safe_filename; + + // Validate path + if (!mlf_validate_font_path($destination)) { + return new WP_Error('invalid_path', 'Invalid file path'); + } + + // Ensure directory exists + if (!wp_mkdir_p($font_dir['path'])) { + return new WP_Error('mkdir_failed', 'Could not create fonts directory'); + } + + // Write file + global $wp_filesystem; + if (empty($wp_filesystem)) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + } + + if (!$wp_filesystem->put_contents($destination, $content, FS_CHMOD_FILE)) { + return new WP_Error('write_failed', 'Could not write font file'); + } + + return $destination; +} +``` + +### Safe File Deletion + +```php +/** + * Safely delete a font file. + * + * @param string $path Full path to the file + * @return bool True on success, false on failure + */ +function mlf_delete_font_file($path) { + // Validate path is within fonts directory + if (!mlf_validate_font_path($path)) { + return false; + } + + // Must be a .woff2 file + if (pathinfo($path, PATHINFO_EXTENSION) !== 'woff2') { + return false; + } + + // File must exist + if (!file_exists($path)) { + return true; // Already gone, that's fine + } + + return wp_delete_file($path); +} +``` + +--- + +## HTTP Request Security + +### Outbound Requests (Google Fonts) + +```php +$response = wp_remote_get($url, [ + 'timeout' => 15, + 'sslverify' => true, // Always verify SSL + 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', +]); + +// Check for errors +if (is_wp_error($response)) { + // Log error, return gracefully + error_log('MLF: Google Fonts request failed - ' . $response->get_error_message()); + return new WP_Error('request_failed', 'Could not connect to Google Fonts'); +} + +// Check HTTP status +$status = wp_remote_retrieve_response_code($response); +if ($status !== 200) { + return new WP_Error('http_error', 'Google Fonts returned status ' . $status); +} + +// Get body +$body = wp_remote_retrieve_body($response); +if (empty($body)) { + return new WP_Error('empty_response', 'Empty response from Google Fonts'); +} +``` + +### URL Validation (Google Fonts only) + +```php +/** + * Validate that a URL is a legitimate Google Fonts URL. + * + * @param string $url URL to validate + * @return bool True if valid Google Fonts URL + */ +function mlf_is_valid_google_fonts_url($url) { + $parsed = wp_parse_url($url); + + if (!$parsed || !isset($parsed['host'])) { + return false; + } + + // Only allow Google Fonts domains + $allowed_hosts = [ + 'fonts.googleapis.com', + 'fonts.gstatic.com', + ]; + + return in_array($parsed['host'], $allowed_hosts, true); +} +``` + +--- + +## AJAX Handler Complete Template + +```php + 'Security check failed'], 403); + } + + // 2. CAPABILITY CHECK + if (!current_user_can('edit_theme_options')) { + wp_send_json_error(['message' => 'Unauthorized'], 403); + } + + // 3. INPUT VALIDATION + $font_name = isset($_POST['font_name']) ? sanitize_text_field($_POST['font_name']) : ''; + if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name) || strlen($font_name) > 100) { + wp_send_json_error(['message' => 'Invalid font name']); + } + + $weights = isset($_POST['weights']) ? array_map('absint', (array) $_POST['weights']) : []; + $weights = array_intersect($weights, [100, 200, 300, 400, 500, 600, 700, 800, 900]); + if (empty($weights)) { + wp_send_json_error(['message' => 'At least one weight required']); + } + + $styles = isset($_POST['styles']) ? (array) $_POST['styles'] : []; + $styles = array_intersect($styles, ['normal', 'italic']); + if (empty($styles)) { + wp_send_json_error(['message' => 'At least one style required']); + } + + // 4. PROCESS REQUEST + try { + $downloader = new MLF_Font_Downloader(); + $result = $downloader->download($font_name, $weights, $styles); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $result->get_error_message()]); + } + + wp_send_json_success([ + 'message' => sprintf('Successfully installed %s', esc_html($font_name)), + 'font_id' => $result, + ]); + } catch (Exception $e) { + error_log('MLF Download Error: ' . $e->getMessage()); + wp_send_json_error(['message' => 'An unexpected error occurred']); + } + } + + /** + * Handle font deletion AJAX request. + */ + public function handle_delete() { + // 1. NONCE CHECK + if (!check_ajax_referer('mlf_delete_font', 'nonce', false)) { + wp_send_json_error(['message' => 'Security check failed'], 403); + } + + // 2. CAPABILITY CHECK + if (!current_user_can('edit_theme_options')) { + wp_send_json_error(['message' => 'Unauthorized'], 403); + } + + // 3. INPUT VALIDATION + $font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0; + if ($font_id < 1) { + wp_send_json_error(['message' => 'Invalid font ID']); + } + + // Verify font exists and is ours + $font = get_post($font_id); + if (!$font || $font->post_type !== 'wp_font_family') { + wp_send_json_error(['message' => 'Font not found']); + } + if (get_post_meta($font_id, '_mlf_imported', true) !== '1') { + wp_send_json_error(['message' => 'Cannot delete theme fonts']); + } + + // 4. PROCESS REQUEST + try { + $registry = new MLF_Font_Registry(); + $result = $registry->delete_font($font_id); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $result->get_error_message()]); + } + + wp_send_json_success(['message' => 'Font deleted successfully']); + } catch (Exception $e) { + error_log('MLF Delete Error: ' . $e->getMessage()); + wp_send_json_error(['message' => 'An unexpected error occurred']); + } + } +} +``` + +--- + +## Security Checklist + +Before committing any code: + +- [ ] ABSPATH check at top of every PHP file +- [ ] index.php exists in every directory +- [ ] All AJAX handlers verify nonce first +- [ ] All AJAX handlers check capability second +- [ ] All user input sanitized with appropriate function +- [ ] All user input validated against allowlists where applicable +- [ ] All output escaped with appropriate function +- [ ] File paths validated to prevent traversal +- [ ] No direct SQL queries (use WordPress APIs) +- [ ] No `unserialize()` on user input +- [ ] No `eval()` or similar dynamic execution +- [ ] External URLs validated before use +- [ ] Error messages don't expose sensitive info diff --git a/native/wordpress/maple-icons-wp/WORDPRESS_COMPATIBILITY.md b/native/wordpress/maple-icons-wp/WORDPRESS_COMPATIBILITY.md new file mode 100644 index 0000000..59d61cc --- /dev/null +++ b/native/wordpress/maple-icons-wp/WORDPRESS_COMPATIBILITY.md @@ -0,0 +1,560 @@ +# WORDPRESS_COMPATIBILITY.md — WordPress & Plugin Compatibility + +## Overview + +This document covers compatibility requirements for WordPress core systems and popular plugins. Reference this when building the font registry class and integration points. + +--- + +## WordPress Version Requirements + +**Minimum: WordPress 6.5** + +WordPress 6.5 introduced the Font Library API which this plugin depends on. Earlier versions will not work. + +```php +// Check on activation +register_activation_hook(__FILE__, function() { + if (version_compare(get_bloginfo('version'), '6.5', '<')) { + deactivate_plugins(plugin_basename(__FILE__)); + wp_die( + 'Maple Local Fonts requires WordPress 6.5 or higher for Font Library support.', + 'Plugin Activation Error', + ['back_link' => true] + ); + } +}); + +// Also check on admin init (in case WP was downgraded) +add_action('admin_init', function() { + if (version_compare(get_bloginfo('version'), '6.5', '<')) { + deactivate_plugins(plugin_basename(__FILE__)); + add_action('admin_notices', function() { + echo '

Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher.

'; + }); + } +}); +``` + +--- + +## WordPress Font Library API + +### How It Works + +WordPress 6.5+ stores fonts using custom post types: + +| Post Type | Purpose | +|-----------|---------| +| `wp_font_family` | Font family (e.g., "Open Sans") | +| `wp_font_face` | Individual weight/style variant (child of family) | + +Fonts are stored in `wp-content/fonts/` by default. + +### Getting the Fonts Directory + +```php +// ALWAYS use this function, never hardcode paths +$font_dir = wp_get_font_dir(); + +// Returns: +[ + 'path' => '/var/www/html/wp-content/fonts', + 'url' => 'https://example.com/wp-content/fonts', + 'subdir' => '', + 'basedir' => '/var/www/html/wp-content/fonts', + 'baseurl' => 'https://example.com/wp-content/fonts', +] +``` + +### Registering a Font Family + +```php +/** + * Register a font family with WordPress Font Library. + * + * @param string $font_name Display name (e.g., "Open Sans") + * @param string $font_slug Slug (e.g., "open-sans") + * @param array $files Array of downloaded file data + * @return int|WP_Error Font family post ID or error + */ +function mlf_register_font_family($font_name, $font_slug, $files) { + // Check if font already exists + $existing = get_posts([ + 'post_type' => 'wp_font_family', + 'name' => $font_slug, + 'posts_per_page' => 1, + 'post_status' => 'publish', + ]); + + if (!empty($existing)) { + return new WP_Error('font_exists', 'Font family already installed'); + } + + // Build font family settings + $font_family_settings = [ + 'name' => $font_name, + 'slug' => $font_slug, + 'fontFamily' => sprintf('"%s", sans-serif', $font_name), + 'fontFace' => [], + ]; + + // Add each font face + foreach ($files as $file) { + $font_dir = wp_get_font_dir(); + $relative_path = str_replace($font_dir['path'], '', $file['path']); + + $font_family_settings['fontFace'][] = [ + 'fontFamily' => $font_name, + 'fontWeight' => $file['weight'], + 'fontStyle' => $file['style'], + 'src' => 'file:.' . $font_dir['basedir'] . $relative_path, + ]; + } + + // Create font family post + $family_id = wp_insert_post([ + 'post_type' => 'wp_font_family', + 'post_title' => $font_name, + 'post_name' => $font_slug, + 'post_status' => 'publish', + 'post_content' => wp_json_encode($font_family_settings), + ]); + + if (is_wp_error($family_id)) { + return $family_id; + } + + // Mark as imported by our plugin (for identification) + update_post_meta($family_id, '_mlf_imported', '1'); + update_post_meta($family_id, '_mlf_import_date', current_time('mysql')); + + // Create font face posts (children) + foreach ($files as $file) { + $font_dir = wp_get_font_dir(); + + $face_settings = [ + 'fontFamily' => $font_name, + 'fontWeight' => $file['weight'], + 'fontStyle' => $file['style'], + 'src' => 'file:.' . $font_dir['baseurl'] . '/' . basename($file['path']), + ]; + + wp_insert_post([ + 'post_type' => 'wp_font_face', + 'post_parent' => $family_id, + 'post_status' => 'publish', + 'post_content' => wp_json_encode($face_settings), + ]); + } + + // Clear font caches + delete_transient('wp_font_library_fonts'); + + return $family_id; +} +``` + +### Deleting a Font Family + +```php +/** + * Delete a font family and its files. + * + * @param int $family_id Font family post ID + * @return bool|WP_Error True on success, error on failure + */ +function mlf_delete_font_family($family_id) { + $family = get_post($family_id); + + if (!$family || $family->post_type !== 'wp_font_family') { + return new WP_Error('not_found', 'Font family not found'); + } + + // Verify it's one we imported + if (get_post_meta($family_id, '_mlf_imported', true) !== '1') { + return new WP_Error('not_ours', 'Cannot delete fonts not imported by this plugin'); + } + + // Get font faces + $faces = get_children([ + 'post_parent' => $family_id, + 'post_type' => 'wp_font_face', + ]); + + $font_dir = wp_get_font_dir(); + + // Delete font face files and posts + foreach ($faces as $face) { + $settings = json_decode($face->post_content, true); + + if (isset($settings['src'])) { + // Convert file:. URL to path + $src = $settings['src']; + $src = str_replace('file:.', '', $src); + + // Handle both URL and path formats + if (strpos($src, $font_dir['baseurl']) !== false) { + $file_path = str_replace($font_dir['baseurl'], $font_dir['path'], $src); + } else { + $file_path = $font_dir['path'] . '/' . basename($src); + } + + // Validate path before deletion + if (mlf_validate_font_path($file_path) && file_exists($file_path)) { + wp_delete_file($file_path); + } + } + + wp_delete_post($face->ID, true); + } + + // Delete family post + wp_delete_post($family_id, true); + + // Clear caches + delete_transient('wp_font_library_fonts'); + + return true; +} +``` + +### Listing Installed Fonts + +```php +/** + * Get all fonts imported by this plugin. + * + * @return array Array of font data + */ +function mlf_get_imported_fonts() { + $fonts = get_posts([ + 'post_type' => 'wp_font_family', + 'posts_per_page' => 100, + 'post_status' => 'publish', + 'meta_key' => '_mlf_imported', + 'meta_value' => '1', + ]); + + $result = []; + + foreach ($fonts as $font) { + $settings = json_decode($font->post_content, true); + + // Get variants + $faces = get_children([ + 'post_parent' => $font->ID, + 'post_type' => 'wp_font_face', + ]); + + $variants = []; + foreach ($faces as $face) { + $face_settings = json_decode($face->post_content, true); + $variants[] = [ + 'weight' => $face_settings['fontWeight'] ?? '400', + 'style' => $face_settings['fontStyle'] ?? 'normal', + ]; + } + + $result[] = [ + 'id' => $font->ID, + 'name' => $settings['name'] ?? $font->post_title, + 'slug' => $settings['slug'] ?? $font->post_name, + 'variants' => $variants, + 'import_date' => get_post_meta($font->ID, '_mlf_import_date', true), + ]; + } + + return $result; +} +``` + +--- + +## Gutenberg FSE Integration + +### How Fonts Appear in the Editor + +Once registered via the Font Library API, fonts automatically appear in: + +1. **Global Styles** → Typography → Font dropdown +2. **Block settings** → Typography → Font dropdown (when per-block typography is enabled) + +No additional integration code is needed — WordPress handles this automatically. + +### Theme.json Compatibility + +**DO NOT:** +- Directly modify theme.json +- Filter `wp_theme_json_data_theme` to inject fonts (let Font Library handle it) +- Override global styles CSS directly + +**DO:** +- Use the Font Library API (post types) +- Let WordPress generate CSS custom properties +- Trust the system + +### CSS Custom Properties + +When a font is applied in Global Styles, WordPress generates: + +```css +body { + --wp--preset--font-family--open-sans: "Open Sans", sans-serif; +} +``` + +And applies it: + +```css +body { + font-family: var(--wp--preset--font-family--open-sans); +} +``` + +Our plugin doesn't need to touch this — it's automatic. + +--- + +## WooCommerce Compatibility + +### HPOS (High-Performance Order Storage) + +WooCommerce's HPOS moves order data from post meta to custom tables. We must declare compatibility. + +```php +// Declare HPOS compatibility +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 + ); + } +}); +``` + +### Why We're Compatible + +Our plugin: +- Does NOT interact with orders at all +- Does NOT query wp_posts for order data +- Does NOT use wp_postmeta for order data +- Only uses wp_font_family and wp_font_face post types + +We're inherently compatible because we don't touch WooCommerce data. + +### Frontend Considerations + +**DO NOT:** +- Override `.woocommerce` class styles +- Override `.wc-block-*` styles +- Target cart/checkout elements specifically + +**DO:** +- Let WooCommerce elements inherit from body/heading fonts +- Let global styles cascade naturally + +WooCommerce product titles, descriptions, and other text will naturally inherit the fonts set via Global Styles. No special handling needed. + +--- + +## Wordfence Compatibility + +### Potential Concerns + +1. **Outbound requests** to Google Fonts during import +2. **AJAX endpoints** for admin actions +3. **File operations** in wp-content + +### Why We're Compatible + +**Outbound Requests:** +- Only occur during admin import (user-initiated action) +- Target well-known domains (fonts.googleapis.com, fonts.gstatic.com) +- Use standard `wp_remote_get()` which Wordfence allows +- No runtime external requests on frontend + +**AJAX Endpoints:** +- Use standard `admin-ajax.php` (not custom endpoints) +- Include proper nonces +- Follow WordPress patterns that Wordfence expects + +**File Operations:** +- Write only to `wp-content/fonts/` (WordPress default directory) +- Use WordPress Filesystem API +- Don't create executable files + +### Testing with Wordfence + +Test these scenarios with Wordfence active: + +- [ ] Learning Mode: Import should succeed +- [ ] Enabled Mode: Import should succeed +- [ ] Rate Limiting: Admin AJAX not blocked +- [ ] Firewall: No false positives on font download + +--- + +## LearnDash Compatibility + +### Overview + +LearnDash is a WordPress LMS that uses: +- Custom post types (courses, lessons, topics, quizzes) +- Custom templates +- Focus Mode (distraction-free learning) + +### Why We're Compatible + +Our plugin: +- Doesn't touch LearnDash post types +- Doesn't modify LearnDash templates +- Doesn't inject CSS on frontend +- Lets Global Styles cascade to LearnDash content + +LearnDash course content, lesson text, and quiz questions will inherit the fonts set in Global Styles automatically. + +### Focus Mode Consideration + +LearnDash Focus Mode uses its own template. Fonts set via Global Styles will apply because: +- Focus Mode still loads theme.json styles +- CSS custom properties cascade to all content +- No special handling needed + +**DO NOT:** +- Target `.learndash-*` classes specifically +- Override Focus Mode styles +- Inject custom CSS for LearnDash + +--- + +## WPForms Compatibility + +### Overview + +WPForms renders forms via shortcodes and blocks. Form styling is handled by WPForms. + +### Why We're Compatible + +- Form labels and text inherit from body font +- We don't override `.wpforms-*` classes +- No JavaScript conflicts (we have no frontend JS) + +### Consideration + +If a user wants form text in a different font, they should use WPForms' built-in styling options or custom CSS — not expect our plugin to handle it. + +--- + +## General Best Practices + +### What We Hook Into + +```php +// Admin menu +add_action('admin_menu', [$this, 'register_menu']); + +// Admin assets (only on our page) +add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); + +// AJAX handlers +add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']); +add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']); + +// WooCommerce HPOS compatibility +add_action('before_woocommerce_init', [$this, 'declare_hpos_compatibility']); +``` + +### What We DON'T Hook Into + +```php +// NO frontend hooks +// add_action('wp_enqueue_scripts', ...); // DON'T DO THIS +// add_action('wp_head', ...); // DON'T DO THIS +// add_action('wp_footer', ...); // DON'T DO THIS + +// NO theme modification hooks +// add_filter('wp_theme_json_data_theme', ...); // Let Font Library handle it + +// NO WooCommerce hooks +// add_action('woocommerce_*', ...); // DON'T DO THIS + +// NO content filters +// add_filter('the_content', ...); // DON'T DO THIS +``` + +--- + +## Conflict Debugging + +If a user reports a conflict, check: + +### 1. Plugin Load Order + +Our plugin should load with default priority. Check if another plugin is: +- Modifying the Font Library +- Overriding font CSS +- Filtering theme.json + +### 2. CSS Specificity + +If fonts aren't applying: +- Check browser DevTools for CSS cascade +- Look for more specific selectors overriding global styles +- Check for `!important` declarations + +### 3. Cache Issues + +Font changes not appearing: +- Clear browser cache +- Clear any caching plugins (WP Rocket, W3TC, etc.) +- Clear CDN cache if applicable +- WordPress transients: `delete_transient('wp_font_library_fonts')` + +### 4. JavaScript Errors + +If admin page isn't working: +- Check browser console for JS errors +- Look for conflicts with other admin scripts +- Verify jQuery isn't being dequeued + +--- + +## Compatibility Checklist + +Before releasing: + +### WordPress Core +- [ ] Works on WordPress 6.5 +- [ ] Works on WordPress 6.6+ +- [ ] Font Library API integration works +- [ ] Fonts appear in Global Styles +- [ ] Fonts apply correctly on frontend + +### WooCommerce +- [ ] HPOS compatibility declared +- [ ] No errors in WooCommerce status page +- [ ] Product pages render correctly with custom fonts +- [ ] Cart/Checkout not affected + +### Wordfence +- [ ] Import works with firewall enabled +- [ ] No blocked requests +- [ ] No false positive security alerts + +### LearnDash +- [ ] Course content inherits fonts +- [ ] Focus Mode renders correctly +- [ ] No JavaScript conflicts + +### WPForms +- [ ] Forms render correctly +- [ ] No styling conflicts + +### Other +- [ ] No PHP errors in debug.log +- [ ] No JavaScript errors in console +- [ ] Admin page loads correctly +- [ ] No memory issues during import diff --git a/native/wordpress/maple-icons-wp/assets/admin.css b/native/wordpress/maple-icons-wp/assets/admin.css new file mode 100644 index 0000000..175b3d3 --- /dev/null +++ b/native/wordpress/maple-icons-wp/assets/admin.css @@ -0,0 +1,294 @@ +/** + * Maple Icons - Admin Styles + * + * @package MapleIcons + */ + +/* Admin page wrapper */ +.mi-admin-wrap { + max-width: 1200px; +} + +.mi-admin-intro { + background: #fff; + border: 1px solid #c3c4c7; + border-left: 4px solid #2271b1; + padding: 12px 16px; + margin: 20px 0; +} + +.mi-admin-intro p { + margin: 0; + font-size: 14px; +} + +/* Icon sets grid */ +.mi-sets-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 20px; + margin: 20px 0; +} + +/* Individual set card */ +.mi-set-card { + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 4px; + padding: 20px; + position: relative; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.mi-set-card:hover { + border-color: #2271b1; +} + +.mi-set-card.mi-set-active { + border-color: #00a32a; + box-shadow: 0 0 0 1px #00a32a; +} + +.mi-set-card.mi-set-downloading { + opacity: 0.8; + pointer-events: none; +} + +/* Card header */ +.mi-set-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.mi-set-name { + margin: 0; + font-size: 16px; + font-weight: 600; + color: #1d2327; +} + +/* Badges */ +.mi-badge { + display: inline-block; + padding: 2px 8px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + border-radius: 3px; +} + +.mi-badge-active { + background: #00a32a; + color: #fff; +} + +.mi-badge-downloaded { + background: #dcdcde; + color: #50575e; +} + +/* Set metadata */ +.mi-set-meta { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 12px; + font-size: 13px; + color: #50575e; +} + +.mi-set-count { + font-weight: 600; + color: #1d2327; +} + +/* External link */ +.mi-set-link { + display: inline-block; + margin-bottom: 16px; + font-size: 13px; + text-decoration: none; +} + +.mi-set-link:hover { + text-decoration: underline; +} + +/* Action buttons */ +.mi-set-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.mi-set-actions .button { + flex: 1; + text-align: center; +} + +.mi-delete-btn { + color: #d63638 !important; + border-color: #d63638 !important; +} + +.mi-delete-btn:hover { + background: #d63638 !important; + color: #fff !important; +} + +/* Progress bar */ +.mi-set-progress { + margin-top: 16px; +} + +.mi-progress-bar { + height: 8px; + background: #dcdcde; + border-radius: 4px; + overflow: hidden; + margin-bottom: 8px; +} + +.mi-progress-fill { + height: 100%; + background: #2271b1; + border-radius: 4px; + width: 0; + transition: width 0.3s ease; +} + +.mi-progress-text { + font-size: 12px; + color: #50575e; +} + +/* Messages */ +.mi-set-message { + margin-top: 12px; + padding: 8px 12px; + border-radius: 4px; + font-size: 13px; +} + +.mi-set-message.mi-message-success { + background: #edfaef; + border: 1px solid #00a32a; + color: #00450c; +} + +.mi-set-message.mi-message-error { + background: #fcf0f1; + border: 1px solid #d63638; + color: #8a1f22; +} + +.mi-set-message.mi-message-info { + background: #f0f6fc; + border: 1px solid #2271b1; + color: #135e96; +} + +/* Usage section */ +.mi-admin-usage, +.mi-admin-info { + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 4px; + padding: 20px; + margin: 20px 0; +} + +.mi-admin-usage h2, +.mi-admin-info h2 { + margin-top: 0; + font-size: 16px; + border-bottom: 1px solid #c3c4c7; + padding-bottom: 10px; +} + +.mi-admin-usage ol { + margin: 16px 0 0; + padding-left: 24px; +} + +.mi-admin-usage li { + margin-bottom: 8px; + font-size: 14px; +} + +.mi-admin-info p { + font-size: 14px; + margin-bottom: 12px; +} + +.mi-admin-info p:last-child { + margin-bottom: 0; +} + +.mi-admin-info code { + background: #f0f0f1; + padding: 2px 6px; + border-radius: 3px; + font-size: 12px; +} + +/* Loading spinner */ +.mi-spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid #c3c4c7; + border-top-color: #2271b1; + border-radius: 50%; + animation: mi-spin 0.8s linear infinite; + margin-right: 8px; + vertical-align: middle; +} + +@keyframes mi-spin { + to { + transform: rotate(360deg); + } +} + +/* Button loading state */ +.button.mi-loading { + position: relative; + color: transparent !important; +} + +.button.mi-loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 14px; + height: 14px; + margin: -7px 0 0 -7px; + border: 2px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: mi-spin 0.8s linear infinite; +} + +.button-primary.mi-loading::after { + border-color: rgba(255, 255, 255, 0.3); + border-top-color: #fff; +} + +/* Responsive adjustments */ +@media screen and (max-width: 782px) { + .mi-sets-grid { + grid-template-columns: 1fr; + } + + .mi-set-actions { + flex-direction: column; + } + + .mi-set-actions .button { + width: 100%; + } +} diff --git a/native/wordpress/maple-icons-wp/assets/admin.js b/native/wordpress/maple-icons-wp/assets/admin.js new file mode 100644 index 0000000..52ed546 --- /dev/null +++ b/native/wordpress/maple-icons-wp/assets/admin.js @@ -0,0 +1,255 @@ +/** + * Maple Icons - Admin JavaScript + * + * @package MapleIcons + */ + +(function($) { + 'use strict'; + + /** + * Show message on a card. + * + * @param {jQuery} $card The card element. + * @param {string} message The message text. + * @param {string} type Message type: success, error, info. + */ + function showMessage($card, message, type) { + var $message = $card.find('.mi-set-message'); + $message + .removeClass('mi-message-success mi-message-error mi-message-info') + .addClass('mi-message-' + type) + .text(message) + .show(); + + // Auto-hide success messages after 5 seconds. + if (type === 'success') { + setTimeout(function() { + $message.fadeOut(); + }, 5000); + } + } + + /** + * Hide message on a card. + * + * @param {jQuery} $card The card element. + */ + function hideMessage($card) { + $card.find('.mi-set-message').hide(); + } + + /** + * Show progress bar. + * + * @param {jQuery} $card The card element. + */ + function showProgress($card) { + $card.find('.mi-set-progress').show(); + $card.find('.mi-set-actions').hide(); + } + + /** + * Hide progress bar. + * + * @param {jQuery} $card The card element. + */ + function hideProgress($card) { + $card.find('.mi-set-progress').hide(); + $card.find('.mi-set-actions').show(); + } + + /** + * Update progress bar. + * + * @param {jQuery} $card The card element. + * @param {number} percentage Progress percentage (0-100). + * @param {string} text Progress text. + */ + function updateProgress($card, percentage, text) { + $card.find('.mi-progress-fill').css('width', percentage + '%'); + $card.find('.mi-progress-text').text(text); + } + + /** + * Poll download progress. + * + * @param {string} slug Icon set slug. + * @param {jQuery} $card The card element. + */ + function pollProgress(slug, $card) { + $.ajax({ + url: miAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'mi_get_progress', + nonce: miAdmin.nonce, + slug: slug + }, + success: function(response) { + if (response.success && response.data.downloading) { + var percentage = response.data.percentage || 0; + var text = response.data.completed + ' / ' + response.data.total + ' icons'; + updateProgress($card, percentage, text); + + // Continue polling if still downloading. + setTimeout(function() { + pollProgress(slug, $card); + }, 1000); + } + } + }); + } + + /** + * Handle download button click. + */ + $(document).on('click', '.mi-download-btn', function(e) { + e.preventDefault(); + + var $btn = $(this); + var $card = $btn.closest('.mi-set-card'); + var slug = $btn.data('slug'); + + if ($btn.hasClass('mi-loading')) { + return; + } + + $btn.addClass('mi-loading'); + $card.addClass('mi-set-downloading'); + hideMessage($card); + showProgress($card); + updateProgress($card, 0, miAdmin.strings.downloading); + + // Start polling for progress. + setTimeout(function() { + pollProgress(slug, $card); + }, 500); + + $.ajax({ + url: miAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'mi_download_set', + nonce: miAdmin.nonce, + slug: slug + }, + success: function(response) { + hideProgress($card); + $btn.removeClass('mi-loading'); + $card.removeClass('mi-set-downloading'); + + if (response.success) { + showMessage($card, response.data.message || miAdmin.strings.downloadSuccess, 'success'); + // Reload page to update UI state. + setTimeout(function() { + location.reload(); + }, 1500); + } else { + showMessage($card, response.data.message || miAdmin.strings.downloadError, 'error'); + } + }, + error: function() { + hideProgress($card); + $btn.removeClass('mi-loading'); + $card.removeClass('mi-set-downloading'); + showMessage($card, miAdmin.strings.downloadError, 'error'); + } + }); + }); + + /** + * Handle delete button click. + */ + $(document).on('click', '.mi-delete-btn', function(e) { + e.preventDefault(); + + var $btn = $(this); + var $card = $btn.closest('.mi-set-card'); + var slug = $btn.data('slug'); + + if ($btn.hasClass('mi-loading')) { + return; + } + + if (!confirm(miAdmin.strings.confirmDelete)) { + return; + } + + $btn.addClass('mi-loading'); + hideMessage($card); + + $.ajax({ + url: miAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'mi_delete_set', + nonce: miAdmin.nonce, + slug: slug + }, + success: function(response) { + $btn.removeClass('mi-loading'); + + if (response.success) { + showMessage($card, response.data.message || miAdmin.strings.deleteSuccess, 'success'); + // Reload page to update UI state. + setTimeout(function() { + location.reload(); + }, 1500); + } else { + showMessage($card, response.data.message || miAdmin.strings.deleteError, 'error'); + } + }, + error: function() { + $btn.removeClass('mi-loading'); + showMessage($card, miAdmin.strings.deleteError, 'error'); + } + }); + }); + + /** + * Handle activate button click. + */ + $(document).on('click', '.mi-activate-btn, .mi-deactivate-btn', function(e) { + e.preventDefault(); + + var $btn = $(this); + var $card = $btn.closest('.mi-set-card'); + var slug = $btn.data('slug'); + + if ($btn.hasClass('mi-loading')) { + return; + } + + $btn.addClass('mi-loading'); + hideMessage($card); + + $.ajax({ + url: miAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'mi_set_active', + nonce: miAdmin.nonce, + slug: slug + }, + success: function(response) { + $btn.removeClass('mi-loading'); + + if (response.success) { + showMessage($card, response.data.message || miAdmin.strings.activateSuccess, 'success'); + // Reload page to update UI state. + setTimeout(function() { + location.reload(); + }, 1000); + } else { + showMessage($card, response.data.message || miAdmin.strings.activateError, 'error'); + } + }, + error: function() { + $btn.removeClass('mi-loading'); + showMessage($card, miAdmin.strings.activateError, 'error'); + } + }); + }); + +})(jQuery); diff --git a/native/wordpress/maple-icons-wp/assets/index.php b/native/wordpress/maple-icons-wp/assets/index.php new file mode 100644 index 0000000..398909d --- /dev/null +++ b/native/wordpress/maple-icons-wp/assets/index.php @@ -0,0 +1,5 @@ + admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'mi_admin_nonce' ), + 'strings' => array( + 'downloading' => __( 'Downloading...', 'maple-icons' ), + 'deleting' => __( 'Deleting...', 'maple-icons' ), + 'activating' => __( 'Activating...', 'maple-icons' ), + 'confirmDelete' => __( 'Are you sure you want to delete this icon set?', 'maple-icons' ), + 'downloadError' => __( 'Download failed. Please try again.', 'maple-icons' ), + 'deleteError' => __( 'Delete failed. Please try again.', 'maple-icons' ), + 'activateError' => __( 'Activation failed. Please try again.', 'maple-icons' ), + 'downloadSuccess'=> __( 'Icon set downloaded successfully!', 'maple-icons' ), + 'deleteSuccess' => __( 'Icon set deleted successfully!', 'maple-icons' ), + 'activateSuccess'=> __( 'Icon set activated!', 'maple-icons' ), + ), + ) + ); + } + + /** + * Render the settings page. + */ + public function render_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $registry = MI_Icon_Registry::get_instance(); + $all_sets = MI_Icon_Sets::get_all(); + $downloaded = $registry->get_downloaded_sets(); + $active_set = $registry->get_active_set(); + + ?> +
+

+ +
+

+
+ +
+

+ +
+ $set ) : ?> + +
+
+

+ + + + + +
+ +
+ + + + + + + 0 ) : ?> + + + + +
+ + +
+ → + + + +
+ + + + + + + + + + +
+ + + + +
+ +
+
+ +
+

+
    +
  1. +
  2. +
  3. +
  4. +
  5. +
+
+ +
+

+

+ +

+

+ +

+

+ ' . esc_html( MI_ICONS_DIR ) . '' + ); + ?> +

+
+
+ __( 'Security check failed.', 'maple-icons' ) ), + 403 + ); + } + + // 2. Capability check. + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( + array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ), + 403 + ); + } + + // 3. Input validation. + $slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : ''; + + if ( empty( $slug ) ) { + wp_send_json_error( + array( 'message' => __( 'Icon set slug is required.', 'maple-icons' ) ) + ); + } + + if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + wp_send_json_error( + array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) ) + ); + } + + // Check if already downloaded. + $registry = MI_Icon_Registry::get_instance(); + if ( $registry->is_downloaded( $slug ) ) { + wp_send_json_error( + array( 'message' => __( 'This icon set is already downloaded.', 'maple-icons' ) ) + ); + } + + // 4. Process download. + // Increase time limit for large downloads. + set_time_limit( 600 ); // 10 minutes. + + $downloader = new MI_Downloader(); + $result = $downloader->download_set( $slug ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( + array( 'message' => $result->get_error_message() ) + ); + } + + if ( ! $result['success'] ) { + wp_send_json_error( + array( + 'message' => sprintf( + /* translators: %1$d: downloaded count, %2$d: failed count */ + __( 'Download completed with errors. %1$d downloaded, %2$d failed.', 'maple-icons' ), + $result['downloaded'], + $result['failed'] + ), + 'errors' => array_slice( $result['errors'], 0, 10 ), // Limit errors shown. + 'downloaded' => $result['downloaded'], + 'failed' => $result['failed'], + ) + ); + } + + // Mark as downloaded. + $registry->mark_downloaded( $slug, $result['downloaded'] ); + + // If no active set, make this one active. + if ( ! $registry->get_active_set() ) { + $registry->set_active( $slug ); + } + + $registry->refresh(); + + wp_send_json_success( + array( + 'message' => sprintf( + /* translators: %d: number of icons */ + __( 'Successfully downloaded %d icons.', 'maple-icons' ), + $result['downloaded'] + ), + 'icon_count' => $result['downloaded'], + 'is_active' => $registry->get_active_set() === $slug, + ) + ); + } + + /** + * Handle delete set request. + */ + public function handle_delete_set() { + // 1. Nonce verification. + if ( ! check_ajax_referer( 'mi_admin_nonce', 'nonce', false ) ) { + wp_send_json_error( + array( 'message' => __( 'Security check failed.', 'maple-icons' ) ), + 403 + ); + } + + // 2. Capability check. + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( + array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ), + 403 + ); + } + + // 3. Input validation. + $slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : ''; + + if ( empty( $slug ) ) { + wp_send_json_error( + array( 'message' => __( 'Icon set slug is required.', 'maple-icons' ) ) + ); + } + + if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + wp_send_json_error( + array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) ) + ); + } + + // 4. Delete the set. + $registry = MI_Icon_Registry::get_instance(); + $result = $registry->delete_set( $slug ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( + array( 'message' => $result->get_error_message() ) + ); + } + + wp_send_json_success( + array( + 'message' => __( 'Icon set deleted successfully.', 'maple-icons' ), + ) + ); + } + + /** + * Handle set active request. + */ + public function handle_set_active() { + // 1. Nonce verification. + if ( ! check_ajax_referer( 'mi_admin_nonce', 'nonce', false ) ) { + wp_send_json_error( + array( 'message' => __( 'Security check failed.', 'maple-icons' ) ), + 403 + ); + } + + // 2. Capability check. + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( + array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ), + 403 + ); + } + + // 3. Input validation. + $slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : ''; + + // Empty slug is allowed (to deactivate). + if ( ! empty( $slug ) && ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + wp_send_json_error( + array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) ) + ); + } + + // 4. Set active. + $registry = MI_Icon_Registry::get_instance(); + $result = $registry->set_active( $slug ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( + array( 'message' => $result->get_error_message() ) + ); + } + + wp_send_json_success( + array( + 'message' => empty( $slug ) + ? __( 'No icon set is now active.', 'maple-icons' ) + : __( 'Icon set activated.', 'maple-icons' ), + 'active_set' => $slug, + ) + ); + } + + /** + * Handle get progress request. + */ + public function handle_get_progress() { + // 1. Nonce verification. + if ( ! check_ajax_referer( 'mi_admin_nonce', 'nonce', false ) ) { + wp_send_json_error( + array( 'message' => __( 'Security check failed.', 'maple-icons' ) ), + 403 + ); + } + + // 2. Capability check. + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( + array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ), + 403 + ); + } + + // 3. Input validation. + $slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : ''; + + if ( empty( $slug ) || ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + wp_send_json_error( + array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) ) + ); + } + + // 4. Get progress. + $downloader = new MI_Downloader(); + $progress = $downloader->get_progress( $slug ); + + if ( false === $progress ) { + wp_send_json_success( + array( + 'downloading' => false, + 'completed' => 0, + 'total' => 0, + ) + ); + } + + wp_send_json_success( + array( + 'downloading' => true, + 'completed' => $progress['completed'], + 'total' => $progress['total'], + 'percentage' => $progress['total'] > 0 + ? round( ( $progress['completed'] / $progress['total'] ) * 100 ) + : 0, + ) + ); + } + + /** + * Handle search icons request (for block editor). + */ + public function handle_search_icons() { + // 1. Nonce verification. + if ( ! check_ajax_referer( 'mi_block_nonce', 'nonce', false ) ) { + wp_send_json_error( + array( 'message' => __( 'Security check failed.', 'maple-icons' ) ), + 403 + ); + } + + // 2. Capability check (edit_posts for block usage). + if ( ! current_user_can( 'edit_posts' ) ) { + wp_send_json_error( + array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ), + 403 + ); + } + + // 3. Input validation. + $query = isset( $_POST['query'] ) ? sanitize_text_field( $_POST['query'] ) : ''; + $style = isset( $_POST['style'] ) ? sanitize_key( $_POST['style'] ) : ''; + $limit = isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : MI_SEARCH_LIMIT; + $offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0; + + // Limit the limit. + if ( $limit > 100 ) { + $limit = 100; + } + + // 4. Search. + $registry = MI_Icon_Registry::get_instance(); + $results = $registry->search_icons( $query, $style, $limit, $offset ); + + wp_send_json_success( $results ); + } + + /** + * Handle get icon SVG request (for block editor). + */ + public function handle_get_icon_svg() { + // 1. Nonce verification. + if ( ! check_ajax_referer( 'mi_block_nonce', 'nonce', false ) ) { + wp_send_json_error( + array( 'message' => __( 'Security check failed.', 'maple-icons' ) ), + 403 + ); + } + + // 2. Capability check. + if ( ! current_user_can( 'edit_posts' ) ) { + wp_send_json_error( + array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ), + 403 + ); + } + + // 3. Input validation. + $slug = isset( $_POST['set'] ) ? sanitize_key( $_POST['set'] ) : ''; + $style = isset( $_POST['style'] ) ? sanitize_key( $_POST['style'] ) : ''; + $name = isset( $_POST['name'] ) ? sanitize_file_name( $_POST['name'] ) : ''; + + if ( empty( $slug ) || empty( $style ) || empty( $name ) ) { + wp_send_json_error( + array( 'message' => __( 'Missing required parameters.', 'maple-icons' ) ) + ); + } + + // 4. Get SVG. + $registry = MI_Icon_Registry::get_instance(); + $svg = $registry->get_icon_svg( $slug, $style, $name ); + + if ( is_wp_error( $svg ) ) { + wp_send_json_error( + array( 'message' => $svg->get_error_message() ) + ); + } + + wp_send_json_success( + array( + 'svg' => $svg, + 'set' => $slug, + 'style' => $style, + 'name' => $name, + ) + ); + } +} diff --git a/native/wordpress/maple-icons-wp/includes/class-mi-downloader.php b/native/wordpress/maple-icons-wp/includes/class-mi-downloader.php new file mode 100644 index 0000000..61a0e0f --- /dev/null +++ b/native/wordpress/maple-icons-wp/includes/class-mi-downloader.php @@ -0,0 +1,505 @@ + array( + 'xmlns' => true, + 'viewbox' => true, + 'width' => true, + 'height' => true, + 'fill' => true, + 'stroke' => true, + 'stroke-width' => true, + 'stroke-linecap' => true, + 'stroke-linejoin' => true, + 'class' => true, + 'aria-hidden' => true, + 'role' => true, + 'focusable' => true, + 'style' => true, + ), + 'path' => array( + 'd' => true, + 'fill' => true, + 'stroke' => true, + 'stroke-width' => true, + 'stroke-linecap' => true, + 'stroke-linejoin' => true, + 'fill-rule' => true, + 'clip-rule' => true, + 'opacity' => true, + 'fill-opacity' => true, + 'stroke-opacity' => true, + ), + 'circle' => array( + 'cx' => true, + 'cy' => true, + 'r' => true, + 'fill' => true, + 'stroke' => true, + 'stroke-width' => true, + 'opacity' => true, + ), + 'rect' => array( + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + 'rx' => true, + 'ry' => true, + 'fill' => true, + 'stroke' => true, + 'stroke-width' => true, + 'opacity' => true, + ), + 'line' => array( + 'x1' => true, + 'y1' => true, + 'x2' => true, + 'y2' => true, + 'stroke' => true, + 'stroke-width' => true, + 'stroke-linecap' => true, + 'opacity' => true, + ), + 'polyline' => array( + 'points' => true, + 'fill' => true, + 'stroke' => true, + 'stroke-width' => true, + 'stroke-linecap' => true, + 'stroke-linejoin' => true, + 'opacity' => true, + ), + 'polygon' => array( + 'points' => true, + 'fill' => true, + 'stroke' => true, + 'stroke-width' => true, + 'opacity' => true, + ), + 'ellipse' => array( + 'cx' => true, + 'cy' => true, + 'rx' => true, + 'ry' => true, + 'fill' => true, + 'stroke' => true, + 'stroke-width' => true, + 'opacity' => true, + ), + 'g' => array( + 'fill' => true, + 'stroke' => true, + 'stroke-width' => true, + 'transform' => true, + 'opacity' => true, + ), + 'defs' => array(), + 'clippath' => array( + 'id' => true, + ), + 'use' => array( + 'href' => true, + 'xlink:href' => true, + ), + ); + + /** + * Download an entire icon set. + * + * @param string $slug Icon set slug. + * @param callable|null $progress_callback Optional progress callback. + * @return array|WP_Error Download results or error. + */ + public function download_set( $slug, $progress_callback = null ) { + // Validate slug. + if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + return new WP_Error( + 'invalid_set', + __( 'Invalid icon set.', 'maple-icons' ) + ); + } + + $set_config = MI_Icon_Sets::get( $slug ); + $manifest = MI_Icon_Sets::load_manifest( $slug ); + + if ( is_wp_error( $manifest ) ) { + return $manifest; + } + + // Create base directory. + $base_dir = MI_ICONS_DIR . $slug; + if ( ! $this->ensure_directory( $base_dir ) ) { + return new WP_Error( + 'directory_error', + __( 'Could not create icon directory.', 'maple-icons' ) + ); + } + + // Create style directories. + foreach ( $set_config['styles'] as $style_slug => $style_config ) { + $style_dir = $base_dir . '/' . $style_slug; + if ( ! $this->ensure_directory( $style_dir ) ) { + return new WP_Error( + 'directory_error', + __( 'Could not create style directory.', 'maple-icons' ) + ); + } + } + + $icons = isset( $manifest['icons'] ) ? $manifest['icons'] : array(); + $total_icons = count( $icons ); + $downloaded = 0; + $failed = 0; + $errors = array(); + $total_to_download = 0; + + // Calculate total icons to download (icon × styles). + foreach ( $icons as $icon ) { + $icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( $set_config['styles'] ); + $total_to_download += count( $icon_styles ); + } + + // Initialize progress transient. + set_transient( + 'mi_download_progress_' . $slug, + array( + 'completed' => 0, + 'total' => $total_to_download, + 'status' => 'downloading', + ), + HOUR_IN_SECONDS + ); + + $current = 0; + + // Download each icon. + foreach ( $icons as $icon ) { + $icon_name = $icon['name']; + $icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( $set_config['styles'] ); + + foreach ( $icon_styles as $style ) { + if ( ! isset( $set_config['styles'][ $style ] ) ) { + continue; + } + + $result = $this->download_icon( $slug, $style, $icon_name ); + + if ( is_wp_error( $result ) ) { + $failed++; + $errors[] = sprintf( '%s/%s: %s', $style, $icon_name, $result->get_error_message() ); + } else { + $downloaded++; + } + + $current++; + + // Update progress. + set_transient( + 'mi_download_progress_' . $slug, + array( + 'completed' => $current, + 'total' => $total_to_download, + 'status' => 'downloading', + ), + HOUR_IN_SECONDS + ); + + // Call progress callback if provided. + if ( is_callable( $progress_callback ) ) { + call_user_func( $progress_callback, $current, $total_to_download ); + } + + // Allow some breathing room for the server. + if ( 0 === $current % MI_DOWNLOAD_BATCH_SIZE ) { + usleep( 100000 ); // 100ms pause every batch. + } + } + } + + // Clear progress transient. + delete_transient( 'mi_download_progress_' . $slug ); + + return array( + 'success' => $failed === 0, + 'downloaded' => $downloaded, + 'failed' => $failed, + 'total' => $total_to_download, + 'errors' => $errors, + ); + } + + /** + * Download a single icon from CDN. + * + * @param string $slug Icon set slug. + * @param string $style Style slug. + * @param string $name Icon name. + * @return string|WP_Error Local file path or error. + */ + public function download_icon( $slug, $style, $name ) { + // Validate inputs. + if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) ); + } + + if ( ! MI_Icon_Sets::is_valid_style( $slug, $style ) ) { + return new WP_Error( 'invalid_style', __( 'Invalid icon style.', 'maple-icons' ) ); + } + + // Validate icon name (only allow alphanumeric, hyphens, underscores). + if ( ! preg_match( '/^[a-z0-9\-_]+$/i', $name ) ) { + return new WP_Error( 'invalid_name', __( 'Invalid icon name.', 'maple-icons' ) ); + } + + $cdn_url = MI_Icon_Sets::get_cdn_url( $slug, $style, $name ); + $local_path = MI_Icon_Sets::get_local_path( $slug, $style, $name ); + + // Check if already downloaded. + if ( file_exists( $local_path ) ) { + return $local_path; + } + + // Fetch from CDN. + $response = wp_remote_get( + $cdn_url, + array( + 'timeout' => MI_DOWNLOAD_TIMEOUT, + 'sslverify' => true, + ) + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $status_code = wp_remote_retrieve_response_code( $response ); + if ( 200 !== $status_code ) { + return new WP_Error( + 'cdn_error', + sprintf( + /* translators: %d: HTTP status code */ + __( 'CDN returned status %d.', 'maple-icons' ), + $status_code + ) + ); + } + + $svg_content = wp_remote_retrieve_body( $response ); + if ( empty( $svg_content ) ) { + return new WP_Error( 'empty_response', __( 'Empty response from CDN.', 'maple-icons' ) ); + } + + // Get set config for normalization. + $set_config = MI_Icon_Sets::get( $slug ); + + // Normalize and sanitize SVG. + $svg_content = $this->normalize_svg( $svg_content, $set_config ); + $svg_content = $this->sanitize_svg( $svg_content ); + + if ( empty( $svg_content ) ) { + return new WP_Error( 'invalid_svg', __( 'Invalid or empty SVG content.', 'maple-icons' ) ); + } + + // Ensure directory exists. + $dir = dirname( $local_path ); + if ( ! $this->ensure_directory( $dir ) ) { + return new WP_Error( 'directory_error', __( 'Could not create directory.', 'maple-icons' ) ); + } + + // Write file. + global $wp_filesystem; + if ( empty( $wp_filesystem ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + } + + if ( ! $wp_filesystem->put_contents( $local_path, $svg_content, FS_CHMOD_FILE ) ) { + return new WP_Error( 'write_error', __( 'Could not write SVG file.', 'maple-icons' ) ); + } + + return $local_path; + } + + /** + * Normalize SVG content based on set configuration. + * + * @param string $svg SVG content. + * @param array $set_config Icon set configuration. + * @return string Normalized SVG content. + */ + private function normalize_svg( $svg, $set_config ) { + // Strip XML declaration. + $svg = preg_replace( '/<\?xml[^>]*\?>/i', '', $svg ); + + // Strip DOCTYPE. + $svg = preg_replace( '/]*>/i', '', $svg ); + + // Strip comments. + $svg = preg_replace( '//s', '', $svg ); + + // Strip title and desc elements. + $svg = preg_replace( '/]*>.*?<\/title>/is', '', $svg ); + $svg = preg_replace( '/]*>.*?<\/desc>/is', '', $svg ); + + // Normalize viewBox for Phosphor (256 → 24). + if ( ! empty( $set_config['normalize'] ) ) { + $svg = preg_replace( + '/viewBox=["\']0\s+0\s+256\s+256["\']/i', + 'viewBox="0 0 24 24"', + $svg + ); + } + + // Fix hardcoded colors for Material. + if ( ! empty( $set_config['color_fix'] ) ) { + // Replace hardcoded hex colors. + $svg = preg_replace( '/fill=["\']#[0-9a-fA-F]{3,6}["\']/', 'fill="currentColor"', $svg ); + $svg = preg_replace( '/fill=["\']black["\']/', 'fill="currentColor"', $svg ); + $svg = preg_replace( '/fill=["\']rgb\([^)]+\)["\']/', 'fill="currentColor"', $svg ); + + // Same for stroke. + $svg = preg_replace( '/stroke=["\']#[0-9a-fA-F]{3,6}["\']/', 'stroke="currentColor"', $svg ); + $svg = preg_replace( '/stroke=["\']black["\']/', 'stroke="currentColor"', $svg ); + } + + // Remove width/height attributes (let CSS control size). + $svg = preg_replace( '/\s(width|height)=["\'][^"\']*["\']/i', '', $svg ); + + // Ensure there's no leading/trailing whitespace. + $svg = trim( $svg ); + + return $svg; + } + + /** + * Sanitize SVG content to remove potentially dangerous elements. + * + * @param string $svg SVG content. + * @return string Sanitized SVG content. + */ + private function sanitize_svg( $svg ) { + // Remove script tags. + $svg = preg_replace( '/]*>.*?<\/script>/is', '', $svg ); + + // Remove event handlers. + $svg = preg_replace( '/\s+on\w+\s*=/i', ' data-removed=', $svg ); + + // Remove javascript: URLs. + $svg = preg_replace( '/javascript:/i', 'removed:', $svg ); + + // Remove data: URLs (except for certain safe uses). + $svg = preg_replace( '/data:[^"\'>\s]+/i', 'removed:', $svg ); + + // Use WordPress kses with our allowed tags. + $svg = wp_kses( $svg, self::$allowed_svg_elements ); + + // Verify it's still a valid SVG. + if ( strpos( $svg, '' ) === false ) { + return ''; + } + + return $svg; + } + + /** + * Ensure a directory exists, creating it if necessary. + * + * @param string $dir Directory path. + * @return bool True if directory exists or was created. + */ + private function ensure_directory( $dir ) { + if ( file_exists( $dir ) ) { + return is_dir( $dir ); + } + + return wp_mkdir_p( $dir ); + } + + /** + * Delete all icons for a set. + * + * @param string $slug Icon set slug. + * @return bool True on success. + */ + public function delete_set( $slug ) { + if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + return false; + } + + $set_dir = MI_ICONS_DIR . $slug; + + // Validate the path is within our icons directory. + $real_path = realpath( $set_dir ); + $allowed_base = realpath( MI_ICONS_DIR ); + + if ( false === $real_path || false === $allowed_base ) { + // Directory doesn't exist, nothing to delete. + return true; + } + + if ( strpos( $real_path, $allowed_base ) !== 0 ) { + // Path traversal attempt. + return false; + } + + // Recursively delete the directory. + return $this->delete_directory( $set_dir ); + } + + /** + * Recursively delete a directory. + * + * @param string $dir Directory path. + * @return bool True on success. + */ + private function delete_directory( $dir ) { + if ( ! is_dir( $dir ) ) { + return true; + } + + $files = array_diff( scandir( $dir ), array( '.', '..' ) ); + + foreach ( $files as $file ) { + $path = $dir . '/' . $file; + if ( is_dir( $path ) ) { + $this->delete_directory( $path ); + } else { + wp_delete_file( $path ); + } + } + + return rmdir( $dir ); + } + + /** + * Get download progress for a set. + * + * @param string $slug Icon set slug. + * @return array|false Progress data or false if not downloading. + */ + public function get_progress( $slug ) { + return get_transient( 'mi_download_progress_' . $slug ); + } +} diff --git a/native/wordpress/maple-icons-wp/includes/class-mi-icon-registry.php b/native/wordpress/maple-icons-wp/includes/class-mi-icon-registry.php new file mode 100644 index 0000000..2a2d06c --- /dev/null +++ b/native/wordpress/maple-icons-wp/includes/class-mi-icon-registry.php @@ -0,0 +1,416 @@ +load_settings(); + } + + /** + * Load settings from database. + */ + private function load_settings() { + $this->settings = get_option( + 'maple_icons_settings', + array( + 'active_set' => '', + 'downloaded_sets' => array(), + ) + ); + } + + /** + * Save settings to database. + * + * @return bool True on success. + */ + private function save_settings() { + return update_option( 'maple_icons_settings', $this->settings ); + } + + /** + * Get all downloaded sets. + * + * @return array Array of downloaded set data. + */ + public function get_downloaded_sets() { + return isset( $this->settings['downloaded_sets'] ) ? $this->settings['downloaded_sets'] : array(); + } + + /** + * Get the active set slug. + * + * @return string|null Active set slug or null if none. + */ + public function get_active_set() { + $active = isset( $this->settings['active_set'] ) ? $this->settings['active_set'] : ''; + return ! empty( $active ) ? $active : null; + } + + /** + * Set the active icon set. + * + * @param string $slug Icon set slug. + * @return bool|WP_Error True on success, error on failure. + */ + public function set_active( $slug ) { + // Allow empty string to deactivate. + if ( empty( $slug ) ) { + $this->settings['active_set'] = ''; + return $this->save_settings(); + } + + // Validate slug. + if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) ); + } + + // Check if downloaded. + if ( ! $this->is_downloaded( $slug ) ) { + return new WP_Error( 'not_downloaded', __( 'Icon set is not downloaded.', 'maple-icons' ) ); + } + + $this->settings['active_set'] = $slug; + return $this->save_settings(); + } + + /** + * Check if a set is downloaded. + * + * @param string $slug Icon set slug. + * @return bool True if downloaded. + */ + public function is_downloaded( $slug ) { + $downloaded = $this->get_downloaded_sets(); + return isset( $downloaded[ $slug ] ); + } + + /** + * Mark a set as downloaded. + * + * @param string $slug Icon set slug. + * @param int $icon_count Number of icons downloaded. + * @return bool True on success. + */ + public function mark_downloaded( $slug, $icon_count ) { + if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + return false; + } + + $set_config = MI_Icon_Sets::get( $slug ); + + $this->settings['downloaded_sets'][ $slug ] = array( + 'version' => $set_config['version'], + 'downloaded_at' => current_time( 'mysql' ), + 'icon_count' => $icon_count, + ); + + return $this->save_settings(); + } + + /** + * Remove a set from downloaded list. + * + * @param string $slug Icon set slug. + * @return bool True on success. + */ + public function unmark_downloaded( $slug ) { + if ( isset( $this->settings['downloaded_sets'][ $slug ] ) ) { + unset( $this->settings['downloaded_sets'][ $slug ] ); + } + + // If this was the active set, clear it. + if ( $this->settings['active_set'] === $slug ) { + $this->settings['active_set'] = ''; + } + + return $this->save_settings(); + } + + /** + * Get all icons for a set and style. + * + * @param string $slug Icon set slug. + * @param string $style Style slug. + * @return array Array of icon data. + */ + public function get_icons_for_set( $slug, $style ) { + if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + return array(); + } + + if ( ! MI_Icon_Sets::is_valid_style( $slug, $style ) ) { + return array(); + } + + // Load manifest. + $manifest = MI_Icon_Sets::load_manifest( $slug ); + if ( is_wp_error( $manifest ) ) { + return array(); + } + + $icons = isset( $manifest['icons'] ) ? $manifest['icons'] : array(); + $result = array(); + + foreach ( $icons as $icon ) { + $icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( MI_Icon_Sets::get( $slug )['styles'] ); + + // Only include if this icon has the requested style. + if ( in_array( $style, $icon_styles, true ) ) { + $result[] = array( + 'name' => $icon['name'], + 'tags' => isset( $icon['tags'] ) ? $icon['tags'] : array(), + 'category' => isset( $icon['category'] ) ? $icon['category'] : '', + ); + } + } + + return $result; + } + + /** + * Search icons in the active set. + * + * @param string $query Search query. + * @param string $style Optional style filter. + * @param int $limit Maximum results. + * @param int $offset Offset for pagination. + * @return array Search results. + */ + public function search_icons( $query = '', $style = '', $limit = MI_SEARCH_LIMIT, $offset = 0 ) { + $active_set = $this->get_active_set(); + if ( ! $active_set ) { + return array( + 'icons' => array(), + 'total' => 0, + ); + } + + $set_config = MI_Icon_Sets::get( $active_set ); + if ( ! $set_config ) { + return array( + 'icons' => array(), + 'total' => 0, + ); + } + + // Default to first style if not specified. + if ( empty( $style ) ) { + $style = $set_config['default_style']; + } + + // Validate style. + if ( ! MI_Icon_Sets::is_valid_style( $active_set, $style ) ) { + $style = $set_config['default_style']; + } + + // Load manifest. + $manifest = MI_Icon_Sets::load_manifest( $active_set ); + if ( is_wp_error( $manifest ) ) { + return array( + 'icons' => array(), + 'total' => 0, + ); + } + + $icons = isset( $manifest['icons'] ) ? $manifest['icons'] : array(); + $query = strtolower( trim( $query ) ); + $results = array(); + + foreach ( $icons as $icon ) { + $icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( $set_config['styles'] ); + + // Skip if this icon doesn't have the requested style. + if ( ! in_array( $style, $icon_styles, true ) ) { + continue; + } + + // If no query, include all. + if ( empty( $query ) ) { + $results[] = $icon; + continue; + } + + // Search in name. + if ( strpos( strtolower( $icon['name'] ), $query ) !== false ) { + $results[] = $icon; + continue; + } + + // Search in tags. + if ( isset( $icon['tags'] ) && is_array( $icon['tags'] ) ) { + foreach ( $icon['tags'] as $tag ) { + if ( strpos( strtolower( $tag ), $query ) !== false ) { + $results[] = $icon; + break; + } + } + } + } + + $total = count( $results ); + + // Apply offset and limit. + $results = array_slice( $results, $offset, $limit ); + + // Format results. + $formatted = array(); + foreach ( $results as $icon ) { + $formatted[] = array( + 'name' => $icon['name'], + 'tags' => isset( $icon['tags'] ) ? $icon['tags'] : array(), + 'category' => isset( $icon['category'] ) ? $icon['category'] : '', + 'set' => $active_set, + 'style' => $style, + ); + } + + return array( + 'icons' => $formatted, + 'total' => $total, + 'set' => $active_set, + 'style' => $style, + 'styles' => MI_Icon_Sets::get_style_labels( $active_set ), + ); + } + + /** + * Get SVG content for a specific icon. + * + * @param string $slug Icon set slug. + * @param string $style Style slug. + * @param string $name Icon name. + * @return string|WP_Error SVG content or error. + */ + public function get_icon_svg( $slug, $style, $name ) { + // Validate inputs. + if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) ); + } + + if ( ! MI_Icon_Sets::is_valid_style( $slug, $style ) ) { + return new WP_Error( 'invalid_style', __( 'Invalid icon style.', 'maple-icons' ) ); + } + + // Validate icon name. + if ( ! preg_match( '/^[a-z0-9\-_]+$/i', $name ) ) { + return new WP_Error( 'invalid_name', __( 'Invalid icon name.', 'maple-icons' ) ); + } + + $local_path = MI_Icon_Sets::get_local_path( $slug, $style, $name ); + + // Validate path is within icons directory (prevent path traversal). + $real_path = realpath( $local_path ); + $allowed_base = realpath( MI_ICONS_DIR ); + + if ( false === $real_path ) { + return new WP_Error( 'not_found', __( 'Icon not found.', 'maple-icons' ) ); + } + + if ( false === $allowed_base || strpos( $real_path, $allowed_base ) !== 0 ) { + return new WP_Error( 'invalid_path', __( 'Invalid icon path.', 'maple-icons' ) ); + } + + if ( ! file_exists( $real_path ) ) { + return new WP_Error( 'not_found', __( 'Icon not found.', 'maple-icons' ) ); + } + + $svg = file_get_contents( $real_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + + if ( false === $svg ) { + return new WP_Error( 'read_error', __( 'Could not read icon file.', 'maple-icons' ) ); + } + + return $svg; + } + + /** + * Delete a downloaded set. + * + * @param string $slug Icon set slug. + * @return bool|WP_Error True on success, error on failure. + */ + public function delete_set( $slug ) { + if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) { + return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) ); + } + + if ( ! $this->is_downloaded( $slug ) ) { + return new WP_Error( 'not_downloaded', __( 'Icon set is not downloaded.', 'maple-icons' ) ); + } + + // Delete files. + $downloader = new MI_Downloader(); + $deleted = $downloader->delete_set( $slug ); + + if ( ! $deleted ) { + return new WP_Error( 'delete_error', __( 'Could not delete icon files.', 'maple-icons' ) ); + } + + // Update settings. + $this->unmark_downloaded( $slug ); + + return true; + } + + /** + * Get info about a downloaded set. + * + * @param string $slug Icon set slug. + * @return array|null Set info or null if not downloaded. + */ + public function get_downloaded_info( $slug ) { + $downloaded = $this->get_downloaded_sets(); + return isset( $downloaded[ $slug ] ) ? $downloaded[ $slug ] : null; + } + + /** + * Refresh settings from database. + */ + public function refresh() { + $this->load_settings(); + } +} diff --git a/native/wordpress/maple-icons-wp/includes/class-mi-icon-sets.php b/native/wordpress/maple-icons-wp/includes/class-mi-icon-sets.php new file mode 100644 index 0000000..a1ce998 --- /dev/null +++ b/native/wordpress/maple-icons-wp/includes/class-mi-icon-sets.php @@ -0,0 +1,328 @@ + array( + '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' => array( + 'outline' => array( + 'path' => '24/outline', + 'label' => 'Outline', + ), + 'solid' => array( + 'path' => '24/solid', + 'label' => 'Solid', + ), + 'mini' => array( + 'path' => '20/solid', + 'label' => 'Mini', + ), + ), + 'default_style' => 'outline', + 'viewbox' => '0 0 24 24', + 'normalize' => false, + 'color_fix' => false, + 'description' => 'Beautiful hand-crafted SVG icons by the makers of Tailwind CSS.', + 'icon_count' => 292, + ), + 'lucide' => array( + 'slug' => 'lucide', + 'name' => 'Lucide', + 'version' => '0.303.0', + 'license' => 'ISC', + 'url' => 'https://lucide.dev', + 'cdn_base' => 'https://cdn.jsdelivr.net/npm/lucide-static@0.303.0/', + 'styles' => array( + 'icons' => array( + 'path' => 'icons', + 'label' => 'Default', + ), + ), + 'default_style' => 'icons', + 'viewbox' => '0 0 24 24', + 'normalize' => false, + 'color_fix' => false, + 'description' => 'Beautiful & consistent icon toolkit made by the community.', + 'icon_count' => 1411, + ), + 'feather' => array( + 'slug' => 'feather', + 'name' => 'Feather', + 'version' => '4.29.1', + 'license' => 'MIT', + 'url' => 'https://feathericons.com', + 'cdn_base' => 'https://cdn.jsdelivr.net/npm/feather-icons@4.29.1/', + 'styles' => array( + 'icons' => array( + 'path' => 'dist/icons', + 'label' => 'Default', + ), + ), + 'default_style' => 'icons', + 'viewbox' => '0 0 24 24', + 'normalize' => false, + 'color_fix' => false, + 'description' => 'Simply beautiful open source icons.', + 'icon_count' => 287, + ), + 'phosphor' => array( + 'slug' => 'phosphor', + 'name' => 'Phosphor', + 'version' => '2.1.1', + 'license' => 'MIT', + 'url' => 'https://phosphoricons.com', + 'cdn_base' => 'https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2.1.1/', + 'styles' => array( + 'regular' => array( + 'path' => 'assets/regular', + 'label' => 'Regular', + ), + 'bold' => array( + 'path' => 'assets/bold', + 'label' => 'Bold', + ), + 'light' => array( + 'path' => 'assets/light', + 'label' => 'Light', + ), + 'thin' => array( + 'path' => 'assets/thin', + 'label' => 'Thin', + ), + 'fill' => array( + 'path' => 'assets/fill', + 'label' => 'Fill', + ), + 'duotone' => array( + 'path' => 'assets/duotone', + 'label' => 'Duotone', + ), + ), + 'default_style' => 'regular', + 'viewbox' => '0 0 256 256', + 'normalize' => true, // Needs viewBox normalization to 24x24. + 'color_fix' => false, + 'description' => 'A flexible icon family for interfaces, diagrams, presentations, and more.', + 'icon_count' => 1248, + ), + 'material' => array( + 'slug' => 'material', + 'name' => 'Material Design Icons', + 'version' => '0.14.13', + 'license' => 'Apache-2.0', + 'url' => 'https://fonts.google.com/icons', + 'cdn_base' => 'https://cdn.jsdelivr.net/npm/@material-design-icons/svg@0.14.13/', + 'styles' => array( + 'filled' => array( + 'path' => 'filled', + 'label' => 'Filled', + ), + 'outlined' => array( + 'path' => 'outlined', + 'label' => 'Outlined', + ), + 'round' => array( + 'path' => 'round', + 'label' => 'Round', + ), + 'sharp' => array( + 'path' => 'sharp', + 'label' => 'Sharp', + ), + 'two-tone' => array( + 'path' => 'two-tone', + 'label' => 'Two Tone', + ), + ), + 'default_style' => 'filled', + 'viewbox' => '0 0 24 24', + 'normalize' => false, + 'color_fix' => true, // Needs hardcoded color replacement. + 'description' => 'Material Design Icons by Google. Beautiful, delightful, and easy to use.', + 'icon_count' => 2189, + ), + ); + } + + /** + * Get a specific icon set by slug. + * + * @param string $slug Icon set slug. + * @return array|null Icon set configuration or null if not found. + */ + public static function get( $slug ) { + $sets = self::get_all(); + return isset( $sets[ $slug ] ) ? $sets[ $slug ] : null; + } + + /** + * Get all available set slugs. + * + * @return array Array of set slugs. + */ + public static function get_slugs() { + return array_keys( self::get_all() ); + } + + /** + * Validate that a slug is a valid preset. + * + * @param string $slug Slug to validate. + * @return bool True if valid, false otherwise. + */ + public static function is_valid_slug( $slug ) { + return in_array( $slug, self::get_slugs(), true ); + } + + /** + * Validate that a style is valid for a given set. + * + * @param string $slug Icon set slug. + * @param string $style Style slug. + * @return bool True if valid, false otherwise. + */ + public static function is_valid_style( $slug, $style ) { + $set = self::get( $slug ); + if ( ! $set ) { + return false; + } + return isset( $set['styles'][ $style ] ); + } + + /** + * Get the CDN URL for a specific icon. + * + * @param string $slug Icon set slug. + * @param string $style Style slug. + * @param string $name Icon name. + * @return string|null CDN URL or null if invalid. + */ + public static function get_cdn_url( $slug, $style, $name ) { + $set = self::get( $slug ); + if ( ! $set || ! isset( $set['styles'][ $style ] ) ) { + return null; + } + + $style_config = $set['styles'][ $style ]; + return $set['cdn_base'] . $style_config['path'] . '/' . $name . '.svg'; + } + + /** + * Get the local file path for a specific icon. + * + * @param string $slug Icon set slug. + * @param string $style Style slug. + * @param string $name Icon name. + * @return string Local file path. + */ + public static function get_local_path( $slug, $style, $name ) { + return MI_ICONS_DIR . $slug . '/' . $style . '/' . $name . '.svg'; + } + + /** + * Get the local URL for a specific icon. + * + * @param string $slug Icon set slug. + * @param string $style Style slug. + * @param string $name Icon name. + * @return string Local URL. + */ + public static function get_local_url( $slug, $style, $name ) { + return MI_ICONS_URL . $slug . '/' . $style . '/' . $name . '.svg'; + } + + /** + * Get the manifest file path for a set. + * + * @param string $slug Icon set slug. + * @return string Manifest file path. + */ + public static function get_manifest_path( $slug ) { + return MI_PRESETS_DIR . $slug . '.json'; + } + + /** + * Load and parse a manifest file. + * + * @param string $slug Icon set slug. + * @return array|WP_Error Manifest data or error. + */ + public static function load_manifest( $slug ) { + $path = self::get_manifest_path( $slug ); + + if ( ! file_exists( $path ) ) { + return new WP_Error( + 'manifest_not_found', + sprintf( + /* translators: %s: Icon set name */ + __( 'Manifest file not found for %s.', 'maple-icons' ), + $slug + ) + ); + } + + $content = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + if ( false === $content ) { + return new WP_Error( + 'manifest_read_error', + __( 'Could not read manifest file.', 'maple-icons' ) + ); + } + + $manifest = json_decode( $content, true ); + if ( json_last_error() !== JSON_ERROR_NONE ) { + return new WP_Error( + 'manifest_parse_error', + __( 'Could not parse manifest file.', 'maple-icons' ) + ); + } + + return $manifest; + } + + /** + * Get style labels for a set. + * + * @param string $slug Icon set slug. + * @return array Array of style labels. + */ + public static function get_style_labels( $slug ) { + $set = self::get( $slug ); + if ( ! $set ) { + return array(); + } + + $labels = array(); + foreach ( $set['styles'] as $style_slug => $style_config ) { + $labels[ $style_slug ] = $style_config['label']; + } + + return $labels; + } +} diff --git a/native/wordpress/maple-icons-wp/includes/index.php b/native/wordpress/maple-icons-wp/includes/index.php new file mode 100644 index 0000000..398909d --- /dev/null +++ b/native/wordpress/maple-icons-wp/includes/index.php @@ -0,0 +1,5 @@ + true ) + ); + } + + // Create icons directory if it doesn't exist. + if ( ! file_exists( MI_ICONS_DIR ) ) { + wp_mkdir_p( MI_ICONS_DIR ); + } + + // Initialize default settings. + $default_settings = array( + 'active_set' => '', + 'downloaded_sets' => array(), + ); + + if ( false === get_option( 'maple_icons_settings' ) ) { + add_option( 'maple_icons_settings', $default_settings ); + } + + // Flush rewrite rules. + flush_rewrite_rules(); +} +register_activation_hook( MI_PLUGIN_FILE, 'mi_activate' ); + +/** + * Plugin deactivation hook. + */ +function mi_deactivate() { + // Clean up transients. + global $wpdb; + $wpdb->query( + "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mi_%' OR option_name LIKE '_transient_timeout_mi_%'" + ); + + flush_rewrite_rules(); +} +register_deactivation_hook( MI_PLUGIN_FILE, 'mi_deactivate' ); + +/** + * Check WordPress version on admin init (in case WP was downgraded). + */ +function mi_check_wp_version() { + if ( version_compare( get_bloginfo( 'version' ), '6.5', '<' ) ) { + deactivate_plugins( MI_PLUGIN_BASENAME ); + add_action( 'admin_notices', 'mi_wp_version_notice' ); + } +} +add_action( 'admin_init', 'mi_check_wp_version' ); + +/** + * Display admin notice for WordPress version requirement. + */ +function mi_wp_version_notice() { + echo '

'; + esc_html_e( 'Maple Icons has been deactivated. It requires WordPress 6.5 or higher.', 'maple-icons' ); + echo '

'; +} + +/** + * Initialize the plugin. + */ +function mi_init() { + // Load text domain. + load_plugin_textdomain( 'maple-icons', false, dirname( MI_PLUGIN_BASENAME ) . '/languages' ); + + // Register the Gutenberg block. + mi_register_block(); +} +add_action( 'init', 'mi_init' ); + +/** + * Register the Maple Icons Gutenberg block. + */ +function mi_register_block() { + // Check if build directory exists. + $block_path = MI_PLUGIN_DIR . 'build'; + + if ( ! file_exists( $block_path ) ) { + return; + } + + register_block_type( $block_path ); +} + +/** + * Enqueue block editor assets. + */ +function mi_enqueue_block_assets() { + // Only load in block editor. + if ( ! is_admin() ) { + return; + } + + $screen = get_current_screen(); + if ( ! $screen || ! $screen->is_block_editor() ) { + return; + } + + // Localize script data for the block editor. + $active_set = ''; + $settings = get_option( 'maple_icons_settings', array() ); + if ( ! empty( $settings['active_set'] ) ) { + $active_set = $settings['active_set']; + } + + wp_localize_script( + 'maple-icon-editor-script', + 'miBlock', + array( + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'mi_block_nonce' ), + 'activeSet' => $active_set, + 'settingsUrl' => admin_url( 'options-general.php?page=maple-icons' ), + ) + ); +} +add_action( 'enqueue_block_editor_assets', 'mi_enqueue_block_assets' ); + +/** + * Initialize admin functionality. + */ +function mi_admin_init() { + // Initialize admin page. + new MI_Admin_Page(); + + // Initialize AJAX handler. + new MI_Ajax_Handler(); +} +add_action( 'admin_init', 'mi_admin_init' ); + +/** + * Add settings link to plugin action links. + * + * @param array $links Plugin action links. + * @return array Modified action links. + */ +function mi_add_action_links( $links ) { + $settings_link = sprintf( + '%s', + admin_url( 'options-general.php?page=maple-icons' ), + __( 'Settings', 'maple-icons' ) + ); + array_unshift( $links, $settings_link ); + return $links; +} +add_filter( 'plugin_action_links_' . MI_PLUGIN_BASENAME, 'mi_add_action_links' ); + +/** + * Declare WooCommerce HPOS compatibility. + */ +function mi_declare_hpos_compatibility() { + if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( + 'custom_order_tables', + MI_PLUGIN_FILE, + true + ); + } +} +add_action( 'before_woocommerce_init', 'mi_declare_hpos_compatibility' ); diff --git a/native/wordpress/maple-icons-wp/presets/feather.json b/native/wordpress/maple-icons-wp/presets/feather.json new file mode 100644 index 0000000..2e2b7f0 --- /dev/null +++ b/native/wordpress/maple-icons-wp/presets/feather.json @@ -0,0 +1,293 @@ +{ + "slug": "feather", + "name": "Feather", + "version": "4.29.1", + "icons": [ + {"name": "activity", "tags": ["pulse", "health", "action", "motion"], "category": "health", "styles": ["icons"]}, + {"name": "airplay", "tags": ["stream", "cast", "mirroring"], "category": "devices", "styles": ["icons"]}, + {"name": "alert-circle", "tags": ["warning", "alert", "danger"], "category": "alerts", "styles": ["icons"]}, + {"name": "alert-octagon", "tags": ["warning", "alert", "danger", "stop"], "category": "alerts", "styles": ["icons"]}, + {"name": "alert-triangle", "tags": ["warning", "alert", "danger"], "category": "alerts", "styles": ["icons"]}, + {"name": "align-center", "tags": ["text", "alignment", "center"], "category": "text", "styles": ["icons"]}, + {"name": "align-justify", "tags": ["text", "alignment", "justify"], "category": "text", "styles": ["icons"]}, + {"name": "align-left", "tags": ["text", "alignment", "left"], "category": "text", "styles": ["icons"]}, + {"name": "align-right", "tags": ["text", "alignment", "right"], "category": "text", "styles": ["icons"]}, + {"name": "anchor", "tags": ["link", "nav", "navigation"], "category": "navigation", "styles": ["icons"]}, + {"name": "aperture", "tags": ["camera", "photo", "lens"], "category": "photography", "styles": ["icons"]}, + {"name": "archive", "tags": ["box", "storage", "backup"], "category": "files", "styles": ["icons"]}, + {"name": "arrow-down", "tags": ["direction", "down"], "category": "arrows", "styles": ["icons"]}, + {"name": "arrow-down-circle", "tags": ["direction", "down", "circle"], "category": "arrows", "styles": ["icons"]}, + {"name": "arrow-down-left", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["icons"]}, + {"name": "arrow-down-right", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["icons"]}, + {"name": "arrow-left", "tags": ["direction", "back", "previous"], "category": "arrows", "styles": ["icons"]}, + {"name": "arrow-left-circle", "tags": ["direction", "back", "circle"], "category": "arrows", "styles": ["icons"]}, + {"name": "arrow-right", "tags": ["direction", "forward", "next"], "category": "arrows", "styles": ["icons"]}, + {"name": "arrow-right-circle", "tags": ["direction", "forward", "circle"], "category": "arrows", "styles": ["icons"]}, + {"name": "arrow-up", "tags": ["direction", "up"], "category": "arrows", "styles": ["icons"]}, + {"name": "arrow-up-circle", "tags": ["direction", "up", "circle"], "category": "arrows", "styles": ["icons"]}, + {"name": "arrow-up-left", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["icons"]}, + {"name": "arrow-up-right", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["icons"]}, + {"name": "at-sign", "tags": ["email", "mention", "at"], "category": "communication", "styles": ["icons"]}, + {"name": "award", "tags": ["achievement", "badge", "trophy"], "category": "objects", "styles": ["icons"]}, + {"name": "bar-chart", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["icons"]}, + {"name": "bar-chart-2", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["icons"]}, + {"name": "battery", "tags": ["power", "energy", "charge"], "category": "devices", "styles": ["icons"]}, + {"name": "battery-charging", "tags": ["power", "energy", "charge"], "category": "devices", "styles": ["icons"]}, + {"name": "bell", "tags": ["notification", "alert", "alarm"], "category": "notifications", "styles": ["icons"]}, + {"name": "bell-off", "tags": ["notification", "mute", "silent"], "category": "notifications", "styles": ["icons"]}, + {"name": "bluetooth", "tags": ["wireless", "connection"], "category": "connectivity", "styles": ["icons"]}, + {"name": "bold", "tags": ["text", "format", "strong"], "category": "text", "styles": ["icons"]}, + {"name": "book", "tags": ["read", "library", "education"], "category": "objects", "styles": ["icons"]}, + {"name": "book-open", "tags": ["read", "library", "education"], "category": "objects", "styles": ["icons"]}, + {"name": "bookmark", "tags": ["save", "favorite", "tag"], "category": "actions", "styles": ["icons"]}, + {"name": "box", "tags": ["package", "container", "storage"], "category": "objects", "styles": ["icons"]}, + {"name": "briefcase", "tags": ["work", "job", "business"], "category": "objects", "styles": ["icons"]}, + {"name": "calendar", "tags": ["date", "schedule", "event"], "category": "time", "styles": ["icons"]}, + {"name": "camera", "tags": ["photo", "picture", "image"], "category": "devices", "styles": ["icons"]}, + {"name": "camera-off", "tags": ["photo", "disabled", "off"], "category": "devices", "styles": ["icons"]}, + {"name": "cast", "tags": ["stream", "broadcast", "chromecast"], "category": "devices", "styles": ["icons"]}, + {"name": "check", "tags": ["done", "complete", "success"], "category": "actions", "styles": ["icons"]}, + {"name": "check-circle", "tags": ["done", "complete", "success"], "category": "actions", "styles": ["icons"]}, + {"name": "check-square", "tags": ["done", "complete", "checkbox"], "category": "actions", "styles": ["icons"]}, + {"name": "chevron-down", "tags": ["expand", "dropdown", "arrow"], "category": "arrows", "styles": ["icons"]}, + {"name": "chevron-left", "tags": ["back", "previous", "arrow"], "category": "arrows", "styles": ["icons"]}, + {"name": "chevron-right", "tags": ["forward", "next", "arrow"], "category": "arrows", "styles": ["icons"]}, + {"name": "chevron-up", "tags": ["collapse", "up", "arrow"], "category": "arrows", "styles": ["icons"]}, + {"name": "chevrons-down", "tags": ["expand", "more", "double"], "category": "arrows", "styles": ["icons"]}, + {"name": "chevrons-left", "tags": ["back", "previous", "double"], "category": "arrows", "styles": ["icons"]}, + {"name": "chevrons-right", "tags": ["forward", "next", "double"], "category": "arrows", "styles": ["icons"]}, + {"name": "chevrons-up", "tags": ["collapse", "less", "double"], "category": "arrows", "styles": ["icons"]}, + {"name": "chrome", "tags": ["browser", "google", "web"], "category": "brands", "styles": ["icons"]}, + {"name": "circle", "tags": ["shape", "dot", "record"], "category": "shapes", "styles": ["icons"]}, + {"name": "clipboard", "tags": ["copy", "paste", "board"], "category": "actions", "styles": ["icons"]}, + {"name": "clock", "tags": ["time", "watch", "schedule"], "category": "time", "styles": ["icons"]}, + {"name": "cloud", "tags": ["weather", "storage", "sky"], "category": "weather", "styles": ["icons"]}, + {"name": "cloud-drizzle", "tags": ["weather", "rain", "drizzle"], "category": "weather", "styles": ["icons"]}, + {"name": "cloud-lightning", "tags": ["weather", "storm", "thunder"], "category": "weather", "styles": ["icons"]}, + {"name": "cloud-off", "tags": ["weather", "offline", "disconnect"], "category": "weather", "styles": ["icons"]}, + {"name": "cloud-rain", "tags": ["weather", "rain", "precipitation"], "category": "weather", "styles": ["icons"]}, + {"name": "cloud-snow", "tags": ["weather", "snow", "winter"], "category": "weather", "styles": ["icons"]}, + {"name": "code", "tags": ["programming", "development", "html"], "category": "development", "styles": ["icons"]}, + {"name": "codepen", "tags": ["brand", "code", "development"], "category": "brands", "styles": ["icons"]}, + {"name": "codesandbox", "tags": ["brand", "code", "development"], "category": "brands", "styles": ["icons"]}, + {"name": "coffee", "tags": ["drink", "cup", "cafe"], "category": "objects", "styles": ["icons"]}, + {"name": "columns", "tags": ["layout", "grid", "table"], "category": "layout", "styles": ["icons"]}, + {"name": "command", "tags": ["keyboard", "mac", "key"], "category": "devices", "styles": ["icons"]}, + {"name": "compass", "tags": ["navigation", "direction", "explore"], "category": "navigation", "styles": ["icons"]}, + {"name": "copy", "tags": ["duplicate", "clone", "clipboard"], "category": "actions", "styles": ["icons"]}, + {"name": "corner-down-left", "tags": ["arrow", "return", "enter"], "category": "arrows", "styles": ["icons"]}, + {"name": "corner-down-right", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]}, + {"name": "corner-left-down", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]}, + {"name": "corner-left-up", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]}, + {"name": "corner-right-down", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]}, + {"name": "corner-right-up", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]}, + {"name": "corner-up-left", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]}, + {"name": "corner-up-right", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]}, + {"name": "cpu", "tags": ["processor", "chip", "computer"], "category": "devices", "styles": ["icons"]}, + {"name": "credit-card", "tags": ["payment", "card", "money"], "category": "commerce", "styles": ["icons"]}, + {"name": "crop", "tags": ["image", "edit", "resize"], "category": "editing", "styles": ["icons"]}, + {"name": "crosshair", "tags": ["target", "focus", "aim"], "category": "ui", "styles": ["icons"]}, + {"name": "database", "tags": ["storage", "data", "server"], "category": "development", "styles": ["icons"]}, + {"name": "delete", "tags": ["remove", "backspace", "erase"], "category": "actions", "styles": ["icons"]}, + {"name": "disc", "tags": ["cd", "music", "record"], "category": "media", "styles": ["icons"]}, + {"name": "divide", "tags": ["math", "division", "split"], "category": "math", "styles": ["icons"]}, + {"name": "divide-circle", "tags": ["math", "division", "split"], "category": "math", "styles": ["icons"]}, + {"name": "divide-square", "tags": ["math", "division", "split"], "category": "math", "styles": ["icons"]}, + {"name": "dollar-sign", "tags": ["money", "currency", "payment"], "category": "commerce", "styles": ["icons"]}, + {"name": "download", "tags": ["save", "export", "download"], "category": "actions", "styles": ["icons"]}, + {"name": "download-cloud", "tags": ["save", "cloud", "download"], "category": "actions", "styles": ["icons"]}, + {"name": "dribbble", "tags": ["brand", "design", "social"], "category": "brands", "styles": ["icons"]}, + {"name": "droplet", "tags": ["water", "liquid", "drop"], "category": "weather", "styles": ["icons"]}, + {"name": "edit", "tags": ["pencil", "write", "modify"], "category": "actions", "styles": ["icons"]}, + {"name": "edit-2", "tags": ["pencil", "write", "modify"], "category": "actions", "styles": ["icons"]}, + {"name": "edit-3", "tags": ["pencil", "write", "modify"], "category": "actions", "styles": ["icons"]}, + {"name": "external-link", "tags": ["link", "open", "new window"], "category": "actions", "styles": ["icons"]}, + {"name": "eye", "tags": ["view", "visible", "show"], "category": "actions", "styles": ["icons"]}, + {"name": "eye-off", "tags": ["hide", "invisible", "hidden"], "category": "actions", "styles": ["icons"]}, + {"name": "facebook", "tags": ["brand", "social", "network"], "category": "brands", "styles": ["icons"]}, + {"name": "fast-forward", "tags": ["media", "skip", "forward"], "category": "media", "styles": ["icons"]}, + {"name": "feather", "tags": ["brand", "logo", "icon"], "category": "brands", "styles": ["icons"]}, + {"name": "figma", "tags": ["brand", "design", "tool"], "category": "brands", "styles": ["icons"]}, + {"name": "file", "tags": ["document", "page", "paper"], "category": "files", "styles": ["icons"]}, + {"name": "file-minus", "tags": ["document", "remove", "delete"], "category": "files", "styles": ["icons"]}, + {"name": "file-plus", "tags": ["document", "add", "new"], "category": "files", "styles": ["icons"]}, + {"name": "file-text", "tags": ["document", "text", "content"], "category": "files", "styles": ["icons"]}, + {"name": "film", "tags": ["video", "movie", "cinema"], "category": "media", "styles": ["icons"]}, + {"name": "filter", "tags": ["funnel", "sort", "filter"], "category": "actions", "styles": ["icons"]}, + {"name": "flag", "tags": ["report", "mark", "country"], "category": "objects", "styles": ["icons"]}, + {"name": "folder", "tags": ["directory", "files", "organize"], "category": "files", "styles": ["icons"]}, + {"name": "folder-minus", "tags": ["directory", "remove", "delete"], "category": "files", "styles": ["icons"]}, + {"name": "folder-plus", "tags": ["directory", "add", "new"], "category": "files", "styles": ["icons"]}, + {"name": "framer", "tags": ["brand", "design", "tool"], "category": "brands", "styles": ["icons"]}, + {"name": "frown", "tags": ["sad", "unhappy", "emoji"], "category": "emoji", "styles": ["icons"]}, + {"name": "gift", "tags": ["present", "reward", "surprise"], "category": "objects", "styles": ["icons"]}, + {"name": "git-branch", "tags": ["version", "control", "code"], "category": "development", "styles": ["icons"]}, + {"name": "git-commit", "tags": ["version", "control", "code"], "category": "development", "styles": ["icons"]}, + {"name": "git-merge", "tags": ["version", "control", "code"], "category": "development", "styles": ["icons"]}, + {"name": "git-pull-request", "tags": ["version", "control", "code"], "category": "development", "styles": ["icons"]}, + {"name": "github", "tags": ["brand", "code", "repository"], "category": "brands", "styles": ["icons"]}, + {"name": "gitlab", "tags": ["brand", "code", "repository"], "category": "brands", "styles": ["icons"]}, + {"name": "globe", "tags": ["world", "earth", "web"], "category": "objects", "styles": ["icons"]}, + {"name": "grid", "tags": ["layout", "squares", "dashboard"], "category": "layout", "styles": ["icons"]}, + {"name": "hard-drive", "tags": ["storage", "disk", "data"], "category": "devices", "styles": ["icons"]}, + {"name": "hash", "tags": ["tag", "hashtag", "number"], "category": "communication", "styles": ["icons"]}, + {"name": "headphones", "tags": ["audio", "music", "listen"], "category": "devices", "styles": ["icons"]}, + {"name": "heart", "tags": ["love", "favorite", "like"], "category": "objects", "styles": ["icons"]}, + {"name": "help-circle", "tags": ["question", "help", "support"], "category": "actions", "styles": ["icons"]}, + {"name": "hexagon", "tags": ["shape", "polygon", "6"], "category": "shapes", "styles": ["icons"]}, + {"name": "home", "tags": ["house", "main", "dashboard"], "category": "navigation", "styles": ["icons"]}, + {"name": "image", "tags": ["picture", "photo", "gallery"], "category": "media", "styles": ["icons"]}, + {"name": "inbox", "tags": ["email", "messages", "mail"], "category": "communication", "styles": ["icons"]}, + {"name": "info", "tags": ["information", "help", "about"], "category": "actions", "styles": ["icons"]}, + {"name": "instagram", "tags": ["brand", "social", "photo"], "category": "brands", "styles": ["icons"]}, + {"name": "italic", "tags": ["text", "format", "style"], "category": "text", "styles": ["icons"]}, + {"name": "key", "tags": ["password", "security", "lock"], "category": "security", "styles": ["icons"]}, + {"name": "layers", "tags": ["stack", "levels", "depth"], "category": "design", "styles": ["icons"]}, + {"name": "layout", "tags": ["grid", "template", "design"], "category": "layout", "styles": ["icons"]}, + {"name": "life-buoy", "tags": ["help", "support", "rescue"], "category": "objects", "styles": ["icons"]}, + {"name": "link", "tags": ["url", "chain", "connect"], "category": "actions", "styles": ["icons"]}, + {"name": "link-2", "tags": ["url", "chain", "connect"], "category": "actions", "styles": ["icons"]}, + {"name": "linkedin", "tags": ["brand", "social", "professional"], "category": "brands", "styles": ["icons"]}, + {"name": "list", "tags": ["menu", "items", "bullet"], "category": "text", "styles": ["icons"]}, + {"name": "loader", "tags": ["loading", "spinner", "wait"], "category": "ui", "styles": ["icons"]}, + {"name": "lock", "tags": ["security", "private", "password"], "category": "security", "styles": ["icons"]}, + {"name": "log-in", "tags": ["login", "signin", "enter"], "category": "actions", "styles": ["icons"]}, + {"name": "log-out", "tags": ["logout", "signout", "exit"], "category": "actions", "styles": ["icons"]}, + {"name": "mail", "tags": ["email", "message", "envelope"], "category": "communication", "styles": ["icons"]}, + {"name": "map", "tags": ["location", "navigation", "directions"], "category": "maps", "styles": ["icons"]}, + {"name": "map-pin", "tags": ["location", "marker", "place"], "category": "maps", "styles": ["icons"]}, + {"name": "maximize", "tags": ["fullscreen", "expand", "resize"], "category": "actions", "styles": ["icons"]}, + {"name": "maximize-2", "tags": ["fullscreen", "expand", "resize"], "category": "actions", "styles": ["icons"]}, + {"name": "meh", "tags": ["neutral", "emoji", "face"], "category": "emoji", "styles": ["icons"]}, + {"name": "menu", "tags": ["hamburger", "navigation", "bars"], "category": "navigation", "styles": ["icons"]}, + {"name": "message-circle", "tags": ["chat", "comment", "conversation"], "category": "communication", "styles": ["icons"]}, + {"name": "message-square", "tags": ["chat", "comment", "conversation"], "category": "communication", "styles": ["icons"]}, + {"name": "mic", "tags": ["microphone", "audio", "record"], "category": "media", "styles": ["icons"]}, + {"name": "mic-off", "tags": ["microphone", "mute", "silent"], "category": "media", "styles": ["icons"]}, + {"name": "minimize", "tags": ["collapse", "shrink", "resize"], "category": "actions", "styles": ["icons"]}, + {"name": "minimize-2", "tags": ["collapse", "shrink", "resize"], "category": "actions", "styles": ["icons"]}, + {"name": "minus", "tags": ["subtract", "remove", "delete"], "category": "math", "styles": ["icons"]}, + {"name": "minus-circle", "tags": ["subtract", "remove", "delete"], "category": "math", "styles": ["icons"]}, + {"name": "minus-square", "tags": ["subtract", "remove", "delete"], "category": "math", "styles": ["icons"]}, + {"name": "monitor", "tags": ["screen", "display", "desktop"], "category": "devices", "styles": ["icons"]}, + {"name": "moon", "tags": ["dark", "night", "theme"], "category": "weather", "styles": ["icons"]}, + {"name": "more-horizontal", "tags": ["menu", "options", "dots"], "category": "ui", "styles": ["icons"]}, + {"name": "more-vertical", "tags": ["menu", "options", "dots"], "category": "ui", "styles": ["icons"]}, + {"name": "mouse-pointer", "tags": ["cursor", "click", "select"], "category": "ui", "styles": ["icons"]}, + {"name": "move", "tags": ["drag", "reorder", "arrows"], "category": "actions", "styles": ["icons"]}, + {"name": "music", "tags": ["audio", "sound", "note"], "category": "media", "styles": ["icons"]}, + {"name": "navigation", "tags": ["direction", "arrow", "location"], "category": "navigation", "styles": ["icons"]}, + {"name": "navigation-2", "tags": ["direction", "arrow", "location"], "category": "navigation", "styles": ["icons"]}, + {"name": "octagon", "tags": ["shape", "stop", "polygon"], "category": "shapes", "styles": ["icons"]}, + {"name": "package", "tags": ["box", "delivery", "shipping"], "category": "objects", "styles": ["icons"]}, + {"name": "paperclip", "tags": ["attachment", "file", "clip"], "category": "actions", "styles": ["icons"]}, + {"name": "pause", "tags": ["media", "stop", "wait"], "category": "media", "styles": ["icons"]}, + {"name": "pause-circle", "tags": ["media", "stop", "wait"], "category": "media", "styles": ["icons"]}, + {"name": "pen-tool", "tags": ["design", "draw", "vector"], "category": "design", "styles": ["icons"]}, + {"name": "percent", "tags": ["discount", "math", "percentage"], "category": "math", "styles": ["icons"]}, + {"name": "phone", "tags": ["call", "contact", "mobile"], "category": "communication", "styles": ["icons"]}, + {"name": "phone-call", "tags": ["call", "ringing", "incoming"], "category": "communication", "styles": ["icons"]}, + {"name": "phone-forwarded", "tags": ["call", "forward", "redirect"], "category": "communication", "styles": ["icons"]}, + {"name": "phone-incoming", "tags": ["call", "receive", "answer"], "category": "communication", "styles": ["icons"]}, + {"name": "phone-missed", "tags": ["call", "missed", "unanswered"], "category": "communication", "styles": ["icons"]}, + {"name": "phone-off", "tags": ["call", "hang up", "end"], "category": "communication", "styles": ["icons"]}, + {"name": "phone-outgoing", "tags": ["call", "dial", "outbound"], "category": "communication", "styles": ["icons"]}, + {"name": "pie-chart", "tags": ["analytics", "statistics", "data"], "category": "charts", "styles": ["icons"]}, + {"name": "play", "tags": ["media", "start", "video"], "category": "media", "styles": ["icons"]}, + {"name": "play-circle", "tags": ["media", "start", "video"], "category": "media", "styles": ["icons"]}, + {"name": "plus", "tags": ["add", "create", "new"], "category": "math", "styles": ["icons"]}, + {"name": "plus-circle", "tags": ["add", "create", "new"], "category": "math", "styles": ["icons"]}, + {"name": "plus-square", "tags": ["add", "create", "new"], "category": "math", "styles": ["icons"]}, + {"name": "pocket", "tags": ["save", "bookmark", "read later"], "category": "brands", "styles": ["icons"]}, + {"name": "power", "tags": ["on", "off", "shutdown"], "category": "devices", "styles": ["icons"]}, + {"name": "printer", "tags": ["print", "document", "paper"], "category": "devices", "styles": ["icons"]}, + {"name": "radio", "tags": ["audio", "broadcast", "music"], "category": "media", "styles": ["icons"]}, + {"name": "refresh-ccw", "tags": ["reload", "sync", "update"], "category": "actions", "styles": ["icons"]}, + {"name": "refresh-cw", "tags": ["reload", "sync", "update"], "category": "actions", "styles": ["icons"]}, + {"name": "repeat", "tags": ["loop", "replay", "refresh"], "category": "media", "styles": ["icons"]}, + {"name": "rewind", "tags": ["media", "back", "previous"], "category": "media", "styles": ["icons"]}, + {"name": "rotate-ccw", "tags": ["undo", "rotate", "turn"], "category": "actions", "styles": ["icons"]}, + {"name": "rotate-cw", "tags": ["redo", "rotate", "turn"], "category": "actions", "styles": ["icons"]}, + {"name": "rss", "tags": ["feed", "subscribe", "blog"], "category": "communication", "styles": ["icons"]}, + {"name": "save", "tags": ["disk", "floppy", "store"], "category": "actions", "styles": ["icons"]}, + {"name": "scissors", "tags": ["cut", "trim", "edit"], "category": "actions", "styles": ["icons"]}, + {"name": "search", "tags": ["find", "magnify", "look"], "category": "actions", "styles": ["icons"]}, + {"name": "send", "tags": ["message", "email", "submit"], "category": "communication", "styles": ["icons"]}, + {"name": "server", "tags": ["hosting", "database", "backend"], "category": "devices", "styles": ["icons"]}, + {"name": "settings", "tags": ["gear", "cog", "preferences"], "category": "ui", "styles": ["icons"]}, + {"name": "share", "tags": ["social", "send", "forward"], "category": "actions", "styles": ["icons"]}, + {"name": "share-2", "tags": ["social", "send", "network"], "category": "actions", "styles": ["icons"]}, + {"name": "shield", "tags": ["security", "protection", "safe"], "category": "security", "styles": ["icons"]}, + {"name": "shield-off", "tags": ["security", "unprotected", "unsafe"], "category": "security", "styles": ["icons"]}, + {"name": "shopping-bag", "tags": ["shop", "purchase", "buy"], "category": "commerce", "styles": ["icons"]}, + {"name": "shopping-cart", "tags": ["shop", "cart", "ecommerce"], "category": "commerce", "styles": ["icons"]}, + {"name": "shuffle", "tags": ["random", "mix", "music"], "category": "media", "styles": ["icons"]}, + {"name": "sidebar", "tags": ["layout", "panel", "navigation"], "category": "layout", "styles": ["icons"]}, + {"name": "skip-back", "tags": ["media", "previous", "rewind"], "category": "media", "styles": ["icons"]}, + {"name": "skip-forward", "tags": ["media", "next", "forward"], "category": "media", "styles": ["icons"]}, + {"name": "slack", "tags": ["brand", "chat", "communication"], "category": "brands", "styles": ["icons"]}, + {"name": "slash", "tags": ["ban", "cancel", "disabled"], "category": "ui", "styles": ["icons"]}, + {"name": "sliders", "tags": ["settings", "controls", "adjust"], "category": "ui", "styles": ["icons"]}, + {"name": "smartphone", "tags": ["phone", "mobile", "device"], "category": "devices", "styles": ["icons"]}, + {"name": "smile", "tags": ["happy", "emoji", "face"], "category": "emoji", "styles": ["icons"]}, + {"name": "speaker", "tags": ["audio", "sound", "volume"], "category": "media", "styles": ["icons"]}, + {"name": "square", "tags": ["shape", "box", "rectangle"], "category": "shapes", "styles": ["icons"]}, + {"name": "star", "tags": ["favorite", "rating", "bookmark"], "category": "objects", "styles": ["icons"]}, + {"name": "stop-circle", "tags": ["media", "stop", "end"], "category": "media", "styles": ["icons"]}, + {"name": "sun", "tags": ["light", "day", "brightness"], "category": "weather", "styles": ["icons"]}, + {"name": "sunrise", "tags": ["morning", "dawn", "sun"], "category": "weather", "styles": ["icons"]}, + {"name": "sunset", "tags": ["evening", "dusk", "sun"], "category": "weather", "styles": ["icons"]}, + {"name": "tablet", "tags": ["device", "ipad", "screen"], "category": "devices", "styles": ["icons"]}, + {"name": "tag", "tags": ["label", "category", "price"], "category": "commerce", "styles": ["icons"]}, + {"name": "target", "tags": ["goal", "aim", "focus"], "category": "ui", "styles": ["icons"]}, + {"name": "terminal", "tags": ["console", "command", "code"], "category": "development", "styles": ["icons"]}, + {"name": "thermometer", "tags": ["temperature", "weather", "health"], "category": "weather", "styles": ["icons"]}, + {"name": "thumbs-down", "tags": ["dislike", "bad", "negative"], "category": "actions", "styles": ["icons"]}, + {"name": "thumbs-up", "tags": ["like", "good", "positive"], "category": "actions", "styles": ["icons"]}, + {"name": "toggle-left", "tags": ["switch", "off", "disable"], "category": "ui", "styles": ["icons"]}, + {"name": "toggle-right", "tags": ["switch", "on", "enable"], "category": "ui", "styles": ["icons"]}, + {"name": "tool", "tags": ["wrench", "settings", "repair"], "category": "objects", "styles": ["icons"]}, + {"name": "trash", "tags": ["delete", "remove", "bin"], "category": "actions", "styles": ["icons"]}, + {"name": "trash-2", "tags": ["delete", "remove", "bin"], "category": "actions", "styles": ["icons"]}, + {"name": "trello", "tags": ["brand", "project", "kanban"], "category": "brands", "styles": ["icons"]}, + {"name": "trending-down", "tags": ["chart", "decrease", "analytics"], "category": "charts", "styles": ["icons"]}, + {"name": "trending-up", "tags": ["chart", "increase", "analytics"], "category": "charts", "styles": ["icons"]}, + {"name": "triangle", "tags": ["shape", "polygon", "warning"], "category": "shapes", "styles": ["icons"]}, + {"name": "truck", "tags": ["delivery", "shipping", "transport"], "category": "objects", "styles": ["icons"]}, + {"name": "tv", "tags": ["television", "screen", "display"], "category": "devices", "styles": ["icons"]}, + {"name": "twitch", "tags": ["brand", "streaming", "gaming"], "category": "brands", "styles": ["icons"]}, + {"name": "twitter", "tags": ["brand", "social", "network"], "category": "brands", "styles": ["icons"]}, + {"name": "type", "tags": ["text", "font", "typography"], "category": "text", "styles": ["icons"]}, + {"name": "umbrella", "tags": ["weather", "rain", "protection"], "category": "weather", "styles": ["icons"]}, + {"name": "underline", "tags": ["text", "format", "style"], "category": "text", "styles": ["icons"]}, + {"name": "unlock", "tags": ["security", "open", "access"], "category": "security", "styles": ["icons"]}, + {"name": "upload", "tags": ["export", "send", "share"], "category": "actions", "styles": ["icons"]}, + {"name": "upload-cloud", "tags": ["export", "cloud", "share"], "category": "actions", "styles": ["icons"]}, + {"name": "user", "tags": ["person", "account", "profile"], "category": "users", "styles": ["icons"]}, + {"name": "user-check", "tags": ["person", "verified", "approved"], "category": "users", "styles": ["icons"]}, + {"name": "user-minus", "tags": ["person", "remove", "unfriend"], "category": "users", "styles": ["icons"]}, + {"name": "user-plus", "tags": ["person", "add", "invite"], "category": "users", "styles": ["icons"]}, + {"name": "user-x", "tags": ["person", "delete", "remove"], "category": "users", "styles": ["icons"]}, + {"name": "users", "tags": ["people", "team", "group"], "category": "users", "styles": ["icons"]}, + {"name": "video", "tags": ["camera", "film", "record"], "category": "media", "styles": ["icons"]}, + {"name": "video-off", "tags": ["camera", "disabled", "mute"], "category": "media", "styles": ["icons"]}, + {"name": "voicemail", "tags": ["message", "audio", "phone"], "category": "communication", "styles": ["icons"]}, + {"name": "volume", "tags": ["sound", "audio", "speaker"], "category": "media", "styles": ["icons"]}, + {"name": "volume-1", "tags": ["sound", "audio", "low"], "category": "media", "styles": ["icons"]}, + {"name": "volume-2", "tags": ["sound", "audio", "high"], "category": "media", "styles": ["icons"]}, + {"name": "volume-x", "tags": ["mute", "silent", "no sound"], "category": "media", "styles": ["icons"]}, + {"name": "watch", "tags": ["time", "wearable", "clock"], "category": "devices", "styles": ["icons"]}, + {"name": "wifi", "tags": ["wireless", "internet", "connection"], "category": "connectivity", "styles": ["icons"]}, + {"name": "wifi-off", "tags": ["wireless", "offline", "disconnect"], "category": "connectivity", "styles": ["icons"]}, + {"name": "wind", "tags": ["weather", "air", "breeze"], "category": "weather", "styles": ["icons"]}, + {"name": "x", "tags": ["close", "cancel", "remove"], "category": "actions", "styles": ["icons"]}, + {"name": "x-circle", "tags": ["close", "cancel", "error"], "category": "actions", "styles": ["icons"]}, + {"name": "x-octagon", "tags": ["close", "stop", "error"], "category": "actions", "styles": ["icons"]}, + {"name": "x-square", "tags": ["close", "cancel", "remove"], "category": "actions", "styles": ["icons"]}, + {"name": "youtube", "tags": ["brand", "video", "streaming"], "category": "brands", "styles": ["icons"]}, + {"name": "zap", "tags": ["lightning", "power", "energy"], "category": "objects", "styles": ["icons"]}, + {"name": "zap-off", "tags": ["lightning", "disabled", "off"], "category": "objects", "styles": ["icons"]}, + {"name": "zoom-in", "tags": ["magnify", "enlarge", "plus"], "category": "actions", "styles": ["icons"]}, + {"name": "zoom-out", "tags": ["magnify", "shrink", "minus"], "category": "actions", "styles": ["icons"]} + ] +} diff --git a/native/wordpress/maple-icons-wp/presets/heroicons.json b/native/wordpress/maple-icons-wp/presets/heroicons.json new file mode 100644 index 0000000..8f08c1e --- /dev/null +++ b/native/wordpress/maple-icons-wp/presets/heroicons.json @@ -0,0 +1,295 @@ +{ + "slug": "heroicons", + "name": "Heroicons", + "version": "2.1.1", + "icons": [ + {"name": "academic-cap", "tags": ["education", "graduation", "school", "hat", "learning"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "adjustments-horizontal", "tags": ["settings", "controls", "sliders", "options", "configuration"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "adjustments-vertical", "tags": ["settings", "controls", "sliders", "options", "configuration"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "archive-box", "tags": ["storage", "box", "container", "archive"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "archive-box-arrow-down", "tags": ["download", "storage", "archive", "save"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "archive-box-x-mark", "tags": ["delete", "remove", "archive", "cancel"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-down", "tags": ["direction", "navigation", "down", "move"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-down-circle", "tags": ["direction", "navigation", "down", "circle"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-down-left", "tags": ["direction", "navigation", "diagonal"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-down-on-square", "tags": ["download", "save", "import"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-down-on-square-stack", "tags": ["download", "save", "multiple"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-down-right", "tags": ["direction", "navigation", "diagonal"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-down-tray", "tags": ["download", "save", "import"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-left", "tags": ["direction", "navigation", "back", "previous"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-left-circle", "tags": ["direction", "navigation", "back", "circle"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-left-end-on-rectangle", "tags": ["logout", "exit", "leave"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-left-start-on-rectangle", "tags": ["login", "enter", "signin"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-long-down", "tags": ["direction", "navigation", "long"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-long-left", "tags": ["direction", "navigation", "long", "back"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-long-right", "tags": ["direction", "navigation", "long", "forward"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-long-up", "tags": ["direction", "navigation", "long"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-path", "tags": ["refresh", "reload", "sync", "repeat"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-path-rounded-square", "tags": ["refresh", "reload", "sync"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-right", "tags": ["direction", "navigation", "forward", "next"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-right-circle", "tags": ["direction", "navigation", "forward", "circle"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-right-end-on-rectangle", "tags": ["login", "enter", "signin"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-right-start-on-rectangle", "tags": ["logout", "exit", "leave"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-top-right-on-square", "tags": ["external", "link", "open", "new window"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-trending-down", "tags": ["chart", "decrease", "analytics", "down"], "category": "charts", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-trending-up", "tags": ["chart", "increase", "analytics", "up"], "category": "charts", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-up", "tags": ["direction", "navigation", "up", "move"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-up-circle", "tags": ["direction", "navigation", "up", "circle"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-up-left", "tags": ["direction", "navigation", "diagonal"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-up-on-square", "tags": ["upload", "share", "export"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-up-on-square-stack", "tags": ["upload", "share", "multiple"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-up-right", "tags": ["direction", "navigation", "diagonal"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-up-tray", "tags": ["upload", "export", "share"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-uturn-down", "tags": ["undo", "return", "back"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-uturn-left", "tags": ["undo", "return", "back"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-uturn-right", "tags": ["redo", "forward"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrow-uturn-up", "tags": ["return", "back"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrows-pointing-in", "tags": ["minimize", "collapse", "shrink"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrows-pointing-out", "tags": ["maximize", "expand", "fullscreen"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "arrows-right-left", "tags": ["swap", "exchange", "transfer"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "arrows-up-down", "tags": ["sort", "reorder", "move"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "at-symbol", "tags": ["email", "at", "mention", "contact"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "backspace", "tags": ["delete", "remove", "erase", "keyboard"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "backward", "tags": ["media", "rewind", "previous"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "banknotes", "tags": ["money", "cash", "payment", "currency"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "bars-2", "tags": ["menu", "hamburger", "navigation"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "bars-3", "tags": ["menu", "hamburger", "navigation"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "bars-3-bottom-left", "tags": ["menu", "text", "align left"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "bars-3-bottom-right", "tags": ["menu", "text", "align right"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "bars-3-center-left", "tags": ["menu", "text", "align"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "bars-4", "tags": ["menu", "list", "navigation"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "bars-arrow-down", "tags": ["sort", "descending", "order"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "bars-arrow-up", "tags": ["sort", "ascending", "order"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "battery-0", "tags": ["power", "energy", "empty"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "battery-50", "tags": ["power", "energy", "half"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "battery-100", "tags": ["power", "energy", "full"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "beaker", "tags": ["science", "lab", "chemistry", "experiment"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "bell", "tags": ["notification", "alert", "alarm", "ring"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "bell-alert", "tags": ["notification", "alert", "alarm", "warning"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "bell-slash", "tags": ["notification", "mute", "silent", "off"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "bell-snooze", "tags": ["notification", "snooze", "delay", "later"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "bolt", "tags": ["lightning", "power", "energy", "electric"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "bolt-slash", "tags": ["lightning", "power", "off", "disabled"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "book-open", "tags": ["read", "education", "learning", "library"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "bookmark", "tags": ["save", "favorite", "tag", "mark"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "bookmark-slash", "tags": ["unsave", "remove", "unbookmark"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "bookmark-square", "tags": ["save", "favorite", "square"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "briefcase", "tags": ["work", "job", "business", "career"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "bug-ant", "tags": ["bug", "debug", "error", "insect"], "category": "development", "styles": ["outline", "solid", "mini"]}, + {"name": "building-library", "tags": ["library", "institution", "government"], "category": "buildings", "styles": ["outline", "solid", "mini"]}, + {"name": "building-office", "tags": ["office", "work", "business", "company"], "category": "buildings", "styles": ["outline", "solid", "mini"]}, + {"name": "building-office-2", "tags": ["office", "skyscraper", "business"], "category": "buildings", "styles": ["outline", "solid", "mini"]}, + {"name": "building-storefront", "tags": ["shop", "store", "retail", "commerce"], "category": "buildings", "styles": ["outline", "solid", "mini"]}, + {"name": "cake", "tags": ["birthday", "celebration", "party", "dessert"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "calculator", "tags": ["math", "calculate", "numbers", "finance"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "calendar", "tags": ["date", "schedule", "event", "time"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "calendar-days", "tags": ["date", "schedule", "month", "planner"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "camera", "tags": ["photo", "picture", "image", "photography"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "chart-bar", "tags": ["analytics", "statistics", "graph", "data"], "category": "charts", "styles": ["outline", "solid", "mini"]}, + {"name": "chart-bar-square", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["outline", "solid", "mini"]}, + {"name": "chart-pie", "tags": ["analytics", "statistics", "pie chart", "data"], "category": "charts", "styles": ["outline", "solid", "mini"]}, + {"name": "chat-bubble-bottom-center", "tags": ["message", "chat", "comment", "conversation"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "chat-bubble-bottom-center-text", "tags": ["message", "chat", "text", "comment"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "chat-bubble-left", "tags": ["message", "chat", "comment"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "chat-bubble-left-ellipsis", "tags": ["message", "typing", "chat"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "chat-bubble-left-right", "tags": ["conversation", "chat", "discuss"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "chat-bubble-oval-left", "tags": ["message", "chat", "bubble"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "chat-bubble-oval-left-ellipsis", "tags": ["message", "typing", "chat"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "check", "tags": ["done", "complete", "success", "tick"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "check-badge", "tags": ["verified", "approved", "certified"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "check-circle", "tags": ["done", "complete", "success", "approved"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "chevron-double-down", "tags": ["expand", "more", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "chevron-double-left", "tags": ["previous", "back", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "chevron-double-right", "tags": ["next", "forward", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "chevron-double-up", "tags": ["collapse", "less", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "chevron-down", "tags": ["expand", "dropdown", "more"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "chevron-left", "tags": ["previous", "back", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "chevron-right", "tags": ["next", "forward", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "chevron-up", "tags": ["collapse", "less", "close"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "chevron-up-down", "tags": ["sort", "select", "dropdown"], "category": "arrows", "styles": ["outline", "solid", "mini"]}, + {"name": "circle-stack", "tags": ["database", "storage", "data", "layers"], "category": "development", "styles": ["outline", "solid", "mini"]}, + {"name": "clipboard", "tags": ["copy", "paste", "clipboard"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "clipboard-document", "tags": ["copy", "document", "paste"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "clipboard-document-check", "tags": ["copy", "done", "verified"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "clipboard-document-list", "tags": ["list", "tasks", "checklist"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "clock", "tags": ["time", "schedule", "watch", "hour"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "cloud", "tags": ["weather", "storage", "sky", "upload"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "cloud-arrow-down", "tags": ["download", "cloud", "sync"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "cloud-arrow-up", "tags": ["upload", "cloud", "sync"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "code-bracket", "tags": ["code", "development", "programming"], "category": "development", "styles": ["outline", "solid", "mini"]}, + {"name": "code-bracket-square", "tags": ["code", "development", "programming"], "category": "development", "styles": ["outline", "solid", "mini"]}, + {"name": "cog", "tags": ["settings", "gear", "configuration", "options"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "cog-6-tooth", "tags": ["settings", "gear", "configuration"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "cog-8-tooth", "tags": ["settings", "gear", "configuration"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "command-line", "tags": ["terminal", "console", "cli", "code"], "category": "development", "styles": ["outline", "solid", "mini"]}, + {"name": "computer-desktop", "tags": ["desktop", "monitor", "screen", "pc"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "cpu-chip", "tags": ["processor", "chip", "hardware", "computer"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "credit-card", "tags": ["payment", "card", "money", "finance"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "cube", "tags": ["3d", "box", "shape", "object"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "cube-transparent", "tags": ["3d", "box", "transparent"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "currency-bangladeshi", "tags": ["money", "currency", "taka"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "currency-dollar", "tags": ["money", "currency", "usd", "dollar"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "currency-euro", "tags": ["money", "currency", "eur", "euro"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "currency-pound", "tags": ["money", "currency", "gbp", "pound"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "currency-rupee", "tags": ["money", "currency", "inr", "rupee"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "currency-yen", "tags": ["money", "currency", "jpy", "yen"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "cursor-arrow-rays", "tags": ["cursor", "click", "pointer"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "cursor-arrow-ripple", "tags": ["cursor", "click", "pointer"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "device-phone-mobile", "tags": ["phone", "mobile", "smartphone"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "device-tablet", "tags": ["tablet", "ipad", "device"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "document", "tags": ["file", "document", "page", "paper"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "document-arrow-down", "tags": ["download", "file", "document"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "document-arrow-up", "tags": ["upload", "file", "document"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "document-chart-bar", "tags": ["report", "analytics", "document"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "document-check", "tags": ["verified", "approved", "document"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "document-duplicate", "tags": ["copy", "duplicate", "clone"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "document-magnifying-glass", "tags": ["search", "find", "document"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "document-minus", "tags": ["remove", "delete", "document"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "document-plus", "tags": ["add", "new", "create", "document"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "document-text", "tags": ["file", "text", "document", "content"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "ellipsis-horizontal", "tags": ["more", "menu", "options", "dots"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "ellipsis-horizontal-circle", "tags": ["more", "menu", "options"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "ellipsis-vertical", "tags": ["more", "menu", "options", "dots"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "envelope", "tags": ["email", "mail", "message", "letter"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "envelope-open", "tags": ["email", "mail", "read", "open"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "exclamation-circle", "tags": ["warning", "alert", "error", "danger"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "exclamation-triangle", "tags": ["warning", "alert", "caution", "danger"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "eye", "tags": ["view", "visible", "show", "watch"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "eye-dropper", "tags": ["color", "picker", "dropper", "design"], "category": "design", "styles": ["outline", "solid", "mini"]}, + {"name": "eye-slash", "tags": ["hide", "invisible", "hidden", "privacy"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "face-frown", "tags": ["sad", "unhappy", "emoji", "emotion"], "category": "emoji", "styles": ["outline", "solid", "mini"]}, + {"name": "face-smile", "tags": ["happy", "smile", "emoji", "emotion"], "category": "emoji", "styles": ["outline", "solid", "mini"]}, + {"name": "film", "tags": ["video", "movie", "media", "cinema"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "finger-print", "tags": ["security", "identity", "biometric", "auth"], "category": "security", "styles": ["outline", "solid", "mini"]}, + {"name": "fire", "tags": ["flame", "hot", "trending", "popular"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "flag", "tags": ["report", "mark", "flag", "bookmark"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "folder", "tags": ["directory", "folder", "files", "organize"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "folder-arrow-down", "tags": ["download", "folder", "save"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "folder-minus", "tags": ["remove", "folder", "delete"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "folder-open", "tags": ["open", "folder", "browse"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "folder-plus", "tags": ["add", "folder", "new", "create"], "category": "files", "styles": ["outline", "solid", "mini"]}, + {"name": "forward", "tags": ["media", "next", "skip"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "funnel", "tags": ["filter", "sort", "funnel"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "gif", "tags": ["image", "animation", "gif", "media"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "gift", "tags": ["present", "gift", "reward", "surprise"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "gift-top", "tags": ["present", "gift", "reward"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "globe-alt", "tags": ["world", "earth", "internet", "web"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "globe-americas", "tags": ["world", "earth", "americas"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "globe-asia-australia", "tags": ["world", "earth", "asia"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "globe-europe-africa", "tags": ["world", "earth", "europe"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "hand-raised", "tags": ["stop", "hand", "halt", "wait"], "category": "gestures", "styles": ["outline", "solid", "mini"]}, + {"name": "hand-thumb-down", "tags": ["dislike", "thumbs down", "bad"], "category": "gestures", "styles": ["outline", "solid", "mini"]}, + {"name": "hand-thumb-up", "tags": ["like", "thumbs up", "good", "approve"], "category": "gestures", "styles": ["outline", "solid", "mini"]}, + {"name": "hashtag", "tags": ["tag", "hashtag", "topic", "social"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "heart", "tags": ["love", "favorite", "like", "heart"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "home", "tags": ["house", "home", "main", "dashboard"], "category": "navigation", "styles": ["outline", "solid", "mini"]}, + {"name": "home-modern", "tags": ["house", "home", "building"], "category": "buildings", "styles": ["outline", "solid", "mini"]}, + {"name": "identification", "tags": ["id", "card", "identity", "badge"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "inbox", "tags": ["mail", "inbox", "messages"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "inbox-arrow-down", "tags": ["receive", "inbox", "download"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "inbox-stack", "tags": ["inbox", "messages", "stack"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "information-circle", "tags": ["info", "help", "about", "details"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "key", "tags": ["password", "security", "access", "lock"], "category": "security", "styles": ["outline", "solid", "mini"]}, + {"name": "language", "tags": ["translate", "language", "international"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "lifebuoy", "tags": ["help", "support", "rescue", "assistance"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "light-bulb", "tags": ["idea", "light", "bulb", "creative"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "link", "tags": ["url", "link", "chain", "connect"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "list-bullet", "tags": ["list", "bullet", "items", "menu"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "lock-closed", "tags": ["security", "lock", "private", "protected"], "category": "security", "styles": ["outline", "solid", "mini"]}, + {"name": "lock-open", "tags": ["unlock", "open", "access"], "category": "security", "styles": ["outline", "solid", "mini"]}, + {"name": "magnifying-glass", "tags": ["search", "find", "zoom", "look"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "magnifying-glass-circle", "tags": ["search", "find", "explore"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "magnifying-glass-minus", "tags": ["zoom out", "search", "minus"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "magnifying-glass-plus", "tags": ["zoom in", "search", "plus"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "map", "tags": ["location", "map", "directions", "navigate"], "category": "maps", "styles": ["outline", "solid", "mini"]}, + {"name": "map-pin", "tags": ["location", "pin", "marker", "place"], "category": "maps", "styles": ["outline", "solid", "mini"]}, + {"name": "megaphone", "tags": ["announce", "broadcast", "marketing"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "microphone", "tags": ["audio", "voice", "record", "speak"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "minus", "tags": ["subtract", "remove", "minus", "decrease"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "minus-circle", "tags": ["remove", "delete", "minus"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "moon", "tags": ["dark", "night", "mode", "theme"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "musical-note", "tags": ["music", "audio", "sound", "note"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "newspaper", "tags": ["news", "article", "blog", "press"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "no-symbol", "tags": ["ban", "prohibited", "forbidden", "block"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "paint-brush", "tags": ["design", "art", "paint", "brush"], "category": "design", "styles": ["outline", "solid", "mini"]}, + {"name": "paper-airplane", "tags": ["send", "message", "email", "submit"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "paper-clip", "tags": ["attachment", "clip", "attach", "file"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "pause", "tags": ["media", "pause", "stop", "wait"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "pause-circle", "tags": ["media", "pause", "stop"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "pencil", "tags": ["edit", "write", "pencil", "modify"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "pencil-square", "tags": ["edit", "write", "compose"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "phone", "tags": ["call", "phone", "contact", "mobile"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "phone-arrow-down-left", "tags": ["incoming", "call", "receive"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "phone-arrow-up-right", "tags": ["outgoing", "call", "dial"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "phone-x-mark", "tags": ["hang up", "end call", "decline"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "photo", "tags": ["image", "picture", "photo", "gallery"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "play", "tags": ["media", "play", "start", "video"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "play-circle", "tags": ["media", "play", "video"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "play-pause", "tags": ["media", "toggle", "play", "pause"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "plus", "tags": ["add", "create", "new", "plus"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "plus-circle", "tags": ["add", "create", "new"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "power", "tags": ["on", "off", "power", "shutdown"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "presentation-chart-bar", "tags": ["presentation", "chart", "analytics"], "category": "charts", "styles": ["outline", "solid", "mini"]}, + {"name": "presentation-chart-line", "tags": ["presentation", "chart", "analytics"], "category": "charts", "styles": ["outline", "solid", "mini"]}, + {"name": "printer", "tags": ["print", "printer", "document"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "puzzle-piece", "tags": ["plugin", "extension", "puzzle", "addon"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "qr-code", "tags": ["qr", "code", "scan", "barcode"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "question-mark-circle", "tags": ["help", "question", "support", "faq"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "queue-list", "tags": ["list", "queue", "playlist"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "radio", "tags": ["radio", "audio", "broadcast"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "receipt-percent", "tags": ["discount", "sale", "coupon"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "receipt-refund", "tags": ["refund", "return", "money back"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "rectangle-group", "tags": ["layout", "grid", "components"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "rectangle-stack", "tags": ["layers", "stack", "cards"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "rocket-launch", "tags": ["launch", "startup", "rocket", "deploy"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "rss", "tags": ["feed", "rss", "subscribe", "blog"], "category": "communication", "styles": ["outline", "solid", "mini"]}, + {"name": "scale", "tags": ["balance", "justice", "legal", "weight"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "scissors", "tags": ["cut", "scissors", "trim", "edit"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "server", "tags": ["server", "hosting", "database", "backend"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "server-stack", "tags": ["servers", "hosting", "infrastructure"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "share", "tags": ["share", "send", "social", "forward"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "shield-check", "tags": ["security", "protected", "verified", "safe"], "category": "security", "styles": ["outline", "solid", "mini"]}, + {"name": "shield-exclamation", "tags": ["security", "warning", "alert"], "category": "security", "styles": ["outline", "solid", "mini"]}, + {"name": "shopping-bag", "tags": ["shop", "bag", "purchase", "buy"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "shopping-cart", "tags": ["cart", "shop", "ecommerce", "buy"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "signal", "tags": ["wifi", "signal", "connection", "network"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "signal-slash", "tags": ["no signal", "offline", "disconnected"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "sparkles", "tags": ["magic", "sparkle", "new", "ai"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "speaker-wave", "tags": ["audio", "sound", "volume", "speaker"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "speaker-x-mark", "tags": ["mute", "silent", "no sound"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "square-2-stack", "tags": ["copy", "duplicate", "stack"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "square-3-stack-3d", "tags": ["layers", "3d", "stack"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "squares-2x2", "tags": ["grid", "layout", "dashboard"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "squares-plus", "tags": ["add", "widget", "new"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "star", "tags": ["favorite", "star", "rating", "bookmark"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "stop", "tags": ["media", "stop", "end"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "stop-circle", "tags": ["media", "stop", "end"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "sun", "tags": ["light", "day", "bright", "theme"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "swatch", "tags": ["color", "palette", "design", "theme"], "category": "design", "styles": ["outline", "solid", "mini"]}, + {"name": "table-cells", "tags": ["table", "grid", "spreadsheet", "data"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "tag", "tags": ["label", "tag", "category", "price"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "ticket", "tags": ["ticket", "event", "pass", "coupon"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "trash", "tags": ["delete", "remove", "trash", "bin"], "category": "actions", "styles": ["outline", "solid", "mini"]}, + {"name": "trophy", "tags": ["award", "winner", "achievement", "prize"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "truck", "tags": ["delivery", "shipping", "truck", "transport"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "tv", "tags": ["television", "tv", "screen", "display"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "user", "tags": ["person", "user", "account", "profile"], "category": "users", "styles": ["outline", "solid", "mini"]}, + {"name": "user-circle", "tags": ["avatar", "profile", "account"], "category": "users", "styles": ["outline", "solid", "mini"]}, + {"name": "user-group", "tags": ["team", "group", "users", "people"], "category": "users", "styles": ["outline", "solid", "mini"]}, + {"name": "user-minus", "tags": ["remove user", "unfriend", "delete"], "category": "users", "styles": ["outline", "solid", "mini"]}, + {"name": "user-plus", "tags": ["add user", "invite", "new user"], "category": "users", "styles": ["outline", "solid", "mini"]}, + {"name": "users", "tags": ["people", "team", "community"], "category": "users", "styles": ["outline", "solid", "mini"]}, + {"name": "variable", "tags": ["code", "variable", "math", "programming"], "category": "development", "styles": ["outline", "solid", "mini"]}, + {"name": "video-camera", "tags": ["video", "camera", "record", "film"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "video-camera-slash", "tags": ["video off", "camera off", "mute"], "category": "media", "styles": ["outline", "solid", "mini"]}, + {"name": "view-columns", "tags": ["columns", "layout", "grid"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "view-finder-circle", "tags": ["focus", "target", "aim"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "wallet", "tags": ["money", "payment", "wallet", "finance"], "category": "commerce", "styles": ["outline", "solid", "mini"]}, + {"name": "wifi", "tags": ["wireless", "internet", "connection", "network"], "category": "devices", "styles": ["outline", "solid", "mini"]}, + {"name": "window", "tags": ["browser", "window", "app"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "wrench", "tags": ["tools", "settings", "repair", "fix"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "wrench-screwdriver", "tags": ["tools", "settings", "repair"], "category": "objects", "styles": ["outline", "solid", "mini"]}, + {"name": "x-circle", "tags": ["close", "cancel", "remove", "delete"], "category": "ui", "styles": ["outline", "solid", "mini"]}, + {"name": "x-mark", "tags": ["close", "cancel", "remove", "x"], "category": "ui", "styles": ["outline", "solid", "mini"]} + ] +} diff --git a/native/wordpress/maple-icons-wp/presets/index.php b/native/wordpress/maple-icons-wp/presets/index.php new file mode 100644 index 0000000..398909d --- /dev/null +++ b/native/wordpress/maple-icons-wp/presets/index.php @@ -0,0 +1,5 @@ +query( + "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mi_%' OR option_name LIKE '_transient_timeout_mi_%'" +); + +/** + * Remove downloaded icons directory. + */ +$icons_dir = WP_CONTENT_DIR . '/maple-icons/'; + +if ( is_dir( $icons_dir ) ) { + /** + * Recursively delete a directory and its contents. + * + * @param string $dir Directory path. + * @return bool True on success, false on failure. + */ + function mi_delete_directory( $dir ) { + if ( ! is_dir( $dir ) ) { + return false; + } + + $files = array_diff( scandir( $dir ), array( '.', '..' ) ); + + foreach ( $files as $file ) { + $path = $dir . '/' . $file; + + if ( is_dir( $path ) ) { + mi_delete_directory( $path ); + } else { + unlink( $path ); + } + } + + return rmdir( $dir ); + } + + mi_delete_directory( $icons_dir ); +}