ee8797c142
Co-authored-by: Copilot <copilot@github.com>
177 lines
4.8 KiB
Markdown
177 lines
4.8 KiB
Markdown
# Progress
|
||
|
||
A horizontal progress bar. Value is clamped to 0–100. Three sizes control the bar height.
|
||
|
||
---
|
||
|
||
## HTML structure
|
||
|
||
```
|
||
div.w-full.bg-secondary.rounded-full.overflow-hidden.{size class}
|
||
div.bg-primary.rounded-full.h-full.transition-all[style="width: {value}%"]
|
||
```
|
||
|
||
---
|
||
|
||
## CSS mechanics
|
||
|
||
| Class | Effect |
|
||
|---|---|
|
||
| `bg-secondary` | Neutral track color |
|
||
| `bg-primary` | Filled indicator color |
|
||
| `rounded-full overflow-hidden` | Pill-shaped track; fills also become pill-shaped |
|
||
| `transition-all` | Smooth animation when `width` changes |
|
||
|
||
**Size classes applied to the outer track:**
|
||
|
||
| Size | Class | Height |
|
||
|---|---|---|
|
||
| `sm` | `h-1.5` | 6 px |
|
||
| `default` | `h-2.5` | 10 px |
|
||
| `lg` | `h-4` | 16 px |
|
||
|
||
---
|
||
|
||
## Constructor signature
|
||
|
||
```csharp
|
||
public Progress(int value, string size = "default")
|
||
```
|
||
|
||
| Parameter | Description |
|
||
|---|---|
|
||
| `value` | Fill percentage; clamped to 0–100 |
|
||
| `size` | `"sm"` / `"default"` / `"lg"` |
|
||
|
||
---
|
||
|
||
## Usage examples
|
||
|
||
### Inline usage
|
||
|
||
```csharp
|
||
new Progress(value: 72)
|
||
new Progress(value: 40, size: "sm")
|
||
new Progress(value: 100, size: "lg")
|
||
```
|
||
|
||
### Inside a Card
|
||
|
||
```csharp
|
||
new Card(
|
||
title: "Disk usage",
|
||
content: $"""
|
||
<div class="mb-2 flex justify-between text-sm">
|
||
<span>Used</span>
|
||
<span>{used} GB / {total} GB</span>
|
||
</div>
|
||
{progressHtml}
|
||
""")
|
||
```
|
||
|
||
(Pre-render the `Progress` to a string using `HtmxRenderContext` and `ArrayBufferWriter<byte>`.)
|
||
|
||
### HTMX live update
|
||
|
||
```html
|
||
<div id="progress-bar"
|
||
hx-get="/job/42/progress"
|
||
hx-trigger="every 1s"
|
||
hx-swap="outerHTML">
|
||
$$ProgressBar$$
|
||
</div>
|
||
```
|
||
|
||
The endpoint returns a partial re-render of this fragment with the updated `value`.
|
||
|
||
---
|
||
|
||
## Tips and tricks
|
||
|
||
- Values below 0 are treated as 0; values above 100 are treated as 100 — no manual clamping needed.
|
||
- Use `size: "sm"` for compact UI areas such as table rows.
|
||
- To animate progress smoothly, let `transition-all` do the work: re-render the component via HTMX on a polling interval or push updates via SSE.
|
||
- For an indeterminate spinner, use `Skeleton` instead (it has `animate-pulse` built in).
|
||
- For an indeterminate spinner, use `Skeleton` instead (it has `animate-pulse` built in).
|
||
|
||
---
|
||
|
||
## Complete page example
|
||
|
||
**`Templates/JobStatusPage.htmx`**
|
||
```html
|
||
<div class="max-w-md mx-auto py-10">
|
||
<h1 class="text-2xl font-bold mb-2">Processing</h1>
|
||
<p class="text-sm text-muted-foreground mb-6">$$StatusText$$</p>
|
||
<div class="mb-2 flex justify-between text-sm">
|
||
<span>Progress</span>
|
||
<span>$$ProgressLabel$$</span>
|
||
</div>
|
||
$$ProgressBar$$
|
||
$$DoneAlert$$
|
||
</div>
|
||
```
|
||
|
||
**`Templates/JobStatusPage.htmx.cs`**
|
||
```csharp
|
||
namespace Htmx.ApiDemo.Templates;
|
||
|
||
public sealed class JobStatusPage : JobStatusPageBase
|
||
{
|
||
private readonly byte[] _statusText;
|
||
private readonly byte[] _progressLabel;
|
||
private readonly IHtmxComponent _progressBar;
|
||
private readonly IHtmxComponent _doneAlert;
|
||
|
||
public JobStatusPage(int percent, string statusText)
|
||
{
|
||
_statusText = System.Net.WebUtility.HtmlEncode(statusText).ToUtf8Bytes();
|
||
_progressLabel = $"{percent}%".ToUtf8Bytes();
|
||
_progressBar = new Components.Progress(value: percent);
|
||
_doneAlert = percent >= 100
|
||
? new Components.Alert(title: "Complete!", description: "Your export is ready.")
|
||
: HtmxEmpty.Instance;
|
||
}
|
||
|
||
protected override void RenderStatusText(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_statusText);
|
||
protected override void RenderProgressLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_progressLabel);
|
||
protected override void RenderProgressBar(HtmxRenderContext ctx) => _progressBar.Render(ctx.Next());
|
||
protected override void RenderDoneAlert(HtmxRenderContext ctx) => _doneAlert.Render(ctx.Next());
|
||
}
|
||
```
|
||
|
||
**GET handler with HTMX polling**
|
||
```csharp
|
||
[Handler]
|
||
[MapGet("/jobs/{jobId}/status")]
|
||
public static partial class GetJobStatusHandler
|
||
{
|
||
public record Query([property: FromRoute] string JobId);
|
||
|
||
private static async Task<IResult> HandleAsync(
|
||
Query q, HttpContext ctx, JobQueue jobs, CancellationToken ct)
|
||
{
|
||
var job = await jobs.GetAsync(q.JobId, ct);
|
||
if (job is null) return Results.NotFound();
|
||
|
||
var page = new JobStatusPage(job.PercentComplete, job.StatusText);
|
||
|
||
// If polling (HTMX partial), only return the progress fragment
|
||
if (ctx.Request.Headers.ContainsKey("HX-Request"))
|
||
{
|
||
// Stop polling when done
|
||
if (job.PercentComplete >= 100)
|
||
ctx.Response.Headers.Append("HX-Trigger", "jobComplete");
|
||
|
||
return await ctx.WriteHtmxPage(page, title: "Processing");
|
||
}
|
||
|
||
// Full page load — include polling trigger
|
||
ctx.Response.Headers.Append("HX-Trigger-After-Settle",
|
||
"""{"startPolling": {"interval": 1000, "target": "#progress-region"}}""");
|
||
|
||
return await ctx.WriteHtmxPage(page, title: "Processing");
|
||
}
|
||
}
|
||
```
|