Card Component Added
This commit is contained in:
@@ -57,6 +57,16 @@
|
|||||||
</Icon>
|
</Icon>
|
||||||
<ChildContent>Forms</ChildContent>
|
<ChildContent>Forms</ChildContent>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
<SidebarMenuItem Href="/cards" Tooltip="Cards">
|
||||||
|
<Icon>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
||||||
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M3 9h18" />
|
||||||
|
</svg>
|
||||||
|
</Icon>
|
||||||
|
<ChildContent>Cards</ChildContent>
|
||||||
|
</SidebarMenuItem>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
@page "/cards"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
<PageTitle>Cards</PageTitle>
|
||||||
|
|
||||||
|
<div class="space-y-8 max-w-lg">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold tracking-tight">Cards Demo</h1>
|
||||||
|
<p class="text-muted-foreground">Card components with multiple layouts.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* ── Basic card ── *@
|
||||||
|
<Card data-testid="card-basic">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Card Title</CardTitle>
|
||||||
|
<CardDescription>Card Description</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p>Card Content</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<p>Card Footer</p>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
@* ── Card with action in header ── *@
|
||||||
|
<Card data-testid="card-login">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Login to your account</CardTitle>
|
||||||
|
<CardAction>
|
||||||
|
<Button Variant="@ButtonVariant.Link" Class="text-sm">Sign Up</Button>
|
||||||
|
</CardAction>
|
||||||
|
<CardDescription>Enter your email below to login to your account</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<FormField Label="Email" For="email">
|
||||||
|
<TextInput Id="email" Type="email" Placeholder="m@example.com" />
|
||||||
|
</FormField>
|
||||||
|
<FormField Label="Password" For="password">
|
||||||
|
<TextInput Id="password" Type="password" />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter Class="flex-col gap-2">
|
||||||
|
<Button Class="w-full">Login</Button>
|
||||||
|
<Button Variant="@ButtonVariant.Outline" Class="w-full">Login with Google</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
@* ── Image card ── *@
|
||||||
|
<Card data-testid="card-image">
|
||||||
|
<CardImage Src="https://placehold.co/600x300/2a2a2a/2a2a2a" Alt="Event cover" WrapperClass="h-48" />
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Design systems meetup</CardTitle>
|
||||||
|
<CardAction>
|
||||||
|
<span class="rounded-full border border-border px-3 py-0.5 text-xs font-medium text-muted-foreground">
|
||||||
|
Featured
|
||||||
|
</span>
|
||||||
|
</CardAction>
|
||||||
|
<CardDescription>A practical talk on component APIs, accessibility, and shipping faster.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter>
|
||||||
|
<Button Class="w-full">View Event</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,250 @@
|
|||||||
|
using Microsoft.Playwright;
|
||||||
|
|
||||||
|
namespace Enciphered.Blazor.UIComponents.Tests;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class CardTests : PlaywrightTestBase
|
||||||
|
{
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async Task GoToCardsAsync()
|
||||||
|
{
|
||||||
|
await Page.GotoAsync($"{BaseUrl}/cards", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });
|
||||||
|
await Page.WaitForSelectorAsync("[data-testid='card-basic']", new PageWaitForSelectorOptions { Timeout = 10_000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
private ILocator Card(string testId) => Page.Locator($"[data-testid='{testId}']");
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────
|
||||||
|
// Basic card – structure
|
||||||
|
// ────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task BasicCard_Is_Visible()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
await Expect(Card("card-basic")).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task BasicCard_Has_Rounded_Border_Classes()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var card = Card("card-basic");
|
||||||
|
await Expect(card).ToHaveClassAsync(new System.Text.RegularExpressions.Regex("rounded-xl"));
|
||||||
|
await Expect(card).ToHaveClassAsync(new System.Text.RegularExpressions.Regex("border"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task BasicCard_Renders_Title()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var title = Card("card-basic").Locator("h3");
|
||||||
|
await Expect(title).ToBeVisibleAsync();
|
||||||
|
await Expect(title).ToHaveTextAsync("Card Title");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task BasicCard_Renders_Description()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var desc = Card("card-basic").Locator("[data-slot='card-description']");
|
||||||
|
await Expect(desc).ToBeVisibleAsync();
|
||||||
|
await Expect(desc).ToHaveTextAsync("Card Description");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task BasicCard_Renders_Content()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var content = Card("card-basic").Locator("p:has-text('Card Content')");
|
||||||
|
await Expect(content).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task BasicCard_Renders_Footer()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var footer = Card("card-basic").Locator("p:has-text('Card Footer')");
|
||||||
|
await Expect(footer).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task BasicCard_Description_Appears_Below_Title()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var title = Card("card-basic").Locator("h3");
|
||||||
|
var desc = Card("card-basic").Locator("[data-slot='card-description']");
|
||||||
|
|
||||||
|
var titleBox = await title.BoundingBoxAsync();
|
||||||
|
var descBox = await desc.BoundingBoxAsync();
|
||||||
|
|
||||||
|
Assert.That(titleBox, Is.Not.Null, "Title bounding box should exist");
|
||||||
|
Assert.That(descBox, Is.Not.Null, "Description bounding box should exist");
|
||||||
|
Assert.That(descBox!.Y, Is.GreaterThan(titleBox!.Y), "Description should be positioned below the title");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────
|
||||||
|
// Login card – header with action
|
||||||
|
// ────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LoginCard_Is_Visible()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
await Expect(Card("card-login")).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LoginCard_Renders_Title()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var title = Card("card-login").Locator("h3");
|
||||||
|
await Expect(title).ToHaveTextAsync("Login to your account");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LoginCard_Renders_Action()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var action = Card("card-login").Locator("[data-slot='card-action']");
|
||||||
|
await Expect(action).ToBeVisibleAsync();
|
||||||
|
await Expect(action).ToContainTextAsync("Sign Up");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LoginCard_Action_Is_Right_Of_Title()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var title = Card("card-login").Locator("h3");
|
||||||
|
var action = Card("card-login").Locator("[data-slot='card-action']");
|
||||||
|
|
||||||
|
var titleBox = await title.BoundingBoxAsync();
|
||||||
|
var actionBox = await action.BoundingBoxAsync();
|
||||||
|
|
||||||
|
Assert.That(titleBox, Is.Not.Null);
|
||||||
|
Assert.That(actionBox, Is.Not.Null);
|
||||||
|
Assert.That(actionBox!.X, Is.GreaterThan(titleBox!.X), "Action should be to the right of the title");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LoginCard_Renders_Description()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var desc = Card("card-login").Locator("[data-slot='card-description']");
|
||||||
|
await Expect(desc).ToContainTextAsync("Enter your email below");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LoginCard_Has_Email_Input()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var email = Card("card-login").Locator("input#email");
|
||||||
|
await Expect(email).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LoginCard_Has_Password_Input()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var password = Card("card-login").Locator("input#password");
|
||||||
|
await Expect(password).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LoginCard_Has_Login_Button()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var btn = Card("card-login").Locator("button:has-text('Login')").First;
|
||||||
|
await Expect(btn).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LoginCard_Has_Google_Button()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var btn = Card("card-login").Locator("button:has-text('Login with Google')");
|
||||||
|
await Expect(btn).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────
|
||||||
|
// Image card
|
||||||
|
// ────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ImageCard_Is_Visible()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
await Expect(Card("card-image")).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ImageCard_Renders_Image()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var img = Card("card-image").Locator("img");
|
||||||
|
await Expect(img).ToBeVisibleAsync();
|
||||||
|
await Expect(img).ToHaveAttributeAsync("alt", "Event cover");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ImageCard_Image_Appears_Above_Title()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var img = Card("card-image").Locator("img");
|
||||||
|
var title = Card("card-image").Locator("h3");
|
||||||
|
|
||||||
|
var imgBox = await img.BoundingBoxAsync();
|
||||||
|
var titleBox = await title.BoundingBoxAsync();
|
||||||
|
|
||||||
|
Assert.That(imgBox, Is.Not.Null);
|
||||||
|
Assert.That(titleBox, Is.Not.Null);
|
||||||
|
Assert.That(titleBox!.Y, Is.GreaterThan(imgBox!.Y), "Title should be below the image");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ImageCard_Renders_Title()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var title = Card("card-image").Locator("h3");
|
||||||
|
await Expect(title).ToHaveTextAsync("Design systems meetup");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ImageCard_Renders_Featured_Badge()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var badge = Card("card-image").Locator("[data-slot='card-action']");
|
||||||
|
await Expect(badge).ToContainTextAsync("Featured");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ImageCard_Renders_Description()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var desc = Card("card-image").Locator("[data-slot='card-description']");
|
||||||
|
await Expect(desc).ToContainTextAsync("A practical talk on component APIs");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ImageCard_Has_View_Event_Button()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
var btn = Card("card-image").Locator("button:has-text('View Event')");
|
||||||
|
await Expect(btn).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────
|
||||||
|
// All three cards render on the page
|
||||||
|
// ────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task CardsPage_Renders_Three_Cards()
|
||||||
|
{
|
||||||
|
await GoToCardsAsync();
|
||||||
|
|
||||||
|
await Expect(Card("card-basic")).ToBeVisibleAsync();
|
||||||
|
await Expect(Card("card-login")).ToBeVisibleAsync();
|
||||||
|
await Expect(Card("card-image")).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@namespace Enciphered.Blazor.UIComponents
|
||||||
|
|
||||||
|
<div class="@ComputedClass" @attributes="AdditionalAttributes">
|
||||||
|
@ChildContent
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>The card's inner content (CardHeader, CardContent, CardFooter, etc.).</summary>
|
||||||
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Additional CSS classes appended to the card wrapper.</summary>
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
|
||||||
|
[Parameter(CaptureUnmatchedValues = true)]
|
||||||
|
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
||||||
|
|
||||||
|
private const string BaseClass =
|
||||||
|
"rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden";
|
||||||
|
|
||||||
|
private string ComputedClass =>
|
||||||
|
string.IsNullOrEmpty(Class) ? BaseClass : $"{BaseClass} {Class}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@namespace Enciphered.Blazor.UIComponents
|
||||||
|
|
||||||
|
<div data-slot="card-action" class="@ComputedClass" @attributes="AdditionalAttributes">
|
||||||
|
@ChildContent
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>Action content rendered at the trailing edge of the header (e.g. a link or button).</summary>
|
||||||
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Additional CSS classes.</summary>
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
|
||||||
|
[Parameter(CaptureUnmatchedValues = true)]
|
||||||
|
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
||||||
|
|
||||||
|
private const string BaseClass =
|
||||||
|
"self-start ml-auto";
|
||||||
|
|
||||||
|
private string ComputedClass =>
|
||||||
|
string.IsNullOrEmpty(Class) ? BaseClass : $"{BaseClass} {Class}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@namespace Enciphered.Blazor.UIComponents
|
||||||
|
|
||||||
|
<div class="@ComputedClass" @attributes="AdditionalAttributes">
|
||||||
|
@ChildContent
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>Main body content of the card.</summary>
|
||||||
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Additional CSS classes.</summary>
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
|
||||||
|
[Parameter(CaptureUnmatchedValues = true)]
|
||||||
|
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
||||||
|
|
||||||
|
private const string BaseClass = "p-6 pt-0";
|
||||||
|
|
||||||
|
private string ComputedClass =>
|
||||||
|
string.IsNullOrEmpty(Class) ? BaseClass : $"{BaseClass} {Class}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@namespace Enciphered.Blazor.UIComponents
|
||||||
|
|
||||||
|
<p data-slot="card-description" class="@ComputedClass" @attributes="AdditionalAttributes">
|
||||||
|
@ChildContent
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>Description text or markup rendered below the title.</summary>
|
||||||
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Additional CSS classes.</summary>
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
|
||||||
|
[Parameter(CaptureUnmatchedValues = true)]
|
||||||
|
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
||||||
|
|
||||||
|
private const string BaseClass =
|
||||||
|
"text-sm text-muted-foreground";
|
||||||
|
|
||||||
|
private string ComputedClass =>
|
||||||
|
string.IsNullOrEmpty(Class) ? BaseClass : $"{BaseClass} {Class}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@namespace Enciphered.Blazor.UIComponents
|
||||||
|
|
||||||
|
<div class="@ComputedClass" @attributes="AdditionalAttributes">
|
||||||
|
@ChildContent
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>Footer content — typically action buttons.</summary>
|
||||||
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Additional CSS classes.</summary>
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
|
||||||
|
[Parameter(CaptureUnmatchedValues = true)]
|
||||||
|
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
||||||
|
|
||||||
|
private const string BaseClass =
|
||||||
|
"flex items-center p-6 pt-0";
|
||||||
|
|
||||||
|
private string ComputedClass =>
|
||||||
|
string.IsNullOrEmpty(Class) ? BaseClass : $"{BaseClass} {Class}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@namespace Enciphered.Blazor.UIComponents
|
||||||
|
|
||||||
|
<div class="@ComputedClass" @attributes="AdditionalAttributes">
|
||||||
|
@ChildContent
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>Header content — typically CardTitle, CardDescription, and CardAction.</summary>
|
||||||
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Additional CSS classes.</summary>
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
|
||||||
|
[Parameter(CaptureUnmatchedValues = true)]
|
||||||
|
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
||||||
|
|
||||||
|
private const string BaseClass =
|
||||||
|
"flex flex-col gap-1.5 p-6 has-[data-slot=card-action]:grid has-[data-slot=card-action]:grid-cols-[1fr_auto] has-[data-slot=card-action]:items-center [&>[data-slot=card-description]]:col-span-full";
|
||||||
|
|
||||||
|
private string ComputedClass =>
|
||||||
|
string.IsNullOrEmpty(Class) ? BaseClass : $"{BaseClass} {Class}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@namespace Enciphered.Blazor.UIComponents
|
||||||
|
|
||||||
|
<div class="@ComputedWrapperClass">
|
||||||
|
<img src="@Src" alt="@Alt" class="@ComputedImageClass" @attributes="AdditionalAttributes" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>Image source URL.</summary>
|
||||||
|
[Parameter, EditorRequired] public string Src { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Image alt text for accessibility.</summary>
|
||||||
|
[Parameter] public string Alt { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Additional CSS classes for the image element.</summary>
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Additional CSS classes for the wrapper div.</summary>
|
||||||
|
[Parameter] public string? WrapperClass { get; set; }
|
||||||
|
|
||||||
|
[Parameter(CaptureUnmatchedValues = true)]
|
||||||
|
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
||||||
|
|
||||||
|
private const string BaseImageClass = "w-full h-full object-cover";
|
||||||
|
private const string BaseWrapperClass = "overflow-hidden";
|
||||||
|
|
||||||
|
private string ComputedImageClass =>
|
||||||
|
string.IsNullOrEmpty(Class) ? BaseImageClass : $"{BaseImageClass} {Class}";
|
||||||
|
|
||||||
|
private string ComputedWrapperClass =>
|
||||||
|
string.IsNullOrEmpty(WrapperClass) ? BaseWrapperClass : $"{BaseWrapperClass} {WrapperClass}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@namespace Enciphered.Blazor.UIComponents
|
||||||
|
|
||||||
|
<h3 class="@ComputedClass" @attributes="AdditionalAttributes">
|
||||||
|
@ChildContent
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>Title text or markup.</summary>
|
||||||
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Additional CSS classes.</summary>
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
|
||||||
|
[Parameter(CaptureUnmatchedValues = true)]
|
||||||
|
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
||||||
|
|
||||||
|
private const string BaseClass =
|
||||||
|
"text-lg font-semibold leading-none tracking-tight";
|
||||||
|
|
||||||
|
private string ComputedClass =>
|
||||||
|
string.IsNullOrEmpty(Class) ? BaseClass : $"{BaseClass} {Class}";
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user