Added components, authentication and authorization

This commit is contained in:
2026-05-04 16:53:19 +05:00
parent 493cd71d17
commit fb1cb8e834
37 changed files with 3545 additions and 21 deletions
@@ -0,0 +1 @@
<button type="$$Type$$" class="$$Classes$$" $$HxAttrs$$>$$Label$$</button>
@@ -0,0 +1,59 @@
namespace Htmx.ApiDemo.Templates.Components;
/// <summary>
/// shadcn-style Button component.
/// Variant: default | destructive | outline | secondary | ghost | link
/// Size: default | sm | lg | icon
/// </summary>
public sealed class Button : ButtonBase
{
private static readonly Dictionary<string, string> VariantClasses = new()
{
["default"] = "bg-primary text-primary-foreground hover:bg-primary/90",
["destructive"] = "bg-destructive text-destructive-foreground hover:bg-destructive/90",
["outline"] = "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
["secondary"] = "bg-secondary text-secondary-foreground hover:bg-secondary/80",
["ghost"] = "hover:bg-accent hover:text-accent-foreground",
["link"] = "text-primary underline-offset-4 hover:underline",
};
private static readonly Dictionary<string, string> SizeClasses = new()
{
["default"] = "h-10 px-4 py-2 text-sm",
["sm"] = "h-9 rounded-md px-3 text-xs",
["lg"] = "h-11 rounded-md px-8 text-base",
["icon"] = "h-10 w-10",
};
private const string BaseClasses =
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium " +
"ring-offset-background transition-colors " +
"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 readonly byte[] _labelData;
private readonly byte[] _classesData;
private readonly byte[] _typeData;
private readonly byte[] _hxAttrsData;
public Button(
string label,
string variant = "default",
string size = "default",
string type = "button",
string hxAttrs = "")
{
_labelData = label.ToUtf8Bytes();
_typeData = type.ToUtf8Bytes();
_hxAttrsData = hxAttrs.ToUtf8Bytes();
var v = VariantClasses.GetValueOrDefault(variant, VariantClasses["default"]);
var s = SizeClasses.GetValueOrDefault(size, SizeClasses["default"]);
_classesData = $"{BaseClasses} {s} {v}".ToUtf8Bytes();
}
protected override void RenderLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_labelData);
protected override void RenderClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_classesData);
protected override void RenderType(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_typeData);
protected override void RenderHxAttrs(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_hxAttrsData);
}
@@ -0,0 +1,38 @@
<div id="cal-$$Id$$"
class="calendar-root inline-block min-w-72 rounded-md border border-border bg-card p-4 shadow-sm"
data-year="$$Year$$"
data-month="$$Month$$"
data-sel-day="$$SelectedDay$$"
data-sel-month="$$SelectedMonth$$"
data-sel-year="$$SelectedYear$$"
data-view="days">
<!-- Header row -->
<div class="mb-3 flex items-center justify-between">
<button type="button" class="cal-prev cal-nav inline-flex h-8 w-8 items-center justify-center rounded-md border border-input
bg-transparent hover:bg-accent hover:text-accent-foreground transition-colors text-base"
aria-label="Previous month">&#8249;</button>
<button type="button" class="cal-month-label text-sm font-semibold px-2 py-0.5 rounded-md
hover:bg-accent hover:text-accent-foreground transition-colors cursor-pointer"></button>
<button type="button" class="cal-next cal-nav inline-flex h-8 w-8 items-center justify-center rounded-md border border-input
bg-transparent hover:bg-accent hover:text-accent-foreground transition-colors text-base"
aria-label="Next month">&#8250;</button>
</div>
<!-- Day-of-week headers -->
<div class="cal-dow-row mb-1 grid grid-cols-7 text-center">
<span class="cal-dow">Su</span>
<span class="cal-dow">Mo</span>
<span class="cal-dow">Tu</span>
<span class="cal-dow">We</span>
<span class="cal-dow">Th</span>
<span class="cal-dow">Fr</span>
<span class="cal-dow">Sa</span>
</div>
<!-- Day grid (populated by JS below) -->
<div class="cal-grid grid grid-cols-7 gap-0.5 text-center"></div>
<!-- Hidden input -->
<input type="hidden" name="$$Name$$" class="cal-hidden-input" value="$$DefaultValue$$" />
</div>
@@ -0,0 +1,43 @@
namespace Htmx.ApiDemo.Templates.Components;
/// <summary>
/// shadcn-style Calendar (date-picker) component driven entirely by HyperScript.
/// Pass a selected date to pre-highlight a day; defaults to today.
/// </summary>
public sealed class Calendar : CalendarBase
{
private readonly byte[] _idData;
private readonly byte[] _nameData;
private readonly byte[] _yearData;
private readonly byte[] _monthData; // 0-based JS month
private readonly byte[] _selectedDayData;
private readonly byte[] _selectedMonthData; // 0-based
private readonly byte[] _selectedYearData;
private readonly byte[] _defaultValueData;
public Calendar(
string id,
string name = "date",
DateOnly? selected = null)
{
var date = selected ?? DateOnly.FromDateTime(DateTime.Today);
_idData = id.ToUtf8Bytes();
_nameData = name.ToUtf8Bytes();
_yearData = date.Year.ToString().ToUtf8Bytes();
_monthData = (date.Month - 1).ToString().ToUtf8Bytes(); // JS months are 0-based
_selectedDayData = date.Day.ToString().ToUtf8Bytes();
_selectedMonthData= (date.Month - 1).ToString().ToUtf8Bytes();
_selectedYearData = date.Year.ToString().ToUtf8Bytes();
_defaultValueData = date.ToString("yyyy-MM-dd").ToUtf8Bytes();
}
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
protected override void RenderName(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nameData);
protected override void RenderYear(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_yearData);
protected override void RenderMonth(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_monthData);
protected override void RenderSelectedDay(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_selectedDayData);
protected override void RenderSelectedMonth(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_selectedMonthData);
protected override void RenderSelectedYear(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_selectedYearData);
protected override void RenderDefaultValue(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_defaultValueData);
}
@@ -0,0 +1,41 @@
<div id="calr-$$Id$$"
class="calr-root inline-block min-w-72 rounded-md border border-border bg-card p-4 shadow-sm"
data-year="$$Year$$"
data-month="$$Month$$"
data-start="$$DefaultStart$$"
data-end="$$DefaultEnd$$"
data-view="days">
<!-- Header row -->
<div class="mb-3 flex items-center justify-between">
<button type="button" class="calr-prev cal-nav inline-flex h-8 w-8 items-center justify-center rounded-md border border-input
bg-transparent hover:bg-accent hover:text-accent-foreground transition-colors text-base"
aria-label="Previous month">&#8249;</button>
<button type="button" class="calr-month-label text-sm font-semibold px-2 py-0.5 rounded-md
hover:bg-accent hover:text-accent-foreground transition-colors cursor-pointer"></button>
<button type="button" class="calr-next cal-nav inline-flex h-8 w-8 items-center justify-center rounded-md border border-input
bg-transparent hover:bg-accent hover:text-accent-foreground transition-colors text-base"
aria-label="Next month">&#8250;</button>
</div>
<!-- Day-of-week headers -->
<div class="cal-dow-row mb-1 grid grid-cols-7 text-center">
<span class="cal-dow">Su</span>
<span class="cal-dow">Mo</span>
<span class="cal-dow">Tu</span>
<span class="cal-dow">We</span>
<span class="cal-dow">Th</span>
<span class="cal-dow">Fr</span>
<span class="cal-dow">Sa</span>
</div>
<!-- Day grid (populated by JS) -->
<div class="calr-grid grid grid-cols-7 text-center"></div>
<!-- Range label -->
<div class="calr-label mt-3 text-xs text-muted-foreground min-h-4"></div>
<!-- Hidden inputs -->
<input type="hidden" name="$$NameStart$$" class="calr-hidden-start" value="$$DefaultStart$$" />
<input type="hidden" name="$$NameEnd$$" class="calr-hidden-end" value="$$DefaultEnd$$" />
</div>
@@ -0,0 +1,49 @@
namespace Htmx.ApiDemo.Templates.Components;
/// <summary>
/// shadcn-style range Calendar. Lets the user pick a start and end date.
/// State and rendering are handled by components.js (initCalendarRange).
/// Fires a <c>rangeChange</c> CustomEvent with <c>{ start, end }</c> detail.
/// </summary>
public sealed class CalendarRange : CalendarRangeBase
{
private readonly byte[] _idData;
private readonly byte[] _nameStartData;
private readonly byte[] _nameEndData;
private readonly byte[] _yearData;
private readonly byte[] _monthData;
private readonly byte[] _defaultStartData;
private readonly byte[] _defaultEndData;
public CalendarRange(
string id,
string name = "date",
DateOnly? selectedStart = null,
DateOnly? selectedEnd = null)
{
// Show the start month if provided, otherwise today
var viewDate = selectedStart ?? DateOnly.FromDateTime(DateTime.Today);
_idData = id.ToUtf8Bytes();
_nameStartData = (name + "-start").ToUtf8Bytes();
_nameEndData = (name + "-end").ToUtf8Bytes();
_yearData = viewDate.Year.ToString().ToUtf8Bytes();
_monthData = (viewDate.Month - 1).ToString().ToUtf8Bytes(); // 0-based
_defaultStartData = selectedStart.HasValue
? selectedStart.Value.ToString("yyyy-MM-dd").ToUtf8Bytes()
: [] ;
_defaultEndData = selectedEnd.HasValue
? selectedEnd.Value.ToString("yyyy-MM-dd").ToUtf8Bytes()
: [];
}
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
protected override void RenderNameStart(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nameStartData);
protected override void RenderNameEnd(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nameEndData);
protected override void RenderYear(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_yearData);
protected override void RenderMonth(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_monthData);
protected override void RenderDefaultStart(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_defaultStartData);
protected override void RenderDefaultEnd(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_defaultEndData);
}
@@ -0,0 +1,15 @@
<div class="flex flex-col gap-1.5">
$$Label$$
<input
id="$$Id$$"
name="$$Name$$"
type="$$InputType$$"
placeholder="$$Placeholder$$"
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm
ring-offset-background placeholder:text-muted-foreground
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
disabled:cursor-not-allowed disabled:opacity-50 $$ExtraClasses$$"
$$HxAttrs$$
/>
$$Description$$
</div>
@@ -0,0 +1,52 @@
namespace Htmx.ApiDemo.Templates.Components;
/// <summary>
/// shadcn-style Input component with optional label and description.
/// InputType: text | email | password | number | search | tel | url | date | time
/// </summary>
public sealed class Input : InputBase
{
private readonly byte[] _idData;
private readonly byte[] _nameData;
private readonly byte[] _inputTypeData;
private readonly byte[] _placeholderData;
private readonly byte[] _labelData;
private readonly byte[] _descriptionData;
private readonly byte[] _extraClassesData;
private readonly byte[] _hxAttrsData;
public Input(
string id,
string name = "",
string inputType = "text",
string placeholder = "",
string label = "",
string description = "",
string extraClasses = "",
string hxAttrs = "")
{
_idData = id.ToUtf8Bytes();
_nameData = (string.IsNullOrEmpty(name) ? id : name).ToUtf8Bytes();
_inputTypeData = inputType.ToUtf8Bytes();
_placeholderData = placeholder.ToUtf8Bytes();
_extraClassesData = extraClasses.ToUtf8Bytes();
_hxAttrsData = hxAttrs.ToUtf8Bytes();
_labelData = string.IsNullOrEmpty(label)
? []
: $"""<label for="{id}" class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{label}</label>""".ToUtf8Bytes();
_descriptionData = string.IsNullOrEmpty(description)
? []
: $"""<p class="text-xs text-muted-foreground">{description}</p>""".ToUtf8Bytes();
}
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
protected override void RenderName(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nameData);
protected override void RenderInputType(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_inputTypeData);
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 RenderExtraClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_extraClassesData);
protected override void RenderHxAttrs(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_hxAttrsData);
}
@@ -0,0 +1,14 @@
<div class="flex flex-col gap-1.5">
$$Label$$
<select
id="$$Id$$"
name="$$Name$$"
class="flex h-10 w-full items-center justify-between rounded-md border border-input
bg-background px-3 py-2 text-sm ring-offset-background
focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2
disabled:cursor-not-allowed disabled:opacity-50 appearance-none $$ExtraClasses$$"
$$HxAttrs$$>
$$Options$$
</select>
$$Description$$
</div>
@@ -0,0 +1,56 @@
namespace Htmx.ApiDemo.Templates.Components;
/// <summary>
/// shadcn-style Select (native HTML select) component.
/// </summary>
public sealed class Select : SelectBase
{
private readonly byte[] _idData;
private readonly byte[] _nameData;
private readonly byte[] _labelData;
private readonly byte[] _descriptionData;
private readonly byte[] _optionsData;
private readonly byte[] _extraClassesData;
private readonly byte[] _hxAttrsData;
/// <param name="options">Collection of (value, display) tuples. Mark selected with selectedValue.</param>
public Select(
string id,
IEnumerable<(string Value, string Display)> options,
string selectedValue = "",
string name = "",
string label = "",
string description = "",
string extraClasses = "",
string hxAttrs = "")
{
_idData = id.ToUtf8Bytes();
_nameData = (string.IsNullOrEmpty(name) ? id : name).ToUtf8Bytes();
_extraClassesData = extraClasses.ToUtf8Bytes();
_hxAttrsData = hxAttrs.ToUtf8Bytes();
_labelData = string.IsNullOrEmpty(label)
? []
: $"""<label for="{id}" class="text-sm font-medium leading-none">{label}</label>""".ToUtf8Bytes();
_descriptionData = string.IsNullOrEmpty(description)
? []
: $"""<p class="text-xs text-muted-foreground">{description}</p>""".ToUtf8Bytes();
var sb = new System.Text.StringBuilder();
foreach (var (value, display) in options)
{
var selected = value == selectedValue ? " selected" : "";
sb.Append($"""<option value="{value}"{selected}>{display}</option>""");
}
_optionsData = sb.ToString().ToUtf8Bytes();
}
protected override void RenderId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_idData);
protected override void RenderName(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nameData);
protected override void RenderLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_labelData);
protected override void RenderDescription(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_descriptionData);
protected override void RenderOptions(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_optionsData);
protected override void RenderExtraClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_extraClassesData);
protected override void RenderHxAttrs(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_hxAttrsData);
}
@@ -0,0 +1,28 @@
<div class="timepicker-root flex flex-col gap-1.5" data-use12h="$$Use12h$$" id="tp-$$UniqueId$$">
$$Label$$
<div class="flex items-center gap-1">
<!-- Hour -->
<input type="number" min="$$HourMin$$" max="$$HourMax$$" step="1"
class="timepicker-hour h-10 w-16 rounded-md border border-input bg-background px-2 text-center text-sm
ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
focus-visible:ring-offset-2"
value="$$DefaultHour$$" />
<span class="text-sm font-bold text-foreground">:</span>
<!-- Minute -->
<input type="number" min="0" max="59" step="1"
class="timepicker-minute h-10 w-16 rounded-md border border-input bg-background px-2 text-center text-sm
ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
focus-visible:ring-offset-2"
value="$$DefaultMinute$$" />
<!-- AM/PM toggle (only rendered when use12h=true) -->
$$AmPmToggle$$
<!-- Hidden input that stores HH:MM value -->
<input type="hidden" name="$$Name$$" class="timepicker-hidden" value="$$DefaultValue$$" />
</div>
$$Description$$
</div>
@@ -0,0 +1,89 @@
namespace Htmx.ApiDemo.Templates.Components;
/// <summary>
/// shadcn-style TimePicker. Syncs hour+minute inputs to a hidden HH:MM field via inline JS.
/// </summary>
public sealed class TimePicker : TimePickerBase
{
private readonly byte[] _uniqueIdData;
private readonly byte[] _nameData;
private readonly byte[] _use12hData;
private readonly byte[] _labelData;
private readonly byte[] _descriptionData;
private readonly byte[] _defaultHourData;
private readonly byte[] _defaultMinuteData;
private readonly byte[] _defaultValueData;
private readonly byte[] _hourMinData;
private readonly byte[] _hourMaxData;
private readonly byte[] _amPmToggleData;
public TimePicker(
string name = "time",
TimeOnly? selected = null,
string label = "",
string description = "",
bool use12h = false)
{
var time = selected ?? TimeOnly.FromDateTime(DateTime.Now);
var uid = Guid.NewGuid().ToString("N")[..8]; // short unique suffix
_uniqueIdData = uid.ToUtf8Bytes();
_nameData = name.ToUtf8Bytes();
_use12hData = (use12h ? "true" : "false").ToUtf8Bytes();
_defaultValueData = time.ToString("HH:mm").ToUtf8Bytes();
_labelData = string.IsNullOrEmpty(label)
? []
: $"""<span class="text-sm font-medium leading-none">{label}</span>""".ToUtf8Bytes();
_descriptionData = string.IsNullOrEmpty(description)
? []
: $"""<p class="text-xs text-muted-foreground">{description}</p>""".ToUtf8Bytes();
if (use12h)
{
int hour12 = time.Hour % 12;
if (hour12 == 0) hour12 = 12;
bool isPm = time.Hour >= 12;
_defaultHourData = hour12.ToString().ToUtf8Bytes();
_defaultMinuteData = time.Minute.ToString("D2").ToUtf8Bytes();
_hourMinData = "1".ToUtf8Bytes();
_hourMaxData = "12".ToUtf8Bytes();
_amPmToggleData = BuildAmPmToggle(isPm);
}
else
{
_defaultHourData = time.Hour.ToString("D2").ToUtf8Bytes();
_defaultMinuteData = time.Minute.ToString("D2").ToUtf8Bytes();
_hourMinData = "0".ToUtf8Bytes();
_hourMaxData = "23".ToUtf8Bytes();
_amPmToggleData = [];
}
}
private static byte[] BuildAmPmToggle(bool isPm)
{
var amSel = isPm ? "" : " selected";
var pmSel = isPm ? " selected" : "";
return $"""
<select class="timepicker-ampm h-10 rounded-md border border-input bg-background px-2 text-sm
focus:outline-none focus:ring-2 focus:ring-ring">
<option value="AM"{amSel}>AM</option>
<option value="PM"{pmSel}>PM</option>
</select>
""".ToUtf8Bytes();
}
protected override void RenderUniqueId(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_uniqueIdData);
protected override void RenderName(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_nameData);
protected override void RenderUse12h(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_use12hData);
protected override void RenderLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_labelData);
protected override void RenderDescription(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_descriptionData);
protected override void RenderDefaultHour(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_defaultHourData);
protected override void RenderDefaultMinute(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_defaultMinuteData);
protected override void RenderDefaultValue(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_defaultValueData);
protected override void RenderHourMin(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_hourMinData);
protected override void RenderHourMax(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_hourMaxData);
protected override void RenderAmPmToggle(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_amPmToggleData);
}