diff --git a/Htmx.ApiDemo/Properties/launchSettings.json b/Htmx.ApiDemo/Properties/launchSettings.json index e74f83a..12cf25f 100644 --- a/Htmx.ApiDemo/Properties/launchSettings.json +++ b/Htmx.ApiDemo/Properties/launchSettings.json @@ -4,8 +4,8 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "todos", + "launchBrowser": false, + "launchUrl": "/", "applicationUrl": "http://localhost:5120", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/Htmx.ApiDemo/Templates/Components/Accordion.htmx b/Htmx.ApiDemo/Templates/Components/Accordion.htmx new file mode 100644 index 0000000..79733d7 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Accordion.htmx @@ -0,0 +1,3 @@ +
+ $$Items$$ +
diff --git a/Htmx.ApiDemo/Templates/Components/Accordion.htmx.cs b/Htmx.ApiDemo/Templates/Components/Accordion.htmx.cs new file mode 100644 index 0000000..89f62b4 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Accordion.htmx.cs @@ -0,0 +1,55 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// 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). +/// +public sealed class Accordion : AccordionBase +{ + private const string ChevronSvg = + """"""; + + 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($""" +
+

+ +

+
+
{content}
+
+
+ """); + } + + _itemsData = sb.ToString().ToUtf8Bytes(); + } + + protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData); + protected override void RenderItems(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_itemsData); +} diff --git a/Htmx.ApiDemo/Templates/Components/Alert.htmx b/Htmx.ApiDemo/Templates/Components/Alert.htmx new file mode 100644 index 0000000..4be6b4a --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Alert.htmx @@ -0,0 +1,7 @@ + diff --git a/Htmx.ApiDemo/Templates/Components/Alert.htmx.cs b/Htmx.ApiDemo/Templates/Components/Alert.htmx.cs new file mode 100644 index 0000000..e5bec89 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Alert.htmx.cs @@ -0,0 +1,40 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Alert component. +/// Variant: default | destructive +/// +public sealed class Alert : AlertBase +{ + private static readonly Dictionary 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 = $"""
{title}
""".ToUtf8Bytes(); + _descriptionData = string.IsNullOrEmpty(description) + ? [] + : $"""
{description}
""".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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Avatar.htmx b/Htmx.ApiDemo/Templates/Components/Avatar.htmx new file mode 100644 index 0000000..05d1e8e --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Avatar.htmx @@ -0,0 +1,3 @@ + + $$Content$$ + diff --git a/Htmx.ApiDemo/Templates/Components/Avatar.htmx.cs b/Htmx.ApiDemo/Templates/Components/Avatar.htmx.cs new file mode 100644 index 0000000..465553c --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Avatar.htmx.cs @@ -0,0 +1,31 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// 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) +/// +public sealed class Avatar : AvatarBase +{ + private static readonly Dictionary 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) + ? $"""{fallback}""".ToUtf8Bytes() + : $"""{fallback}""".ToUtf8Bytes(); + } + + protected override void RenderSizeClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_sizeClassesData); + protected override void RenderContent(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_contentData); +} diff --git a/Htmx.ApiDemo/Templates/Components/Badge.htmx b/Htmx.ApiDemo/Templates/Components/Badge.htmx new file mode 100644 index 0000000..bb65b2e --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Badge.htmx @@ -0,0 +1 @@ +$$Text$$ diff --git a/Htmx.ApiDemo/Templates/Components/Badge.htmx.cs b/Htmx.ApiDemo/Templates/Components/Badge.htmx.cs new file mode 100644 index 0000000..b1a8371 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Badge.htmx.cs @@ -0,0 +1,33 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Badge component. +/// Variant: default | secondary | destructive | outline +/// +public sealed class Badge : BadgeBase +{ + private static readonly Dictionary 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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Breadcrumb.htmx b/Htmx.ApiDemo/Templates/Components/Breadcrumb.htmx new file mode 100644 index 0000000..8ccca42 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Breadcrumb.htmx @@ -0,0 +1,5 @@ + diff --git a/Htmx.ApiDemo/Templates/Components/Breadcrumb.htmx.cs b/Htmx.ApiDemo/Templates/Components/Breadcrumb.htmx.cs new file mode 100644 index 0000000..9d982fc --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Breadcrumb.htmx.cs @@ -0,0 +1,42 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// 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). +/// +public sealed class Breadcrumb : BreadcrumbBase +{ + private const string ChevronSvg = + """"""; + + 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("""
  • """); + + if (isLast || string.IsNullOrEmpty(href)) + sb.Append($"""{label}"""); + else + sb.Append($"""{label}"""); + + if (!isLast) + sb.Append($""""""); + + sb.Append("
  • "); + } + + _itemsData = sb.ToString().ToUtf8Bytes(); + } + + protected override void RenderItems(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_itemsData); +} diff --git a/Htmx.ApiDemo/Templates/Components/Card.htmx b/Htmx.ApiDemo/Templates/Components/Card.htmx new file mode 100644 index 0000000..6600df7 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Card.htmx @@ -0,0 +1,5 @@ +
    + $$Header$$ +
    $$Content$$
    + $$Footer$$ +
    diff --git a/Htmx.ApiDemo/Templates/Components/Card.htmx.cs b/Htmx.ApiDemo/Templates/Components/Card.htmx.cs new file mode 100644 index 0000000..e0254ba --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Card.htmx.cs @@ -0,0 +1,48 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Card component with optional header (title + description) and footer. +/// +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) + ? [] + : $"""
    {footer}
    """.ToUtf8Bytes(); + } + + private static byte[] BuildHeader(string title, string description) + { + var sb = new System.Text.StringBuilder(); + sb.Append("""
    """); + if (!string.IsNullOrEmpty(title)) + sb.Append($"""

    {title}

    """); + if (!string.IsNullOrEmpty(description)) + sb.Append($"""

    {description}

    """); + sb.Append("
    "); + 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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Checkbox.htmx b/Htmx.ApiDemo/Templates/Components/Checkbox.htmx new file mode 100644 index 0000000..06f6846 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Checkbox.htmx @@ -0,0 +1,13 @@ +
    + + $$Label$$ +
    diff --git a/Htmx.ApiDemo/Templates/Components/Checkbox.htmx.cs b/Htmx.ApiDemo/Templates/Components/Checkbox.htmx.cs new file mode 100644 index 0000000..f44980e --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Checkbox.htmx.cs @@ -0,0 +1,35 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Checkbox with an optional label. +/// +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) + ? [] + : $"""""".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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Dialog.htmx b/Htmx.ApiDemo/Templates/Components/Dialog.htmx new file mode 100644 index 0000000..eccc42c --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Dialog.htmx @@ -0,0 +1,11 @@ + +
    + $$Header$$ +
    $$Content$$
    + $$Footer$$ +
    +
    diff --git a/Htmx.ApiDemo/Templates/Components/Dialog.htmx.cs b/Htmx.ApiDemo/Templates/Components/Dialog.htmx.cs new file mode 100644 index 0000000..f6f9630 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Dialog.htmx.cs @@ -0,0 +1,55 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// 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. +/// +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) + ? [] + : $"""
    {footer}
    """.ToUtf8Bytes(); + } + + private static byte[] BuildHeader(string id, string title, string description) + { + var sb = new System.Text.StringBuilder(); + sb.Append("""
    """); + sb.Append("""
    """); + if (!string.IsNullOrEmpty(title)) + sb.Append($"""

    {title}

    """); + if (!string.IsNullOrEmpty(description)) + sb.Append($"""

    {description}

    """); + sb.Append("
    "); + sb.Append(""""); + sb.Append("
    "); + 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); +} diff --git a/Htmx.ApiDemo/Templates/Components/DropdownMenu.htmx b/Htmx.ApiDemo/Templates/Components/DropdownMenu.htmx new file mode 100644 index 0000000..c151650 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/DropdownMenu.htmx @@ -0,0 +1,9 @@ + diff --git a/Htmx.ApiDemo/Templates/Components/DropdownMenu.htmx.cs b/Htmx.ApiDemo/Templates/Components/DropdownMenu.htmx.cs new file mode 100644 index 0000000..e8f466a --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/DropdownMenu.htmx.cs @@ -0,0 +1,55 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// 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. +/// +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(); + 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("""
    """); + } + else if (string.IsNullOrEmpty(href)) + { + sb.Append($""""""); + } + else + { + sb.Append($"""{label}"""); + } + } + _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); +} diff --git a/Htmx.ApiDemo/Templates/Components/FileInput.htmx b/Htmx.ApiDemo/Templates/Components/FileInput.htmx new file mode 100644 index 0000000..8baca5f --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/FileInput.htmx @@ -0,0 +1,18 @@ +
    + $$Label$$ + + $$Description$$ +
    diff --git a/Htmx.ApiDemo/Templates/Components/FileInput.htmx.cs b/Htmx.ApiDemo/Templates/Components/FileInput.htmx.cs new file mode 100644 index 0000000..2b4caa0 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/FileInput.htmx.cs @@ -0,0 +1,51 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style FileInput component with optional label and description. +/// +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) + ? [] + : $"""""".ToUtf8Bytes(); + + _descriptionData = string.IsNullOrEmpty(description) + ? [] + : $"""

    {description}

    """.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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Pagination.htmx b/Htmx.ApiDemo/Templates/Components/Pagination.htmx new file mode 100644 index 0000000..490ce26 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Pagination.htmx @@ -0,0 +1,5 @@ + diff --git a/Htmx.ApiDemo/Templates/Components/Pagination.htmx.cs b/Htmx.ApiDemo/Templates/Components/Pagination.htmx.cs new file mode 100644 index 0000000..71d73a2 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Pagination.htmx.cs @@ -0,0 +1,49 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// 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}" +/// +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 + ? $"""""".ToUtf8Bytes() + : $"""""".ToUtf8Bytes(); + + _nextData = current >= total + ? $"""""".ToUtf8Bytes() + : $"""""".ToUtf8Bytes(); + + var sb = new System.Text.StringBuilder(); + for (int p = 1; p <= total; p++) + { + if (p == current) + sb.Append($"""{p}"""); + else + sb.Append($"""{p}"""); + } + _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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Progress.htmx b/Htmx.ApiDemo/Templates/Components/Progress.htmx new file mode 100644 index 0000000..3c2460f --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Progress.htmx @@ -0,0 +1,4 @@ +
    +
    +
    diff --git a/Htmx.ApiDemo/Templates/Components/Progress.htmx.cs b/Htmx.ApiDemo/Templates/Components/Progress.htmx.cs new file mode 100644 index 0000000..9e05862 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Progress.htmx.cs @@ -0,0 +1,26 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Progress bar. Value is clamped to 0–100. +/// Size: sm (h-2) | default (h-4) | lg (h-6) +/// +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); +} diff --git a/Htmx.ApiDemo/Templates/Components/RadioGroup.htmx b/Htmx.ApiDemo/Templates/Components/RadioGroup.htmx new file mode 100644 index 0000000..3f9ba10 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/RadioGroup.htmx @@ -0,0 +1,6 @@ +
    + $$GroupLabel$$ +
    + $$Items$$ +
    +
    diff --git a/Htmx.ApiDemo/Templates/Components/RadioGroup.htmx.cs b/Htmx.ApiDemo/Templates/Components/RadioGroup.htmx.cs new file mode 100644 index 0000000..2148728 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/RadioGroup.htmx.cs @@ -0,0 +1,46 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style RadioGroup. +/// Direction: flex-col | flex-row +/// +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) + ? [] + : $"""{label}""".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($""" + + """); + } + _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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Separator.htmx b/Htmx.ApiDemo/Templates/Components/Separator.htmx new file mode 100644 index 0000000..305e090 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Separator.htmx @@ -0,0 +1 @@ + diff --git a/Htmx.ApiDemo/Templates/Components/Separator.htmx.cs b/Htmx.ApiDemo/Templates/Components/Separator.htmx.cs new file mode 100644 index 0000000..1a8599b --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Separator.htmx.cs @@ -0,0 +1,24 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Separator component. +/// Orientation: horizontal | vertical +/// +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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Skeleton.htmx b/Htmx.ApiDemo/Templates/Components/Skeleton.htmx new file mode 100644 index 0000000..3cacee9 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Skeleton.htmx @@ -0,0 +1 @@ +
    diff --git a/Htmx.ApiDemo/Templates/Components/Skeleton.htmx.cs b/Htmx.ApiDemo/Templates/Components/Skeleton.htmx.cs new file mode 100644 index 0000000..a9dd567 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Skeleton.htmx.cs @@ -0,0 +1,17 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Skeleton loading placeholder. +/// Pass size classes via the classes parameter, e.g. "h-4 w-48" or "h-10 w-full". +/// +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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Slider.htmx b/Htmx.ApiDemo/Templates/Components/Slider.htmx new file mode 100644 index 0000000..35a6ab4 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Slider.htmx @@ -0,0 +1,17 @@ +
    + $$Label$$ + + $$Description$$ +
    diff --git a/Htmx.ApiDemo/Templates/Components/Slider.htmx.cs b/Htmx.ApiDemo/Templates/Components/Slider.htmx.cs new file mode 100644 index 0000000..5392d17 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Slider.htmx.cs @@ -0,0 +1,59 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Slider (range input) with optional label and description. +/// +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) + ? [] + : $"""""".ToUtf8Bytes(); + + _descriptionData = string.IsNullOrEmpty(description) + ? [] + : $"""

    {description}

    """.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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Switch.htmx b/Htmx.ApiDemo/Templates/Components/Switch.htmx new file mode 100644 index 0000000..f1ad1cc --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Switch.htmx @@ -0,0 +1,10 @@ + diff --git a/Htmx.ApiDemo/Templates/Components/Switch.htmx.cs b/Htmx.ApiDemo/Templates/Components/Switch.htmx.cs new file mode 100644 index 0000000..57963c0 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Switch.htmx.cs @@ -0,0 +1,32 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Switch (toggle). Rendered as a styled checkbox. +/// JS in components.js handles the visual on/off state. +/// +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) + ? [] + : $"""{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 RenderChecked(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_checkedData); + protected override void RenderLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_labelData); +} diff --git a/Htmx.ApiDemo/Templates/Components/Table.htmx b/Htmx.ApiDemo/Templates/Components/Table.htmx new file mode 100644 index 0000000..5d57f82 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Table.htmx @@ -0,0 +1,14 @@ +
    + + $$Caption$$ + + + $$Headers$$ + + + + $$Rows$$ + + $$Footer$$ +
    +
    diff --git a/Htmx.ApiDemo/Templates/Components/Table.htmx.cs b/Htmx.ApiDemo/Templates/Components/Table.htmx.cs new file mode 100644 index 0000000..9063766 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Table.htmx.cs @@ -0,0 +1,50 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Table component. +/// Headers: column header strings. +/// Rows: each row is an IEnumerable of cell strings. +/// Caption and Footer are optional. +/// +public sealed class Table : TableBase +{ + private readonly byte[] _captionData; + private readonly byte[] _headersData; + private readonly byte[] _rowsData; + private readonly byte[] _footerData; + + public Table( + IEnumerable headers, + IEnumerable> rows, + string caption = "", + string footer = "") + { + _captionData = string.IsNullOrEmpty(caption) + ? [] + : $"""{caption}""".ToUtf8Bytes(); + + var hSb = new System.Text.StringBuilder(); + foreach (var h in headers) + hSb.Append($"""{h}"""); + _headersData = hSb.ToString().ToUtf8Bytes(); + + var rSb = new System.Text.StringBuilder(); + foreach (var row in rows) + { + rSb.Append(""""""); + foreach (var cell in row) + rSb.Append($"""{cell}"""); + rSb.Append(""); + } + _rowsData = rSb.ToString().ToUtf8Bytes(); + + _footerData = string.IsNullOrEmpty(footer) + ? [] + : $"""{footer}""".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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Tabs.htmx b/Htmx.ApiDemo/Templates/Components/Tabs.htmx new file mode 100644 index 0000000..d33ef96 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Tabs.htmx @@ -0,0 +1,7 @@ +
    +
    + $$TabsList$$ +
    + $$TabsPanels$$ +
    diff --git a/Htmx.ApiDemo/Templates/Components/Tabs.htmx.cs b/Htmx.ApiDemo/Templates/Components/Tabs.htmx.cs new file mode 100644 index 0000000..2957d7e --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Tabs.htmx.cs @@ -0,0 +1,54 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Tabs component. Tabs are activated client-side via components.js. +/// Pass a list of (Id, Label, Content) tuples. +/// +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($""" + + """); + + panelSb.Append($""" +
    + {content} +
    + """); + } + + _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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Textarea.htmx b/Htmx.ApiDemo/Templates/Components/Textarea.htmx new file mode 100644 index 0000000..4960545 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Textarea.htmx @@ -0,0 +1,15 @@ +
    + $$Label$$ + + $$Description$$ +
    diff --git a/Htmx.ApiDemo/Templates/Components/Textarea.htmx.cs b/Htmx.ApiDemo/Templates/Components/Textarea.htmx.cs new file mode 100644 index 0000000..dff0c7b --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Textarea.htmx.cs @@ -0,0 +1,55 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Textarea component with optional label and description. +/// +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) + ? [] + : $"""""".ToUtf8Bytes(); + + _descriptionData = string.IsNullOrEmpty(description) + ? [] + : $"""

    {description}

    """.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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Toast.htmx b/Htmx.ApiDemo/Templates/Components/Toast.htmx new file mode 100644 index 0000000..d6d9665 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Toast.htmx @@ -0,0 +1,18 @@ + diff --git a/Htmx.ApiDemo/Templates/Components/Toast.htmx.cs b/Htmx.ApiDemo/Templates/Components/Toast.htmx.cs new file mode 100644 index 0000000..83d009d --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Toast.htmx.cs @@ -0,0 +1,34 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// shadcn-style Toast notification. Typically created dynamically via window.showToast(), +/// but can also be server-rendered and injected via htmx. +/// +public sealed class Toast : ToastBase +{ + private static readonly Dictionary 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 = $"""
    {title}
    """.ToUtf8Bytes(); + _descriptionData = string.IsNullOrEmpty(description) + ? [] + : $"""
    {description}
    """.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); +} diff --git a/Htmx.ApiDemo/Templates/Components/ToastViewport.htmx b/Htmx.ApiDemo/Templates/Components/ToastViewport.htmx new file mode 100644 index 0000000..5c0a346 --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/ToastViewport.htmx @@ -0,0 +1,4 @@ +
    + $$Toasts$$ +
    diff --git a/Htmx.ApiDemo/Templates/Components/ToastViewport.htmx.cs b/Htmx.ApiDemo/Templates/Components/ToastViewport.htmx.cs new file mode 100644 index 0000000..5cdd15c --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/ToastViewport.htmx.cs @@ -0,0 +1,20 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// Fixed viewport container for toast notifications. +/// Place once in the page (or layout). Toasts appear via window.showToast() from components.js. +/// +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); +} diff --git a/Htmx.ApiDemo/Templates/Components/Tooltip.htmx b/Htmx.ApiDemo/Templates/Components/Tooltip.htmx new file mode 100644 index 0000000..161064d --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Tooltip.htmx @@ -0,0 +1,9 @@ + + $$Trigger$$ + + $$Text$$ + + diff --git a/Htmx.ApiDemo/Templates/Components/Tooltip.htmx.cs b/Htmx.ApiDemo/Templates/Components/Tooltip.htmx.cs new file mode 100644 index 0000000..c2c6f7f --- /dev/null +++ b/Htmx.ApiDemo/Templates/Components/Tooltip.htmx.cs @@ -0,0 +1,36 @@ +namespace Htmx.ApiDemo.Templates.Components; + +/// +/// CSS-only Tooltip using group-hover. Wraps a trigger element. +/// Position: "top" | "bottom" | "left" | "right" (default: top) +/// +public sealed class Tooltip : TooltipBase +{ + private static readonly Dictionary 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(); + 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); +} diff --git a/Htmx.ApiDemo/Templates/UiDemo.htmx b/Htmx.ApiDemo/Templates/UiDemo.htmx index 9ac6605..00b9688 100644 --- a/Htmx.ApiDemo/Templates/UiDemo.htmx +++ b/Htmx.ApiDemo/Templates/UiDemo.htmx @@ -65,4 +65,256 @@ +
    + + +
    +

    Badge

    +
    + $$BadgeDefault$$ + $$BadgeSecondary$$ + $$BadgeDestructive$$ + $$BadgeOutline$$ +
    +
    + +
    + + +
    +

    Card

    +
    + $$CardDemo$$ +
    +
    + +
    + + +
    +

    Separator

    +
    +

    Horizontal

    + $$SeparatorH$$ +

    Vertical (inline)

    +
    + Section + $$SeparatorV$$ + Another + $$SeparatorV$$ + More +
    +
    +
    + +
    + + +
    +

    Skeleton

    +
    + $$SkeletonTitle$$ + $$SkeletonLine1$$ + $$SkeletonLine2$$ + $$SkeletonAvatar$$ +
    +
    + +
    + + +
    +

    Avatar

    +
    + $$AvatarSm$$ + $$AvatarDefault$$ + $$AvatarLg$$ + $$AvatarImg$$ +
    +
    + +
    + + +
    +

    Progress

    +
    + $$Progress25$$ + $$Progress60$$ + $$Progress100$$ +
    +
    + +
    + + +
    +

    Alert

    +
    + $$AlertDefault$$ + $$AlertDestructive$$ +
    +
    + +
    + + +
    +

    Breadcrumb

    + $$BreadcrumbDemo$$ +
    + +
    + + +
    +

    Checkbox

    +
    + $$CheckboxAccept$$ + $$CheckboxChecked$$ +
    +
    + +
    + + +
    +

    Radio Group

    +
    + $$RadioGroupCol$$ + $$RadioGroupRow$$ +
    +
    + +
    + + +
    +

    Switch

    +
    + $$SwitchOff$$ + $$SwitchOn$$ +
    +
    + +
    + + +
    +

    Textarea

    +
    + $$TextareaDemo$$ +
    +
    + +
    + + +
    +

    Slider

    +
    + $$SliderDemo$$ +
    +
    + +
    + + +
    +

    File Input

    +
    + $$FileInputDemo$$ +
    +
    + +
    + + +
    +

    Table

    + $$TableDemo$$ +
    + +
    + + +
    +

    Pagination

    + $$PaginationDemo$$ +
    + +
    + + +
    +

    Tabs

    +
    + $$TabsDemo$$ +
    +
    + +
    + + +
    +

    Accordion

    +
    + $$AccordionDemo$$ +
    +
    + +
    + + +
    +

    Tooltip

    +
    + $$TooltipTop$$ + $$TooltipBottom$$ + $$TooltipRight$$ +
    +
    + +
    + + +
    +

    Toast

    +
    + + +
    +
    + +
    + + +
    +

    Dialog

    + + $$DialogDemo$$ +
    + +
    + + +
    +

    Dropdown Menu

    + $$DropdownDemo$$ +
    + + + $$ToastViewportDemo$$ + diff --git a/Htmx.ApiDemo/Templates/UiDemo.htmx.cs b/Htmx.ApiDemo/Templates/UiDemo.htmx.cs index fc6bff9..ac778f0 100644 --- a/Htmx.ApiDemo/Templates/UiDemo.htmx.cs +++ b/Htmx.ApiDemo/Templates/UiDemo.htmx.cs @@ -6,6 +6,7 @@ namespace Htmx.ApiDemo.Templates; public sealed class UiDemo : UiDemoBase { + // ── Buttons ────────────────────────────────────────────────────────── public IHtmxComponent BtnDefault { get; } public IHtmxComponent BtnDestructive { get; } public IHtmxComponent BtnOutline { get; } @@ -15,19 +16,106 @@ public sealed class UiDemo : UiDemoBase public IHtmxComponent BtnSm { get; } public IHtmxComponent BtnLg { get; } + // ── Inputs ─────────────────────────────────────────────────────────── public IHtmxComponent InputText { get; } public IHtmxComponent InputEmail { get; } public IHtmxComponent InputPassword { get; } public IHtmxComponent InputSearch { get; } - public IHtmxComponent SelectDemo { get; } - public IHtmxComponent CalendarDemo { get; } - public IHtmxComponent CalendarRangeDemo{ get; } + // ── Select / Calendar / TimePicker ─────────────────────────────────── + public IHtmxComponent SelectDemo { get; } + public IHtmxComponent CalendarDemo { get; } + public IHtmxComponent CalendarRangeDemo { get; } public IHtmxComponent TimePickerDemo { 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() { + // Buttons BtnDefault = new Button("Default"); BtnDestructive = new Button("Destructive", variant: "destructive"); BtnOutline = new Button("Outline", variant: "outline"); @@ -37,12 +125,14 @@ public sealed class UiDemo : UiDemoBase BtnSm = new Button("Small", size: "sm"); BtnLg = new Button("Large", size: "lg"); + // Inputs InputText = new Input("username", label: "Username", placeholder: "Enter username"); InputEmail = new Input("email", inputType: "email", label: "Email", placeholder: "you@example.com"); InputPassword = new Input("password", inputType: "password", label: "Password", placeholder: "••••••••"); InputSearch = new Input("search", inputType: "search", label: "Search", placeholder: "Search…", hxAttrs: "hx-get=\"/search\" hx-trigger=\"keyup changed delay:300ms\" hx-target=\"#search-results\""); + // Select / Calendar / TimePicker SelectDemo = new Select( id: "framework", label: "Framework", @@ -52,9 +142,151 @@ public sealed class UiDemo : UiDemoBase CalendarDemo = new Calendar(id: "demo-cal", name: "demo-date"); CalendarRangeDemo = new CalendarRange(id: "demo-calr", name: "demo-range"); - TimePickerDemo = new TimePicker(name: "time-24h", label: "Time (24h)"); 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: "

    Cards group related content and provide a contained, elevated surface for information.

    ", + footer: ""); + + // 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", "

    This is the overview tab content.

    "), + ("settings", "Settings", "

    Manage your settings here.

    "), + ("billing", "Billing", "

    View billing information.

    "), + ]); + + // 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: "" + + ""); + + // 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); @@ -71,11 +303,66 @@ public sealed class UiDemo : UiDemoBase protected override void RenderInputPassword(HtmxRenderContext ctx) => InputPassword.Render(ctx); protected override void RenderInputSearch(HtmxRenderContext ctx) => InputSearch.Render(ctx); - protected override void RenderSelectDemo(HtmxRenderContext ctx) => SelectDemo.Render(ctx); - protected override void RenderCalendarDemo(HtmxRenderContext ctx) => CalendarDemo.Render(ctx); - protected override void RenderCalendarRangeDemo(HtmxRenderContext ctx) => CalendarRangeDemo.Render(ctx); - protected override void RenderTimePickerDemo(HtmxRenderContext ctx) => TimePickerDemo.Render(ctx); - protected override void RenderTimePicker12hDemo(HtmxRenderContext ctx) => TimePicker12hDemo.Render(ctx); + protected override void RenderSelectDemo(HtmxRenderContext ctx) => SelectDemo.Render(ctx); + protected override void RenderCalendarDemo(HtmxRenderContext ctx) => CalendarDemo.Render(ctx); + protected override void RenderCalendarRangeDemo(HtmxRenderContext ctx) => CalendarRangeDemo.Render(ctx); + protected override void RenderTimePickerDemo(HtmxRenderContext ctx) => TimePickerDemo.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); } diff --git a/Htmx.ApiDemo/wwwroot/css/input.css b/Htmx.ApiDemo/wwwroot/css/input.css index 3d520cd..518e0aa 100644 --- a/Htmx.ApiDemo/wwwroot/css/input.css +++ b/Htmx.ApiDemo/wwwroot/css/input.css @@ -1,5 +1,8 @@ @import "tailwindcss"; +@source "../../**/*.{html,htmx,cs}"; +@source "../../src/**/!(*.g).cs"; + @theme { --color-background: hsl(var(--background)); --color-foreground: hsl(var(--foreground)); diff --git a/Htmx.ApiDemo/wwwroot/css/output.css b/Htmx.ApiDemo/wwwroot/css/output.css index c5a2acc..e9351c3 100644 --- a/Htmx.ApiDemo/wwwroot/css/output.css +++ b/Htmx.ApiDemo/wwwroot/css/output.css @@ -8,9 +8,12 @@ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --color-black: #000; + --color-white: #fff; --spacing: 0.25rem; --container-xs: 20rem; --container-sm: 24rem; + --container-md: 28rem; + --container-lg: 32rem; --container-xl: 36rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); @@ -22,12 +25,18 @@ --text-lg--line-height: calc(1.75 / 1.125); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2 / 1.5); + --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --tracking-tight: -0.025em; + --leading-relaxed: 1.625; + --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); --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-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-font-family: var(--font-sans); @@ -35,10 +44,14 @@ --color-background: hsl(var(--background)); --color-foreground: hsl(var(--foreground)); --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-foreground: hsl(var(--primary-foreground)); --color-secondary: hsl(var(--secondary)); --color-secondary-foreground: hsl(var(--secondary-foreground)); + --color-muted: hsl(var(--muted)); --color-muted-foreground: hsl(var(--muted-foreground)); --color-accent: hsl(var(--accent)); --color-accent-foreground: hsl(var(--accent-foreground)); @@ -198,12 +211,32 @@ } } @layer utilities { + .pointer-events-auto { + pointer-events: auto; + } .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 { position: fixed; } + .relative { + position: relative; + } .static { position: static; } @@ -228,30 +261,108 @@ .end { 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: calc(var(--spacing) * 0); } + .left-1\/2 { + left: calc(1 / 2 * 100%); + } + .left-full { + left: 100%; + } .z-20 { z-index: 20; } .z-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 { margin-top: calc(var(--spacing) * 1); } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } .mt-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 { margin-bottom: calc(var(--spacing) * 1); } + .mb-2 { + margin-bottom: calc(var(--spacing) * 2); + } .mb-3 { margin-bottom: calc(var(--spacing) * 3); } .mb-4 { margin-bottom: calc(var(--spacing) * 4); } + .ml-2 { + margin-left: calc(var(--spacing) * 2); + } + .block { + display: block; + } .flex { display: flex; } @@ -270,12 +381,24 @@ .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 { height: calc(var(--spacing) * 4); } .h-5 { height: calc(var(--spacing) * 5); } + .h-6 { + height: calc(var(--spacing) * 6); + } .h-8 { height: calc(var(--spacing) * 8); } @@ -288,18 +411,45 @@ .h-11 { height: calc(var(--spacing) * 11); } + .h-12 { + height: calc(var(--spacing) * 12); + } + .h-14 { + height: calc(var(--spacing) * 14); + } .h-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-height: calc(var(--spacing) * 4); } + .min-h-20 { + min-height: calc(var(--spacing) * 20); + } .min-h-dvh { min-height: 100dvh; } .min-h-full { min-height: 100%; } + .w-3\.5 { + width: calc(var(--spacing) * 3.5); + } + .w-3\/4 { + width: calc(3 / 4 * 100%); + } .w-4 { width: calc(var(--spacing) * 4); } @@ -315,15 +465,39 @@ .w-10 { width: calc(var(--spacing) * 10); } + .w-11 { + width: calc(var(--spacing) * 11); + } + .w-14 { + width: calc(var(--spacing) * 14); + } .w-16 { width: calc(var(--spacing) * 16); } + .w-20 { + width: calc(var(--spacing) * 20); + } + .w-48 { + width: calc(var(--spacing) * 48); + } .w-64 { width: calc(var(--spacing) * 64); } .w-full { 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-width: var(--container-sm); } @@ -333,6 +507,9 @@ .max-w-xs { max-width: var(--container-xs); } + .min-w-40 { + min-width: calc(var(--spacing) * 40); + } .min-w-72 { min-width: calc(var(--spacing) * 72); } @@ -342,13 +519,41 @@ .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 { --tw-translate-x: -100%; 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; } + .resize-y { + resize: vertical; + } .appearance-none { appearance: none; } @@ -361,18 +566,33 @@ .flex-col { flex-direction: column; } + .flex-col-reverse { + flex-direction: column-reverse; + } + .flex-row { + flex-direction: row; + } .flex-wrap { flex-wrap: wrap; } .items-center { align-items: center; } + .items-start { + align-items: flex-start; + } .justify-between { justify-content: space-between; } .justify-center { justify-content: center; } + .justify-end { + justify-content: flex-end; + } + .justify-start { + justify-content: flex-start; + } .gap-0\.5 { gap: calc(var(--spacing) * 0.5); } @@ -391,6 +611,9 @@ .gap-4 { gap: calc(var(--spacing) * 4); } + .gap-6 { + gap: calc(var(--spacing) * 6); + } .gap-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))); } } + .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 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -429,6 +659,30 @@ 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; } @@ -438,9 +692,15 @@ .rounded-full { border-radius: calc(infinity * 1px); } + .rounded-lg { + border-radius: var(--radius-lg); + } .rounded-md { border-radius: var(--radius-md); } + .rounded-sm { + border-radius: var(--radius-sm); + } .border { border-style: var(--tw-border-style); border-width: 1px; @@ -460,12 +720,21 @@ .border-border { border-color: var(--color-border); } + .border-destructive { + border-color: var(--color-destructive); + } .border-destructive\/30 { border-color: color-mix(in srgb, hsl(var(--destructive)) 30%, transparent); @supports (color: color-mix(in lab, red, red)) { 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-color: var(--color-input); } @@ -478,6 +747,9 @@ background-color: color-mix(in oklab, var(--color-black) 50%, transparent); } } + .bg-border { + background-color: var(--color-border); + } .bg-card { background-color: var(--color-card); } @@ -496,6 +768,21 @@ 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 { background-color: var(--color-primary); } @@ -505,6 +792,18 @@ .bg-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 { padding: calc(var(--spacing) * 4); } @@ -514,6 +813,9 @@ .px-2 { padding-inline: calc(var(--spacing) * 2); } + .px-2\.5 { + padding-inline: calc(var(--spacing) * 2.5); + } .px-3 { padding-inline: calc(var(--spacing) * 3); } @@ -532,6 +834,9 @@ .py-1 { padding-block: calc(var(--spacing) * 1); } + .py-1\.5 { + padding-block: calc(var(--spacing) * 1.5); + } .py-2 { padding-block: calc(var(--spacing) * 2); } @@ -544,9 +849,24 @@ .py-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-align: center; } + .text-left { + text-align: left; + } + .align-middle { + vertical-align: middle; + } .text-2xl { font-size: var(--text-2xl); line-height: var(--tw-leading, var(--text-2xl--line-height)); @@ -579,6 +899,10 @@ --tw-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 { --tw-font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold); @@ -587,9 +911,15 @@ --tw-tracking: var(--tracking-tight); letter-spacing: var(--tracking-tight); } + .break-words { + overflow-wrap: break-word; + } .whitespace-nowrap { white-space: nowrap; } + .text-card-foreground { + color: var(--color-card-foreground); + } .text-destructive { color: var(--color-destructive); } @@ -602,6 +932,9 @@ .text-muted-foreground { color: var(--color-muted-foreground); } + .text-popover-foreground { + color: var(--color-popover-foreground); + } .text-primary { color: var(--color-primary); } @@ -618,9 +951,18 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + .accent-primary { + accent-color: var(--color-primary); + } .opacity-0 { opacity: 0%; } + .opacity-70 { + opacity: 70%; + } + .opacity-90 { + opacity: 90%; + } .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)); 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)); 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 { --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); } + .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 { --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,); 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-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)); @@ -663,6 +1018,14 @@ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); 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 { --tw-duration: 300ms; transition-duration: 300ms; @@ -671,10 +1034,21 @@ --tw-ease: var(--ease-in-out); transition-timing-function: var(--ease-in-out); } + .outline-none { + --tw-outline-style: none; + outline-style: none; + } .select-none { -webkit-user-select: none; user-select: none; } + .group-hover\:opacity-100 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + opacity: 100%; + } + } + } .peer-disabled\:cursor-not-allowed { &:is(:where(.peer):disabled ~ *) { cursor: not-allowed; @@ -685,11 +1059,59 @@ 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 { 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 { @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 { @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 { @media (hover: hover) { @@ -734,6 +1186,13 @@ } } } + .hover\:text-foreground { + &:hover { + @media (hover: hover) { + color: var(--color-foreground); + } + } + } .hover\:underline { &: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 { --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)); } } + .sm\:flex-col { + @media (width >= 40rem) { + flex-direction: column; + } + } .md\:relative { @media (width >= 48rem) { position: relative; @@ -823,6 +1310,11 @@ display: none; } } + .md\:max-w-\[420px\] { + @media (width >= 48rem) { + max-width: 420px; + } + } .md\:translate-x-0 { @media (width >= 48rem) { --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); } } + .\[\&_p\]\:leading-relaxed { + & p { + --tw-leading: var(--leading-relaxed); + line-height: var(--leading-relaxed); + } + } .\[\&\.open\]\:translate-x-0 { &.open { --tw-translate-x: calc(var(--spacing) * 0); 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 { :root { @@ -1121,11 +1660,41 @@ inherits: false; 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 { syntax: "*"; inherits: false; 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 { syntax: "*"; inherits: false; @@ -1310,13 +1879,25 @@ syntax: "*"; inherits: false; } +@keyframes pulse { + 50% { + opacity: 0.5; + } +} @layer properties { @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 { --tw-translate-x: 0; --tw-translate-y: 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-x-reverse: 0; + --tw-divide-y-reverse: 0; --tw-border-style: solid; --tw-leading: initial; --tw-font-weight: initial; diff --git a/Htmx.ApiDemo/wwwroot/js/components.js b/Htmx.ApiDemo/wwwroot/js/components.js index 2b360e9..e8db1f0 100644 --- a/Htmx.ApiDemo/wwwroot/js/components.js +++ b/Htmx.ApiDemo/wwwroot/js/components.js @@ -456,3 +456,262 @@ document.addEventListener('DOMContentLoaded', 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 = + '
    ' + + (title ? '
    ' + title + '
    ' : '') + + (description ? '
    ' + description + '
    ' : '') + + '
    ' + + ''; + + 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); + }); + }); +})(); +