v0.1.0 (unreleased)

Initial development. 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).