+
Hello, $$User$$!
Welcome to high-performance htmx rendering.
+
+
\ No newline at end of file
diff --git a/Htmx.ApiDemo/Templates/Greeting.htmx.cs b/Htmx.ApiDemo/Templates/Greeting.htmx.cs
index 30ce110..2091c56 100644
--- a/Htmx.ApiDemo/Templates/Greeting.htmx.cs
+++ b/Htmx.ApiDemo/Templates/Greeting.htmx.cs
@@ -1,32 +1,38 @@
+using Immediate.Apis.Shared;
+using Immediate.Handlers.Shared;
+
namespace Htmx.ApiDemo.Templates;
public sealed class Greeting : GreetingBase
{
private byte[] _userData = [];
+ private byte[] _countData = [];
+ private byte[] _greetingIdData = [];
public required string Username { init => _userData = value.ToUtf8Bytes(); }
+ public required int Count { init => _countData = $"{value}".ToUtf8Bytes(); }
+ public required Guid GreetingId { init => _greetingIdData = $"{value}".ToUtf8Bytes(); }
+
+ protected override void RenderCount(HtmxRenderContext context) => context.Writer.WriteUtf8(_countData);
+ protected override void RenderGreetingId(HtmxRenderContext context) => context.Writer.WriteUtf8(_greetingIdData);
protected override void RenderUser(HtmxRenderContext context) => context.Writer.WriteUtf8(_userData);
}
[Handler]
-[MapGet("/greet/{username}")]
+[MapGet("/greet/{username}/{count?}/{id?}")]
public static partial class GetGreetingHandler
{
- public record Query(string Username);
+ public record Query(string Username, int? Count, Guid? Id);
private static ValueTask HandleAsync(
Query query,
- IHttpContextAccessor httpContextAccessor,
+ IHttpContextAccessor httpContextAccessor,
CancellationToken token)
{
- var context = httpContextAccessor.HttpContext;
- if(context is null)
- throw new InvalidOperationException("HttpContext is not available.");
-
- var template = new Greeting { Username = query.Username };
-
- context.Response.ContentType = "text/html; charset=utf-8";
- context.Response.BodyWriter.WriteHtmx(template);
+ var context = httpContextAccessor.HttpContext
+ ?? throw new InvalidOperationException("HttpContext is not available.");
+ var template = new Greeting { Username = query.Username, Count = query.Count + 1 ?? 0, GreetingId = query.Id ?? Guid.NewGuid() };
+ context.WriteHtmxBody(template);
return ValueTask.CompletedTask;
}
}
\ No newline at end of file
diff --git a/Htmx.ApiDemo/Templates/Login.htmx b/Htmx.ApiDemo/Templates/Login.htmx
new file mode 100644
index 0000000..53c2016
--- /dev/null
+++ b/Htmx.ApiDemo/Templates/Login.htmx
@@ -0,0 +1,45 @@
+
+
+
+
+
Sign in
+
Enter your credentials to access your account
+
+
+ $$ErrorMessage$$
+
+
+
+
+ Don't have an account?
+ Sign up
+
+
+
+
diff --git a/Htmx.ApiDemo/Templates/Login.htmx.cs b/Htmx.ApiDemo/Templates/Login.htmx.cs
new file mode 100644
index 0000000..77b81f4
--- /dev/null
+++ b/Htmx.ApiDemo/Templates/Login.htmx.cs
@@ -0,0 +1,88 @@
+using Htmx.ApiDemo.Data;
+using Immediate.Apis.Shared;
+using Immediate.Handlers.Shared;
+using Microsoft.AspNetCore.Antiforgery;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Htmx.ApiDemo.Templates;
+
+public sealed class Login : LoginBase
+{
+ private readonly byte[] _errorData;
+ private readonly byte[] _afTokenData;
+
+ public Login(string? errorMessage = null, string? afToken = null)
+ {
+ _errorData = string.IsNullOrEmpty(errorMessage)
+ ? []
+ : $"""
{System.Web.HttpUtility.HtmlEncode(errorMessage)}
""".ToUtf8Bytes();
+
+ _afTokenData = string.IsNullOrEmpty(afToken)
+ ? []
+ : $"""
""".ToUtf8Bytes();
+ }
+
+ protected override void RenderErrorMessage(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_errorData);
+ protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afTokenData);
+}
+
+
+[Handler]
+[MapGet("/login")]
+public static partial class GetLoginHandler
+{
+ public record Query;
+
+ private static ValueTask HandleAsync(
+ Query _,
+ IHttpContextAccessor httpContextAccessor,
+ IAntiforgery antiforgery,
+ CancellationToken token)
+ {
+ var ctx = httpContextAccessor.HttpContext
+ ?? throw new InvalidOperationException("HttpContext is not available.");
+
+ if (ctx.User.Identity?.IsAuthenticated == true)
+ {
+ ctx.Response.Redirect("/");
+ return ValueTask.CompletedTask;
+ }
+
+ var afTokens = antiforgery.GetAndStoreTokens(ctx);
+ ctx.WriteHtmxPage(new Login(afToken: afTokens.RequestToken), title: "Sign in", appName: "HtmxApp", pageTitle: "Sign in");
+ return ValueTask.CompletedTask;
+ }
+}
+
+
+[Handler]
+[MapPost("/login")]
+public static partial class PostLoginHandler
+{
+ public record Command(
+ [property: FromForm] string Email,
+ [property: FromForm] string Password
+ );
+
+ private static async ValueTask HandleAsync(
+ [AsParameters] Command command,
+ IHttpContextAccessor httpContextAccessor,
+ IAntiforgery antiforgery,
+ AuthService authService,
+ CancellationToken token)
+ {
+ var ctx = httpContextAccessor.HttpContext
+ ?? throw new InvalidOperationException("HttpContext is not available.");
+
+ var (success, error) = await authService.LoginAsync(command.Email, command.Password);
+
+ if (success)
+ {
+ ctx.Response.Redirect("/");
+ return;
+ }
+
+ var afTokens = antiforgery.GetAndStoreTokens(ctx);
+ ctx.WriteHtmxPage(new Login(error, afToken: afTokens.RequestToken), title: "Sign in", appName: "HtmxApp", pageTitle: "Sign in");
+ }
+}
diff --git a/Htmx.ApiDemo/Templates/Logout.cs b/Htmx.ApiDemo/Templates/Logout.cs
new file mode 100644
index 0000000..8b67af7
--- /dev/null
+++ b/Htmx.ApiDemo/Templates/Logout.cs
@@ -0,0 +1,28 @@
+using Htmx.ApiDemo.Data;
+using Immediate.Apis.Shared;
+using Immediate.Handlers.Shared;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Htmx.ApiDemo.Templates;
+
+[Handler]
+[MapPost("/logout")]
+public static partial class PostLogoutHandler
+{
+ // Empty command — [AsParameters] ensures form content-type is accepted
+ // and antiforgery token in the form is validated by the middleware.
+ public record Command;
+
+ private static async ValueTask HandleAsync(
+ [AsParameters] Command _,
+ AuthService authService,
+ IHttpContextAccessor httpContextAccessor,
+ CancellationToken token)
+ {
+ await authService.SignOutAsync();
+
+ var ctx = httpContextAccessor.HttpContext
+ ?? throw new InvalidOperationException("HttpContext is not available.");
+ ctx.Response.Redirect("/login");
+ }
+}
diff --git a/Htmx.ApiDemo/Templates/MainLayout.htmx b/Htmx.ApiDemo/Templates/MainLayout.htmx
new file mode 100644
index 0000000..aec463a
--- /dev/null
+++ b/Htmx.ApiDemo/Templates/MainLayout.htmx
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
$$Title$$
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $$PageTitle$$
+
+
+
+
+
+
+ $$UserSection$$
+
+
+
+
+
+ $$Body$$
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Htmx.ApiDemo/Templates/MainLayout.htmx.cs b/Htmx.ApiDemo/Templates/MainLayout.htmx.cs
new file mode 100644
index 0000000..c88be67
--- /dev/null
+++ b/Htmx.ApiDemo/Templates/MainLayout.htmx.cs
@@ -0,0 +1,91 @@
+using Htmx.ApiDemo;
+using Htmx.ApiDemo.Templates.Components;
+using Immediate.Apis.Shared;
+using Immediate.Handlers.Shared;
+
+namespace Htmx.ApiDemo.Templates;
+
+public sealed class MainLayout : MainLayoutBase
+{
+ private byte[] _titleData = [];
+ private byte[] _appNameData = [];
+ private byte[] _pageTitleData = [];
+ private byte[] _userSectionData = [];
+
+ public IHtmxComponent Body { get; }
+
+ public MainLayout(IHtmxComponent body, string title = "App", string appName = "MyApp",
+ string pageTitle = "Dashboard", string? userName = null, string? afToken = null)
+ {
+ Body = body;
+ _titleData = title.ToUtf8Bytes();
+ _appNameData = appName.ToUtf8Bytes();
+ _pageTitleData = pageTitle.ToUtf8Bytes();
+
+ var afInput = string.IsNullOrEmpty(afToken)
+ ? ""
+ : $"""
""";
+
+ _userSectionData = userName is not null
+ ? $"""
+
+
+ {System.Web.HttpUtility.HtmlEncode(GetInitials(userName))}
+
+
+
+ """.ToUtf8Bytes()
+ : """
+
+ Sign in
+
+ """.ToUtf8Bytes();
+ }
+
+ private static string GetInitials(string name)
+ {
+ var parts = name.Trim().Split(' ', System.StringSplitOptions.RemoveEmptyEntries);
+ return parts.Length >= 2
+ ? $"{parts[0][0]}{parts[^1][0]}".ToUpperInvariant()
+ : name.Length > 0 ? name[0].ToString().ToUpperInvariant() : "?";
+ }
+
+ protected override void RenderBody(HtmxRenderContext context) => Body.Render(context);
+ protected override void RenderTitle(HtmxRenderContext context) => context.Writer.WriteUtf8(_titleData);
+ protected override void RenderAppName(HtmxRenderContext context) => context.Writer.WriteUtf8(_appNameData);
+ protected override void RenderPageTitle(HtmxRenderContext context) => context.Writer.WriteUtf8(_pageTitleData);
+ protected override void RenderUserSection(HtmxRenderContext context) => context.Writer.WriteUtf8(_userSectionData);
+}
+
+
+[Handler]
+[MapGet("/")]
+public static partial class GetIndexHandler
+{
+ public record Command;
+
+ private static ValueTask HandleAsync(
+ Command command,
+ IHttpContextAccessor httpContextAccessor,
+ CancellationToken token)
+ {
+ var context = httpContextAccessor.HttpContext
+ ?? throw new InvalidOperationException("HttpContext is not available.");
+
+ var greet = new Greeting { Username = "Enciphered", Count = 0, GreetingId = Guid.NewGuid() };
+ context.WriteHtmxPage(greet, title: "Home", appName: "HtmxApp", pageTitle: "Home");
+
+ return ValueTask.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/Htmx.ApiDemo/Templates/Register.htmx b/Htmx.ApiDemo/Templates/Register.htmx
new file mode 100644
index 0000000..c08d497
--- /dev/null
+++ b/Htmx.ApiDemo/Templates/Register.htmx
@@ -0,0 +1,63 @@
+
+
+
+
+
Create an account
+
Fill in the details below to get started
+
+
+ $$ErrorMessage$$
+
+
+
+
+ Already have an account?
+ Sign in
+
+
+
+
diff --git a/Htmx.ApiDemo/Templates/Register.htmx.cs b/Htmx.ApiDemo/Templates/Register.htmx.cs
new file mode 100644
index 0000000..3ff1d86
--- /dev/null
+++ b/Htmx.ApiDemo/Templates/Register.htmx.cs
@@ -0,0 +1,99 @@
+using Htmx.ApiDemo.Data;
+using Immediate.Apis.Shared;
+using Immediate.Handlers.Shared;
+using Microsoft.AspNetCore.Antiforgery;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Htmx.ApiDemo.Templates;
+
+public sealed class Register : RegisterBase
+{
+ private readonly byte[] _errorData;
+ private readonly byte[] _afTokenData;
+
+ public Register(string? errorMessage = null, string? afToken = null)
+ {
+ _errorData = string.IsNullOrEmpty(errorMessage)
+ ? []
+ : $"""
{System.Web.HttpUtility.HtmlEncode(errorMessage)}
""".ToUtf8Bytes();
+
+ _afTokenData = string.IsNullOrEmpty(afToken)
+ ? []
+ : $"""
""".ToUtf8Bytes();
+ }
+
+ protected override void RenderErrorMessage(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_errorData);
+ protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afTokenData);
+}
+
+
+[Handler]
+[MapGet("/register")]
+public static partial class GetRegisterHandler
+{
+ public record Query;
+
+ private static ValueTask HandleAsync(
+ Query _,
+ IHttpContextAccessor httpContextAccessor,
+ IAntiforgery antiforgery,
+ CancellationToken token)
+ {
+ var ctx = httpContextAccessor.HttpContext
+ ?? throw new InvalidOperationException("HttpContext is not available.");
+
+ if (ctx.User.Identity?.IsAuthenticated == true)
+ {
+ ctx.Response.Redirect("/");
+ return ValueTask.CompletedTask;
+ }
+
+ var afTokens = antiforgery.GetAndStoreTokens(ctx);
+ ctx.WriteHtmxPage(new Register(afToken: afTokens.RequestToken), title: "Register", appName: "HtmxApp", pageTitle: "Create account");
+ return ValueTask.CompletedTask;
+ }
+}
+
+
+[Handler]
+[MapPost("/register")]
+public static partial class PostRegisterHandler
+{
+ public record Command(
+ [property: FromForm] string Email,
+ [property: FromForm] string Password,
+ [property: FromForm] string ConfirmPassword,
+ [property: FromForm] string? DisplayName
+ );
+
+ private static async ValueTask HandleAsync(
+ [AsParameters] Command command,
+ IHttpContextAccessor httpContextAccessor,
+ IAntiforgery antiforgery,
+ AuthService authService,
+ CancellationToken token)
+ {
+ var ctx = httpContextAccessor.HttpContext
+ ?? throw new InvalidOperationException("HttpContext is not available.");
+
+ if (command.Password != command.ConfirmPassword)
+ {
+ var afTokens1 = antiforgery.GetAndStoreTokens(ctx);
+ ctx.WriteHtmxPage(new Register("Passwords do not match.", afToken: afTokens1.RequestToken),
+ title: "Register", appName: "HtmxApp", pageTitle: "Create account");
+ return;
+ }
+
+ var (success, error) = await authService.RegisterAsync(command.Email, command.Password, command.DisplayName);
+
+ if (success)
+ {
+ ctx.Response.Redirect("/");
+ return;
+ }
+
+ var afTokens2 = antiforgery.GetAndStoreTokens(ctx);
+ ctx.WriteHtmxPage(new Register(error, afToken: afTokens2.RequestToken),
+ title: "Register", appName: "HtmxApp", pageTitle: "Create account");
+ }
+}
diff --git a/Htmx.ApiDemo/Templates/UiDemo.htmx b/Htmx.ApiDemo/Templates/UiDemo.htmx
new file mode 100644
index 0000000..9ac6605
--- /dev/null
+++ b/Htmx.ApiDemo/Templates/UiDemo.htmx
@@ -0,0 +1,68 @@
+
+
+
+
+ Buttons
+
+ $$BtnDefault$$
+ $$BtnDestructive$$
+ $$BtnOutline$$
+ $$BtnSecondary$$
+ $$BtnGhost$$
+ $$BtnLink$$
+ $$BtnSm$$
+ $$BtnLg$$
+
+
+
+
+
+
+
+ Inputs
+
+ $$InputText$$
+ $$InputEmail$$
+ $$InputPassword$$
+ $$InputSearch$$
+
+
+
+
+
+
+
+ Select
+
+ $$SelectDemo$$
+
+
+
+
+
+
+
+ Calendar
+ $$CalendarDemo$$
+
+
+
+
+
+
+ Calendar Range
+ $$CalendarRangeDemo$$
+
+
+
+
+
+
+ Time Picker
+
+ $$TimePickerDemo$$
+ $$TimePicker12hDemo$$
+
+
+
+
diff --git a/Htmx.ApiDemo/Templates/UiDemo.htmx.cs b/Htmx.ApiDemo/Templates/UiDemo.htmx.cs
new file mode 100644
index 0000000..fc6bff9
--- /dev/null
+++ b/Htmx.ApiDemo/Templates/UiDemo.htmx.cs
@@ -0,0 +1,101 @@
+using Htmx.ApiDemo.Templates.Components;
+using Immediate.Apis.Shared;
+using Immediate.Handlers.Shared;
+
+namespace Htmx.ApiDemo.Templates;
+
+public sealed class UiDemo : UiDemoBase
+{
+ public IHtmxComponent BtnDefault { get; }
+ public IHtmxComponent BtnDestructive { get; }
+ public IHtmxComponent BtnOutline { get; }
+ public IHtmxComponent BtnSecondary { get; }
+ public IHtmxComponent BtnGhost { get; }
+ public IHtmxComponent BtnLink { get; }
+ public IHtmxComponent BtnSm { get; }
+ public IHtmxComponent BtnLg { get; }
+
+ 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; }
+ public IHtmxComponent TimePickerDemo { get; }
+ public IHtmxComponent TimePicker12hDemo { get; }
+
+ public UiDemo()
+ {
+ BtnDefault = new Button("Default");
+ BtnDestructive = new Button("Destructive", variant: "destructive");
+ BtnOutline = new Button("Outline", variant: "outline");
+ BtnSecondary = new Button("Secondary", variant: "secondary");
+ BtnGhost = new Button("Ghost", variant: "ghost");
+ BtnLink = new Button("Link", variant: "link");
+ BtnSm = new Button("Small", size: "sm");
+ BtnLg = new Button("Large", size: "lg");
+
+ 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\"");
+
+ SelectDemo = new Select(
+ id: "framework",
+ label: "Framework",
+ options: [("htmx", "HTMX"), ("react", "React"), ("vue", "Vue"), ("svelte", "Svelte")],
+ selectedValue: "htmx",
+ description: "Choose your preferred framework");
+
+ 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);
+ }
+
+ protected override void RenderBtnDefault(HtmxRenderContext ctx) => BtnDefault.Render(ctx);
+ protected override void RenderBtnDestructive(HtmxRenderContext ctx) => BtnDestructive.Render(ctx);
+ protected override void RenderBtnOutline(HtmxRenderContext ctx) => BtnOutline.Render(ctx);
+ protected override void RenderBtnSecondary(HtmxRenderContext ctx) => BtnSecondary.Render(ctx);
+ protected override void RenderBtnGhost(HtmxRenderContext ctx) => BtnGhost.Render(ctx);
+ protected override void RenderBtnLink(HtmxRenderContext ctx) => BtnLink.Render(ctx);
+ protected override void RenderBtnSm(HtmxRenderContext ctx) => BtnSm.Render(ctx);
+ protected override void RenderBtnLg(HtmxRenderContext ctx) => BtnLg.Render(ctx);
+
+ protected override void RenderInputText(HtmxRenderContext ctx) => InputText.Render(ctx);
+ protected override void RenderInputEmail(HtmxRenderContext ctx) => InputEmail.Render(ctx);
+ 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);
+}
+
+
+[Handler]
+[MapGet("/ui-demo")]
+public static partial class GetUiDemoHandler
+{
+ public record Query;
+
+ private static ValueTask HandleAsync(
+ Query query,
+ IHttpContextAccessor httpContextAccessor,
+ CancellationToken token)
+ {
+ var context = httpContextAccessor.HttpContext
+ ?? throw new InvalidOperationException("HttpContext is not available.");
+
+ var page = new UiDemo();
+ context.WriteHtmxPage(page, title: "UI Demo", appName: "HtmxApp", pageTitle: "UI Components");
+
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/Htmx.ApiDemo/appsettings.json b/Htmx.ApiDemo/appsettings.json
index 4d56694..1dd6e4c 100644
--- a/Htmx.ApiDemo/appsettings.json
+++ b/Htmx.ApiDemo/appsettings.json
@@ -1,4 +1,8 @@
{
+ "ConnectionStrings": {
+ "DefaultConnection": "mongodb://localhost:27017"
+ },
+ "MongoDbName": "HtmxAppDb",
"Logging": {
"LogLevel": {
"Default": "Information",
diff --git a/Htmx.ApiDemo/package.json b/Htmx.ApiDemo/package.json
new file mode 100644
index 0000000..13622c0
--- /dev/null
+++ b/Htmx.ApiDemo/package.json
@@ -0,0 +1,6 @@
+{
+ "dependencies": {
+ "@tailwindcss/cli": "^4.2.4",
+ "tailwindcss": "^4.2.4"
+ }
+}
diff --git a/Htmx.ApiDemo/wwwroot/css/input.css b/Htmx.ApiDemo/wwwroot/css/input.css
new file mode 100644
index 0000000..3d520cd
--- /dev/null
+++ b/Htmx.ApiDemo/wwwroot/css/input.css
@@ -0,0 +1,187 @@
+@import "tailwindcss";
+
+@theme {
+ --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));
+
+ --color-destructive: hsl(var(--destructive));
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
+
+ --color-border: hsl(var(--border));
+ --color-input: hsl(var(--input));
+ --color-ring: hsl(var(--ring));
+
+ --radius-lg: var(--radius);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-sm: calc(var(--radius) - 4px);
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
+}
+
+/* ── Calendar component ───────────────────────────────────────────────── */
+@layer components {
+ .cal-dow {
+ @apply text-xs font-medium text-muted-foreground py-1;
+ }
+
+ .cal-day {
+ @apply h-9 w-full rounded-md text-sm text-center
+ text-foreground bg-transparent
+ hover:bg-accent hover:text-accent-foreground
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
+ transition-colors cursor-pointer;
+ }
+
+ .cal-day-selected {
+ @apply bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground;
+ }
+
+ .cal-day-today {
+ @apply font-semibold underline underline-offset-2;
+ }
+
+ .cal-nav {
+ @apply text-lg leading-none;
+ }
+
+ /* ── Month / year quick-pick grid ── */
+ .cal-view-btn {
+ @apply h-9 w-full rounded-md text-sm text-center
+ text-foreground bg-transparent
+ hover:bg-accent hover:text-accent-foreground
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
+ transition-colors cursor-pointer;
+ }
+
+ .cal-view-btn-selected {
+ @apply bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground;
+ }
+
+ /* ── CalendarRange day states ── */
+ .calr-day {
+ @apply h-9 w-full text-sm text-center text-foreground
+ transition-colors cursor-pointer focus-visible:outline-none
+ focus-visible:ring-2 focus-visible:ring-ring;
+ }
+
+ /* Plain days (no range involvement) */
+ .calr-day-plain {
+ @apply rounded-md hover:bg-accent hover:text-accent-foreground;
+ }
+
+ /* Start cap — primary, rounded left only */
+ .calr-day-start {
+ @apply rounded-l-md bg-primary text-primary-foreground
+ hover:bg-primary;
+ }
+
+ /* End cap — primary, rounded right only */
+ .calr-day-end {
+ @apply rounded-r-md bg-primary text-primary-foreground
+ hover:bg-primary;
+ }
+
+ /* Days strictly between start and end */
+ .calr-day-mid {
+ @apply rounded-none bg-accent text-accent-foreground
+ hover:bg-accent;
+ }
+}
+
+/* ── Select – custom caret via background SVG ─────────────────────────── */
+@layer components {
+ select.appearance-none {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ padding-right: 2.25rem;
+ }
+}
+
+/* ── TimePicker – hide number spinner arrows ──────────────────────────── */
+@layer utilities {
+ input[type=number].timepicker-hour,
+ input[type=number].timepicker-minute {
+ -moz-appearance: textfield;
+ }
+ input[type=number].timepicker-hour::-webkit-outer-spin-button,
+ input[type=number].timepicker-hour::-webkit-inner-spin-button,
+ input[type=number].timepicker-minute::-webkit-outer-spin-button,
+ input[type=number].timepicker-minute::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+}
diff --git a/Htmx.ApiDemo/wwwroot/css/output.css b/Htmx.ApiDemo/wwwroot/css/output.css
new file mode 100644
index 0000000..c5a2acc
--- /dev/null
+++ b/Htmx.ApiDemo/wwwroot/css/output.css
@@ -0,0 +1,1365 @@
+/*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */
+@layer properties;
+@layer theme, base, components, utilities;
+@layer theme {
+ :root, :host {
+ --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
+ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
+ "Courier New", monospace;
+ --color-black: #000;
+ --spacing: 0.25rem;
+ --container-xs: 20rem;
+ --container-sm: 24rem;
+ --container-xl: 36rem;
+ --text-xs: 0.75rem;
+ --text-xs--line-height: calc(1 / 0.75);
+ --text-sm: 0.875rem;
+ --text-sm--line-height: calc(1.25 / 0.875);
+ --text-base: 1rem;
+ --text-base--line-height: calc(1.5 / 1);
+ --text-lg: 1.125rem;
+ --text-lg--line-height: calc(1.75 / 1.125);
+ --text-2xl: 1.5rem;
+ --text-2xl--line-height: calc(2 / 1.5);
+ --font-weight-medium: 500;
+ --font-weight-semibold: 600;
+ --font-weight-bold: 700;
+ --tracking-tight: -0.025em;
+ --radius-md: calc(var(--radius) - 2px);
+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
+ --default-transition-duration: 150ms;
+ --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ --default-font-family: var(--font-sans);
+ --default-mono-font-family: var(--font-mono);
+ --color-background: hsl(var(--background));
+ --color-foreground: hsl(var(--foreground));
+ --color-card: hsl(var(--card));
+ --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-foreground: hsl(var(--muted-foreground));
+ --color-accent: hsl(var(--accent));
+ --color-accent-foreground: hsl(var(--accent-foreground));
+ --color-destructive: hsl(var(--destructive));
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
+ --color-border: hsl(var(--border));
+ --color-input: hsl(var(--input));
+ --color-ring: hsl(var(--ring));
+ }
+}
+@layer base {
+ *, ::after, ::before, ::backdrop, ::file-selector-button {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ border: 0 solid;
+ }
+ html, :host {
+ line-height: 1.5;
+ -webkit-text-size-adjust: 100%;
+ tab-size: 4;
+ font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
+ font-feature-settings: var(--default-font-feature-settings, normal);
+ font-variation-settings: var(--default-font-variation-settings, normal);
+ -webkit-tap-highlight-color: transparent;
+ }
+ hr {
+ height: 0;
+ color: inherit;
+ border-top-width: 1px;
+ }
+ abbr:where([title]) {
+ -webkit-text-decoration: underline dotted;
+ text-decoration: underline dotted;
+ }
+ h1, h2, h3, h4, h5, h6 {
+ font-size: inherit;
+ font-weight: inherit;
+ }
+ a {
+ color: inherit;
+ -webkit-text-decoration: inherit;
+ text-decoration: inherit;
+ }
+ b, strong {
+ font-weight: bolder;
+ }
+ code, kbd, samp, pre {
+ font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
+ font-feature-settings: var(--default-mono-font-feature-settings, normal);
+ font-variation-settings: var(--default-mono-font-variation-settings, normal);
+ font-size: 1em;
+ }
+ small {
+ font-size: 80%;
+ }
+ sub, sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+ }
+ sub {
+ bottom: -0.25em;
+ }
+ sup {
+ top: -0.5em;
+ }
+ table {
+ text-indent: 0;
+ border-color: inherit;
+ border-collapse: collapse;
+ }
+ :-moz-focusring {
+ outline: auto;
+ }
+ progress {
+ vertical-align: baseline;
+ }
+ summary {
+ display: list-item;
+ }
+ ol, ul, menu {
+ list-style: none;
+ }
+ img, svg, video, canvas, audio, iframe, embed, object {
+ display: block;
+ vertical-align: middle;
+ }
+ img, video {
+ max-width: 100%;
+ height: auto;
+ }
+ button, input, select, optgroup, textarea, ::file-selector-button {
+ font: inherit;
+ font-feature-settings: inherit;
+ font-variation-settings: inherit;
+ letter-spacing: inherit;
+ color: inherit;
+ border-radius: 0;
+ background-color: transparent;
+ opacity: 1;
+ }
+ :where(select:is([multiple], [size])) optgroup {
+ font-weight: bolder;
+ }
+ :where(select:is([multiple], [size])) optgroup option {
+ padding-inline-start: 20px;
+ }
+ ::file-selector-button {
+ margin-inline-end: 4px;
+ }
+ ::placeholder {
+ opacity: 1;
+ }
+ @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
+ ::placeholder {
+ color: currentcolor;
+ @supports (color: color-mix(in lab, red, red)) {
+ color: color-mix(in oklab, currentcolor 50%, transparent);
+ }
+ }
+ }
+ textarea {
+ resize: vertical;
+ }
+ ::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+ ::-webkit-date-and-time-value {
+ min-height: 1lh;
+ text-align: inherit;
+ }
+ ::-webkit-datetime-edit {
+ display: inline-flex;
+ }
+ ::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+ }
+ ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
+ padding-block: 0;
+ }
+ ::-webkit-calendar-picker-indicator {
+ line-height: 1;
+ }
+ :-moz-ui-invalid {
+ box-shadow: none;
+ }
+ button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
+ appearance: button;
+ }
+ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
+ height: auto;
+ }
+ [hidden]:where(:not([hidden="until-found"])) {
+ display: none !important;
+ }
+}
+@layer utilities {
+ .pointer-events-none {
+ pointer-events: none;
+ }
+ .fixed {
+ position: fixed;
+ }
+ .static {
+ position: static;
+ }
+ .inset-0 {
+ inset: calc(var(--spacing) * 0);
+ }
+ .inset-y-0 {
+ inset-block: calc(var(--spacing) * 0);
+ }
+ .-start {
+ inset-inline-start: calc(var(--spacing) * -1);
+ }
+ .start {
+ inset-inline-start: var(--spacing);
+ }
+ .\!end {
+ inset-inline-end: var(--spacing) !important;
+ }
+ .-end {
+ inset-inline-end: calc(var(--spacing) * -1);
+ }
+ .end {
+ inset-inline-end: var(--spacing);
+ }
+ .left-0 {
+ left: calc(var(--spacing) * 0);
+ }
+ .z-20 {
+ z-index: 20;
+ }
+ .z-30 {
+ z-index: 30;
+ }
+ .mt-1 {
+ margin-top: calc(var(--spacing) * 1);
+ }
+ .mt-3 {
+ margin-top: calc(var(--spacing) * 3);
+ }
+ .mb-1 {
+ margin-bottom: calc(var(--spacing) * 1);
+ }
+ .mb-3 {
+ margin-bottom: calc(var(--spacing) * 3);
+ }
+ .mb-4 {
+ margin-bottom: calc(var(--spacing) * 4);
+ }
+ .flex {
+ display: flex;
+ }
+ .grid {
+ display: grid;
+ }
+ .hidden {
+ display: none;
+ }
+ .inline {
+ display: inline;
+ }
+ .inline-block {
+ display: inline-block;
+ }
+ .inline-flex {
+ display: inline-flex;
+ }
+ .h-4 {
+ height: calc(var(--spacing) * 4);
+ }
+ .h-5 {
+ height: calc(var(--spacing) * 5);
+ }
+ .h-8 {
+ height: calc(var(--spacing) * 8);
+ }
+ .h-9 {
+ height: calc(var(--spacing) * 9);
+ }
+ .h-10 {
+ height: calc(var(--spacing) * 10);
+ }
+ .h-11 {
+ height: calc(var(--spacing) * 11);
+ }
+ .h-16 {
+ height: calc(var(--spacing) * 16);
+ }
+ .min-h-4 {
+ min-height: calc(var(--spacing) * 4);
+ }
+ .min-h-dvh {
+ min-height: 100dvh;
+ }
+ .min-h-full {
+ min-height: 100%;
+ }
+ .w-4 {
+ width: calc(var(--spacing) * 4);
+ }
+ .w-5 {
+ width: calc(var(--spacing) * 5);
+ }
+ .w-8 {
+ width: calc(var(--spacing) * 8);
+ }
+ .w-9 {
+ width: calc(var(--spacing) * 9);
+ }
+ .w-10 {
+ width: calc(var(--spacing) * 10);
+ }
+ .w-16 {
+ width: calc(var(--spacing) * 16);
+ }
+ .w-64 {
+ width: calc(var(--spacing) * 64);
+ }
+ .w-full {
+ width: 100%;
+ }
+ .max-w-sm {
+ max-width: var(--container-sm);
+ }
+ .max-w-xl {
+ max-width: var(--container-xl);
+ }
+ .max-w-xs {
+ max-width: var(--container-xs);
+ }
+ .min-w-72 {
+ min-width: calc(var(--spacing) * 72);
+ }
+ .flex-1 {
+ flex: 1;
+ }
+ .shrink-0 {
+ flex-shrink: 0;
+ }
+ .-translate-x-full {
+ --tw-translate-x: -100%;
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
+ .cursor-pointer {
+ cursor: pointer;
+ }
+ .appearance-none {
+ appearance: none;
+ }
+ .grid-cols-1 {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+ }
+ .grid-cols-7 {
+ grid-template-columns: repeat(7, minmax(0, 1fr));
+ }
+ .flex-col {
+ flex-direction: column;
+ }
+ .flex-wrap {
+ flex-wrap: wrap;
+ }
+ .items-center {
+ align-items: center;
+ }
+ .justify-between {
+ justify-content: space-between;
+ }
+ .justify-center {
+ justify-content: center;
+ }
+ .gap-0\.5 {
+ gap: calc(var(--spacing) * 0.5);
+ }
+ .gap-1 {
+ gap: calc(var(--spacing) * 1);
+ }
+ .gap-1\.5 {
+ gap: calc(var(--spacing) * 1.5);
+ }
+ .gap-2 {
+ gap: calc(var(--spacing) * 2);
+ }
+ .gap-3 {
+ gap: calc(var(--spacing) * 3);
+ }
+ .gap-4 {
+ gap: calc(var(--spacing) * 4);
+ }
+ .gap-8 {
+ gap: calc(var(--spacing) * 8);
+ }
+ .space-y-1 {
+ :where(& > :not(:last-child)) {
+ --tw-space-y-reverse: 0;
+ margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));
+ margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));
+ }
+ }
+ .space-y-2 {
+ :where(& > :not(:last-child)) {
+ --tw-space-y-reverse: 0;
+ margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));
+ margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
+ }
+ }
+ .space-y-4 {
+ :where(& > :not(:last-child)) {
+ --tw-space-y-reverse: 0;
+ margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));
+ margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
+ }
+ }
+ .space-y-6 {
+ :where(& > :not(:last-child)) {
+ --tw-space-y-reverse: 0;
+ margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));
+ margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
+ }
+ }
+ .space-y-10 {
+ :where(& > :not(:last-child)) {
+ --tw-space-y-reverse: 0;
+ margin-block-start: calc(calc(var(--spacing) * 10) * var(--tw-space-y-reverse));
+ margin-block-end: calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)));
+ }
+ }
+ .overflow-hidden {
+ overflow: hidden;
+ }
+ .overflow-y-auto {
+ overflow-y: auto;
+ }
+ .rounded-full {
+ border-radius: calc(infinity * 1px);
+ }
+ .rounded-md {
+ border-radius: var(--radius-md);
+ }
+ .border {
+ border-style: var(--tw-border-style);
+ border-width: 1px;
+ }
+ .border-t {
+ border-top-style: var(--tw-border-style);
+ border-top-width: 1px;
+ }
+ .border-r {
+ border-right-style: var(--tw-border-style);
+ border-right-width: 1px;
+ }
+ .border-b {
+ border-bottom-style: var(--tw-border-style);
+ border-bottom-width: 1px;
+ }
+ .border-border {
+ border-color: var(--color-border);
+ }
+ .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-input {
+ border-color: var(--color-input);
+ }
+ .bg-background {
+ background-color: var(--color-background);
+ }
+ .bg-black\/50 {
+ 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);
+ }
+ }
+ .bg-card {
+ background-color: var(--color-card);
+ }
+ .bg-card\/80 {
+ background-color: color-mix(in srgb, hsl(var(--card)) 80%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ background-color: color-mix(in oklab, var(--color-card) 80%, transparent);
+ }
+ }
+ .bg-destructive {
+ background-color: var(--color-destructive);
+ }
+ .bg-destructive\/15 {
+ background-color: color-mix(in srgb, hsl(var(--destructive)) 15%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ background-color: color-mix(in oklab, var(--color-destructive) 15%, transparent);
+ }
+ }
+ .bg-primary {
+ background-color: var(--color-primary);
+ }
+ .bg-secondary {
+ background-color: var(--color-secondary);
+ }
+ .bg-transparent {
+ background-color: transparent;
+ }
+ .p-4 {
+ padding: calc(var(--spacing) * 4);
+ }
+ .p-6 {
+ padding: calc(var(--spacing) * 6);
+ }
+ .px-2 {
+ padding-inline: calc(var(--spacing) * 2);
+ }
+ .px-3 {
+ padding-inline: calc(var(--spacing) * 3);
+ }
+ .px-4 {
+ padding-inline: calc(var(--spacing) * 4);
+ }
+ .px-5 {
+ padding-inline: calc(var(--spacing) * 5);
+ }
+ .px-8 {
+ padding-inline: calc(var(--spacing) * 8);
+ }
+ .py-0\.5 {
+ padding-block: calc(var(--spacing) * 0.5);
+ }
+ .py-1 {
+ padding-block: calc(var(--spacing) * 1);
+ }
+ .py-2 {
+ padding-block: calc(var(--spacing) * 2);
+ }
+ .py-3 {
+ padding-block: calc(var(--spacing) * 3);
+ }
+ .py-4 {
+ padding-block: calc(var(--spacing) * 4);
+ }
+ .py-12 {
+ padding-block: calc(var(--spacing) * 12);
+ }
+ .text-center {
+ text-align: center;
+ }
+ .text-2xl {
+ font-size: var(--text-2xl);
+ line-height: var(--tw-leading, var(--text-2xl--line-height));
+ }
+ .text-base {
+ font-size: var(--text-base);
+ line-height: var(--tw-leading, var(--text-base--line-height));
+ }
+ .text-lg {
+ font-size: var(--text-lg);
+ line-height: var(--tw-leading, var(--text-lg--line-height));
+ }
+ .text-sm {
+ font-size: var(--text-sm);
+ line-height: var(--tw-leading, var(--text-sm--line-height));
+ }
+ .text-xs {
+ font-size: var(--text-xs);
+ line-height: var(--tw-leading, var(--text-xs--line-height));
+ }
+ .leading-none {
+ --tw-leading: 1;
+ line-height: 1;
+ }
+ .font-bold {
+ --tw-font-weight: var(--font-weight-bold);
+ font-weight: var(--font-weight-bold);
+ }
+ .font-medium {
+ --tw-font-weight: var(--font-weight-medium);
+ font-weight: var(--font-weight-medium);
+ }
+ .font-semibold {
+ --tw-font-weight: var(--font-weight-semibold);
+ font-weight: var(--font-weight-semibold);
+ }
+ .tracking-tight {
+ --tw-tracking: var(--tracking-tight);
+ letter-spacing: var(--tracking-tight);
+ }
+ .whitespace-nowrap {
+ white-space: nowrap;
+ }
+ .text-destructive {
+ color: var(--color-destructive);
+ }
+ .text-destructive-foreground {
+ color: var(--color-destructive-foreground);
+ }
+ .text-foreground {
+ color: var(--color-foreground);
+ }
+ .text-muted-foreground {
+ color: var(--color-muted-foreground);
+ }
+ .text-primary {
+ color: var(--color-primary);
+ }
+ .text-primary-foreground {
+ color: var(--color-primary-foreground);
+ }
+ .text-secondary-foreground {
+ color: var(--color-secondary-foreground);
+ }
+ .underline-offset-4 {
+ text-underline-offset: 4px;
+ }
+ .antialiased {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ .opacity-0 {
+ opacity: 0%;
+ }
+ .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);
+ }
+ .shadow-lg {
+ --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-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-offset-background {
+ --tw-ring-offset-color: var(--color-background);
+ }
+ .outline {
+ outline-style: var(--tw-outline-style);
+ outline-width: 1px;
+ }
+ .filter {
+ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
+ }
+ .backdrop-blur {
+ --tw-backdrop-blur: blur(8px);
+ -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-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));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ }
+ .transition-opacity {
+ transition-property: opacity;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ }
+ .transition-transform {
+ transition-property: transform, translate, scale, rotate;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ }
+ .duration-300 {
+ --tw-duration: 300ms;
+ transition-duration: 300ms;
+ }
+ .ease-in-out {
+ --tw-ease: var(--ease-in-out);
+ transition-timing-function: var(--ease-in-out);
+ }
+ .select-none {
+ -webkit-user-select: none;
+ user-select: none;
+ }
+ .peer-disabled\:cursor-not-allowed {
+ &:is(:where(.peer):disabled ~ *) {
+ cursor: not-allowed;
+ }
+ }
+ .peer-disabled\:opacity-70 {
+ &:is(:where(.peer):disabled ~ *) {
+ opacity: 70%;
+ }
+ }
+ .placeholder\:text-muted-foreground {
+ &::placeholder {
+ color: var(--color-muted-foreground);
+ }
+ }
+ .hover\:bg-accent {
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-accent);
+ }
+ }
+ }
+ .hover\:bg-destructive\/90 {
+ &:hover {
+ @media (hover: hover) {
+ background-color: color-mix(in srgb, hsl(var(--destructive)) 90%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ background-color: color-mix(in oklab, var(--color-destructive) 90%, transparent);
+ }
+ }
+ }
+ }
+ .hover\:bg-primary\/90 {
+ &:hover {
+ @media (hover: hover) {
+ background-color: color-mix(in srgb, hsl(var(--primary)) 90%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ background-color: color-mix(in oklab, var(--color-primary) 90%, transparent);
+ }
+ }
+ }
+ }
+ .hover\:bg-secondary\/80 {
+ &:hover {
+ @media (hover: hover) {
+ background-color: color-mix(in srgb, hsl(var(--secondary)) 80%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ background-color: color-mix(in oklab, var(--color-secondary) 80%, transparent);
+ }
+ }
+ }
+ }
+ .hover\:text-accent-foreground {
+ &:hover {
+ @media (hover: hover) {
+ color: var(--color-accent-foreground);
+ }
+ }
+ }
+ .hover\:underline {
+ &:hover {
+ @media (hover: hover) {
+ text-decoration-line: underline;
+ }
+ }
+ }
+ .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);
+ 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-ring {
+ &:focus {
+ --tw-ring-color: var(--color-ring);
+ }
+ }
+ .focus\:ring-offset-2 {
+ &:focus {
+ --tw-ring-offset-width: 2px;
+ --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ }
+ }
+ .focus\:outline-none {
+ &:focus {
+ --tw-outline-style: none;
+ outline-style: none;
+ }
+ }
+ .focus-visible\:ring-1 {
+ &:focus-visible {
+ --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-visible\:ring-2 {
+ &:focus-visible {
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + 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-visible\:ring-ring {
+ &:focus-visible {
+ --tw-ring-color: var(--color-ring);
+ }
+ }
+ .focus-visible\:ring-offset-2 {
+ &:focus-visible {
+ --tw-ring-offset-width: 2px;
+ --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ }
+ }
+ .focus-visible\:outline-none {
+ &:focus-visible {
+ --tw-outline-style: none;
+ outline-style: none;
+ }
+ }
+ .disabled\:pointer-events-none {
+ &:disabled {
+ pointer-events: none;
+ }
+ }
+ .disabled\:cursor-not-allowed {
+ &:disabled {
+ cursor: not-allowed;
+ }
+ }
+ .disabled\:opacity-50 {
+ &:disabled {
+ opacity: 50%;
+ }
+ }
+ .sm\:grid-cols-2 {
+ @media (width >= 40rem) {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ }
+ .md\:relative {
+ @media (width >= 48rem) {
+ position: relative;
+ }
+ }
+ .md\:hidden {
+ @media (width >= 48rem) {
+ display: none;
+ }
+ }
+ .md\:translate-x-0 {
+ @media (width >= 48rem) {
+ --tw-translate-x: calc(var(--spacing) * 0);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
+ }
+ .md\:px-6 {
+ @media (width >= 48rem) {
+ padding-inline: calc(var(--spacing) * 6);
+ }
+ }
+ .md\:shadow-none {
+ @media (width >= 48rem) {
+ --tw-shadow: 0 0 #0000;
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
+ }
+ .\[\&\.open\]\:translate-x-0 {
+ &.open {
+ --tw-translate-x: calc(var(--spacing) * 0);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
+ }
+}
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+ --radius: 0.5rem;
+ }
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ }
+}
+@layer base {
+ * {
+ border-color: var(--color-border);
+ }
+ body {
+ background-color: var(--color-background);
+ color: var(--color-foreground);
+ }
+}
+@layer components {
+ .cal-dow {
+ padding-block: calc(var(--spacing) * 1);
+ font-size: var(--text-xs);
+ line-height: var(--tw-leading, var(--text-xs--line-height));
+ --tw-font-weight: var(--font-weight-medium);
+ font-weight: var(--font-weight-medium);
+ color: var(--color-muted-foreground);
+ }
+ .cal-day {
+ height: calc(var(--spacing) * 9);
+ width: 100%;
+ cursor: pointer;
+ border-radius: var(--radius-md);
+ background-color: transparent;
+ text-align: center;
+ font-size: var(--text-sm);
+ line-height: var(--tw-leading, var(--text-sm--line-height));
+ color: var(--color-foreground);
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-accent);
+ }
+ }
+ &:hover {
+ @media (hover: hover) {
+ color: var(--color-accent-foreground);
+ }
+ }
+ &:focus-visible {
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + 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-visible {
+ --tw-ring-color: var(--color-ring);
+ }
+ &:focus-visible {
+ --tw-outline-style: none;
+ outline-style: none;
+ }
+ }
+ .cal-day-selected {
+ background-color: var(--color-primary);
+ color: var(--color-primary-foreground);
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-primary);
+ }
+ }
+ &:hover {
+ @media (hover: hover) {
+ color: var(--color-primary-foreground);
+ }
+ }
+ }
+ .cal-day-today {
+ --tw-font-weight: var(--font-weight-semibold);
+ font-weight: var(--font-weight-semibold);
+ text-decoration-line: underline;
+ text-underline-offset: 2px;
+ }
+ .cal-nav {
+ font-size: var(--text-lg);
+ line-height: var(--tw-leading, var(--text-lg--line-height));
+ --tw-leading: 1;
+ line-height: 1;
+ }
+ .cal-view-btn {
+ height: calc(var(--spacing) * 9);
+ width: 100%;
+ cursor: pointer;
+ border-radius: var(--radius-md);
+ background-color: transparent;
+ text-align: center;
+ font-size: var(--text-sm);
+ line-height: var(--tw-leading, var(--text-sm--line-height));
+ color: var(--color-foreground);
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-accent);
+ }
+ }
+ &:hover {
+ @media (hover: hover) {
+ color: var(--color-accent-foreground);
+ }
+ }
+ &:focus-visible {
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + 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-visible {
+ --tw-ring-color: var(--color-ring);
+ }
+ &:focus-visible {
+ --tw-outline-style: none;
+ outline-style: none;
+ }
+ }
+ .cal-view-btn-selected {
+ background-color: var(--color-primary);
+ color: var(--color-primary-foreground);
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-primary);
+ }
+ }
+ &:hover {
+ @media (hover: hover) {
+ color: var(--color-primary-foreground);
+ }
+ }
+ }
+ .calr-day {
+ height: calc(var(--spacing) * 9);
+ width: 100%;
+ cursor: pointer;
+ text-align: center;
+ font-size: var(--text-sm);
+ line-height: var(--tw-leading, var(--text-sm--line-height));
+ color: var(--color-foreground);
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ &:focus-visible {
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + 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-visible {
+ --tw-ring-color: var(--color-ring);
+ }
+ &:focus-visible {
+ --tw-outline-style: none;
+ outline-style: none;
+ }
+ }
+ .calr-day-plain {
+ border-radius: var(--radius-md);
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-accent);
+ }
+ }
+ &:hover {
+ @media (hover: hover) {
+ color: var(--color-accent-foreground);
+ }
+ }
+ }
+ .calr-day-start {
+ border-top-left-radius: var(--radius-md);
+ border-bottom-left-radius: var(--radius-md);
+ background-color: var(--color-primary);
+ color: var(--color-primary-foreground);
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-primary);
+ }
+ }
+ }
+ .calr-day-end {
+ border-top-right-radius: var(--radius-md);
+ border-bottom-right-radius: var(--radius-md);
+ background-color: var(--color-primary);
+ color: var(--color-primary-foreground);
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-primary);
+ }
+ }
+ }
+ .calr-day-mid {
+ border-radius: 0;
+ background-color: var(--color-accent);
+ color: var(--color-accent-foreground);
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-accent);
+ }
+ }
+ }
+}
+@layer components {
+ select.appearance-none {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ padding-right: 2.25rem;
+ }
+}
+@layer utilities {
+ input[type=number].timepicker-hour, input[type=number].timepicker-minute {
+ -moz-appearance: textfield;
+ }
+ input[type=number].timepicker-hour::-webkit-outer-spin-button, input[type=number].timepicker-hour::-webkit-inner-spin-button, input[type=number].timepicker-minute::-webkit-outer-spin-button, input[type=number].timepicker-minute::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+}
+@property --tw-translate-x {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-translate-y {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-translate-z {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-space-y-reverse {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-border-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+}
+@property --tw-leading {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-font-weight {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-tracking {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-shadow-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-shadow-alpha {
+ syntax: "
";
+ inherits: false;
+ initial-value: 100%;
+}
+@property --tw-inset-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-inset-shadow-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-inset-shadow-alpha {
+ syntax: "";
+ inherits: false;
+ initial-value: 100%;
+}
+@property --tw-ring-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ring-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-inset-ring-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-inset-ring-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-ring-inset {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ring-offset-width {
+ syntax: "";
+ inherits: false;
+ initial-value: 0px;
+}
+@property --tw-ring-offset-color {
+ syntax: "*";
+ inherits: false;
+ initial-value: #fff;
+}
+@property --tw-ring-offset-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-outline-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+}
+@property --tw-blur {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-brightness {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-contrast {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-grayscale {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-hue-rotate {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-invert {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-opacity {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-saturate {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-sepia {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-drop-shadow {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-drop-shadow-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-drop-shadow-alpha {
+ syntax: "";
+ inherits: false;
+ initial-value: 100%;
+}
+@property --tw-drop-shadow-size {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-blur {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-brightness {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-contrast {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-grayscale {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-hue-rotate {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-invert {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-opacity {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-saturate {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-sepia {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-duration {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ease {
+ syntax: "*";
+ inherits: false;
+}
+@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-space-y-reverse: 0;
+ --tw-border-style: solid;
+ --tw-leading: initial;
+ --tw-font-weight: initial;
+ --tw-tracking: initial;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-color: initial;
+ --tw-shadow-alpha: 100%;
+ --tw-inset-shadow: 0 0 #0000;
+ --tw-inset-shadow-color: initial;
+ --tw-inset-shadow-alpha: 100%;
+ --tw-ring-color: initial;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-inset-ring-color: initial;
+ --tw-inset-ring-shadow: 0 0 #0000;
+ --tw-ring-inset: initial;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-outline-style: solid;
+ --tw-blur: initial;
+ --tw-brightness: initial;
+ --tw-contrast: initial;
+ --tw-grayscale: initial;
+ --tw-hue-rotate: initial;
+ --tw-invert: initial;
+ --tw-opacity: initial;
+ --tw-saturate: initial;
+ --tw-sepia: initial;
+ --tw-drop-shadow: initial;
+ --tw-drop-shadow-color: initial;
+ --tw-drop-shadow-alpha: 100%;
+ --tw-drop-shadow-size: initial;
+ --tw-backdrop-blur: initial;
+ --tw-backdrop-brightness: initial;
+ --tw-backdrop-contrast: initial;
+ --tw-backdrop-grayscale: initial;
+ --tw-backdrop-hue-rotate: initial;
+ --tw-backdrop-invert: initial;
+ --tw-backdrop-opacity: initial;
+ --tw-backdrop-saturate: initial;
+ --tw-backdrop-sepia: initial;
+ --tw-duration: initial;
+ --tw-ease: initial;
+ }
+ }
+}
diff --git a/Htmx.ApiDemo/wwwroot/js/components.js b/Htmx.ApiDemo/wwwroot/js/components.js
new file mode 100644
index 0000000..2b360e9
--- /dev/null
+++ b/Htmx.ApiDemo/wwwroot/js/components.js
@@ -0,0 +1,458 @@
+/* ─────────────────────────────────────────────────────────────────────────
+ * components.js – client-side logic for htmx server-rendered components
+ * ───────────────────────────────────────────────────────────────────────── */
+
+// ── Calendar ──────────────────────────────────────────────────────────────
+
+(function () {
+ var MONTHS = [
+ 'January', 'February', 'March', 'April', 'May', 'June',
+ 'July', 'August', 'September', 'October', 'November', 'December'
+ ];
+
+ function renderCalendar(root) {
+ var view = root.dataset.view || 'days';
+ var year = parseInt(root.dataset.year, 10);
+ var month = parseInt(root.dataset.month, 10);
+ var selD = parseInt(root.dataset.selDay, 10);
+ var selM = parseInt(root.dataset.selMonth, 10);
+ var selY = parseInt(root.dataset.selYear, 10);
+
+ var labelBtn = root.querySelector('.cal-month-label');
+ var grid = root.querySelector('.cal-grid');
+ var dowRow = root.querySelector('.cal-dow-row');
+
+ // ── Update header label based on view ──
+ if (view === 'days') {
+ labelBtn.textContent = MONTHS[month] + ' ' + year;
+ } else if (view === 'months') {
+ labelBtn.textContent = year;
+ } else { // years
+ var ds = Math.floor(year / 12) * 12;
+ labelBtn.textContent = ds + ' – ' + (ds + 11);
+ }
+
+ // Show DOW row only in day view
+ if (dowRow) dowRow.style.display = view === 'days' ? '' : 'none';
+
+ grid.innerHTML = '';
+
+ if (view === 'days') {
+ grid.style.gridTemplateColumns = ''; // let CSS class (grid-cols-7) take over
+
+ var firstDay = new Date(year, month, 1).getDay();
+ var daysInMonth = new Date(year, month + 1, 0).getDate();
+
+ for (var i = 0; i < firstDay; i++) {
+ grid.appendChild(document.createElement('div'));
+ }
+
+ for (var d = 1; d <= daysInMonth; d++) {
+ var btn = document.createElement('button');
+ btn.type = 'button';
+ btn.textContent = d;
+ btn.className = 'cal-day';
+
+ if (d === selD && month === selM && year === selY) {
+ btn.classList.add('cal-day-selected');
+ }
+
+ btn.dataset.date = year + '-'
+ + String(month + 1).padStart(2, '0') + '-'
+ + String(d).padStart(2, '0');
+
+ btn.addEventListener('click', (function (b, r) {
+ return function () {
+ var parts = b.dataset.date.split('-');
+ r.dataset.selYear = parts[0];
+ r.dataset.selMonth = parseInt(parts[1], 10) - 1;
+ r.dataset.selDay = parseInt(parts[2], 10);
+ r.querySelectorAll('.cal-day').forEach(function (el) {
+ el.classList.remove('cal-day-selected');
+ });
+ b.classList.add('cal-day-selected');
+ r.querySelector('.cal-hidden-input').value = b.dataset.date;
+ r.dispatchEvent(new CustomEvent('calendarChange', {
+ detail: { date: b.dataset.date },
+ bubbles: true
+ }));
+ };
+ })(btn, root));
+
+ grid.appendChild(btn);
+ }
+
+ } else if (view === 'months') {
+ grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
+
+ MONTHS.forEach(function (name, i) {
+ var btn = document.createElement('button');
+ btn.type = 'button';
+ btn.textContent = name.slice(0, 3);
+ btn.className = 'cal-view-btn' + (i === month ? ' cal-view-btn-selected' : '');
+ btn.addEventListener('click', function () {
+ root.dataset.month = i;
+ root.dataset.view = 'days';
+ renderCalendar(root);
+ });
+ grid.appendChild(btn);
+ });
+
+ } else { // years
+ var decadeStart = Math.floor(year / 12) * 12;
+ grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
+
+ for (var yi = 0; yi < 12; yi++) {
+ (function (y) {
+ var btn = document.createElement('button');
+ btn.type = 'button';
+ btn.textContent = y;
+ btn.className = 'cal-view-btn' + (y === year ? ' cal-view-btn-selected' : '');
+ btn.addEventListener('click', function () {
+ root.dataset.year = y;
+ root.dataset.view = 'months';
+ renderCalendar(root);
+ });
+ grid.appendChild(btn);
+ })(decadeStart + yi);
+ }
+ }
+ }
+
+ function initCalendar(root) {
+ root.querySelector('.cal-prev').addEventListener('click', function () {
+ var view = root.dataset.view || 'days';
+ var m = parseInt(root.dataset.month, 10);
+ var y = parseInt(root.dataset.year, 10);
+ if (view === 'days') {
+ if (m === 0) { m = 11; y--; } else { m--; }
+ root.dataset.month = m;
+ root.dataset.year = y;
+ } else if (view === 'months') {
+ root.dataset.year = y - 1;
+ } else { // years
+ root.dataset.year = Math.floor(y / 12) * 12 - 12;
+ }
+ renderCalendar(root);
+ });
+
+ root.querySelector('.cal-next').addEventListener('click', function () {
+ var view = root.dataset.view || 'days';
+ var m = parseInt(root.dataset.month, 10);
+ var y = parseInt(root.dataset.year, 10);
+ if (view === 'days') {
+ if (m === 11) { m = 0; y++; } else { m++; }
+ root.dataset.month = m;
+ root.dataset.year = y;
+ } else if (view === 'months') {
+ root.dataset.year = y + 1;
+ } else { // years
+ root.dataset.year = Math.floor(y / 12) * 12 + 12;
+ }
+ renderCalendar(root);
+ });
+
+ root.querySelector('.cal-month-label').addEventListener('click', function () {
+ var view = root.dataset.view || 'days';
+ if (view === 'days') root.dataset.view = 'months';
+ else if (view === 'months') root.dataset.view = 'years';
+ // already at years — nothing deeper
+ renderCalendar(root);
+ });
+
+ renderCalendar(root);
+ }
+
+ // Initialise all calendars on page load, and again after any htmx swap
+ function initAll() {
+ document.querySelectorAll('.calendar-root').forEach(initCalendar);
+ }
+
+ document.addEventListener('DOMContentLoaded', initAll);
+ document.addEventListener('htmx:afterSwap', initAll);
+})();
+
+
+// ── CalendarRange ─────────────────────────────────────────────────────────
+
+(function () {
+ var MONTHS = [
+ 'January', 'February', 'March', 'April', 'May', 'June',
+ 'July', 'August', 'September', 'October', 'November', 'December'
+ ];
+
+ function cmpDate(a, b) { return a < b ? -1 : a > b ? 1 : 0; }
+
+ function isBetween(d, start, end) {
+ return cmpDate(d, start) > 0 && cmpDate(d, end) < 0;
+ }
+
+ function toDateStr(year, month, day) {
+ return year + '-'
+ + String(month + 1).padStart(2, '0') + '-'
+ + String(day).padStart(2, '0');
+ }
+
+ function updateLabel(root) {
+ var lbl = root.querySelector('.calr-label');
+ if (!lbl) return;
+ var start = root.dataset.start;
+ var end = root.dataset.end;
+ if (start && end) { lbl.textContent = start + ' → ' + end; return; }
+ if (start) { lbl.textContent = start + ' → pick end date'; return; }
+ lbl.textContent = '';
+ }
+
+ // Only updates CSS classes on already-rendered buttons — no DOM destruction.
+ function updateHoverClasses(root, hoverDate) {
+ var start = root.dataset.start;
+ var end = root.dataset.end;
+ var rangeEnd = (start && !end && hoverDate && cmpDate(hoverDate, start) >= 0)
+ ? hoverDate : end;
+
+ root.querySelectorAll('.calr-day').forEach(function (btn) {
+ var ds = btn.dataset.date;
+ var isStart = !!(start && ds === start);
+ var isEnd = !!(end && ds === end);
+ var isHoverEnd = !!(!end && start && hoverDate && ds === hoverDate
+ && cmpDate(hoverDate, start) > 0);
+ var isMid = !!(start && rangeEnd && isBetween(ds, start, rangeEnd));
+
+ btn.classList.remove('calr-day-start', 'calr-day-end', 'calr-day-mid', 'calr-day-plain');
+ if (isStart) btn.classList.add('calr-day-start');
+ if (isEnd || isHoverEnd) btn.classList.add('calr-day-end');
+ if (isMid) btn.classList.add('calr-day-mid');
+ if (!isStart && !isEnd && !isHoverEnd && !isMid) btn.classList.add('calr-day-plain');
+ });
+ }
+
+ // Full re-render of the grid. Called on mount, click, and view changes.
+ function renderRange(root) {
+ var view = root.dataset.view || 'days';
+ var year = parseInt(root.dataset.year, 10);
+ var month = parseInt(root.dataset.month, 10);
+ var start = root.dataset.start || '';
+ var end = root.dataset.end || '';
+
+ var labelBtn = root.querySelector('.calr-month-label');
+ var grid = root.querySelector('.calr-grid');
+ var dowRow = root.querySelector('.cal-dow-row');
+
+ // ── Update header label ──
+ if (view === 'days') {
+ labelBtn.textContent = MONTHS[month] + ' ' + year;
+ } else if (view === 'months') {
+ labelBtn.textContent = year;
+ } else { // years
+ var ds = Math.floor(year / 12) * 12;
+ labelBtn.textContent = ds + ' – ' + (ds + 11);
+ }
+
+ if (dowRow) dowRow.style.display = view === 'days' ? '' : 'none';
+
+ grid.innerHTML = '';
+
+ // ── Clear event handlers (will be reassigned per view below) ──
+ grid.onmouseover = null;
+ grid.onmouseleave = null;
+
+ if (view === 'days') {
+ grid.style.gridTemplateColumns = '';
+
+ var firstDay = new Date(year, month, 1).getDay();
+ var daysInMonth = new Date(year, month + 1, 0).getDate();
+
+ for (var i = 0; i < firstDay; i++) {
+ grid.appendChild(document.createElement('div'));
+ }
+
+ for (var d = 1; d <= daysInMonth; d++) {
+ var dateStr = toDateStr(year, month, d);
+ var btn = document.createElement('button');
+ btn.type = 'button';
+ btn.textContent = d;
+ btn.dataset.date = dateStr;
+
+ var isStart = start && dateStr === start;
+ var isEnd = end && dateStr === end;
+ var isMid = start && end && isBetween(dateStr, start, end);
+ var cls = 'calr-day';
+ if (isStart) cls += ' calr-day-start';
+ else if (isEnd) cls += ' calr-day-end';
+ else if (isMid) cls += ' calr-day-mid';
+ else cls += ' calr-day-plain';
+ btn.className = cls;
+
+ grid.appendChild(btn);
+ }
+
+ // Click: update state → full re-render
+ grid.onclick = function (e) {
+ var btn = e.target.closest('.calr-day');
+ if (!btn) return;
+ var ds = btn.dataset.date;
+ var s = root.dataset.start;
+ var en = root.dataset.end;
+
+ if (!s || (s && en)) {
+ root.dataset.start = ds;
+ root.dataset.end = '';
+ } else {
+ if (cmpDate(ds, s) > 0) {
+ root.dataset.end = ds;
+ } else if (cmpDate(ds, s) < 0) {
+ root.dataset.start = ds;
+ root.dataset.end = '';
+ } else {
+ root.dataset.start = '';
+ root.dataset.end = '';
+ }
+ }
+
+ root.querySelector('.calr-hidden-start').value = root.dataset.start;
+ root.querySelector('.calr-hidden-end').value = root.dataset.end;
+ root.dispatchEvent(new CustomEvent('rangeChange', {
+ detail: { start: root.dataset.start, end: root.dataset.end },
+ bubbles: true
+ }));
+
+ renderRange(root);
+ updateLabel(root);
+ };
+
+ grid.onmouseover = function (e) {
+ var btn = e.target.closest('.calr-day');
+ if (!btn) return;
+ updateHoverClasses(root, btn.dataset.date);
+ };
+
+ grid.onmouseleave = function () {
+ updateHoverClasses(root, null);
+ };
+
+ } else if (view === 'months') {
+ grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
+
+ MONTHS.forEach(function (name, i) {
+ var btn = document.createElement('button');
+ btn.type = 'button';
+ btn.textContent = name.slice(0, 3);
+ btn.className = 'cal-view-btn' + (i === month ? ' cal-view-btn-selected' : '');
+ btn.addEventListener('click', function () {
+ root.dataset.month = i;
+ root.dataset.view = 'days';
+ renderRange(root);
+ });
+ grid.appendChild(btn);
+ });
+
+ } else { // years
+ var decadeStart = Math.floor(year / 12) * 12;
+ grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
+
+ for (var yi = 0; yi < 12; yi++) {
+ (function (y) {
+ var btn = document.createElement('button');
+ btn.type = 'button';
+ btn.textContent = y;
+ btn.className = 'cal-view-btn' + (y === year ? ' cal-view-btn-selected' : '');
+ btn.addEventListener('click', function () {
+ root.dataset.year = y;
+ root.dataset.view = 'months';
+ renderRange(root);
+ });
+ grid.appendChild(btn);
+ })(decadeStart + yi);
+ }
+ }
+ }
+
+ function initCalendarRange(root) {
+ root.querySelector('.calr-prev').addEventListener('click', function () {
+ var view = root.dataset.view || 'days';
+ var m = parseInt(root.dataset.month, 10);
+ var y = parseInt(root.dataset.year, 10);
+ if (view === 'days') {
+ if (m === 0) { m = 11; y--; } else { m--; }
+ root.dataset.month = m;
+ root.dataset.year = y;
+ } else if (view === 'months') {
+ root.dataset.year = y - 1;
+ } else { // years
+ root.dataset.year = Math.floor(y / 12) * 12 - 12;
+ }
+ renderRange(root);
+ });
+
+ root.querySelector('.calr-next').addEventListener('click', function () {
+ var view = root.dataset.view || 'days';
+ var m = parseInt(root.dataset.month, 10);
+ var y = parseInt(root.dataset.year, 10);
+ if (view === 'days') {
+ if (m === 11) { m = 0; y++; } else { m++; }
+ root.dataset.month = m;
+ root.dataset.year = y;
+ } else if (view === 'months') {
+ root.dataset.year = y + 1;
+ } else { // years
+ root.dataset.year = Math.floor(y / 12) * 12 + 12;
+ }
+ renderRange(root);
+ });
+
+ root.querySelector('.calr-month-label').addEventListener('click', function () {
+ var view = root.dataset.view || 'days';
+ if (view === 'days') root.dataset.view = 'months';
+ else if (view === 'months') root.dataset.view = 'years';
+ renderRange(root);
+ });
+
+ renderRange(root);
+ updateLabel(root);
+ }
+
+ function initAll() {
+ document.querySelectorAll('.calr-root').forEach(initCalendarRange);
+ }
+
+ document.addEventListener('DOMContentLoaded', initAll);
+ document.addEventListener('htmx:afterSwap', initAll);
+})();
+
+
+// ── TimePicker ────────────────────────────────────────────────────────────
+
+(function () {
+ function syncTime(root) {
+ var h = parseInt(root.querySelector('.timepicker-hour').value, 10) || 0;
+ var m = parseInt(root.querySelector('.timepicker-minute').value, 10) || 0;
+ var use12h = root.dataset.use12h === 'true';
+ var h24 = h;
+
+ if (use12h) {
+ var ampmEl = root.querySelector('.timepicker-ampm');
+ var ampm = ampmEl ? ampmEl.value : 'AM';
+ if (ampm === 'PM') { h24 = h === 12 ? 12 : h + 12; }
+ else { h24 = h === 12 ? 0 : h; }
+ }
+
+ root.querySelector('.timepicker-hidden').value =
+ String(h24).padStart(2, '0') + ':' + String(m).padStart(2, '0');
+ }
+
+ function initTimePicker(root) {
+ var sync = syncTime.bind(null, root);
+ root.querySelector('.timepicker-hour').addEventListener('input', sync);
+ root.querySelector('.timepicker-minute').addEventListener('input', sync);
+ var ampmEl = root.querySelector('.timepicker-ampm');
+ if (ampmEl) ampmEl.addEventListener('change', sync);
+ sync();
+ }
+
+ function initAll() {
+ document.querySelectorAll('.timepicker-root').forEach(initTimePicker);
+ }
+
+ document.addEventListener('DOMContentLoaded', initAll);
+ document.addEventListener('htmx:afterSwap', initAll);
+})();
diff --git a/Htmx.SourceGenerator/HtmxGenerator.cs b/Htmx.SourceGenerator/HtmxGenerator.cs
index fe829c2..3fbf095 100644
--- a/Htmx.SourceGenerator/HtmxGenerator.cs
+++ b/Htmx.SourceGenerator/HtmxGenerator.cs
@@ -1,4 +1,14 @@
-namespace Htmx.SourceGenerator
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Htmx.SourceGenerator
{
[Generator]
public class HtmxGenerator : IIncrementalGenerator