f6ae86617c
Co-authored-by: Copilot <copilot@github.com>
171 lines
5.0 KiB
Markdown
171 lines
5.0 KiB
Markdown
# Card
|
|
|
|
A bordered box for grouping related content — like a physical card you might hold in your hand. It has three distinct zones: a header (title + subtitle), a body (your content), and a footer (usually actions).
|
|
|
|
---
|
|
|
|
## Quick example
|
|
|
|
```csharp
|
|
new Card(
|
|
content: "<p>Your subscription renews on July 1.</p>",
|
|
title: "Billing",
|
|
description: "Current plan: Pro")
|
|
```
|
|
|
|
---
|
|
|
|
## All the options
|
|
|
|
```csharp
|
|
public Card(
|
|
string content,
|
|
string title = "",
|
|
string description = "",
|
|
string footer = "",
|
|
string extraClasses = "")
|
|
```
|
|
|
|
| Parameter | What it does |
|
|
|---|---|
|
|
| `content` | The body of the card — always shown. Raw HTML. |
|
|
| `title` | Optional bold heading at the top of the card. |
|
|
| `description` | Optional smaller subtitle below the title. |
|
|
| `footer` | Optional section at the bottom, typically holding action buttons. Raw HTML. |
|
|
| `extraClasses` | Additional Tailwind classes on the outer `div` — useful for `max-w-sm`, `col-span-2`, etc. |
|
|
|
|
The header section (title + description) is omitted entirely when both are empty. Same for the footer.
|
|
|
|
---
|
|
|
|
## Real-world examples
|
|
|
|
### A stats card on a dashboard
|
|
|
|
```csharp
|
|
new Card(
|
|
title: "Total Users",
|
|
description: "All registered accounts",
|
|
content: $"<p class=\"text-4xl font-bold\">{userCount:N0}</p>")
|
|
```
|
|
|
|
### A confirmation card with footer buttons
|
|
|
|
Buttons and other components need to be pre-rendered to HTML strings when used inside `content` or `footer`:
|
|
|
|
```csharp
|
|
string ToHtml(IHtmxComponent c)
|
|
{
|
|
var w = new System.Buffers.ArrayBufferWriter<byte>();
|
|
c.Render(new HtmxRenderContext(w));
|
|
return System.Text.Encoding.UTF8.GetString(w.WrittenSpan);
|
|
}
|
|
|
|
new Card(
|
|
title: "Delete account",
|
|
description: "This action cannot be undone.",
|
|
content: "<p>All your data will be permanently removed.</p>",
|
|
footer: ToHtml(new Button("Cancel", variant: "outline"))
|
|
+ ToHtml(new Button("Delete", variant: "destructive", type: "submit")))
|
|
```
|
|
|
|
### A grid of cards
|
|
|
|
Cards are most commonly placed in a CSS grid in the page template:
|
|
|
|
```html
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
$$Card1$$
|
|
$$Card2$$
|
|
$$Card3$$
|
|
</div>
|
|
```
|
|
|
|
### Constrained width (e.g. a login card)
|
|
|
|
```csharp
|
|
new Card(
|
|
content: "...login form HTML...",
|
|
title: "Welcome back",
|
|
description: "Sign in to your account",
|
|
extraClasses: "max-w-sm mx-auto")
|
|
```
|
|
|
|
---
|
|
|
|
## How it works
|
|
|
|
Card uses CSS variables (`bg-card`, `text-card-foreground`, `border-border`) which automatically adapt to dark mode. The header and footer sections are skipped entirely in the rendered HTML when they are not needed — they do not leave empty divs behind.
|
|
|
|
All strings passed to `content` and `footer` are raw HTML. HTML-encode any user-supplied values before passing them in.
|
|
|
|
---
|
|
|
|
## Complete page example
|
|
|
|
**`Templates/DashboardPage.htmx`**
|
|
```html
|
|
<div class="max-w-5xl mx-auto py-10">
|
|
<h1 class="text-2xl font-bold mb-8">Dashboard</h1>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
$$UsersCard$$
|
|
$$RevenueCard$$
|
|
$$OrdersCard$$
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
**`Templates/DashboardPage.htmx.cs`**
|
|
```csharp
|
|
namespace Htmx.ApiDemo.Templates;
|
|
|
|
public sealed class DashboardPage : DashboardPageBase
|
|
{
|
|
private readonly IHtmxComponent _users;
|
|
private readonly IHtmxComponent _revenue;
|
|
private readonly IHtmxComponent _orders;
|
|
|
|
public DashboardPage(DashboardStats stats)
|
|
{
|
|
_users = new Components.Card(
|
|
title: "Total users",
|
|
description: "Registered accounts",
|
|
content: $"<p class=\"text-4xl font-bold\">{stats.UserCount:N0}</p>");
|
|
|
|
_revenue = new Components.Card(
|
|
title: "Revenue",
|
|
description: "This month",
|
|
content: $"<p class=\"text-4xl font-bold\">${stats.MonthlyRevenue:N2}</p>");
|
|
|
|
_orders = new Components.Card(
|
|
title: "Open orders",
|
|
description: "Awaiting fulfillment",
|
|
content: $"<p class=\"text-4xl font-bold\">{stats.OpenOrders}</p>",
|
|
footer: """<a href="/orders" class="text-sm text-primary underline">View all</a>""");
|
|
}
|
|
|
|
protected override void RenderUsersCard(HtmxRenderContext ctx) => _users.Render(ctx.Next());
|
|
protected override void RenderRevenueCard(HtmxRenderContext ctx) => _revenue.Render(ctx.Next());
|
|
protected override void RenderOrdersCard(HtmxRenderContext ctx) => _orders.Render(ctx.Next());
|
|
}
|
|
```
|
|
|
|
**GET handler**
|
|
```csharp
|
|
[Handler]
|
|
[MapGet("/dashboard")]
|
|
public static partial class GetDashboardHandler
|
|
{
|
|
public record Query();
|
|
|
|
private static Task<IResult> HandleAsync(
|
|
Query _, HttpContext ctx, CancellationToken ct)
|
|
{
|
|
var stats = new DashboardStats(UserCount: 1_204, MonthlyRevenue: 48_320.50m, OpenOrders: 37);
|
|
return ctx.WriteHtmxPage(new DashboardPage(stats), title: "Dashboard");
|
|
}
|
|
}
|
|
|
|
public record DashboardStats(int UserCount, decimal MonthlyRevenue, int OpenOrders);
|
|
```
|