Skip to main content

CHANGELOG.md

# Changelog

## v0.13.0

- **Auth, simplified.** `mix skua.setup` / `mix skua.new` now offer just two auth
  choices — **default Phoenix (magic link)** or **OTP** (plus none). `password_otp`
  is no longer surfaced in the wizards (the generator still supports it via
  `mix skua.gen.auth --auth password_otp` if you call it directly).
- **Optional sticky nav.** The generated `SiteNav` takes a `sticky` attr (default
  off). Enable it (`<SiteNav.site_nav sticky … />`) and the bar sticks to the top:
  **floating, translucent + blurred glass on wide screens** (≥1024px, where it
  sits over the gutter) and **solid below** that, where a transparent bar would
  overlay page content. The nav's base background moved from an inline style to a
  `.sk-sitenav` class so the sticky variant can theme over it.

## v0.12.0

**Themed on every page + a light/dark toggle in the bar.**

- The theme background now applies to **every** page, not just the homepage:
  `skua.css` sets `html, body { background: var(--sk-canvas) }` + the foreground/
  font, so the auth pages and dashboard match the active theme (and follow
  light/dark) instead of falling through to white. The generated home and
  dashboard wrappers now use the same `--sk-canvas` page token for consistency.
- `mix skua.gen.pages` now puts a **light/dark `theme_toggle` in the top bar**
  (always visible, beside the nav links / hamburger), so generated apps ship with
  theme switching out of the box.

## v0.11.1

**Fix: generated auth forms now submit.** Skua's `<.button>` defaults to
`type="button"` (so a stray button never submits a form by accident), but the
generated `mix skua.gen.auth` login / OTP-verify / register / settings views used
`<.button>` with no explicit type — so clicking "Log in" / "Register" rendered
but did nothing. `skua.gen.auth` now adds `type="submit"` to the typeless
`<.button>`s in every generated auth LiveView (idempotent). If you generated auth
on 0.11.0 or earlier, add `type="submit"` to the `<.button>` in your
`user_live/*.ex` forms (or re-run `mix skua.gen.auth`).

## v0.11.0

**`mix skua.setup` — one interactive command to wire the whole app.**

- Walks you through **theme** (100 + none), **auth**
  (`none`/`magic_link`/`otp`/`password_otp`), and a **feature checklist**
  (starter pages · SEO files · strip daisyUI), then runs `skua.install` →
  `skua.gen.auth` → `skua.gen.pages` → `skua.gen.seo` in the one correct order.
  Each step runs as a fresh `mix` subprocess, so a later step sees a dep an
  earlier one added (Hammer, after `gen.auth`) — no in-process `deps.get` crash.
- Real **raw-mode arrow-key navigation** on a TTY (↑/↓ or `j`/`k` to move, space
  to toggle features, enter to confirm, `q`/Ctrl-C to cancel with the terminal
  restored). Reads from `/dev/tty` under `stty` raw; **falls back to numbered
  prompts** under CI / pipes / no-TTY so the same task always works.
- Fully scriptable: `mix skua.setup --theme rose-pine --auth otp --yes`, plus
  `--no-pages` / `--no-seo` / `--no-strip-daisy`.

## v0.10.0

**Per-page SEO meta** for the generated pages — public pages get rich tags,
scoped pages stay out of search by default.

- New `Skua.Components.Meta.seo_meta/1` renders `<meta>` tags for description,
  Open Graph (incl. `og:site_name`, `og:image:alt`, `og:locale`), and Twitter
  cards, plus an optional `robots` directive and a canonical link — all from
  optional per-page assigns. Each tag is omitted when its assign is `nil`, so
  pages **opt in**.
- `mix skua.gen.pages` injects it into the root layout `<head>` (after
  `<.live_title>`), gives the public `HomeLive` a sensible `page_description`,
  and sets `page_robots: "noindex, nofollow"` on the authenticated
  `DashboardLive`. Set `page_description` / `page_image` / `page_canonical` /
  `page_robots` in any LiveView's `mount/3` to control its meta. Default-off for
  scoped/authenticated routes — nothing is emitted unless a page opts in.

## v0.9.0

The generated `SiteNav` is now **responsive** — it no longer overflows on
narrow screens.

- From the `sm` breakpoint up, the links stay inline as before. Below it, they
  collapse into a hamburger toggle that opens a stacked dropdown — built on a
  native `<details>`/`<summary>`, so the open/close gets keyboard and
  screen-reader support for free (the icon swaps to an ✕ when open).
- One shared `nav_links` set feeds both layouts, so there's a single place to
  edit the links; the buttons go full-width in the mobile menu.
- Verified at 375 / 768 / 1280px with no overflow or clipping. Everything stays
  yours to edit. Only `mix skua.gen.pages` emits this — no change to the
  component library or its APIs.

## v0.8.0

A generator for **public-facing discovery files** — crawler + LLM visibility out
of the box, with private surfaces off by default.

- `mix skua.gen.seo` scaffolds an editable `priv/static/robots.txt` and
  `priv/static/llms.txt`, and wires `static_paths/0` so both serve at
  `/robots.txt` and `/llms.txt`.
- The default `robots.txt` **Disallows the conventional scoped/auth/dev prefixes**
  (`/users`, `/dashboard`, `/dev`) and carries a commented `Sitemap:` line. The
  `llms.txt` follows the [llms.txt](https://llmstxt.org) convention (name,
  summary, link sections) and describes **public** content only.
- Both files are plain statics served by `Plug.Static` (above the router), so
  they can't leak scoped or authenticated routes — public-only by default. Edit
  the two files to point at your content. Idempotent: re-running preserves your
  `llms.txt` and any marker-bearing `robots.txt`, and only re-applies the
  `static_paths/0` edit if needed.

## v0.7.1

- **OTP login now auto-registers an unknown email on first code request** — the
  true passwordless "enter your email, get a code" flow. Requesting a code for an
  address with no account creates one (unconfirmed) and sends the code, so a
  brand-new user gets a code from the login screen without a separate Register
  step. New `Accounts.deliver_login_otp_to/1` does the find-or-create + send;
  both the `otp` and `password_otp` login screens use it. Stays
  enumeration-uniform (every email gets the same response and a code) and
  rate-limited, and a new account is inert until its first code is verified, so
  this grants no access on its own. (Fixes the "dev mailbox gets no code" surprise
  when signing in with an email that wasn't registered yet.)

## v0.7.0

A pack of **100 prepackaged themes**, applied at install time.

- `mix skua.install --theme <name>` writes the chosen theme's token overrides
  (dark on `:root`, light on `:root[data-theme="light"]`) into your
  `assets/css/app.css`, so the whole component kit re-skins from one set of
  variables — and it stays fully editable. Default `mix skua.install` (no flag)
  keeps Skua's built-in palette; themes are opt-in. Re-running with a different
  `--theme` swaps the block in place.
- `mix skua.themes` lists all 100.
- The 100 include refined originals (Greenfield, Midnight, Brutalist, Terminal,
  Paper, Nord, Mono, Sunset, Solar, Contrast), community palettes (Dracula,
  Gruvbox, Tokyo Night, Catppuccin, Rosé Pine, Monokai, One Dark, Ayu,
  Everforest, Cobalt…), retro/print/movement/nature/bold sets, and 50 app-style
  homages under fictional names (Potion, Discordia, GabGPT, Notify, Strapped,
  Staycation, Dualingo…). Each ships dark + light token sets with checked
  contrast and varied radius, spacing, fonts, and body size.

## v0.6.0

Generated-app polish for `mix skua.gen.pages` and `mix skua.gen.auth`.

### Pages — `mix skua.gen.pages`

- **Homepage redesign:** a full-height hero with the live Phoenix + Skua version
  badges **above** the app name (centered), and a full-height **Components**
  section whose heading matches the hero. The showcase is now interactive —
  buttons that fire each toast kind (info/success/warning/error) and open a
  dialog, drawer, popover, menu, and tooltip — alongside cards, badges, an alert,
  tabs (with a table + list), an accordion, avatars, a progress bar, and a form.
  All Skua design tokens (theme-aware), no ad-hoc utility colors.
- **Shared nav as buttons:** `SiteNav` now renders Register as a ghost button and
  Sign in as a primary button (auth-aware: a signed-in user sees their email +
  Settings + Log out).
- **No more duplicate nav:** the stock `phx.gen.auth` Register/Log in menu is
  stripped from the root layout, so only the Skua `SiteNav` shows. The `/` route
  is moved into the `:current_user` live session so the nav is genuinely
  auth-aware on the homepage.

### Auth — `mix skua.gen.auth`

- **Dev mailbox link:** the generated OTP login + verify screens show an
  "Open mailbox ↗" link to Swoosh's local mailbox (`/dev/mailbox`) — strictly
  dev-gated (renders only when `:dev_routes` is set) — so the emailed sign-in code
  is one click away in development.
- **Automatic DB setup (dev only):** generated apps create their database and run
  pending migrations automatically on first boot, so `mix phx.server` just works
  with no manual `mix ecto.create && mix ecto.migrate`. Strictly gated to dev via
  a `:skua_auto_setup` config flag (set only in `config/dev.exs`) — it is a
  guaranteed no-op in test and prod.

## v0.5.0

- `mix skua.gen.auth` now wires the Resend production mailer for the **`otp`** and
  **`password_otp`** flows too, not just `magic_link` — so one-time codes deliver
  off-box in prod out of the box (dev/test keep Swoosh's local mailbox; prod reads
  `RESEND_API_KEY` at runtime, and a `.env.example` is generated). The mailer step
  is shared across all three login flows.
- Docs: the README Install section now notes that `igniter.new` (and `phx.new`)
  are Mix archives — install with `mix archive.install hex igniter_new` — and
  points to the plain-Mix path for anyone not using Igniter.

## v0.4.0

Default-page generator — `mix skua.gen.pages`.

- Scaffolds a shared, auth-aware `SiteNav` and injects it into `Layouts.app`, so
  every page gets the same nav: section links plus a signed-in user's email +
  Log out, or Register / Sign in when signed out.
- Generates `HomeLive` at `/` — a hero with the live Phoenix + Skua version
  badges and a showcase of real Skua components (cards, badges, form inputs, an
  alert, tabs, a table, and a list), styled with Skua design tokens (theme-aware).
- Generates an authenticated `DashboardLive` at `/dashboard` — a left sidebar
  (Dashboard / Settings + Log out pinned to the bottom) and a content area of
  Skua stat cards.
- Routes are wired idempotently: `/` via the installer's router patch, the
  dashboard inside the `:require_authenticated_user` live session that
  `phx.gen.auth` generates (falling back to the public scope, with a notice, when
  auth isn't installed). Removes the stock `page_controller_test` invalidated by
  repointing `/`.
- Run `mix skua.install` first (for the components), and `mix skua.gen.auth` for
  the auth-aware nav and the dashboard's auth gate.

## v0.3.0

Authentication generator — `mix skua.gen.auth`.

- Runs `mix phx.gen.auth` then applies one of four flows: `magic_link` (default),
  `otp`, `password_otp`, `custom`. Generated code is yours to edit; the task is
  idempotent and skips the base generation when an `Accounts` context exists.
- **Secure OTP login** (`otp` / `password_otp`): one-time codes from
  `:crypto.strong_rand_bytes` with rejection sampling (unique every time, no
  modulo bias), stored only as a SHA-256 hash, verified in constant time
  (`Plug.Crypto.secure_compare`). Verification is atomic and single-use under
  concurrency (a `FOR UPDATE`-locked row inside `Repo.transact`), with a
  session-fixation guard and enumeration-safe generic responses.
- **Built-in rate limiting** via [Hammer](https://hexdocs.pm/hammer): the request
  step is limited *before* the user lookup (existence-independent) and the verify
  step is limited against brute force. Both limits (and code length / expiry) are
  configurable — length/expiry via `--otp-length` / `--otp-expiry`, limits in the
  generated `config/config.exs`.
- **`password_otp`** offers password login *and* "email me a code" on one screen,
  sharing the secure OTP path and the rate limiter.
- **`magic_link`** adds a Resend production mailer: dev/test keep
  `Swoosh.Adapters.Local`; prod reads `RESEND_API_KEY` from the environment at
  runtime (a `.env.example` is generated). No key is needed to boot dev/test.
- Generated apps stay green: each flow swaps the now-obsolete stock auth tests
  for ones matching the flow.

## v0.2.0

One-command install on Igniter, plus automatic daisyUI removal.

### One-command install

- `mix skua.install` is now an [Igniter](https://hexdocs.pm/igniter) task, so
  fresh and existing projects install in a single command:
  - `mix igniter.install skua` — existing app.
  - `mix igniter.new my_app --with phx.new --install skua` — brand-new app.
  - `mix skua.install` still works when run directly.
- `assets/js/app.js` is patched with `igniter_js` AST codemods (parser-safe
  import + `...skuaHooks` spread), falling back to the proven regex edit when
  `igniter_js` isn't present. Other files go through Igniter's staged rewrites,
  so you get a diff preview, `--yes`, and idempotent re-runs.
- Without Igniter, `mix skua.install` falls back to the self-contained plain-Mix
  task — Skua still compiles and installs on apps that don't carry Igniter.
- Version guard: the installer refuses cleanly (no partial apply) on Phoenix
  < 1.8, LiveView < 1.1, or Tailwind < v4.
- `{:igniter, "~> 0.8", optional: true}` and `{:igniter_js, "~> 0.4", optional:
  true}` added — optional, so consumers never carry them to prod.

### `--strip-daisy`

- `mix skua.install --strip-daisy` removes Phoenix 1.8's bundled daisyUI so the
  app renders purely on Skua. Defaults to **auto** (strips when daisyUI is
  present, since Skua replaces it); `--no-strip-daisy` opts out.
- Three layers: removes the daisyUI `@plugin` blocks + vendored files (keeping
  heroicons and the `data-theme` dark variant); adds a `@theme` **token bridge**
  mapping `base-*`/`primary`/`error`/… to Skua tokens, so daisy color utilities
  in your templates keep resolving and follow the theme; and swaps the stock
  daisy `theme_toggle/1` for Skua's. `core_components.ex` is never rewritten in
  place.
- Install logic extracted to `Skua.Install.Patches` — one source of truth for
  both the Igniter and plain-Mix paths.

### Install safety

Every file-editing transform that can't apply cleanly now reports a manual
instruction instead of corrupting the file (an adversarial review surfaced
these):

- A multiline `import …CoreComponents,` import, a `hooks: {…}` object with an
  inline hook, or a customized daisyUI block (nested rules) bail to a manual
  step rather than producing an uncompilable file.
- The router transform ignores commented-out routes.
- Umbrella apps refuse cleanly at the root with guidance; web paths derive
  suffix-aware (no doubled `_web`).
- The install transforms are unit-tested (`test/skua/install/patches_test.exs`).

Verified end-to-end across configs: Igniter + `--strip-daisy`, plain-Mix
(idempotent re-run), and `--no-strip-daisy` (daisy coexists) — each compiles,
builds assets, and (when stripping) ships zero daisyUI in the built CSS.

## v0.1.0

Initial public release. See `PLAN.md` for the roadmap.

### Unified scale tokens + sliders

Four master knobs, one each for rounding, type, icons, and spacing — change one
value and its whole scale rescales across every component:

- `--skua-radius` → all rounding (already wired; verified 100%, no strays).
- `--skua-font-size` → the entire type scale (`--sk-fs-2xs…2xl` + headings/lead
  derive from it; control density tiers derive too). Every hardcoded font-size
  is gone (only `code`'s relative `em` remains).
- `--skua-icon-size` → every glyph/indicator (`--sk-icon-2xs…lg`); spinners,
  empty-state icons, switch glyph, status dot all derive.
- `--skua-space` → the 8-point spacing grid (`--sk-space-0h…6`); 105 padding/
  margin/gap values now derive. A handful of off-grid optical values (1/3/5/7/9/
  11px hairline nudges and accent insets) stay literal by design.

Values were chosen to reproduce today's pixels exactly, so this is a zero-
regression refactor. The showcase token panel now lists all four knobs.

- `Skua.Components.Form.slider/1` (+ `SkuaSlider` hook) — single-handle by
  default, `range` for a two-handle range. Pointer-drag + full keyboard
  (arrows/PageUp-Down/Home/End), ARIA `slider` roles, hidden inputs so values
  post (single under `name`, range under `name[min]`/`name[max]`). Track/thumb
  derive from `--sk-space`/`--sk-icon` so they scale with the knobs.
- Fixed: `slider`/`segmented` fall-back ids now derive from the field name, so
  two of the same control on a page no longer collide.

### Default fonts + select/panel polish

- Default font is now **Inter** (sans) with **IBM Plex Mono** for code, replacing
  Space Grotesk — set via `--skua-font` and the new `--skua-font-mono` token (all
  mono usages — `code`, `kbd`, token grid — derive from it). The installer loads
  both from Google Fonts.
- Fixed: top-layer surfaces (`.sk-panel` listboxes, dialog, drawer, tooltip,
  toast) are appended to `<body>`, outside `.sk-page`, so they now set
  `font-family: var(--skua-font)` explicitly — otherwise an open dropdown/menu
  fell back to the system font instead of the Skua font.
- `select/1` now reflects **programmatic** value changes (an external `change` on
  its hidden `<select>` updates the trigger + listbox), not only user picks.

### Layout & feedback layer (v1 gap-fill)

Ten components rounding out the set beyond the form-first core — all
token-driven (rounding from `--sk-r`/`--sk-r-sm`/`--sk-r-lg`, colours from the
semantic tokens), so they re-skin globally and adapt to the light theme. Only
two new first-party hooks; the rest are zero-JS or reuse existing engines.

- `Skua.Components.Tabs.tabs/1` — client-side ARIA tablist (`SkuaTabs` hook):
  roving tabindex, ←/→/Home/End, panels switch with no server round-trip and
  are re-asserted across LiveView patches.
- `Skua.Components.Tooltip.tooltip/1` — top-layer label (`SkuaTooltip` hook)
  shown on hover/focus, hidden on leave/blur/Esc, viewport-flipped, wires
  `aria-describedby` to the trigger.
- `Skua.Components.Overlay.drawer/1` — edge-anchored slide-over (left/right/top/
  bottom) on a native `<dialog>`; **reuses the dialog engine** (`SkuaDialog`
  hook + `open_dialog`/`close_dialog`), so no new JS.
- `Skua.Components.Display`: `alert/1` (info/success/warning/error/neutral,
  persistent callout), `accordion/1` (native `<details>`, `exclusive` grouping,
  zero JS), `breadcrumb/1`, `avatar/1` (image + initials fallback, xs–xl,
  circle/square), `progress/1` (determinate + indeterminate), `skeleton/1`
  (text/circle/rect shimmer).
- `Skua.Components.Form.segmented/1` — single-select segmented control on native
  radios; field-aware like `input`/`select`, submits and works with
  `phx-change` with no JS.
- `use Skua` and the installer now import `Skua.Components.Tabs`/`.Tooltip`.

Generated home gains a showcase for each. Tests: 78 passing (+10).

### Slot-driven component gaps filled (CoreComponents parity-plus)

All token-driven — rounding derives from the single `--skua-radius` token, so
buttons, inputs, cards, **and table edges** round together when you set it.

- `Skua.Components.Table.table/1` — slot-driven, **pure presentation bound to
  your server state** (never touches your query). `:col` slots (with `label`,
  `field`, `sortable`, `align`), `:action`, `:empty`. Sortable headers emit a
  `phx-click` sort event (field + flipped dir); sorting/paging is yours to
  handle. Stream-friendly (`phx-update="stream"`), sticky header, hover,
  rounded edges (`--sk-r`). A superset of `CoreComponents.table/1` (same
  `row_id`/`row_item`/`row_click`/`col`/`action`) so `phx.gen.live` tables drop
  in and render Skua-styled.
- `Skua.Components.Table.pagination/1` — `page`/`per_page`/`total` + `on_page`
  event; "Showing X–Y of Z" + a windowed page list with ellipses. Bound to your
  state, works for offset paging of any source.
- `Skua.Components.Display`: `header/1` (subtitle/actions slots), `list/1`
  (description list, item/title), `empty_state/1` (icon/title/desc/action),
  `spinner/1` (sm/md/lg). `header`/`list` are drop-ins for the CoreComponents
  equivalents.
- Installer now also excepts `header`/`table`/`list` from CoreComponents (like
  `button`/`input`), so generated headers/tables/lists become Skua-styled.

Generated home gains a real **sortable, paginated, server-driven table** plus a
description list and an empty state. Verified in-browser: name-asc default,
column sort, page navigation, rounded table edges (`--sk-r`), clean console.
66 tests.

### Polymorphic `<.input>` — `phx.gen.auth`/scaffold drop-in

`Skua.Components.Form.input/1` now dispatches on `type` exactly like
`CoreComponents.input/1`, so generated `phx.gen.auth` / `phx.gen.live` forms
render Skua-styled and never break when Skua takes over `<.input>`:

- `type="checkbox"` → Skua checkbox (hidden `false` companion, errors).
- `type="select"` (pass `options`, optional `prompt`/`multiple`) → a native
  `<select>` styled as a Skua input (`sk-native-select`).
- `type="textarea"` (optional `rows`) → Skua textarea.
- `type="hidden"` → bare hidden input.
- text types → unchanged (with `:leading`/`:trailing` affix slots).

`Skua.Field.display_errors/1` added (read a field's display errors without the
value-clobbering of `normalize/1`). 61 tests.

- `type="select"` delegates to the **real Skua `<.select>`** (token-styled
  listbox), not a styled-native hybrid — it was the only polymorphic type that
  didn't match Skua's look.
- `Skua.Components.Select` gains a `prompt` attr: a single select with a prompt
  renders an empty leading option, so it can start **unselected** (the
  placeholder shows) instead of the browser auto-selecting the first option.
  The empty option carries the unselected state on the native `<select>` but
  never appears as a listbox row. Verified in-browser: the role select shows
  "Choose a role" with native value `""`.

### Second polish pass (live-demo feedback)

**Toasts** — reworked for stacking:
- `Skua.Components.Toast.toaster/1` + `toast/4` (`push_event`-driven) stack MANY
  toasts at once, each with its own severity timer (`SkuaToaster` hook). The
  old flash-based `flash_group/1` now also renders all four kinds
  (info/success/warning/error), not just info/error.
- Demo toast buttons are all `ghost` (no danger button for error) and stack.

**Overlays:**
- Nested popovers now position **beside** their parent panel (right, flipping
  left, then vertical) instead of overlapping it.
- Native modal `<dialog>` is explicitly centered (`inset:0; margin:auto`) so a
  host reset can't push it to the corner.

**Forms:**
- Multi-select chips get a real gutter (7px) and roomier trigger padding.
- OTP now persists typed values across patches (`phx-update="ignore"` + the hook
  seeds cells from the value) and shows `0` placeholders.
- New `datetime_input/1` (and `data-time` on `date_input`): a time bar
  (hour / minute / AM·PM, or `time_format="24"` military) sits above the
  calendar; the hidden value is an ISO datetime.

**Type scale / tokens / cards:**
- Default font is now **Space Grotesk** (`--skua-font`; the installer adds the
  Google Fonts link to the root layout).
- `.sk-lead` keeps the body color (not muted) and a bit larger; body paragraph
  bumped to 15px.
- `Skua.Components.Display.card/1` (title/subtitle/footer slots).
- Generated home gains a **design-tokens** reference (color swatches + radius /
  border / shadow / motion / font) so the themeable surface is visible.

**Top bar:** the theme toggle switch matches the form switch dimensions (36×20).

Build is now minified (`build.js`) — bundle ~8.8 KB gzip with 11 hooks. 55 tests.

### Polish pass (feedback from the live demo)

- **Theme toggle is now an animated switch** (`.sk-switch` style, sliding thumb
  carrying a sun/moon glyph) instead of an icon button.
- **Typography:** `.sk-lead` is now h4-sized (clamp 1.25–1.5rem, regular weight)
  and wins inside `.sk-content`.
- **Page background:** the installed home renders full-bleed on Skua's canvas
  (`.sk-page`) so no host (daisyUI) background shows through — "greenfield" is
  the clean neutral default, dark canvas `#0a0a0c`.
- **Phone validation note removed** from the demo; phone field is BYO-validation.
- **Bug fixes (caught in the live demo):**
  - Nested popovers no longer land in the corner — `PanelStack.show` re-measures
    on the next animation frame, fixing the stale-geometry race for a panel
    opened from inside another panel.
  - Multi-select badge spacing: the chip remove (`×`) is a `<button>` and now
    gets its native chrome reset.
  - Creatable combobox now **persists** created options across LiveView patches
    (the hook re-injects client-created options on every sync, so a created tag
    survives the server re-render that would otherwise wipe it).
- **New components for zip parity** (all token-styled, keyboard/ARIA-correct):
  - `Skua.Components.Menu` — `menu`/`menu_item`/`menu_label`/`menu_separator`
    with the W3C APG menu keyboard model (`SkuaMenu` hook) and a `role=menu`
    top-layer panel.
  - `Skua.Components.Form.otp_input/1` (the `SkuaOtp` hook gets a component) and
    `chip_toggle/1` (checkbox chip group bound to an array field via `:has`).
  - `input/1` gains `:leading`/`:trailing` affix slots.
  - `Skua.Components.Display` — `badge/1`, `dot/1`.
- The generated home showcases all of it; the installer imports `Menu` +
  `Display`. Bundle ~9.2 KB gzip (10 hooks). 49 tests.

### Installer (`mix skua.install`)

- Plain, idempotent Mix task (no Igniter dependency for consumers) that wires
  Skua into a Phoenix 1.8 app and scaffolds an editable starter home page.
  **Path-dep aware**: writes resolved asset paths when Skua is a `:path` dep
  (no `deps/skua`), clean `deps/skua`/`"skua"` forms when it's a hex dep.
  Steps: patch app.css `@import`, app.js hooks import + spread, web.ex imports
  (excepting `button`/`input`), route flashes through Skua's toast group, strip
  the default Phoenix navbar/branding, generate `home_live.ex`, route `/` to it,
  add a pre-paint theme script. Every step degrades to a printed manual
  instruction if a file doesn't match the default layout.
- Generated home (`priv/templates/skua_home.ex.eex`) showcases the install:
  `Phoenix vX + Skua vX` badges, what's native, edit/use hints, theme toggle,
  typography specimens, a combobox + multi/create combobox, date, phone, nested
  popovers, a viewport-aware edge popover, a native modal, and per-kind toast
  trigger buttons. Verified end-to-end in a fresh `mix phx.new` app.
- Popover fix found via the fresh demo: the trigger **is** the styled button now
  (`trigger_variant` attr; the `trigger` slot is its label) — nesting a
  `<.button>` inside the slot previously produced invalid nested buttons that
  broke popover nesting.
- `Skua.Phone.validate_phone/3` now calls `Ecto.Changeset` via `apply/3`, so
  apps without the optional `ecto` dep compile without warnings.


- Project skeleton: mix project with the `:phoenix_live_view` compiler wired
  for colocated-hook extraction.
- Token + component CSS layer ported from the styled-layer prototype
  (`assets/css/skua.css` — 12 semantic tokens + 3 motion tokens).
- Reference prototypes vendored under `_component_defaults/` (styled-layer
  demo, LiveView hook/component prototypes, skua.sh brand system).

### Phase 1 (foundations)

- `Skua.Field`: `Phoenix.HTML.FormField` normalization — derived id/name/value,
  bracket-safe DOM ids, changeset errors gated on `used_input?`, pluggable
  error translation.
- `Skua.Components.Form`: `button`, `label`, `error`, `input`, `textarea`, and
  `toggle` (checkbox/radio/switch). Toggles are now **keyboard-operable** (real
  focusable input clipped via `.sk-opt-input`, CSS-driven visual) — fixing the
  prototype's `hidden`-input bug — and checkboxes emit a hidden `false`
  companion so deselection is never dropped from `phx-change`.
- `Skua.Components.Overlay`: `popover` (fixed: real focusable trigger with a
  measurable box + `aria-*`, no `display:contents` bug) and `dialog` (native
  `<dialog>` + `showModal()` with `JS.ignore_attributes("open")` for morphdom
  safety).
- JS hooks bundle (`import { hooks } from "skua"`): rewritten `PanelStack` with
  focus save/restore, `SkuaPopover`, `SkuaDialog`, `SkuaOtp`, `SkuaAutofill`
  (the `data-rename` footgun dropped). ~2.9 KB gzip. Built via `node build.js`.
- `usage-rules.md` for the AI-legibility story; tests for the FormField layer.
- **Deviation from plan §3.2:** JS ships as a classic ES-module bundle, not
  colocated hooks (PanelStack sharing). See PLAN.md.
- `Skua.Components.Select` + rewritten `SkuaSelect` hook: accessible
  single/multi `<select>` (text or badge display, searchable, creatable) with a
  real `<select>` as the server-authoritative value carrier and a W3C APG
  combobox/listbox on top — `role=combobox/listbox/option`,
  `aria-activedescendant`, `aria-selected`, and full keyboard support
  (Arrow/Home/End, Enter, Space-to-toggle, Escape, type-ahead, Backspace to
  remove the last chip). The prototype had only Enter/Escape. Multiple selects
  append `[]` to the name and emit a hidden empty companion so deselect-all
  reaches `phx-change`. Bundle now ~5.8 KB gzip.
- Phone harness (ported from the aif-core dogfooding app, consolidated and
  zero-dep by default):
  - `Skua.Phone.Countries` — the 230-country `{name, iso2, dial}` dataset.
  - `Skua.Phone` — `countries/0`, `calling_code/1`, `e164/2`, `normalize/1`,
    `valid?/1` (E.164; delegates to `ex_phone_number` when installed),
    `infer_country/1`, `national_number/2`, `country_to_flag/1`, `filter/1`,
    and the Ecto changeset validator `validate_phone/3`.
  - `Skua.Components.Phone` — FormField-integrated phone field: searchable
    country listbox (PanelStack + APG roles/keyboard) + as-you-type national
    input + hidden canonical E.164. Country data ships per-render via a data
    attribute (no bloat to the shared bundle).
  - `{:ecto, optional: true}` added for the changeset validator. Bundle
    ~6.7 KB gzip with all six hooks.
- `Skua.Components.Toast` + `SkuaToast` hook: Phoenix-flash toasts.
  `flash_group/1` stacks `:info`/`:error` in a fixed top-layer container;
  `flash/1` styles a single flash (kind → variant) with `role=alert`, a close
  button, and hover-pausing auto-dismiss. Drop-in for the core_components
  `flash`/`flash_group` API after `--strip-daisy`.
- `Skua.Components.Date` + rewritten `SkuaDate` hook: a date input (hidden ISO
  value carrier + calendar) with the **W3C APG date-grid** keyboard model —
  `role=grid/gridcell`, roving tabindex, Arrow (±day/±week), Home/End (week),
  PageUp/PageDown (±month), Enter/Space to pick, Escape to close — plus
  `min`/`max` bounds and `aria-selected`/`aria-label` per day. The prototype's
  calendar was click-only divs. Accepts ISO strings or `Date` structs.
- Bundle ~8.6 KB gzip (min+gzip) with all eight hooks; calendar/day cells are
  now focusable `<button>`s.

**Phase 1 component set complete**: form inputs, select/combobox, phone, date,
dialog, popover, toast — all FormField-integrated and keyboard/ARIA-accessible.

### Browser verification (caught two real bugs)

Verified the full component set in a fresh Phoenix 1.8 app (path dep) — see
`guides/local-testing.md`. Confirmed working in-browser: select top-layer
listbox + keyboard, date APG grid (Arrow/Home/End nav + Enter select), phone
country picker + E.164 assembly, native dialog modal + focus trap. Two bugs the
unit tests couldn't catch, now fixed (+ regression tests):

- **Dialog showed when closed**: `.sk-dialog { display: flex }` (from the
  prototype's JS-overlay era) overrode the native `<dialog>`'s UA
  `display: none`. Now `.sk-dialog:not([open])` stays hidden, `[open]` lays out,
  and the scrim moved to `::backdrop`; entry animation keys off `[open]`.
- **Toast/toggle had no DOM id**: `assign_new(:id, …)` no-ops because the
  `attr :id` default already set the key to `nil`, so the `SkuaToast` hook
  errored ("no DOM ID"). `flash/1` and `toggle/1` now derive id (and toggle's
  name) explicitly — the same footgun fixed earlier in `Skua.Field`.

41 tests passing. Remaining before release: Phase 3 (installer, `--strip-daisy`,
doctor).