**Minor release.** A wave of new components for data-dense SaaS apps, plus the earlier editor / layout additions and a broad set of enrichments to existing components — all additive and backward-compatible. The SaaS set: [`<x-wirekit::data-table>`](https://docs.wirekit.app/components/data-table) (client-mode sort / search / select / column-manager + a server contract), [`<x-wirekit::filter-builder>`](https://docs.wirekit.app/components/filter-builder) (active-filter chips + a typed add/edit popover), [`<x-wirekit::status-matrix>`](https://docs.wirekit.app/components/status-matrix) (a 2D grid of tristate / toggle / status / heat cells), [`<x-wirekit::notification-center>`](https://docs.wirekit.app/components/notification-center) (a bell + grouped, realtime-capable panel), [`<x-wirekit::usage-meter>`](https://docs.wirekit.app/components/usage-meter) (usage-vs-limit meters + a plan-paywall gate), [`<x-wirekit::event-calendar>`](https://docs.wirekit.app/components/event-calendar) (month / week / agenda scheduling views), and [`<x-wirekit::map>`](https://docs.wirekit.app/components/map) (a MapLibre / Leaflet adapter with an accessible marker list). Also new: [`<x-wirekit::editor>`](https://docs.wirekit.app/components/editor), a Tiptap rich-text adapter; [`<x-wirekit::sticky-panel>`](https://docs.wirekit.app/components/sticky-panel), a sticky companion column; and [`<x-wirekit::collapsible>`](https://docs.wirekit.app/components/collapsible), a standalone WAI-ARIA disclosure. [`<x-wirekit::color-picker>`](https://docs.wirekit.app/components/color-picker) gains a from-scratch `popover` HSV picker. Existing components grow: checkbox / radio `size` + `variant="card"`; a `success` (valid) state and `field.set` / `field.legend` grouping for form controls; per-tab icons / badges and vertical tabs; select `<optgroup>` groups; dropdown checkbox / radio items with shortcut hints; slider step marks + value tooltip; a vertical carousel; a frozen table column; textarea `rows="auto"`; an image-compare `ratio` prop; and an empty-state `variant`.

### Security

- **The sandbox preview renderer no longer compiles developer-supplied prop or slot values as Blade.** `Pushery\WireKit\Sandbox\SandboxRenderer` assembled its preview markup by interpolating prop and slot values into a Blade string that was then compiled — so a value containing Blade echo or directive syntax (`{{ … }}`, `@…`) was evaluated server-side rather than rendered as text. Values are now bound as runtime data and referenced through Blade expressions, so they never reach the Blade compiler as source. HTML-escaping of string values is unchanged. If you embed the sandbox renderer behind any untrusted input, update to this release.
- **The sandbox audit log neutralizes control characters in the component-name field.** A component name carrying a tab or newline could forge an extra tab-delimited log record; control characters are now collapsed before the line is written.
- **The ApexCharts adapter's config merge skips prototype-polluting keys.** As defense-in-depth, the deep-merge that layers your chart config over WireKit's themed defaults now drops `__proto__` / `constructor` / `prototype` keys, so a config assembled from app-influenced JSON can't tamper with object prototypes.

### Added

- **New [`<x-wirekit::data-table>`](https://docs.wirekit.app/components/data-table) — a sortable, searchable, selectable data table.** The ergonomic client-mode wrapper handles the common case entirely in the browser: pass a `rows` array and a `columns` definition and it sorts (string + numeric, with `aria-sort` headers), searches (a filter box), selects rows (per-row checkboxes plus a tri-state select-all and a bulk-action bar), toggles column visibility (a column manager), and switches density. Cell types: `text`, `number`, and `badge` (a status word maps to a tinted intent pill). For large datasets the `server` flag emits `sort-change` / `search-change` / `selection-change` events so you re-query from Livewire (a documented `WithDataTable` trait is the contract). The table sits in a labeled, keyboard-reachable scroll region; slots cover `toolbar`, `bulkActions`, and `rowActions`.
- **New [`<x-wirekit::filter-builder>`](https://docs.wirekit.app/components/filter-builder) — an active-filter chip bar.** Active filters render as removable chips; an "Add filter" popover walks the user from a field to an operator valid for that field's type (no `contains` on a number) to a typed value editor (`text` / `number` / `select` / `date` / `bool`). It emits the normalized `[{field, op, value}]` array as a `filter-change` event and as JSON on a hidden input, so `wire:model` and plain forms both bind; an optional `searchable` box emits its own `search-change`. Chips are keyboard-removable and the popover is a labeled `role="dialog"` with focus management.
- **New [`<x-wirekit::status-matrix>`](https://docs.wirekit.app/components/status-matrix) — a 2D grid of typed status cells.** One engine, four cell types via `cell-type`: `tristate` (inherit → allow → deny, editable — a role-permission matrix), `toggle` (on/off — a preferences grid), `status` (an intent badge — a compliance/controls grid), and `heat` (a color-scaled value — a retention heatmap). Sticky row and column headers; editable cells emit the normalized cell map (`cell-change` + JSON bridge) and diff against a baseline. Every encoding is conveyed in text or shape, never color alone — tristate cells use distinct check / cross / dash glyphs and carry the state word in their `aria-label`, heat cells always print their value. The heat ramp is themeable via `heatFrom` → `heatTo` (a cold→hot color interpolation), and each heat value rides in a contrast-checked chip so it stays readable at any point on the ramp.
- **New [`<x-wirekit::notification-center>`](https://docs.wirekit.app/components/notification-center) — a bell with a grouped notification panel.** An unread badge opens a `role="dialog"` panel of grouped, actionable notifications with an optional single-select type filter (a radiogroup with arrow-key selection), an empty state, a "see all" footer, and optimistic realtime insertion (an optional window-event bridge for Laravel Echo). Activating a row marks it read and emits `notification-action` with `{ id, href }` — items with `href` render as real links, and `actionLabel` adds a call-to-action line. Read changes emit `notification-read` / `notification-read-all` and mirror the unread count to a hidden input. The bell's accessible name carries the unread count in words, so the state never depends on the badge color; an `open` prop supports inline embeds.
- **New [`<x-wirekit::usage-meter>`](https://docs.wirekit.app/components/usage-meter) — usage-vs-limit meters for billing surfaces.** Shows how much of a plan limit has been consumed with a labeled bar (composing [`<x-wirekit::progress>`](https://docs.wirekit.app/components/progress)) and a `used / limit (%)` readout, shifting intent as usage approaches the limit. The over-limit and approaching-limit states are spelled out in words, never color alone. Ships `<x-wirekit::usage-meter.panel>` (a responsive grid of meters) and `<x-wirekit::usage-meter.gate>` (a plan-paywall wrapper that dims and inert-disables a gated action with a reason and an upgrade prompt). `:limit="null"` renders an unlimited tier; thresholds are config-overridable.
- **New [`<x-wirekit::event-calendar>`](https://docs.wirekit.app/components/event-calendar) — a scheduling calendar with month / week / agenda views.** The month view is a day grid with event pills and a "+N more" overflow; the week view is an hour-row time grid with overlap-split event blocks and a current-time line; the agenda view is a chronological list grouped by day. Navigation and the view switcher recompute the visible window, and clicking an event emits `event-click`. A `dayMarkers` prop tags individual days as holiday, working, or blocked — blocked days carry a diagonal-hatch overlay (the `wk-day-blocked` class) and spell out the unavailable state in their accessible name, never relying on color alone. Built on CSS grid with no external dependency, token-themed, and distinct from [`<x-wirekit::calendar>`](https://docs.wirekit.app/components/calendar) (the date-picker widget). Every event is a focusable button with a full date/time label.
- **New [`<x-wirekit::map>`](https://docs.wirekit.app/components/map) — a map adapter (MapLibre / Leaflet peer dependency).** WireKit ships the themed chrome, the declarative API, the Alpine glue, and — mandatory for accessibility — an always-present marker list; the heavy map engine and the tiles are your app's concern. With no library loaded it degrades gracefully to the marker list plus a placeholder. It diffs markers reactively for Echo-pushed live positions and emits `marker-click` on selection. Clicking a marker — on the map pin OR in the list — selects it: the map pans and the matching list entry is highlighted, so the two stay in sync. A marker's `intent` colors both its list dot and its map pin (the pin via MapLibre's marker color or a themed Leaflet pin). Each pin opens a hover bubble whose content follows the marker's data shape — a text-only label, a styled name + detail card (`body`), a photo card (`image`), or a photo-only bubble (`tooltip: 'image'`) — and the selected list row highlights via `highlight="ring|fill"` in a choosable `highlight-color`. Both engines render on-canvas zoom controls, and an `attribution` prop passes tile attribution (e.g. `© OpenStreetMap contributors`) through to the map's attribution control — required by some tile providers' usage policies. The marker list is the screen-reader path — each entry a focusable button labeled with the name and coordinates, so the data never depends on the map being visible.
- **`variant="card"` on [`<x-wirekit::checkbox>`](https://docs.wirekit.app/components/checkbox) and [`<x-wirekit::radio>`](https://docs.wirekit.app/components/radio) — the selectable-card pattern.** Turns the whole control into a bordered, fully-clickable card that highlights (accent border + tinted surface) when checked — ideal for feature toggles and pricing-tier pickers. It reacts to its own input via CSS `:has()`, so no JavaScript is required. `variant="default"` (the inline control + label) is unchanged.
- **`size` prop on [`<x-wirekit::checkbox>`](https://docs.wirekit.app/components/checkbox) and [`<x-wirekit::radio>`](https://docs.wirekit.app/components/radio).** Both controls now scale via `sm` / `md` (default) / `lg`, matching the `size` already on [`<x-wirekit::toggle>`](https://docs.wirekit.app/components/toggle) — so a form mixing checkboxes, radios, and switches stays visually consistent at any size. The box (or circle) and its checkmark / dot scale together. Defaults to `md`, so existing checkboxes and radios are unchanged.
- **`success` (valid) state on [`<x-wirekit::input>`](https://docs.wirekit.app/components/input), [`<x-wirekit::select>`](https://docs.wirekit.app/components/select), and [`<x-wirekit::textarea>`](https://docs.wirekit.app/components/textarea).** The mirror of the existing error state: pass a string to render a green border plus a confirmation message below the field (e.g. `success="Username available"`), or `:success="true"` for just the green border. The field stays valid — it never sets `aria-invalid`; the message is linked via `aria-describedby` instead. `error` always wins when both are set. Backed by a new `--color-wk-border-success` design token (aligned with `--color-wk-success-text`, exactly as `--color-wk-border-error` aligns with `--color-wk-danger-text`), so the border and confirmation text read as a coherent pair in both light and dark mode.
- **`rows="auto"` on [`<x-wirekit::textarea>`](https://docs.wirekit.app/components/textarea) grows the field with its content.** No JavaScript — it uses the CSS `field-sizing: content` feature (supported across the WireKit browser baseline). The numeric `rows` still serves as the minimum height. Numeric `rows` behaves exactly as before.
- **`ratio` prop on [`<x-wirekit::image-compare>`](https://docs.wirekit.app/components/image-compare) sizes the component itself.** Image-compare layers two absolutely-positioned images, so it needs a box with explicit dimensions. Previously you wrapped it in an element with an `aspect-ratio` (or a fixed height); now pass `ratio="16/9"` (or `"4/3"`, `"1/1"`, …) directly and the component applies the aspect-ratio to itself — no wrapper needed. **Additive and backward-compatible:** omit `ratio` and the component keeps filling the height of whatever box you give it, so an existing `<div style="aspect-ratio: …"><x-wirekit::image-compare … /></div>` continues to work unchanged.
- **`variant` on [`<x-wirekit::empty-state>`](https://docs.wirekit.app/components/empty-state).** `default` (no container chrome — unchanged), `outline` (a dashed-bordered placeholder that reads as a drop zone), or `muted` (a filled muted-surface card). Lets an empty state stand on its own as a card without wrapping it in a `<x-wirekit::card>`.
- **`animation` prop on [`<x-wirekit::skeleton>`](https://docs.wirekit.app/components/skeleton) — `shimmer` / `pulse` / `none`.** `shimmer` (default) is the gradient sweep; `pulse` is the lighter opacity fade (half the GPU layers); `none` is a static placeholder with no motion. The legacy `:shimmer="false"` still works (equivalent to `animation="pulse"`). All three respect `prefers-reduced-motion: reduce`.
- **[`<x-wirekit::dropdown>`](https://docs.wirekit.app/components/dropdown) gains checkbox / radio menu items and a `shortcut` hint.** `<x-wirekit::dropdown.checkbox-item>` is a self-toggling `menuitemcheckbox` (Alpine owns the checked state, seeded from `:checked`); `<x-wirekit::dropdown.radio-item>` is a `menuitemradio` whose selection is coordinated by a shared Alpine variable named via `model`. Any item (including these) accepts a `shortcut` prop that pins a keyboard-shortcut hint (e.g. `⌘K`) to the inline-end. All follow the WAI-ARIA Menu pattern.
- **[`<x-wirekit::table>`](https://docs.wirekit.app/components/table) gains a `stickyColumn` prop.** Freezes the leftmost column while the rest of the table scrolls horizontally — ideal for wide tables where the first column is the row's identity. The frozen column keeps a solid background, and pairs with `stickyHeader` (the top-left corner stays on top). (The footer/totals row was already available via `<x-wirekit::table.foot>`.)
- **[`<x-wirekit::carousel>`](https://docs.wirekit.app/components/carousel) gains `orientation="vertical"`.** Scrolls slides up/down instead of left/right — the previous/next buttons move to the top and bottom edges, the indicators stack on the side, and the carousel pins a default viewport height (override with a height class or `style`) so each slide fills it. Horizontal stays the default and is unchanged.
- **[`<x-wirekit::slider>`](https://docs.wirekit.app/components/slider) gains step `marks` and a value `tooltip`.** `marks` draws tick marks along the track — a list of positions (`[0, 25, 50, 75, 100]`) or a position-to-label map (`[0 => 'Min', 100 => 'Max']`). `tooltip` floats a value bubble above the thumb that follows it as the user drags. The slider reserves its own vertical space for both (no hand-padding around the field, no clipped bubble inside `overflow: hidden` ancestors, no labels overlapping the content below), and it carries the same min-width usability floor as `<x-wirekit::range-slider>` so it stays draggable in shrink-to-fit contexts (flex/grid auto items, table cells). Both features are off by default, so existing sliders are unchanged.
- **[`<x-wirekit::color-picker>`](https://docs.wirekit.app/components/color-picker) gains a `popover` mode — a full custom HSV picker.** Add `popover` for a design-system-consistent panel: a saturation/value plane, hue + alpha sliders, a format-aware text field (cycle HEX → RGB → HSL → OKLCH — `oklch` is WireKit's own token color space), a screen eyedropper (where supported), copy-to-clipboard with an inline copied-checkmark confirmation, your `presets` swatches, and a recent-colors history (localStorage). It's built from scratch — no third-party library, ~6 KB of glue. The plane and sliders are keyboard-operable (`role="slider"` + arrow keys). On touch devices you can keep the platform's own color sheet: opt-in `native-on-mobile` opens the OS dialog on touch-primary pointers (`pointer: coarse`) while desktop pointers keep the popover. The default (native `<input type="color">`) is unchanged byte-for-byte, and the form-binding contract is identical, so flipping `popover` on never breaks submission.
- **New [`<x-wirekit::editor>`](https://docs.wirekit.app/components/editor) — a rich-text editor (Tiptap peer-dependency adapter).** Drops into a Livewire form like `<x-wirekit::textarea>`: a Blade tag, a hidden form field, `wire:model` support, theme-token styling, full keyboard access. It adapts Tiptap (built on ProseMirror), which stays a **peer dependency** you install from npm — WireKit ships only the lightweight Alpine glue (and gracefully falls back to a `<textarea>` if Tiptap isn't loaded). Props cover `toolbar` presets (`basic` / `full` / `custom`), `format` (`html` / `json`), `editable`, `placeholder`, `maxLength` (renders a live, soft character counter in the bottom bar with a debounced screen-reader announcement), `size`, `maxHeight` (cap the editable area so a long document scrolls inside the field instead of growing the page), and the usual `label` / `hint` / `error`. The undo / redo toolbar buttons disable themselves when there is nothing to undo or redo. Ships with `<x-wirekit::editor.toolbar>` (a `role="toolbar"` of `aria-pressed` command buttons) and token-driven document typography (`wk-editor-content`). The content region is a `role="textbox"` with `aria-multiline`. Sanitize saved HTML on store (the docs page calls this out).
- **New [`<x-wirekit::sticky-panel>`](https://docs.wirekit.app/components/sticky-panel) — a sticky companion-column layout primitive.** Pins a panel beside an article while the page scrolls (a cart summary, filter sidebar, table of contents, comparison builder) using native CSS `position: sticky` for the pinning itself. It has optional non-scrolling `header` and `footer` slots with a scrollable body between them, and it's bounded to the viewport height (`100dvh`, so the footer never scrolls out of reach). The body is a keyboard-reachable scroll region (WCAG 2.1.1) with `overscroll-behavior: contain`, and shows top/bottom scroll shadows when it overflows (opt out with `:scrollShadow="false"`) — they render as overlays *above* the content, so a hovered row at a scroll edge never covers them, and auto-hide at the scroll extremes. A pure-CSS `wk-scroll-shadow` utility class plus the overlay classes (`wk-scroll-shadow-top` / `wk-scroll-shadow-bottom`) are exposed for your own containers. Below the `hideBelow` breakpoint it un-sticks and flows inline (or set `mobileBehavior="hide"`).
- **New [`<x-wirekit::collapsible>`](https://docs.wirekit.app/components/collapsible) — a standalone single disclosure.** One trigger toggles one collapsible region — the bare WAI-ARIA Disclosure pattern, no card chrome or group coordination. The trigger is a real `<button>` (keyboard-operable), wired with `aria-expanded` + `aria-controls`; the region animates open/closed via Alpine's `x-collapse`. Pass `trigger="…"` for a plain label or a `trigger` slot for rich content, and `:open="true"` to start expanded. Reach for it for a "read more" or an advanced-options panel; use [`<x-wirekit::accordion>`](https://docs.wirekit.app/components/accordion) when you have a coordinated *group* of panels.
- **[`<x-wirekit::select>`](https://docs.wirekit.app/components/select) options now support `<optgroup>` groups and per-option `disabled`.** The `:options` array accepts three shapes you can mix freely: flat (`['de' => 'Germany']`), grouped (`['Europe' => ['de' => 'Germany']]` → an `<optgroup label="Europe">`), and per-option attributes (`['de' => ['label' => 'Germany', 'disabled' => true]]`). Flat options render exactly as before.
- **[`<x-wirekit::field>`](https://docs.wirekit.app/components/field) gains `orientation="horizontal"`, plus new `<x-wirekit::field.set>` / `<x-wirekit::field.legend>` for grouped fields.** `orientation="horizontal"` places the label in a column beside the control (the classic settings-form layout) with the error/hint still aligned under the control. `<x-wirekit::field.set legend="…" hint="…">` renders a native `<fieldset>` + `<legend>` — the WCAG-recommended way to group a radio or checkbox set so the legend is announced before each control; use `<x-wirekit::field.legend>` in the slot for rich legend content. Defaults are unchanged (vertical).
- **[`<x-wirekit::tabs>`](https://docs.wirekit.app/components/tabs) gains per-tab `icon` + `badge` and a `vertical` orientation.** Using the array-of-objects items shape, each tab can carry an `icon` (a leading glyph) and a `badge` (a trailing count/status chip): `['key' => 'inbox', 'label' => 'Inbox', 'icon' => 'inbox', 'badge' => 8]`. `orientation="vertical"` stacks the tabs in a column beside their panels (settings screens, side nav) with the correct WAI-ARIA keyboard model — Up/Down arrows for a vertical tablist, Left/Right for horizontal. The tab bar was already horizontally scrollable when labels overflow. All additive; existing tabs are unchanged.
- **`wirekit:install` now stops with a clear message when the project is still on Tailwind CSS v3.** WireKit's styles are built on the Tailwind v4 engine (`@theme`, `@source`, `color-mix()`, `@property`) and cannot run on v3. The Tailwind version lives in npm — out of reach of Composer's `require` constraints — so the install command is the first place WireKit can check it: a v3 project now aborts before anything is written and points you at `npm install tailwindcss@latest @tailwindcss/vite@latest` plus the Tailwind v4 upgrade guide, instead of failing later with a confusing wall of errors. `wirekit:doctor` reports the same guidance as a failed check. Detection only acts on positive v3 evidence, so a valid v4 (or an undetermined) setup is never blocked.
- **New `--text-wk-2xs` design token — the smallest text size in the scale.** A micro/caption step below `--text-wk-xs`, for dense UI like table sub-labels, chip counts, and timestamps. Available to your own CSS like every other `--text-wk-*` token and documented in the theming reference.
- **[`<x-wirekit::replay-button>`](https://docs.wirekit.app/components/replay-button) is now publicly documented.** The component was always callable in the package; its documentation page, README listing, the Public CSS API catalog row, and the machine-readable component manifests now all surface it. The button re-mounts the closest `[data-replay-target]` ancestor from a saved snapshot — it powers the `↻ Replay` control on docs.wirekit.app's live previews, and it's a ready-made building block for re-running an animation, resetting an interactive demo, or restoring a dismissed badge or alert in your own app.

### Changed

- **`dist/wirekit.js`, `dist/wirekit-alpine.js`, and `dist/wirekit.esm.js` grew to absorb the new editor adapter, the from-scratch color picker, and the SaaS data components (data-table, filter-builder, status-matrix, notification-center, event-calendar, map).** Raw sizes: `wirekit.js` ~148 KB (~43 KB gzip), `wirekit-alpine.js` ~194 KB (~59 KB gzip), `wirekit.esm.js` ~148 KB (~43 KB gzip). The core chart-only bundle (`wirekit.core.js`, ~11 KB) is unchanged.

### Fixed

- **[`<x-wirekit::charts-apex>`](https://docs.wirekit.app/components/charts-apex) tooltip now HTML-escapes the series color before rendering.** The unified tooltip renderer escaped the x-axis label, series name, and data value but interpolated the resolved series color into the marker's inline `style="background: …"` unescaped. An app that wired chart colors from unvalidated user input could break out of the attribute — now the color goes through the same escape helper as its sibling values, so only a valid CSS color reaches the DOM.
- **[`<x-wirekit::range-slider>`](https://docs.wirekit.app/components/range-slider) value bubbles now track, clamp, and merge cleanly.** Each thumb carries a live value bubble above its handle that follows the handle as you drag. At the ends the bubbles close flush with their handles instead of overhanging the track; when the two thumbs sit close enough that the bubbles would collide they collapse into a single combined "min – max" bubble centered between the handles — which now lines up at exactly the same height as the individual bubbles. All of it updates live during a drag.
- **[`<x-wirekit::slider tooltip>`](https://docs.wirekit.app/components/slider) value bubble now sits over the thumb when the slider is given a width.** Passing an explicit width could push the floating value bubble far to the side of the handle; the width is now applied so the bubble, the track, and the tick `marks` all share it, keeping the bubble aligned with the thumb (and the marks aligned with the track) at any size.
- **[`<x-wirekit::range-slider>`](https://docs.wirekit.app/components/range-slider) and [`<x-wirekit::image-compare>`](https://docs.wirekit.app/components/image-compare) no longer force a layout read on every drag frame.** Both read the track's bounding rectangle once at the start of a drag instead of re-measuring it on each `pointermove` — the track doesn't move while dragging, so the per-frame measurement was wasted work that could cause jank on long tracks or low-end devices. Dragging is now smoother; the click-to-position path still measures fresh.
- **`wirekit:doctor` no longer false-warns that no font CSS was found when fonts are correctly published.** The font-assets check scanned only the top level of `public/vendor/wirekit/fonts/`, but `vendor:publish --tag=wirekit-fonts` writes font CSS one level deeper (`fonts/<category>/<name>/<name>.css`) — so a correct publish was reported as "Font directory exists but no CSS files found". The check now scans recursively. On a fresh install using the default font (where the system-ui stack means no font CSS is published), the empty directory is reported as informational rather than a warning.