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 & 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 & 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 & 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 & 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"><dialog></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