Form-first UI components for Phoenix LiveView (1.1+) on Phoenix 1.8+: rich select, date picker, OTP input, dialogs, menus, tables, toasts and top-layer panels — server-authoritative, viewport-aware, themeable from 12 CSS tokens, with zero third-party JavaScript (one JS import, ~9 KB min+gzip).

A daisyUI replacement you install and manage through mix tasks built on Igniter — including --strip-daisy to remove Phoenix 1.8's bundled daisyUI.

Install

One command — Igniter adds the dep and wires everything (CSS, JS hooks, component imports, flash → Skua toasts, strips the default Phoenix navbar, and scaffolds an editable starter home at /):

# existing app
mix igniter.install skua

# brand-new app — one-time: install the project-generator archive first
mix archive.install hex igniter_new
mix igniter.new my_app --with phx.new --install skua

(igniter.new and phx.new are Mix archives, not built-in tasks — if either reports "could not be found", install it once with mix archive.install hex igniter_new / mix archive.install hex phx_new. Or skip archives entirely and use the plain-Mix path below.)

Skua replaces daisyUI, so the installer removes Phoenix 1.8's bundled daisyUI by default — it deletes the vendored files and bridges daisy's color utilities (bg-base-100, text-error, …) to Skua tokens so existing markup keeps resolving. Keep daisyUI instead with --no-strip-daisy:

mix igniter.install skua --no-strip-daisy

(If your daisyUI config was customized with nested rules, the installer leaves it alone and prints a manual step rather than risk mangling it.)

Without Igniter — add the dep and run the plain Mix task (Igniter is optional; the task still works):

# mix.exs
{:skua, "~> 0.7.0"}
mix deps.get
mix skua.install            # --strip-daisy optional
mix phx.server              # open /

Every step is idempotent — re-run any time after mix deps.update skua. The installer degrades to printed manual steps if your app diverges from the default layout. Local-testing notes in guides/local-testing.md.

Authentication

mix skua.gen.auth runs mix phx.gen.auth and then applies a flow on top. The generated code is yours to edit.

mix skua.gen.auth --auth magic_link    # default — Phoenix's passwordless flow
mix skua.gen.auth --auth otp           # one-time-code login (no passwords)
mix skua.gen.auth --auth password_otp  # password + OTP on one login screen
mix skua.gen.auth --auth custom        # stock phx.gen.auth, nothing added

The otp and password_otp flows ship a production-grade one-time-code login: CSPRNG codes (unique every time, never :rand), SHA-256-hashed storage, single-use under concurrency (FOR UPDATE + transaction), constant-time compare, a session-fixation guard, enumeration-safe responses, and built-in Hammer rate limiting on both the request and verify steps — all tunable in the generated config/config.exs. Code length and expiry are flags:

mix skua.gen.auth --auth otp --otp-length 8 --otp-expiry 5

All three login flows (magic_link, otp, password_otp) wire a Resend production mailer so the link/code actually delivers off-box (dev/test keep Swoosh's local mailbox; prod reads RESEND_API_KEY at runtime — see the generated .env.example).

Two dev conveniences are wired automatically: the OTP login/verify screens show a dev-only "Open mailbox ↗" link to Swoosh's local mailbox so the sign-in code is one click away, and the generated app creates + migrates its database on first boot in dev — so mix deps.get && mix phx.server just works, no manual mix ecto.create && mix ecto.migrate. Both are strictly dev-gated (no effect in test or prod).

Pages

mix skua.gen.pages scaffolds editable starter pages and wires their routes (run it after skua.install, and after skua.gen.auth for the auth-aware nav and the dashboard's auth gate):

mix skua.gen.pages
  • a shared, auth-aware SiteNav — section links plus Register (ghost button) / Sign in (primary button), or a signed-in user's email + Settings + Log out. It replaces the stock phx.gen.auth nav, so there's no duplicate top bar, and it is responsive: inline from the sm breakpoint up, collapsing into a keyboard-accessible hamburger menu (a native <details>) on narrow screens.
  • HomeLive at / — a full-height hero with the live Phoenix + Skua version badges above the app name, and a full-height interactive showcase: buttons that fire each toast kind and open a dialog/drawer/popover/menu/tooltip, plus cards, badges, an alert, tabs, a table, a list, an accordion, and a form.
  • DashboardLive at /dashboard — an authenticated page with a sidebar (Dashboard / Settings + Log out) and Skua stat cards.

Everything generated is yours to edit; re-running overwrites the pages and leaves the routes and the nav injection in place.

SEO & discovery

mix skua.gen.seo scaffolds two editable, public-facing discovery files and wires them to be served:

mix skua.gen.seo
  • priv/static/robots.txt — public crawl rules. The conventional scoped/authenticated prefixes a Skua app generates (/users, /dashboard, /dev) are Disallowed by default, plus a commented Sitemap: line to fill in. A stock phx.new robots.txt is replaced; one this task already wrote is left alone so your edits survive re-runs.
  • priv/static/llms.txt — an llms.txt template (name, summary, link sections) describing your public content for LLMs and agents. Never overwritten once it exists.

Both are served by Plug.Static, which sits above the router — so they bypass your :browser/auth pipelines entirely and can't leak scoped or authenticated routes. Public-only by default; point them at real content by editing the two files. Serves at /robots.txt and /llms.txt.

Per-page meta

mix skua.gen.pages also wires Skua.Components.Meta.seo_meta/1 into the root layout <head>, driven by per-page assigns. The public homepage gets a page_description and full Open Graph + Twitter tags; the authenticated dashboard sets page_robots: "noindex, nofollow". Control any page's meta from its mount/3:

assign(socket,
  page_title: "Pricing",
  page_description: "Simple, transparent pricing.",
  page_image: "https://example.com/og/pricing.png",
  page_canonical: "https://example.com/pricing"
)

Each tag is omitted when its assign is unset, so scoped/authenticated pages emit nothing by default (or set page_robots to keep them out of search).

Themes

Skua ships 100 prepackaged themes — each a coherent token set (dark + light, contrast-checked, with its own radius/spacing/fonts/body-size). Bake one in:

mix skua.install --theme greenfield
mix skua.themes                       # list all 100

--theme <name> writes that theme's tokens into your assets/css/app.css (:root for dark, :root[data-theme="light"] for light), so the whole component kit re-skins from one place — and it stays yours to edit. Plain mix skua.install (no flag) keeps Skua's built-in palette; themes are opt-in, and re-running with a different --theme swaps the block.

The set spans refined originals (greenfield, midnight, brutalist, terminal, paper, nord, mono, sunset, solar, contrast), community palettes (dracula, gruvbox, tokyo-night, catppuccin, rose-pine, monokai, one-dark, ayu, everforest, cobalt…), retro / print / design-movement / nature / bold sets, and 50 app-style homages under fictional names (potion, discordia, gabgpt, notify, strapped, staycation, dualingo…). Run mix skua.themes for the full list.

Repository layout

  • PLAN.md — the full project plan (architecture decisions, installer spec, strip-daisy design, release pipeline, roadmap).
  • assets/css/skua.css — the token + component CSS layer (the stable artifact).
  • lib/ — components, the install mix tasks (Skua.Install.Patches holds the shared install logic), and supporting modules.
  • _component_defaults/ — reference prototypes the library is being built from (styled-layer HTML demo, LiveView hook/component prototypes, and the skua.sh brand system). Not shipped in the hex package.

License

MIT — see LICENSE.md.