Add reverse lookup, passthrough, and expand icon map

- Reverse lookup: resolveNative() maps Material/FA names to semantic keys
- Passthrough: unknown Material icon names render as-is when theme uses Material
- New icons: cancel, check-circle, error-circle, return, add-to-cart, payment,
  wallet, cloud-upload, sync, swap-vert, send, undo, attach, hand, note,
  summary, category, grid, view-grid, layers, palette, aspect-ratio, ruler,
  architecture, scale, zoom-in, zoom-out, security, file-document, quote, receipt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 11:12:36 +00:00
parent beee228691
commit df2691d918

View File

@@ -29,6 +29,7 @@ class MprIcons
private static $iconSet = null;
private static $smartyRegistered = false;
private static $reverseMap = null;
/**
* Semantic name => [FA class, Material name]
@@ -42,6 +43,7 @@ class MprIcons
// E-commerce
'cart' => ['fa fa-shopping-cart', 'shopping_cart'],
'add-to-cart' => ['fa fa-cart-plus', 'add_shopping_cart'],
'orders' => ['fa fa-list', 'receipt_long'],
'truck' => ['fa fa-truck', 'local_shipping'],
'credit-card' => ['fa fa-credit-card', 'credit_card'],
@@ -50,12 +52,20 @@ class MprIcons
'percent' => ['fa fa-percent', 'percent'],
'inventory' => ['fa fa-cube', 'inventory_2'],
'payments' => ['fa fa-money', 'payments'],
'payment' => ['fa fa-money', 'payment'],
'wallet' => ['fa fa-money', 'account_balance_wallet'],
'campaign' => ['fa fa-bullhorn', 'campaign'],
'trophy' => ['fa fa-trophy', 'emoji_events'],
'receipt' => ['fa fa-file-text-o', 'receipt'],
'quote' => ['fa fa-quote-right', 'request_quote'],
'return' => ['fa fa-undo', 'assignment_return'],
// Status
'success' => ['fa fa-check', 'check'],
'check-circle' => ['fa fa-check-circle', 'check_circle'],
'error' => ['fa fa-times', 'close'],
'error-circle' => ['fa fa-times-circle', 'error'],
'cancel' => ['fa fa-ban', 'cancel'],
'warning' => ['fa fa-exclamation-triangle', 'warning'],
'info' => ['fa fa-info-circle', 'info'],
@@ -66,14 +76,21 @@ class MprIcons
'settings' => ['fa fa-cog', 'settings'],
'download' => ['fa fa-download', 'download'],
'upload' => ['fa fa-upload', 'upload'],
'cloud-upload' => ['fa fa-cloud-upload', 'cloud_upload'],
'refresh' => ['fa fa-refresh', 'refresh'],
'sync' => ['fa fa-refresh', 'sync'],
'save' => ['fa fa-save', 'save'],
'print' => ['fa fa-print', 'print'],
'copy' => ['fa fa-copy', 'content_copy'],
'tune' => ['fa fa-sliders', 'tune'],
'build' => ['fa fa-wrench', 'build'],
'swap' => ['fa fa-exchange', 'swap_horiz'],
'swap-vert' => ['fa fa-exchange', 'swap_vert'],
'touch' => ['fa fa-hand-pointer-o', 'touch_app'],
'hand' => ['fa fa-hand-paper-o', 'pan_tool'],
'send' => ['fa fa-paper-plane', 'send'],
'undo' => ['fa fa-undo', 'undo'],
'attach' => ['fa fa-paperclip', 'attach_file'],
// Analytics & Charts
'chart' => ['fa fa-bar-chart', 'bar_chart'],
@@ -104,12 +121,12 @@ class MprIcons
'phone' => ['fa fa-phone', 'phone'],
'comment' => ['fa fa-comment', 'comment'],
'chat' => ['fa fa-comments', 'chat'],
'send' => ['fa fa-paper-plane', 'send'],
// Security
'lock' => ['fa fa-lock', 'lock'],
'unlock' => ['fa fa-unlock', 'lock_open'],
'shield' => ['fa fa-shield', 'shield'],
'security' => ['fa fa-lock', 'security'],
'key' => ['fa fa-key', 'vpn_key'],
'verified' => ['fa fa-check-circle', 'verified_user'],
'bolt' => ['fa fa-bolt', 'bolt'],
@@ -134,15 +151,28 @@ class MprIcons
'eye' => ['fa fa-eye', 'visibility'],
'eye-off' => ['fa fa-eye-slash', 'visibility_off'],
'file' => ['fa fa-file', 'description'],
'file-document' => ['fa fa-file-text', 'insert_drive_file'],
'folder' => ['fa fa-folder', 'folder'],
'link' => ['fa fa-link', 'link'],
'external' => ['fa fa-external-link', 'open_in_new'],
'help' => ['fa fa-question-circle', 'help'],
'note' => ['fa fa-sticky-note', 'note'],
'summary' => ['fa fa-list-alt', 'summarize'],
'category' => ['fa fa-th-large', 'category'],
// Documents & Quotes
'quote' => ['fa fa-file-text-o', 'request_quote'],
'receipt' => ['fa fa-file-text', 'receipt'],
'undo' => ['fa fa-undo', 'undo'],
// Layout & Design
'grid' => ['fa fa-th', 'grid_on'],
'view-grid' => ['fa fa-th-large', 'view_module'],
'layers' => ['fa fa-clone', 'layers'],
'palette' => ['fa fa-paint-brush', 'palette'],
'aspect-ratio' => ['fa fa-arrows-alt', 'aspect_ratio'],
'ruler' => ['fa fa-minus', 'straighten'],
'architecture' => ['fa fa-building', 'architecture'],
'scale' => ['fa fa-balance-scale', 'scale'],
// Zoom
'zoom-in' => ['fa fa-search-plus', 'zoom_in'],
'zoom-out' => ['fa fa-search-minus', 'zoom_out'],
// Loading
'spinner' => ['fa fa-spinner fa-spin', 'hourglass_empty'],
@@ -150,9 +180,25 @@ class MprIcons
];
/**
* Reverse map: Material icon name => semantic name (built lazily)
* Reverse lookup: resolve a native Material/FA icon name to its semantic key.
* Builds the reverse map lazily on first call.
*/
private static $reverseMap = null;
private static function resolveNative(string $nativeName): ?string
{
if (self::$reverseMap === null) {
self::$reverseMap = [];
foreach (self::$map as $semantic => [$fa, $material]) {
if (!isset(self::$reverseMap[$material])) {
self::$reverseMap[$material] = $semantic;
}
$faShort = str_replace(['fa fa-', 'fa-spin'], '', trim($fa));
if ($faShort && !isset(self::$reverseMap[$faShort])) {
self::$reverseMap[$faShort] = $semantic;
}
}
}
return self::$reverseMap[$nativeName] ?? null;
}
/**
* Get icon HTML - main method
@@ -163,11 +209,18 @@ class MprIcons
*/
public static function get(string $name, string $class = ''): string
{
// Direct semantic lookup
if (!isset(self::$map[$name])) {
// Reverse lookup: try matching a raw Material/FA icon name
$name = self::resolveNative($name);
if (!$name) {
$resolved = self::resolveNative($name);
if ($resolved) {
$name = $resolved;
} else {
// Passthrough: if theme uses Material Icons, render the name as-is.
// Handles the long tail of Material Icons not in the semantic map.
$set = self::getIconSet();
if ($set === self::MATERIAL && preg_match('/^[a-z][a-z0-9_]*$/', $name)) {
$extra = $class ? ' ' . $class : '';
return '<i class="material-icons' . $extra . '">' . $name . '</i>';
}
return '';
}
}
@@ -183,38 +236,20 @@ class MprIcons
return '<i class="' . $fa . $extra . '"></i>';
}
/**
* Resolve a native Material/FA icon name to its semantic name.
* Returns the semantic name or null if not found.
*/
private static function resolveNative(string $nativeName): ?string
{
if (self::$reverseMap === null) {
self::$reverseMap = [];
foreach (self::$map as $semantic => [$fa, $material]) {
// Map Material name → semantic
if (!isset(self::$reverseMap[$material])) {
self::$reverseMap[$material] = $semantic;
}
// Map FA class (e.g. "fa fa-trash" → "delete")
$faShort = str_replace(['fa fa-', 'fa-spin'], '', trim($fa));
if (!isset(self::$reverseMap[$faShort])) {
self::$reverseMap[$faShort] = $semantic;
}
}
}
return self::$reverseMap[$nativeName] ?? null;
}
/**
* Get raw class/name without HTML wrapper
*/
public static function raw(string $name): string
{
if (!isset(self::$map[$name])) {
$name = self::resolveNative($name) ?? '';
if (!$name) {
$resolved = self::resolveNative($name);
if ($resolved) {
$name = $resolved;
} else {
$set = self::getIconSet();
if ($set === self::MATERIAL && preg_match('/^[a-z][a-z0-9_]*$/', $name)) {
return $name;
}
return '';
}
}
@@ -600,7 +635,7 @@ class MprIcons
*/
public static function exists(string $name): bool
{
return isset(self::$map[$name]);
return isset(self::$map[$name]) || self::resolveNative($name) !== null;
}
/**