defmodule Skua.Components.Date do
@moduledoc """
A date input — a hidden ISO `<input>` that carries the value plus a trigger
that opens a keyboard-navigable calendar (W3C APG date-grid pattern).
<.date_input field={@form[:due_on]} label="Due date" />
<.date_input field={@form[:start]} min="2026-01-01" max="2026-12-31" />
The hidden input holds `yyyy-mm-dd`, so `phx-change`/`phx-submit` and Ecto
`:date` casting work unchanged. The calendar grid supports arrow keys
(±1 day / ±1 week), Home/End (week), PageUp/PageDown (month), and
Enter/Space to pick.
"""
use Phoenix.Component
alias Skua.Components.Form
alias Skua.Field
attr :field, Phoenix.HTML.FormField, default: nil
attr :id, :string, default: nil
attr :name, :string, default: nil
attr :value, :any, default: nil, doc: "ISO yyyy-mm-dd"
attr :min, :string, default: nil
attr :max, :string, default: nil
attr :today, :string, default: nil
attr :label, :string, default: nil
attr :hint, :string, default: nil
attr :errors, :list, default: nil
attr :required, :boolean, default: false
attr :placeholder, :string, default: "Pick a date…"
attr :time, :boolean, default: false, doc: "also pick a time (datetime value)"
attr :time_format, :string, default: "12", values: ~w(12 24), doc: "12h AM/PM or 24h military"
def date_input(assigns) do
assigns =
assigns
|> assign_new(:time, fn -> false end)
|> assign_new(:time_format, fn -> "12" end)
|> Field.normalize()
|> then(fn a -> assign(a, :id, a.id || "sk-date") end)
assigns =
assigns
|> assign(:value, iso(assigns.value))
|> assign(:describedby, describedby(assigns))
~H"""
<div class="sk-field">
<Form.label :if={@label} for={"#{@id}-trigger"} required={@required}>{@label}</Form.label>
<div
id={@id}
class="sk-date"
phx-hook="SkuaDate"
data-value={@value}
data-min={@min}
data-max={@max}
data-today={@today}
data-placeholder={@placeholder}
data-time={@time && ""}
data-time-format={@time_format}
>
<input type="hidden" name={@name} value={@value} data-sk-date-value required={@required} />
<button
type="button"
id={"#{@id}-trigger"}
class={["sk-input sk-focusable sk-select-trigger", @errors != [] && "is-invalid"]}
data-sk-trigger
aria-haspopup="dialog"
aria-expanded="false"
aria-invalid={(@errors != [] && "true") || nil}
aria-describedby={@describedby}
>
<span data-sk-value></span>
<svg
class="sk-affix sk-glyph"
viewBox="0 0 24 24"
style="margin-left:auto"
aria-hidden="true"
>
<rect x="3" y="4" width="18" height="18" rx="2" /><path d="M16 2v4M8 2v4M3 10h18" />
</svg>
</button>
</div>
<span class="sk-msg">
<Form.error :if={@errors != []} id={"#{@id}-error"} errors={@errors} />
<span :if={@hint && @errors == []} id={"#{@id}-hint"} class="sk-help">{@hint}</span>
</span>
</div>
"""
end
@doc """
A date **and time** picker — a time bar (hour / minute / AM·PM, or 24h
military) sits above the calendar. The hidden value is an ISO datetime
(`yyyy-mm-ddThh:mm`).
<.datetime_input field={@form[:starts_at]} label="Starts" />
<.datetime_input field={@form[:starts_at]} time_format="24" />
"""
attr :field, Phoenix.HTML.FormField, default: nil
attr :id, :string, default: nil
attr :name, :string, default: nil
attr :value, :any, default: nil, doc: "ISO yyyy-mm-ddThh:mm"
attr :min, :string, default: nil
attr :max, :string, default: nil
attr :today, :string, default: nil
attr :label, :string, default: nil
attr :hint, :string, default: nil
attr :errors, :list, default: nil
attr :required, :boolean, default: false
attr :placeholder, :string, default: "Pick a date & time…"
attr :time_format, :string, default: "12", values: ~w(12 24)
def datetime_input(assigns) do
assigns
|> assign(:time, true)
|> assign_new(:placeholder, fn -> "Pick a date & time…" end)
|> date_input()
end
defp iso(%Date{} = d), do: Date.to_iso8601(d)
defp iso(%NaiveDateTime{} = d), do: NaiveDateTime.to_iso8601(d)
defp iso(%DateTime{} = d), do: DateTime.to_iso8601(d)
defp iso(value) when is_binary(value), do: value
defp iso(_), do: nil
defp describedby(%{errors: errors, id: id}) when errors != [], do: "#{id}-error"
defp describedby(%{hint: hint, id: id}) when not is_nil(hint), do: "#{id}-hint"
defp describedby(_), do: nil
end