# Changelog

## [Unreleased]

### Added

### Changed

### Fixed

---

## [1.2.0] - 2026-05-30

### Pure-admin v2.6.0 + v2.7.0 + v2.7.1 + v2.8.0 sync

Four upstream releases absorbed in a single library bump:

- **v2.6.0** (`05b416b`) — KPI showcase suite, framework token consolidation, Tailwind role palette, `pa-stat--square` redesign.
- **v2.7.0** (`12b9d23`) — `pa-modal--banded`, `pa-gauge` rebuild, CSS-variable consolidation sweep, link tokens, sidebar / btn-split / timeline / chip / live-card / outline-secondary fixes.
- **v2.7.1** (`2754d24`) — KPI showcases promoted from inline demo styles into permanent `pa-kpi-*` core components (8 SCSS partials, all `kpi-*` classes renamed to `pa-kpi-*`, per-component cascade vars namespaced to `--pa-kpi-*`).
- **v2.8.0** (2026-05-28) — the formerly-`[Unreleased]` post-2.7.1 commits graduated to a stable release (generic terminal tab strip, `auto-fit` cell-min grids on gauges + editorial, layout-ratio modifiers on hero + bento, composable `--no-prev`/`--no-delta`/`--no-target` toggles on numeric strip, `--no-delta` on sparkline list — all of which our Phase 2 already covered) **plus one architectural fix** (see below).

#### v2.8.0 architectural fix — CSS variable defaults at `:root` in the unthemed bundle

The bundled `@keenmate/pure-admin-core/css` (`dist/css/main.css`) now emits a complete neutral default for every `--pa-*` / `--base-*` token at `:root`. Before 2.8.0, only themes emitted `:root { --pa-* }` — consumers using the unthemed bundle standalone, OR any page during the FOUC window before its theme stylesheet finished loading, had `var(--pa-positive)` / `--pa-success-bg` / etc. resolve to invalid values. KPI sparklines and deltas rendered near-black via inherited text colour; web components fell back to hardcoded literals.

The fix lives in upstream's `main.scss` (not `_core.scss` — `_core.scss` stays purely component CSS, consumed by BOTH the unthemed bundle AND themes via `@import`; emitting `:root` there would duplicate when a theme also emits its own). Themes are unaffected — they bypass `main.scss` entirely.

**Impact for keen_pure_admin consumers**: no wrapper change required. Bump `@keenmate/pure-admin-core` to `^2.8.0` and KPI sparklines / deltas now render with reasonable neutral colours even before a theme link resolves — or with no theme at all. Supersedes the 2.7.1-era partial fix that only emitted the 5-step sentiment scale at `:root`.

#### KPI component family — 9 new modules / 12+ new function components

Built one module per showcase (matching `@keenmate/svelte-pure-admin` 1:1 in component names and prop names), all on the v2.7.1 `pa-kpi-*` class surface.

- **`PureAdmin.Components.Kpi`** (substrate)
    - `kpi_tile/1` — base tile (head · label · value · prev row · sparkline slot · optional hover detail). Used by Terminal grid + standalone. Status pill is `status_text` + `status_variant` (built-ins `warn` / `good` / `neutral`); `:head` snippet overrides the whole head row. Sentiment variants on value and delta use the 5-step scale (`very_positive` / `positive` / `neutral` / `negative` / `very_negative`). `variant` (formerly `spark_direction`) colours the sparkline via `currentColor`. `is_standalone` for tiles outside a `pa-kpi-terminal__grid`.
    - `kpi_detail/1` — popover element (`pa-kpi-detail` + `__title`). Auto-builds its `<dl>` from typed props (Current / Previous / Δ absolute / Δ percent / Target) on the host tile, or accepts raw markup via `:inner_block`. Replaces the previous `kpi_tile_detail/1` slot scaffold.
    - `kpi_sparkline/1` — opt-in convenience for the simple SVG polyline + trailing-dot pattern, on `pa-kpi-tile__spark`. Consumers using D3 / ApexCharts / Chart.js / Contex / etc. plug their renderer into the `:chart` slot instead.
- **`PureAdmin.Components.KpiDetail`** — shared helpers (`build_auto_rows/1`, `delta_to_sentiment/1`, `sentiment_class/1`, `dasherize/1`) used by every tile / row component. Mirrors `kpi-detail.ts` from svelte-pure-admin.
- **`PureAdmin.Components.KpiTerminal`** — `kpi_terminal/1` card wrapper with generic `:pane` tab strip (each pane has `id`, `label_text`, optional `is_active`); no panes → children wrapped in a single `pa-kpi-terminal__grid--2col`. `:header_controls` for custom toolbars between title and LIVE pill. The retired VALUE/Δ%/TREND view-mode toggle is replaced by the generic tab strip.
- **`PureAdmin.Components.KpiSparklineList`** — `kpi_sparkline_list/1` + `kpi_sparkline_row/1`. `is_no_delta` drops the rightmost Δ% column; `is_chart_first` rotates the L→R order 90° at narrow widths.
- **`PureAdmin.Components.KpiGaugeList`** — `kpi_gauge_list/1` + `kpi_gauge/1`. Default cell-min-driven `auto-fit` grid; switch via `grid_layout="2col"` or `"max_2".."max_6"`. `cell_min_width` overrides `--pa-kpi-gauge-cell-min`. `tick_position` / `tick_color` knobs.
- **`PureAdmin.Components.KpiHero`** — `kpi_hero_list/1` + `kpi_hero_main/1` + `kpi_hero_side/1`. `hero_split="2_3"` / `"3_4"` shifts weight to the hero (default 1:1). Hero has `:meta` slot (or `delta_text` / `period_text` / `target_text` for the canonical pattern), `:chart` for the sparkline; rail is a `:rail` slot.
- **`PureAdmin.Components.KpiBento`** — `kpi_bento/1` + `kpi_bento_tile/1`. Default 6-tile hero-left layout; `bento_layout="hero_right"` mirrors; `bento_layout="5_tile"` is hero + 4 supporting. `row_height` overrides `--pa-kpi-bento-row-height`. Set `is_hero` on the first tile.
- **`PureAdmin.Components.KpiStrip`** — `kpi_strip/1` + `kpi_strip_row/1`. Composable `no_previous_value` / `no_delta_percent` / `no_target_bar` toggles. Header row auto-generated from visible columns; override via `header_labels` (map keyed by column atom) or suppress via `no_header` or replace via `:head` slot. `target_bar_percent` drives the bar fill (capped at 100% visually); `target_percent_text` is the label below (may exceed 100).
- **`PureAdmin.Components.KpiEditorial`** — `kpi_editorial/1` + `kpi_editorial_tile/1`. Cell-min-driven `auto-fit` grid; `is_2_columns` boolean shorthand or `grid_layout="max_N"` cap modifiers. `target_text` auto-renders as `<em>tgt</em>{value}` in the meta row.
- **Three JS hooks** in `lib/assets/js/hooks/`:
    - `PureAdminKpiTile` — cursor-anchored Floating UI popover (virtual reference element); moves `.pa-kpi-detail` to `<body>` on mount, restores on `destroyed`. Auto-engaged whenever a tile / row has a popover.
    - `PureAdminKpiSparkDot` — converts SVG `<circle>` endpoints to `.pa-kpi-spark-dot` CSS spans so dots stay round under `preserveAspectRatio="none"`.
    - `PureAdminKpiTerminalTabs` — client-side tab strip wiring for `pa-kpi-terminal__tab` / `pa-kpi-terminal__pane`. Scoped per terminal so nested terminals (if any) stay isolated.

#### Other v2.7.0 component reworks

- **`Modal` — new `is_banded` boolean.** Emits `pa-modal--banded` alongside the existing `:variant` role modifier. Composes — `<.modal variant="success" is_banded>` produces `pa-modal pa-modal--success pa-modal--banded`. Buttons inside the bands auto-invert via the framework's CSS (light theme renders dark-on-pale; dark theme renders light-on-muted). No markup change beyond the new class.
- **`gauge/1` rebuild** — moved the label out of the donut so `__inner` holds only the value text. Label now renders as a sibling row alongside `__min` and `__max` below the gauge (matches v2.7.0 layout). New `:size` attr emits `--pa-gauge-size` inline (default upstream `12rem`). The `--value` style declaration is unchanged. Existing apps render the label in its new position automatically when they upgrade to `@keenmate/pure-admin-core` ^2.7.0; no markup change required.
- **`Stat` — 5-step sentiment scale on hero deltas.** `change_direction` now accepts `very_positive` / `very_negative` in addition to `positive` / `negative` / `neutral`. Internally converted to kebab-case (`pa-stat__change--very-positive` etc.) to match upstream's SCSS class names. Neutral colour shifted from `--pa-text-color-2` (grey) to `--pa-neutral` — purely a visual change, no API impact.
- **`Stat` icon `:danger`** — already exposed in the `icon_variant` enum (`primary` / `secondary` / `success` / `info` / `warning` / `danger`). The framework's previous omission of `--danger` was fixed in `_statistics.scss`; our wrapper already emitted the class so this becomes valid markup automatically when consumers upgrade.
- **`Card --live-up` / `--live-down`** — already exposed via `live_state="up"` / `"down"`. Upstream migrated the internal SCSS from `rgba(...)` over role colours to `color-mix()` over the 5-step sentiment scale; no API impact.
- **`btn-split`** — verified the wrapper doesn't emit `overflow: hidden` on `.pa-btn-split` (the v2.7.0 chevron-corner fix relies on the container NOT clipping). Wrapper is correct as-is.
- **`Timeline`** — v2.7.0 visual tweaks (simple-dot border-radius `50% → 30%`, shadow opacity `0.3 → 0.5`) are CSS-only with no wrapper change.

#### Demo app — `/kpi` section + non-KPI showcase updates

- **8 new LiveViews under `/kpi/*`** (sidebar entry "KPI", chart-line icon):
    - `/kpi/dashboard` — Combined dashboard exercising all 7 KPI components on one page (Hero + supporting + Terminal + Editorial + Sparkline list + Comparison gauges + Numeric strip + Bento). Useful for integration / spacing / theming verification.
    - `/kpi/terminal-grid`, `/kpi/sparkline-list`, `/kpi/comparison-gauges`, `/kpi/hero-supporting`, `/kpi/bento`, `/kpi/numeric-strip`, `/kpi/editorial-minimal` — each is a 1:1 port of upstream's `demo/views/kpi-*.mustache`: canonical card + layout-test stress sections (1×3 page-grid, 25/45 asymmetric, mixed grid modifiers) + per-page Usage Guide card + CSS Classes Reference card. The four chart-bearing pages (terminal grid, sparkline list, hero + supporting, bento) also include a Chart.js drop-in section demonstrating the library-agnostic `:chart` slot.
- **Chart.js drop-in** — `chart.js@4.4.3` loaded via CDN in `demo/lib/demo_web/components/layouts/root.html.heex`. New `PureAdminKpiChart` LiveView hook (`demo/assets/js/hooks/kpi_chart.js`, ~130 lines) renders bar / line / area charts into any `<canvas data-kpi-chart>`. Reads `currentColor` from the slot's KPI sentiment cascade, re-renders on `pa:theme-change`. Tied to LiveView mount/destroy lifecycle (not a one-shot DOMContentLoaded scan).
- **Modals demo** — new "Banded Modals · v2.7.0" section with 4 role variants (`is_banded` × success / warning / danger / info).
- **Stats demo** — new "5-step sentiment scale · v2.7.0" card showing all five hero deltas (`very_positive` / `positive` / `neutral` / `negative` / `very_negative`) side-by-side.
- **Cards demo** — new "Live-data direction · live_state" section with up / neutral / down tinted cards.
- **Data visualization demo** — new gauge `:size` examples (8rem / 12rem default / 16rem / 20rem) + subtitle noting the v2.7.0 layout rebuild.

#### Theme system — per-developer disk overrides + manifest improvements

- **`demo/pureadmin.json`** — declared all 15 themes (was 5). Team-wide, fetched via lockfile against `pureadmin.io`.
- **`demo/.pureadmin.json`** (gitignored — per-developer override) — maps every theme slug to `../../pure-admin-themes/{slug}` so themes load from a sibling checkout instead of the remote registry. Mirrors upstream `pure-admin/.pureadmin.json`.
- **`DemoWeb.ThemePlug` — dual-layout support.** Themes are now served correctly from BOTH the registry layout (`css/{name}.css` — zip extraction) AND the local-build layout (`dist/{name}.css` — direct copy from a sibling `pure-admin-themes` checkout). `valid_theme_dir?/2` probes either layout. New `resolve_theme_file/3` transparently maps the public URL `/themes/{slug}/css/{slug}.css` to the actual on-disk file. Without this, the on-demand re-downloader silently overwrote local copies on every page load.
- **`DemoWeb.PageContext.slim_color_variants`** — now passes through `description` per variant so the settings panel JS can use it as a `<option title>` tooltip.
- **`PureAdminSettings` JS hook — manifest-driven CSS path.** `_resolveThemeHref/1` reads `manifest.colorVariants[0].file` to build the stylesheet URL (handles both `dist/` and `css/`). `_applyThemeMode/1` is now pattern-aware via `manifest.modeCssClass` and clears every mode class declared by the manifest (was hardcoded `light` / `dark` only — important for themes declaring custom mode ids).

#### Tooling — Makefile + themes-install wiring

- **`Makefile`** — new `themes-install` target runs `npx @keenmate/pureadmin themes install` from `demo/` (defensive: skips silently if neither `demo/pureadmin.json` nor `demo/.pureadmin.json` exists). `dev:` and `setup:` now depend on `themes-install`, so `make dev` snapshots themes from `.pureadmin.json` disk overrides (or the remote-pinned lockfile) before booting the Phoenix server.

#### Fixed — KPI sparkline escape

- **Sparklines escaping their containers in Hero + supporting / Bento / Combined dashboard.** `PureAdminKpiSparkDot` always wrapped the SVG in `.pa-kpi-spark-wrap` (which has no `height` declaration), even when the SVG's parent was already a tight positioned anchor with explicit `height: 3rem` (`.pa-kpi-hero-main__chart-svg`, `.pa-kpi-bento-tile__chart-svg`). The extra wrap broke the SVG's `height: 100%` chain; combined with `preserveAspectRatio="none"` + `overflow: visible`, the polygon stretched across the entire viewport. Ported upstream's tight-anchor check from `pure-admin/demo/js/kpi-showcases.js`: skip wrapping when `getComputedStyle(parent).position !== 'static'` AND `parent.height ≈ svg.height` (within 4px). Terminal grid (where the SVG IS the anchor with its own explicit height) was unaffected by the bug and remains unaffected by the fix.

#### Fixed — KPI tooltip popover rendered empty (black box)

- **Every KPI tile / row showed an empty black popover on hover** when the host supplied `detail_title_text` + auto-built rows (the typical case). Phoenix's `inner_block` slot is always a non-empty list whenever the caller has open/close tags around the component — even if the only content between the tags is an empty `<%= for d <- @detail do %>…<% end %>` loop that produces no output. `kpi_detail/1`'s `has_inner?` check (`assigns.inner_block != []`) treated that as "consumer provided custom popover content" → took the inner_block branch → rendered nothing → typed `title_text` + `rows` were silently ignored. The visible result was the popover chrome (`pa-kpi-detail` background + shadow) with no content inside.
- **Fix at every call site** (8 tile/row modules: `kpi_tile/1`, `kpi_sparkline_row/1`, `kpi_gauge/1`, `kpi_hero_main/1`, `kpi_hero_side/1`, `kpi_bento_tile/1`, `kpi_strip_row/1`, `kpi_editorial_tile/1`): branch the call into `<.kpi_detail>...</...>` form when the consumer's `:detail` slot has content, and `<.kpi_detail .../>` (self-closing) form otherwise. Self-closing leaves `inner_block == []`, so `kpi_detail/1`'s `has_inner?` check becomes a reliable signal. Auto-built rows render correctly; consumer's custom `:detail` slot still wins when supplied.
- **Defensive: no tooltip when none defined.** `has_detail?` is computed as `@detail != [] or @detail_title_text != nil` — when both are absent, `phx-hook="PureAdminKpiTile"` isn't emitted, the `<.kpi_detail>` element isn't rendered, no `.pa-kpi-detail` exists in the DOM, and the JS hook's `mounted()` also has `if (!this.detail) return` as a safety net.

#### Added — `pa:theme-change` event dispatch in settings panel hook (canvas chart reactivity)

- **`PureAdminSettings` hook now dispatches `pa:theme-change` window events** so consumer code that snapshots colours at draw time (Chart.js, ECharts, D3, custom canvas) can re-sample after a theme appearance change. Three event kinds:
    - `{ kind: "mode", mode }` — fired at the end of `_applyThemeMode` after the `pa-mode-*` body class flips (light↔dark toggle).
    - `{ kind: "variant", variant }` — fired at the end of `_applyColorVariant` after the `pa-color-*` body class flips (color variant picker).
    - `{ kind: "theme", themeId }` — fired tied to the `<link id="pa-theme-css">` element's `load` (or `error`) event after a theme stylesheet swap, so canvas re-sampling happens AFTER the new CSS is in effect — not before. One-shot listener that auto-removes itself; the next swap installs its own.
- **Early-return paths intentionally skip the dispatch** — when the target class is already on `<body>` (nothing visual changed) the events don't fire, so consumers don't waste cycles re-drawing.
- **Matches upstream pure-admin demo's contract** (`demo/js/settings-panel.js` dispatches the same event with the same kinds for mode / variant). Theme-swap dispatch is our addition — upstream doesn't fire on swap; the svelte side (1.8.0) covers this case with their separate `chartColorSync` action that listens for the link `load` independently.
- **Why this matters**: SVG sparklines re-colour live via `currentColor`. Canvas charts (including the existing `PureAdminKpiChart` hook in the demo) cache `getComputedStyle(canvas).color` at draw time. Without this dispatch, the canvas froze on every theme/mode change while the SVG around it updated — visually broken. With this dispatch, the hook's `_recolor()` runs and the canvas re-paints in place.

#### Changed — `PureAdminKpiChart` demo hook gains `stacked-bar` + `doughnut` chart types

- **Two new `data-kpi-type` values** for the `demo/assets/js/hooks/kpi_chart.js` hook:
    - `data-kpi-type="stacked-bar"` — multi-series stacked bar chart. `data-kpi-points` accepts array-of-arrays JSON (`"[[120,140,160,180],[280,290,300,320],[60,70,80,90]]"`); each inner array becomes a stacked series. Optional `data-kpi-labels='["Q1","Q2","Q3","Q4"]'` for x-axis labels (mostly cosmetic — axes stay hidden).
    - `data-kpi-type="doughnut"` — single-series doughnut. `data-kpi-points` is a flat array of slice values; cutout fixed at `62%`, slice spacing `2px`.
- **`SERIES_OPACITY = [0.95, 0.65, 0.42, 0.25, 0.15]`** — multi-series and multi-slice colours derive from the host's resolved `currentColor` at decreasing alpha, so each series/slice reads distinctly while still inheriting the KPI sentiment + theme cascade. Both new types re-paint correctly on `pa:theme-change`.
- **`readPoints/1` is now flatten-safe** — when `data-kpi-points` is array-of-arrays (multi-series format) and a single-series chart type (`line` / `bar`) is requested, it returns the first inner array. So the same data attribute shape can drive either single-series or multi-series charts.

#### Changed — Dashboard rewrite (1:1 with @keenmate/svelte-pure-admin v1.8.0)

- **`/` (`DashboardLive`) overhauled** to mirror svelte-pure-admin's `docs/src/routes/+page.svelte`. Diff vs. the previous version:
    - Old placeholder "Top Sales Products" card (an `<i class="fa-chart-bar">` icon and the text "Chart Placeholder") replaced with a real **`kpi_sparkline_list`** carrying 5 product rows (Epsilon up-strong, Alpha / Delta / Beta up, Gamma down), each backed by a Chart.js area sparkline via `<canvas data-kpi-chart>`.
    - Old "Revenue Trend" section (hand-drawn double `<polyline>` SVG with hardcoded points) replaced with a **`kpi_hero_list` `hero_split="2_3"`** + a 13-point Chart.js area chart in the hero + 3 `kpi_hero_side` rail tiles (YTD Revenue, Q4 Actual, Forecast EOY).
    - Old separate "Revenue Trend & Traffic Sources" row removed — Traffic Sources moved into the right column of the new Top Sales row.
    - Timeline items in "Recent Activity" — first 3 (most-recent) items now carry `is_filled` to match svelte's `isFilled`. Filled markers signal "just happened"; the older two stay outlined.
    - New `mount/3` assigns: `top_sales` (5 products with sentiment-ordered ranking + Chart.js data series strings) and `revenue_trend_points` (13-point string).
    - Everything else (4-up metric cards, KPI squares grid, Recent Orders table, Top Products / System Status / Quick Actions footer row) unchanged.

#### Changed — `/kpi/dashboard` extended with Chart.js stacked bars + doughnuts

- **New section: "Revenue by Segment · Q1–Q4 weekly"** — `kpi_sparkline_list` with 4 rows (Enterprise / SMB / Consumer / Marketplace) each showing a stacked-bar Chart.js chart (New + Renewals + Upgrades, 4 quarters). Each row carries the full popover prop set (Current / Previous / Δ absolute / Δ percent / Target) so the tooltip-fix is also exercised on this section.
- **New section: "Pipeline & Distribution · Chart.js mix"** — `kpi_bento` mixing both new chart types in one layout:
    - Hero: Pipeline by Stage (stacked bars, 8 weeks × 3 stages)
    - Customer Mix + Top Channels + Forecast Confidence (doughnuts at 4 / 5 / 2 slices)
    - Deals by Region + Stalled % (stacked bars, 6 categories × 2 series)
    - Sentiment cascade exercised: Stalled % is `negative` (red), Forecast Confidence is `neutral` (grey), the rest are `positive` / `up_strong` (green tones).
- **Integration test coverage**: with all the recent fixes in place, this page exercises (a) the popover-rendering fix in 4 sparkline rows + 6 bento tiles, (b) the `pa:theme-change` event dispatch by re-colouring both inline-SVG sparklines (via `currentColor`) and Chart.js canvases (via `_recolor()`) on mode flip / theme swap, and (c) the new stacked-bar + doughnut chart types via the `SERIES_OPACITY` cascade.

#### Docs / demo / bookkeeping (catch-up pass)

- **`docs/theming.md` rewritten** — full reference for the v2.8.0 token surface: canonical role tokens (`--pa-success` / `-warning` / `-danger` / `-info`), 5-step sentiment scale, text-contrast tiers (`--pa-text-strong` / `-secondary` / `-tertiary`), surface tints (`--pa-surface-hover` / `-track`), link tokens, chart-trendline tokens, detail-popover chrome, gauge-size, KPI namespaced tokens, plus a callout for the v2.8.0 `:root` defaults architectural fix.
- **`pa-stat--square` prefix-currency mode wired up.** New `is_prefix_symbol` boolean on `stat/1` (square variant only) flips DOM order to `<symbol><number>` for prefix currencies (`$847K`, `¥12.4M`); default false renders `<number><symbol>` for suffix units (`87%`, `23°C`). Matches v2.6.0's "markup order drives visual order" contract — no CSS modifier needed. Demo `/components/stats` gained a "Square stats — mixed units · v2.6.0" card showing all four cases side-by-side.
- **`component-audit.md` re-stamped to `d49531c` (v2.8.0).** Seven existing rows re-anchored to `12b9d23` (v2.7.0): Modal (banded), Card (live-up/down sentiment), Stat (square redesign + 5-step), Timeline (visual tweaks), Button (btn-split chevron-corner fix), DataDisplay (fields-chips polish), DataViz (gauge rebuild). Nine new rows added under "no current snippet" for the KPI family (Kpi, KpiDetail, KpiTerminal, KpiSparklineList, KpiGaugeList, KpiHero, KpiBento, KpiStrip, KpiEditorial) — each anchored to its v2.7.1 SCSS partial plus the v2.8.0 commit that added the layout-modifier follow-ups.
- **`package.json` — declared `peerDependencies: { "@keenmate/pure-admin-core": "^2.8.0" }`.** Previously the package made no machine-readable claim about which framework version it targeted; the CHANGELOG and README said "^2.8.0" but `package.json` was silent.

### Pure-admin v2.5.0 sync

Re-anchored the alert component to pure-admin v2.5.0 (`1f9d818`). The framework rewrote the alert layout: structural children stack via `flex-basis: 100%` instead of inheriting the alert's flex row, the heading defaults to a compact look with the punchy treatment becoming opt-in, and the icon-vs-content alignment was inverted to centre by default.

- **`Alert` — drops the `pa-alert__content` wrapper when no `:icon` slot is supplied (Breaking).** The framework's flex-wrap rules give structural children (`__heading`, `__list`, `__actions`, top-level `<p>`/`<hr>`) `flex-basis: 100%` so each lands on its own row inside `.pa-alert` directly. Wrapping them in `__content` "for consistency" moved them out of the `> p` / `> hr` selector reach, which broke the new layout. The wrapper is still emitted whenever an `:icon` slot is present (icon + non-icon content has to be the two flex children of the alert). **Migration:** apps that styled descendants via `.pa-alert__content >` selectors should switch to `.pa-alert >` selectors when the alert has no icon.
- **`Alert` — new `heading_size` attr (`nil` \| `"lg"`).** v2.5.0 unified `pa-alert__heading` to default to the body font-size + semibold weight. `heading_size="lg"` adds the `pa-alert__heading--lg` modifier for the louder, deliberate-read presentation (blocking errors, system updates, quota warnings). Existing alerts that used `<:heading>` or `heading_text` will render visually smaller than before — pass `heading_size="lg"` to preserve the previous appearance.
- **`Alert` — new `is_multiline` attr.** Adds `pa-alert--multiline` to opt back to `align-items: flex-start`. Use when an icon sits next to multi-line `__content` (heading + body + actions) so the icon stays at the top with the heading instead of centring against the whole stack. Default centred alignment is correct for icon + single-line content.
- **`alert__actions`, sizes, and padding scale** — no markup change required on our side; rendering automatically picks up the new toast-style separator above `__actions`, the real `--sm` / `--lg` size scale, and the centred default alignment as soon as the consumer upgrades `@keenmate/pure-admin-core` to ^2.5.0.
- **`component-audit.md`** — alert row re-stamped to `2ef8034` (2026-04-25, v2.5.0). Other components unchanged since v2.5.0 only touched `_alerts.scss` and supporting variables.

Demo `/components/alerts` page gained a "Header style: compact vs. punchy" card (same Validation failed / Saved messages rendered both ways), an explicit "Sizes" stack (sm / default / lg), and an "Icon with multi-line content" example showing `is_multiline`. The old "Compact Alerts in Grid" card was renamed to "Status strip layout" since it's a real-world layout pattern, not a sizes demo.

### Component audit

Cross-checked every library component against the current `@keenmate/pure-admin-core` HTML snippets and recorded per-component audit status in the new `component-audit.md` at repo root. **30 snippets** reviewed across two passes (anchor pure-admin commit `cf75736`):

- First pass (2026-04-24, anchor `e4f1cd6`): 23 snippets that were upstream-audited at the time.
- Second pass (2026-04-25, anchor `cf75736`): 7 newly-audited snippets that closed upstream's pending list and snippet gaps — `modals.html`, `modal-dialogs.html`, `data-display.html`, `notifications.html`, `statistics.html`, `filter-card.html`, `detail-panel.html`. None of the previously-audited snippets changed.

Drift fixes:

- **`Popconfirm`** — server render now emits the initial `pa-popconfirm--{bottom|top|start|end}` class (previously only `data-placement` was set, so CSS rules keyed on the class rendered inconsistently before Floating UI ran). The client-side position helper in `events/popconfirm.js` now strips the logical `start|end` class pair on flip (was looking for physical `left|right` that never appeared) and maps Floating UI's physical `result.placement` back to our logical class via a `physicalToLogical()` helper — RTL collision-flipped popconfirms now render correctly.
- **`Popover`** — title in `.pa-popover__header` now renders as `<h4>` (was `<span class="pa-popover__title">`) to match the snippet's semantic heading pattern and inherit the framework's heading-reset rules.
- **`Callout`** — `.pa-callout__heading` now renders as `<h4>` (was `<div>`) to match the snippet; picks up the shared heading margin reset instead of needing override rules.
- **`Card`** — the `:tools` slot now emits `<div class="pa-card__actions">` (was `pa-card__tools`, which has no CSS backing). Slot name kept for API stability.
- **`Loader` / `spinner`** — `size` attr narrowed from `[nil, "xs", "sm", "md", "lg", "xl", "2xl"]` to `[nil, "xs"]`. The other sizes produced invalid class names (`pa-spinner--lg`, etc.) that don't exist in the SCSS framework — they all rendered at the default 16 px, which is confusing. Demo page updated to show only default + `--xs`. **Breaking for apps passing those size values** — drop the attr to fall back to default.
- **`Modal`** — header close button class flipped from `pa-btn pa-btn--primary pa-btn--icon-only pa-btn--sm` to `pa-btn pa-btn--sm pa-btn--icon-only pa-btn--secondary` for the default modal and `… pa-btn--light` for themed modals (`variant`/`header_variant` set), matching the snippet's "secondary on neutral header / light on coloured header strip" pattern.
- **`PureAdminDetailPanel` JS hook** — full rewrite to match the contract documented in `detail-panel.html`. Drag handle selector changed from `.pa-detail-panel__handle` (which never matched real markup) to `.pa-detail-panel-resize`; new width is written to `--pa-local-detail-panel-width` on `<html>` (was inline `style.width` on the panel, which only worked when the panel had no width-via-CSS-variable rule — i.e. never on real pure-admin markup); body picks up `pa-detail-panel-resizing` during drag to suppress text selection; handle picks up `pa-detail-panel-resize--active`; drag direction inverts in RTL; min-width clamped to 200 px. **Breaking for apps wiring the old hook** — they relied on a selector that didn't match the snippet anyway; switch the handle markup to `<div class="pa-detail-panel-resize">` and the panel will resize correctly.
- **`component-audit.md` (new)** at the repo root tracks every component's audit status, which snippet it was verified against, and the pure-admin commit hash at time of verification. Re-audits flip the row back to ⏳ when upstream ships a newer snippet.

Components with acknowledged gaps deferred to a later release (tracked in `component-audit.md`):
- **`Code`** — still a Phase-2 stub (class naming / copy button / syntax tokens not yet modeled).
- **`Profile`** — favourites subsystem (`__favorite-item`, `__favorite-icon`, `__favorite-label`, `__favorite-remove`, `__favorites-add`) not yet exposed as components; apps hand-roll the markup today.
- **`Timeline`** — alternating-layout `__date`/`__time` logic is muddled and `pa-timeline--single-column` isn't exposed.

### Security

- **`Pager` — dropped `Phoenix.HTML.raw/1` on the `icon_*` attrs (audit finding #1, High).** The four navigation icon attrs (`icon_first`, `icon_previous`, `icon_next`, `icon_last`) are now rendered HTML-escaped and default to Unicode chevrons (`«‹›»`) rather than HTML entity strings. Callers that need markup (e.g. Font Awesome, inline SVG) use the new `:first_icon`/`:previous_icon`/`:next_icon`/`:last_icon` slots. **Breaking for apps passing HTML in the string attrs**; migrate those to the slots.
- **`PureAdminFlash` hook — validate URL scheme in markdown links (audit finding #2, High).** Markdown links of the form `[text](url)` previously let any URL — including `javascript:alert(1)` — flow into `href=`. URLs starting with `javascript:`, `data:`, or `vbscript:` (case- and whitespace-insensitive) are now replaced with `#`. Other schemes (http/https/mailto/tel) and relative paths pass through unchanged.
- **Strict-CSP support — removed every inline `onclick=` and `<script>` from components (audit findings #10 and #11).** `popconfirm/1`, `popover/1`, `badge_group/1`, `tabs/1` (scrollable), and `field/1` (copy buttons) previously rendered inline event handlers and embedded `<script>` blocks that required `'unsafe-inline'` on CSP `script-src`. Behaviour moved into a single delegated-events module (`lib/assets/js/events/`) and exposed via **new `initPureAdminEvents()` export** — components now emit `data-pa-*` attributes and the module's document-level click listeners dispatch on them. Apps can now run with `script-src 'self'` out of the box; the FOUC prevention script (`<.fouc_prevention_script />`) remains the single inline `<script>` and needs a per-request nonce in strict-CSP setups.
  - **Migration:** add `initPureAdminEvents()` alongside your existing `initModalDialogs()` call in `app.js`. No template changes required for library components.
- **`desc_table` — `label_width` validated as a CSS length (audit finding #4).** The attr is now interpolated into `style=` only if it matches a single CSS length token (digits + `%`/`px`/`rem`/`em`/`vw`/`vh`/`ch`/`cm`/`mm`/`in`/`pt`/`pc`). Anything containing `;`, whitespace, parens, or extra tokens is ignored rather than injected, closing a CSS injection path if the attr is bound to user input.
- **Flash / Toast — action buttons no longer round-trip through `data-action` (audit finding #5).** `push_flash` / `push_toast` actions were previously serialised to JSON, attribute-escaped, then parsed back on click. Buttons are now built as DOM elements with the action object captured directly in a closure — no JSON in attributes, no bespoke `_escapeAttr`, and no attribute-escape attack surface. Removed the now-unused `_escapeAttr` helper from both hooks.
- **`PureAdminProfilePanel` — URL scheme check before navigation (audit finding #6).** Favourite items previously assigned `dataset.href` directly to `window.location.href`. `javascript:` / `data:` / `vbscript:` / `file:` URIs are now rejected; everything else (http/https, `mailto:`, `tel:`, `sms:`, deep-link schemes like `slack://`, `intent://`, `myapp://`, relative paths) passes through.
- **`PureAdmin.Helpers.safe_url/1` (audit finding #7).** New helper that returns `url` if it uses a safe scheme, otherwise a fallback (defaults to `"#"`). Deny-list semantics — blocks only the four schemes a browser will execute (`javascript:`, `data:`, `vbscript:`, `file:`), so custom app schemes and non-HTTP protocols still work. Applied automatically to `href={@href}` in `pa_link/1`, `button/1`, `navbar_nav_item/1`, `sidebar_item/1`, and `profile_nav_item/1`, so apps binding user content to those attrs get defense-in-depth for free.
- **`getPageContext()` — robust parse (audit finding #8).** The hidden-input JSON is now parsed inside a try/catch and validated to be a plain object; malformed or non-object payloads fall back to `{}` with a `console.warn` instead of throwing into the caller.
- **Settings / sidebar resize — localStorage inputs validated (audit finding #9).** `font-size` and `container-width` settings are now allowlist-checked before being concatenated into a CSS class name. Sidebar width is integer-parsed and clamped to `[180, 500]` when restored from storage (and only the numeric value, no unit, is persisted).
- **`PureAdmin.custom()` modal — documented trust boundary (audit finding #12).** The caller-supplied `render(container, close)` function writes directly to the DOM — the JSDoc now makes it explicit that this is a ⚠ XSS sink if user content is inserted via `innerHTML`.

### Demo

- Swap the *Stored Submissions* card for `<.table_card is_scrollable>` on the `/phoenix/form-demo` page so narrow viewports scroll the table horizontally inside the card instead of clipping the rightmost columns.

---

## [1.1.0] - 2026-04-23 [PUBLISHED]

### Added

- **`:field` attr on form components** — `input/1`, `textarea/1`, `select/1`, `checkbox/1`, `radio/1`, and `form_group/1` now accept `field={@form[:x]}` for one-line Phoenix form binding. Derives `name`, `id`, `value` (or `checked`), and error state from the `Phoenix.HTML.FormField` struct; explicit attrs still win. `used_input?/1` is respected so unsubmitted fields don't show stale errors.
- **Auto-rendered field errors** — when `field=` is set and the field has errors, `input`/`textarea`/`select` automatically render a `form_help` below themselves in the error variant. Opt out with `show_errors={false}`. `form_group` flips to `validation="error"` in the same condition.
- **`PureAdmin.Components.Form.translate_error/1`** — default `%{key}`-interpolating error formatter, overridable via `config :keen_pure_admin, :error_formatter` (MFA tuple or 1-arity function) for Gettext-aware apps.
- **`PureAdmin.DateTime`** — new top-level helper with `format/2` (styles: `:short_date`, `:long_date`, `:full_date`, `:time`, `:long_time`, `:short_date_time`, `:long_date_time`, `:relative`, or any raw strftime pattern) and `relative/2` (buckets time diffs into `now` / seconds / minutes / hours / yesterday / days / weeks / months / years, past and future; accepts `now:` for deterministic tests). Accepts `Date`, `NaiveDateTime`, and `DateTime`. Month names, weekday names, and relative phrases all flow through `PureAdmin.Translations.t/2`.
- **Translation keys** — 47 new keys under `pureAdmin.datetime.*` (connectors, relative phrases past/future, 12 month names, 7 weekday names).
- **`push_flash/5` gained `replace: true`** — wipes any existing alerts in the container before rendering the new one, so status messages don't stack.
- **`PureAdmin.Components.Flash.clear_flash/2`** — dedicated "clear without pushing" helper.
- **`PureAdminFlash` hook** — honors both the `replace` payload flag and a new `pa:flash-clear` event.

### Fixed

- Getting Started page — resolved an outdented-heredoc compiler warning by moving a code block into a module attribute.

---

## [1.0.0] - 2026-04-11 [PUBLISHED]

First stable release. Phoenix LiveView component library wrapping Pure Admin CSS framework into 35+ function components, 14 JS hooks, and 3 LiveComponents.

### Highlights

- **Drop-in CoreComponents replacement** — `use PureAdmin.Components` replaces the Phoenix-generated `CoreComponents` module
- **`PureAdmin.Config`** — centralized app configuration (`:app_name`, `:app_logo`, `:app_version`, `:copyright`, `:font_class`). Components like `navbar_brand/1` and `footer/1` read from it automatically
- **`package.json`** — enables `import "keen_pure_admin"` in esbuild for hex dependents
- **`pureadmin create` template** — scaffold a full Phoenix LiveView app with PureAdmin layout via the [PureAdmin CLI](https://www.npmjs.com/package/@keenmate/pureadmin)

### What's included

- 35+ function components: layout, navbar, sidebar, buttons, badges, cards, tables, modals, forms, tabs, tooltips, toasts, flash, pagers, loaders, timeline, code, stats, and more
- 14 JS hooks: sidebar toggle/resize/submenu persistence, settings panel, profile panel, command palette, toast/flash, tooltip/popover (Floating UI), split button, char counter, checkbox, infinite scroll
- 3 LiveComponents: CommandPalette, ToastLive, DialogService
- Full BEM class support with `build_classes/3` helper
- i18n via runtime translation callback (~60 keys)
- Page context system (server → JS, CSP-safe)
- Persistent logging (`PureAdmin.logging.enableLogging()` survives page reloads)

### Installation

```elixir
{:keen_pure_admin, "~> 1.0"}
```

See the [README](README.md) for full setup guide or use the PureAdmin CLI:

```bash
npx @keenmate/pureadmin create my-app --template phoenix-liveview
```

Compatible with `@keenmate/pure-admin-core` v2.3.6 and Phoenix LiveView ~> 1.0.

---

## [1.0.0-rc.2] - 2026-04-03 [PUBLISHED]

### Added

- **`PureAdmin.Config`** — application-level configuration system. Set `:app_name`, `:app_logo`, `:app_version`, `:copyright`, `:font_class` in `config.exs` and components read from it automatically
- **`navbar_brand/1`** — falls back to config `:app_name` and `:app_logo` when no inner content provided
- **`footer/1`** — falls back to config `:copyright` (start slot) and `:app_version` (end slot) when no slots provided
- **`PureAdmin.Config.root_html_attrs/0`** — returns `%{class: font_class}` for the `<html>` element, supports `pa-font-responsive` and granular `pa-font-base-{9-12}` / `pa-font-mobile-{9-12}` classes from pure-admin-core v2.3.6
- **Getting Started page** — new demo page with installation, setup timeline, responsive font sizing, available themes, and component overview (mirrors Svelte demo structure)

### Changed

- **Root layout** — removed separate `pure-admin.css` link; theme CSS already includes the core framework
- **Dockerfile** — switched theme download from broken `curl` + zip API to `npx @keenmate/pureadmin themes --dir` CLI, added CSS copy step for `app.css`

### Bug Fixes

- **Sidebar** — fix mobile toggle not working: `toggle_sidebar()` was hardcoded to dispatch to `#sidebar`, now accepts configurable target ID via `navbar_burger` `target` attr
- **Logger** — `enableLogging()` now persists across page reloads via localStorage
- **Sidebar** — optimize resize handler to only act on breakpoint crossings
- **Docker build** — fix 404 for CSS assets (`pure-admin.css`, `audi.css`) — app CSS was never copied to `priv/static` and theme bundle API returned empty zip

## [1.0.0-rc.1] - 2026-04-01 [PUBLISHED]

### Flash Messages

- **`PureAdmin.Components.Flash`** — new module with two approaches:
  - **Standard flash** — `flash/1` and `flash_group/1` as drop-in replacements for CoreComponents, styled with `pa-alert` BEM classes. Works with Phoenix's built-in `put_flash/3`
  - **Independent flash containers** — `flash_container/1` + `push_flash/5` for multiple independent flash groups on the same page. Each container receives messages independently via a JS hook
- **`PureAdminFlash`** JS hook — client-side rendering of flash alerts. Supports markdown body (**bold**, *italic*, `[links](url)`, lists, `---` horizontal rules), action buttons with `pushEvent` callbacks, auto-dismiss, and dismissible close button
- **Markdown body** — flash message text supports basic markdown rendered as proper `pa-alert__content` HTML
- **Action buttons** — flash messages can include action buttons that push events back to the LiveView or dismiss the flash

### Toast Updates (pure-admin 2.3.0)

- **Toast action buttons** — `push_toast/5` accepts `:actions` option with `%{label, event, params, variant, dismiss}` maps. JS hook renders `pa-toast__actions` with `pa-btn--xs` buttons inside `pa-toast__content`. Clicking an action fires `pushEvent` back to the server, then auto-dismisses
- **Toast progress bar** — `:progress` option renders `pa-toast__progress` bar that animates from 100% to 0% over the duration. `:progress_color` option overrides the bar color via inline style
- **Filled toasts via push_toast** — `:filled` option renders `pa-toast--filled-{variant}` class
- **`max_width`** — custom max-width per toast (e.g. `max_width: "50rem"`)
- **Width ratchet** — container `min-width` tracks the widest toast shown, resets when container is empty
- **Click-to-dismiss** — toasts without actions are click-to-dismiss (cursor: pointer). Toasts with actions require close button or action click

### Command Palette v2

- **Multi-step commands** (`/prefix`) — register commands with `steps`, each with `prompt`, `placeholder`, and optional `free_text`. Steps progress sequentially with selections displayed as locked tokens. Commands complete via `handle_info({:command_complete, cmd_id, selections})`
- **Search contexts** (`:prefix`) — register scoped search contexts with `shortcut` and `aliases`. Typing `:p laptop` searches products for "laptop"
- **Global search** — typing without a prefix searches across all data
- **6 modes** — `idle`, `command_list`, `command_step`, `context_list`, `context_search`, `global_search` with full state machine transitions
- **Step filtering** — typing in a command step filters the step options in real-time
- **Step back** — Backspace at position 0 or Escape goes to previous step (or back to command/context list)
- **`cp:` event protocol** — namespaced events (`cp:toggle`, `cp:input`, `cp:select`, `cp:step_back`, etc.) replacing `command_palette_` prefix
- **`cp:reset_input` push_event** — force-clears browser input on mode transitions (LiveView doesn't patch focused inputs)
- **Debounced search** — 150ms debounce for context and global search, instant for command/context list filtering
- **Mode-aware keyboard** — Escape goes back in step/context modes instead of closing. Footer hints update per mode
- **Two display styles** — `display="inline"` (default, Svelte-style: full sentence in input with command badge) and `display="tokens"` (original: colored token spans above a clean input). Switchable at runtime

### Command Palette (pure-admin 2.3.3 / 2.3.4)


- **`pa-command-palette__input-wrapper`** — new wrapper around input + context label for correct positioning
- **`pa-command-palette__token-prompt`** — step prompt text between token badges (replaces `__token--prompt`)
- **Standard `pa-badge`** — item badges now use `pa-badge` instead of custom `pa-command-palette__item-badge`
- **Token badges** — step tokens in tokens mode use `pa-badge pa-badge--primary` for command name, plain `pa-badge` for values
- **Tokens `&:empty` hiding** — tokens div hides automatically when empty via CSS
- **Home screen** — idle state shows categorized list of commands (with Alt+key hotkey badges) and search contexts, all clickable
- **Hotkeys** — `Alt+D` Deploy, `Alt+A` Assign, `Alt+G` Go to Page, `Alt+T` Switch Theme. Work globally and inside the palette
- **Global search includes commands/contexts** — typing "deploy" finds the Deploy command alongside data results. Selecting a command/context enters that mode
- **Form codes** — `/go` page options have numeric codes (e.g., `24` for Alerts). `filter_options` matches on label, description, and exact code
- **`pa-command-palette__home`** — home screen container with `__home-section` separators and `__home-heading` labels
- **`pa-command-palette__shortcut`** — flex container for multi-key hotkey badge groups

### Translations (i18n)

- **`PureAdmin.Translations`** — new module with runtime translation callback system. Ships with ~60 English defaults under `pureAdmin.*` flat keys. Apps override via config: `config :keen_pure_admin, translate: &MyApp.translate/2`
- **`t(key, params)`** — main translation function with `%{param}` interpolation. Falls back to English when callback returns nil or isn't configured
- **`interpolate(string, params)`** — exported helper for app callbacks
- **All components updated** — hardcoded English strings replaced with `t()` calls across command palette, pager, popconfirm, modal, alert, toast, flash, settings panel (~60 keys)

### Components (pure-admin 2.3.1 / 2.3.2)

- **`split_button/1`** — chevron icon now points up (`fa-chevron-up`) for `top-*` placements, down for `bottom-*`
- **`split_button/1` primary icon** — new `icon` attr for Font Awesome icon on the primary button (e.g. `icon="fas fa-download"`)
- **`split_button/1` item icons** — `:item` slot now accepts `icon` attr (e.g. `icon="fas fa-file"`) rendering as `pa-btn-split__item-icon`
- **`split_button/1` inline action buttons** — `:item` slot accepts `action_icon`, `action_event`, `action_value`, `action_variant` attrs for inline action buttons beside menu items (e.g. delete/remove). The JS hook forwards clicks via `pushEvent` since the menu is moved to `document.body`
- **`button/1` label wrapping** — button text is wrapped in `<span class="pa-btn__label">` when an icon is present, enabling proper centering with `align="center"`
- **`split_button/1` menu structure** — uses `pa-btn-split__menu-inner` wrapper and `pa-btn-split__item-row` BEM elements (replaces inline styles), matching pure-admin 2.3.2 two-container pattern
- **`input_group/1`** — `:button` slot now documented to use `class="pa-input-group__button"` on the button element

### Demo

- **Phoenix / LiveView** sidebar section — new section for framework-specific features (matches Svelte demo's "Svelte" section)
- **CoreComponents Migration** page — migration table showing replaced vs manual functions, setup timeline
- **Flash Messages** page — independent containers demo, variant showcase, markdown + action buttons demo, standard `@flash` compatibility
- **Toasts** page — added progress bar demos (standard + filled), action toast demos (Undo, Retry, Update, Filled + Actions), theme color toasts with filled subsection
- **Buttons** page — split buttons card moved after Responsive Direction to match pure-admin layout 1:1, consolidated into single card with subsections (Sizes, Upward Placement, Custom Icons)

### Theme Cache Invalidation

- **`ThemePlug` auto-refresh** — cached themes are validated against pureadmin.io using `content_sha` from the theme's `checksums` field. On first access after startup, the plug sends a conditional request (`If-None-Match`) to the API in the background. If the server returns 200 (sha mismatch), the theme is re-downloaded without blocking the current request. 304 means the cache is fresh. Freshness checks are throttled to once per 10 minutes per theme
- **`make themes-clear`** — new Makefile target to force-clear the theme cache, triggering fresh downloads on next access

### Documentation

- **Prerequisites** section added to README and getting-started guide (`mix phx.new --no-tailwind`)
- **Main site** link added to README ([pureadmin.io](https://pureadmin.io))
- **Theme installation** guide — actual zip structure from pureadmin.io API, self-contained relative paths. Three installation options: Pure Admin CLI (`@keenmate/pureadmin`), manual download, CI/CD Dockerfile
- **Creating custom themes** — new section in theming docs referencing the CLI's `init`, `build`, `pack`, `publish` workflow
- **Theme customization via SCSS** — variable overrides, custom fonts, baseline correction, complete example
- **`make help`** — all Makefile targets now have `## description` comments

Compatible with `@keenmate/pure-admin-core` v2.3.5.

## v1.0.0-rc.1 (initial)

First release candidate. Consolidates all v0.x development into a stable API.

### Highlights

- **35+ function components** covering the full Pure Admin CSS framework: layout, navigation, forms, tables, data display, modals, toasts, and more
- **13 JS hooks** for interactive features: settings panel, tooltips, popovers, split buttons, sidebar persistence, command palette, character counters, and more
- **Drop-in `CoreComponents` replacement** -- `use PureAdmin.Components` gives you everything
- **Full BEM class support** with `build_classes/3` helper
- **RTL support** for tooltips and popovers
- **Podman/Docker support** for the demo app
- **Live demo** at [elixir.demo.pureadmin.io](https://elixir.demo.pureadmin.io)

### Settings Panel

- **Dynamic theme manifests** — settings panel JS now fetches `/api/themes/manifests` and dynamically populates theme selector (sorted alphabetically), replacing hardcoded `themes` prop
- **Color variants** — new `data-section="color-variant"` section, shown/hidden based on theme manifest. Applies `pa-color-{variant}` CSS class to body. Supports per-variant mode lists
- **Mode per variant** — mode selector updates when color variant changes, auto-hides if only one mode available, auto-applies single mode
- **Font detection from manifest** — "Theme Default" option shows bundled font name (e.g., "Theme Default (Fira Sans Condensed)"), skips Google Fonts download if theme already bundles the font
- **Removed `themes` attr** from `settings_panel/1` — themes are now loaded dynamically, only `default_theme` attr remains

### On-Demand Theme Downloads

- **ThemePlug** — new Plug that serves `/themes/:name.css` with on-demand downloading from pureadmin.io. When a theme CSS is requested that isn't bundled at build time, it downloads from `pureadmin.io/api/themes/:name/download`, extracts CSS and `theme.json` manifest from the zip, and caches to disk
- **`/api/themes/manifests`** — returns all available theme manifests as JSON (from both build-time `priv/static/themes/` and on-demand cache)
- **`/api/themes/:name/manifest`** — returns a single theme's manifest
- **Negative cache** — failed downloads are cached for 10 minutes (ETS-based)
- **Slug validation** — theme names must match `^[a-z0-9-]+$`
- **Pure Erlang** — uses `:httpc` and `:zip` for downloads, no external tools needed in runtime image
- **`?theme=cobalt2`** — query param sets localStorage and swaps CSS link before paint, triggering on-demand download if theme not bundled

### Dockerfile & Deployment

- **Dockerfile** (`demo/Dockerfile`) — multi-stage build with `elixir:1.18-slim` builder and `debian:trixie-slim` runtime. Downloads theme bundles from pureadmin.io at build time (CSS + manifests). Configurable via `THEMES_URL` build arg
- **Makefile** — added `podman-build`, `podman-run`, `podman-stop`, `podman-restart`, `podman-logs`, `podman-clean`, `podman-deploy`, `podman-push` targets matching pure-admin conventions. Registry: `registry.km8.es`
- **.dockerignore** — excludes `_build/`, `deps/`, `node_modules/`, `.git/`
- **`force_ssl`** — now opt-in via `FORCE_SSL=true` build-time env var (was always-on, breaking local container testing)

### New Hooks

- **`PureAdminInfiniteScroll`** — IntersectionObserver-based infinite scroll hook. Fires a LiveView event when a sentinel element scrolls into view. Configurable via `data-event`, `data-has-more`, `data-throttle`, `data-root-margin`. Throttled to prevent rapid-fire triggers.

### Components

- **`list_item/1`** — added `:meta` slot for rich meta content (badges, icons) alongside existing `meta_text` string attr
- **`timeline/1`** — fixed alternating variant to use `<div>` container and correct BEM classes (`pa-timeline__date` + `pa-timeline__icon` instead of `pa-timeline__time` + `pa-timeline__marker`). Added `align` attr (`"start"`, `"end"`) and `is_keep_layout` for alternating layout control — replaces raw `class="pa-timeline--start"` usage
- **`timeline_item/1`** — auto-detects layout from props: block/alternating pattern when `icon_text` or `:icon` is provided, simple pattern otherwise
- **`card/1`** — added `is_bordered` attr for bordered card styling — replaces raw `class="pa-card--bordered"` usage
- **`button_group/1`** — added `responsive` attr (`"sm-vertical"`, `"md-horizontal"`, etc.) for responsive direction changes at breakpoints — replaces raw `class="pa-btn-group--md-vertical"` usage
- **`grid/1`** — added `align="stretch"` to allowed values
- **`column/1`** — added `is_no_padding`, `is_grow`, `is_shrink` props for flex layout control
- **`values:` validation** — added compile-time value validation to all `variant`, `color`, `theme_color`, and `level` attrs across alert, badge, label, composite_badge, button, popconfirm, data_display, form, table, and typography components. Typos like `variant="outine-danger"` or `color="10"` now produce compile warnings
- **pure-admin 2.2.0 support** — updated CSS to v2.2.0. Added `theme_color` attr to `button/1`, `callout/1`, and `toast/1` for theme color slots 1-9. Added `is_filled` attr to `toast/1` for full-color background toasts. Alert `theme_color` now uses proper `pa-alert--color-{N}` / `pa-alert--outline-color-{N}` classes (was `pa-bg-color-{N}`)

### Demo

- **45 demo pages** covering all Pure Admin CSS components (up from 34)
- **Dashboard** — rewritten to match pure-admin reference 1:1: 4 hero stat KPIs, 6 square stats with color variants, revenue trend SVG placeholder, traffic sources table, timeline activity feed, recent orders with badges, top products, system status with badge meta, quick actions
- **Timeline pages** — split into 4 pages matching pure-admin: Simple (color-coded, filled bullets), Block (alternating with layout modifiers: start/end/keep-layout), Feed (avatars, comments, date headers, load more, infinite scroll), Advanced
- **Design pages** — new section: Colors (semantic + theme slots), Theme Variables (45 CSS custom properties), CSS Helpers (visibility, borders, overflow, cursor), Layouts (structure, navbar, sidebar, container width)
- **New pages** — Components Overview (index with 27 component cards), Notifications (interactive list with filtering), Sizing & Layout (width/spacing/display utilities), Virtual Scroll (infinite scroll demo with `PureAdminInfiniteScroll` hook, virtual scroll planned)
- **Raw HTML consolidation** — converted remaining raw `pa-` HTML across 10+ demo pages to use components
- **Root layout** — theme CSS loaded via `<link id="pa-theme-css">` with inline script that reads `?theme=` from URL and swaps href before paint
- **Footer** — updated version to v1.0.0-rc.1

### README

- Added Hex/GitHub/path installation options
- Added full setup guide (CSS, Floating UI, Font Awesome, FOUC, toasts)
- Added Podman build/run/deploy instructions with `make` targets
- Updated component and hook tables

Compatible with `@keenmate/pure-admin-core` v2.2.0.

---

## v0.3.4

### New Components
- **comparison**: New `comparison_table/1`, `comparison_row/1`, `comparison_section/1`, `comparison_value/1` — comparison table components for two-column and three-column data diff patterns (version control, merge conflicts). `:cell` slot with `is_changed`, `is_solid`, `is_conflict` modifiers. `comparison_value/1` includes copy-to-clipboard button with visual feedback (icon swap)

### Layout
- **sidebar_submenu**: Fix accordion behavior — opening one submenu no longer closes all others. Each submenu now uses scoped `toggle_submenu/1` with `{:closest, ".pa-sidebar__item"}` and ID-targeted `<ul>`
- **sidebar_submenu**: Add `id` attr for stable submenu identification, `phx-hook="PureAdminSidebarSubmenu"` for localStorage persistence of open/closed state across navigations
- **sidebar_submenu**: Add FOAC prevention — `fouc_prevention_script` injects `<style>` tag from localStorage before sidebar HTML renders, eliminating flash of collapsed submenus
- **split_button**: Close other open split buttons when opening a new one
- **split_button**: Add `on_click` and `action` attrs to `:item` slot for LiveView event handling via hook `pushEvent`

### JS
- **clipboard**: Global `kpa:clipboard-copy` event listener — copy to clipboard via `JS.dispatch`, with visual icon feedback (clipboard → checkmark → revert). Works anywhere, no per-page setup needed
- **PureAdminSidebarSubmenu** (new hook): Persists sidebar submenu open/closed state to localStorage. Restores state on mount, URL-active submenus always win over localStorage. Uses MutationObserver to detect JS command class changes
- **PureAdminSplitButton**: Fix multiple open menus — opening a split button now closes any other open one

### Demo
- Add Comparison Tables demo (`/tables/comparison`) — two-column, three-column, solid background variants using `table_card`
- Restructure routes to hierarchical paths (`/components/buttons`, `/tables/standard`, etc.) — enables `String.starts_with?` for sidebar submenu `is_open` derivation from URL
- Add global toast service — `<.toast_container>` in app layout, PubSub-based `handle_info` hook in `on_mount` for cross-page toast delivery
- Add Split Buttons demo section to buttons page with primary actions, dropdown items, sizes, and toast feedback
- Background task toasts now use PubSub broadcast instead of direct `send/2`, so toasts arrive on whichever page is active

## v0.3.3

### Components (pure-admin 2.1.0 sync)
- **button**: Add `split_button/1` — split button with primary action + dropdown toggle via `PureAdminSplitButton` JS hook. `:item` slot with `is_danger` modifier, `placement` attr for Floating UI positioning, `on_click` for primary action
- **tooltip**: Add `is_keyword` modifier — dotted underline + help cursor for inline term explanations (`pa-tooltip--keyword`)

### Demo
- Add Table Multi-Select demo with cross-filter selection, summary bar, expandable details, bulk actions
- Update pure-admin CSS to v2.1.0

## v0.3.2

### Components
- **table**: Add `table_card/1` — card wrapper with header/footer, color variants (primary/success/warning/danger), theme colors (1-9), `is_scrollable`, `is_plain`
- **table**: Add `:foot` slot for `<tfoot>` support (colspan, totals rows)
- **table**: Add `align` attr on `:col` slot (start/center/end) for per-column text alignment
- **table**: Add `is_responsive_grid` modifier for CSS Grid responsive collapse
- **table**: Render `:action` column first (leftmost) to match pure-admin reference
- **table_container**: Add `is_panel` mode with `title_text`, `:header`, `:actions` slots
- **table_container/table_card**: Use `<h3>` for titles to match pure-admin CSS selectors (colored header text)
- **pager**: Fix layout to match pure-admin: controls-left | info-center | controls-right (was all-controls then info)
- **pager**: Add `icon_first`, `icon_previous`, `icon_next`, `icon_last` attrs for custom icon sets
- **pager**: Change info format to `/ N pages` (was `Page ... of N`)
- **data_display**: Add `is_value_end` and `is_value_center` modifiers to `banded/1` and `desc_table/1`
- **form**: Add `input_wrapper/1` — wraps input/select with optional clear (×) button (`pa-input-wrapper` + `pa-input-wrapper__clear`), `has_clear`, `on_clear` attrs
- **filter_card**: New `filter_card/1` component — expandable filter card with `:filters`, `:advanced_filters`, `:actions` slots, toggle/clear/refresh/apply buttons, `is_expanded`, `is_loading`, `is_disabled` states, matching Svelte `FilterCard`

### Demo
- Split tables into three pages: Standard Tables, Table Sizing, Responsive
- Rewrite Standard Tables demo to match pure-admin reference 1:1 (same data, sections, structure)
- Rewrite Table Sizing demo to match pure-admin reference 1:1 (same data, card structure with inline code headers, text action buttons with correct sizes per variant)
- Rewrite Responsive Tables demo to match pure-admin reference 1:1 (How It Works grid, basic/product/orders tables with actions and badges, CSS Grid Custom Layouts with data-grid/data-span, HTML Implementation with grid advanced section, SCSS variables reference, Testing Tips, LiveView code examples)
- Add Table Filters demo matching pure-admin reference (basic search filter, expandable filters with advanced section toggle, inline horizontal filters, active filter tags with composite badges)
- Remove invented `pa-page-title`/`pa-page-subtitle` CSS classes from all demo pages — use plain `<p>` like the reference
- Clean up `demo.css` — remove unused chart/activity/status classes

## v0.3.1

### New Components
- `popconfirm/1` — small confirmation dialogs anchored to trigger buttons via Floating UI, with `message`, `placement` (top/bottom/start/end, RTL-aware), `icon_variant` (danger/warning/info), `is_compact`, `confirm_event`/`confirm_value` for LiveView integration, click-outside-to-close, move-to-body positioning

### RTL Support
- `tooltip/1` — position values renamed: `right` → `end`, `left` → `start` (RTL-aware via `document.dir`)
- `popover/1` — placement values renamed: `right` → `end`, `left` → `start` (RTL-aware via `document.dir`)
- Tooltip JS hook — added `resolveLogicalPlacement()` that maps `start`/`end` to physical `left`/`right` based on document direction

### Components
- `badge/1` — replaced `width` attr (`pa-badge--w-Nx` classes) with `max_width` attr using `maxwr-N text-truncate` utility classes
- `modal/1` — fixed popover alignment classes (`pa-popover--center`, `pa-popover--end`) to be copied to content element when moved to `document.body`
- Tooltip JS — fixed theme color variants not applied to floating tooltips (regex only matched first class, which was always `floating`)

### JS
- `modal_dialogs.js` — new ES module wrapping pure-admin's programmatic dialog API (`PureAdmin.confirm/alert/prompt/custom`), exported via `initModalDialogs()`

### Demo
- **Modal Dialogs page** — new page with confirm/alert/prompt demos, position options, sequential dialogs, LiveView server integration, API reference tables
- **Modals page** — rewritten to match pure-admin reference (grouped layout, settings modal, richer modal content)
- **Tooltips page** — updated to use `start`/`end` position naming
- **Badges page** — updated fixed-width section to use `max_width` utility approach, renamed "Left-Side Ellipsis" to "Start-Side Ellipsis"
- **Popconfirm page** — new page with basic popconfirms (delete/archive/reset with icon variants), compact variant, table row delete confirmations with LiveView integration
- **Command Palette page** — new page with Spotlight-style search overlay, Ctrl+K shortcut, context switching (/p, /o, /u, /i), keyboard navigation, pagination, LiveView server-side search
- **Data Display page** — new page with pa-fields layouts: stacked, multi-column grid (cols-2/3/4), field groups, horizontal, table-style bordered, striped, compact, inline, row, relaxed, filled, color-coded borders
- **Data Display 2 page** — new page with advanced patterns: Ant Design descriptions table (1/2/fixed columns), dot leaders (invoice style), property cards, banded rows
- **Data Visualization page** — new page with CSS-only visualizations: progress bars (sizes/colors/striped/animated/rounded), stacked bars with legends, progress rings, dashboard gauges, data bars in tables, activity heatmaps, sparkline bars
- **Detail Panel page** — new page with inline split-view and overlay modes, table row selection, field-group detail content
- Fixed all compile warnings across demo (nested `if` parentheses, `dynamic_tag` name deprecation, undefined attributes, missing slots)

## v0.3.0

Compatible with `@keenmate/pure-admin-core` v2.0.2.

### Components
- `section/1` — added `title_text` attr that renders an `<h3 class="pa-section-title">` heading
- `code/1` — fixed to render plain `<code>` without `pa-code` class, matching Svelte reference
- `card/1` — added `:subtitle` slot (rich HTML counterpart to `subtitle_text`)
- `card/1` — fixed `subtitle_text` to render with `pa-text pa-text--secondary` class matching Svelte reference
- `card/1` — fixed title rendering: plain `<h3>` without wrapper div when no icon is present, matching Svelte reference
- `card/1` — fixed non-inline tabs to render outside header as sibling (matching reference DOM structure)
- `card/1` — fixed inline tabs to render after title (not before), matching reference order
- `form_label/1` — added `is_required` attr that renders asterisk indicator
- `checkbox/1` — added `:label_content` slot, `is_indeterminate` (via PureAdminCheckbox hook), `is_x_mark`
- `checkbox_box/1` — added `is_indeterminate` support
- `tabs/1` — scrollable overflow now renders proper scroll buttons and scroll container
- `tab_item/1` — added deterministic `id` and `:not()` exclusion to prevent 2px flash on tab switch
- `switch_tab/3` — scoped tab/panel switching via `tabs_id` + content container id to prevent cross-group interference
- `label/1` — fixed outline to use `pa-label--outline` class (not `pa-label--outline-{variant}`), matching Svelte reference; added `xs`/`xl` size support
- `badge_group/1` — added `limit`, `total`, `is_expanded`, `on_toggle`, `more_text`, `collapse_text` attrs for expand/collapse with two modes: server-side (fires LiveView event for lazy loading) and client-side (CSS-based hiding with JS class toggle, survives LiveView DOM patching)
- `badge/1` — added `width` attr (`pa-badge--w-{size}` BEM class) and `is_ellipsis_start` for left-side truncation
- `composite_badge/1` — added `is_interactive`, `on_label_click`, `on_button_click`, `label_variant`, `button_variant`, `button_text`, `:icon_content` slot for full interactive support with separate label/button click events
- `checkbox_box/1` — new low-level checkbox (input + box) for tables and composite components
- `checkbox_list/1` — new container with variant (compact/bordered/striped) and layout (inline/grid/2col/3col)
- `checkbox_list_item/1` — new item with label, description, state (disabled/locked), and `:actions` slot
- `basic_list/1` — new component for styled `<ul>` with spacing, icon, bordered, striped, inline, unstyled variants
- `ordered_list/1` — new component for styled `<ol>` with numeric, roman, alpha styles
- `definition_list/1` — new component for styled `<dl>` with standard and inline layouts

### JS Hooks
- `PureAdminCharCounter` — new hook for textarea/input character counting with configurable max, translatable message templates via `data-msg`/`data-msg-over` with `{count}`/`{max}` placeholders
- `PureAdminCheckbox` — new hook for syncing `indeterminate` property from `data-indeterminate` attribute (required for tri-state checkboxes)

### Components (enhanced)
- `tooltip/1` — new CSS-only tooltip wrapper with position, variant, multiline, help cursor support
- `popover/1` — new click-triggered popover with title, placement, size, alignment, custom trigger slot
- `pager/1` — enhanced with page input, first/last buttons, configurable events, custom info text, `:controls` and `:info` slots
- `load_more/1` — enhanced with `phx-click` support via `:global` attrs
- `loader_center/1` — new centered loader container (flexbox centering)
- `loader_overlay/1` — enhanced to accept custom content via slot (not just default spinner)
- `toast/1` — added `title_text`, `message_text`, `is_visible`, `on_close`, `:icon` slot, close button with SVG icon
- `toast_container/1` — updated positions to use logical names (top-end, top-center, top-start, bottom-end, bottom-center, bottom-start)

### Demo
- **Cards page** — complete rewrite matching Svelte pure-admin reference (14 sections: same-height, basic, header three-part layout, colored, theme colors, bordered, ghost, underlined headers, statistics, statistics with trends, interactive, advanced features, data display, CSS classes reference)
- **Grid page** — complete rewrite matching Svelte pure-admin reference (overview, basic usage, percentage columns, fraction columns, responsive grid, offsets, row alignment, no gutter, visibility utilities, nested grids, quick reference, code examples)
- **Buttons page** — complete rewrite matching Svelte pure-admin reference (variants, sizes, outline, states, block, button groups with gap sizes, vertical alignment, responsive direction, text truncation, icon buttons, icon-only, fixed width, text alignment, ripple effects, loading states, usage guide, CSS classes reference)
- **Inputs page** — new page matching Svelte pure-admin reference (text inputs with states/sizes/validation/theme colors, input groups with prepend/append/buttons/toggle mode, input types, select dropdowns, textareas, checkboxes & radios with sizes, width variations, CSS classes reference)
- **Validations page** — new page matching Svelte pure-admin reference (10 validation patterns: inline field errors, summary block, combined summary+inline, border+icon only, right-side indicators, helper text transforms, toast notifications, validation timing strategies, multi-field/cross-field, progressive multi-step, CSS classes reference)
- **Tabs page** — complete rewrite matching Svelte pure-admin reference (card header tabs, standalone, icons, fixed width, pills, vertical, boxed, sizes, badges, centered, full width, border-top, icon-only horizontal/vertical, standalone page-level, standalone vertical, bordered horizontal/vertical, long titles with wrap/collapse/scrollable, inline tabs in header)
- **Validations page** — interactive demos: char counter with JS hook, validation timing strategies (real-time/blur/submit), cross-field validation (password match, date range)
- **Badges page** — complete rewrite matching Svelte pure-admin reference (badge sizes reference table, basic badges, pill badges, badges with icons, label sizes reference, labels with outline, badge groups with expand/collapse, fixed-width badges with tooltips, left-side ellipsis, composite badges with mixed colors and interactive click handlers, usage examples)
- **Lists page** — complete rewrite matching Svelte pure-admin reference (basic unordered lists with spacing variants, ordered lists with numeric/roman/alpha, definition lists with standard/inline, icon lists with success/danger/info/warning, bordered/striped, inline/unstyled, complex lists with avatars, implementation guide)
- **Checkbox Lists page** — new page matching Svelte reference (tri-state checkboxes, select-all pattern, disabled states, checkbox lists with descriptions, item states, list variants, task list with actions, inline/grid/multi-column layouts, interactive table with row selection and select-all)
- **Alerts page** — rewrite matching Svelte reference (basic alerts with strong labels, text icon alerts, dismissible alerts, rich content with heading/list/actions, outline alerts, compact alerts in responsive grid)
- **Callouts page** — rewrite matching Svelte reference (basic callouts, headings, icons, lists, sizes, code, links, grid layout, callout vs alert comparison)
- **Loaders page** — rewrite matching Svelte reference (spinner sizes/colors, inline spinners, centered loaders, loaders with text, card loading states, loader types, button loading states)
- **Pagers page** — new page (basic pager, first/last buttons, alignment variants, custom info text, load more with loading state, pager in card footer, CSS classes reference)
- **Tooltips page** — new page matching Svelte reference (positions, color variants, theme colors, button tooltips, icon-only tooltips, multiline, inline text, popovers with sizes/alignment/positions)
- **Toasts page** — new page matching Svelte reference (6 position demos, 5 variant buttons, persistent toasts, action toasts, multiple stacking toasts, long-running background task demo with server push, architecture docs)
- **Sidebar** — reorganized to match Svelte pure-admin layout (Components submenu with Grid, separate Tables and Timeline submenus, Forms as top-level item)

## v0.2.0

### Navbar subcomponents
- `navbar_brand/1` — brand/logo section with optional `logo` image
- `navbar_nav/1` — navigation link group (`position="start"` or `"end"`)
- `navbar_nav_item/1` — nav link with optional `has_dropdown` and `:dropdown` slot
- `navbar_dropdown/1` — CSS-driven dropdown menu (supports `is_level2` for nesting)
- `navbar_title/1` — page title in center section
- `navbar_search/1` — search widget with keyboard shortcut hint
- `navbar_profile_btn/1` — profile button with name and `:icon` slot

### Notifications
- `notifications/1` — bell button with badge count and dropdown panel
- `notification_item/1` — individual notification with variant, title, text, time slots

### Profile panel (enhanced)
- `profile_panel/1` — full slide-out panel with overlay, avatar, name/email/role, `:nav`, `:tabs`, `:footer_` slots
- `profile_nav_item/1` — navigation item within profile panel
- `toggle_profile_panel/1`, `close_profile_panel/1` — JS commands
- `PureAdminProfilePanel` JS hook — tab switching, favorites, click-outside-to-close

### Settings panel
- `settings_panel/1` — floating settings panel (theme mode, layout width, sidebar, fonts, etc.)
- `fouc_prevention_script/0` — inline script preventing flash of unstyled content
- `PureAdminSettings` JS hook — client-side localStorage-based settings management

### Layout
- Added `id` attr to `layout/1`
- `toggle_notifications/1` JS command

### Demo
- Navbar uses three-section layout (start/center/end) matching pure-admin reference
- Components dropdown with nested "More ›" submenu
- Notifications bell with sample items
- Profile panel with tabs (Profile/Favorites), nav items, and footer actions

## v0.1.0

- Initial release
- Phase 1: Foundation + 10 key components (button, badge, alert, card, table, modal, tabs, form, layout, grid)
- BEM class builder helpers
- JS hook scaffold
- `use PureAdmin.Components` for bulk import
