# Changelog
## 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/1` — `page`/`per_page`/`total` + `on_page`
event; "Showing X–Y of Z" + a windowed page list with ellipses. Bound to your
state, works for offset paging of any source.
- `Skua.Components.Display`: `header/1` (subtitle/actions slots), `list/1`
(description list, item/title), `empty_state/1` (icon/title/desc/action),
`spinner/1` (sm/md/lg). `header`/`list` are drop-ins for the CoreComponents
equivalents.
- Installer now also excepts `header`/`table`/`list` from CoreComponents (like
`button`/`input`), so generated headers/tables/lists become Skua-styled.
Generated home gains a real **sortable, paginated, server-driven table** plus a
description list and an empty state. Verified in-browser: name-asc default,
column sort, page navigation, rounded table edges (`--sk-r`), clean console.
66 tests.
### Polymorphic `<.input>` — `phx.gen.auth`/scaffold drop-in
`Skua.Components.Form.input/1` now dispatches on `type` exactly like
`CoreComponents.input/1`, so generated `phx.gen.auth` / `phx.gen.live` forms
render Skua-styled and never break when Skua takes over `<.input>`:
- `type="checkbox"` → Skua checkbox (hidden `false` companion, errors).
- `type="select"` (pass `options`, optional `prompt`/`multiple`) → a native
`<select>` styled as a Skua input (`sk-native-select`).
- `type="textarea"` (optional `rows`) → Skua textarea.
- `type="hidden"` → bare hidden input.
- text types → unchanged (with `:leading`/`:trailing` affix slots).
`Skua.Field.display_errors/1` added (read a field's display errors without the
value-clobbering of `normalize/1`). 61 tests.
- `type="select"` delegates to the **real Skua `<.select>`** (token-styled
listbox), not a styled-native hybrid — it was the only polymorphic type that
didn't match Skua's look.
- `Skua.Components.Select` gains a `prompt` attr: a single select with a prompt
renders an empty leading option, so it can start **unselected** (the
placeholder shows) instead of the browser auto-selecting the first option.
The empty option carries the unselected state on the native `<select>` but
never appears as a listbox row. Verified in-browser: the role select shows
"Choose a role" with native value `""`.
### Second polish pass (live-demo feedback)
**Toasts** — reworked for stacking:
- `Skua.Components.Toast.toaster/1` + `toast/4` (`push_event`-driven) stack MANY
toasts at once, each with its own severity timer (`SkuaToaster` hook). The
old flash-based `flash_group/1` now also renders all four kinds
(info/success/warning/error), not just info/error.
- Demo toast buttons are all `ghost` (no danger button for error) and stack.
**Overlays:**
- Nested popovers now position **beside** their parent panel (right, flipping
left, then vertical) instead of overlapping it.
- Native modal `<dialog>` is explicitly centered (`inset:0; margin:auto`) so a
host reset can't push it to the corner.
**Forms:**
- Multi-select chips get a real gutter (7px) and roomier trigger padding.
- OTP now persists typed values across patches (`phx-update="ignore"` + the hook
seeds cells from the value) and shows `0` placeholders.
- New `datetime_input/1` (and `data-time` on `date_input`): a time bar
(hour / minute / AM·PM, or `time_format="24"` military) sits above the
calendar; the hidden value is an ISO datetime.
**Type scale / tokens / cards:**
- Default font is now **Space Grotesk** (`--skua-font`; the installer adds the
Google Fonts link to the root layout).
- `.sk-lead` keeps the body color (not muted) and a bit larger; body paragraph
bumped to 15px.
- `Skua.Components.Display.card/1` (title/subtitle/footer slots).
- Generated home gains a **design-tokens** reference (color swatches + radius /
border / shadow / motion / font) so the themeable surface is visible.
**Top bar:** the theme toggle switch matches the form switch dimensions (36×20).
Build is now minified (`build.js`) — bundle ~8.8 KB gzip with 11 hooks. 55 tests.
### Polish pass (feedback from the live demo)
- **Theme toggle is now an animated switch** (`.sk-switch` style, sliding thumb
carrying a sun/moon glyph) instead of an icon button.
- **Typography:** `.sk-lead` is now h4-sized (clamp 1.25–1.5rem, regular weight)
and wins inside `.sk-content`.
- **Page background:** the installed home renders full-bleed on Skua's canvas
(`.sk-page`) so no host (daisyUI) background shows through — "greenfield" is
the clean neutral default, dark canvas `#0a0a0c`.
- **Phone validation note removed** from the demo; phone field is BYO-validation.
- **Bug fixes (caught in the live demo):**
- Nested popovers no longer land in the corner — `PanelStack.show` re-measures
on the next animation frame, fixing the stale-geometry race for a panel
opened from inside another panel.
- Multi-select badge spacing: the chip remove (`×`) is a `<button>` and now
gets its native chrome reset.
- Creatable combobox now **persists** created options across LiveView patches
(the hook re-injects client-created options on every sync, so a created tag
survives the server re-render that would otherwise wipe it).
- **New components for zip parity** (all token-styled, keyboard/ARIA-correct):
- `Skua.Components.Menu` — `menu`/`menu_item`/`menu_label`/`menu_separator`
with the W3C APG menu keyboard model (`SkuaMenu` hook) and a `role=menu`
top-layer panel.
- `Skua.Components.Form.otp_input/1` (the `SkuaOtp` hook gets a component) and
`chip_toggle/1` (checkbox chip group bound to an array field via `:has`).
- `input/1` gains `:leading`/`:trailing` affix slots.
- `Skua.Components.Display` — `badge/1`, `dot/1`.
- The generated home showcases all of it; the installer imports `Menu` +
`Display`. Bundle ~9.2 KB gzip (10 hooks). 49 tests.
### Installer (`mix skua.install`)
- Plain, idempotent Mix task (no Igniter dependency for consumers) that wires
Skua into a Phoenix 1.8 app and scaffolds an editable starter home page.
**Path-dep aware**: writes resolved asset paths when Skua is a `:path` dep
(no `deps/skua`), clean `deps/skua`/`"skua"` forms when it's a hex dep.
Steps: patch app.css `@import`, app.js hooks import + spread, web.ex imports
(excepting `button`/`input`), route flashes through Skua's toast group, strip
the default Phoenix navbar/branding, generate `home_live.ex`, route `/` to it,
add a pre-paint theme script. Every step degrades to a printed manual
instruction if a file doesn't match the default layout.
- Generated home (`priv/templates/skua_home.ex.eex`) showcases the install:
`Phoenix vX + Skua vX` badges, what's native, edit/use hints, theme toggle,
typography specimens, a combobox + multi/create combobox, date, phone, nested
popovers, a viewport-aware edge popover, a native modal, and per-kind toast
trigger buttons. Verified end-to-end in a fresh `mix phx.new` app.
- Popover fix found via the fresh demo: the trigger **is** the styled button now
(`trigger_variant` attr; the `trigger` slot is its label) — nesting a
`<.button>` inside the slot previously produced invalid nested buttons that
broke popover nesting.
- `Skua.Phone.validate_phone/3` now calls `Ecto.Changeset` via `apply/3`, so
apps without the optional `ecto` dep compile without warnings.
- Project skeleton: mix project with the `:phoenix_live_view` compiler wired
for colocated-hook extraction.
- Token + component CSS layer ported from the styled-layer prototype
(`assets/css/skua.css` — 12 semantic tokens + 3 motion tokens).
- Reference prototypes vendored under `_component_defaults/` (styled-layer
demo, LiveView hook/component prototypes, skua.sh brand system).
### Phase 1 (foundations)
- `Skua.Field`: `Phoenix.HTML.FormField` normalization — derived id/name/value,
bracket-safe DOM ids, changeset errors gated on `used_input?`, pluggable
error translation.
- `Skua.Components.Form`: `button`, `label`, `error`, `input`, `textarea`, and
`toggle` (checkbox/radio/switch). Toggles are now **keyboard-operable** (real
focusable input clipped via `.sk-opt-input`, CSS-driven visual) — fixing the
prototype's `hidden`-input bug — and checkboxes emit a hidden `false`
companion so deselection is never dropped from `phx-change`.
- `Skua.Components.Overlay`: `popover` (fixed: real focusable trigger with a
measurable box + `aria-*`, no `display:contents` bug) and `dialog` (native
`<dialog>` + `showModal()` with `JS.ignore_attributes("open")` for morphdom
safety).
- JS hooks bundle (`import { hooks } from "skua"`): rewritten `PanelStack` with
focus save/restore, `SkuaPopover`, `SkuaDialog`, `SkuaOtp`, `SkuaAutofill`
(the `data-rename` footgun dropped). ~2.9 KB gzip. Built via `node build.js`.
- `usage-rules.md` for the AI-legibility story; tests for the FormField layer.
- **Deviation from plan §3.2:** JS ships as a classic ES-module bundle, not
colocated hooks (PanelStack sharing). See PLAN.md.
- `Skua.Components.Select` + rewritten `SkuaSelect` hook: accessible
single/multi `<select>` (text or badge display, searchable, creatable) with a
real `<select>` as the server-authoritative value carrier and a W3C APG
combobox/listbox on top — `role=combobox/listbox/option`,
`aria-activedescendant`, `aria-selected`, and full keyboard support
(Arrow/Home/End, Enter, Space-to-toggle, Escape, type-ahead, Backspace to
remove the last chip). The prototype had only Enter/Escape. Multiple selects
append `[]` to the name and emit a hidden empty companion so deselect-all
reaches `phx-change`. Bundle now ~5.8 KB gzip.
- Phone harness (ported from the aif-core dogfooding app, consolidated and
zero-dep by default):
- `Skua.Phone.Countries` — the 230-country `{name, iso2, dial}` dataset.
- `Skua.Phone` — `countries/0`, `calling_code/1`, `e164/2`, `normalize/1`,
`valid?/1` (E.164; delegates to `ex_phone_number` when installed),
`infer_country/1`, `national_number/2`, `country_to_flag/1`, `filter/1`,
and the Ecto changeset validator `validate_phone/3`.
- `Skua.Components.Phone` — FormField-integrated phone field: searchable
country listbox (PanelStack + APG roles/keyboard) + as-you-type national
input + hidden canonical E.164. Country data ships per-render via a data
attribute (no bloat to the shared bundle).
- `{:ecto, optional: true}` added for the changeset validator. Bundle
~6.7 KB gzip with all six hooks.
- `Skua.Components.Toast` + `SkuaToast` hook: Phoenix-flash toasts.
`flash_group/1` stacks `:info`/`:error` in a fixed top-layer container;
`flash/1` styles a single flash (kind → variant) with `role=alert`, a close
button, and hover-pausing auto-dismiss. Drop-in for the core_components
`flash`/`flash_group` API after `--strip-daisy`.
- `Skua.Components.Date` + rewritten `SkuaDate` hook: a date input (hidden ISO
value carrier + calendar) with the **W3C APG date-grid** keyboard model —
`role=grid/gridcell`, roving tabindex, Arrow (±day/±week), Home/End (week),
PageUp/PageDown (±month), Enter/Space to pick, Escape to close — plus
`min`/`max` bounds and `aria-selected`/`aria-label` per day. The prototype's
calendar was click-only divs. Accepts ISO strings or `Date` structs.
- Bundle ~8.6 KB gzip (min+gzip) with all eight hooks; calendar/day cells are
now focusable `<button>`s.
**Phase 1 component set complete**: form inputs, select/combobox, phone, date,
dialog, popover, toast — all FormField-integrated and keyboard/ARIA-accessible.
### Browser verification (caught two real bugs)
Verified the full component set in a fresh Phoenix 1.8 app (path dep) — see
`guides/local-testing.md`. Confirmed working in-browser: select top-layer
listbox + keyboard, date APG grid (Arrow/Home/End nav + Enter select), phone
country picker + E.164 assembly, native dialog modal + focus trap. Two bugs the
unit tests couldn't catch, now fixed (+ regression tests):
- **Dialog showed when closed**: `.sk-dialog { display: flex }` (from the
prototype's JS-overlay era) overrode the native `<dialog>`'s UA
`display: none`. Now `.sk-dialog:not([open])` stays hidden, `[open]` lays out,
and the scrim moved to `::backdrop`; entry animation keys off `[open]`.
- **Toast/toggle had no DOM id**: `assign_new(:id, …)` no-ops because the
`attr :id` default already set the key to `nil`, so the `SkuaToast` hook
errored ("no DOM ID"). `flash/1` and `toggle/1` now derive id (and toggle's
name) explicitly — the same footgun fixed earlier in `Skua.Field`.
41 tests passing. Remaining before release: Phase 3 (installer, `--strip-daisy`,
doctor).