v0.14.0
- Floating nav refinement. When the generated
SiteNavissticky, 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.newnow offer just two auth choices — default Phoenix (magic link) or OTP (plus none).password_otpis no longer surfaced in the wizards (the generator still supports it viamix skua.gen.auth --auth password_otpif you call it directly). - Optional sticky nav. The generated
SiteNavtakes astickyattr (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-sitenavclass 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.csssetshtml, 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-canvaspage token for consistency. mix skua.gen.pagesnow puts a light/darktheme_togglein 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 runsskua.install→skua.gen.auth→skua.gen.pages→skua.gen.seoin the one correct order. Each step runs as a freshmixsubprocess, so a later step sees a dep an earlier one added (Hammer, aftergen.auth) — no in-processdeps.getcrash. - Real raw-mode arrow-key navigation on a TTY (↑/↓ or
j/kto move, space to toggle features, enter to confirm,q/Ctrl-C to cancel with the terminal restored). Reads from/dev/ttyundersttyraw; 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/1renders<meta>tags for description, Open Graph (incl.og:site_name,og:image:alt,og:locale), and Twitter cards, plus an optionalrobotsdirective and a canonical link — all from optional per-page assigns. Each tag is omitted when its assign isnil, so pages opt in. mix skua.gen.pagesinjects it into the root layout<head>(after<.live_title>), gives the publicHomeLivea sensiblepage_description, and setspage_robots: "noindex, nofollow"on the authenticatedDashboardLive. Setpage_description/page_image/page_canonical/page_robotsin any LiveView'smount/3to 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
smbreakpoint 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_linksset 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.pagesemits 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.seoscaffolds an editablepriv/static/robots.txtandpriv/static/llms.txt, and wiresstatic_paths/0so both serve at/robots.txtand/llms.txt.- The default
robots.txtDisallows the conventional scoped/auth/dev prefixes (/users,/dashboard,/dev) and carries a commentedSitemap:line. Thellms.txtfollows 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 yourllms.txtand any marker-bearingrobots.txt, and only re-applies thestatic_paths/0edit 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/1does the find-or-create + send; both theotpandpassword_otplogin 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 yourassets/css/app.css, so the whole component kit re-skins from one set of variables — and it stays fully editable. Defaultmix skua.install(no flag) keeps Skua's built-in palette; themes are opt-in. Re-running with a different--themeswaps the block in place.mix skua.themeslists 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:
SiteNavnow 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.authRegister/Log in menu is stripped from the root layout, so only the SkuaSiteNavshows. The/route is moved into the:current_userlive 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_routesis 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.serverjust works with no manualmix ecto.create && mix ecto.migrate. Strictly gated to dev via a:skua_auto_setupconfig flag (set only inconfig/dev.exs) — it is a guaranteed no-op in test and prod.
v0.5.0
mix skua.gen.authnow wires the Resend production mailer for theotpandpassword_otpflows too, not justmagic_link— so one-time codes deliver off-box in prod out of the box (dev/test keep Swoosh's local mailbox; prod readsRESEND_API_KEYat runtime, and a.env.exampleis generated). The mailer step is shared across all three login flows.- Docs: the README Install section now notes that
igniter.new(andphx.new) are Mix archives — install withmix 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
SiteNavand injects it intoLayouts.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
HomeLiveat/— 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
DashboardLiveat/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_userlive session thatphx.gen.authgenerates (falling back to the public scope, with a notice, when auth isn't installed). Removes the stockpage_controller_testinvalidated by repointing/. - Run
mix skua.installfirst (for the components), andmix skua.gen.authfor the auth-aware nav and the dashboard's auth gate.
v0.3.0
Authentication generator — mix skua.gen.auth.
- Runs
mix phx.gen.auththen 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 anAccountscontext exists. - Secure OTP login (
otp/password_otp): one-time codes from:crypto.strong_rand_byteswith 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 (aFOR UPDATE-locked row insideRepo.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 generatedconfig/config.exs. password_otpoffers password login and "email me a code" on one screen, sharing the secure OTP path and the rate limiter.magic_linkadds a Resend production mailer: dev/test keepSwoosh.Adapters.Local; prod readsRESEND_API_KEYfrom the environment at runtime (a.env.exampleis 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.installis 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.installstill works when run directly.
assets/js/app.jsis patched withigniter_jsAST codemods (parser-safe import +...skuaHooksspread), falling back to the proven regex edit whenigniter_jsisn'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.installfalls 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-daisyremoves 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-daisyopts out.- Three layers: removes the daisyUI
@pluginblocks + vendored files (keeping heroicons and thedata-themedark variant); adds a@themetoken bridge mappingbase-*/primary/error/… to Skua tokens, so daisy color utilities in your templates keep resolving and follow the theme; and swaps the stock daisytheme_toggle/1for Skua's.core_components.exis 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, ahooks: {…}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 (onlycode's relativeemremains).--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(+SkuaSliderhook) — single-handle by default,rangefor a two-handle range. Pointer-drag + full keyboard (arrows/PageUp-Down/Home/End), ARIAsliderroles, hidden inputs so values post (single undername, range undername[min]/name[max]). Track/thumb derive from--sk-space/--sk-iconso they scale with the knobs.- Fixed:
slider/segmentedfall-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-fontand the new--skua-font-monotoken (all mono usages —code,kbd, token grid — derive from it). The installer loads both from Google Fonts. - Fixed: top-layer surfaces (
.sk-panellistboxes, dialog, drawer, tooltip, toast) are appended to<body>, outside.sk-page, so they now setfont-family: var(--skua-font)explicitly — otherwise an open dropdown/menu fell back to the system font instead of the Skua font. select/1now reflects programmatic value changes (an externalchangeon 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 (SkuaTabshook): 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 (SkuaTooltiphook) shown on hover/focus, hidden on leave/blur/Esc, viewport-flipped, wiresaria-describedbyto the trigger.Skua.Components.Overlay.drawer/1— edge-anchored slide-over (left/right/top/ bottom) on a native<dialog>; reuses the dialog engine (SkuaDialoghook +open_dialog/close_dialog), so no new JS.Skua.Components.Display:alert/1(info/success/warning/error/neutral, persistent callout),accordion/1(native<details>,exclusivegrouping, 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 likeinput/select, submits and works withphx-changewith no JS.use Skuaand the installer now importSkua.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).:colslots (withlabel,field,sortable,align),:action,:empty. Sortable headers emit aphx-clicksort event (field + flipped dir); sorting/paging is yours to handle. Stream-friendly (phx-update="stream"), sticky header, hover, rounded edges (--sk-r). A superset ofCoreComponents.table/1(samerow_id/row_item/row_click/col/action) sophx.gen.livetables drop in and render Skua-styled.Skua.Components.Table.pagination/1—page/per_page/total+on_pageevent; "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/listare drop-ins for the CoreComponents equivalents.- Installer now also excepts
header/table/listfrom CoreComponents (likebutton/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 (hiddenfalsecompanion, errors).type="select"(passoptions, optionalprompt/multiple) → a native<select>styled as a Skua input (sk-native-select).type="textarea"(optionalrows) → Skua textarea.type="hidden"→ bare hidden input.- text types → unchanged (with
:leading/:trailingaffix 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.Selectgains apromptattr: 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 (SkuaToasterhook). The old flash-basedflash_group/1now 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 shows0placeholders. - New
datetime_input/1(anddata-timeondate_input): a time bar (hour / minute / AM·PM, ortime_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-leadkeeps 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-switchstyle, sliding thumb carrying a sun/moon glyph) instead of an icon button. - Typography:
.sk-leadis 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.showre-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).
- Nested popovers no longer land in the corner —
- New components for zip parity (all token-styled, keyboard/ARIA-correct):
Skua.Components.Menu—menu/menu_item/menu_label/menu_separatorwith the W3C APG menu keyboard model (SkuaMenuhook) and arole=menutop-layer panel.Skua.Components.Form.otp_input/1(theSkuaOtphook gets a component) andchip_toggle/1(checkbox chip group bound to an array field via:has).input/1gains:leading/:trailingaffix 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
:pathdep (nodeps/skua), cleandeps/skua/"skua"forms when it's a hex dep. Steps: patch app.css@import, app.js hooks import + spread, web.ex imports (exceptingbutton/input), route flashes through Skua's toast group, strip the default Phoenix navbar/branding, generatehome_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 vXbadges, 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 freshmix phx.newapp.Popover fix found via the fresh demo: the trigger is the styled button now (
trigger_variantattr; thetriggerslot is its label) — nesting a<.button>inside the slot previously produced invalid nested buttons that broke popover nesting.Skua.Phone.validate_phone/3now callsEcto.Changesetviaapply/3, so apps without the optionalectodep compile without warnings.Project skeleton: mix project with the
:phoenix_live_viewcompiler 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.FormFieldnormalization — derived id/name/value, bracket-safe DOM ids, changeset errors gated onused_input?, pluggable error translation.Skua.Components.Form:button,label,error,input,textarea, andtoggle(checkbox/radio/switch). Toggles are now keyboard-operable (real focusable input clipped via.sk-opt-input, CSS-driven visual) — fixing the prototype'shidden-input bug — and checkboxes emit a hiddenfalsecompanion so deselection is never dropped fromphx-change.Skua.Components.Overlay:popover(fixed: real focusable trigger with a measurable box +aria-*, nodisplay:contentsbug) anddialog(native<dialog>+showModal()withJS.ignore_attributes("open")for morphdom safety).- JS hooks bundle (
import { hooks } from "skua"): rewrittenPanelStackwith focus save/restore,SkuaPopover,SkuaDialog,SkuaOtp,SkuaAutofill(thedata-renamefootgun dropped). ~2.9 KB gzip. Built vianode build.js. usage-rules.mdfor 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+ rewrittenSkuaSelecthook: 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 reachesphx-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 toex_phone_numberwhen installed),infer_country/1,national_number/2,country_to_flag/1,filter/1, and the Ecto changeset validatorvalidate_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+SkuaToasthook: Phoenix-flash toasts.flash_group/1stacks:info/:errorin a fixed top-layer container;flash/1styles a single flash (kind → variant) withrole=alert, a close button, and hover-pausing auto-dismiss. Drop-in for the core_componentsflash/flash_groupAPI after--strip-daisy.Skua.Components.Date+ rewrittenSkuaDatehook: 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 — plusmin/maxbounds andaria-selected/aria-labelper day. The prototype's calendar was click-only divs. Accepts ISO strings orDatestructs.- 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 UAdisplay: 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 theattr :iddefault already set the key tonil, so theSkuaToasthook errored ("no DOM ID").flash/1andtoggle/1now derive id (and toggle's name) explicitly — the same footgun fixed earlier inSkua.Field.
41 tests passing. Remaining before release: Phase 3 (installer, --strip-daisy,
doctor).