Created more components
This commit is contained in:
@@ -4,8 +4,8 @@
|
|||||||
"http": {
|
"http": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": false,
|
||||||
"launchUrl": "todos",
|
"launchUrl": "/",
|
||||||
"applicationUrl": "http://localhost:5120",
|
"applicationUrl": "http://localhost:5120",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -65,4 +65,256 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Badge ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Badge</h2>
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
$$BadgeDefault$$
|
||||||
|
$$BadgeSecondary$$
|
||||||
|
$$BadgeDestructive$$
|
||||||
|
$$BadgeOutline$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Card ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Card</h2>
|
||||||
|
<div class="max-w-sm">
|
||||||
|
$$CardDemo$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Separator ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Separator</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm text-muted-foreground">Horizontal</p>
|
||||||
|
$$SeparatorH$$
|
||||||
|
<p class="text-sm text-muted-foreground">Vertical (inline)</p>
|
||||||
|
<div class="flex items-center gap-3 h-5 text-sm">
|
||||||
|
<span>Section</span>
|
||||||
|
$$SeparatorV$$
|
||||||
|
<span>Another</span>
|
||||||
|
$$SeparatorV$$
|
||||||
|
<span>More</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Skeleton ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Skeleton</h2>
|
||||||
|
<div class="flex flex-col gap-3 max-w-xs">
|
||||||
|
$$SkeletonTitle$$
|
||||||
|
$$SkeletonLine1$$
|
||||||
|
$$SkeletonLine2$$
|
||||||
|
$$SkeletonAvatar$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Avatar ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Avatar</h2>
|
||||||
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
|
$$AvatarSm$$
|
||||||
|
$$AvatarDefault$$
|
||||||
|
$$AvatarLg$$
|
||||||
|
$$AvatarImg$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Progress ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Progress</h2>
|
||||||
|
<div class="flex flex-col gap-4 max-w-md">
|
||||||
|
$$Progress25$$
|
||||||
|
$$Progress60$$
|
||||||
|
$$Progress100$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Alert ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Alert</h2>
|
||||||
|
<div class="flex flex-col gap-3 max-w-lg">
|
||||||
|
$$AlertDefault$$
|
||||||
|
$$AlertDestructive$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Breadcrumb ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Breadcrumb</h2>
|
||||||
|
$$BreadcrumbDemo$$
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Checkbox ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Checkbox</h2>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
$$CheckboxAccept$$
|
||||||
|
$$CheckboxChecked$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── RadioGroup ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Radio Group</h2>
|
||||||
|
<div class="flex flex-wrap gap-8">
|
||||||
|
$$RadioGroupCol$$
|
||||||
|
$$RadioGroupRow$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Switch ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Switch</h2>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
$$SwitchOff$$
|
||||||
|
$$SwitchOn$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Textarea ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Textarea</h2>
|
||||||
|
<div class="max-w-md">
|
||||||
|
$$TextareaDemo$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Slider ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Slider</h2>
|
||||||
|
<div class="flex flex-col gap-4 max-w-md">
|
||||||
|
$$SliderDemo$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── File Input ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">File Input</h2>
|
||||||
|
<div class="max-w-md">
|
||||||
|
$$FileInputDemo$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Table ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Table</h2>
|
||||||
|
$$TableDemo$$
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Pagination ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Pagination</h2>
|
||||||
|
$$PaginationDemo$$
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Tabs ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Tabs</h2>
|
||||||
|
<div class="max-w-lg">
|
||||||
|
$$TabsDemo$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Accordion ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Accordion</h2>
|
||||||
|
<div class="max-w-lg">
|
||||||
|
$$AccordionDemo$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Tooltip ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Tooltip</h2>
|
||||||
|
<div class="flex flex-wrap gap-6 items-center pt-4">
|
||||||
|
$$TooltipTop$$
|
||||||
|
$$TooltipBottom$$
|
||||||
|
$$TooltipRight$$
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Toast ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Toast</h2>
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<button type="button"
|
||||||
|
onclick="showToast({ title: 'Saved!', description: 'Your changes have been saved.' })"
|
||||||
|
class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground h-10 px-4 py-2 text-sm font-medium transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||||
|
Show Toast
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
onclick="showToast({ title: 'Error', description: 'Something went wrong.', variant: 'destructive' })"
|
||||||
|
class="inline-flex items-center justify-center rounded-md bg-destructive text-destructive-foreground h-10 px-4 py-2 text-sm font-medium transition-colors hover:bg-destructive/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||||
|
Show Error Toast
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Dialog ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Dialog</h2>
|
||||||
|
<button type="button" data-dialog-open="demo-dialog"
|
||||||
|
class="inline-flex items-center justify-center rounded-md border border-input bg-background h-10 px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||||
|
Open Dialog
|
||||||
|
</button>
|
||||||
|
$$DialogDemo$$
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="border-border" />
|
||||||
|
|
||||||
|
<!-- ── Dropdown Menu ── -->
|
||||||
|
<section>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-foreground">Dropdown Menu</h2>
|
||||||
|
$$DropdownDemo$$
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Toast viewport (fixed, outside the flow) -->
|
||||||
|
$$ToastViewportDemo$$
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace Htmx.ApiDemo.Templates;
|
|||||||
|
|
||||||
public sealed class UiDemo : UiDemoBase
|
public sealed class UiDemo : UiDemoBase
|
||||||
{
|
{
|
||||||
|
// ── Buttons ──────────────────────────────────────────────────────────
|
||||||
public IHtmxComponent BtnDefault { get; }
|
public IHtmxComponent BtnDefault { get; }
|
||||||
public IHtmxComponent BtnDestructive { get; }
|
public IHtmxComponent BtnDestructive { get; }
|
||||||
public IHtmxComponent BtnOutline { get; }
|
public IHtmxComponent BtnOutline { get; }
|
||||||
@@ -15,19 +16,106 @@ public sealed class UiDemo : UiDemoBase
|
|||||||
public IHtmxComponent BtnSm { get; }
|
public IHtmxComponent BtnSm { get; }
|
||||||
public IHtmxComponent BtnLg { get; }
|
public IHtmxComponent BtnLg { get; }
|
||||||
|
|
||||||
|
// ── Inputs ───────────────────────────────────────────────────────────
|
||||||
public IHtmxComponent InputText { get; }
|
public IHtmxComponent InputText { get; }
|
||||||
public IHtmxComponent InputEmail { get; }
|
public IHtmxComponent InputEmail { get; }
|
||||||
public IHtmxComponent InputPassword { get; }
|
public IHtmxComponent InputPassword { get; }
|
||||||
public IHtmxComponent InputSearch { get; }
|
public IHtmxComponent InputSearch { get; }
|
||||||
|
|
||||||
|
// ── Select / Calendar / TimePicker ───────────────────────────────────
|
||||||
public IHtmxComponent SelectDemo { get; }
|
public IHtmxComponent SelectDemo { get; }
|
||||||
public IHtmxComponent CalendarDemo { get; }
|
public IHtmxComponent CalendarDemo { get; }
|
||||||
public IHtmxComponent CalendarRangeDemo{ get; }
|
public IHtmxComponent CalendarRangeDemo { get; }
|
||||||
public IHtmxComponent TimePickerDemo { get; }
|
public IHtmxComponent TimePickerDemo { get; }
|
||||||
public IHtmxComponent TimePicker12hDemo { get; }
|
public IHtmxComponent TimePicker12hDemo { get; }
|
||||||
|
|
||||||
|
// ── Badge ─────────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent BadgeDefault { get; }
|
||||||
|
public IHtmxComponent BadgeSecondary { get; }
|
||||||
|
public IHtmxComponent BadgeDestructive { get; }
|
||||||
|
public IHtmxComponent BadgeOutline { get; }
|
||||||
|
|
||||||
|
// ── Card ──────────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent CardDemo { get; }
|
||||||
|
|
||||||
|
// ── Separator ─────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent SeparatorH { get; }
|
||||||
|
public IHtmxComponent SeparatorV { get; }
|
||||||
|
|
||||||
|
// ── Skeleton ──────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent SkeletonTitle { get; }
|
||||||
|
public IHtmxComponent SkeletonLine1 { get; }
|
||||||
|
public IHtmxComponent SkeletonLine2 { get; }
|
||||||
|
public IHtmxComponent SkeletonAvatar { get; }
|
||||||
|
|
||||||
|
// ── Avatar ────────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent AvatarSm { get; }
|
||||||
|
public IHtmxComponent AvatarDefault { get; }
|
||||||
|
public IHtmxComponent AvatarLg { get; }
|
||||||
|
public IHtmxComponent AvatarImg { get; }
|
||||||
|
|
||||||
|
// ── Progress ──────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent Progress25 { get; }
|
||||||
|
public IHtmxComponent Progress60 { get; }
|
||||||
|
public IHtmxComponent Progress100 { get; }
|
||||||
|
|
||||||
|
// ── Alert ─────────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent AlertDefault { get; }
|
||||||
|
public IHtmxComponent AlertDestructive { get; }
|
||||||
|
|
||||||
|
// ── Breadcrumb ────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent BreadcrumbDemo { get; }
|
||||||
|
|
||||||
|
// ── Checkbox ──────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent CheckboxAccept { get; }
|
||||||
|
public IHtmxComponent CheckboxChecked { get; }
|
||||||
|
|
||||||
|
// ── RadioGroup ────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent RadioGroupCol { get; }
|
||||||
|
public IHtmxComponent RadioGroupRow { get; }
|
||||||
|
|
||||||
|
// ── Switch ────────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent SwitchOff { get; }
|
||||||
|
public IHtmxComponent SwitchOn { get; }
|
||||||
|
|
||||||
|
// ── Textarea ──────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent TextareaDemo { get; }
|
||||||
|
|
||||||
|
// ── Slider ────────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent SliderDemo { get; }
|
||||||
|
|
||||||
|
// ── FileInput ─────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent FileInputDemo { get; }
|
||||||
|
|
||||||
|
// ── Table ─────────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent TableDemo { get; }
|
||||||
|
|
||||||
|
// ── Pagination ────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent PaginationDemo { get; }
|
||||||
|
|
||||||
|
// ── Tabs ──────────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent TabsDemo { get; }
|
||||||
|
|
||||||
|
// ── Accordion ─────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent AccordionDemo { get; }
|
||||||
|
|
||||||
|
// ── Tooltip ───────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent TooltipTop { get; }
|
||||||
|
public IHtmxComponent TooltipBottom { get; }
|
||||||
|
public IHtmxComponent TooltipRight { get; }
|
||||||
|
|
||||||
|
// ── Dialog ────────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent DialogDemo { get; }
|
||||||
|
|
||||||
|
// ── Dropdown ──────────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent DropdownDemo { get; }
|
||||||
|
|
||||||
|
// ── Toast Viewport ────────────────────────────────────────────────────
|
||||||
|
public IHtmxComponent ToastViewportDemo { get; }
|
||||||
|
|
||||||
public UiDemo()
|
public UiDemo()
|
||||||
{
|
{
|
||||||
|
// Buttons
|
||||||
BtnDefault = new Button("Default");
|
BtnDefault = new Button("Default");
|
||||||
BtnDestructive = new Button("Destructive", variant: "destructive");
|
BtnDestructive = new Button("Destructive", variant: "destructive");
|
||||||
BtnOutline = new Button("Outline", variant: "outline");
|
BtnOutline = new Button("Outline", variant: "outline");
|
||||||
@@ -37,12 +125,14 @@ public sealed class UiDemo : UiDemoBase
|
|||||||
BtnSm = new Button("Small", size: "sm");
|
BtnSm = new Button("Small", size: "sm");
|
||||||
BtnLg = new Button("Large", size: "lg");
|
BtnLg = new Button("Large", size: "lg");
|
||||||
|
|
||||||
|
// Inputs
|
||||||
InputText = new Input("username", label: "Username", placeholder: "Enter username");
|
InputText = new Input("username", label: "Username", placeholder: "Enter username");
|
||||||
InputEmail = new Input("email", inputType: "email", label: "Email", placeholder: "you@example.com");
|
InputEmail = new Input("email", inputType: "email", label: "Email", placeholder: "you@example.com");
|
||||||
InputPassword = new Input("password", inputType: "password", label: "Password", placeholder: "••••••••");
|
InputPassword = new Input("password", inputType: "password", label: "Password", placeholder: "••••••••");
|
||||||
InputSearch = new Input("search", inputType: "search", label: "Search", placeholder: "Search…",
|
InputSearch = new Input("search", inputType: "search", label: "Search", placeholder: "Search…",
|
||||||
hxAttrs: "hx-get=\"/search\" hx-trigger=\"keyup changed delay:300ms\" hx-target=\"#search-results\"");
|
hxAttrs: "hx-get=\"/search\" hx-trigger=\"keyup changed delay:300ms\" hx-target=\"#search-results\"");
|
||||||
|
|
||||||
|
// Select / Calendar / TimePicker
|
||||||
SelectDemo = new Select(
|
SelectDemo = new Select(
|
||||||
id: "framework",
|
id: "framework",
|
||||||
label: "Framework",
|
label: "Framework",
|
||||||
@@ -52,9 +142,151 @@ public sealed class UiDemo : UiDemoBase
|
|||||||
|
|
||||||
CalendarDemo = new Calendar(id: "demo-cal", name: "demo-date");
|
CalendarDemo = new Calendar(id: "demo-cal", name: "demo-date");
|
||||||
CalendarRangeDemo = new CalendarRange(id: "demo-calr", name: "demo-range");
|
CalendarRangeDemo = new CalendarRange(id: "demo-calr", name: "demo-range");
|
||||||
|
|
||||||
TimePickerDemo = new TimePicker(name: "time-24h", label: "Time (24h)");
|
TimePickerDemo = new TimePicker(name: "time-24h", label: "Time (24h)");
|
||||||
TimePicker12hDemo = new TimePicker(name: "time-12h", label: "Time (12h)", use12h: true);
|
TimePicker12hDemo = new TimePicker(name: "time-12h", label: "Time (12h)", use12h: true);
|
||||||
|
|
||||||
|
// Badge
|
||||||
|
BadgeDefault = new Badge("Default");
|
||||||
|
BadgeSecondary = new Badge("Secondary", variant: "secondary");
|
||||||
|
BadgeDestructive = new Badge("Destructive", variant: "destructive");
|
||||||
|
BadgeOutline = new Badge("Outline", variant: "outline");
|
||||||
|
|
||||||
|
// Card
|
||||||
|
CardDemo = new Card(
|
||||||
|
title: "Component Card",
|
||||||
|
description: "A reusable card surface with header and footer.",
|
||||||
|
content: "<p class=\"text-sm text-muted-foreground\">Cards group related content and provide a contained, elevated surface for information.</p>",
|
||||||
|
footer: "<button type=\"button\" class=\"inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground h-9 px-4 text-sm font-medium hover:bg-primary/90 transition-colors\">Action</button>");
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
SeparatorH = new Separator();
|
||||||
|
SeparatorV = new Separator(orientation: "vertical");
|
||||||
|
|
||||||
|
// Skeleton
|
||||||
|
SkeletonTitle = new Skeleton("h-5 w-48");
|
||||||
|
SkeletonLine1 = new Skeleton("h-4 w-full");
|
||||||
|
SkeletonLine2 = new Skeleton("h-4 w-3/4");
|
||||||
|
SkeletonAvatar = new Skeleton("h-10 w-10 rounded-full");
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
AvatarSm = new Avatar("SM", size: "sm");
|
||||||
|
AvatarDefault = new Avatar("JD");
|
||||||
|
AvatarLg = new Avatar("AB", size: "lg");
|
||||||
|
AvatarImg = new Avatar("GitHub", src: "https://github.com/github.png", size: "default");
|
||||||
|
|
||||||
|
// Progress
|
||||||
|
Progress25 = new Progress(25);
|
||||||
|
Progress60 = new Progress(60);
|
||||||
|
Progress100 = new Progress(100);
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
AlertDefault = new Alert("Information", description: "This is an informational alert with a default style.");
|
||||||
|
AlertDestructive = new Alert("Error", description: "Something went wrong. Please check your input.", variant: "destructive");
|
||||||
|
|
||||||
|
// Breadcrumb
|
||||||
|
BreadcrumbDemo = new Breadcrumb([
|
||||||
|
("Home", "/"),
|
||||||
|
("Components", "/components"),
|
||||||
|
("UI Demo", ""),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Checkbox
|
||||||
|
CheckboxAccept = new Checkbox("accept-terms", label: "Accept terms and conditions");
|
||||||
|
CheckboxChecked = new Checkbox("newsletter", label: "Subscribe to newsletter", @checked: true);
|
||||||
|
|
||||||
|
// RadioGroup
|
||||||
|
RadioGroupCol = new RadioGroup(
|
||||||
|
name: "plan-v",
|
||||||
|
label: "Plan (vertical)",
|
||||||
|
options: [
|
||||||
|
("starter", "Starter", true),
|
||||||
|
("pro", "Pro", false),
|
||||||
|
("enterprise", "Enterprise", false),
|
||||||
|
]);
|
||||||
|
RadioGroupRow = new RadioGroup(
|
||||||
|
name: "size-h",
|
||||||
|
label: "Size (horizontal)",
|
||||||
|
direction: "flex-row",
|
||||||
|
options: [
|
||||||
|
("sm", "SM", true),
|
||||||
|
("md", "MD", false),
|
||||||
|
("lg", "LG", false),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Switch
|
||||||
|
SwitchOff = new Switch("notif-off", label: "Notifications");
|
||||||
|
SwitchOn = new Switch("darkmode", label: "Dark mode", isChecked: true);
|
||||||
|
|
||||||
|
// Textarea
|
||||||
|
TextareaDemo = new Textarea(
|
||||||
|
id: "bio",
|
||||||
|
label: "Bio",
|
||||||
|
placeholder: "Tell us about yourself…",
|
||||||
|
description: "Max 200 characters.");
|
||||||
|
|
||||||
|
// Slider
|
||||||
|
SliderDemo = new Slider(id: "volume", label: "Volume", value: 40, description: "Drag to adjust");
|
||||||
|
|
||||||
|
// FileInput
|
||||||
|
FileInputDemo = new FileInput(
|
||||||
|
id: "avatar-upload",
|
||||||
|
label: "Profile picture",
|
||||||
|
accept: "image/*",
|
||||||
|
description: "PNG, JPG or GIF up to 2 MB.");
|
||||||
|
|
||||||
|
// Table
|
||||||
|
TableDemo = new Table(
|
||||||
|
headers: ["Name", "Role", "Status"],
|
||||||
|
rows: [
|
||||||
|
["Alice", "Admin", "Active"],
|
||||||
|
["Bob", "Editor", "Active"],
|
||||||
|
["Charlie", "Viewer", "Inactive"],
|
||||||
|
],
|
||||||
|
caption: "Team members");
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
PaginationDemo = new Pagination(current: 3, total: 7, urlPattern: "/ui-demo?page={0}");
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
TabsDemo = new Tabs("demo", [
|
||||||
|
("overview", "Overview", "<p class=\"text-sm text-muted-foreground\">This is the overview tab content.</p>"),
|
||||||
|
("settings", "Settings", "<p class=\"text-sm text-muted-foreground\">Manage your settings here.</p>"),
|
||||||
|
("billing", "Billing", "<p class=\"text-sm text-muted-foreground\">View billing information.</p>"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Accordion
|
||||||
|
AccordionDemo = new Accordion("demo-acc", [
|
||||||
|
("What is htmx?", "htmx allows you to access AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML."),
|
||||||
|
("Is it production ready?", "Yes — htmx is used by thousands of projects in production worldwide."),
|
||||||
|
("Does it replace React?", "htmx is a different tool suited to server-driven UIs. Use whatever fits your team best."),
|
||||||
|
], openIndex: 0);
|
||||||
|
|
||||||
|
// Tooltip
|
||||||
|
TooltipTop = new Tooltip("Top tooltip", new Button("Hover me"), position: "top");
|
||||||
|
TooltipBottom = new Tooltip("Bottom tooltip", new Button("Bottom", variant: "secondary"), position: "bottom");
|
||||||
|
TooltipRight = new Tooltip("Right tooltip", new Button("Right", variant: "outline"), position: "right");
|
||||||
|
|
||||||
|
// Dialog
|
||||||
|
DialogDemo = new Dialog(
|
||||||
|
id: "demo-dialog",
|
||||||
|
title: "Are you sure?",
|
||||||
|
description: "This action cannot be undone.",
|
||||||
|
content: "Please confirm that you want to proceed with this operation.",
|
||||||
|
footer: "<button type=\"button\" data-dialog-close class=\"inline-flex items-center justify-center rounded-md border border-input bg-background h-9 px-4 text-sm font-medium hover:bg-accent transition-colors\">Cancel</button>" +
|
||||||
|
"<button type=\"button\" data-dialog-close class=\"inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground h-9 px-4 text-sm font-medium hover:bg-primary/90 transition-colors\">Confirm</button>");
|
||||||
|
|
||||||
|
// Dropdown
|
||||||
|
DropdownDemo = new DropdownMenu(
|
||||||
|
trigger: new Button("Options ▾", variant: "outline"),
|
||||||
|
items: [
|
||||||
|
("Edit", "/edit", false),
|
||||||
|
("Duplicate", "/dup", false),
|
||||||
|
("", "", true), // separator
|
||||||
|
("Delete", "/delete", false),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Toast Viewport
|
||||||
|
ToastViewportDemo = new ToastViewport();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void RenderBtnDefault(HtmxRenderContext ctx) => BtnDefault.Render(ctx);
|
protected override void RenderBtnDefault(HtmxRenderContext ctx) => BtnDefault.Render(ctx);
|
||||||
@@ -76,6 +308,61 @@ public sealed class UiDemo : UiDemoBase
|
|||||||
protected override void RenderCalendarRangeDemo(HtmxRenderContext ctx) => CalendarRangeDemo.Render(ctx);
|
protected override void RenderCalendarRangeDemo(HtmxRenderContext ctx) => CalendarRangeDemo.Render(ctx);
|
||||||
protected override void RenderTimePickerDemo(HtmxRenderContext ctx) => TimePickerDemo.Render(ctx);
|
protected override void RenderTimePickerDemo(HtmxRenderContext ctx) => TimePickerDemo.Render(ctx);
|
||||||
protected override void RenderTimePicker12hDemo(HtmxRenderContext ctx) => TimePicker12hDemo.Render(ctx);
|
protected override void RenderTimePicker12hDemo(HtmxRenderContext ctx) => TimePicker12hDemo.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderBadgeDefault(HtmxRenderContext ctx) => BadgeDefault.Render(ctx);
|
||||||
|
protected override void RenderBadgeSecondary(HtmxRenderContext ctx) => BadgeSecondary.Render(ctx);
|
||||||
|
protected override void RenderBadgeDestructive(HtmxRenderContext ctx) => BadgeDestructive.Render(ctx);
|
||||||
|
protected override void RenderBadgeOutline(HtmxRenderContext ctx) => BadgeOutline.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderCardDemo(HtmxRenderContext ctx) => CardDemo.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderSeparatorH(HtmxRenderContext ctx) => SeparatorH.Render(ctx);
|
||||||
|
protected override void RenderSeparatorV(HtmxRenderContext ctx) => SeparatorV.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderSkeletonTitle(HtmxRenderContext ctx) => SkeletonTitle.Render(ctx);
|
||||||
|
protected override void RenderSkeletonLine1(HtmxRenderContext ctx) => SkeletonLine1.Render(ctx);
|
||||||
|
protected override void RenderSkeletonLine2(HtmxRenderContext ctx) => SkeletonLine2.Render(ctx);
|
||||||
|
protected override void RenderSkeletonAvatar(HtmxRenderContext ctx) => SkeletonAvatar.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderAvatarSm(HtmxRenderContext ctx) => AvatarSm.Render(ctx);
|
||||||
|
protected override void RenderAvatarDefault(HtmxRenderContext ctx) => AvatarDefault.Render(ctx);
|
||||||
|
protected override void RenderAvatarLg(HtmxRenderContext ctx) => AvatarLg.Render(ctx);
|
||||||
|
protected override void RenderAvatarImg(HtmxRenderContext ctx) => AvatarImg.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderProgress25(HtmxRenderContext ctx) => Progress25.Render(ctx);
|
||||||
|
protected override void RenderProgress60(HtmxRenderContext ctx) => Progress60.Render(ctx);
|
||||||
|
protected override void RenderProgress100(HtmxRenderContext ctx) => Progress100.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderAlertDefault(HtmxRenderContext ctx) => AlertDefault.Render(ctx);
|
||||||
|
protected override void RenderAlertDestructive(HtmxRenderContext ctx) => AlertDestructive.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderBreadcrumbDemo(HtmxRenderContext ctx) => BreadcrumbDemo.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderCheckboxAccept(HtmxRenderContext ctx) => CheckboxAccept.Render(ctx);
|
||||||
|
protected override void RenderCheckboxChecked(HtmxRenderContext ctx) => CheckboxChecked.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderRadioGroupCol(HtmxRenderContext ctx) => RadioGroupCol.Render(ctx);
|
||||||
|
protected override void RenderRadioGroupRow(HtmxRenderContext ctx) => RadioGroupRow.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderSwitchOff(HtmxRenderContext ctx) => SwitchOff.Render(ctx);
|
||||||
|
protected override void RenderSwitchOn(HtmxRenderContext ctx) => SwitchOn.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderTextareaDemo(HtmxRenderContext ctx) => TextareaDemo.Render(ctx);
|
||||||
|
protected override void RenderSliderDemo(HtmxRenderContext ctx) => SliderDemo.Render(ctx);
|
||||||
|
protected override void RenderFileInputDemo(HtmxRenderContext ctx)=> FileInputDemo.Render(ctx);
|
||||||
|
protected override void RenderTableDemo(HtmxRenderContext ctx) => TableDemo.Render(ctx);
|
||||||
|
protected override void RenderPaginationDemo(HtmxRenderContext ctx)=> PaginationDemo.Render(ctx);
|
||||||
|
protected override void RenderTabsDemo(HtmxRenderContext ctx) => TabsDemo.Render(ctx);
|
||||||
|
protected override void RenderAccordionDemo(HtmxRenderContext ctx)=> AccordionDemo.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderTooltipTop(HtmxRenderContext ctx) => TooltipTop.Render(ctx);
|
||||||
|
protected override void RenderTooltipBottom(HtmxRenderContext ctx) => TooltipBottom.Render(ctx);
|
||||||
|
protected override void RenderTooltipRight(HtmxRenderContext ctx) => TooltipRight.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderDialogDemo(HtmxRenderContext ctx) => DialogDemo.Render(ctx);
|
||||||
|
protected override void RenderDropdownDemo(HtmxRenderContext ctx) => DropdownDemo.Render(ctx);
|
||||||
|
|
||||||
|
protected override void RenderToastViewportDemo(HtmxRenderContext ctx) => ToastViewportDemo.Render(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@source "../../**/*.{html,htmx,cs}";
|
||||||
|
@source "../../src/**/!(*.g).cs";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-background: hsl(var(--background));
|
--color-background: hsl(var(--background));
|
||||||
--color-foreground: hsl(var(--foreground));
|
--color-foreground: hsl(var(--foreground));
|
||||||
|
|||||||
@@ -8,9 +8,12 @@
|
|||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
"Courier New", monospace;
|
"Courier New", monospace;
|
||||||
--color-black: #000;
|
--color-black: #000;
|
||||||
|
--color-white: #fff;
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
--container-xs: 20rem;
|
--container-xs: 20rem;
|
||||||
--container-sm: 24rem;
|
--container-sm: 24rem;
|
||||||
|
--container-md: 28rem;
|
||||||
|
--container-lg: 32rem;
|
||||||
--container-xl: 36rem;
|
--container-xl: 36rem;
|
||||||
--text-xs: 0.75rem;
|
--text-xs: 0.75rem;
|
||||||
--text-xs--line-height: calc(1 / 0.75);
|
--text-xs--line-height: calc(1 / 0.75);
|
||||||
@@ -22,12 +25,18 @@
|
|||||||
--text-lg--line-height: calc(1.75 / 1.125);
|
--text-lg--line-height: calc(1.75 / 1.125);
|
||||||
--text-2xl: 1.5rem;
|
--text-2xl: 1.5rem;
|
||||||
--text-2xl--line-height: calc(2 / 1.5);
|
--text-2xl--line-height: calc(2 / 1.5);
|
||||||
|
--font-weight-normal: 400;
|
||||||
--font-weight-medium: 500;
|
--font-weight-medium: 500;
|
||||||
--font-weight-semibold: 600;
|
--font-weight-semibold: 600;
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
--tracking-tight: -0.025em;
|
--tracking-tight: -0.025em;
|
||||||
|
--leading-relaxed: 1.625;
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
--blur-sm: 8px;
|
||||||
--default-transition-duration: 150ms;
|
--default-transition-duration: 150ms;
|
||||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
--default-font-family: var(--font-sans);
|
--default-font-family: var(--font-sans);
|
||||||
@@ -35,10 +44,14 @@
|
|||||||
--color-background: hsl(var(--background));
|
--color-background: hsl(var(--background));
|
||||||
--color-foreground: hsl(var(--foreground));
|
--color-foreground: hsl(var(--foreground));
|
||||||
--color-card: hsl(var(--card));
|
--color-card: hsl(var(--card));
|
||||||
|
--color-card-foreground: hsl(var(--card-foreground));
|
||||||
|
--color-popover: hsl(var(--popover));
|
||||||
|
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||||
--color-primary: hsl(var(--primary));
|
--color-primary: hsl(var(--primary));
|
||||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||||
--color-secondary: hsl(var(--secondary));
|
--color-secondary: hsl(var(--secondary));
|
||||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||||
|
--color-muted: hsl(var(--muted));
|
||||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||||
--color-accent: hsl(var(--accent));
|
--color-accent: hsl(var(--accent));
|
||||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||||
@@ -198,12 +211,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
.pointer-events-auto {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
.pointer-events-none {
|
.pointer-events-none {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: inset(50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
.fixed {
|
.fixed {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.static {
|
.static {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
@@ -228,30 +261,108 @@
|
|||||||
.end {
|
.end {
|
||||||
inset-inline-end: var(--spacing);
|
inset-inline-end: var(--spacing);
|
||||||
}
|
}
|
||||||
|
.top-1\/2 {
|
||||||
|
top: calc(1 / 2 * 100%);
|
||||||
|
}
|
||||||
|
.top-full {
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
.right-0 {
|
||||||
|
right: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
.right-4 {
|
||||||
|
right: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.right-full {
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
.bottom-4 {
|
||||||
|
bottom: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.bottom-full {
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
.left-0 {
|
.left-0 {
|
||||||
left: calc(var(--spacing) * 0);
|
left: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
|
.left-1\/2 {
|
||||||
|
left: calc(1 / 2 * 100%);
|
||||||
|
}
|
||||||
|
.left-full {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
.z-20 {
|
.z-20 {
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
.z-30 {
|
.z-30 {
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
|
.z-50 {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
.z-\[100\] {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
@media (width >= 40rem) {
|
||||||
|
max-width: 40rem;
|
||||||
|
}
|
||||||
|
@media (width >= 48rem) {
|
||||||
|
max-width: 48rem;
|
||||||
|
}
|
||||||
|
@media (width >= 64rem) {
|
||||||
|
max-width: 64rem;
|
||||||
|
}
|
||||||
|
@media (width >= 80rem) {
|
||||||
|
max-width: 80rem;
|
||||||
|
}
|
||||||
|
@media (width >= 96rem) {
|
||||||
|
max-width: 96rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.m-0 {
|
||||||
|
margin: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
.-mx-1 {
|
||||||
|
margin-inline: calc(var(--spacing) * -1);
|
||||||
|
}
|
||||||
|
.my-1 {
|
||||||
|
margin-block: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
.mt-1 {
|
.mt-1 {
|
||||||
margin-top: calc(var(--spacing) * 1);
|
margin-top: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
|
.mt-2 {
|
||||||
|
margin-top: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.mt-3 {
|
.mt-3 {
|
||||||
margin-top: calc(var(--spacing) * 3);
|
margin-top: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.mr-2 {
|
||||||
|
margin-right: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.mb-1 {
|
.mb-1 {
|
||||||
margin-bottom: calc(var(--spacing) * 1);
|
margin-bottom: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.mb-3 {
|
.mb-3 {
|
||||||
margin-bottom: calc(var(--spacing) * 3);
|
margin-bottom: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
.mb-4 {
|
.mb-4 {
|
||||||
margin-bottom: calc(var(--spacing) * 4);
|
margin-bottom: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.ml-2 {
|
||||||
|
margin-left: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@@ -270,12 +381,24 @@
|
|||||||
.inline-flex {
|
.inline-flex {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
.aspect-square {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
.h-2 {
|
||||||
|
height: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.h-3\.5 {
|
||||||
|
height: calc(var(--spacing) * 3.5);
|
||||||
|
}
|
||||||
.h-4 {
|
.h-4 {
|
||||||
height: calc(var(--spacing) * 4);
|
height: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
.h-5 {
|
.h-5 {
|
||||||
height: calc(var(--spacing) * 5);
|
height: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.h-6 {
|
||||||
|
height: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.h-8 {
|
.h-8 {
|
||||||
height: calc(var(--spacing) * 8);
|
height: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
@@ -288,18 +411,45 @@
|
|||||||
.h-11 {
|
.h-11 {
|
||||||
height: calc(var(--spacing) * 11);
|
height: calc(var(--spacing) * 11);
|
||||||
}
|
}
|
||||||
|
.h-12 {
|
||||||
|
height: calc(var(--spacing) * 12);
|
||||||
|
}
|
||||||
|
.h-14 {
|
||||||
|
height: calc(var(--spacing) * 14);
|
||||||
|
}
|
||||||
.h-16 {
|
.h-16 {
|
||||||
height: calc(var(--spacing) * 16);
|
height: calc(var(--spacing) * 16);
|
||||||
}
|
}
|
||||||
|
.h-20 {
|
||||||
|
height: calc(var(--spacing) * 20);
|
||||||
|
}
|
||||||
|
.h-full {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.h-px {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.max-h-screen {
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
.min-h-4 {
|
.min-h-4 {
|
||||||
min-height: calc(var(--spacing) * 4);
|
min-height: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.min-h-20 {
|
||||||
|
min-height: calc(var(--spacing) * 20);
|
||||||
|
}
|
||||||
.min-h-dvh {
|
.min-h-dvh {
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
}
|
}
|
||||||
.min-h-full {
|
.min-h-full {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
.w-3\.5 {
|
||||||
|
width: calc(var(--spacing) * 3.5);
|
||||||
|
}
|
||||||
|
.w-3\/4 {
|
||||||
|
width: calc(3 / 4 * 100%);
|
||||||
|
}
|
||||||
.w-4 {
|
.w-4 {
|
||||||
width: calc(var(--spacing) * 4);
|
width: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
@@ -315,15 +465,39 @@
|
|||||||
.w-10 {
|
.w-10 {
|
||||||
width: calc(var(--spacing) * 10);
|
width: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
|
.w-11 {
|
||||||
|
width: calc(var(--spacing) * 11);
|
||||||
|
}
|
||||||
|
.w-14 {
|
||||||
|
width: calc(var(--spacing) * 14);
|
||||||
|
}
|
||||||
.w-16 {
|
.w-16 {
|
||||||
width: calc(var(--spacing) * 16);
|
width: calc(var(--spacing) * 16);
|
||||||
}
|
}
|
||||||
|
.w-20 {
|
||||||
|
width: calc(var(--spacing) * 20);
|
||||||
|
}
|
||||||
|
.w-48 {
|
||||||
|
width: calc(var(--spacing) * 48);
|
||||||
|
}
|
||||||
.w-64 {
|
.w-64 {
|
||||||
width: calc(var(--spacing) * 64);
|
width: calc(var(--spacing) * 64);
|
||||||
}
|
}
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.w-max {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
.w-px {
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
.max-w-lg {
|
||||||
|
max-width: var(--container-lg);
|
||||||
|
}
|
||||||
|
.max-w-md {
|
||||||
|
max-width: var(--container-md);
|
||||||
|
}
|
||||||
.max-w-sm {
|
.max-w-sm {
|
||||||
max-width: var(--container-sm);
|
max-width: var(--container-sm);
|
||||||
}
|
}
|
||||||
@@ -333,6 +507,9 @@
|
|||||||
.max-w-xs {
|
.max-w-xs {
|
||||||
max-width: var(--container-xs);
|
max-width: var(--container-xs);
|
||||||
}
|
}
|
||||||
|
.min-w-40 {
|
||||||
|
min-width: calc(var(--spacing) * 40);
|
||||||
|
}
|
||||||
.min-w-72 {
|
.min-w-72 {
|
||||||
min-width: calc(var(--spacing) * 72);
|
min-width: calc(var(--spacing) * 72);
|
||||||
}
|
}
|
||||||
@@ -342,13 +519,41 @@
|
|||||||
.shrink-0 {
|
.shrink-0 {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.caption-bottom {
|
||||||
|
caption-side: bottom;
|
||||||
|
}
|
||||||
|
.-translate-x-1\/2 {
|
||||||
|
--tw-translate-x: calc(calc(1 / 2 * 100%) * -1);
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
}
|
||||||
.-translate-x-full {
|
.-translate-x-full {
|
||||||
--tw-translate-x: -100%;
|
--tw-translate-x: -100%;
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
}
|
}
|
||||||
|
.translate-x-0\.5 {
|
||||||
|
--tw-translate-x: calc(var(--spacing) * 0.5);
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
}
|
||||||
|
.-translate-y-1\/2 {
|
||||||
|
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
}
|
||||||
|
.translate-y-2 {
|
||||||
|
--tw-translate-y: calc(var(--spacing) * 2);
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
}
|
||||||
|
.transform {
|
||||||
|
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||||
|
}
|
||||||
|
.animate-pulse {
|
||||||
|
animation: var(--animate-pulse);
|
||||||
|
}
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.resize-y {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
.appearance-none {
|
.appearance-none {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
}
|
}
|
||||||
@@ -361,18 +566,33 @@
|
|||||||
.flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.flex-col-reverse {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
.flex-row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
.flex-wrap {
|
.flex-wrap {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.items-center {
|
.items-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.items-start {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
.justify-between {
|
.justify-between {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.justify-center {
|
.justify-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.justify-end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.justify-start {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
.gap-0\.5 {
|
.gap-0\.5 {
|
||||||
gap: calc(var(--spacing) * 0.5);
|
gap: calc(var(--spacing) * 0.5);
|
||||||
}
|
}
|
||||||
@@ -391,6 +611,9 @@
|
|||||||
.gap-4 {
|
.gap-4 {
|
||||||
gap: calc(var(--spacing) * 4);
|
gap: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.gap-6 {
|
||||||
|
gap: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.gap-8 {
|
.gap-8 {
|
||||||
gap: calc(var(--spacing) * 8);
|
gap: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
@@ -401,6 +624,13 @@
|
|||||||
margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));
|
margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.space-y-1\.5 {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-block-start: calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));
|
||||||
|
margin-block-end: calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
}
|
||||||
|
}
|
||||||
.space-y-2 {
|
.space-y-2 {
|
||||||
:where(& > :not(:last-child)) {
|
:where(& > :not(:last-child)) {
|
||||||
--tw-space-y-reverse: 0;
|
--tw-space-y-reverse: 0;
|
||||||
@@ -429,6 +659,30 @@
|
|||||||
margin-block-end: calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)));
|
margin-block-end: calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.space-x-4 {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
--tw-space-x-reverse: 0;
|
||||||
|
margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));
|
||||||
|
margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.divide-y {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
--tw-divide-y-reverse: 0;
|
||||||
|
border-bottom-style: var(--tw-border-style);
|
||||||
|
border-top-style: var(--tw-border-style);
|
||||||
|
border-top-width: calc(1px * var(--tw-divide-y-reverse));
|
||||||
|
border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.divide-border {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.overflow-auto {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
.overflow-hidden {
|
.overflow-hidden {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -438,9 +692,15 @@
|
|||||||
.rounded-full {
|
.rounded-full {
|
||||||
border-radius: calc(infinity * 1px);
|
border-radius: calc(infinity * 1px);
|
||||||
}
|
}
|
||||||
|
.rounded-lg {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
.rounded-md {
|
.rounded-md {
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
|
.rounded-sm {
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
.border {
|
.border {
|
||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
@@ -460,12 +720,21 @@
|
|||||||
.border-border {
|
.border-border {
|
||||||
border-color: var(--color-border);
|
border-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
.border-destructive {
|
||||||
|
border-color: var(--color-destructive);
|
||||||
|
}
|
||||||
.border-destructive\/30 {
|
.border-destructive\/30 {
|
||||||
border-color: color-mix(in srgb, hsl(var(--destructive)) 30%, transparent);
|
border-color: color-mix(in srgb, hsl(var(--destructive)) 30%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
border-color: color-mix(in oklab, var(--color-destructive) 30%, transparent);
|
border-color: color-mix(in oklab, var(--color-destructive) 30%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.border-destructive\/50 {
|
||||||
|
border-color: color-mix(in srgb, hsl(var(--destructive)) 50%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-color: color-mix(in oklab, var(--color-destructive) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
.border-input {
|
.border-input {
|
||||||
border-color: var(--color-input);
|
border-color: var(--color-input);
|
||||||
}
|
}
|
||||||
@@ -478,6 +747,9 @@
|
|||||||
background-color: color-mix(in oklab, var(--color-black) 50%, transparent);
|
background-color: color-mix(in oklab, var(--color-black) 50%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.bg-border {
|
||||||
|
background-color: var(--color-border);
|
||||||
|
}
|
||||||
.bg-card {
|
.bg-card {
|
||||||
background-color: var(--color-card);
|
background-color: var(--color-card);
|
||||||
}
|
}
|
||||||
@@ -496,6 +768,21 @@
|
|||||||
background-color: color-mix(in oklab, var(--color-destructive) 15%, transparent);
|
background-color: color-mix(in oklab, var(--color-destructive) 15%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.bg-input {
|
||||||
|
background-color: var(--color-input);
|
||||||
|
}
|
||||||
|
.bg-muted {
|
||||||
|
background-color: var(--color-muted);
|
||||||
|
}
|
||||||
|
.bg-muted\/50 {
|
||||||
|
background-color: color-mix(in srgb, hsl(var(--muted)) 50%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-muted) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bg-popover {
|
||||||
|
background-color: var(--color-popover);
|
||||||
|
}
|
||||||
.bg-primary {
|
.bg-primary {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
@@ -505,6 +792,18 @@
|
|||||||
.bg-transparent {
|
.bg-transparent {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
.bg-white {
|
||||||
|
background-color: var(--color-white);
|
||||||
|
}
|
||||||
|
.object-cover {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.p-0 {
|
||||||
|
padding: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
.p-1 {
|
||||||
|
padding: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
.p-4 {
|
.p-4 {
|
||||||
padding: calc(var(--spacing) * 4);
|
padding: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
@@ -514,6 +813,9 @@
|
|||||||
.px-2 {
|
.px-2 {
|
||||||
padding-inline: calc(var(--spacing) * 2);
|
padding-inline: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
.px-2\.5 {
|
||||||
|
padding-inline: calc(var(--spacing) * 2.5);
|
||||||
|
}
|
||||||
.px-3 {
|
.px-3 {
|
||||||
padding-inline: calc(var(--spacing) * 3);
|
padding-inline: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
@@ -532,6 +834,9 @@
|
|||||||
.py-1 {
|
.py-1 {
|
||||||
padding-block: calc(var(--spacing) * 1);
|
padding-block: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
|
.py-1\.5 {
|
||||||
|
padding-block: calc(var(--spacing) * 1.5);
|
||||||
|
}
|
||||||
.py-2 {
|
.py-2 {
|
||||||
padding-block: calc(var(--spacing) * 2);
|
padding-block: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@@ -544,9 +849,24 @@
|
|||||||
.py-12 {
|
.py-12 {
|
||||||
padding-block: calc(var(--spacing) * 12);
|
padding-block: calc(var(--spacing) * 12);
|
||||||
}
|
}
|
||||||
|
.pt-0 {
|
||||||
|
padding-top: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
.pt-4 {
|
||||||
|
padding-top: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.pb-4 {
|
||||||
|
padding-bottom: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.align-middle {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
.text-2xl {
|
.text-2xl {
|
||||||
font-size: var(--text-2xl);
|
font-size: var(--text-2xl);
|
||||||
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||||
@@ -579,6 +899,10 @@
|
|||||||
--tw-font-weight: var(--font-weight-medium);
|
--tw-font-weight: var(--font-weight-medium);
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
}
|
}
|
||||||
|
.font-normal {
|
||||||
|
--tw-font-weight: var(--font-weight-normal);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
}
|
||||||
.font-semibold {
|
.font-semibold {
|
||||||
--tw-font-weight: var(--font-weight-semibold);
|
--tw-font-weight: var(--font-weight-semibold);
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
@@ -587,9 +911,15 @@
|
|||||||
--tw-tracking: var(--tracking-tight);
|
--tw-tracking: var(--tracking-tight);
|
||||||
letter-spacing: var(--tracking-tight);
|
letter-spacing: var(--tracking-tight);
|
||||||
}
|
}
|
||||||
|
.break-words {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
.whitespace-nowrap {
|
.whitespace-nowrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.text-card-foreground {
|
||||||
|
color: var(--color-card-foreground);
|
||||||
|
}
|
||||||
.text-destructive {
|
.text-destructive {
|
||||||
color: var(--color-destructive);
|
color: var(--color-destructive);
|
||||||
}
|
}
|
||||||
@@ -602,6 +932,9 @@
|
|||||||
.text-muted-foreground {
|
.text-muted-foreground {
|
||||||
color: var(--color-muted-foreground);
|
color: var(--color-muted-foreground);
|
||||||
}
|
}
|
||||||
|
.text-popover-foreground {
|
||||||
|
color: var(--color-popover-foreground);
|
||||||
|
}
|
||||||
.text-primary {
|
.text-primary {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
@@ -618,9 +951,18 @@
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
.accent-primary {
|
||||||
|
accent-color: var(--color-primary);
|
||||||
|
}
|
||||||
.opacity-0 {
|
.opacity-0 {
|
||||||
opacity: 0%;
|
opacity: 0%;
|
||||||
}
|
}
|
||||||
|
.opacity-70 {
|
||||||
|
opacity: 70%;
|
||||||
|
}
|
||||||
|
.opacity-90 {
|
||||||
|
opacity: 90%;
|
||||||
|
}
|
||||||
.shadow {
|
.shadow {
|
||||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
@@ -629,10 +971,18 @@
|
|||||||
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
|
.shadow-md {
|
||||||
|
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
.shadow-sm {
|
.shadow-sm {
|
||||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
|
.ring-0 {
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
.ring-offset-background {
|
.ring-offset-background {
|
||||||
--tw-ring-offset-color: var(--color-background);
|
--tw-ring-offset-color: var(--color-background);
|
||||||
}
|
}
|
||||||
@@ -648,6 +998,11 @@
|
|||||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||||
}
|
}
|
||||||
|
.transition-all {
|
||||||
|
transition-property: all;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
|
}
|
||||||
.transition-colors {
|
.transition-colors {
|
||||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
||||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
@@ -663,6 +1018,14 @@
|
|||||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
}
|
}
|
||||||
|
.duration-150 {
|
||||||
|
--tw-duration: 150ms;
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
.duration-200 {
|
||||||
|
--tw-duration: 200ms;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
}
|
||||||
.duration-300 {
|
.duration-300 {
|
||||||
--tw-duration: 300ms;
|
--tw-duration: 300ms;
|
||||||
transition-duration: 300ms;
|
transition-duration: 300ms;
|
||||||
@@ -671,10 +1034,21 @@
|
|||||||
--tw-ease: var(--ease-in-out);
|
--tw-ease: var(--ease-in-out);
|
||||||
transition-timing-function: var(--ease-in-out);
|
transition-timing-function: var(--ease-in-out);
|
||||||
}
|
}
|
||||||
|
.outline-none {
|
||||||
|
--tw-outline-style: none;
|
||||||
|
outline-style: none;
|
||||||
|
}
|
||||||
.select-none {
|
.select-none {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.group-hover\:opacity-100 {
|
||||||
|
&:is(:where(.group):hover *) {
|
||||||
|
@media (hover: hover) {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.peer-disabled\:cursor-not-allowed {
|
.peer-disabled\:cursor-not-allowed {
|
||||||
&:is(:where(.peer):disabled ~ *) {
|
&:is(:where(.peer):disabled ~ *) {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -685,11 +1059,59 @@
|
|||||||
opacity: 70%;
|
opacity: 70%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.file\:border-0 {
|
||||||
|
&::file-selector-button {
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file\:bg-transparent {
|
||||||
|
&::file-selector-button {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file\:text-sm {
|
||||||
|
&::file-selector-button {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file\:font-medium {
|
||||||
|
&::file-selector-button {
|
||||||
|
--tw-font-weight: var(--font-weight-medium);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file\:text-foreground {
|
||||||
|
&::file-selector-button {
|
||||||
|
color: var(--color-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
.placeholder\:text-muted-foreground {
|
.placeholder\:text-muted-foreground {
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--color-muted-foreground);
|
color: var(--color-muted-foreground);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.backdrop\:bg-black\/50 {
|
||||||
|
&::backdrop {
|
||||||
|
background-color: color-mix(in srgb, #000 50%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-black) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.backdrop\:backdrop-blur-sm {
|
||||||
|
&::backdrop {
|
||||||
|
--tw-backdrop-blur: blur(var(--blur-sm));
|
||||||
|
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||||
|
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.open\:block {
|
||||||
|
&:is([open], :popover-open, :open) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
.hover\:bg-accent {
|
.hover\:bg-accent {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@@ -697,6 +1119,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hover\:bg-destructive\/80 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: color-mix(in srgb, hsl(var(--destructive)) 80%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-destructive) 80%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.hover\:bg-destructive\/90 {
|
.hover\:bg-destructive\/90 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@@ -707,6 +1139,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hover\:bg-muted\/50 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: color-mix(in srgb, hsl(var(--muted)) 50%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-muted) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover\:bg-primary\/80 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: color-mix(in srgb, hsl(var(--primary)) 80%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-primary) 80%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.hover\:bg-primary\/90 {
|
.hover\:bg-primary\/90 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@@ -734,6 +1186,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hover\:text-foreground {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
color: var(--color-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.hover\:underline {
|
.hover\:underline {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@@ -741,6 +1200,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hover\:opacity-100 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:bg-accent {
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:text-accent-foreground {
|
||||||
|
&:focus {
|
||||||
|
color: var(--color-accent-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:ring-1 {
|
||||||
|
&:focus {
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
.focus\:ring-2 {
|
.focus\:ring-2 {
|
||||||
&:focus {
|
&:focus {
|
||||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||||
@@ -813,6 +1295,11 @@
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sm\:flex-col {
|
||||||
|
@media (width >= 40rem) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
.md\:relative {
|
.md\:relative {
|
||||||
@media (width >= 48rem) {
|
@media (width >= 48rem) {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -823,6 +1310,11 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.md\:max-w-\[420px\] {
|
||||||
|
@media (width >= 48rem) {
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.md\:translate-x-0 {
|
.md\:translate-x-0 {
|
||||||
@media (width >= 48rem) {
|
@media (width >= 48rem) {
|
||||||
--tw-translate-x: calc(var(--spacing) * 0);
|
--tw-translate-x: calc(var(--spacing) * 0);
|
||||||
@@ -840,12 +1332,59 @@
|
|||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.\[\&_p\]\:leading-relaxed {
|
||||||
|
& p {
|
||||||
|
--tw-leading: var(--leading-relaxed);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
.\[\&\.open\]\:translate-x-0 {
|
.\[\&\.open\]\:translate-x-0 {
|
||||||
&.open {
|
&.open {
|
||||||
--tw-translate-x: calc(var(--spacing) * 0);
|
--tw-translate-x: calc(var(--spacing) * 0);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0 {
|
||||||
|
&:has([role=checkbox]) {
|
||||||
|
padding-right: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\]\:absolute {
|
||||||
|
&>svg {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\]\:top-4 {
|
||||||
|
&>svg {
|
||||||
|
top: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\]\:left-4 {
|
||||||
|
&>svg {
|
||||||
|
left: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\]\:text-destructive {
|
||||||
|
&>svg {
|
||||||
|
color: var(--color-destructive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\]\:text-foreground {
|
||||||
|
&>svg {
|
||||||
|
color: var(--color-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\+div\]\:translate-y-\[-3px\] {
|
||||||
|
&>svg+div {
|
||||||
|
--tw-translate-y: -3px;
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\~\*\]\:pl-7 {
|
||||||
|
&>svg~* {
|
||||||
|
padding-left: calc(var(--spacing) * 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
@@ -1121,11 +1660,41 @@
|
|||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0;
|
initial-value: 0;
|
||||||
}
|
}
|
||||||
|
@property --tw-rotate-x {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-rotate-y {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-rotate-z {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-skew-x {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-skew-y {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
@property --tw-space-y-reverse {
|
@property --tw-space-y-reverse {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0;
|
initial-value: 0;
|
||||||
}
|
}
|
||||||
|
@property --tw-space-x-reverse {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-divide-y-reverse {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
@property --tw-border-style {
|
@property --tw-border-style {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@@ -1310,13 +1879,25 @@
|
|||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
@layer properties {
|
@layer properties {
|
||||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||||
*, ::before, ::after, ::backdrop {
|
*, ::before, ::after, ::backdrop {
|
||||||
--tw-translate-x: 0;
|
--tw-translate-x: 0;
|
||||||
--tw-translate-y: 0;
|
--tw-translate-y: 0;
|
||||||
--tw-translate-z: 0;
|
--tw-translate-z: 0;
|
||||||
|
--tw-rotate-x: initial;
|
||||||
|
--tw-rotate-y: initial;
|
||||||
|
--tw-rotate-z: initial;
|
||||||
|
--tw-skew-x: initial;
|
||||||
|
--tw-skew-y: initial;
|
||||||
--tw-space-y-reverse: 0;
|
--tw-space-y-reverse: 0;
|
||||||
|
--tw-space-x-reverse: 0;
|
||||||
|
--tw-divide-y-reverse: 0;
|
||||||
--tw-border-style: solid;
|
--tw-border-style: solid;
|
||||||
--tw-leading: initial;
|
--tw-leading: initial;
|
||||||
--tw-font-weight: initial;
|
--tw-font-weight: initial;
|
||||||
|
|||||||
@@ -456,3 +456,262 @@
|
|||||||
document.addEventListener('DOMContentLoaded', initAll);
|
document.addEventListener('DOMContentLoaded', initAll);
|
||||||
document.addEventListener('htmx:afterSwap', initAll);
|
document.addEventListener('htmx:afterSwap', initAll);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// ── Switch ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
function updateSwitch(input) {
|
||||||
|
var track = input.parentElement && input.parentElement.querySelector('.switch-track');
|
||||||
|
if (!track) return;
|
||||||
|
var thumb = track.querySelector('.switch-thumb');
|
||||||
|
if (input.checked) {
|
||||||
|
track.classList.add('bg-primary');
|
||||||
|
track.classList.remove('bg-input');
|
||||||
|
if (thumb) thumb.style.transform = 'translateX(1.375rem)';
|
||||||
|
} else {
|
||||||
|
track.classList.remove('bg-primary');
|
||||||
|
track.classList.add('bg-input');
|
||||||
|
if (thumb) thumb.style.transform = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initAll() {
|
||||||
|
document.querySelectorAll('.switch-checkbox').forEach(function (input) {
|
||||||
|
updateSwitch(input);
|
||||||
|
if (!input._switchBound) {
|
||||||
|
input._switchBound = true;
|
||||||
|
input.addEventListener('change', function () { updateSwitch(input); });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initAll);
|
||||||
|
document.addEventListener('htmx:afterSwap', initAll);
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// ── Tabs ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var ACTIVE = 'bg-background text-foreground shadow-sm';
|
||||||
|
var INACTIVE = 'text-muted-foreground';
|
||||||
|
|
||||||
|
function initTabs(root) {
|
||||||
|
if (root._tabsInitialised) return;
|
||||||
|
root._tabsInitialised = true;
|
||||||
|
|
||||||
|
var triggers = Array.from(root.querySelectorAll('.tabs-trigger'));
|
||||||
|
var panels = Array.from(root.querySelectorAll('.tabs-panel'));
|
||||||
|
|
||||||
|
function activate(idx) {
|
||||||
|
triggers.forEach(function (t, i) {
|
||||||
|
var active = i === idx;
|
||||||
|
t.setAttribute('aria-selected', String(active));
|
||||||
|
ACTIVE.split(' ').forEach(function (c) { t.classList.toggle(c, active); });
|
||||||
|
INACTIVE.split(' ').forEach(function (c) { t.classList.toggle(c, !active); });
|
||||||
|
});
|
||||||
|
panels.forEach(function (p, i) { p.hidden = i !== idx; });
|
||||||
|
}
|
||||||
|
|
||||||
|
triggers.forEach(function (trigger, idx) {
|
||||||
|
trigger.addEventListener('click', function () { activate(idx); });
|
||||||
|
});
|
||||||
|
activate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initAll() {
|
||||||
|
document.querySelectorAll('.tabs-root').forEach(initTabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initAll);
|
||||||
|
document.addEventListener('htmx:afterSwap', initAll);
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// ── Accordion ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
function initAccordion(root) {
|
||||||
|
if (root._accInitialised) return;
|
||||||
|
root._accInitialised = true;
|
||||||
|
|
||||||
|
root.querySelectorAll('.accordion-trigger').forEach(function (trigger) {
|
||||||
|
trigger.addEventListener('click', function () {
|
||||||
|
var expanded = trigger.getAttribute('aria-expanded') === 'true';
|
||||||
|
var panel = trigger.closest('.accordion-item').querySelector('.accordion-panel');
|
||||||
|
if (expanded) {
|
||||||
|
trigger.setAttribute('aria-expanded', 'false');
|
||||||
|
panel.style.height = '0';
|
||||||
|
panel.style.opacity = '0';
|
||||||
|
} else {
|
||||||
|
trigger.setAttribute('aria-expanded', 'true');
|
||||||
|
panel.style.height = panel.scrollHeight + 'px';
|
||||||
|
panel.style.opacity = '1';
|
||||||
|
}
|
||||||
|
var chevron = trigger.querySelector('.accordion-chevron');
|
||||||
|
if (chevron) chevron.style.transform = expanded ? '' : 'rotate(180deg)';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initAll() {
|
||||||
|
document.querySelectorAll('.accordion-root').forEach(initAccordion);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initAll);
|
||||||
|
document.addEventListener('htmx:afterSwap', initAll);
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// ── Toast ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
function dismissToast(toast) {
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
toast.style.transform = 'translateY(0.5rem)';
|
||||||
|
setTimeout(function () { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.showToast = function (options) {
|
||||||
|
var viewport = document.querySelector('.toast-viewport');
|
||||||
|
if (!viewport) return;
|
||||||
|
|
||||||
|
var title = options.title || '';
|
||||||
|
var description = options.description || '';
|
||||||
|
var variant = options.variant || 'default';
|
||||||
|
var duration = typeof options.duration === 'number' ? options.duration : 5000;
|
||||||
|
|
||||||
|
var variantCls = variant === 'destructive' ? ' border-destructive text-destructive' : '';
|
||||||
|
|
||||||
|
var toast = document.createElement('div');
|
||||||
|
toast.setAttribute('role', 'alert');
|
||||||
|
toast.className = '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 duration-300 opacity-0 translate-y-2' + variantCls;
|
||||||
|
|
||||||
|
toast.innerHTML =
|
||||||
|
'<div class="grid gap-1">' +
|
||||||
|
(title ? '<div class="text-sm font-semibold">' + title + '</div>' : '') +
|
||||||
|
(description ? '<div class="text-sm opacity-90">' + description + '</div>' : '') +
|
||||||
|
'</div>' +
|
||||||
|
'<button type="button" aria-label="Dismiss"' +
|
||||||
|
' 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">' +
|
||||||
|
'<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>';
|
||||||
|
|
||||||
|
viewport.appendChild(toast);
|
||||||
|
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
toast.classList.remove('opacity-0', 'translate-y-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
var timer = duration > 0 ? setTimeout(function () { dismissToast(toast); }, duration) : null;
|
||||||
|
|
||||||
|
toast.querySelector('.toast-close').addEventListener('click', function () {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
dismissToast(toast);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delegate clicks on server-rendered toast close buttons
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var btn = e.target.closest('.toast-close');
|
||||||
|
if (btn) {
|
||||||
|
var item = btn.closest('.toast-item');
|
||||||
|
if (item) dismissToast(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
var trigger = e.target.closest('.dropdown-trigger');
|
||||||
|
if (!trigger) return;
|
||||||
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
var root = trigger.closest('.dropdown-root');
|
||||||
|
var content = root && root.querySelector('.dropdown-content');
|
||||||
|
var isOpen = content && !content.classList.contains('hidden');
|
||||||
|
|
||||||
|
document.querySelectorAll('.dropdown-root').forEach(closeDropdown);
|
||||||
|
if (!isOpen && root) openDropdown(root);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// ── Dialog ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
// Open
|
||||||
|
var openBtn = e.target.closest('[data-dialog-open]');
|
||||||
|
if (openBtn) {
|
||||||
|
var dlg = document.getElementById('dlg-' + openBtn.dataset.dialogOpen);
|
||||||
|
if (dlg && dlg.showModal) dlg.showModal();
|
||||||
|
}
|
||||||
|
// Close via button
|
||||||
|
var closeBtn = e.target.closest('[data-dialog-close], .dialog-close');
|
||||||
|
if (closeBtn) {
|
||||||
|
var dlg = closeBtn.closest('dialog');
|
||||||
|
if (dlg) dlg.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on backdrop click
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (e.target && e.target.tagName === 'DIALOG') {
|
||||||
|
e.target.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// ── DropdownMenu ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
function closeDropdown(root) {
|
||||||
|
var trigger = root.querySelector('.dropdown-trigger');
|
||||||
|
var content = root.querySelector('.dropdown-content');
|
||||||
|
if (!trigger || !content) return;
|
||||||
|
content.classList.add('hidden');
|
||||||
|
trigger.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDropdown(root) {
|
||||||
|
var trigger = root.querySelector('.dropdown-trigger');
|
||||||
|
var content = root.querySelector('.dropdown-content');
|
||||||
|
if (!trigger || !content) return;
|
||||||
|
content.classList.remove('hidden');
|
||||||
|
trigger.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var trigger = e.target.closest('.dropdown-trigger');
|
||||||
|
if (trigger) {
|
||||||
|
var root = trigger.closest('.dropdown-root');
|
||||||
|
var content = root && root.querySelector('.dropdown-content');
|
||||||
|
var isOpen = content && !content.classList.contains('hidden');
|
||||||
|
|
||||||
|
document.querySelectorAll('.dropdown-root').forEach(closeDropdown);
|
||||||
|
if (!isOpen && root) openDropdown(root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var insideMenu = e.target.closest('.dropdown-content');
|
||||||
|
if (insideMenu) {
|
||||||
|
var rootInMenu = insideMenu.closest('.dropdown-root');
|
||||||
|
if (e.target.closest('a, button') && rootInMenu) {
|
||||||
|
closeDropdown(rootInMenu);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.dropdown-root').forEach(function (root) {
|
||||||
|
if (!root.contains(e.target)) closeDropdown(root);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user