Skip to main content

priv/templates/skua_home.ex.eex

defmodule <%= @live_module %> do
  @moduledoc """
  Skua starter home — generated by `mix skua.install`. Yours to edit: a working
  reference for every Skua component and the showcase of what was installed.
  Delete what you don't need and build your app here, or point `/` elsewhere.
  """
  use <%= @web_module %>, :live_view

  alias Skua.Components.Toast

  @statuses [{"Open", "open"}, {"In review", "review"}, {"Shipped", "shipped"}]
  @frameworks [
    {"Phoenix", "phoenix"},
    {"LiveView", "liveview"},
    {"Ecto", "ecto"},
    {"Tailwind", "tailwind"},
    {"Oban", "oban"},
    {"Bandit", "bandit"}
  ]
  @perms ~w(read write deploy admin)

  # The themeable surface — override any of these in assets/css/app.css :root and
  # every component re-skins in parallel.
  @color_tokens [
    {"Surface", "--skua-bg"},
    {"Elevated", "--skua-bg-elevated"},
    {"Text", "--skua-fg"},
    {"Muted", "--skua-fg-muted"},
    {"Border", "--skua-border"},
    {"Ring", "--skua-ring"},
    {"Accent", "--skua-accent"},
    {"Accent text", "--skua-accent-fg"},
    {"Danger", "--skua-danger"},
    {"Success", "--skua-success"},
    {"Warning", "--skua-warning"},
    {"Info", "--skua-info"}
  ]
  @scalar_tokens [
    {"Radius", "--skua-radius", "one knob → all rounding"},
    {"Font size", "--skua-font-size", "one knob → the type scale"},
    {"Icon size", "--skua-icon-size", "one knob → every glyph"},
    {"Spacing", "--skua-space", "one knob → all padding/gaps"},
    {"Border width", "--sk-bw", "hairline weight"},
    {"Shadow", "--skua-shadow", "floating panels"},
    {"Motion", "--skua-duration", "enter timing"},
    {"Easing", "--skua-ease", "the curve"},
    {"Font", "--skua-font", "Inter"},
    {"Mono", "--skua-font-mono", "IBM Plex Mono"}
  ]

  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(:phoenix_vsn, version(:phoenix))
     |> assign(:skua_vsn, version(:skua))
     |> assign(:statuses, @statuses)
     |> assign(:frameworks, @frameworks)
     |> assign(:perms, @perms)
     |> assign(:color_tokens, @color_tokens)
     |> assign(:scalar_tokens, @scalar_tokens)
     |> assign(:sort, %{field: :name, dir: :asc})
     |> assign(:page, 1)
     |> assign(:per, 6)
     |> assign(:per_options, [6, 12, 24, 48])
     |> assign(:view, "Board")
     |> assign(:form, to_form(%{"perms" => ["read", "write"]}, as: :demo))
     |> assign_table()}
  end

  # table is driven entirely by server state — sort/page events re-query here
  def handle_event("sort", %{"field" => f, "dir" => d}, socket) do
    sort = %{field: String.to_existing_atom(f), dir: String.to_existing_atom(d)}
    {:noreply, socket |> assign(:sort, sort) |> assign(:page, 1) |> assign_table()}
  end

  def handle_event("page", %{"page" => p}, socket) do
    {:noreply, socket |> assign(:page, String.to_integer(p)) |> assign_table()}
  end

  # rows-per-page picker — re-window from page 1 with the new size
  def handle_event("per_page", %{"per_page" => per}, socket) do
    {:noreply,
     socket |> assign(:per, String.to_integer(per)) |> assign(:page, 1) |> assign_table()}
  end

  # toasts go through the stacking toaster — fire several and they stack
  def handle_event("toast", %{"kind" => kind}, socket) do
    {title, body} =
      case kind do
        "success" -> {"Saved", "Your changes are live."}
        "warning" -> {"Heads up", "Double-check that value."}
        "error" -> {"Error", "Something went wrong."}
        _ -> {"Info", "An informational message."}
      end

    {:noreply, Toast.toast(socket, kind, body, title: title)}
  end

  def handle_event("menu", %{"action" => action}, socket) do
    {:noreply, Toast.toast(socket, :info, "Menu action: #{action}")}
  end

  def handle_event("seg", %{"view" => view}, socket) do
    {:noreply, assign(socket, :view, view)}
  end

  def handle_event("validate", %{"demo" => params}, socket) do
    {:noreply, assign(socket, :form, to_form(params, as: :demo))}
  end

  def handle_event("save", %{"demo" => params}, socket) do
    {:noreply,
     socket
     |> Toast.toast(:success, "Phone: #{params["phone"] || "—"} · code: #{params["code"] || "—"}",
       title: "Submitted"
     )
     |> assign(:form, to_form(params, as: :demo))}
  end

  defp version(app) do
    case Application.spec(app, :vsn) do
      nil -> "—"
      vsn -> List.to_string(vsn)
    end
  end

  defp assign_table(socket) do
    %{sort: sort, page: page, per: per} = socket.assigns
    sorted = Enum.sort_by(people(), &Map.get(&1, sort.field), sort.dir)
    total = length(sorted)
    # clamp so a per-page change never leaves you stranded past the last page
    page = page |> max(1) |> min(max(1, ceil(total / per)))
    rows = sorted |> Enum.drop((page - 1) * per) |> Enum.take(per)
    socket |> assign(:rows, rows) |> assign(:page, page) |> assign(:total, total)
  end

  defp people do
    names = ~w(Ada Grace Linus Margaret Dennis Barbara Radia Katherine Edsger Donald Leslie Frances)
    plans = ~w(Free Pro Team)

    for i <- 0..122 do
      name = Enum.at(names, rem(i, length(names)))

      %{
        id: i + 1,
        name: "#{name} #{<<?A + rem(i, 26)>>}.",
        email: "#{String.downcase(name)}#{i}@example.com",
        plan: Enum.at(plans, rem(i, 3)),
        status: (rem(i, 4) == 0 && "invited") || "active"
      }
    end
  end

  def render(assigns) do
    ~H"""
    <div class="sk-page">
      <Toast.toaster />
      <div class="sk-home">
        <header class="sk-home-top">
          <div class="sk-badge-row">
            <.badge>Phoenix v{@phoenix_vsn}</.badge>
            <.badge variant="ok">Skua v{@skua_vsn}</.badge>
          </div>
          <Skua.Components.Theme.theme_toggle />
        </header>

        <section class="sk-content" style="max-width:44rem">
          <h1 class="sk-h1">Your app, designed out of the gate.</h1>
          <p class="sk-lead">
            Skua replaced the default scaffold with headless, themeable, viewport-aware
            components — native top layer, server-authoritative forms, full keyboard + ARIA,
            and zero third-party JS.
          </p>
          <p class="sk-p">
            Everything below is a real Skua component. Theme by overriding the tokens in
            <code class="sk-code">assets/css/app.css</code>; edit this page at
            <code class="sk-code">lib/.../live/home_live.ex</code>.
          </p>
        </section>

        <section class="sk-home-section">
          <h2 class="sk-h2">Type scale</h2>
          <div class="sk-content">
            <h1 class="sk-h1">Heading 1</h1>
            <h2 class="sk-h2">Heading 2</h2>
            <h3 class="sk-h3">Heading 3</h3>
            <h4 class="sk-h4">Heading 4</h4>
            <p class="sk-lead">Lead paragraph — the large base size for intros and standfirsts.</p>
            <p class="sk-p">
              Body paragraph with a <a href="#" class="sk-link">link</a>,
              <code class="sk-code">inline code</code>, and a <span class="sk-kbd">⌘K</span> shortcut.
            </p>
            <p class="sk-muted">Muted secondary text for captions and hints.</p>
          </div>
        </section>

        <section class="sk-home-section">
          <h2 class="sk-h2">Design tokens</h2>
          <p class="sk-small">Override any of these once in your app's <code class="sk-code">:root</code>
            and every component re-skins together.</p>
          <div class="sk-token-grid">
            <div :for={{label, var} <- @color_tokens} class="sk-token">
              <span class="sk-token-swatch" style={"background:var(#{var})"}></span>
              <span class="sk-token-meta"><b>{label}</b><code>{var}</code></span>
            </div>
          </div>
          <div class="sk-token-grid">
            <div :for={{label, var, note} <- @scalar_tokens} class="sk-token">
              <span class="sk-token-meta"><b>{label}</b><code>{var}</code> · {note}</span>
            </div>
          </div>
        </section>

        <section class="sk-home-section">
          <h2 class="sk-h2">Cards</h2>
          <div class="sk-home-cards">
            <.card>
              <:title>Starter</:title>
              <:subtitle>Free forever</:subtitle>
              <p class="sk-p">Every component, the token system, and the installer.</p>
              <:footer><.button variant="secondary">Current plan</.button></:footer>
            </.card>
            <.card>
              <:title>Pro</:title>
              <:subtitle>For teams</:subtitle>
              <p class="sk-p">Priority support and the full component gallery.</p>
              <:footer><.button variant="primary">Upgrade</.button></:footer>
            </.card>
          </div>
        </section>

        <section class="sk-home-section">
          <h2 class="sk-h2">Forms</h2>
          <form phx-change="validate" phx-submit="save" class="sk-home-form">
            <.input field={@form[:email]} type="email" label="Email" placeholder="you@co.com">
              <:leading>
                <svg class="sk-glyph" viewBox="0 0 24 24" aria-hidden="true">
                  <rect x="3" y="5" width="18" height="14" rx="2" /><path d="m3 7 9 6 9-6" />
                </svg>
              </:leading>
            </.input>
            <.phone field={@form[:phone]} label="Phone" />
            <.select field={@form[:status]} label="Status (combobox)" options={@statuses} searchable
              placeholder="Search statuses…" />
            <.select field={@form[:stack]} label="Stack (multi · create your own)" multiple display="badge"
              searchable creatable options={@frameworks} placeholder="Add or create tools…" />
            <.input field={@form[:role]} type="select" label="Role (select via <.input>)"
              prompt="Choose a role" options={[{"Admin", "admin"}, {"Member", "member"}, {"Viewer", "viewer"}]} />
            <.date_input field={@form[:due]} label="Due date" />
            <.datetime_input field={@form[:starts_at]} label="Starts at (date + time)" />
            <.datetime_input field={@form[:window]} label="Maintenance window (24h)" time_format="24" />
            <.input field={@form[:agree]} type="checkbox" label="I agree to the terms (checkbox via <.input>)" />
            <div class="sk-field">
              <span class="sk-label">Permissions (chips)</span>
              <div class="sk-home-row">
                <.chip_toggle :for={p <- @perms} field={@form[:perms]} value={p} label={p} />
              </div>
            </div>
            <div class="sk-field">
              <span class="sk-label">One-time code</span>
              <.otp_input field={@form[:code]} length={6} group={3} />
            </div>
            <.toggle type="switch" field={@form[:notify]} label="Email me about this" />
            <div class="sk-home-actions">
              <.button type="submit" variant="primary">Save</.button>
            </div>
          </form>
        </section>

        <section class="sk-home-section">
          <.header>
            People
            <:subtitle>{@total} members · sortable headers, paginated, fully server-driven.</:subtitle>
            <:actions><.button variant="primary">Invite</.button></:actions>
          </.header>
          <.table id="people" rows={@rows} sort={@sort} on_sort="sort">
            <:col :let={p} field={:name} label="Name" sortable>{p.name}</:col>
            <:col :let={p} field={:email} label="Email" sortable>{p.email}</:col>
            <:col :let={p} label="Plan">
              <.badge variant={(p.plan == "Pro" && "ok") || "neutral"}>{p.plan}</.badge>
            </:col>
            <:col :let={p} label="Status">{p.status}</:col>
            <:action :let={p}>
              <.menu id={"row-#{p.id}"} trigger_variant="ghost">
                <:trigger>Actions</:trigger>
                <.menu_item phx-click="menu" phx-value-action={"View ##{p.id}"}>View</.menu_item>
                <.menu_item danger phx-click="menu" phx-value-action={"Remove ##{p.id}"}>Remove</.menu_item>
              </.menu>
            </:action>
            <:empty>No people yet.</:empty>
          </.table>
          <.pagination
            page={@page}
            per_page={@per}
            total={@total}
            on_page="page"
            per_page_options={@per_options}
            on_per_page="per_page"
          />
        </section>

        <section class="sk-home-section">
          <h2 class="sk-h2">Details &amp; empty states</h2>
          <div class="sk-home-cards">
            <.card>
              <:title>Account</:title>
              <.list>
                <:item title="Name">Ada Lovelace</:item>
                <:item title="Email">ada@example.com</:item>
                <:item title="Plan"><.badge variant="ok">Pro</.badge></:item>
                <:item title="Joined">June 2026</:item>
              </.list>
            </.card>
            <.card>
              <:title>No projects</:title>
              <.empty_state title="Nothing here yet">
                <:icon>
                  <svg class="sk-glyph" viewBox="0 0 24 24" aria-hidden="true">
                    <path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
                  </svg>
                </:icon>
                Create your first project to get going.
                <:action><.button variant="primary">New project</.button></:action>
              </.empty_state>
            </.card>
          </div>
        </section>

        <section class="sk-home-section">
          <h2 class="sk-h2">Menus &amp; overlays</h2>
          <div class="sk-home-row">
            <.menu id="proj-menu" width="200px">
              <:trigger>Actions</:trigger>
              <.menu_label>Project</.menu_label>
              <.menu_item shortcut="⌘R" phx-click="menu" phx-value-action="Rename">Rename</.menu_item>
              <.menu_item shortcut="⌘D" phx-click="menu" phx-value-action="Duplicate">Duplicate</.menu_item>
              <.menu_separator />
              <.menu_item danger phx-click="menu" phx-value-action="Delete">Delete project</.menu_item>
            </.menu>

            <.popover id="nested-pop" pad width="260px" trigger_variant="secondary">
              <:trigger>Nested popover</:trigger>
              <p class="sk-p" style="margin-bottom:.5rem">A nested popover opens beside its parent —
                never overlapping it.</p>
              <.popover id="nested-pop-inner" pad width="200px" trigger_variant="ghost">
                <:trigger>Open nested →</:trigger>
                <p class="sk-small">I sit to the side. Esc closes me first.</p>
              </.popover>
            </.popover>

            <.button variant="secondary" phx-click={Skua.Components.Overlay.open_dialog("welcome")}>
              Open modal
            </.button>
          </div>
        </section>

        <section class="sk-home-section">
          <h2 class="sk-h2">Toasts</h2>
          <p class="sk-small">Click repeatedly — they stack. Auto-dismiss scales with severity
            (info 6s · success 4s · warning 8s · error 10s).</p>
          <div class="sk-home-row">
            <.button variant="ghost" phx-click="toast" phx-value-kind="info">Info</.button>
            <.button variant="ghost" phx-click="toast" phx-value-kind="success">Success</.button>
            <.button variant="ghost" phx-click="toast" phx-value-kind="warning">Warning</.button>
            <.button variant="ghost" phx-click="toast" phx-value-kind="error">Error</.button>
          </div>
        </section>

        <section class="sk-home-section">
          <h2 class="sk-h2">Tabs, accordion &amp; alerts</h2>
          <.tabs id="demo-tabs">
            <:tab label="Overview">
              <p class="sk-p">Tabs switch client-side — no server round-trip — with full keyboard
                support (←/→, Home/End) and roving tabindex.</p>
            </:tab>
            <:tab label="Activity">
              <p class="sk-p">Each panel is a slot, wired as an ARIA
                <code class="sk-code">tablist</code>.</p>
            </:tab>
            <:tab label="Settings">
              <p class="sk-p">The shown tab is re-asserted across LiveView patches, so it never
                resets out from under you.</p>
            </:tab>
          </.tabs>

          <div style="margin-top:1.25rem">
            <.accordion id="faq" exclusive>
              <:item title="Is Skua headless?" open>
                Behaviour rides on browser-native primitives; the look is 12 tokens you own.
              </:item>
              <:item title="Does it need third-party JavaScript?">
                No — only a small first-party hooks bundle.
              </:item>
              <:item title="Will it fight my CSS?">
                Everything is scoped to <code class="sk-code">sk-</code> classes and derives from your tokens.
              </:item>
            </.accordion>
          </div>

          <div style="margin-top:1.25rem; display:flex; flex-direction:column; gap:.6rem">
            <.alert variant="info" title="Tip">Alerts are persistent — unlike toasts, they stay put.</.alert>
            <.alert variant="success" title="Saved">Your changes are live.</.alert>
            <.alert variant="warning" title="Heads up">Your trial ends in 3 days.</.alert>
            <.alert variant="error" title="Couldn't save">Check the highlighted fields and retry.</.alert>
          </div>
        </section>

        <section class="sk-home-section">
          <h2 class="sk-h2">Bits &amp; pieces</h2>

          <.breadcrumb>
            <:crumb navigate="/">Home</:crumb>
            <:crumb navigate="/">Team</:crumb>
            <:crumb>Settings</:crumb>
          </.breadcrumb>

          <div class="sk-badge-row" style="margin-top:1.1rem">
            <.badge>Neutral</.badge>
            <.badge variant="success">Success</.badge>
            <.badge variant="warning">Warning</.badge>
            <.badge variant="info">Info</.badge>
            <.badge variant="danger">Danger</.badge>
          </div>

          <div class="sk-home-row" style="margin-top:1.1rem; align-items:center">
            <.avatar name="Ada Lovelace" size="lg" />
            <.avatar name="Grace Hopper" />
            <.avatar name="Linus T" size="sm" />
            <.avatar name="Box" shape="square" />
          </div>

          <div style="margin-top:1.25rem; max-width:30rem; display:flex; flex-direction:column; gap:.75rem">
            <.progress value={72} label="Upload" />
            <.progress label="Working…" />
          </div>

          <form phx-change="seg" style="margin-top:1.25rem">
            <.segmented name="view" value={@view} options={["List", "Board", "Calendar"]} label="View" />
          </form>

          <div style="margin-top:1.25rem; max-width:30rem; display:flex; flex-direction:column; gap:1rem">
            <.slider name="volume" value={40} min={0} max={100} label="Volume (single handle)" />
            <.slider name="price" range value={[20, 80]} min={0} max={100} step={5} label="Price range (two handles, draggable)" />
          </div>

          <div style="margin-top:1.25rem; max-width:24rem; display:flex; flex-direction:column; gap:.5rem">
            <.skeleton variant="text" width="80%" />
            <.skeleton variant="text" width="60%" />
            <.skeleton variant="rect" height="64px" />
          </div>

          <div class="sk-home-row" style="margin-top:1.25rem; align-items:center">
            <.tooltip id="tip-save" text="Saves to your account (⌘S)">
              <.button variant="secondary">Hover or focus me</.button>
            </.tooltip>
            <.button variant="secondary" phx-click={Skua.Components.Overlay.open_dialog("drawer-demo")}>
              Open drawer
            </.button>
          </div>
        </section>
      </div>

      <.dialog id="welcome">
        <:title>Native modal</:title>
        <:subtitle>showModal() — inert backdrop, focus trap, Esc to close, centered.</:subtitle>
        <p class="sk-p">A real <code class="sk-code">&lt;dialog&gt;</code>. It survives LiveView
          re-renders and needs no JS library.</p>
        <:footer>
          <.button data-sk-close>Close</.button>
          <.button variant="primary" data-sk-close>Got it</.button>
        </:footer>
      </.dialog>

      <.drawer id="drawer-demo" side="right">
        <:title>Filters</:title>
        <:subtitle>A native dialog, anchored to the edge.</:subtitle>
        <p class="sk-p">Drawers reuse the dialog engine — inert backdrop, focus trap, Esc to
          close — and slide in from the side.</p>
        <:footer>
          <.button data-sk-close>Cancel</.button>
          <.button variant="primary" data-sk-close>Apply</.button>
        </:footer>
      </.drawer>
    </div>
    """
  end
end