Files
Enciphered.Blazor.UIComponents/Enciphered.Blazor.UIComponents/Forms/TimePicker.razor
T
shaamilahmed d1f0967a0c Migrate all interactive Blazor components to vanilla JS for full SSR
- Replace server interactivity with vanilla JS (forms.js) for Popover, Calendar, TimePicker, NumberInput, and Counter components

- Rewrite all Razor components to static SSR using data-* attributes for JS hooks

- Simplify InputBase.cs (remove EventCallback, EditContext, SetValueAsync)

- Remove AddInteractiveServerComponents/AddInteractiveServerRenderMode from Program.cs

- Update demo pages: remove @rendermode, replace EditForm with native form

- Add InteractivityGapTests.cs with 30 scoped E2E tests

- Update FormsTests.cs selectors for new static SSR structure

- Fix year picker navigation bug and date format mismatch in forms.js

- All 126 tests passing
2026-04-13 16:45:30 +05:00

121 lines
4.1 KiB
Plaintext

@namespace Enciphered.Blazor.UIComponents
@* ── shadcn/ui-style time picker with scrollable columns (JS-driven) ── *@
@{
var use12 = Use12Hour;
var minStep = MinuteStep < 1 ? 1 : MinuteStep;
var selHour = 0;
var selMinute = 0;
var isPm = false;
if (SelectedTime is { } t)
{
if (use12)
{
isPm = t.Hour >= 12;
selHour = t.Hour % 12;
selMinute = t.Minute;
}
else
{
selHour = t.Hour;
selMinute = t.Minute;
}
}
}
<div class="flex items-stretch gap-1 p-4"
data-timepicker
data-selected-hour="@selHour"
data-selected-minute="@selMinute"
data-selected-pm="@(isPm ? "true" : "false")"
data-use-12-hour="@(use12 ? "true" : "false")"
data-linked-input="@LinkedInputId"
@attributes="AdditionalAttributes">
@* ── Hour column ── *@
<div class="flex flex-col items-center">
<span class="text-xs font-medium text-muted-foreground mb-2">Hr</span>
<div class="h-52 w-16 overflow-y-auto flex flex-col gap-1 scrollbar-thin pr-1">
@for (int h = 0; h < (use12 ? 12 : 24); h++)
{
var hour = use12 ? (h == 0 ? 12 : h) : h;
var hourValue = h;
var isSelected = selHour == hourValue;
<button type="button"
class="@TimeItemClass(isSelected)"
data-tp-hour="@hourValue">
@hour.ToString("D2")
</button>
}
</div>
</div>
<div class="flex items-center px-1.5 text-muted-foreground font-medium text-lg">:</div>
@* ── Minute column ── *@
<div class="flex flex-col items-center">
<span class="text-xs font-medium text-muted-foreground mb-2">Min</span>
<div class="h-52 w-16 overflow-y-auto flex flex-col gap-1 scrollbar-thin pr-1">
@for (int m = 0; m < 60; m += minStep)
{
var minute = m;
var isSel = selMinute == minute;
<button type="button"
class="@TimeItemClass(isSel)"
data-tp-minute="@minute">
@minute.ToString("D2")
</button>
}
</div>
</div>
@if (use12)
{
<div class="flex items-center px-1"></div>
@* ── AM/PM column ── *@
<div class="flex flex-col items-center">
<span class="text-xs font-medium text-muted-foreground mb-2"> </span>
<div class="flex flex-col gap-1">
<button type="button"
class="@TimeItemClass(!isPm)"
data-tp-period="am">
AM
</button>
<button type="button"
class="@TimeItemClass(isPm)"
data-tp-period="pm">
PM
</button>
</div>
</div>
}
</div>
@code {
/// <summary>The currently selected time (for initial render).</summary>
[Parameter] public TimeOnly? SelectedTime { get; set; }
/// <summary>Use 12-hour format with AM/PM. Default is false (24-hour).</summary>
[Parameter] public bool Use12Hour { get; set; }
/// <summary>Minute step interval. Default is 1.</summary>
[Parameter] public int MinuteStep { get; set; } = 1;
/// <summary>The id of the linked hidden input to sync time value to.</summary>
[Parameter] public string? LinkedInputId { get; set; }
/// <summary>Any extra HTML attributes.</summary>
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? AdditionalAttributes { get; set; }
private static string TimeItemClass(bool isSelected)
{
const string baseClass = "h-9 w-full inline-flex items-center justify-center rounded-md text-sm transition-colors cursor-pointer";
return isSelected
? $"{baseClass} bg-primary text-primary-foreground font-semibold"
: $"{baseClass} hover:bg-accent hover:text-accent-foreground";
}
}