Skip to main content

CHANGELOG.md

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