ee8797c142
Co-authored-by: Copilot <copilot@github.com>
185 lines
5.7 KiB
Markdown
185 lines
5.7 KiB
Markdown
# Card
|
|
|
|
A styled container with optional header (title + description) and footer sections. The body content is always rendered; header and footer are conditionally included.
|
|
|
|
---
|
|
|
|
## HTML structure
|
|
|
|
```
|
|
div.rounded-lg.border.border-border.bg-card.text-card-foreground.shadow-sm.{extraClasses}
|
|
div.flex.flex-col.space-y-1.5.p-6 ← header (omitted when no title/description)
|
|
h3.text-2xl.font-semibold ← title
|
|
p.text-sm.text-muted-foreground ← description
|
|
div.p-6.pt-0 ← content (always present)
|
|
{content}
|
|
div.flex.items-center.p-6.pt-0 ← footer (omitted when empty)
|
|
{footer}
|
|
```
|
|
|
|
---
|
|
|
|
## CSS mechanics
|
|
|
|
| Class | Effect |
|
|
|---|---|
|
|
| `bg-card text-card-foreground` | Pulls from CSS variables — dark mode works automatically |
|
|
| `rounded-lg border border-border shadow-sm` | Subtle rounded box with border and drop shadow |
|
|
| `p-6 pt-0` on content | Full padding except top (header provides the top spacing) |
|
|
| `space-y-1.5` on header | Controlled gap between title and description |
|
|
|
|
---
|
|
|
|
## Constructor signature
|
|
|
|
```csharp
|
|
public Card(
|
|
string content,
|
|
string title = "",
|
|
string description = "",
|
|
string footer = "",
|
|
string extraClasses = "")
|
|
```
|
|
|
|
| Parameter | Description |
|
|
|---|---|
|
|
| `content` | Raw HTML for the card body (always rendered) |
|
|
| `title` | Optional heading in the header area |
|
|
| `description` | Optional subheading below the title |
|
|
| `footer` | Optional raw HTML in the footer area |
|
|
| `extraClasses` | Additional Tailwind classes on the outer `div` |
|
|
|
|
---
|
|
|
|
## Usage examples
|
|
|
|
### Simple content card
|
|
|
|
```csharp
|
|
new Card(content: "<p>Your subscription renews on July 1.</p>")
|
|
```
|
|
|
|
### Card with title and description
|
|
|
|
```csharp
|
|
new Card(
|
|
content: "<p>Manage your billing details and invoices.</p>",
|
|
title: "Billing",
|
|
description: "Your current plan: Pro")
|
|
```
|
|
|
|
### Card with footer actions
|
|
|
|
```csharp
|
|
new Card(
|
|
content: "<p>Are you sure you want to cancel your account?</p>",
|
|
title: "Delete account",
|
|
description: "This action cannot be undone.",
|
|
footer: """
|
|
<button class="inline-flex h-9 rounded-md border border-input px-4 text-sm mr-2">Cancel</button>
|
|
<button class="inline-flex h-9 rounded-md bg-destructive text-destructive-foreground px-4 text-sm">Delete</button>
|
|
""")
|
|
```
|
|
|
|
### Constrained width
|
|
|
|
```csharp
|
|
new Card(
|
|
content: "<p>Hello world</p>",
|
|
title: "Welcome",
|
|
extraClasses: "max-w-sm mx-auto")
|
|
```
|
|
|
|
### Embedding a component as content
|
|
|
|
```csharp
|
|
// Render a Badge to a string then embed in the card body
|
|
var writer = new System.Buffers.ArrayBufferWriter<byte>();
|
|
new Badge("Active").Render(new HtmxRenderContext(writer));
|
|
var badgeHtml = System.Text.Encoding.UTF8.GetString(writer.WrittenSpan);
|
|
|
|
new Card(
|
|
content: $"<p class='mb-2'>Status:</p>{badgeHtml}",
|
|
title: "Account")
|
|
```
|
|
|
|
---
|
|
|
|
## Tips and tricks
|
|
|
|
- `content`, `footer`, title, and description are inserted as raw HTML — HTML-encode any user-supplied strings before passing them in.
|
|
- Use `extraClasses` to set max-width, margin, or custom background without subclassing.
|
|
- If you need a completely custom header layout, omit `title` and `description` and build the header HTML in `content`, adding `p-6` padding yourself.
|
|
- Cards can be placed in a CSS grid: `<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">`.
|
|
- Cards can be placed in a CSS grid: `<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">`.
|
|
|
|
---
|
|
|
|
## 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);
|
|
```
|