Code/IconifyPanel.cs
using System;
using System.Threading.Tasks;
using Sandbox;
using Sandbox.UI;

namespace Iconify;

/// <summary>
/// Renders an icon from the Iconify API.
/// Usage: <iconify icon="ph:house" Size=@(24) Color="white" />
/// Browse icons: https://icones.js.org/
/// </summary>
[Alias( "iconify" )]
public partial class IconifyPanel : Panel
{
	private Texture? _texture;
	private string? _loadedKey;

	/// <summary>
	/// The icon identifier. Format: "prefix:name" e.g. "ph:house", "mdi:home", "tabler:settings"
	/// Browse: https://icones.js.org/
	/// </summary>
	public string Icon { get; set; } = "";

	/// <summary>
	/// Icon color as a hex or named color. Default: white.
	/// </summary>
	public string Color { get; set; } = "white";

	/// <summary>
	/// Icon size in pixels. Default: 24.
	/// </summary>
	public int Size { get; set; } = 24;

	protected override void OnAfterTreeRender( bool firstTime )
	{
		var key = $"{Icon}_{Color}_{Size}";
		if ( key == _loadedKey ) return;
		_loadedKey = key;

		_ = LoadIcon();
	}

	protected override int BuildHash() => HashCode.Combine( Icon, Color, Size, _texture );

	private async Task LoadIcon()
	{
		if ( string.IsNullOrWhiteSpace( Icon ) ) return;

		var parts = Icon.Split( ':', 2 );
		if ( parts.Length != 2 ) return;

		var prefix = parts[0];
		var name = parts[1];

		try
		{
			// Try cache first
			var texture = await IconCache.GetOrFetch( prefix, name, Color, Size );
			if ( texture is not null )
			{
				_texture = texture;
				StateHasChanged();
			}
		}
		catch ( Exception e )
		{
			Log.Warning( $"[Iconify] Failed to load '{Icon}': {e.Message}" );
		}
	}
}