UI/Components/ItemCard/ItemCard.razor

A Blazor/Razor UI component for an item card. Renders an optional image, category (with optional icon), title, and an optional footer with trait chips and flavor text. It defines a Trait record, component parameters, a ToneClass mapper, and a BuildHash override combining fields.

Reflection
@namespace Sunless.Libraries.UI
@using System
@using System.Collections.Generic
@using System.Linq
@using Sandbox
@using Sandbox.UI
@inherits Panel
@attribute [StyleSheet]

<root class="item-card">
	@if ( !string.IsNullOrEmpty( Image ) )
	{
		<div class="item-card-image" style="background-image: url(@Image)"></div>
	}

	<div class="item-card-body">
		@if ( !string.IsNullOrEmpty( Category ) || !string.IsNullOrEmpty( CategoryIcon ) )
		{
			<div class="item-card-category">
				@if ( !string.IsNullOrEmpty( CategoryIcon ) )
				{
					<Icon Name=@CategoryIcon Size=@(IconSize.Small) />
				}
				<div class="item-card-category-label">@Category</div>
			</div>
		}

		<div class="item-card-title">@Title</div>

		@if ( HasFooter )
		{
			<div class="item-card-footer">
				@if ( Traits is not null && Traits.Count > 0 )
				{
					<div class="item-card-traits">
						@foreach ( var t in Traits )
						{
							var tone = ToneClass( t.Tone );
							<div class="item-card-trait @tone">
								@if ( !string.IsNullOrEmpty( t.Icon ) )
								{
									<Icon [email protected] Size=@(IconSize.Small) />
								}
								<div class="item-card-trait-label">@t.Label</div>
							</div>
						}
					</div>
				}

				@if ( !string.IsNullOrEmpty( Flavor ) )
				{
					<div class="item-card-flavor">@Flavor</div>
				}
			</div>
		}
	</div>
</root>

@code
{
	public sealed record Trait( string Label, string Icon = null, MenuTone Tone = MenuTone.Default );

	[Parameter] public string Title { get; set; }
	[Parameter] public string Category { get; set; }
	[Parameter] public string CategoryIcon { get; set; }
	[Parameter] public string Image { get; set; }
	[Parameter] public string Flavor { get; set; }
	[Parameter] public IReadOnlyList<Trait> Traits { get; set; }

	bool HasFooter => (Traits is not null && Traits.Count > 0) || !string.IsNullOrEmpty( Flavor );

	static string ToneClass( MenuTone tone ) => tone switch
	{
		MenuTone.Primary => "tone-primary",
		MenuTone.Confirm => "tone-confirm",
		MenuTone.Danger  => "tone-danger",
		MenuTone.Ghost   => "tone-ghost",
		_ => "",
	};

	protected override int BuildHash() =>
		HashCode.Combine(
			Title, Category, CategoryIcon, Image, Flavor,
			Traits is null ? 0 : Traits.Count,
			Traits is null ? 0 : Traits.Aggregate( 0, ( h, t ) => HashCode.Combine( h, t.Label, t.Icon, t.Tone ) ) );
}