v0.14.1

  • Wider nav. The generated nav is now full-bleed (no max-w cap) with responsive edge padding (px-6 sm:px-8 lg:px-12), so the brand and the links/buttons sit right out at the page margins on wide screens.

v0.14.0

  • Floating nav refinement. When the generated SiteNav is sticky, the bar is now fully transparent (no background, no divider) on wide screens and the lane is wider (max-w-7xl) so the brand + links/buttons sit out in the page margins, floating over the canvas. Below the lane width (≤1023px, where a transparent bar would overlap content) it switches to a solid background with a divider. (Dropped the glass blur in favour of true transparency, per use.)

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.installskua.gen.authskua.gen.pagesskua.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 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: 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 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/1page/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.Menumenu/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.Displaybadge/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.Phonecountries/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).