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 (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).