Created more components
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<div class="accordion-root w-full" id="$$Id$$">
|
||||
$$Items$$
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Accordion. Items collapse/expand client-side via components.js.
|
||||
/// Pass a list of (Title, Content) tuples; set openIndex to expand one by default (-1 = all closed).
|
||||
/// </summary>
|
||||
public sealed class Accordion : AccordionBase
|
||||
{
|
||||
private const string ChevronSvg =
|
||||
"""<svg class="accordion-chevron h-4 w-4 shrink-0 transition-transform duration-200" """ +
|
||||
"""xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" """ +
|
||||
"""stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>""";
|
||||
|
||||
private readonly byte[] _idData;
|
||||
private readonly byte[] _itemsData;
|
||||
|
||||
public Accordion(string id, IEnumerable<(string Title, string Content)> items, int openIndex = -1)
|
||||
{
|
||||
_idData = id.ToUtf8Bytes();
|
||||
|
||||
var list = items.ToList();
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
var (title, content) = list[i];
|
||||
var expanded = i == openIndex;
|
||||
var height = expanded ? "auto" : "0";
|
||||
var opacity = expanded ? "1" : "0";
|
||||
|
||||
sb.Append($"""
|
||||
<div class="accordion-item border-b border-border">
|
||||
<h3 class="flex">
|
||||
<button type="button"
|
||||
class="accordion-trigger flex flex-1 items-center justify-between py-4 font-medium
|
||||
transition-all hover:underline text-left"
|
||||
aria-expanded="{(expanded ? "true" : "false")}">
|
||||
{title}
|
||||
{ChevronSvg}
|
||||
</button>
|
||||
</h3>
|
||||
<div class="accordion-panel overflow-hidden text-sm transition-all duration-200"
|
||||
style="height:{height};opacity:{opacity}">
|
||||
<div class="pb-4 pt-0">{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
""");
|
||||
}
|
||||
|
||||
_itemsData = sb.ToString().ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
|
||||
protected override void RenderItems(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_itemsData);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div role="alert" class="$$Classes$$">
|
||||
$$Icon$$
|
||||
<div>
|
||||
$$Title$$
|
||||
$$Description$$
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Alert component.
|
||||
/// Variant: default | destructive
|
||||
/// </summary>
|
||||
public sealed class Alert : AlertBase
|
||||
{
|
||||
private static readonly Dictionary<string, string> VariantClasses = new()
|
||||
{
|
||||
["default"] = "relative w-full rounded-lg border border-border bg-background p-4 " +
|
||||
"[&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
["destructive"] = "relative w-full rounded-lg border border-destructive/50 p-4 text-destructive " +
|
||||
"[&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-destructive",
|
||||
};
|
||||
|
||||
private readonly byte[] _classesData;
|
||||
private readonly byte[] _iconData;
|
||||
private readonly byte[] _titleData;
|
||||
private readonly byte[] _descriptionData;
|
||||
|
||||
public Alert(
|
||||
string title,
|
||||
string description = "",
|
||||
string variant = "default",
|
||||
string icon = "")
|
||||
{
|
||||
_classesData = VariantClasses.GetValueOrDefault(variant, VariantClasses["default"]).ToUtf8Bytes();
|
||||
_iconData = icon.ToUtf8Bytes();
|
||||
_titleData = $"""<h5 class="mb-1 font-medium leading-none tracking-tight">{title}</h5>""".ToUtf8Bytes();
|
||||
_descriptionData = string.IsNullOrEmpty(description)
|
||||
? []
|
||||
: $"""<div class="text-sm [&_p]:leading-relaxed">{description}</div>""".ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_classesData);
|
||||
protected override void RenderIcon(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_iconData);
|
||||
protected override void RenderTitle(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_titleData);
|
||||
protected override void RenderDescription(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_descriptionData);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<span class="relative flex $$SizeClasses$$ shrink-0 overflow-hidden rounded-full">
|
||||
$$Content$$
|
||||
</span>
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Avatar component. Shows an image or falls back to initials.
|
||||
/// Size: sm (h-8 w-8) | default (h-10 w-10) | lg (h-14 w-14) | xl (h-20 w-20)
|
||||
/// </summary>
|
||||
public sealed class Avatar : AvatarBase
|
||||
{
|
||||
private static readonly Dictionary<string, string> Sizes = new()
|
||||
{
|
||||
["sm"] = "h-8 w-8",
|
||||
["default"] = "h-10 w-10",
|
||||
["lg"] = "h-14 w-14",
|
||||
["xl"] = "h-20 w-20",
|
||||
};
|
||||
|
||||
private readonly byte[] _sizeClassesData;
|
||||
private readonly byte[] _contentData;
|
||||
|
||||
public Avatar(string fallback, string? src = null, string size = "default")
|
||||
{
|
||||
_sizeClassesData = Sizes.GetValueOrDefault(size, Sizes["default"]).ToUtf8Bytes();
|
||||
|
||||
_contentData = !string.IsNullOrEmpty(src)
|
||||
? $"""<img src="{src}" alt="{fallback}" class="aspect-square h-full w-full object-cover" />""".ToUtf8Bytes()
|
||||
: $"""<span class="flex h-full w-full items-center justify-center rounded-full bg-muted text-muted-foreground text-sm font-medium select-none">{fallback}</span>""".ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderSizeClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_sizeClassesData);
|
||||
protected override void RenderContent(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_contentData);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<span class="$$Classes$$">$$Text$$</span>
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Badge component.
|
||||
/// Variant: default | secondary | destructive | outline
|
||||
/// </summary>
|
||||
public sealed class Badge : BadgeBase
|
||||
{
|
||||
private static readonly Dictionary<string, string> VariantClasses = new()
|
||||
{
|
||||
["default"] = "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
["secondary"] = "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
["destructive"] = "bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
["outline"] = "text-foreground border border-input hover:bg-accent",
|
||||
};
|
||||
|
||||
private const string BaseClasses =
|
||||
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors " +
|
||||
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2";
|
||||
|
||||
private readonly byte[] _textData;
|
||||
private readonly byte[] _classesData;
|
||||
|
||||
public Badge(string text, string variant = "default")
|
||||
{
|
||||
_textData = text.ToUtf8Bytes();
|
||||
var v = VariantClasses.GetValueOrDefault(variant, VariantClasses["default"]);
|
||||
_classesData = $"{BaseClasses} {v}".ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderText(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_textData);
|
||||
protected override void RenderClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_classesData);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<nav aria-label="Breadcrumb">
|
||||
<ol class="flex flex-wrap items-center gap-1.5 wrap-break-word text-sm text-muted-foreground">
|
||||
$$Items$$
|
||||
</ol>
|
||||
</nav>
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Breadcrumb navigation.
|
||||
/// Pass items as (Label, Href) tuples — empty Href renders a non-linked span.
|
||||
/// The last item is always rendered as the current page (non-linked, foreground colour).
|
||||
/// </summary>
|
||||
public sealed class Breadcrumb : BreadcrumbBase
|
||||
{
|
||||
private const string ChevronSvg =
|
||||
"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-3.5 w-3.5"><path d="m9 18 6-6-6-6"/></svg>""";
|
||||
|
||||
private readonly byte[] _itemsData;
|
||||
|
||||
public Breadcrumb(IEnumerable<(string Label, string Href)> items)
|
||||
{
|
||||
var list = items.ToList();
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
var (label, href) = list[i];
|
||||
bool isLast = i == list.Count - 1;
|
||||
|
||||
sb.Append("""<li class="inline-flex items-center gap-1.5">""");
|
||||
|
||||
if (isLast || string.IsNullOrEmpty(href))
|
||||
sb.Append($"""<span class="{(isLast ? "font-normal text-foreground" : "")}">{label}</span>""");
|
||||
else
|
||||
sb.Append($"""<a href="{href}" class="hover:text-foreground transition-colors">{label}</a>""");
|
||||
|
||||
if (!isLast)
|
||||
sb.Append($"""<span role="presentation" aria-hidden="true">{ChevronSvg}</span>""");
|
||||
|
||||
sb.Append("</li>");
|
||||
}
|
||||
|
||||
_itemsData = sb.ToString().ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderItems(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_itemsData);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="rounded-lg border border-border bg-card text-card-foreground shadow-sm $$ExtraClasses$$">
|
||||
$$Header$$
|
||||
<div class="p-6 pt-0">$$Content$$</div>
|
||||
$$Footer$$
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Card component with optional header (title + description) and footer.
|
||||
/// </summary>
|
||||
public sealed class Card : CardBase
|
||||
{
|
||||
private readonly byte[] _extraClassesData;
|
||||
private readonly byte[] _headerData;
|
||||
private readonly byte[] _contentData;
|
||||
private readonly byte[] _footerData;
|
||||
|
||||
public Card(
|
||||
string content,
|
||||
string title = "",
|
||||
string description = "",
|
||||
string footer = "",
|
||||
string extraClasses = "")
|
||||
{
|
||||
_extraClassesData = extraClasses.ToUtf8Bytes();
|
||||
_contentData = content.ToUtf8Bytes();
|
||||
|
||||
_headerData = (string.IsNullOrEmpty(title) && string.IsNullOrEmpty(description))
|
||||
? []
|
||||
: BuildHeader(title, description);
|
||||
|
||||
_footerData = string.IsNullOrEmpty(footer)
|
||||
? []
|
||||
: $"""<div class="flex items-center p-6 pt-0">{footer}</div>""".ToUtf8Bytes();
|
||||
}
|
||||
|
||||
private static byte[] BuildHeader(string title, string description)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.Append("""<div class="flex flex-col space-y-1.5 p-6">""");
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
sb.Append($"""<h3 class="text-2xl font-semibold leading-none tracking-tight">{title}</h3>""");
|
||||
if (!string.IsNullOrEmpty(description))
|
||||
sb.Append($"""<p class="text-sm text-muted-foreground">{description}</p>""");
|
||||
sb.Append("</div>");
|
||||
return sb.ToString().ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderExtraClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_extraClassesData);
|
||||
protected override void RenderHeader(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_headerData);
|
||||
protected override void RenderContent(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_contentData);
|
||||
protected override void RenderFooter(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_footerData);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="$$Id$$"
|
||||
name="$$Name$$"
|
||||
value="$$Value$$"
|
||||
$$Checked$$
|
||||
class="h-4 w-4 shrink-0 rounded-sm border border-input ring-offset-background
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
||||
focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50
|
||||
accent-primary cursor-pointer" />
|
||||
$$Label$$
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Checkbox with an optional label.
|
||||
/// </summary>
|
||||
public sealed class Checkbox : CheckboxBase
|
||||
{
|
||||
private readonly byte[] _idData;
|
||||
private readonly byte[] _nameData;
|
||||
private readonly byte[] _valueData;
|
||||
private readonly byte[] _checkedData;
|
||||
private readonly byte[] _labelData;
|
||||
|
||||
public Checkbox(
|
||||
string id,
|
||||
string label = "",
|
||||
string name = "",
|
||||
string value = "true",
|
||||
bool @checked = false)
|
||||
{
|
||||
_idData = id.ToUtf8Bytes();
|
||||
_nameData = (string.IsNullOrEmpty(name) ? id : name).ToUtf8Bytes();
|
||||
_valueData = value.ToUtf8Bytes();
|
||||
_checkedData = (@checked ? "checked" : "").ToUtf8Bytes();
|
||||
_labelData = string.IsNullOrEmpty(label)
|
||||
? []
|
||||
: $"""<label for="{id}" class="text-sm font-medium leading-none cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{label}</label>""".ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
|
||||
protected override void RenderName(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nameData);
|
||||
protected override void RenderValue(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_valueData);
|
||||
protected override void RenderChecked(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_checkedData);
|
||||
protected override void RenderLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_labelData);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<dialog id="dlg-$$Id$$"
|
||||
class="dialog-root rounded-lg border border-border bg-background p-0 shadow-lg
|
||||
fixed left-1/2 top-1/2 m-0 w-full max-w-lg -translate-x-1/2 -translate-y-1/2
|
||||
backdrop:bg-black/50 backdrop:backdrop-blur-sm
|
||||
open:block">
|
||||
<div class="flex flex-col gap-4 p-6">
|
||||
$$Header$$
|
||||
<div class="text-sm text-muted-foreground">$$Content$$</div>
|
||||
$$Footer$$
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -0,0 +1,55 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Dialog using the native HTML <dialog> element.
|
||||
/// Open with data-dialog-open="id" on any button; close with data-dialog-close or .dialog-close.
|
||||
/// JS wiring is in components.js.
|
||||
/// </summary>
|
||||
public sealed class Dialog : DialogBase
|
||||
{
|
||||
private readonly byte[] _idData;
|
||||
private readonly byte[] _headerData;
|
||||
private readonly byte[] _contentData;
|
||||
private readonly byte[] _footerData;
|
||||
|
||||
public Dialog(
|
||||
string id,
|
||||
string content,
|
||||
string title = "",
|
||||
string description = "",
|
||||
string footer = "")
|
||||
{
|
||||
_idData = id.ToUtf8Bytes();
|
||||
_contentData = content.ToUtf8Bytes();
|
||||
|
||||
_headerData = (string.IsNullOrEmpty(title) && string.IsNullOrEmpty(description))
|
||||
? []
|
||||
: BuildHeader(id, title, description);
|
||||
|
||||
_footerData = string.IsNullOrEmpty(footer)
|
||||
? []
|
||||
: $"""<div class="flex justify-end gap-2">{footer}</div>""".ToUtf8Bytes();
|
||||
}
|
||||
|
||||
private static byte[] BuildHeader(string id, string title, string description)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.Append("""<div class="flex items-start justify-between gap-4">""");
|
||||
sb.Append("""<div class="flex flex-col gap-1.5">""");
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
sb.Append($"""<h2 class="text-lg font-semibold leading-none tracking-tight">{title}</h2>""");
|
||||
if (!string.IsNullOrEmpty(description))
|
||||
sb.Append($"""<p class="text-sm text-muted-foreground">{description}</p>""");
|
||||
sb.Append("</div>");
|
||||
sb.Append("""<button type="button" data-dialog-close class="dialog-close rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" aria-label="Close">""");
|
||||
sb.Append("""<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>""");
|
||||
sb.Append("</button>");
|
||||
sb.Append("</div>");
|
||||
return sb.ToString().ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
|
||||
protected override void RenderHeader(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_headerData);
|
||||
protected override void RenderContent(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_contentData);
|
||||
protected override void RenderFooter(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_footerData);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="dropdown-root relative inline-block">
|
||||
<div class="dropdown-trigger $$TriggerClasses$$" role="button" tabindex="0" aria-haspopup="menu" aria-expanded="false">
|
||||
$$Trigger$$
|
||||
</div>
|
||||
<div class="dropdown-content hidden absolute $$Position$$ z-50 min-w-40 rounded-md border border-border
|
||||
bg-popover p-1 text-popover-foreground shadow-md" role="menu">
|
||||
$$Items$$
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// CSS-native DropdownMenu using <details>/<summary>.
|
||||
/// Position: "left-0 top-full mt-1" (default) | "right-0 top-full mt-1" | etc.
|
||||
/// Items: pre-built HTML — use BuildItem() helper for consistent styling.
|
||||
/// </summary>
|
||||
public sealed class DropdownMenu : DropdownMenuBase
|
||||
{
|
||||
private const string ItemClasses =
|
||||
"relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm " +
|
||||
"outline-none transition-colors hover:bg-accent hover:text-accent-foreground " +
|
||||
"focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50";
|
||||
|
||||
private readonly byte[] _triggerClassesData;
|
||||
private readonly byte[] _triggerData;
|
||||
private readonly byte[] _positionData;
|
||||
private readonly byte[] _itemsData;
|
||||
|
||||
public DropdownMenu(
|
||||
IHtmxComponent trigger,
|
||||
IEnumerable<(string Label, string Href, bool IsSeparator)> items,
|
||||
string position = "left-0 top-full mt-1")
|
||||
{
|
||||
// Render trigger to bytes
|
||||
var writer = new System.Buffers.ArrayBufferWriter<byte>();
|
||||
trigger.Render(new HtmxRenderContext(writer));
|
||||
_triggerData = writer.WrittenSpan.ToArray();
|
||||
_triggerClassesData = []; // trigger already supplies its own classes
|
||||
_positionData = position.ToUtf8Bytes();
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (var (label, href, isSeparator) in items)
|
||||
{
|
||||
if (isSeparator)
|
||||
{
|
||||
sb.Append("""<div class="-mx-1 my-1 h-px bg-border"></div>""");
|
||||
}
|
||||
else if (string.IsNullOrEmpty(href))
|
||||
{
|
||||
sb.Append($"""<button type="button" class="{ItemClasses}">{label}</button>""");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($"""<a href="{href}" class="{ItemClasses}">{label}</a>""");
|
||||
}
|
||||
}
|
||||
_itemsData = sb.ToString().ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderTriggerClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_triggerClassesData);
|
||||
protected override void RenderTrigger(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_triggerData);
|
||||
protected override void RenderPosition(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_positionData);
|
||||
protected override void RenderItems(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_itemsData);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="flex flex-col gap-1.5">
|
||||
$$Label$$
|
||||
<input
|
||||
id="$$Id$$"
|
||||
name="$$Name$$"
|
||||
type="file"
|
||||
$$Accept$$
|
||||
$$Multiple$$
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm
|
||||
ring-offset-background
|
||||
file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground
|
||||
placeholder:text-muted-foreground
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
||||
focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 $$ExtraClasses$$"
|
||||
$$HxAttrs$$
|
||||
/>
|
||||
$$Description$$
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style FileInput component with optional label and description.
|
||||
/// </summary>
|
||||
public sealed class FileInput : FileInputBase
|
||||
{
|
||||
private readonly byte[] _idData;
|
||||
private readonly byte[] _nameData;
|
||||
private readonly byte[] _acceptData;
|
||||
private readonly byte[] _multipleData;
|
||||
private readonly byte[] _labelData;
|
||||
private readonly byte[] _descriptionData;
|
||||
private readonly byte[] _extraClassesData;
|
||||
private readonly byte[] _hxAttrsData;
|
||||
|
||||
public FileInput(
|
||||
string id,
|
||||
string name = "",
|
||||
string accept = "",
|
||||
bool multiple = false,
|
||||
string label = "",
|
||||
string description = "",
|
||||
string extraClasses = "",
|
||||
string hxAttrs = "")
|
||||
{
|
||||
_idData = id.ToUtf8Bytes();
|
||||
_nameData = (string.IsNullOrEmpty(name) ? id : name).ToUtf8Bytes();
|
||||
_acceptData = string.IsNullOrEmpty(accept) ? [] : $"""accept="{accept}" """.ToUtf8Bytes();
|
||||
_multipleData = multiple ? "multiple".ToUtf8Bytes() : [];
|
||||
_extraClassesData = extraClasses.ToUtf8Bytes();
|
||||
_hxAttrsData = hxAttrs.ToUtf8Bytes();
|
||||
|
||||
_labelData = string.IsNullOrEmpty(label)
|
||||
? []
|
||||
: $"""<label for="{id}" class="text-sm font-medium leading-none">{label}</label>""".ToUtf8Bytes();
|
||||
|
||||
_descriptionData = string.IsNullOrEmpty(description)
|
||||
? []
|
||||
: $"""<p class="text-xs text-muted-foreground">{description}</p>""".ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
|
||||
protected override void RenderName(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nameData);
|
||||
protected override void RenderAccept(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_acceptData);
|
||||
protected override void RenderMultiple(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_multipleData);
|
||||
protected override void RenderLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_labelData);
|
||||
protected override void RenderDescription(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_descriptionData);
|
||||
protected override void RenderExtraClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_extraClassesData);
|
||||
protected override void RenderHxAttrs(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_hxAttrsData);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<nav aria-label="Pagination" class="flex items-center gap-1">
|
||||
$$Prev$$
|
||||
$$Pages$$
|
||||
$$Next$$
|
||||
</nav>
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Pagination. Generates prev/next and page-number buttons.
|
||||
/// urlPattern: format string where {0} is replaced by the page number, e.g. "/items?page={0}"
|
||||
/// </summary>
|
||||
public sealed class Pagination : PaginationBase
|
||||
{
|
||||
private const string BtnBase =
|
||||
"inline-flex items-center justify-center rounded-md border border-input bg-background " +
|
||||
"px-3 h-9 text-sm font-medium ring-offset-background transition-colors " +
|
||||
"hover:bg-accent hover:text-accent-foreground " +
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 " +
|
||||
"disabled:pointer-events-none disabled:opacity-50";
|
||||
|
||||
private const string ActiveBtn =
|
||||
"inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground " +
|
||||
"px-3 h-9 text-sm font-medium ring-offset-background " +
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2";
|
||||
|
||||
private readonly byte[] _prevData;
|
||||
private readonly byte[] _pagesData;
|
||||
private readonly byte[] _nextData;
|
||||
|
||||
public Pagination(int current, int total, string urlPattern = "?page={0}")
|
||||
{
|
||||
_prevData = current <= 1
|
||||
? $"""<button type="button" class="{BtnBase}" disabled aria-label="Previous page">‹</button>""".ToUtf8Bytes()
|
||||
: $"""<a href="{string.Format(urlPattern, current - 1)}" class="{BtnBase}" aria-label="Previous page">‹</a>""".ToUtf8Bytes();
|
||||
|
||||
_nextData = current >= total
|
||||
? $"""<button type="button" class="{BtnBase}" disabled aria-label="Next page">›</button>""".ToUtf8Bytes()
|
||||
: $"""<a href="{string.Format(urlPattern, current + 1)}" class="{BtnBase}" aria-label="Next page">›</a>""".ToUtf8Bytes();
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
for (int p = 1; p <= total; p++)
|
||||
{
|
||||
if (p == current)
|
||||
sb.Append($"""<span class="{ActiveBtn}" aria-current="page">{p}</span>""");
|
||||
else
|
||||
sb.Append($"""<a href="{string.Format(urlPattern, p)}" class="{BtnBase}">{p}</a>""");
|
||||
}
|
||||
_pagesData = sb.ToString().ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderPrev(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_prevData);
|
||||
protected override void RenderPages(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_pagesData);
|
||||
protected override void RenderNext(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nextData);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="relative $$HeightClass$$ w-full overflow-hidden rounded-full bg-secondary"
|
||||
role="progressbar" aria-valuenow="$$ValueNow$$" aria-valuemin="0" aria-valuemax="100">
|
||||
<div class="h-full bg-primary transition-all duration-300" style="width:$$ValueNow$$%"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Progress bar. Value is clamped to 0–100.
|
||||
/// Size: sm (h-2) | default (h-4) | lg (h-6)
|
||||
/// </summary>
|
||||
public sealed class Progress : ProgressBase
|
||||
{
|
||||
private readonly byte[] _valueNowData;
|
||||
private readonly byte[] _heightClassData;
|
||||
|
||||
public Progress(int value = 0, string size = "default")
|
||||
{
|
||||
var clamped = Math.Clamp(value, 0, 100);
|
||||
_valueNowData = clamped.ToString().ToUtf8Bytes();
|
||||
_heightClassData = size switch
|
||||
{
|
||||
"sm" => "h-2".ToUtf8Bytes(),
|
||||
"lg" => "h-6".ToUtf8Bytes(),
|
||||
_ => "h-4".ToUtf8Bytes(),
|
||||
};
|
||||
}
|
||||
|
||||
protected override void RenderValueNow(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_valueNowData);
|
||||
protected override void RenderHeightClass(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_heightClassData);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
$$GroupLabel$$
|
||||
<div class="flex $$Direction$$ gap-3" role="radiogroup">
|
||||
$$Items$$
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style RadioGroup.
|
||||
/// Direction: flex-col | flex-row
|
||||
/// </summary>
|
||||
public sealed class RadioGroup : RadioGroupBase
|
||||
{
|
||||
private readonly byte[] _groupLabelData;
|
||||
private readonly byte[] _directionData;
|
||||
private readonly byte[] _itemsData;
|
||||
|
||||
public RadioGroup(
|
||||
string name,
|
||||
IEnumerable<(string Value, string Label, bool Selected)> options,
|
||||
string label = "",
|
||||
string direction = "flex-col")
|
||||
{
|
||||
_groupLabelData = string.IsNullOrEmpty(label)
|
||||
? []
|
||||
: $"""<span class="text-sm font-medium leading-none">{label}</span>""".ToUtf8Bytes();
|
||||
|
||||
_directionData = direction.ToUtf8Bytes();
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (var (value, optLabel, selected) in options)
|
||||
{
|
||||
var optId = $"{name}-{value}";
|
||||
var sel = selected ? " checked" : "";
|
||||
sb.Append($"""
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm">
|
||||
<input type="radio" id="{optId}" name="{name}" value="{value}"{sel}
|
||||
class="h-4 w-4 border border-input accent-primary
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
||||
disabled:cursor-not-allowed disabled:opacity-50" />
|
||||
{optLabel}
|
||||
</label>
|
||||
""");
|
||||
}
|
||||
_itemsData = sb.ToString().ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderGroupLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_groupLabelData);
|
||||
protected override void RenderDirection(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_directionData);
|
||||
protected override void RenderItems(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_itemsData);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<div class="$$Classes$$" role="separator" aria-orientation="$$Orientation$$"></div>
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Separator component.
|
||||
/// Orientation: horizontal | vertical
|
||||
/// </summary>
|
||||
public sealed class Separator : SeparatorBase
|
||||
{
|
||||
private readonly byte[] _classesData;
|
||||
private readonly byte[] _orientationData;
|
||||
|
||||
public Separator(string orientation = "horizontal", string extraClasses = "")
|
||||
{
|
||||
var cls = orientation == "vertical"
|
||||
? $"inline-block h-full w-px bg-border {extraClasses}"
|
||||
: $"block h-px w-full bg-border {extraClasses}";
|
||||
|
||||
_classesData = cls.Trim().ToUtf8Bytes();
|
||||
_orientationData = orientation.ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_classesData);
|
||||
protected override void RenderOrientation(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_orientationData);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<div class="animate-pulse rounded-md bg-muted $$Classes$$"></div>
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Skeleton loading placeholder.
|
||||
/// Pass size classes via the classes parameter, e.g. "h-4 w-48" or "h-10 w-full".
|
||||
/// </summary>
|
||||
public sealed class Skeleton : SkeletonBase
|
||||
{
|
||||
private readonly byte[] _classesData;
|
||||
|
||||
public Skeleton(string classes = "h-4 w-full")
|
||||
{
|
||||
_classesData = classes.ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_classesData);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
$$Label$$
|
||||
<input
|
||||
type="range"
|
||||
id="$$Id$$"
|
||||
name="$$Name$$"
|
||||
min="$$Min$$"
|
||||
max="$$Max$$"
|
||||
step="$$Step$$"
|
||||
value="$$Value$$"
|
||||
class="h-2 w-full cursor-pointer appearance-none rounded-full bg-secondary accent-primary
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
||||
focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 $$ExtraClasses$$"
|
||||
$$HxAttrs$$
|
||||
/>
|
||||
$$Description$$
|
||||
</div>
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Slider (range input) with optional label and description.
|
||||
/// </summary>
|
||||
public sealed class Slider : SliderBase
|
||||
{
|
||||
private readonly byte[] _idData;
|
||||
private readonly byte[] _nameData;
|
||||
private readonly byte[] _minData;
|
||||
private readonly byte[] _maxData;
|
||||
private readonly byte[] _stepData;
|
||||
private readonly byte[] _valueData;
|
||||
private readonly byte[] _labelData;
|
||||
private readonly byte[] _descriptionData;
|
||||
private readonly byte[] _extraClassesData;
|
||||
private readonly byte[] _hxAttrsData;
|
||||
|
||||
public Slider(
|
||||
string id,
|
||||
string name = "",
|
||||
int min = 0,
|
||||
int max = 100,
|
||||
int step = 1,
|
||||
int value = 50,
|
||||
string label = "",
|
||||
string description = "",
|
||||
string extraClasses = "",
|
||||
string hxAttrs = "")
|
||||
{
|
||||
_idData = id.ToUtf8Bytes();
|
||||
_nameData = (string.IsNullOrEmpty(name) ? id : name).ToUtf8Bytes();
|
||||
_minData = min.ToString().ToUtf8Bytes();
|
||||
_maxData = max.ToString().ToUtf8Bytes();
|
||||
_stepData = step.ToString().ToUtf8Bytes();
|
||||
_valueData = value.ToString().ToUtf8Bytes();
|
||||
_extraClassesData = extraClasses.ToUtf8Bytes();
|
||||
_hxAttrsData = hxAttrs.ToUtf8Bytes();
|
||||
|
||||
_labelData = string.IsNullOrEmpty(label)
|
||||
? []
|
||||
: $"""<label for="{id}" class="text-sm font-medium leading-none">{label}</label>""".ToUtf8Bytes();
|
||||
|
||||
_descriptionData = string.IsNullOrEmpty(description)
|
||||
? []
|
||||
: $"""<p class="text-xs text-muted-foreground">{description}</p>""".ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
|
||||
protected override void RenderName(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nameData);
|
||||
protected override void RenderMin(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_minData);
|
||||
protected override void RenderMax(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_maxData);
|
||||
protected override void RenderStep(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_stepData);
|
||||
protected override void RenderValue(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_valueData);
|
||||
protected override void RenderLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_labelData);
|
||||
protected override void RenderDescription(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_descriptionData);
|
||||
protected override void RenderExtraClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_extraClassesData);
|
||||
protected override void RenderHxAttrs(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_hxAttrsData);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<label class="inline-flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" id="$$Id$$" name="$$Name$$" $$Checked$$
|
||||
value="true" class="sr-only switch-checkbox" />
|
||||
<span class="switch-track relative inline-flex h-6 w-11 shrink-0 items-center rounded-full
|
||||
bg-input transition-colors duration-200">
|
||||
<span class="switch-thumb pointer-events-none block h-5 w-5 rounded-full bg-white
|
||||
shadow-lg ring-0 transition-transform duration-200 translate-x-0.5"></span>
|
||||
</span>
|
||||
$$Label$$
|
||||
</label>
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Switch (toggle). Rendered as a styled checkbox.
|
||||
/// JS in components.js handles the visual on/off state.
|
||||
/// </summary>
|
||||
public sealed class Switch : SwitchBase
|
||||
{
|
||||
private readonly byte[] _idData;
|
||||
private readonly byte[] _nameData;
|
||||
private readonly byte[] _checkedData;
|
||||
private readonly byte[] _labelData;
|
||||
|
||||
public Switch(
|
||||
string id,
|
||||
string label = "",
|
||||
string name = "",
|
||||
bool isChecked = false)
|
||||
{
|
||||
_idData = id.ToUtf8Bytes();
|
||||
_nameData = (string.IsNullOrEmpty(name) ? id : name).ToUtf8Bytes();
|
||||
_checkedData = (isChecked ? "checked" : "").ToUtf8Bytes();
|
||||
_labelData = string.IsNullOrEmpty(label)
|
||||
? []
|
||||
: $"""<span class="text-sm font-medium leading-none">{label}</span>""".ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
|
||||
protected override void RenderName(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nameData);
|
||||
protected override void RenderChecked(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_checkedData);
|
||||
protected override void RenderLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_labelData);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="w-full overflow-auto rounded-md border border-border">
|
||||
<table class="w-full caption-bottom text-sm">
|
||||
$$Caption$$
|
||||
<thead>
|
||||
<tr class="border-b border-border bg-muted/50 transition-colors">
|
||||
$$Headers$$
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
$$Rows$$
|
||||
</tbody>
|
||||
$$Footer$$
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Table component.
|
||||
/// Headers: column header strings.
|
||||
/// Rows: each row is an IEnumerable of cell strings.
|
||||
/// Caption and Footer are optional.
|
||||
/// </summary>
|
||||
public sealed class Table : TableBase
|
||||
{
|
||||
private readonly byte[] _captionData;
|
||||
private readonly byte[] _headersData;
|
||||
private readonly byte[] _rowsData;
|
||||
private readonly byte[] _footerData;
|
||||
|
||||
public Table(
|
||||
IEnumerable<string> headers,
|
||||
IEnumerable<IEnumerable<string>> rows,
|
||||
string caption = "",
|
||||
string footer = "")
|
||||
{
|
||||
_captionData = string.IsNullOrEmpty(caption)
|
||||
? []
|
||||
: $"""<caption class="mt-4 text-sm text-muted-foreground">{caption}</caption>""".ToUtf8Bytes();
|
||||
|
||||
var hSb = new System.Text.StringBuilder();
|
||||
foreach (var h in headers)
|
||||
hSb.Append($"""<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0">{h}</th>""");
|
||||
_headersData = hSb.ToString().ToUtf8Bytes();
|
||||
|
||||
var rSb = new System.Text.StringBuilder();
|
||||
foreach (var row in rows)
|
||||
{
|
||||
rSb.Append("""<tr class="border-b border-border transition-colors hover:bg-muted/50">""");
|
||||
foreach (var cell in row)
|
||||
rSb.Append($"""<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0">{cell}</td>""");
|
||||
rSb.Append("</tr>");
|
||||
}
|
||||
_rowsData = rSb.ToString().ToUtf8Bytes();
|
||||
|
||||
_footerData = string.IsNullOrEmpty(footer)
|
||||
? []
|
||||
: $"""<tfoot><tr class="border-t border-border font-medium"><td colspan="99" class="p-4">{footer}</td></tr></tfoot>""".ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderCaption(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_captionData);
|
||||
protected override void RenderHeaders(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_headersData);
|
||||
protected override void RenderRows(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_rowsData);
|
||||
protected override void RenderFooter(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_footerData);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="tabs-root w-full" id="tabs-$$Id$$">
|
||||
<div class="inline-flex h-10 w-full items-center justify-start rounded-md bg-muted p-1 text-muted-foreground"
|
||||
role="tablist">
|
||||
$$TabsList$$
|
||||
</div>
|
||||
$$TabsPanels$$
|
||||
</div>
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Tabs component. Tabs are activated client-side via components.js.
|
||||
/// Pass a list of (Id, Label, Content) tuples.
|
||||
/// </summary>
|
||||
public sealed class Tabs : TabsBase
|
||||
{
|
||||
private readonly byte[] _idData;
|
||||
private readonly byte[] _tabsListData;
|
||||
private readonly byte[] _tabsPanelsData;
|
||||
|
||||
private const string TriggerBase =
|
||||
"tabs-trigger inline-flex items-center justify-center whitespace-nowrap rounded-sm " +
|
||||
"px-3 py-1.5 text-sm font-medium ring-offset-background transition-all " +
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring " +
|
||||
"focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
|
||||
|
||||
private const string PanelBase =
|
||||
"tabs-panel mt-2 ring-offset-background " +
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2";
|
||||
|
||||
public Tabs(string id, IEnumerable<(string Id, string Label, string Content)> tabs)
|
||||
{
|
||||
_idData = id.ToUtf8Bytes();
|
||||
|
||||
var tabList = tabs.ToList();
|
||||
var triggerSb = new System.Text.StringBuilder();
|
||||
var panelSb = new System.Text.StringBuilder();
|
||||
|
||||
foreach (var (tabId, label, content) in tabList)
|
||||
{
|
||||
triggerSb.Append($"""
|
||||
<button type="button" role="tab" aria-selected="false" aria-controls="tabpanel-{tabId}"
|
||||
class="{TriggerBase}">
|
||||
{label}
|
||||
</button>
|
||||
""");
|
||||
|
||||
panelSb.Append($"""
|
||||
<div id="tabpanel-{tabId}" role="tabpanel" class="{PanelBase}">
|
||||
{content}
|
||||
</div>
|
||||
""");
|
||||
}
|
||||
|
||||
_tabsListData = triggerSb.ToString().ToUtf8Bytes();
|
||||
_tabsPanelsData = panelSb.ToString().ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
|
||||
protected override void RenderTabsList(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_tabsListData);
|
||||
protected override void RenderTabsPanels(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_tabsPanelsData);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="flex flex-col gap-1.5">
|
||||
$$Label$$
|
||||
<textarea
|
||||
id="$$Id$$"
|
||||
name="$$Name$$"
|
||||
rows="$$Rows$$"
|
||||
placeholder="$$Placeholder$$"
|
||||
class="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm
|
||||
ring-offset-background placeholder:text-muted-foreground
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
||||
focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50
|
||||
resize-y min-h-20 $$ExtraClasses$$"
|
||||
$$HxAttrs$$>$$DefaultValue$$</textarea>
|
||||
$$Description$$
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Textarea component with optional label and description.
|
||||
/// </summary>
|
||||
public sealed class Textarea : TextareaBase
|
||||
{
|
||||
private readonly byte[] _idData;
|
||||
private readonly byte[] _nameData;
|
||||
private readonly byte[] _rowsData;
|
||||
private readonly byte[] _placeholderData;
|
||||
private readonly byte[] _labelData;
|
||||
private readonly byte[] _descriptionData;
|
||||
private readonly byte[] _defaultValueData;
|
||||
private readonly byte[] _extraClassesData;
|
||||
private readonly byte[] _hxAttrsData;
|
||||
|
||||
public Textarea(
|
||||
string id,
|
||||
string name = "",
|
||||
string placeholder = "",
|
||||
string label = "",
|
||||
string description = "",
|
||||
string defaultValue = "",
|
||||
string extraClasses = "",
|
||||
string hxAttrs = "",
|
||||
int rows = 4)
|
||||
{
|
||||
_idData = id.ToUtf8Bytes();
|
||||
_nameData = (string.IsNullOrEmpty(name) ? id : name).ToUtf8Bytes();
|
||||
_rowsData = rows.ToString().ToUtf8Bytes();
|
||||
_placeholderData = placeholder.ToUtf8Bytes();
|
||||
_extraClassesData = extraClasses.ToUtf8Bytes();
|
||||
_hxAttrsData = hxAttrs.ToUtf8Bytes();
|
||||
_defaultValueData = defaultValue.ToUtf8Bytes();
|
||||
|
||||
_labelData = string.IsNullOrEmpty(label)
|
||||
? []
|
||||
: $"""<label for="{id}" class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{label}</label>""".ToUtf8Bytes();
|
||||
|
||||
_descriptionData = string.IsNullOrEmpty(description)
|
||||
? []
|
||||
: $"""<p class="text-xs text-muted-foreground">{description}</p>""".ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
|
||||
protected override void RenderName(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nameData);
|
||||
protected override void RenderRows(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_rowsData);
|
||||
protected override void RenderPlaceholder(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_placeholderData);
|
||||
protected override void RenderLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_labelData);
|
||||
protected override void RenderDescription(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_descriptionData);
|
||||
protected override void RenderDefaultValue(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_defaultValueData);
|
||||
protected override void RenderExtraClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_extraClassesData);
|
||||
protected override void RenderHxAttrs(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_hxAttrsData);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="toast-item pointer-events-auto relative flex w-full items-center justify-between
|
||||
space-x-4 overflow-hidden rounded-md border border-border bg-background p-4
|
||||
shadow-lg transition-all $$ExtraClasses$$"
|
||||
role="alert">
|
||||
<div class="grid gap-1">
|
||||
$$Title$$
|
||||
$$Description$$
|
||||
</div>
|
||||
<button type="button"
|
||||
class="toast-close inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md
|
||||
text-muted-foreground hover:text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
aria-label="Dismiss">
|
||||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// shadcn-style Toast notification. Typically created dynamically via window.showToast(),
|
||||
/// but can also be server-rendered and injected via htmx.
|
||||
/// </summary>
|
||||
public sealed class Toast : ToastBase
|
||||
{
|
||||
private static readonly Dictionary<string, string> VariantClasses = new()
|
||||
{
|
||||
["default"] = "",
|
||||
["destructive"] = "border-destructive text-destructive",
|
||||
};
|
||||
|
||||
private readonly byte[] _titleData;
|
||||
private readonly byte[] _descriptionData;
|
||||
private readonly byte[] _extraClassesData;
|
||||
|
||||
public Toast(
|
||||
string title,
|
||||
string description = "",
|
||||
string variant = "default")
|
||||
{
|
||||
_titleData = $"""<div class="text-sm font-semibold">{title}</div>""".ToUtf8Bytes();
|
||||
_descriptionData = string.IsNullOrEmpty(description)
|
||||
? []
|
||||
: $"""<div class="text-sm opacity-90">{description}</div>""".ToUtf8Bytes();
|
||||
_extraClassesData = VariantClasses.GetValueOrDefault(variant, "").ToUtf8Bytes();
|
||||
}
|
||||
|
||||
protected override void RenderTitle(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_titleData);
|
||||
protected override void RenderDescription(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_descriptionData);
|
||||
protected override void RenderExtraClasses(HtmxRenderContext ctx)=> ctx.Writer.WriteUtf8(_extraClassesData);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="fixed bottom-4 right-4 z-[100] flex max-h-screen w-full flex-col-reverse gap-2 p-4 sm:flex-col md:max-w-[420px] pointer-events-none toast-viewport"
|
||||
id="$$Id$$">
|
||||
$$Toasts$$
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Fixed viewport container for toast notifications.
|
||||
/// Place once in the page (or layout). Toasts appear via window.showToast() from components.js.
|
||||
/// </summary>
|
||||
public sealed class ToastViewport : ToastViewportBase
|
||||
{
|
||||
private readonly byte[] _idData;
|
||||
private readonly byte[] _toastsData;
|
||||
|
||||
public ToastViewport(string id = "toast-viewport")
|
||||
{
|
||||
_idData = id.ToUtf8Bytes();
|
||||
_toastsData = [];
|
||||
}
|
||||
|
||||
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
|
||||
protected override void RenderToasts(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_toastsData);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<span class="group relative inline-flex items-center">
|
||||
$$Trigger$$
|
||||
<span class="pointer-events-none absolute $$Position$$ z-50 w-max max-w-xs
|
||||
rounded-md bg-popover px-3 py-1.5 text-xs text-popover-foreground
|
||||
shadow-md border border-border whitespace-nowrap
|
||||
opacity-0 group-hover:opacity-100 transition-opacity duration-150">
|
||||
$$Text$$
|
||||
</span>
|
||||
</span>
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace Htmx.ApiDemo.Templates.Components;
|
||||
|
||||
/// <summary>
|
||||
/// CSS-only Tooltip using group-hover. Wraps a trigger element.
|
||||
/// Position: "top" | "bottom" | "left" | "right" (default: top)
|
||||
/// </summary>
|
||||
public sealed class Tooltip : TooltipBase
|
||||
{
|
||||
private static readonly Dictionary<string, string> PositionClasses = new()
|
||||
{
|
||||
["top"] = "bottom-full left-1/2 -translate-x-1/2 mb-2",
|
||||
["bottom"] = "top-full left-1/2 -translate-x-1/2 mt-2",
|
||||
["left"] = "right-full top-1/2 -translate-y-1/2 mr-2",
|
||||
["right"] = "left-full top-1/2 -translate-y-1/2 ml-2",
|
||||
};
|
||||
|
||||
private readonly byte[] _triggerData;
|
||||
private readonly byte[] _textData;
|
||||
private readonly byte[] _positionData;
|
||||
|
||||
public Tooltip(string text, IHtmxComponent trigger, string position = "top")
|
||||
{
|
||||
_textData = text.ToUtf8Bytes();
|
||||
_positionData = PositionClasses.GetValueOrDefault(position, PositionClasses["top"]).ToUtf8Bytes();
|
||||
|
||||
var bufferWriter = new System.IO.Pipelines.Pipe().Writer;
|
||||
// Render trigger to bytes via a simple ArrayBufferWriter
|
||||
var writer = new System.Buffers.ArrayBufferWriter<byte>();
|
||||
trigger.Render(new HtmxRenderContext(writer));
|
||||
_triggerData = writer.WrittenSpan.ToArray();
|
||||
}
|
||||
|
||||
protected override void RenderTrigger(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_triggerData);
|
||||
protected override void RenderText(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_textData);
|
||||
protected override void RenderPosition(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_positionData);
|
||||
}
|
||||
Reference in New Issue
Block a user