defmodule Corex.Timer do @moduledoc ~S''' Phoenix implementation of [Zag.js Timer](https://zagjs.com/components/react/timer). Countdown-only leading-zero collapse (hide unused day/hour columns) is **on by default** when `countdown` is true. Override with `collapse_leading_zeros={false}` or with fixed `segments`. ## Examples ### Basic ```heex <.timer id="t" start_ms={60_000} class="timer"> <:start_trigger><.heroicon name="hero-play" class="icon" /> <:pause_trigger><.heroicon name="hero-pause" class="icon" /> <:resume_trigger><.heroicon name="hero-play" class="icon" /> <:reset_trigger><.heroicon name="hero-arrow-path" class="icon" /> ``` ### Countdown ```heex <.timer id="t" countdown start_ms={90_000} target_ms={0} class="timer"> <:start_trigger><.heroicon name="hero-play" class="icon" /> <:pause_trigger><.heroicon name="hero-pause" class="icon" /> <:resume_trigger><.heroicon name="hero-play" class="icon" /> <:reset_trigger><.heroicon name="hero-arrow-path" class="icon" /> ``` ### Fixed segments (always four columns) ```heex <.timer id="t" countdown start_ms={@ms} segments={[:days, :hours, :minutes, :seconds]} class="timer" /> ``` ### Separator slot Omit `:separator` to render nothing between digit columns. Pass it to supply markup (for example `:` or `·`) between segments. ```heex <.timer id="t" start_ms={60_000} class="timer"> <:separator>· ``` ### Region label (a11y) ```heex <.timer id="t" translation={%Corex.Timer.Translation{area_label: "Countdown"}} class="timer" /> ``` ### Unit labels (optional) Per-column caption under each digit cell (stacked column). Omit a slot to hide that unit’s label. ```heex <.timer id="t" countdown start_ms={@ms} target_ms={0} class="timer"> <:day_label>Days <:hour_label>Hours <:minute_label>Minutes <:second_label>Seconds ``` Action slots (`:start_trigger`, `:pause_trigger`, `:resume_trigger`, `:reset_trigger`) are optional. ## Styling ```css [data-scope="timer"][data-part="root"] {} [data-scope="timer"][data-part="area"] {} [data-scope="timer"][data-part="item-label"] {} [data-scope="timer"][data-part="item"] {} [data-scope="timer"][data-part="separator"] {} [data-scope="timer"][data-part="control"] {} [data-scope="timer"][data-part="action-trigger"] {} ``` ```css @import "../corex/components/timer.css"; ``` ```heex <.timer class="timer timer--accent timer--lg"> ``` ''' @doc type: :component use Phoenix.Component alias Corex.Timer.Anatomy.{ ActionTrigger, Area, Control, Item, ItemLabel, Props, Root, Segment, Separator } alias Corex.Timer.Connect alias Corex.Timer.Translation, as: TimerTranslation @parts [:days, :hours, :minutes, :seconds] attr(:id, :string, required: false) attr(:countdown, :boolean, default: false) attr(:start_ms, :integer, default: 0) attr(:target_ms, :integer, default: nil) attr(:auto_start, :boolean, default: true) attr(:interval, :integer, default: 1000) attr(:on_tick, :string, default: nil) attr(:on_tick_client, :string, default: nil) attr(:on_complete, :string, default: nil) attr(:on_complete_client, :string, default: nil) attr(:collapse_leading_zeros, :boolean, default: nil, doc: "When nil and countdown without fixed segments, leading zero units are hidden (minimum minutes and seconds visible)." ) attr(:segments, :list, default: nil, doc: "Fixed subset of [:days, :hours, :minutes, :seconds] in natural order; disables collapse when set." ) attr(:translation, TimerTranslation, default: nil, doc: "Zag timer translations; supports area_label for the timer region aria-label." ) attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"], doc: "Text direction for styling; nil follows the document." ) attr(:orientation, :string, default: "horizontal", values: ["horizontal", "vertical"], doc: "Layout orientation for CSS." ) attr(:rest, :global) slot(:separator, required: false) slot(:day_label, required: false) slot(:hour_label, required: false) slot(:minute_label, required: false) slot(:second_label, required: false) slot :start_trigger, required: false do attr(:class, :string, required: false) end slot :pause_trigger, required: false do attr(:class, :string, required: false) end slot :resume_trigger, required: false do attr(:class, :string, required: false) end slot :reset_trigger, required: false do attr(:class, :string, required: false) end def timer(assigns) do assigns = assign_new(assigns, :id, fn -> "timer-#{System.unique_integer([:positive])}" end) segments = normalize_segments(assigns.segments) time_values = time_values(assigns.start_ms) visibility_hidden = visibility_hidden( assigns.countdown, assigns.collapse_leading_zeros, segments, time_values ) id = assigns.id dir = assigns.dir orientation = assigns.orientation running = assigns.auto_start assigns = assigns |> assign(:time_values, time_values) |> assign(:running, running) |> assign(:paused, false) |> assign(:segments, segments) |> assign( :has_timer_controls?, assigns.start_trigger != [] or assigns.pause_trigger != [] or assigns.resume_trigger != [] or assigns.reset_trigger != [] ) |> assign(:visibility_hidden, visibility_hidden) |> assign(:props_struct, props_struct(assigns, segments)) |> assign(:root_struct, %Root{id: id, dir: dir, orientation: orientation}) |> assign(:area_struct, %Area{id: id, dir: dir, orientation: orientation}) |> assign(:control_struct, %Control{id: id, dir: dir, orientation: orientation}) |> assign( :start_trigger_struct, %ActionTrigger{ id: id, action: "start", hidden: running, dir: dir, orientation: orientation } ) |> assign( :pause_trigger_struct, %ActionTrigger{ id: id, action: "pause", hidden: not running, dir: dir, orientation: orientation } ) |> assign( :resume_trigger_struct, %ActionTrigger{ id: id, action: "resume", hidden: true, dir: dir, orientation: orientation } ) |> assign( :reset_trigger_struct, %ActionTrigger{ id: id, action: "reset", hidden: not running, dir: dir, orientation: orientation } ) ~H"""