Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Wirekit Laravel Package

pushery/wirekit

WireKit is a free, open-source UI component library for Laravel Livewire. Build dashboards and marketing pages with Tailwind CSS v4 and Alpine.js—themeable, accessible, dark-mode aware components for forms, navigation, overlays, layouts, and more.

View on GitHub
Deep Wiki
Context7
v2.26.1

Patch — the day under the pointer.

Fixed

  • calendar shades the day the pointer is actually on. While only the start of a range is set, the day under the pointer stands in for the end — and it was the one day in the preview with no surface: the days on either side of it shaded, and the one being chosen blank until it was clicked. It is shaded rather than filled, because a filled day would claim a choice nobody has made yet. A screen reader is told about it too; the two channels had failed apart.

Documentation

  • inline-edit says what happens to the draft when the reader clicks away. The page described where focus lands and stopped there, so the more consequential half was left to be discovered: leaving the control without confirming also discards what was typed. That is the same outcome as Escape, reached a different way, and it is the price of the explicit default — nothing is written until the reader confirms, so a click into the next field cannot save half a thought either. A round trip is not a departure, and the difference is now stated where it is relevant rather than only in the morph section.

  • grid warns that template is not responsive. min promises a column never overflows its container, and the page said so; template makes the opposite promise — an explicit track list is handed to CSS as written, so a fixed track is that wide at every viewport — and the page said nothing. The example beside it reserves 10rem + 8rem before the content pane gets anything, which on a phone leaves the middle column a few characters wide and pushes the grid past the screen edge. It renders; it is simply unreadable, and nothing warns you. Now the page does, with the pattern to use below the width such a layout needs.

v2.26.0

Minor — two column layouts the grid could not express, a switch the tooltip never had, a reorder that was announced and never built, and German. Nothing here changes what an unchanged call site renders. The fixes share a shape worth naming: each was a component that was right in the render a test looks at and wrong a moment later — a control the server had changed, a code the browser rejected only with scripting off, a field that moved its neighbors the instant validation fired.

Added

  • grid can count columns by the container, and set them by hand. min="14rem" is the ordinary responsive card grid: as many columns as fit, at least that wide, one on a narrow screen. It is not cols with breakpoints, and the difference is the point — cols measures the viewport, so the two look identical until the container is narrower than the window (a sidebar opens, a split view, an embedded preview) and the breakpoint version keeps counting columns that no longer fit. template="14rem 1fr 18rem" takes an explicit track list, for the layouts cols cannot express at all: it only knows equal columns, and the two commonest application shells there are — the three-pane workspace and the week grid — are neither equal nor a count. Both accept any CSS length or track syntax; template wins over min, which wins over cols.

  • German ships with the package. Set your application's locale to de and every string WireKit renders on your behalf follows — no publishing step, no configuration. Until now the package shipped the English key reference and nothing else, so each project translated the same strings again without being able to see the others' work. Publish the catalog if you want to adjust a phrase to your own voice; your copy wins per key. See Localization.

  • tooltip can be switched off. disabled silences it without removing it, for a control that has become inert. pointer-events: none on the wrapper is not a way to do this, however much it looks like one: mouseenter reaches every ancestor of the element the pointer actually hit, so the panel still opens over something that is supposed to be dead. Bind data-wk-tooltip-disabled instead of passing the prop when the answer changes while the page is open.

  • kanban reorders, by pointer and by keyboard. sortable previously produced attributes for you to wire a library to. It now moves cards on its own and dispatches one wirekit:sortable:reordered event carrying the whole new sequence. The keyboard path is part of it rather than an addition — Space lifts, the arrows move, Space drops, Escape puts the card back — because a list that can only be reordered by dragging cannot be reordered at all without a pointer.

  • sidebar lets you own the scrolling middle. scroll-shadows="false" removes the fading edge affordance and keeps everything else. The two used to arrive together, so being rid of the shadows meant dropping header and footer — and with them the pinned head and foot, which is an unrelated capability.

  • input, select and textarea can hold their height. reserve-message keeps the message line's height whether or not there is a message. In a row of fields, an appearing error otherwise grows the field and every neighbor re-anchors — and no value of align-items fixes it, because none of them anchors siblings to an element two levels down. Off by default: in a stacked form the reserved line is an empty row under every field.

Fixed

  • A value the server changed now reaches segmented-control and rating. Livewire patches the DOM in place, and the component read its starting value once — so a selection changed on the server left the control showing the old one while the form submitted the new one. Only visible on a round trip your user did not initiate, which is why it survived: clicking the control yourself hides it completely.

  • otp-input accepts the case it says it accepts. A single-case alphabet folds case, and the promise lived only in the script — the browser's own validation still held an uppercase-only pattern. With scripting unavailable, a code typed in the case the label shows was rejected on submit, citing a format nobody had mentioned.

  • A refused optimistic action inside a menu is now visible. Clicking a menu item closes the menu, and the refusal was rendered into the closed panel: announced to a screen reader, and to nobody looking at the page. It is now surfaced when — and only when — its own control has no visible box.

Changed

  • An expected refusal no longer has to reach your error tracker. The optimistic guide asks for a thrown exception, correctly — a validation rejection is a successful response and confirms the value instead of restoring it. It now also says what throwing costs, and how to keep a refusal your application makes on purpose out of your incident feed. Measured on our own documentation site: one reader on one page produced 46 reports from a demo that refuses by design.
v2.25.0

Minor — a date range, a vocabulary the icon system was missing, and a long run of fixes for behavior that looked right and was not. Nothing here changes what an unchanged call site renders. The additions are opt-in; the fixes are the larger half, and most of them share a shape worth naming — a component doing its job at the moment anyone would look, and stopping afterwards. Several were invisible by construction rather than by oversight: a refusal that rendered exactly like a success, a control that changed on screen and never reached the server, an editable surface with no name for a screen reader.

Added

  • calendar takes a date range. Add range and it selects in two clicks — the first sets the start, the second the end, and a second click before the start becomes the start instead, so there is no rule about which end to pick first. While only the start is set, the day under the pointer stands in for the end, so the shading shows what you are about to choose rather than nothing until you have chosen it. value reads and writes YYYY-MM-DD/YYYY-MM-DD, the same spelling date-picker uses, and the form receives name[start] and name[end] beside the combined field — so a handler written for a single date keeps working. Both ends and every day between them are announced, in the single-month grid and side-by-side months alike.

  • .wk-only-below-md — shown only below the md breakpoint, and the counterpart to .wk-hide-below-md. A dense desktop affordance often wants a different shape on a phone rather than a smaller one, and swapping the two takes both halves.

  • Nine semantic icon aliases, in every base preset: bell, bell-slash, tag, send, archive, reply, forward, image and message. Notification, labeling, mail and media — concepts every administrative interface has, and none of them had a word.

    The gap did not present as a gap, which is why it lasted. A page that needed one of these reached for the icon package's own glyph name — envelope, paper-airplane, magnifying-glass — and it worked. It worked because the optional icon package happened to be installed, which is a dependency on the icon set rather than a contract with this library. On an install without it, the same markup throws.

    bell and bell-slash move out of the heroicons-app extension into the base set. Same glyph, so nothing rendered changes; what changes is that they now resolve without stacking an extension.

  • The sidebar's scrolling list shows where it continues. A long navigation column scrolls, and a scrollbar alone is easy to miss on a track that fades when idle. The list now carries the same edge shadows sticky-panel uses: at the top there is a shadow below the fold and none above it, and at the bottom the reverse. Sentinel-driven rather than masked, so the shadow appears exactly when there is somewhere left to scroll — a mask dims the edge whether or not anything follows, which leaves the last item greyed out once you have reached it.

  • charts.apex_license is in the published config. The doctor told you to record your ApexCharts tier "in config/wirekit.php" and the file shipped without that key, so the instruction pointed at nothing. The charts block also never listed apexcharts among its adapters, though the component and the script bundle both offer it.

Fixed

  • <x-wirekit::textarea rows="auto"> silently did nothing on a browser at the supported floor. Auto-growing uses CSS field-sizing: content, which is newer than WireKit's baseline (Chrome 123 / Safari 17.4 against 111 / 16.4) — so on a baseline browser the prop named the behavior and the behavior was absent, with nothing saying so. The component's own comment described the property as being inside the baseline. It now applies wk-autosize, a real rule inside [@supports](https://github.com/supports) (field-sizing: content), and the class is documented in the public CSS API as progressive enhancement: where the property is unsupported the textarea keeps its rows minimum and stays scrollable and resizable. No API change — rows="auto" is unchanged.

  • An open inline editor could close itself across a Livewire re-render, discarding the draft. The component announces on window when it opens so that other editors stand down, and it recognized the sender by comparing against its own root element — captured once when the component initialized. A re-render that replaces that element while keeping the component alive left the comparison pointing at a detached node, so the editor treated its own announcement as another editor's and closed. Nothing was logged; the field simply reverted to its previous value. The root is now resolved when the event arrives.

  • The editor's editable surface had no accessible name when it carried a visible label. A <label for> names labelable elements; the surface a ProseMirror engine builds is a div with contenteditable and role="textbox", which is not one, so the reference never reached it — and because the component suppresses its own aria-label whenever a visible label is present, the field ended up with neither. A screen reader announced an unnamed text box. The label is now wired by reference (aria-labelledby), and aria-label remains the fallback for an editor without a visible label. No API change; nothing to update in your code.

  • Muted text on a callout or alert was below AA contrast in dark mode. These components tint their background with 15% of a state color, and in dark mode the default accent is near-white — so the tint moves the surface toward light text rather than away from it. A secondary line written as <x-wirekit::text variant="muted"> inside one measured 4.45:1, just under the 4.5:1 WCAG AA requirement for normal text, while the same token measured 7.63:1 on the page background it was set against. --color-wk-text-muted in dark mode moves from oklch(70.8% 0 0) to oklch(74% 0 0): 5.00:1 on the worst tinted surface, and every other dark pairing improves with it. Muted text on a tinted callout surface is now part of the guaranteed set documented in Theming. text-subtle and text-placeholder are unchanged — they are guaranteed on the page and input backgrounds, and the guide now says so explicitly.

    If you override --color-wk-text-muted for dark mode, check it against a callout as well as against the page background; the two surfaces are further apart than they look.

  • An optimistic control inside a panel that opens over the page never reached the server. The value changed on screen, no request was sent, no error was raised, and the control stayed in its provisional state indefinitely — announcing that it was saving, forever. It affected any optimistic control whose panel is rendered outside the component that owns it: a popover color picker, a menu, a combobox list. Only the keyboard path reached it, because a drag ends on a handler bound to the document while the layer's own element is still in place — so a pointer never reproduced it and a keyboard hit it every time.

  • select ignored its value. A <select> has no value content attribute, and the prop was not declared — so value="pro" landed on the element, HTML ignored it, and the browser fell back to the first option. Every standalone select showed one choice while everything reading the component believed another. The prop is declared now, the matching option carries selected, and a placeholder pre-selects itself only when nothing else is chosen.

  • carousel slide dots were an 8px tap target 6px apart, failing both WCAG 2.5.8 AA's size floor and its spacing exception. The dots are still 8px — an indicator the size of a button is not an indicator — and they now sit 24px center to center, which is exactly the distance at which the criterion's notional circles stop intersecting. The spacing exception therefore applies on its own merits. A 44px hit region was tried first and was worse than the problem: at the old pitch the expanders overlapped, so a tap aimed at one dot landed on its neighbor.

  • <x-wirekit::tooltip> no longer runs off the edge of a phone. It was the only overlay in the library without cross-axis shifting, so a placement="right" tooltip on a narrow screen went past the right edge and stayed there — measured at 184px off-screen in a 375px viewport. Floating UI's default shift only moves along the placement's main axis, which for left/right is vertical, and flipping gives up when both sides overflow. Six sibling overlays already passed this flag.

  • An anchored panel no longer opens one panel-width away from what opened it. Every overlay that positions itself against a trigger — menus, popovers, comboboxes, the data table's column manager — could be placed as though it had no size, because it was measured before the browser had laid it out. A placement that aligns a panel's far edge subtracts the panel's own width, and subtracting zero puts the panel's near edge exactly where its far edge belongs. On a 375px screen the column manager opened at 290px and ran to 482px, a hundred pixels past the edge, with a trigger that ended at 290.

    Whether it happened at all depended on the browser: the same markup placed correctly in Chrome and Edge and incorrectly in Safari, on iOS and on the desktop, because the two differ in how much layout is guaranteed by the moment a panel becomes visible. The positioner now waits for the panel to have a box before it measures one, so the placement is the same everywhere.

  • A refused optimistic action no longer shows the reader the server's error page. The layer already did the right thing — it took the value back and announced the refusal — and then Livewire painted its own error page over the top, because nothing told it the failure had been handled. On a phone that is a full-screen stack trace covering the very rollback the component just performed.

    A component that opts into optimistic owns its failure path by definition, so it now says so: Livewire's default display is suppressed for the request it handled, and the refusal goes to the browser console instead, where a developer looks and a reader does not. Handling a failure and hiding it are different acts; only the second would be a defect. Nothing changes for a component that has not opted in.

  • <x-wirekit::hover-card> can be dismissed with Escape. The handler was bound to the panel, and the panel is teleported while the card is opened by pointing at or focusing the trigger — so the key went one way and the listener sat the other. WCAG 1.4.13 requires content shown on hover or focus to be dismissible without moving the pointer or the focus, and Escape is the mechanism it names.

  • The mobile drawer in <x-wirekit::app-shell> fills the shell instead of floating on it. Below the breakpoint the sidebar slides in over the page, but it kept the card treatment it wears in a padded column — as tall as its content, with rounded corners. Measured with the drawer open at 393px: the shell was 256px tall and the panel a reader actually sees was 85px of it, reading as a card dropped on the content rather than as a drawer. It now fills its column and meets the edge flush. Above the breakpoint the card is untouched.

  • <x-wirekit::app-shell>'s mobile drawer is anchored to the shell rather than to the page. The drawer and its backdrop were position: fixed, which measures against the viewport only while no ancestor is a containing block — contain, transform, filter and perspective all make one, and any of them can appear above an embedded shell. When one did, the opened drawer measured itself against that ancestor instead: it started 64px above the shell and took that ancestor's height. The shell now owns the positioning context, so the two coincide in a full-page app and in an embedded one alike.

  • A closed off-canvas drawer is now gone rather than merely moved. <x-wirekit::app-shell> parked its mobile sidebar at -translate-x-full, which puts it off-screen only while something clips it — and a position: fixed element is clipped by the viewport only while no ancestor is its containing block. contain: layout makes an ancestor exactly that, as do transform, filter and perspective, and then -100% means 256px to the left of that box: the whole menu paints beside the content. The drawer now also carries visibility: hidden when closed, so nothing depends on a host clipping it.

    The same change fixes a defect that was never about painting: a translated drawer keeps its place in the tab order and in the accessibility tree, so keyboard and screen-reader users could reach a menu that nobody can see. visibility transitions alongside transform, so the closing animation still plays.

  • <x-wirekit::tooltip> colors its panel before showing it, not after. Setting open displays the panel immediately; the themeable variables were copied onto it only after the tick that followed. In between, the panel is displayed and still carries the default color. Whether a browser paints inside that window was never established — but an observer can read a shown panel without its color, which is enough to make the contract untestable without a race.

  • Optimistic controls now settle instead of staying provisional. A control marked optimistic painted its dashed pending outline when you left it and then waited forever: no confirmation, no rollback, and a refusal announced nothing. It reached the server through Alpine's $wire, which in some render contexts hands back a stand-in attached to no component — it answers every method name, every call returns nothing, and no request is made. The component that owns the control is resolved from the page now, so the answer comes back and the state resolves.

    The check beside it went too: typeof $wire[method] === 'function' is true for a method that does not exist, because that object answers for any name. It could never fail, so it protected nothing. What is checked instead is whether a component was found at all.

  • <x-wirekit::sticky-panel>'s width is a ceiling, and only where the panel is stuck. It was applied as a fixed width capped at max-width: 100%, which caps the case that does not arise — the container being narrower. Below the breakpoint the panel un-sticks into ordinary flow, and there the container is wider: a 15rem summary sat in a 335px column with the space beside it doing nothing. The panel fills its column now and stops growing at width, which is what the prop always described. Above the breakpoint nothing changes.

  • Inline edit no longer discards what you typed when something else on the page updates. The editor closed and dropped its draft on every blur, without asking where the focus had gone. A Livewire update anywhere on the page patches the DOM around a focused input, and the browser fires a blur while it does — focus returns immediately, nobody went anywhere, and the text was gone. No message, no undo, on a round trip that had nothing to do with the field. It now waits a tick and checks: if focus came back to the same editor, the draft stays.

  • Tooltip color overrides work again. Moving the panel to the end of the document so a masking ancestor could not clip it also ended its descent from the trigger — and the documented way to restyle one tooltip is an inline --color-wk-tooltip-bg / --color-wk-tooltip-text on the component, which the panel read by inheritance. The panel kept rendering, in theme defaults, and every override silently stopped applying. The values are now copied onto the panel each time it opens, read from the trigger, so an inline style, a class or a scoped theme all resolve the same way they used to.

  • A chart that updates keeps its keyboard fix. ApexCharts re-stamps tabindex on every SVG it rebuilds. The correction ran at moments — after a render, after a theme swap — which is a list somebody has to remember to extend. A page streaming data rebuilds its SVG every tick, so the fix ran once, correctly, and the next update put the tab stops straight back: a focus stop inside a subtree assistive technology has been told to ignore. It is now driven by what the chart does rather than by a list of moments, and it covers the legend entries too, which have the same defect one level down.

  • Two scroll animations ignored prefers-reduced-motion. An explicit behavior argument overrides the CSS rule that covers everything else, so scroll-to-top and the tour stepper animated for a reader who had asked them not to. Both go through the shared helper now. The CSS half of reduced-motion is blanket and covers a component added tomorrow; the JavaScript half is a list, and these two were not on it.

  • A sidebar header or footer lines up with its own items. The zones carried no inline padding while sidebar.item and sidebar.group apply their own, so a brand row rendered flush against the column's edge and everything below it looked inset. Applied on the zone rather than asked of the caller: the alignment is a property of the column, and supplying a brand row should not require knowing which token the items happen to use.

  • wirekit:doctor no longer promises a silence it cannot deliver. The ApexCharts license reminder offered three values to record — community, commercial, oem — and matched two of them. Anyone on the community tier followed the instruction, saw the identical message with the identical advice, and had every reason to conclude the command is imprecise. The reminder itself stays, deliberately: the revenue threshold is a continuing condition rather than an install step, so a project crosses it without any file changing. What changed is the advice. A declared community tier is now confirmed back to you with the reason it still speaks, an unrecognized value is named rather than treated as unset, and the unset case keeps its full explanation without the promise.

  • wirekit:export-blocks emitted a source_url that could not resolve. It pointed at a branch and a directory that the public repository does not have — neither the branch nor the directory exists there. Each block now links to its page's raw Markdown on the documentation site, which is the sibling of the preview_url beside it and follows the same visibility rules.

Documentation

  • sidebar shows its event addressing in a live example. The wirekit:sidebar:toggle / :toggled pair — how a trigger outside the sidebar reaches it, and how { id } picks one of several — was documented in a code block and never rendered anywhere. The page now carries a running two-sidebar demo with one id each, so you can see a toggle address one and leave the other alone.

  • Three refusal demos now say what a refusal does. The optimistic examples on calendar, reaction and toggle-button showed the behavior without stating it, so a reader had to infer the contract from watching.

  • Two of them stated the opposite of what the component does. number-input and color-picker promised that a refused value comes back. Both are deliberately built to KEEP it — deleting what somebody typed because a save failed is worse than the failed save — and the pages say so now. A reader trusting the old wording would have filed a bug against correct behavior.

  • The fab example stacked two floating buttons in one corner. The component pins itself to a screen corner, so the wrapper each demo sat in did nothing and both landed on the right, one under the other. The first now declares position="start", which also shows the prop.

  • footer link columns sit further apart. Stacked links at the tightest gap put their centers 23.4px apart, half a pixel under the threshold where a column of small targets stops being comfortable to tap.

  • Documentation previews used icon names that no preset registers. Eight pages taught glyph names from the icon package rather than this library's own aliases, which meant the snippet worked on a machine with that package installed and threw on one without. Every one now uses the alias a developer is meant to learn — search rather than the magnifier's glyph name, inbox rather than envelope, edit rather than pencil.

  • CONTRIBUTING.md named tooling that is not in the package. Its setup and pre-commit blocks listed commands whose test suite, build scripts, linter configuration and package.json are all stripped from the distributed tree — so for every reader who has that file, the commands resolved to nothing. It points at the documentation site instead.

  • The icon page lists every semantic alias, and a test keeps it that way. It documented 39 of them while the preview block directly above rendered all 71, and said so — the tables were described as a selection rather than the full list, on the reasoning that a hand-maintained table goes stale. The reasoning is sound and the page still contradicted itself on one screen: anyone counting it concluded the vocabulary was half its size.

    All of them are listed now, in four added groups — content and chrome, people and records, notifications and mail, media controls. Stale counts elsewhere on the page are gone rather than corrected, and the shipped config no longer understates the vocabulary. php artisan wirekit:icons remains the answer to a different question: what your install resolves, extensions included, which no table can know.

  • Three optimistic UI pages were missing a demo the overview promised. The overview says each page lets you accept, refuse and watch a slow answer; the color picker had no slow answer and the calendar and editor showed only the refusal. A sentence true of most pages reads as true of all of them, so somebody following it and finding no rollback concludes there is none.


v2.24.0

Minor — the full-bleed sidebar column, two smaller additions, and five fixes. Every addition defaults to what the library already did, so an unchanged call site renders exactly as before; opting in is what changes shape, and it changes it on purpose. The fixes are the quieter half — each one makes something that already looked fine actually be fine.

Added

  • The sidebar can be a full-bleed column, and the application shell can seat it against its own edge. The most common administrative layout — sidebar running the full height, topbar beginning beside it rather than above — could not be expressed, and the reason was the same in five separate places: the thing that needed changing existed only as a literal inside a template, where no call site could reach it.

    Five props, all defaulting to today's behavior:

    • app-shell sidebar-insetfalse meets the shell's top and inline-start edge with nothing between them.
    • app-shell header-placement="content" — the topbar renders inside the content column, so the sidebar runs the full height.
    • sidebar variant="flush" — no radius, no surrounding border, surface from the host, one logical inline-end edge. Two variants rather than a set of booleans, because the in-between states are the broken-looking ones: a rounded panel without a border reads as a rendering fault, and a full-bleed column with a radius shows a sliver of page at each corner.
    • sidebar header / footer slots — the three-zone column, and only when one is supplied, so no existing call site changes.
    • sidebar toggle="start|end|none" — where the collapse control sits, or that there is none.

    toggle="none" needed more than leaving the button out. Alpine merges scope downwards only, so a trigger in your topbar is not inside the sidebar's tree and can never call its toggle(). The state stays with the sidebar and the outside reaches it through a wirekit:sidebar:toggle window event — with no id it addresses every sidebar, with an id only that one. The sidebar answers with wirekit:sidebar:toggled on init as well as on change, because a button outside it has to render aria-expanded from its first paint and cannot read the sidebar's state to do so.

  • The machine-readable manifests advertise the newest RELEASED version. components.json, api-map.json and blocks.json each carry a released_version field — a bare version string, so a tool that serves or mirrors this package can compare what it is showing against what the package claims.

    It is a separate field from version, deliberately. version answers "which build is installed here", which on a branch pin is a branch pseudo-version rather than a release; a check built on it would compare a branch name to a release number and stay green forever. Two fields that both read as "the version" would be the hazard, so the new one says which question it answers.

  • Slider tick marks can carry a meaning, not just a label. A third marks shape takes ['label' => …, 'description' => …] per position, for sliders whose steps stand for something the label cannot say — five positions from −2 to +2, each a policy.

    Without it the alternative is worse than it sounds: a reader who wants to know what a position means has to move the slider to find out, changing the very thing they were still deciding about.

    The description reaches a reader two ways, because either alone leaves a gap. It becomes the tick's title, which is a pointer affordance — and there is no hover on touch, so that alone would hide the meaning from the readers most likely to be guessing at it. So it is also what the slider announces: moving through the positions reads out the meaning of the one you are on instead of the bare number, one at a time rather than every mark at once. The label is what you see; the description is what the position means.

    Both older shapes render exactly as before — no title, no description — so this is opt-in per mark.

  • Five icon aliases for concepts every administrative interface has: users, history, legal, badge and layers. Available on all four presets, so an alias resolves whichever one is configured.

    These were missing, and the gap did not present as a gap: an application reaching for "user management" or "audit log" found no word for it and used the icon package's own glyph name instead. That works — and only because the full package happens to be installed, which is a dependency on the icon set rather than a contract with this library. One application discovered that when it tried to ship only the icons it renders: the restriction removed the coincidence and four of its test files went red.

    users is a separate word from user on purpose. Managing accounts is a different menu item from your own profile, and the two sharing one glyph was forced rather than chosen. It also moves out of the marketing extension into the base set — same glyph, so nothing rendered changes.

  • reading-toc works inside your own scroll region. Its jump moved the page, which does nothing in a shell that scrolls an inner container instead — a fixed header with the content scrolling beneath it. Clicking a heading simply did not move, and nothing said why: the page had nowhere to go, and not going there raises no error. It now finds the container that actually scrolls, with no prop to set.

  • A collapsed sidebar group can still say something is waiting. sidebar.collapsible now takes a trailing slot on its trigger. A group is collapsed to keep the list short, and the counters on the items inside went with it — with persist, permanently: collapse once and the numbers are never seen again without going to look. Alpine's open is in scope, so x-show="! open" shows it only while the group is closed, which is usually what you want. A slot rather than a badge prop on purpose: a count is one answer and a silent dot is another, and a total across several queues asserts an urgency the number cannot know.

  • Scroll shadows on the inline axis, for a bar that scrolls sideways. wk-scroll-shadow-start and wk-scroll-shadow-end are the horizontal counterparts to the existing top/bottom pair — a tab strip, a chip row or a toolbar can now show the same "there is more this way" cue. They share the block-axis pair's two variables, so a theme tunes both axes in one place, and they are named for the writing direction rather than for left and right, so a right-to-left interface gets the cue on the side its content continues toward. <x-wirekit::sticky-panel> drives them from the same observer, which never cared which axis it was watching. This is not what fade does: that mask is static and dims the edge even where nothing follows, which leaves the last item of a strip looking disabled.

Fixed

  • wirekit:verify no longer reports a completeness it never checked. Its config-drift check compared section and component names — two of the 186 options the config actually has — and then said your published file "covers every option this version offers". A file published one minor earlier could be missing options inside sections it already had, and the command confirmed it was current. The comparison now covers every option, grouped by the section that owns them so the output stays readable. Expect a longer warning the first time you run it if you have not re-published in a while: the missing options were always there, only unmentioned. They still resolve at runtime — nothing behaves differently — but your file now shows you what you can set.

  • WireKit::defaults() now changes what renders. The documented way to set component defaults from PHP stored its values and nothing read them: the call succeeded, and every component rendered as though it had never been made. The repair is not a new resolution path — components already read their defaults from wirekit.components.* config, so there were two mechanisms for one job and only one was connected. The call feeds that config, which wires it for every component at once and keeps a single order of precedence. An explicit attribute at the call site still wins, as a default should.

  • A tooltip is no longer cut off by whatever it happens to sit inside. Its panel now renders on the <body> rather than beside its trigger. The case that surfaced it was a tooltip inside a scroll area with fade, where roughly 18 px of the panel was missing: the panel is fixed-positioned and so escapes overflow clipping, but fade works with a mask, and a mask applies to everything an element renders — fixed descendants included. Leaving the subtree settles that case and every relative of it at once: a clipping card, a transformed ancestor, an isolated stacking context.

  • wk-touch-target no longer moves an element that positions itself, and scroll-to-top finally gets the 44 px target. The class guarantees the enlarged tap area a positioned ancestor, and did so by setting position: relative unconditionally. WireKit's stylesheet is unlayered while Tailwind's position utilities are not, and an unlayered declaration wins over a layered one whatever the specificity or import order — so a fixed element became relative, its right/bottom turned into flow offsets, and it moved. Scroll-to-top is fixed by design, so applying the class pushed it off the left edge of the screen; and because the rule is inside [@media](https://github.com/media) (pointer: coarse), this happened only on the devices the class exists for. A host that already declares its position now keeps it, and scroll-to-top carries the class itself — it had been left at a 40 px target while sibling controls got 44.

  • [@wirekitScripts](https://github.com/wirekitScripts) can load the ApexCharts adapter, so a chart no longer goes missing without saying why. The adapter is a separate bundle — an app that draws no charts should ship no chart code — but leaving it out was punishing: the page rendered, the console said wirekitApexChart is not defined, and only the chart was absent. Nothing failed at build time and nothing appeared server-side, so the developer had to work out that a second file existed and hand-write a tag against a route path documented nowhere they were looking. Set scripts.apex to true in config/wirekit.php and both tags are emitted, in the order the adapter needs, with the same cache-busting and CSP nonce as the main bundle. It stays off by default, because that is the point of the split.

  • wire:model on file-upload now reaches the file input, so Livewire uploads work. The binding was applied to the component's wrapping element, and Livewire decides what a model binding means by reading the element's type: on a file input it takes the upload path, on anything else it binds a plain value. A wrapper has no type, so the upload path was never entered — and the failure was quiet in the worst way, because the component's own list showed the chosen file's name and size. The field looked filled while the server had nothing, and submitting failed with "the field is required". Modifiers (wire:model.live, .blur) travel with the binding; wire:key and wire:ignore stay on the wrapper, where they describe what they are meant to describe.

  • An ApexCharts chart no longer puts keyboard focus somewhere screen readers are told to ignore. The rendered chart carries the library's own tab stops — on the SVG itself and on each legend entry — while WireKit mounts it inside an aria-hidden container, because the chart's accessible name and role="img" belong to the element around it. Tabbing therefore landed inside a region assistive technology skips: the focus ring moved, nothing was announced, and there was nothing to operate once you arrived (the toolbar and zoom are off by default). Those tab stops are now removed — but only while the container is hidden, so a chart you deliberately expose keeps its focusable SVG. The tab stops are removed at the source rather than deflected by a handler that pushed focus back out as it arrived — one listener less per chart, and the behavior no longer depends on that handler running in time.

  • A sortable table header's click target was its label, not its cell. With sortAction, the label and sort indicator now sit in a full-width button that fills the header cell, so the whole cell is the target rather than the few characters of text — it was under the 24 px minimum that WCAG 2.5.8 asks for, on a control people click repeatedly while scanning a table.

    Two details you may notice. The cell's padding moved onto the button rather than being added to it: an inline-flex child grows the line box, so keeping both made the row 52 px where 36 was expected. And the focus ring is inset, because a ring drawn outside a button that fills its cell is clipped by the cell.

  • A filled one-time code can be corrected by typing from the left. Every cell had to be clicked and cleared individually first, which is what a row of single-character inputs does by default: maxlength="1" is already satisfied by the character in the cell, so with the caret after it the browser refuses the keystroke — nothing happens, and no advance to the next cell happens either. Focusing a cell now selects its character, so typing replaces it and moves on exactly as if the row were empty. Clicking and tabbing both, and pasting over a filled row is unchanged.

  • An icon alias pointed at an icon that does not exist. server resolved to a Phosphor glyph the package has never shipped, and unknown icon names throw rather than degrade — so a page using it broke. It now points at Phosphor's rack metaphor.

    Worth naming the reason it lasted, because it was structural rather than careless: three of the four icon packages were optional dependencies, so no test could resolve a single one of their aliases, and the defect landed in one of those three. Those packages are now development dependencies and every alias of every resolvable preset is checked against the files actually shipped. The fourth cannot be installed alongside the Laravel versions this package supports, so it is reported as unverifiable rather than skipped quietly.

  • Alert dialog documents the order focus is resolved in, and the two ways focusReturnTo can make things worse. The prop existed and worked; what was missing is the part a reader needs before setting it.

    It beats a surviving trigger rather than acting as a fallback for the case where the trigger disappears — so setting it on a dialog whose trigger usually survives pulls focus away from where the reader was, which is worse than the default. And the target must outlive the action: the selector resolves at close time, so pointing it inside the row you just deleted falls through silently, with no error, to the state the prop exists to avoid.

  • Using a raw icon name is documented as the trade it is. An icon name the vocabulary does not define still resolves when the package provides one — that has always worked, was never written down, and is the reason applications reached for it without knowing what it costs.

    What it costs: your application is coupled to the icon package rather than to this library. Restrict that package to the icons you actually render and the raw names stop resolving; one application had to revert exactly that change because four of its test files depended on them. The page now says when reaching for a raw name is legitimate, when it is not, and that a fallthrough is logged in development.

  • Progress's label id changed on every render. The id linking the visible label to the bar was regenerated each time, which only matters where this component is most used: inside a polling region, every poll produced a new id, so the accessible name was re-resolved on a control whose whole purpose is being watched while it changes.

    It is invisible in any single render — the label and the bar always agreed with each other, just on a different value every second. The id now derives from the one you pass, and the page says to pass one when the bar sits in a polling region.

Documentation

  • Every component that supports optimistic UI now has a demo you can run. The capability was documented on each component's page and shown running on none of them, which is a poor trade: the interesting part is what the screen does in the second before the server answers, and prose cannot show that. Each page now has a Try it section with one control per outcome — accepted, refused, and a slow answer that holds the provisional state on screen long enough to read.

  • The optimistic-UI page no longer states how many components support it. The list directly beneath the sentence already answers that, and a number in prose goes stale on the next release.

v2.23.0

Minor release — accessibility promises the library documented and did not keep. Every item here corrects something that was already wrong; the three props and two helpers exist to make two of those corrections configurable, and it is those additions that make this a minor rather than a patch.

Three things do change what an unchanged call site renders, so budget a look at them rather than treating the upgrade as visually inert: an inline-edit textarea now opens at the height of its content instead of a fixed three rows; an inline-edit with width="full" reserves one action slot in read mode, so the value's box is narrower by that much; and every control already using optimistic gains the pending outline described below. Nothing else moves, and no prop changes meaning.

Fixed

  • A pending optimistic value now looks pending. While an optimistic control waits for the server it drew nothing: the state was announced to a screen reader through aria-busy and nothing in the stylesheet painted it, so a sighted user saw a finished change where the contract says the change has to read as withdrawable. In-flight controls now carry a dashed outline that breathes slowly.

    Dashed rather than solid, because a solid outline in the accent color is what focus looks like — a pending control drawn that way would say the wrong thing. An outline rather than a ring, so nothing reflows when a value goes in flight. And never a dim: dimming degrades the text exactly when the reader most wants to read the value. Under prefers-reduced-motion: reduce the outline stays and only the breathing stops.

    This reaches every component that supports optimistic and every hand-mounted wirekitOptimistic(...), with no change needed at your call site.

  • inline-edit no longer abandons an unanswered save in silence. When your handler never reports the outcome — a missing paired event, a dropped request — the editor stopped waiting and said nothing at all. It now reports the save as not confirmed, and deliberately not as failed: a missing acknowledgement is not evidence the write did not happen, and calling it a failure invites a second edit over a value that is already stored. Your text stays in the field either way.

  • inline-edit gives the editor the box the value had. With width="full" the field came out narrower than the text it replaced by exactly one button's width, because edit mode has two trailing controls where read mode has one. Subtle on a single-line input, obvious on a textarea. Both boxes are now one width.

  • inline-edit's textarea opens at the height of the text it replaces. It opened at three rows regardless, so a value that read as four wrapped lines became a box you had to scroll to see your own text in.

  • The Quick Start in README.md taught a prop button does not accept. The Delete example used variant="danger", and button takes intent + surface — so the attribute fell through to the markup unread and the button rendered in the accent color: a destructive action styled as the primary call to action. WireKit::defaults()'s docblock had the same mistake twice, including a variant for input, which has neither a variant nor a surface.

    variant is not retired in general — it is a real prop on alert, card, text and ten others. It is retired on button and badge only, so do not sweep variant= across a codebase; and variant="outline" on a button was a surface, not a renamed intent.

Added

  • saveTimeout and unknownMessage on inline-edit — how long to wait for your saved / failed answer before giving up, and what the live region says when that bound is reached. Both were read by the component and never passed, which is why the report above was silent.

  • rows on inline-edit, defaulting to auto: the textarea sizes to its content and grows while you type. A row count pins a fixed height, which is the previous behavior.

  • Pushery\WireKit\Support\StrictnessGate::unknownPropNames() — the attribute names on a component that are neither declared props nor legitimate passthrough, as a return value rather than a log line. Useful if you want to assert in your own test suite that your templates only pass props the component declares; the existing warning path now calls it, so your check and the runtime's cannot disagree.

  • Pushery\WireKit\Support\BladeParser::extractWireKitComponentUsagesFromSource() — every <x-wirekit::…> usage in a string with the attribute names it carries. It walks each tag rather than matching it, so a > inside x-show="count > 3" does not truncate the tag and a class list does not read as attribute names.

Documentation

  • The inline-edit previews look broken and are not, and the page now says so. Edit a title, confirm, and the value snaps back — because nothing behind a documentation preview answers the confirmation, and the component refuses to show a value as saved on its own authority. That is the whole contract in one interaction, and it now reads that way instead of like a fault.

  • A section on what happens when nothing answers a confirmation, with both new props.

  • The textarea demo contradicted its own copy. Its value claimed to wrap onto more than one line and fitted on one, in the single demo meant to tell a textarea from a text input.

  • Optimistic UI now lists the components that have it. Twenty-three components ship the optimistic path and the page named none of them, so the one page you read to decide whether to use the feature could not tell you whether your component supported it. The page also has a preview of what a pending control looks like, and a shorter title.

  • Every variant= example in the theming and component pages was already correct; only the README and the docblock had fallen behind.

v2.22.0

Minor release — three themes in one version: an optimistic-UI layer, a Content-Security-Policy build, and a new component with five capabilities an application could not reach before. Nothing existing changes shape: every prop added here has a default that renders exactly what the previous version rendered, and both new script bundles are ones nobody loads by accident.

Read in one sentence each: a component can show the result of an action before the server confirms it and put it back if the answer is no; the whole catalog now works under a policy that forbids unsafe-eval, which took moving every inline expression into a registered factory; and a displayed value can become an editor in place.

Added

  • Optimistic UI — a component can show the result of an action before the server has confirmed it, and put it back if the answer is no. Add optimistic="yourMethod" to a supported component and load wirekit-optimistic.js alongside whichever bundle you already use. Supported today on toggle, checkbox, rating, reaction, select, segmented control, combobox, multi-select, calendar, date picker, time picker, input, textarea, password input, slider, range slider, number input, tags input, one-time code, toggle button, editor, color picker and the checkbox item inside a dropdown.

    Every one of them had to earn it. A component is not declared safe — it becomes reachable once its own rendered accessibility test is green, and some still do not make it: a date picker in range mode is excluded because an undo of two values has no single right answer. Components that are not covered say so, on the component, with the specific obstacle.

    A field you type into does not undo. Putting the old value back costs a toggle or a select nothing — it is simply the other choice. In a text field the old value belongs to the server and the new one is your work, so restoring it would delete what you wrote because a save failed. Those fields keep the value instead, mark it unsaved, and say both: that it did not save, and that it is still there. Nothing is read back to you, which matters most in a password field.

    A control with two ways in takes the safer exit for both. A number field has steppers and a box you can type in; a tag field has a list and a box you can type in. Stepping or removing is a discrete choice that could safely spring back — but a value typed while such a request is still out would be overwritten when it does, so the safety would depend on whether you happened to be typing. Neither half undoes: the change stays and says it was not saved.

    A slider commits when the gesture ends, not per frame — at the end of a drag, or immediately on a keypress, since one press is already a finished decision. There is no settle delay anywhere in this: a timer would make the same gesture behave differently on a fast machine than on a slow one. A refusal returns the thumb to where the gesture began rather than to where it was released, and for a range the pair moves as one value, so both handles return together.

    What it announces is the part worth reading. The flip announces once and hedged — "Saving" — so the new value is audible as provisional. Confirmation is silent, because what was announced is what happened; only a deviation speaks a second time, which is what makes an undo recognizable as an undo. Where the component sits in a form that already shows a validation message, the undo stays silent and leaves that message to speak. An aborted request announces nothing at all. Focus never moves.

    The factory ships in its own bundle and in no other, so an application that does not use it pays nothing — neither the bytes nor the announcement behavior. A component without the prop renders exactly as it did before, down to the byte.


  • A Content-Security-Policy build: 'scripts' => ['bundle' => 'csp']. Alpine's standard build evaluates its expressions with new Function(...), so a policy without script-src 'unsafe-eval' left every interactive component inert — a dropdown that never opened, a modal that never closed, and no error to say why. The CSP bundle is built against Alpine's own Content-Security-Policy distribution and brings its own Alpine, so it is loaded instead of yours, not alongside it.

    Reaching that meant the whole catalog had to stop asking for what a strict policy forbids. Every inline x-data object moved into a registered factory, every directive payload is encoded rather than interpolated, and the expressions that remain are single expressions with no arrow functions, template literals or optional chaining. That work is invisible in the standard build and is what makes the strict one possible.

    See Content Security Policy.

  • Plural forms are chosen in the browser by the language's own rule. A count that changes on the client cannot have its wording decided on the server: the page would show six and announce five. Translations now travel as forms and the browser picks between them with Intl.PluralRules. This replaces a count === 1 ? singular : plural choice, which is correct only for languages that have exactly two forms — Polish has three and Arabic six, and the wrong one was picked without ever looking wrong to a reader of English. Reaches the countdown's screen-reader text and every accessible name that embeds a count.

  • Inline edit — a displayed value becomes an editor in place, and nothing is written until the reader confirms. The component owns the interaction and not the saving: it emits an event carrying the field name, the new value and the previous one, and your Livewire component decides what that means.

    It waits for you to say the save worked. The end of a request is not evidence of success — a validation failure completes one too — so a component that closed there would discard the error message you just rendered and leave the reader looking at their old value with no explanation.

    Four control types, an editor slot for anything else, and three affordance styles: the pencil always visible, on hover, or only once tabbed to. There is deliberately no way to remove it, because plain text is not focusable and a value with no button cannot be reached by keyboard at all.

  • One-time code fields accept a non-numeric alphabet. A recovery code is often deliberately not digits — dropping the ambiguous pairs buys entropy per character and survives being read aloud. Such a code could not be typed into the field at all: every keystroke was discarded and the boxes stayed empty, with no message, while the reader held the correct code. One alphabet prop drives the keystroke filter, the paste filter, the validation pattern and the mobile keyboard together.

  • An application can state its own motion preference. Motion followed the operating system and nothing else, and the blanket reduced-motion rule made overriding it an arms race — it carries !important and matches the whole surface. data-reduce-motion on <html> takes reduce or no-preference; absent, the operating system decides exactly as before. The middle value is the point: a media query expresses two states, and someone who set the system flag for an unrelated reason needed a third.

  • The stylesheet and script directives accept a CSP nonce. [@wirekitStyles](https://github.com/wirekitStyles)($nonce) and [@wirekitScripts](https://github.com/wirekitScripts)($nonce), matching [@wirekitThemeScript](https://github.com/wirekitThemeScript). Under a strict-dynamic policy the nonce is the only thing that grants a resource, so without a parameter there was no way to allow the two tags that load the library.

  • Component-internal icon buttons reach the 44px touch target. The floor was bound to two marker classes, and a button rendered from inside another component carries neither — so a page built only from WireKit showed a row where two controls grew on touch and the third did not. The new wk-touch-target class centers a transparent 44×44 hit area inside a control without changing its size, and you can use it on your own icon buttons.

  • Usage meter accepts its own word for the unlimited tier. unlimitedLabel overrides the translated default, for the application whose fair-use tier is named "Unlimited" — a proper noun that has to read identically in every language. Until now that word could be held on surfaces the application rendered itself but not on the meter, so the rule survived only by nobody adding the key to a catalog.

  • A theme can reach four surfaces it previously could not. Dressing specific surfaces — putting a glass class on every panel, say — needs a stable way to find them, and only the card carried one. A dropdown panel, a popover panel, a modal body and a drawer body now each emit a data-wk-* attribute for exactly that purpose, and theming documents the complete set.

    Worth stating because of how the gap presented: a selector that matches nothing throws nothing, warns nothing, and produces no visual difference to compare against. A theme mapping six surfaces reached one, and read as though it reached six. The page now also names the one marker that is not a themeable surface — data-wk-tip marks an element that has a tooltip, not a tooltip, so dressing it frosts the wrong thing while looking like it worked.

Fixed

  • Four announcements that never announced anything. A live region that arrives on the page together with its text is a new node, and a screen reader says nothing at all — the region has to exist first and be filled afterwards. Affected the alert region behind failed streams and three sibling surfaces.

  • A toast is no longer announced twice. Each toast carried its own live region inside a container that was already one, so the same text was queued by two announcers.

  • A rating set with the keyboard never reached the form. Arrow keys moved the visible score without writing the hidden input, so submitting sent the score the page had before it was touched — and the star buttons had no translatable label at all.

  • Clearing a combobox did not reach the form, for the same reason: the visible field emptied and the submitted value did not.

  • A range slider and a one-time-code field each shipped a fix that could never run — both components registered a factory that nothing on the page used, so the corrected behavior was present in the bundle and unreachable from the markup.

  • A slider's value bubble stopped overwriting the element's style attribute, which discarded anything else set on it.

  • wirekit:doctor warned about three things that were not wrong. Each came from a check reading for a shape instead of the property it names, and a warning nobody can act on is worse than none — it teaches people the doctor is noisy, and then the real findings go unread too.

    A null-guard written as if (this._observer) { … } was rejected because only the inverted and optional-chaining spellings were recognized, though the accepted forms do strictly less. An observer created once for the lifetime of the page was reported as a leak, when giving it teardown would have been the defect. And an application self-hosting Chart.js was told its charts "render but draw nothing" while they demonstrably drew — that registration step applies only to the module import, not to the self-hosted build.

    The null-guard check now judges each disconnect() by its own surroundings rather than asking whether the file contains a guard anywhere. That distinction matters in both directions: the file-wide question rejected a correct guard, and simply teaching it the third spelling would have let a genuinely unguarded call hide behind a correct one elsewhere in the same file.

  • Inline edit could not be opened at all. Clicking the pencil did nothing — not on a busy page, not on an empty one with a single field. The editor opened and closed itself within the same tick, so nothing was logged, nothing threw, and the control simply looked dead.

    The cause was an identity check comparing two values that are not the same element. Opening announces itself so that other open editors close; that announcement identified its sender by the element in scope, and "in scope" means the button when the announcement comes from a click, but the component when the announcement is received. No editor ever recognized itself, so each one closed itself the instant it opened.

    It also resisted being debugged: instrumenting the open path changes which element is in scope, and the fault disappears while you look at it.

  • Inline edit shrank its own field the moment you clicked it, and put the confirm and cancel buttons above the field's center line rather than on it. Both applied to every control type.

    The width is the interesting half, because the markup looked right. The control carried w-full, which reads as "fills the row" and does not: as a flex item it keeps its automatic basis and settles at its intrinsic size — around twenty characters — no matter how wide the row is. So a value that read across the full width opened into a field a fifth of that.

    Moving the growth onto the control did not help either, and that is only visible in a measured box rather than in the markup: these are complete form controls, each rendering its own wrapper around label, field and hint, and that wrapper is the flex item while the field is a grandchild. Growth on a grandchild grows nothing. The editor now fills the row it is in, and the buttons sit on the midline of a single-line control — while a multi-line editor keeps them at the top edge, next to where typing starts, because centering against a box that has grown to six rows would float them far from both the first line and the last.

  • Liquid glass — the refraction never actually rendered. .wk-glass-refract asked for its distortion inside backdrop-filter, as blur(…) url(#wk-glass-refract), behind a support check for exactly that. The browser accepts the value, reports it as supported, keeps it — and draws nothing from it. Measured twice, independently: the card is pixel-for-pixel identical with the filter and without it. So the two tiers the page invites you to compare were the same image, and every check short of comparing rendered pixels agreed that all was well.

    The distortion now sits on a layer behind the content, applied through filter — which does draw. The surface's own text is unaffected, because only the backdrop is displaced.

    Moving it there was necessary and, on its own, still not enough to see anything, which is worth stating plainly because the reason generalizes. .wk-glass-refract had inherited Tier 1's blur(20px) over a 72% wash, and frosting exists precisely to destroy the structure behind it. A refraction can only bend a backdrop that still arrives, so displacing an already-flat wash produced an exactly flat wash — no error anywhere, and nothing on screen at any strength.

    Tier 2 is therefore now clear glass rather than frosted glass with an effect on top, with a much thinner blur and a much thinner wash. That is a visible change to anything already using .wk-glass-refract: the backdrop shows through where it used to be hidden. wk-glass is unchanged and remains the surface to pick when content on top needs a calm background.

    Two further consequences. The displacement moves the backdrop layer's own edge as well, which gave the surface a rippling outline — that layer now overshoots and the surface clips it, so the edge is straight again; the cost is that a Tier 2 surface clips overflowing descendants and cannot host a dropdown or tooltip. And it is no longer restricted to one browser: the mechanism is inside the supported baseline everywhere. The page said "Chrome only" and no longer does.

    And the strength was still too low to see. With the backdrop restored, the displacement moved measurably and the pattern behind the surface stayed a regular grid — so the page's own sentence, "watch the dotted pattern bend behind the box", was still not true. Judged at three strengths as images rather than by a number: the grid only visibly curves from roughly triple the previous value, and beyond that it dissolves into swirls and stops reading as a pattern at all. The shipped value now sits where the dots are drawn into arcs and still read as dots.

    Both failure modes are held by tests — a floor under the displacement strength, and a ceiling over the blur and the wash, so neither can quietly return to values that render nothing. The floor was raised with this change: it had been set at "moves at least one pixel", which is not what the page promises and let the effect stay invisible while passing.

    A second, smaller defect on the same page: the SVG filter reaches a page only through <x-wirekit::glass />, and on the documentation page that component appeared solely inside a code example — something to copy, not something the page ran. So the demos referenced a filter that was not there. Both halves had to be fixed; either alone leaves the effect invisible.

  • Sparklines drew nothing under ApexCharts 6. The chart configuration reached the library carrying keys from the other adapter's vocabulary. ApexCharts used to ignore what it did not recognize; version 6 iterates one of those keys and threw before drawing. The failure was quiet in the worst way — a sparkline is usually decoration beside the number it illustrates, so the page still looked complete and only an empty box gave it away. ApexCharts 5 and 6 are both verified in a real browser, and the chart page now states the supported range.

  • Inline edit closed itself when a save was refused. The editor is meant to stay open until a save is confirmed, and it did — right up until the case that matters most. A validation failure re-renders the field with an error message, which changes it enough that Livewire replaces the element instead of patching it, and the component came back in read mode. The reader was returned to their old value, holding an error about an edit they could no longer see. A field carrying a validation error now opens as editable, which is both the fix and simply what is true of such a field.

  • Countdown left a bound value false when the deadline had already passed. Binding with x-model and a target in the past produced a countdown that showed its expired state while the bound value stayed false for the life of the page — a resend button, for instance, that never became clickable again without a reload. The completion event is one-shot by design; the bound value should never have been.

  • Countdown's screen-reader text agrees in number. It joined the value to a plural label, so a single second was announced as "1 seconds", and it lowercased the label — wrong in every language that capitalizes nouns. The units are now pluralized through the translator, which also means a translation decides its own casing and word order.

  • Table's scrollable region had an English name in every language. A responsive table wraps itself in a focusable region whose accessible name fell back to a hard-coded English string, so a reader tabbing into it on a translated page heard English. Callers that pass tableLabel were never affected — which is why it lasted: the failure only appeared when the component was used exactly as documented.

  • Twenty-eight strings inside component behavior are translatable. Copy confirmations, sidebar collapse labels, password show/hide, filter-chip actions, map coordinates, calendar overflow, tour navigation and more were written as literals inside Alpine expressions rather than passed through the translator. That shape is invisible to every tool that hunts missing translations — they all look for the translator call — so unlike an ordinary oversight this one could not surface on its own.

  • wirekit:doctor names the token that is actually missing. When a token pair could not be compared it reported the Tailwind side as unset regardless of which side was absent, sending you to the file where nothing was wrong. It now names the specific token, so reading the line no longer requires knowing which side is which.

Changed

  • Product card is documented as a navigation surface. It renders a link and no server action, which is what its documentation now says.

Documentation

  • The Content Security Policy requirement is written down. The interactive components need script-src 'unsafe-eval', because Alpine evaluates expressions with new Function. Without it nothing throws — a one-time-code field still accepts typing and simply stops advancing — so this was previously learned by watching components quietly not work.
  • The liquid glass page states its theme requirement above the demos, and names the right theme. It sat in a trailing section below every demo, and named a theme that does not show the effect.
  • Toast examples show the real integration. The code panel carried demo scaffolding and a hardcoded payload, which read as though every click replays a fixed event. It now shows a Livewire action with the toast dispatched after the write returns, and the failure branch beside it.
v2.21.1

Patch release — accessibility fixes, most of them repairs to things 2.21.0 and 2.20.0 introduced. No new props, no new configuration. One change renames a DOM id; it is called out below.

Fixed

  • A floating action button with words in it had no accessible name at all. The wrapper around its content was hidden from assistive technology unconditionally, so a button reading Send feedback was announced as nothing and could not be reached by voice control. The wrapper is now hidden only when there is nothing in it to read. An icon-only button is unaffected.
  • prefers-reduced-motion no longer reaches past WireKit's own markup. The rule matched any element whose class attribute merely contained the characters wk-, which every design-token utility does — bg-[var(--color-wk-bg)] on a <body>, the documented way to tint a page, put the entire document in scope and clamped the application's own animations. It now matches a class token, so a developer's animations are their own again. Components animated through Alpine's shorthand transitions keep their coverage.
  • The modal and drawer close buttons reach the 44×44 target. They rendered 32×32 with nothing widening them. A transparent centered expander supplies the hit area; the visible button is unchanged.
  • The touch-target floor applies to both axes on text fields. It set a minimum height only, so the one-time-code digit boxes grew to 44px tall and stayed 40px wide — every box in the library's densest row of targets was still under the minimum. Buttons keep the height floor alone: widening them would grow a row of small icon buttons past its container, and giving those a real touch target needs its own treatment.
  • The active sidebar item renders its emphasized foreground. The resting muted color and the active color are utilities of equal specificity, so which one wins is decided by the order Tailwind emits them — and the muted one came last. An application retinting the active state lost the same way, leaving !important as the only way out.
  • Radio ids are page-unique like every other name-derived control. Two identical radio sets on one page — a filter bar and the same filter in a dialog — emitted the same ids, and because the hint and error ids derive from them, the second set's description pointed at the first set's help text.
  • The one-time-code digit boxes no longer collide with a same-named control. id="code-2" meant both digit 2 of the first control and the second control called code, so one of these beside any same-named field emitted a duplicate id at the default length. The digit ids change shape: code-0 becomes code-digit-0. They are not documented and nothing inside WireKit addresses them, but code reaching for them by id needs the new spelling.
  • The one-time-code row wraps instead of overflowing. Each box has a fixed width and cannot shrink, so an eight-digit code needed more room than a sign-in card offers and ran past its edge.
  • Eleven accessible names now go through the translator. The one-time-code group and its per-digit labels, plus names in color picker, date picker, carousel, stage card, notification center, tour and map, were English text with a value interpolated into it — translatable in appearance, fixed in practice. They use placeholders, so a language that orders the words differently is served correctly.
  • The component sandbox no longer offers two variants that alert and callout refuse. Its schema advertised secondary and accent, which are not in either component's vocabulary, so picking one in the prop editor threw instead of rendering. Both components take the six canonical intents and the schema now says so.
  • An inline event handler is no longer reported as an unknown prop. Writing <x-wirekit::button onclick="history.back()"> — ordinary HTML — produced a development-time warning saying the prop does not exist. Every on* handler was affected.
  • The component sandbox shows a bound attribute for a numeric prop. Its Show Code panel offered level="4" where the schema declares an integer; both render the same today, but the snippet is what a developer copies, and it should teach the shape that stays correct once anything compares the value strictly.
  • The published package manifest no longer carries a development-only Composer scripts block. It referenced a directory the distributed package does not contain. Composer never ran it for an installed dependency, so no existing installation is affected.

Documentation

  • The 2.21.0 entry now names the 16px floor on text fields that shipped with the touch-target change. Both live in the same coarse-pointer rule, and the 16px half is the one that stops iOS Safari from zooming the page in when a field takes focus — and not zooming back out. The behavior has been there since 2.21.0; only its description was missing.
v2.21.0

Minor release — accessibility fixes across dialogs, touch targets and motion, plus a visible name for floating-action items. Everything here is additive or a fix; existing markup renders the same.

Added

  • Modal gains an ariaLabel prop. A dialog built without a modal header — a confirmation, a media lightbox, anything whose heading is its own markup — can now be named directly. Without a name and without a header the component says so during development instead of rendering a dialog that screen readers announce as just "dialog".
  • Floating action items show their name on hover and on keyboard focus. An icon-only action carried its name only for assistive tech; anyone looking at the screen saw a row of circles. The label appears on hover and on focus, is positioned so it never shifts the buttons, and cannot intercept a click.
  • Speed-dial actions can hide their visible name. hideLabel takes the name off the screen while aria-label keeps it — the bare icon-only dial the pattern started as. The label still shows by default.
  • The single-action floating button can sit along the top edge. placement="block-start" moves it there, combining with position so all four corners are reachable; block-end stays the default and existing markup is unchanged. The top offset clears the status bar and notch the same way the bottom one clears the home indicator.
  • intent now works on every component whose colors carry a severity. Alert, callout, text, reading progress, timeline items and the circular progress accept intent alongside the variant they already had — the same spelling button and badge use. Writing intent on one of them used to do nothing at all: it was emitted as a stray HTML attribute and the component rendered its default color. variant keeps working unchanged; when both are given, intent decides.

Changed

  • Form controls and buttons reach a 44px touch target on touch devices. Inputs, selects, textareas and buttons rendered 40px tall, which clears the WCAG 2.5.8 minimum but misses the 2.5.5 target of 44×44. The floor applies only where the pointer is coarse, so the desktop rendering is unchanged.
  • prefers-reduced-motion is honored across the component library. Twenty components animate their entrance and exit; only one treatment was previously covered, so a modal still faded and scaled for a user who had asked their system to reduce motion. The rule is scoped to WireKit's own elements — a developer's own animations are left alone.

Fixed

  • A modal without a header no longer renders a dialog with no accessible name. The panel referenced a title element that only the header creates, so a headerless dialog pointed at nothing — which resolves to no name at all.
  • aria-* attributes passed to a modal now reach the dialog. They were applied to an outer wrapper with no role, where ARIA prohibits them and assistive tech never sees them.
  • The floating button documentation now opens the dialog it describes. The example titled a single-action FAB that opens a dialog opened nothing, while the button carried aria-haspopup="dialog" — an attribute that describes behavior to assistive technology rather than creating it, so the page was making a promise the code did not keep.
  • A floating button with words in it is announced by those words. It carried aria-label="Action" unconditionally, so a button reading Send feedback was announced as Action — and someone using voice control could not activate it by the words on screen. Visible text now becomes the accessible name; an explicit label is kept when it contains that text, and an icon-only button is unaffected.
  • The interactive component previews show WireKit markup in their code panel instead of compiled HTML. The preview knew which markup it had rendered and did not pass it on, so the documentation site had nothing else to display.
v2.20.0

Minor release — sidebar and app-shell layout controls, a floating action button, two form-field affordances, and a run of accessibility and correctness fixes. Everything here is additive or a fix; existing code renders identically.

Added

  • A single-action floating action button — <x-wirekit::fab.button>. A circular, screen-corner button for the one primary action a screen offers (compose, add, feedback). It follows the writing direction, keeps clear of the iOS home indicator and a landscape notch via safe-area insets, and announces to assistive tech what it opens.
  • Sidebar sections fold and remember their state. <x-wirekit::sidebar.group collapsible> turns a section heading into a disclosure that folds its items; <x-wirekit::sidebar.collapsible> gains the same, a persist="key" that remembers the open state across reloads, and a variant="heading" that styles its trigger as a small uppercase section label instead of a nav row.
  • App shell gains a viewport mode. <x-wirekit::app-shell viewport> pins the shell to the viewport height so the sidebar and main region scroll internally — brand at the top, account menu at the bottom — instead of the page growing past the fold. The default keeps the document-scroll behavior.
  • Input gains a mono variant and leading/trailing icon slots. mono renders the field value in the monospace font (codes, SKUs, hashes); <x-slot:leading> / <x-slot:trailing> place an icon or addon inside the field frame.
  • Checkbox gains a hideLabel prop — a checkbox with no visible label that keeps its accessible name, closing an API gap with the other form controls.
  • File upload's remove button gains a translatable, overridable removeLabel (default Remove :name), so its accessible name is no longer hardcoded English.

Changed

  • Dropdown, popover, menubar, multi-select and color-picker panels follow their trigger on scroll and resize. An open panel used to stay at its opening coordinates while the page scrolled; it now re-anchors to its trigger and tears its listeners down on close.

Fixed

  • Form controls that share a name on one page now each get a unique DOM id. Input, textarea, select and the other name-derived controls emitted the same id twice when two fields shared a name (a create-and-edit form, a filter bar plus a modal), leaving every field after the first without an accessible name. Each instance now derives a page-unique id, so label[for] and aria-describedby resolve to the right field. Opt out with wirekit.a11y.dedupe_ids.
  • File upload's remove button now meets the 44px touch-target size. Its clickable area was ~18px; it now fills a 44×44 target while the visible chip stays small.
  • Sidebar sections no longer disappear in the collapsed icon rail. A <x-wirekit::sidebar.collapsible> hid all its items when the sidebar folded to the icon rail; its child icons now stay reachable as a flat list, like a static group.
  • A retinted active sidebar item keeps its color on hover. The base hover style is now scoped to non-active items, so a themed active state is not overridden while the pointer is over it.
  • Tooltip honors a delay or offset of 0 instead of swallowing the zero and applying the default.
  • Unbound prop="false" reads as off across the remaining tri-state props that still treated the unbound string "false" as truthy.
  • Config class overrides now reach dotted sub-components. wirekit.components.sidebar.item.classes.* (and every other parent.child sub-component) applied only to top-level components before; the override now resolves for sub-components too.
  • php artisan wirekit:publish-fonts re-publishes fonts when the bundled files change, and wirekit:doctor reports stale published fonts, instead of silently leaving an old copy in place.

Documentation

  • Alert dialog and field docs clarify the action-button count and where a validation error's accessible ownership lives.
v2.19.0

Minor release — overlay panels that escape their container, a boolean attribute that finally means what it says, and a slider that shows its labels. Everything here is additive or a fix; existing code renders identically, except the prop="false" case, which was broken before and is now correct.

Added

  • Dropdown, combobox, multi-select and data-table panels escape a clipping card and cap to the viewport. Putting one of these inside a card — the most ordinary filter-bar layout there is — used to clip the open panel at the card's edge, and a long menu on a short window clipped its top items. The panels are now positioned against their field and capped to the available height, so they open in full and scroll when tall. No developer wrapper needed.
  • window.wirekitPosition exposes the same tested positioning helper WireKit uses internally, for developers who position their own overlays.

Fixed

  • prop="false" now turns a feature off instead of on. Passing an unbound boolean attribute — disabled="false", required="false", faq's schema="false" — used to do the opposite of what it read as, silently, because an unbound Blade attribute is the truthy string "false". Every boolean prop across the component library now reads "false", "0", "off" and "no" the way they are written. Bound props (:disabled="false") are unchanged. The direction that was broken is the one you reach for deliberately — turning something off.
  • range-slider shows its named values. value-text-map gave each stop a word — "Free", "Enterprise" — but sent it only to screen readers, so the handles and legend still showed the raw number. They now show the word too; unnamed stops fall back to the number.

Documentation

  • Supported Laravel versions are stated as 12+/13+. The package has been built and tested against Laravel 13 for a while; the docs now say so.
v2.18.3

Patch release — rendering and accessibility corrections. Four defects that were invisible in the markup you write and visible in what your users get: a stray space in running text, a boolean attribute that did the opposite of what it said, an accessible name that never arrived, and invalid attributes on rendered elements.

Fixed

  • Inline components put a visible space in front of whatever followed them. Run <x-wirekit::code>wirekit:install</x-wirekit::code>. rendered as Run wirekit:install . — the component's view ended with a newline, and HTML collapses that to a space. It also reached structured data: faq's plain-text mode derives its schema answer from the rendered HTML, so the space shipped inside the FAQPage JSON-LD. Sixteen components were affected, among them code, link component, kbd, mark and badge. There was no way to work around this from the call site, because the space originated inside the component's own output.
  • <x-wirekit::faq schema="false"> emitted the FAQPage JSON-LD anyway. An unbound Blade attribute arrives as a string, and the string "false" is true — so switching the schema off switched it on, silently, with the page rendering normally either way. That is precisely the spelling you reach for when a page carries a second FAQ block and must not emit two competing FAQPage nodes. schema, multiple and plain-text now read "false", "0", "off" and "no" the way they are written; the bound form :schema="false" behaves exactly as before.
  • faq's accessible name was not exposed to screen readers. The label prop was applied to an element that ARIA does not allow to carry a name, so assistive technology did not announce it and accessibility scans reported a violation on a page that had done nothing wrong. The question list now carries a role that can hold the name, and only when a name is actually set.
  • variant, size and announce-errors rendered as invalid HTML attributes. Passing one of these to a component left it in the markup — <div variant="flush" size="lg">, <input announce-errors="false">. Harmless to the display, but invalid HTML from a library that promises clean semantic output, and visible in any validator. Fixed across all eighteen affected components, including faq, input, select, textarea and toggle.
  • range-slider's merged value badge faded in on page load. When two thumbs sit close enough for their value badges to combine, the combined badge animated in from nothing on first paint instead of simply being there — a visible blink for a state that was never in question. It now paints its initial state outright; moving a thumb into or out of the merge still animates.
v2.18.2

Patch release — setup-documentation corrections. No code changes: the package renders exactly as it did in 2.18.1. What changed is the getting-started walkthrough, which in two places described a setup step inaccurately enough to leave a first-time reader with a failing build.

Documentation

  • Getting started told readers to create a resources/js/bootstrap.js that their build cannot resolve. The page treated a missing bootstrap.js as a file to be recreated from the block shown. That holds for laravel/laravel, which ships the file and lists axios in package.json — but the Livewire Starter Kit ships neither, so following the instruction there ended in failed to resolve import "axios" on the next npm run build. The section now distinguishes the two scaffolds, states plainly that creating nothing is the correct action when axios is absent (WireKit never uses it), and gives the install command for readers who want Laravel's baseline regardless.
  • The same block described the axios global as a Livewire requirement. It is not: Livewire issues its own fetch() calls and never reads window.axios. The comments said otherwise, which made an explicitly optional step read as mandatory.
  • The first-Livewire-page walkthrough named a command without the flag that produces its files. The paragraph promised app/Livewire/Showcase.php and resources/views/livewire/showcase.blade.php while naming make:livewire alone. Livewire 4 defaults to a single-file component, so readers who ran it as written got resources/views/components/⚡showcase.blade.php and could not find any file the walkthrough went on to edit. The flag is now stated before the claim, together with what happens without it.
  • Code block and the Chart.js advanced guide now install what their examples import. Both showed an import for a package (highlight.js and chart.js respectively) whose install step lived on another page.
v2.18.1

Patch release — localization and accuracy fixes. No new capability and no behavior change for an English application; every fix below either corrects output in a non-English locale or removes something that should never have shipped.

Fixed

  • alert announced its variant in English whatever the page language was. The screen-reader prefix a reader speaks before every alert — "Notice", "Warning", "Error" — was a literal, so a German page announced "Notice: Alle Locales werden gemeinsam freigegeben". 2.18.0 made the catalog translatable and this component was missed. The five labels now route through the translator, with the matching keys in the published en.json.
  • alert silently ignored a role you passed it. The component emitted its own role before your attributes, and HTML keeps the first occurrence, so your value sat inert in the markup beside the one that won. A caller-supplied role now takes precedence.
  • Numbers rendered with English separators in every locale. usage-meter grouped thousands as 1,234 where German, Italian, Spanish and Portuguese want 1.234 and French wants a space; attachment rendered file sizes as 2.4 MB instead of 2,4 MB; rating announced 4.2. All three now format for the active application locale. Applications without ext-intl keep exactly the output they had — the extension is not a new requirement.
  • rating announced a hardcoded English sentence. A readonly rating told screen readers "4.2 out of 5 stars" and named its interactive group "Rating", in every language. Both are translatable now.

Documentation

  • Structured data's site-identity builders were missing from the 2.18.0 notes. Schema::webSite(…), Schema::organization(…) and Schema::softwareApplication(…) build the three nodes almost every marketing page needs, Schema::graph([…]) combines them into a single [@graph](https://github.com/graph), and Schema::node(…) is the escape hatch for any schema.org type the builders do not model. They shipped in 2.18.0; only the announcement was missing. The structured-data page documents the full set.
v2.18.0

Minor release — a broad wave of developer-facing capability, accessibility and accuracy work. Everything here is additive and backward-compatible: new props default to today's behavior, new commands and components are opt-in, and no existing tag renders differently unless you reach for one of the new options.

Added

  • wirekit:publish-fonts publishes the font families your config names, not the whole tree. Point fonts.sans / fonts.serif / fonts.mono at bundled families and this command copies exactly those into public/, roughly 430 KB for a typical two-family setup against 5.8 MB for everything. --all publishes the full tree (for an app with a runtime font picker), --prune removes families the config no longer names, --force overwrites. See fonts.
  • Fonts now load even when they were never published. A configured family is served straight from the installed package over a /wirekit/fonts/… route, so a page can no longer silently fall back to system fonts because a publish step was missed. Publishing remains the fast path (a static file beats a PHP round trip); it is now a performance choice, not a correctness one.
  • The whole component catalog is translatable. Every user- and screen-reader-visible string routes through Laravel's translator, WireKit registers its own language directory, and a wirekit-lang publish tag drops a complete en.json reference into your app to rename per locale and fill in. A new localization guide walks the publish → translate flow. A developer building a multilingual product can now localize WireKit itself, which was previously impossible without republishing every view.
  • range-slider announces a spoken value per handle. A new valueTextMap prop gives each thumb its own aria-valuetext — a tier range can announce "Free" and "Enterprise" instead of "0" and "100". Unnamed stops fall back to their number.
  • New <x-wirekit::form> wrapper sets one error-announcement policy for every control inside it, so you configure aria-live error behavior once per form rather than per field.
  • theme-controller gained a cookie storage driver and configurable button chrome. The cookie driver lets the server render the correct theme on first paint (no flash); the chrome (size, surface, icon slots) is now overridable.
  • stream gained a fetch transport and a manual mode, the two shapes it needed to drive a token stream from a plain fetch response or from your own code rather than only Server-Sent Events.
  • countdown is now a headless clock — a slot plus a full remaining breakdown (days/hours/minutes/seconds), so you can render the time however you like.
  • pricing-table renders its own monthly/annual toggle and fits its grid to the plan count; pricing-tier now forwards minor-unit and locale price formatting instead of dropping it.
  • status-tiles gained an opt-in showStatus, image-gallery a fit prop, and faq a plain-text schema answer with a per-item escape hatch — each additive and byte-compatible by default.
  • Media-playback icons ship in every base preset (play / pause / stop / etc.), so a player UI has consistent glyphs without a custom icon set.
  • Sub-components are discoverable through the AI-tooling surface. wirekit:show card.body, the JSON export, the MCP catalog and the Boost manifest now report every sub-component with its own props — table.th's column-scope option, card.body, and the rest — so an editor or agent authoring WireKit markup can find the composition pieces, not just the top-level tags.

Changed

  • A published config/wirekit.php is no longer a ceiling. The config merge is now recursive, so an app that published the config once keeps receiving every new component default a later release adds, while still winning on any value it actually set. Previously a published components array replaced the package's entire section, quietly freezing the config to the keys it had the day it was published. wirekit:doctor gained a check that names any section your published file is missing.
  • The AI-tooling manifest and Boost skill derive each component's tag from the registry, so the class-based chart's real tag is reported correctly, and ComponentRegistry reads props for directory-form components, not just top-level ones.

Fixed

  • slider announced a value the reader could no longer see. Under wire:model, a server-driven change moved the thumb but not the spoken value, so a screen reader read the old number — and with a labeled mark map, the wrong meaning. The component now re-reads the element after each Livewire update. Apps without Livewire are unaffected.
  • Accent-toned text no longer drops below contrast when a brand accent is re-tinted, and the theme-controller <select> and data-table search <input> moved onto the contrast-compliant resting border token.
  • A batch of documentation-accuracy and accessibility corrections across component pages: row-header styling, a named radial-progress, correct status-tile link semantics, prop-list and default-value fixes on several pages, and the shipped Cursor rules no longer teach a handful of wrong patterns.
v2.17.1

Patch release — dependency maintenance. No API change and nothing to migrate: the bundled positioning engine moves forward a minor version, and the chart adapter is confirmed against the next major of its optional peer.

Changed

  • Bundled Floating UI updated to 1.8. This is the positioning engine behind dropdown, tooltip, modal, combobox and menubar. It ships inside the WireKit bundle, so there is nothing to install — placement, flipping and collision behavior are unchanged.
  • The Chart ApexCharts adapter now supports ApexCharts 6 alongside 5. The peer dependency you install can be either major; the adapter's option shape and theming are the same on both.
v2.17.0

Minor release. Additive and backward-compatible throughout: a new form-control hover token, per-preset font publish tags, and a form-control accessibility pass finished across every control, every hover state, and every theme preset. A form-control accessibility pass that finishes what 2.16.0 started, an internationalization fix for pagination, and a stdio server that no longer gives up after a minute of quiet. Everything here is additive and backward-compatible.

Added

  • --color-wk-border-strong-hover design token. The hover border for form controls, and the counterpart to the --color-wk-border-strong resting border that shipped in 2.16.0. One value is correct in both themes: it sits below the resting border on light and above it on dark, so the edge always moves away from the field fill rather than toward it. See design tokens.
  • Per-preset font publish tags. vendor:publish now offers a tag per font preset instead of only the all-or-nothing bundle, so an app can publish just the family it uses. A newly added preset gets its tag automatically. See fonts.
  • Segmented Control selected / unselected appearance. The two segment states are now addressable through theming and personalization, which previously could not reach them because the appearance was decided at runtime rather than at render time.

Fixed

  • Every form control now keeps a 3:1 border, at rest and on hover. 2.16.0 fixed the resting border on input, select, textarea and checkbox; fourteen sibling controls still drew theirs from the decorative border token at 1.29:1 in the light theme and 1.56:1 in the dark. All of them now use --color-wk-border-strong. Separately, the hover state pointed at a decorative token that is lighter than the resting border, so a control that met the contrast floor dropped to 1.87:1 precisely while the pointer was on it — the worst possible moment to lose a visible edge. Hover now uses the new --color-wk-border-strong-hover.
  • Every theme preset keeps its form-control borders contrast-compliant. Presets that tint their own control border — Aurora, Brutalist, Retro Terminal, Cupertino — now define --color-wk-border-strong and --color-wk-border-strong-hover alongside it, so a form control keeps the theme's edge instead of falling back to the stock neutral, and every one clears 3:1 against the field fill in both light and dark. Aurora's values were also corrected (they had reached 1.75:1 light / 2.13:1 dark). If you pasted a preset block into your own stylesheet, re-copy it.
  • wirekit:mcp-serve no longer exits after 60 seconds of quiet. Editors that launch the server over a socket rather than a pipe — which is what Node-based clients do — hit PHP's socket read timeout, and the server read that timeout as the client hanging up. It exited with status 0 and an empty error stream, so the editor reported a disconnect with nothing to go on, a reconnect worked instantly, and the whole thing looked like flakiness. Idle timeouts and real hangups are now told apart.
  • Countdown segments no longer overflow on a narrow screen. The boxed-segment row did not wrap, so a five-unit countdown ran past the edge of a phone-width viewport, and the box-pulse animation clipped its own outermost segment. The row now wraps and reserves room for the pulse.
  • Pagination is translatable as whole sentences. The summary and the page indicator were assembled from separate fragments, which cannot be translated correctly into languages that order those parts differently. Each is now a single translatable string with placeholders. The translation keys are documented for the first time.
  • Sidebar item counters no longer push the icon off-center in the collapsed rail. A sidebar.item carrying a badge shared its flex row with the icon at rail width. The counter now becomes a corner dot when the rail is collapsed, with the digits kept available to screen readers.

Documentation

  • Bundle sizes are quoted in one place. Dependencies is the only page that states them, and it is the only one measured against the shipped files; other pages link to it. Two pages had drifted apart on the same figure.
v2.16.1

Patch release — re-publishes 2.16.0. Nothing here is new. The 2.16.0 package that reached Packagist was built from an incomplete tree: it is missing the Status Tiles and Stream components entirely, along with most of the other changes listed under 2.16.0 below. The tag and the source were correct throughout; only the published package was not.

If you installed 2.16.0, upgrade. Everything the 2.16.0 notes describe is in this release and nothing else changes, so the upgrade carries no migration and no behavior difference beyond gaining what 2.16.0 promised.

v2.16.0

Minor release. Two new components plus a broad set of accessibility, internationalization, and composition improvements, and a new form-control border token that meets WCAG non-text contrast. Everything here is additive and backward-compatible.

Added

  • Status Tiles. N entities as colored status tiles read at a glance — a fleet light, with an optional count-per-intent legend. Status never rides on color alone: every tile carries a distinct icon shape and a screen-reader status word, and an href makes a tile a keyboard-operable link.
  • Stream. A primitive for streaming text output over Server-Sent Events, with the hard parts handled once — a single live region announces that a response is generating and then the result (never re-read on every token), prefers-reduced-motion reveals the buffered text at once, and a dropped connection or an explicit stop resolves to a defined terminal state. A simulate mode types a fixed string out from a local timer for a demo or typewriter effect with no endpoint.
  • Badge wrap. A long label can now wrap across lines: the pill grows with the text instead of the second line spilling below its surface.
  • Image Gallery per-item overlays. An itemOverlay render-callback layers a control — a badge, a report button, a required content label — over each thumbnail as a sibling of the zoom trigger, so the thumbnail still opens the lightbox while the overlay stays interactive.
  • Command Palette server-side search. The palette now emits a debounced query event on every keystroke, so you can drive a server-backed search — feed the query to a Livewire property and re-render the results — instead of only filtering a fixed list.
  • Slider valueTextMap. The spoken aria-valuetext can now be decoupled from the visual tick labels — show numeric ticks yet announce semantic meaning (1 => 'Low').
  • Countdown completion event. The countdown now dispatches an event when it expires and exposes its done state, so it can drive a sibling control without running a second clock.
  • Sidebar item badge. sidebar.item gained a badge prop for a trailing unread counter — the common notifications / inbox nav pattern.
  • Table row headers. table.th gained a headerScope prop so a per-row header cell can be scope="row", previously reachable only as a column header.
  • Tooltip focusableTrigger. A tooltip on a non-interactive trigger — an icon, a text span — can now be reached by keyboard instead of being hover-only.
  • Design tokens. The --gap-wk-* scale is now the full xs2xl ladder (matching --space-wk-*), and a new --color-wk-border-strong token gives form controls a contrast-compliant resting border. See design tokens.
  • Error-announcement config default. A new wirekit.a11y.announce_error config key lets an app that runs its own live region opt every form control out of the built-in error announcement in one place, instead of per control.

Fixed

  • Form-control borders now meet WCAG non-text contrast (1.4.11). Input, select, textarea and checkbox drew their resting edge at 1.29:1 in the light theme — effectively invisible against the field fill. They now use the dedicated --color-wk-border-strong token (≥3:1 in both themes); the decorative border on cards and dividers is unchanged.
  • Fonts no longer fall back to system fonts silently in production. A configured but unpublished bundled font now leaves a visible signal in every environment plus a throttled server-side warning, instead of vanishing to system fonts with no trace outside local development.
  • Editor binding and custom toolbar. wire:model on the editor now binds the textarea — it previously landed on the wrapper and lost input — and a custom toolbar slot now renders instead of being overridden by the default set.
  • Notification Center announcements. New notifications are now announced to screen readers through a polite live region.
  • Pagination nested-route 404. The component now emits absolute hrefs, so a paginator under a nested Livewire route no longer 404s; and every user-visible string — body text and aria-labels — is now translatable.
  • Combobox attribute forwarding. Every caller attribute now reaches the role="combobox" input rather than a roleless wrapper.
  • Translatable strings. The "opens in new tab" screen-reader hint on the link component and its siblings, the threshold band labels and reset-cadence notes on the usage meter, and the copy-confirmation text on the clipboard button, are now translatable instead of hardcoded English.
  • Checkbox description association. A caller aria-describedby is now merged with the checkbox's own hint / error target instead of being silently dropped.
  • Table sort headers. A sortable column in Livewire sort mode no longer shows a pointer cursor across the whole cell when only the button is the click target.
  • Contrast helper parses rgb(). WcagContrast::parseToLinearRgb() now understands rgb() / rgba() color strings, not only #hex and oklch().
v2.15.0

Feature release. A broad set of marketing, ecommerce, control and mobile components — the blocks a landing page, a store, and a mobile app-shell were missing — plus SEO structured-data builders and a drop-in dark-mode toggle. All additive; one existing component (Carousel) was rebuilt on a more robust foundation with no API break.

Added

  • Pricing Table. The plan grid — tiers, features, a highlighted plan, monthly/annual framing — the shape a SaaS pricing page needs, composed from tokens rather than a bespoke layout.
  • Testimonial. A cited quote with author, role, avatar and an optional read-only star rating (announced as a single record, never an operable control), plus a grid to lay several out.
  • FAQ. An accordion of questions that emits FAQPage JSON-LD derived from what it actually rendered — the structured data can't drift from the visible copy because it is generated from it.
  • Logo Cloud and Team Section. The two marketing blocks WireKit had no exemplar for — a "trusted by" logo strip and a people grid.
  • Announcement Banner. A dismissible page-edge bar (top or bottom) with an optional inline CTA. The dismissal persists by default; set persist off for a session-only bar that reappears next visit instead of being remembered.
  • Bento Grid. An asymmetric feature showcase — cells span a real column/row ladder and collapse to a single stacked column when the grid itself is narrow (a container query — a sidebar, a card, a split view — reflowing on its own width, independent of the viewport), reclaiming their spans once it is wide again; an unknown span degrades to a normal cell rather than a broken track claim.
  • Mockup. Frame chrome — browser, window, code, phone and tablet — for screenshots and demos. Every surface, border and shadow is a design token, so the frame follows the theme; the chrome is decorative and hidden from assistive technology.
  • Product Card. The ecommerce keystone — image, price with an optional compare-at, rating, stock state, and a call-to-action.
  • Button Group. Welds adjacent controls (buttons, an input + button) into one unit — inner radii collapsed to a single seam, RTL-safe via logical properties.
  • Toggle Button. An aria-pressed toggle — a button that stays down, for a single on/off state. Opt into self-toggle and it flips its own pressed state on click (a formatting toolbar with no wiring); the controlled form — the pressed state lives in your app — stays the default.
  • Indicator. A corner-badge positioner — a count or dot that rides the corner of any element (an avatar, an icon button) via logical insets.
  • Radial Progress. A circular progress ring — a real progressbar with the value announced, drawn from tokens. Opt into animate to sweep the fill from empty on first paint and animate later value changes instead of snapping, gated by prefers-reduced-motion.
  • Bottom Nav and FAB. The two mobile app-shell pieces WireKit was missing — a fixed bottom tab bar (opt into interactive to track the current tab client-side: clicking a tab marks it active with no page load) and a floating action button that fans out into secondary actions (keyboard-operable).
  • Theme Controller and Swap. Drop-in dark mode — a control that toggles the theme with no wiring and no flash-of-wrong-theme, and a swap primitive that cross-fades between two states (the sun/moon icon being the canonical case).
  • Schema builders. Typed PHP builders for schema.org JSON-LD (the same engine the FAQ block uses to derive its FAQPage data), for emitting structured data from your own components.

Changed

  • Carousel was rebuilt on native scroll-snap. The slider now rides the browser's own scroll-snap instead of a JavaScript transform, so it is smoother, keyboard- and touch-native, and lighter; autoplay gained a stop button, and a perView shows 2–4 slides at once. No API break — existing usage keeps working.

Fixed

  • Progress — label and value no longer jam at narrow widths. The label / value row gained a column gap so the two can never touch.
v2.14.0

Feature release. The Chat / AI Conversation suite: six new components for building chat transcripts and AI-native surfaces, plus fade, delivery-status, readability, animated-progress and copy-affordance additions to existing components. All additive — nothing changes for existing components — with one accessibility fix to the rating component.

Added

  • Shimmer. An animated highlight that sweeps across the letterforms of live text — the "Generating response…" / "Thinking…" affordance for AI streaming and long-running status. Unlike a skeleton's block shimmer, this shimmers the real copy rather than a gray placeholder, so the text stays readable to a screen reader throughout. Bind active to a streaming flag and it turns itself on and off with no conditional markup and no JavaScript. The sweep is disabled under prefers-reduced-motion and its duration is a token you can override per instance.
  • Conversation. A stick-to-bottom transcript scroller for chat and streaming. It keeps the newest message in view while a response streams in, but the moment the reader scrolls up to read history it stops yanking them back down — and offers an announced "jump to latest" with an unread count instead. New content appended above the fold never shifts what the reader is looking at. The viewport is a labeled live region, reachable and scrollable by keyboard.
  • Attachment. The display card for a file on a message — icon, name, human-readable size and type — and an attachment-group that lays several out, stacked by default or as a scroll-snapped row. Sizes and types are formatted server-side, so nothing about the file depends on JavaScript. Fills the existing attachments slot on the message component. An opt-in animate gives the uploading bar a shimmer sweep so an in-flight upload reads as active; it is disabled under prefers-reduced-motion.
  • Chat Marker. The in-thread meta row for the things in a conversation that are not messages — a streaming or tool-call status, a system note, a "New messages" divider, a timestamp separator. Presentational by design: compose a link or button inside it when a marker needs to be actionable.
  • Message Typing. The three-dot "someone is composing" indicator. Given an author, it shows that person's avatar and name above the dots — indented like their message — so the reader sees who is typing. It carries a visually-hidden "… is typing" text so a screen-reader user loses nothing when the dots freeze under prefers-reduced-motion, and it is a static announcement rather than a per-keystroke flicker that would flood assistive tech.
  • Assistant Message. The AI turn — the assistant-side counterpart to the human chat bubble, and WireKit's first AI-native surface. Assistant, user and system roles (a system turn is a centered notice), a streaming state, a prose body, an optional model chip on assistant turns, a collapsible reasoning disclosure, and footer chips and actions slots. The streaming body is announced a complete sentence at a time rather than character by character, so a screen reader hears finished thoughts, never a half-clause re-read on every token. An intent tints the bubble when a turn IS a state — an error answer (danger), a caution (warning), a confirmation (success) — with the body text kept regular so the tint reads as a marker, never as low-contrast colored text.

Changed

  • Scroll Area gained an edge fade. A fade option masks the overflow edges along the scroll axis so the content itself dissolves toward the edge — adapting to any background automatically, since there is no colored overlay to keep in sync with the surface. The fade lifts entirely when a child near the edge takes keyboard focus, so a focused control is never faded out from under the reader. Edge depth is a token.
  • Message gained a delivery-status ladder. A status prop shows sending → sent → delivered → read (or failed), the affordance a messaging UI needs for a bubble's own state. It follows the convention every messaging app shares — one check for sent, two checks for delivered, two accent checks for read — and is never carried by color or glyph alone: each rung states its own wording for assistive technology. Pass statusTime and the glyph carries a tooltip with the exact moment ("Read at 9:15 AM"), woven into its accessible text too so a screen reader gets the time without the hover.
  • Progress gained an optional animated fill. An animation overlays motion on a determinate bar — stripes (a barber-pole) or shimmer (a light sweep) — the "work in flight" affordance for uploads and streaming. The bar's value is unchanged and the motion is disabled under prefers-reduced-motion.
  • Clipboard Button gained a bare icon-only mode. A compact copy glyph — muted gray at rest, popping to the success green on copy — for action rows where a full labeled button is too heavy. Requires an aria-label since there is no visible text to name it.
  • Prose gained readability presets. A preset picks a reading rhythm — tighter line spacing for chat, roomier line spacing for long-form reading, larger text for emphasis — on top of the existing prose styling. (This is the vertical rhythm only; the separate measure prop that clamps line length is untouched.)

Fixed

  • Rating: a read-only rating is now a record, not a switched-off control. A read-only rating used to render a hidden form field and announce itself as a radio group with several stars simultaneously "selected" — so a page showing a dozen product scores carried a dozen stray form fields, and a screen reader heard a broken multi-select. A read-only rating is now announced as a single image with one readable value ("4.2 out of 5 stars"), carries no form field, and exposes nothing to operate. The interactive rating — where the reader gives a score — now implements its full documented keyboard model: both arrow axes move the selection, and Home / End jump to the first and last star.
v2.13.0

Feature release. Four new components for user-content media and running deadlines — three Display (countdown, image, image gallery) and a reusable media-viewer Overlay (lightbox). All additive — nothing changes for existing components.

Added

  • Countdown. A live countdown to an absolute deadline, ticking down client-side with no polling. The display scales from years down to seconds (a far-off deadline reads in years and days, not tens of thousands of days), or you can pick an exact set of units. It colors itself when the deadline is near (urgent) and again once it has passed (overdue). Large values get locale-aware thousands separators, an optional segments variant renders each unit as a labeled box that animates when its value changes — either the whole box pulses (animate="box", the default) or only the changing number flashes color (animate="text"), both honoring prefers-reduced-motion — and it stays accessible — the ticking value is exposed to screen readers as a stable deadline label, not announced every second.
  • Image. A content image rendered as a semantic <figure> with required alt text, native lazy loading, an optional CLS-safe ratio box (space reserved before the image loads), cover/contain fit, and an optional caption. Renders only — a signed, ACL-protected download URL works unchanged.
  • Image Gallery. A responsive grid of content images with an accessible lightbox. Clicking a thumbnail opens a focus-trapped dialog that navigates with the arrow keys, closes on Escape, and returns focus to the thumbnail it opened from. Set :lightbox="false" for a plain responsive grid.
  • Lightbox. The gallery's zoom overlay as a standalone component you can drive from any trigger and any layout — images, video, and embeds. It is a focus-trapped dialog that steps through its items with the arrow keys, closes on Escape, and returns focus to whatever opened it. Open it from a control inside the component or from anywhere on the page via a wirekit-lightbox-open event; configure looping, captions, and a per-instance backdrop color. Media fills most of the viewport (very wide and very tall images, and video, scale to ~90% of the screen with their aspect ratio intact); off-screen slides load lazily and show a loading spinner until they resolve; captions sit on a semi-transparent dark scrim for readability and wrap centered under the media. The Image Gallery now builds on it internally.
v2.12.0

Feature release. A wave of accessibility and Livewire-integration improvements across the form controls, plus new infrastructure icons. Every change defaults to the current behavior, so upgrading is safe and requires no code changes.

Added

  • Combobox — accessible name. New label, hideLabel, and ariaLabel props, matching Select and Multi-Select. A combobox in a facet toolbar can now carry a proper accessible name without hand-rolling an external <label for>.
  • Form controls — announced validation errors. Every error-rendering control (Select, Checkbox, Radio, Textarea, Toggle, Number Input, OTP Input, Password Input, Multi-Select, Combobox, Tags Input, Time Picker, Date Picker, Editor) now renders its error message in a polite live region by default, so a server-side validation error that appears after submit is announced even when focus has moved to the submit button. Opt out per field with :announce-error="false".
  • Slider — labeled-mark announcements. When marks is a labeled map ([0 => 'Low', 100 => 'High']), the slider now exposes the label to assistive tech via a live aria-valuetext and shows it in the value tooltip — a screen reader announces "Low" instead of "0". Plain sliders are unchanged.
  • Button — scoped loading spinner. New loading-target prop scopes the loading spinner and disable to the button's own Livewire action, so on a polling page the spinner no longer flashes on every wire:poll refresh or unrelated action.
  • Table — keyboard-operable Livewire sort. New sort-action prop on <x-wirekit::table.th> wraps the header label in a real <button> with a focus ring, so server-side sorting is operable by keyboard, not mouse only.
  • Message — accessible actions and localized time. The actions slot now reveals on hover or keyboard focus (and actions-reveal="always" keeps it visible for touch). The timestamp is now locale-aware — 9:15 PM for English, 21:15 for German and other 24-hour locales — with a time-format prop for an explicit format.
  • Icon — infrastructure glyphs. New aliases across every base preset: server, database, cloud, shield / shield-check, inbox, bolt, and refresh.
  • Theming — auditable tinted surfaces. Pushery\WireKit\Theming\WcagContrast now parses color-mix(in srgb, …) values, so the soft (tinted) surfaces used by Badge, Alert, and Stat can be contrast-checked directly.

Fixed

  • Brand — responsive logo no longer vanishes. <x-wirekit::brand> built its responsive show/hide classes by splicing the breakpoint into the class name at render time, which a Tailwind v4 CSS-first build cannot discover — so the desktop logo could silently disappear after a WireKit upgrade. The classes are now emitted as full literals.
  • Segmented Control, Rating, OTP Input — wire:model modifiers. These controls dropped wire:model modifiers (.live / .blur / .debounce) when forwarding the binding to their hidden input, so a live binding never updated. All modifier forms are now forwarded.
  • Inputinputmode no longer warns. Standard HTML attributes like inputmode, enterkeyhint, autocapitalize, and spellcheck no longer log a spurious "unknown prop" warning.
  • Icon — no more crash on an unknown alias. An unknown icon name in a browser request now renders an inert placeholder and logs the problem, instead of throwing and taking down the whole page. Console and test runs still fail fast so typos surface early.
  • Combobox — the dropdown now matches its size. A large or small combobox opened a dropdown whose option rows stayed medium-sized; the option padding and text now scale with the size prop so the open panel matches its trigger.
  • Combobox — no stray focus warning. Clicking the chevron to close the dropdown could leave keyboard focus on the decorative toggle, which browsers flag as focus trapped on an aria-hidden element. Focus now always returns to the input.
  • Button — a joined input abuts its button cleanly. When an <x-wirekit::input> is joined to a trailing button inside <x-wirekit::button.group>, the field's seam-side corner is now squared and its duplicated seam border removed, so the input meets the button flush instead of showing a rounded edge and a doubled border.
v2.11.1

Patch release. Documentation-only clarifications to the Getting Started guide. No component code changed — upgrading is optional.

Documentation

  • Getting Started — JavaScript pipeline. Clarified that resources/js/app.js and resources/js/bootstrap.js already ship with a standard Laravel app, so there is nothing to create there. The one thing that matters is not importing Alpine yourself — Livewire provides it.
  • Getting Started — layout example. The example layout now shows the [@wirekitStyles](https://github.com/wirekitStyles) directive in <head> alongside [@wirekitScripts](https://github.com/wirekitScripts), so pasting it verbatim loads WireKit's design tokens (colors, spacing, radii, shadows).
  • Getting Started — your first Livewire page. Clarified that make:livewire scaffolds empty files whose contents you then replace with the example shown, and that the example route replaces Laravel's default welcome route instead of adding a second route for /.
v2.11.0

Minor release. Overridable navigation-landmark and heading-level semantics, screen-reader-announced form errors, and an engine-neutral rich-text-editor factory — all backward-compatible.

Added

  • <x-wirekit::sidebar> gains a label prop for the navigation landmark's accessible name. It defaults to "Sidebar"; override it — or pass aria-label / aria-labelledby directly — when a page has more than one navigation landmark, so assistive technology can tell them apart. No duplicate or conflicting name is emitted.
  • <x-wirekit::empty-state> gains a level prop for the title's heading level. Choose 16 (default 3) to fit the surrounding document outline so screen-reader heading navigation stays correct. The default still renders an <h3>.
  • <x-wirekit::field> and <x-wirekit::input> announce validation errors to screen readers. The error message now renders as a polite ARIA live region by default, so an error that appears dynamically — for example after a Livewire round-trip — is announced without the focus having to return to the field. Opt out with announceError="false" when your page runs its own live region.

Changed

  • The <x-wirekit::editor> rich-text component now uses an engine-neutral window.wirekitEditor(config) factory. The previous window.tiptapEditor name keeps working as a deprecated alias, so upgrading is a one-line rename in your editor bootstrap.
v2.10.0

Minor release. An opt-in accent stripe for callouts, a readable line-length measure for prose, a refined intent treatment on KPI tiles and stage cards, and refreshed marketing-blueprint copy — all backward-compatible.

Added

  • <x-wirekit::callout> gains an opt-in stripe prop. It adds a one-sided accent bar and is off by default (the plain callout is the alert-style tinted border). Pass stripe to bring the bar back.
  • <x-wirekit::prose> gains a measure prop — a readable line-length clamp. Long-form text caps at a comfortable ~65 characters per line by default; use measure="wide" (~78ch) for a roomier column or measure="none" for the full container width. Themeable via the new --measure-wk token.

Changed

  • <x-wirekit::callout> no longer shows the accent stripe by default. The plain callout is now the balanced tinted border; add the new stripe prop to restore the one-sided bar.
  • <x-wirekit::stat> and <x-wirekit::stage-card> intent treatment is now a balanced 4-sided tinted border (previously a one-sided left bar). The intent color cue is unchanged — only the shape is refined.
  • <x-wirekit::prose> now clamps long-form text to a readable measure by default. Existing prose wraps at ~65ch; set measure="none" to restore the previous full-container-width behavior.

Documentation

  • Refreshed the marketing-blueprint demo copy (hero, testimonials, feature, and landing previews) with neutral, concrete placeholder content.
v2.9.0

Minor release. A local MCP server, discoverable AI tooling, new SaaS icon aliases, a dropdown form-submit affordance, dev-mode composition + prop warnings, and documentation — all backward-compatible.

Added

  • A local MCP server — php artisan wirekit:mcp-serve. AI coding assistants spawn it over stdio and query the component catalog live while authoring, so the editor reads real prop signatures and design tokens instead of guessing them. It is local and read-only — no port, no daemon, no network, always version-matched to your installed WireKit. See the AI-tooling guide for copy-paste editor config and the CLI reference.
  • A tool-neutral AGENTS.md at the package root. It points AI coding assistants at the wirekit:list / wirekit:show / wirekit:icons discovery commands, so assistants that do not read the Cursor rules still find the tooling. The README gains a matching "Using WireKit with AI assistants" section.
  • A Laravel Boost skill manifest — php artisan wirekit:boost-skills. Publishes .boost/wirekit.json — every component with its real props + defaults, the theme presets, the customization decision tree, and the CLI — so a Laravel-Boost-aware editor autocompletes WireKit. Auto-generated from the installed package, so it cannot drift from your version; re-run to refresh after an upgrade. See the CLI reference.
  • Five high-frequency icon aliases now resolve out of the box on every base presetsettings, gear, dashboard, billing, and credit-card. Previously settings / gear / dashboard / billing had no glyph on a base-only setup. See <x-wirekit::icon>.
  • <x-wirekit::dropdown.item> now honors a caller type. A no-href item still defaults to type="button", but type="submit" lets it drive a wrapping <form> — the canonical CSRF logout for non-Livewire apps. A "Sign out & form actions" recipe documents both the Livewire and the form-submit paths.
  • A hideLabel prop on <x-wirekit::input>, <x-wirekit::select>, and <x-wirekit::textarea>. It keeps the real <label> (so the control keeps its accessible name) but renders it visually hidden — for a compact field in a toolbar or header, where the default stacked label reads wrong.
  • Dev-mode warnings for two silent mistakes (debug only, silent in production). <x-wirekit::card> warns when content is placed directly in it with no card.body (it would otherwise render flush against the border), and a misspelled prop on a prop-rich component — the form controls plus button, badge, alert, stat, callout, progress, and tooltip — now logs a "did you mean" hint instead of silently doing nothing.

Changed

  • The shipped .cursor/rules/wirekit.mdc AI-authoring rules were corrected to match the shipped API. Several prop names (the layout primitives take gap, not space; the button takes intent), token names, and a composition example had drifted from the components they describe. The file now matches the real surface and gains the card.body and app-shell composition patterns, with a guard that keeps it from drifting again.

Fixed

  • A caller aria-label now reaches the actual control on more form components. On <x-wirekit::otp-input>, <x-wirekit::rating>, <x-wirekit::tags-input>, and <x-wirekit::file-upload>, a caller aria-label previously landed on the outer wrapper, where assistive technology ignored it. It now names the role-bearing element — the role="group" / role="radiogroup", or the text / file <input> — so screen readers announce it. A class-level guard now locks this across every single-interactive-element component.

Documentation

  • Setup and reference guidance for the most common first-build mistakes. A consolidated "Styles not applying?" troubleshooting checklist and a "using WireKit tokens in your own Tailwind classes" note on the integration and customization pages; a Spacing-and-Layout token section on the Design Tokens page; a typography signpost steering body copy to <x-wirekit::text>; a note that navbar and the app-shell are alternative layout shells; and corrected prop / token references across several component pages. The AI-tooling guide now features the local MCP server with copy-paste editor config, and component reference pages lead with their live examples — usage notes and prop conventions moved below.
v2.8.1

Patch release. A Livewire 4 layout-setup documentation correction plus a matching installer-hint fix — fully backward-compatible.

Fixed

  • wirekit:install now names the correct layout path in its no-layout hint. When no app layout exists and Livewire's livewire:layout command is unavailable, the installer's fallback instruction pointed at resources/views/components/layouts/app.blade.php. On a fresh Livewire 4 app the layout actually belongs at resources/views/layouts/app.blade.php (Livewire 4's default layouts::app namespace), so the hint now names that path.

Documentation

  • The Getting Started layout walkthrough now matches what the installer actually creates. It recommended resources/views/components/layouts/app.blade.php and pinned the example page-component to #[Layout('components.layouts.app')] — but on a fresh Livewire 4 app (no Starter Kit), wirekit:install, via Livewire's own livewire:layout, creates resources/views/layouts/app.blade.php (Livewire 4's default layouts::app). Following the old steps left #[Layout] pointing at a file that was never created. The page now frames the two real cases (fresh app vs. Starter Kit), drops the #[Layout] attribute from the example (Livewire's default already wraps the component in the created layout), and clarifies that make:livewire scaffolds empty stubs you fill in and that the route is yours to add.

v2.8.0

Minor release. One additive component prop plus accessibility, tooling, and documentation fixes — all backward-compatible.

Added

  • <x-wirekit::brand> gains a darkLogo prop for a mode-aware logo swap. Set darkLogo alongside logo and the component renders a light/dark <img> pair that swaps automatically under the .dark class — no more rendering two <x-wirekit::brand> elements toggled by hand. It composes with mobileLogo (the compact mobile mark stays mode-neutral below the breakpoint; the light/dark swap applies to the desktop logo above it). Omitting darkLogo keeps the previous single-logo behavior byte-for-byte.

Fixed

  • <x-wirekit::range-slider> now forwards a caller aria-label / aria-describedby to the sliders instead of a non-focusable wrapper. The dual-thumb slider spread its attribute bag onto the outer <div>, so a caller-supplied accessible name never reached the two role="slider" thumbs (WCAG 4.1.2 / 1.3.1). The wrapper is now a labeled role="group": the visible label names the group, each thumb's name embeds that context ("…minimum" / "…maximum"), and the hint plus any caller aria-describedby are announced on the focusable thumbs.
  • wirekit:verify (alias wirekit:doctor) no longer mis-reads CSS comments in your app.css. A comment that merely mentioned an [@import](https://github.com/import) of wirekit.css could trip a false "imported" pass, and a --font-wk-* example pasted in a comment above the real declaration could mask the token-alignment check. The command now strips CSS comments before scanning, so only live CSS is read.
  • <x-wirekit::reaction> and <x-wirekit::scroll-to-top> now let a caller's aria-label override the component's default. Both hardcoded their default aria-label before spreading the attribute bag, so a developer-supplied aria-label rendered as a duplicate the browser ignores (first wins). They now merge the default into the bag, so your aria-label takes precedence — and the sensible default still applies when you pass none.

Documentation

  • New guidance for dark-mode dark: utilities in your own markup on the integration page: if you write Tailwind dark: utilities yourself while loading WireKit via the [@wirekitStyles](https://github.com/wirekitStyles) <link>, add [@custom-variant](https://github.com/custom-variant) dark (&:where(.dark, .dark *)) to your app.css so they follow the .dark class rather than the OS prefers-color-scheme.
  • wirekit:doctor:a11y now documents its scope — the contrast audit covers your Blade usage and --color-wk-* token overrides, not colors inside your own CSS custom classes.
  • The theming guide now warns that token overrides wrapped in [@layer](https://github.com/layer) silently lose to WireKit's unlayered defaults — keep --color-wk-* overrides in a plain :root {} block.

v2.7.2

Patch release. A documentation addition, fully backward-compatible.

Documentation

  • WireKit's release history is now browsable online at docs.wirekit.app/changelog, with a dedicated page per version. The README "Documentation" table and this file's header now link it, so you can read the release notes as navigable per-version pages instead of scrolling one long file. The page list stays in step with each published release.

v2.7.1

Patch release. A single layout fix, fully backward-compatible.

Fixed

  • <x-wirekit::app-shell> now reflows its main content when a collapsible sidebar collapses to its icon rail. The shell's sidebar column was a fixed 16rem, so when the nested <x-wirekit::sidebar collapsible> shrank to its 3.5rem rail the main content kept its left offset and a ~12.5rem gap appeared. On desktop the column now tracks the sidebar's width — following the --wk-sidebar-w variable when expanded and shrinking to the rail width when the sidebar is collapsed — so the content reclaims the freed space. Mobile (the off-canvas overlay) is unchanged.

v2.7.0

Minor release. An additive enrichment wave across forms, navigation, and the editor toolchain. Every new feature is opt-in and backward-compatible. One rendered-output change is called out under Changed — the standalone calendar now defaults to a Monday-first week to match the rest of WireKit's date components. Adds nested submenu flyouts to the three menu components, collapse-to-icon sidebars, multi-month calendars, grouped comboboxes, clearable/copyable inputs, a clearable color picker with a custom-trigger slot, a standalone editor adapter bundle, and an editor-preset scaffolder.

Added

  • <x-wirekit::dropdown>, <x-wirekit::context-menu>, and <x-wirekit::menubar> gain nested submenu flyouts. A new opt-in sub-component on each parent — <x-wirekit::dropdown.submenu>, <x-wirekit::context-menu.submenu>, <x-wirekit::menubar.submenu> — opens a child flyout beside its parent item on hover, click, or ArrowRight. Each accepts label (or a <x-slot:label>), icon, placement (default right-start), offset, and disabled, supports arbitrary nesting depth, and implements the full WAI-ARIA submenu keyboard model (ArrowRight/Enter/Space to open and focus the first child; ArrowLeft/Escape to close and refocus the parent) with hover-open collision flipping. Flat menus are unaffected and render identically.

  • <x-wirekit::sidebar> gains a collapse-to-icon rail. Set collapsible to add an auto-rendered toggle that narrows the nav to an icon rail; collapsed sets the initial state and persist="key" remembers it in localStorage. Item labels, group headings, and chevrons collapse to icon-only via pure CSS (no per-item wiring), while links keep their accessible names. The expanded width is overridable via the --wk-sidebar-w variable (default 16rem). Default off — existing sidebars render unchanged.

  • <x-wirekit::calendar> gains multi-month display and quick navigation. months (1–4, default 1) renders consecutive months side by side, with arrow-key focus crossing between grids; selectableHeader swaps the static month/year label for native <select> jump controls (whose open-arrow now matches <x-wirekit::select>); and a new weekStartsOn prop (0 Sunday / 1 Monday) sets the first day of the week. months and selectableHeader default off.

  • <x-wirekit::combobox> gains option grouping. Pass a nested map (['Europe' => ['de' => 'Germany', …]], mirroring <x-wirekit::select>) to render <optgroup>-style headings. The keyboard model stays flat — arrow keys flow across group boundaries as one list and empty groups auto-hide. Ungrouped comboboxes are unaffected.

  • <x-wirekit::input> gains clearable and copyable trailing affordances. clearable shows an X button (visible only while the field has content) that empties and refocuses the field; copyable shows a copy-to-clipboard button with a brief "Copied" state announced to screen readers. Both default off and coexist with the prefix/suffix slots; wire:model / x-model stay in sync.

  • <x-wirekit::color-picker> gains a withClear "no color" button and a custom trigger slot (both popover mode). withClear adds a button that clears the bound value to empty and resets the picker to "no color" — the swatch, plane marker, hue, and value field all reflect the cleared state (the native <input type="color"> can't represent an empty value, so this is popover-only by design). A <x-slot:trigger> replaces the default swatch with your own content while WireKit keeps the open/close, anchoring, and dialog semantics wired. Both default off — existing pickers render unchanged.

  • New wirekit-tiptap.js editor adapter bundle (~2 KB gzip). A standalone bundle that registers only the editor's Alpine factory, so developers loading the lean wirekit.core.js can add the rich-text <x-wirekit::editor> without pulling in the full overlay bundle. It mirrors the chart adapter's shape — MIT glue only, Tiptap stays your peer dependency. Purely additive: the editor still ships in wirekit.js and wirekit-alpine.js. See Dependencies.

  • New php artisan wirekit:editor-preset command. Scaffolds the window.tiptapEditor(config) factory the editor calls at init, pre-wired per toolbar preset (basic or full) with every callback forwarded and the security-correct link-protocol configuration in place. --write=<path> writes to a file; --force overwrites an existing target. See the CLI reference.

Changed

  • <x-wirekit::calendar> now starts the week on Monday by default (was Sunday). This aligns the standalone calendar with <x-wirekit::event-calendar> and the rest of WireKit's date components. The weekday header and day grid shift accordingly; pass week-starts-on="0" (or set components.calendar.week-starts-on in config/wirekit.php) for a Sunday-first week. This is the only rendered-output change in this release.

Fixed

  • <x-wirekit::reading-spine> with boundary="container" now stays pinned instead of scrolling away with the article. When the reading surface is a bounded scroll container (the reading-sidebar recipe, a modal or panel reading view), the sidebar previously scrolled out of view with the content; it now pins to the container's viewport, offset by the new --reading-spine-offset-top variable (default 1rem). Also applies via <x-wirekit::reading-shell> with boundary="container".

  • php artisan wirekit:install now creates an app layout when your project doesn't have one yet. A fresh Laravel + Livewire 4 project (without a starter kit) ships no resources/views/components/layouts/app.blade.php, so Livewire page components failed at runtime with "layout view not found" and the installer previously only told you to add one by hand. The installer now delegates to Livewire's own php artisan livewire:layout to create the layout, then injects [@wirekitStyles](https://github.com/wirekitStyles) + [@wirekitScripts](https://github.com/wirekitScripts) — ordering [@wirekitScripts](https://github.com/wirekitScripts) before [@livewireScripts](https://github.com/livewireScripts) so WireKit's Alpine plugins register before Livewire boots Alpine. wirekit:doctor's hint and the Getting Started walkthrough were updated to match (including that Livewire 4 scaffolds single-file components by default — pass --class for the class-based form shown in the guide).

  • Form controls no longer trigger mobile Safari's zoom-on-focus. <x-wirekit::select>, <x-wirekit::input>, <x-wirekit::textarea>, and every composite control that renders its own field — combobox, multi-select, command-palette, number/password/OTP/tags input, date/time picker, the color-picker hex field, and the filter-builder/data-table search field — rendered below 16px. iOS Safari zooms and shifts the viewport whenever a focused field is smaller than 16px, so tapping a control made the page jump and the field read as too small. On touch devices these controls are now pinned to the 16px threshold; desktop keeps the compact design-token sizing.

  • <x-wirekit::calendar> fills the available width on phones instead of a fixed narrow grid. The standalone month grid renders at a compact ~312px content width; on a narrow phone — especially inside a padded container — that looked too narrow and could clip the trailing weekday column. Below the sm breakpoint the calendar now expands to fill its container; from sm up it keeps the compact content-width grid unchanged.

Documentation

  • <x-wirekit::editor> gains an "Advanced: mentions, slash commands & collaboration" section. Documents how to use the editor's extensions array to add [@mention](https://github.com/mention) autocomplete, slash-command menus (including AI-assist items that call your own endpoint — WireKit never calls an AI service itself), and real-time collaboration via Yjs. The collaboration guidance is explicit that it requires a sync backend you run.

  • New reading-sidebar recipe. A vertical sticky reading sidebar that fuses an always-expanded table of contents, active-section highlighting, and an article-scoped progress bar, composed from <x-wirekit::reading-shell>. See Reading sidebar.

  • <x-wirekit::sticky-panel> auto-collapse pattern gains a runnable preview. The documented compose-with-<x-wirekit::collapsible> auto-collapse pattern now ships a live preview, so it's copy-pasteable rather than prose-only.


v2.6.6

Patch release. Implements the documented value pre-selection prop on multi-select, fixes icon-name rendering on the sidebar components and the replay button's default icon, registers the list component in the machine-readable manifests, and documents the Livewire binding contract on the stateful form controls.

Added

  • <x-wirekit::multi-select> now implements the value prop for pre-selecting options on load. Pass an array of option keys (:value="['php', 'js']") or a comma-separated string and the matching pills render immediately. The prop was already documented but not previously implemented, so the pre-selection example rendered empty.

Fixed

  • <x-wirekit::list> now appears in wirekit:list, wirekit:export-json, and the generated .wirekit-schema.json manifest. The component shipped and was fully documented, but was absent from the internal component registry that drives those manifests — so AI tooling and IDE extensions reading the catalog never saw it. The component itself always rendered correctly; only the machine-readable manifest omitted it.

  • <x-wirekit::sidebar.item> and <x-wirekit::sidebar.collapsible> now resolve a string icon name to an SVG. Passing icon="cube" previously rendered the literal text "cube" instead of the icon; it now resolves through the icon system, consistent with the other components that accept an icon name. A custom <x-slot:icon> still works as before.

  • <x-wirekit::replay-button> renders its default circular-arrow icon when no custom content is given. The default icon was previously suppressed, leaving an empty button.

Documentation


v2.6.5

Patch release. Adds one opt-in, fully backward-compatible event to Tabs so a server can observe tab switches; no breaking changes.

Added

  • <x-wirekit::tabs> now dispatches a wirekit:tab-changed browser event on every tab switch, so a Livewire component can react to a change without rebuilding the tablist by hand. The event bubbles to window, and its detail carries { tab, label } (the activated item's key and its label). It fires on change only — not on the initial render, and not when the already-active tab is re-clicked. Listen for it on a wrapper and forward it into your component, e.g. <div x-on:wirekit:tab-changed="$wire.onTabChanged($event.detail.tab)">. Rendering stays client-side and existing tabs are unaffected (an unobserved event is a no-op). See Observing tab changes server-side.

Fixed

  • php artisan wirekit:doctor:a11y --theme-contrast no longer fails on resting decorative borders. Per WCAG 1.4.11 (non-text contrast applies only to UI elements that convey state or identify a boundary), the audit now treats the resting decorative borders (border, border-strong) as advisory — printed as INFO (decorative, WCAG 1.4.11 exempt) — and hard-checks only the borders that communicate at 3:1 (the focus ring, border-error, border-success). Previously it failed the decorative border/background pairing against a flat 3:1 bar, so even a stock install reported a failure on its own intentionally low-contrast dividers. See the theming guide for the exact exempt token pairings.

Documentation

  • <x-wirekit::tabs> — the page now leads with a live example, and a new "Observing tab changes server-side" section documents the wirekit:tab-changed event end-to-end.
  • <x-wirekit::editor> — added a "Factory config contract" table listing every key WireKit passes to your window.tiptapEditor(config) factory, so you can write the factory from the reference instead of reading the source.
  • <x-wirekit::icon> — documented the single-alias override (map one alias directly instead of stacking a whole extension preset) and why live stays a marketing alias.
  • <x-wirekit::slider> — added a pitfall: a dragged slider bound with wire:model.live sends a Livewire round-trip per step; use .debounce / .lazy instead.
  • Authoring custom components — added a recipe for asserting WireKit's debug-mode "unknown prop" warning in a test.

v2.6.4

Patch release. CLI introspection + editor-default fixes, fully backward-compatible.

Changed

  • The <x-wirekit::editor> extensions config now defaults to an empty array (was ['StarterKit']). Your window.tiptapEditor factory owns the real Tiptap extension set; the extensions value is only an optional list of name hints for a factory that reads them. The old non-empty string default was a foot-gun — a factory that spread it straight into Tiptap's extensions would throw (strings aren't Extension objects). The documented factory ignores the hints and is unaffected; set your own hints only if your factory consumes them.

Fixed

  • php artisan wirekit:show <component> now lists the component's slots (both the human output and --as=json), matching wirekit:export-json and the .wirekit-schema.json manifest. Previously show printed props and sub-components but omitted slots entirely, so introspecting a component (e.g. wirekit:show dropdown) never surfaced its named-slot quick-form contract such as <x-wirekit::dropdown>'s <x-slot:trigger>.
  • Four common <x-wirekit::icon> aliases — copy, globe, book, lightbulb — now resolve on every base icon preset (heroicons, lucide, phosphor, tabler) instead of throwing. Previously they lived only in the optional heroicons-app / marketing preset layers, so a base-only setup raised an exception unless those presets were stacked. They're now part of the shared base alias set, with each library's correct icon (e.g. tabler's world / bulb).

v2.6.3

Patch release. CLI and component-usage fixes, fully backward-compatible.

Changed

  • Invalid input now exits with code 1 consistently across every php artisan wirekit:* command. A few commands (wirekit:install, wirekit:verify / wirekit:doctor) previously exited 2 on a bad flag value or an invalid invocation, while the rest exited 1. They now all exit 1 — the Laravel/Artisan failure convention. Both are non-zero, so if ! command checks are unaffected; only a script branching on the specific code 2 needs to switch to 1. The wirekit:install --ignore-failed-flags help text — which wrongly implied the exit code equaled the number of failed flags — now correctly states it is 1.

Fixed

  • <x-wirekit::chart> now fails with a clear, actionable message instead of a cryptic "Undefined variable" error. The chart is a class-based component: written as the anonymous <x-wirekit::chart> (double colon) instead of <x-wirekit-chart> (single hyphen) it has no class to supply its data and previously threw Undefined variable $height. It now explains which tag to use and links to the docs.
  • php artisan wirekit:class-by-area now rejects an unknown --format instead of silently falling back to the summary. A typo'd --format=jsom previously rendered the summary and exited 0; it now exits non-zero with an Available: summary, full, json list, matching --area and every other command's option validation.
  • php artisan wirekit:install --diff now reports the actual layout file it would edit. The dry-run hard-coded a single layout path; it now resolves and names the same candidate the real install would touch — including the resources/views/layouts/app.blade.php convention — or lists every candidate it probes when none exist yet.

Documentation


v2.6.2

Patch release. A diagnostic-tool fix and release-notes accuracy, fully backward-compatible — no changes to components or styles.

Fixed

  • php artisan wirekit:doctor:a11y --theme-contrast no longer skips a token whose value falls back to a color function. A token written as var(--your-token, oklch(…)) — a var() with a parenthesized fallback such as oklch(…), rgb(…), or hsl(…) — was reported as "unsupported color format" and skipped instead of being contrast-checked. The audit now resolves those fallbacks, so the pairing is evaluated like any other.
  • The changelog now shows a release date for every published version. A tagged release could previously ship with its section for that version still labeled "Unreleased", so the version you installed and the notes you read could disagree. Every published version now records its release date.

v2.6.1

Patch release. Bug fixes across the schema-export tooling, the editor, the diagnostic commands (doctor / doctor:a11y), and the theme-preset system, all backward-compatible.

Fixed

  • php artisan wirekit:export-json (and the .wirekit-schema.json it writes to your project root) now reports props correctly for components whose [@props](https://github.com/props) block carries a comment with an odd number of quote characters. A comment such as // each item's group or // without a leading " contains an unpaired ' or "; the prop extractor mistook it for the start of a string literal, lost track of where the [@props](https://github.com/props)([…]) array ended, and gave up — reporting the component's props as (none) and listing the prop names under slots instead. IDE autocomplete and AI tooling fed from the manifest saw the wrong shape for <x-wirekit::skip-link>, <x-wirekit::sticky-panel>, and <x-wirekit::notification-center>. The extractor now skips comment spans before tracking string and bracket state, so every component's props and slots export accurately. Re-run php artisan wirekit:install (or wirekit:export-json) to refresh a committed .wirekit-schema.json.
  • <x-wirekit::editor> now logs its "Tiptap not loaded" console hint once per page instead of once per editor. When Tiptap (the editor's peer dependency) isn't loaded, the editor degrades to a plain textarea and logs a one-time hint pointing you at the install steps — but the hint fired on every editor mount, so a page with several editors filled the console with duplicates. It's now deduplicated behind a page-global flag, matching the chart and map components' missing-dependency hints. The textarea fallback itself is unchanged (each editor still degrades independently).
  • php artisan wirekit:doctor now reminds you about the editor and map front-end peer dependencies. Its optional-dependency report covered Chart.js and the QR-code package but stayed silent on Tiptap (for <x-wirekit::editor>) and a map engine — MapLibre GL or Leaflet — (for <x-wirekit::map>). Those are browser globals a PHP command can't probe, so they appear as informational reminders ("only if you use those components"), not pass/fail checks. The integration guide also gained an at-a-glance peer-dependency table.
  • php artisan wirekit:theme <preset> now applies the light palette correctly on the [@wirekitStyles](https://github.com/wirekitStyles) (<link>) path. The generated light block was wrapped in a Tailwind [@theme](https://github.com/theme) {} block, which compiles into the theme cascade layer; because the prebuilt dist/wirekit.css ships its default tokens unlayered, the layered light palette lost the cascade and silently did nothing in light mode (the accent stayed near-black, inline code stayed gray, the chrome stayed on the default font) while dark mode worked. The light palette is now emitted as a plain :root {} block — mirroring the .dark {} block it already emitted — so it wins the cascade and the preset applies in both modes. If you previously worked around this by hand-converting the generated block to :root {}, that edit is now redundant (but harmless).
  • The aurora preset's generated CSS no longer breaks the build. A nested block comment in its --theme-hue override example closed early (CSS block comments don't nest), so the trailing prose leaked into the stylesheet as invalid tokens and the build failed. The annotation is now plain text.
  • php artisan wirekit:doctor:a11y --theme-contrast now audits hue-driven theme tokens instead of skipping them. A preset that expresses its palette through a single source hue — oklch(L C var(--theme-hue)) — previously had its entire accent family reported as "unsupported color format" and skipped, because the audit didn't substitute the var() reference before parsing the color. It now resolves embedded custom-property references (recursively, with a cycle guard) from the same token table, so those pairings are actually contrast-checked.

Documentation

  • The integration guide now collects every optional peer dependency in one at-a-glance table. Previously Tiptap (for <x-wirekit::editor>) and a map engine (for <x-wirekit::map>) were documented only on their component pages, while <x-wirekit::chart> and <x-wirekit::qr-code> had their own scattered notes. The guide now lists all four side by side — each cross-linked to its component page — with concise Editor and Map setup sections.
  • The map documentation now shows how to keep the map engine out of your global bundle. A new bundle-size section on the MapLibre GL guide demonstrates loading the engine only on the routes that actually render a map — the engine is your own dependency (WireKit never bundles it), so a global import would otherwise add its weight to every page.

v2.6.0

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> (client-mode sort / search / select / column-manager + a server contract), <x-wirekit::filter-builder> (active-filter chips + a typed add/edit popover), <x-wirekit::status-matrix> (a 2D grid of tristate / toggle / status / heat cells), <x-wirekit::notification-center> (a bell + grouped, realtime-capable panel), <x-wirekit::usage-meter> (usage-vs-limit meters + a plan-paywall gate), <x-wirekit::event-calendar> (month / week / agenda scheduling views), and <x-wirekit::map> (a MapLibre / Leaflet adapter with an accessible marker list). Also new: <x-wirekit::editor>, a Tiptap rich-text adapter; <x-wirekit::sticky-panel>, a sticky companion column; and <x-wirekit::collapsible>, a standalone WAI-ARIA disclosure. <x-wirekit::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> — 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> — 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> — 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 heatFromheatTo (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> — 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> — 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>) 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> — 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> (the date-picker widget). Every event is a focusable button with a full date/time label.

  • New <x-wirekit::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> and <x-wirekit::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> and <x-wirekit::radio>. Both controls now scale via sm / md (default) / lg, matching the size already on <x-wirekit::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>, <x-wirekit::select>, and <x-wirekit::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> 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> 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>. 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>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> 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> 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> 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> 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> 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> — 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> — 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> — 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> when you have a coordinated group of panels.

  • <x-wirekit::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> 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> 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](https://github.com/theme), [@source](https://github.com/source), color-mix(), [@property](https://github.com/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](https://github.com/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> 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> 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> 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> 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> and <x-wirekit::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.

v2.5.0

Minor release. A new Aurora theme preset — color-confident, toned to the WireKit brand magenta, with dashboard-tuned radii, soft layered shadows (visible in both light and dark mode), and a single --theme-hue variable for one-line retinting. Its scope is bounded to the accent surface only — semantic intents stay on the WireKit default palette. Paired with a foundational cascade fix in dist/wirekit.css that lets every developer-side :root {} token override win without specificity tricks (matters across every preset, not just Aurora). Two new accessibility surfaces: <x-wirekit::skip-link> ships as a drop-in WCAG 2.4.1 (Bypass Blocks) helper, and <x-wirekit::dropdown.trigger> auto-adds a fallback aria-label (the ariaLabelFallback prop, default "Open menu") when its rendered button has no accessible name (icon-only triggers, responsive layouts that hide the visible label below sm). The wirekit:doctor:a11y linter gains an opt-in --theme-contrast stage that computes WCAG ratios for the active theme's token pairings.

Added

  • New aurora theme preset — modern, color-confident, toned to the WireKit brand magenta (hue 306°). php artisan wirekit:theme aurora applies it to your app.css end-to-end, or copy-paste the block from the theming guide — both produce identical output. Modestly rounded, soft layered shadows, cool-tinted surfaces, and a body text color softened off pure black with a whisper of the brand hue (--color-wk-text: oklch(0.24 0.02 var(--theme-hue)), WCAG AAA at 16.3:1) so it reads as part of the theme rather than stark monochrome on the tinted surfaces. The interactive --color-wk-accent reads oklch(0.55 0.22 var(--theme-hue)) — WCAG-AA-clean at 5.28:1 (light) / 6.29:1 (dark) against accent-fg — while two extra tokens, --color-wk-accent-brand and --color-wk-accent-brand-fg, preserve the brand-exact L=0.605 magenta for decorative-only surfaces (logo backplate, hero glyphs) where body text never sits on top.
  • --theme-hue single-source pattern on Aurora. Every hue-dependent token reads oklch(L C var(--theme-hue)) and the preset declares --theme-hue: 306 once at the top of the block. Developers can retint the entire Aurora palette to any hue in ONE LINE — :root { --theme-hue: 264; } produces an indigo Aurora; :root { --theme-hue: 24; } a warm-orange one — without touching the 14 hue-touching tokens individually. The L values are chosen to stay WCAG-AA-clean across the full hue rotation, so the retint is AA-safe at any hue.
  • <x-wirekit::skip-link> — drop-in WCAG 2.4.1 (Bypass Blocks) component. Renders a visually-hidden anchor that becomes a focused pill when keyboard-tabbed, jumping to a target landmark. Default target is #main-content; override via target prop. Pair with <x-wirekit::main id="main-content"> (the id is now a first-class prop on main). Auto-styled to inherit the active theme's accent color and radius.
  • id prop on <x-wirekit::main>. First-class prop so skip-link targeting reads naturally as <x-wirekit::main id="main-content">. Defaults to null (no id emitted). When set, the main element also becomes programmatically focusable (tabindex="-1") so JS-routing edge cases and direct fragment navigation move keyboard focus into the landmark.
  • wirekit:doctor:a11y --theme-contrast — WCAG-contrast audit of the active theme. New optional stage on the existing wirekit:doctor:a11y command. Reads the developer's resources/css/app.css, parses every --color-wk-* override under :root / .dark, computes WCAG 2.1 contrast ratios for the canonical token pairings (accent as text on bg, accent-fg on accent, text on bg, etc.), and reports PASS / WARN / FAIL per pairing for both light and dark modes. Catches the bug class where a developer customizes --color-wk-accent without verifying the new value still clears 4.5:1 against --color-wk-accent-fg. Opt-in stage (the existing static blade-scan continues to run first); enable with --theme-contrast or set WIREKIT_DOCTOR_THEME_CONTRAST=1.
  • Inline <code> is now themeable via two new tokens — --color-wk-code (text) and --color-wk-code-bg (background). Both default to the previous values (body text on the muted surface), so every existing theme is unchanged. <x-wirekit::code> reads them automatically. The Aurora preset sets them to the brand magenta on a light magenta-tint highlight (WCAG AA at 5.74:1 light / 7.56:1 dark, hue-stable across --theme-hue retints), so inline code reads as part of the theme rather than as plain body text.
  • <x-wirekit::accordion> gains variant and size. variant controls the container chrome: bordered (default — the existing self-contained card), flush (no outline, just row dividers — for an FAQ that sits inline in page content), and separated (each item becomes its own standalone card with a gap between them). size="lg" gives the trigger roomier padding and a larger title for marketing and touch-first layouts. Both default to the previous look, so existing accordions are unchanged.
  • <x-wirekit::date-picker> gains a range flag. Renders a linked start + end pair of native date inputs — the end can't be set before the start and vice versa (a small reactive link, no calendar dependency). They submit as name[start] / name[end]; pass an initial range as an array ['start' => .., 'end' => ..] or a YYYY-MM-DD/YYYY-MM-DD string. The single-date default is unchanged.
  • New <x-wirekit::button.group> — joined / segmented button bar. Wrap buttons to join them into a single control: inner corners are squared, the shared border seam collapses to one line, and the active button rises above the seam. Supports orientation="vertical" and a label for the group's accessible name. The same wrapper also joins an input with a trailing button (attached search field, newsletter signup). RTL-safe (logical properties).
  • <x-wirekit::breadcrumb> items accept an icon key. Add 'icon' => 'home' to any breadcrumb item to render a decorative glyph before its label (a leading home icon, per-category icons, etc.). The label stays the accessible link text — the icon is aria-hidden and is never written into the JSON-LD structured data. Items without an icon are unchanged.
  • New <x-wirekit::avatar.group> — stacked / overlapping avatars. Wrap a set of avatars to render them as an overlapping stack, each ringed in the surface color so the discs stay distinct. Set :remaining for a trailing "+N" overflow chip, label for the group's accessible name, and size to match the avatars inside. Plus a documented "avatar with name & detail" composition pattern on the avatar page (Row + Stack + Text — no new component needed).
  • New <x-wirekit::spinner> — accessible loading indicator. A lightweight CSS spinner for in-place activity that has no measurable progress. Four sizes (sm/md/lg/xl), an optional semantic intent color (defaults to currentColor so it inherits its context — drop it inside a button or badge and it matches), and a screen-reader label (default "Loading") announced via role="status". Use it when you can't show a layout preview (Skeleton) or a percentage (Progress).
  • <x-wirekit::badge> gains surface, dismissible, and trailingIcon. surface chooses how the intent color is applied — soft (default, the tinted chip), solid (filled intent background with on-color text), or outline (transparent with an intent-colored ring). dismissible adds a keyboard-operable close button that hides the badge and dispatches a wirekit:badge-dismissed event (great for removable filter chips), with the button's accessible name set via dismissLabel (default "Remove"). trailingIcon mirrors leadingIcon after the label. All three default off / to the existing look, so existing badges are unchanged.

Changed

  • dist/wirekit.css :root / .dark blocks now wrap in :where(...) for specificity 0. Foundational cascade fix: any developer-side :root { --color-wk-accent: ... } override (specificity 0,1,0) now wins over WireKit's defaults regardless of source order — including the very common [@wirekitStyles](https://github.com/wirekitStyles) setup where this stylesheet is injected via a separate <link> tag AFTER the developer's compiled app.css. Pre-fix, developers had to either [@import](https://github.com/import) wirekit.css from inside app.css OR use a higher-specificity selector like html:root to win the cascade. Both workarounds are now unnecessary. Backward compatible — developers already using html:root continue to work (their higher specificity still wins).
  • <x-wirekit::dropdown.trigger> auto-injects a fallback aria-label when the inner button has no accessible name. The component inspects its interactive child (button / link / role="button"); when that element has no aria-label, no aria-labelledby, and no non-empty text content (visible OR sr-only) — the icon-only-trigger and mobile-collapsed-label cases — it sets an aria-label from the new ariaLabelFallback prop (default "Open menu"). Any explicit aria-label / aria-labelledby / text on the trigger still wins. Closes the bug class where a responsive layout hid the label below the sm breakpoint and the icon-only trigger read as just "button" to screen readers.
  • dist/wirekit.esm.js now registers every component plugin, matching the IIFE bundle. The ES-module build previously omitted stat-animate, animate, and the reading-spine / reading-minimap / reading-toc plugins, so developers registering WireKit from the ESM entry point saw <x-wirekit::stat animate> and <x-wirekit::reveal> (plus the reading primitives) render their static value with no animation behavior. All 29 plugins are now registered (bundle: ~31 KB gzip / 109 KB raw, up from ~25 KB / 82 KB).
  • Badge border + depth are now theme-tokens; the Aurora preset renders softer, flat tinted badges. Two new tokens control the badge outline: --border-wk-badge-width (default: the global --border-wk-width) and --shadow-wk-badge (default: the inset-ring + faint drop that lifts the chip). The Aurora preset sets both to a flatter look — no outline, no inset ring — so badges read as soft tinted chips rather than bordered pills. Every other theme is unchanged, and you can restore the bordered look under Aurora (or harden it anywhere) by setting the two tokens back to their defaults in your :root.
  • Dismissible <x-wirekit::badge> and <x-wirekit::alert> now emit the data-replayable contract. A badge or alert with dismissible set renders data-replayable="true", so a <x-wirekit::replay-button> placed in the same DOM tree can restore the element after it has been dismissed (the button re-mounts the closest [data-replay-target] from a saved snapshot). The attribute is inert on its own — it does nothing unless a replay button is wired up — and the dismissible behavior itself is unchanged.

Fixed

  • Theme overrides loaded via the [@wirekitStyles](https://github.com/wirekitStyles) setup no longer get silently overwritten by WireKit's defaults. Symptom: a developer adds :root { --color-wk-accent: ... } to their app.css, runs npm run build, and finds the value is ignored in the browser because wirekit.css loads AFTER app.css and (until this fix) its :root block had equal specificity (0,1,0) plus later source order. The :where() wrap on every top-level :root / .dark block in dist/wirekit.css brings WireKit's defaults to specificity 0, so any developer override wins. Affects every theme preset, not just Aurora.
  • Built-in theme presets that recolor the accent now pin a co-tuned --color-wk-accent-fg, so primary-button labels clear WCAG AA. php artisan wirekit:theme retro-terminal previously left the accent foreground at the default near-white, which paired with the light-green accent at only 2.13:1 — well below the 4.5:1 AA threshold for the button label. retro-terminal now pins a near-black label (8.06:1); soft, material, and cupertino pin a near-white label (5.03–7.73:1). The accent / accent-foreground pair is written together so it stays aligned in both light and dark mode.
  • <x-wirekit::range-slider> no longer collapses to an undraggable sliver in shrink-to-fit containers. The track is a full-width block with no intrinsic width of its own, so inside a width: fit-content wrapper, a flex/grid auto item, or a table cell it shrank to the width of its two bound labels (~60px). The component now pins a 16rem min-width floor while still expanding to 100% in normal block flow, so it stays operable everywhere.
  • <x-wirekit::hero variant="dark"> and <x-wirekit::cta variant="dark"> now establish a dark token context for their content — no more white-on-white nested surfaces. Previously the dark variant only painted a dark band with light text; a token-surfaced child (e.g. <x-wirekit::code-block>, card, input, table) still read light-mode --color-wk-* values, rendering a near-white panel on the dark band — invisible. The dark variant now carries a dark token context so every nested token-driven surface renders dark. As a consequence a variant="dark" band now also stays genuinely dark in dark mode (it previously flipped to near-white). Light-mode appearance is unchanged; variant="accent" is unaffected (it is a colored surface, not a dark one).
  • <x-wirekit::stat animate> count-up no longer risks sticking at "0". Both start-triggers — the animateIn entrance keyframe and the standalone scroll-into-view observer — can occasionally miss on real browsers (a coalesced keyframe; an IntersectionObserver edge case on some touch browsers), which would leave the live counter frozen at "0" over the correct server-rendered value. A safety-net timer now starts the count-up if neither trigger fires, so animate always resolves to its target.
  • Muted text on <x-wirekit::hero variant="accent"> / <x-wirekit::cta variant="accent"> now clears WCAG AA on every preset. The previous muted-text formula on variant="accent" surfaces blended --color-wk-accent-fg at 72% opacity over the accent, which clears AA on dark-accent themes but dropped to ~3.4:1 on medium-lightness accents (Aurora, Soft, Material, and Cupertino — their regular accent-fg-on-accent contrast is only ~4.8–5.3:1, leaving no AA-safe muting headroom). Muted text on a colored accent surface now renders at the full --color-wk-accent-fg, so it inherits the surface's regular text contrast (≥ 4.5:1 on every preset, both light and dark). Real muting is unchanged on variant="dark" and every non-colored surface.

v2.4.2

Patch release. Bug-fix window on top of v2.4.1.

Fixed

  • <x-wirekit::badge tooltip="…"> now uses the WireKit tooltip, not the browser's native one. The badge tooltip prop previously set a native title="", so the explanation rendered as the browser's plain default tooltip — inconsistent with the rest of the library and not touch- or keyboard-friendly. It now composes WireKit's own tooltip component, so the explanation appears on hover, focus, touch, and keyboard, themed to match, and is announced to screen readers via aria-describedby. The tooltip API is unchanged; a badge without a tooltip still renders as a bare <span> with no added markup.

v2.4.1

Patch release. Bug-fix and polish window on top of the v2.4.0 baseline.

Fixed

  • Streamlined the copy-paste code in component-docs previews. The Show-Code panels on /components/sparkline, /components/charts-apex/sparklines, /components/accordion, /components/breadcrumb, /components/button, /components/hover-card, /components/icon, /components/input, /components/popover, /components/qr-code, /components/scroll-to-top, /components/spine-aware, /components/toast, and /components/tooltip now compose WireKit primitives — <x-wirekit::stats> + <x-wirekit::stat> (with the sparkline slot), <x-wirekit::card> + <x-wirekit::card.body>, <x-wirekit::grid>, <x-wirekit::row>, <x-wirekit::stack>, <x-wirekit::prose>, <x-wirekit::text>, and <x-wirekit::heading> — instead of hand-rolled <div> layout scaffolding. The rendered previews are unchanged; the code you copy is cleaner, shorter, and consistent with how the library is meant to be composed.
  • Accessible names on read-only example inputs in the docs. Several read-only example inputs — the popover share-link / share-URL fields plus a couple of search / filter fields elsewhere in the docs — now carry an aria-label, so screen readers announce them instead of an unnamed text field (WCAG 4.1.2).

v2.4.0

Minor release. New display components (stage-card, activity-row), an intent colour axis on stat and progress, deterministic avatar from-initials, badge tooltip / leadingIcon support, a md-compact size tier on inputs and buttons, and chart tooltip value formatting (valueDecimals / valuePrefix / valueSuffix). Plus reading-primitive boundary scoping, a hero tightOnMobile option, menubar / navigation-menu dropdown positioning fixes, sortable-table fixes, and a round of mobile-layout polish. Additive-only — every existing template renders identically. No back-compat breaks.

Added

  • New <x-wirekit::stage-card> component. A pipeline / kanban / roadmap stage card: an intent-coloured left stripe, a faintly intent-tinted body, an optional item-count pill (announced as "N items"), and an optional progress bar. Replaces the per-column inline-style block dashboards previously hand-rolled. role="group" + aria-label from the stage label.
  • New <x-wirekit::activity-row> component. An activity-feed / timeline / changelog row: a kind-coloured leading dot, optional bold actor, content, right-aligned timestamp, and a badge slot. The kind → colour map (commit / merge / deploy / comment / system / user) is extensible via config('wirekit.components.activity-row.kinds'); unknown kinds fall back to a muted dot. The dot is decorative; the kind is exposed as a visually-hidden label.
  • <x-wirekit::stat intent="…"> KPI-tile chrome. Setting intent (primary / success / warning / danger / info / neutral) gives the stat an intent-coloured left stripe + a faintly tinted body and turns it into a labelled role="group". Default (no intent) renders the existing plain card surface unchanged.
  • <x-wirekit::progress intent="…">. intent is now the canonical colour axis (matching badge / button / alert), adding info and neutral. variant keeps working as a back-compat alias; intent wins when both are set.
  • <x-wirekit::avatar from-initials>. Derives a deterministic, theme-independent background colour from the initials hash (AA-contrast background + white text) so the same person always renders the same colour. WireKit::avatarPaletteFor($key) exposes the same palette for custom inline chips.
  • size="md-compact" on <x-wirekit::input>, <x-wirekit::select>, and <x-wirekit::button>. A 2.25rem middle tier between sm and md for dense list/filter toolbars.
  • <x-wirekit::badge> gains tooltip and leadingIcon. tooltip sets a discoverable native title; leadingIcon renders a decorative status glyph before the label. Every badge also gains a subtle depth shadow (inset ring + faint drop shadow) so it reads less flat.
  • <x-wirekit::card.body :padded="false">. Opt out of body padding for edge-to-edge content (a flush hero image or full-bleed table).
  • <x-wirekit::dropdown> accepts an optional <x-slot:trigger> named slot. When the slot is supplied, the parent auto-wraps the trigger element in <x-wirekit::dropdown.trigger> and the default slot in <x-wirekit::dropdown.panel>, so the canonical composition no longer has to be repeated per dropdown. The explicit-sub-component form keeps working unchanged — use whichever reads better for the page. Both forms produce identical rendered HTML and ARIA wiring.
  • Reading- boundary prop on reading-progress, reading-spine, and reading-bookmark.* Default null preserves the existing viewport-pinned behaviour. Pass boundary="container" to scope the primitive to its nearest positioned ancestor instead — useful when embedding a reading surface inside a modal body, sidebar pane, preview iframe, or Livewire panel. The progress bar swaps to position: sticky; the spine and bookmark swap to Tailwind absolute. Documented under "Scoping a primitive to its parent" with the required wrapper recipe.
  • <x-wirekit::hero tightOnMobile> opt-in prop. When true on a size="lg" hero, mobile padding drops from --space-wk-section-md (5 rem) to --space-wk-section-sm (3 rem) while desktop keeps --space-wk-section-lg (7 rem) — useful for dark-variant heroes with gradient overlays where the previous 5 rem mobile padding showed as a visible dead-zone on iPhone-class viewports. No-op for size="sm" and size="md" (mobile already runs at the tightest tier).
  • New docs/strict-validation.md page. A context-matrix reference for WireKit's prop-validation gate: when does an invalid value throw vs log + fall back, how to grep the Laravel log for silent typos that shipped, the env-knob recipe to opt staging into fail-loudly mode, and a Pest test pattern for asserting strict-mode rejection. Linked into the docs sidebar between Variants & Intents and Customization.
  • <x-wirekit-chart> gains valueDecimals, valuePrefix, and valueSuffix (ApexCharts). Round raw float y-values in the tooltip to N decimal places and wrap them with a unit or currency affix — valueDecimals="2" valueSuffix=" ms" turns a hovered 50.523626895740676 into 50.52 ms. All three default to null (raw value, no affix), so existing charts are unchanged. No-op on the Chart.js adapter.
  • .wk-reading-content responsive article wrapper class. Wrap the <article> in a reading-spine composition with class="wk-reading-content" and it reserves the spine's inline-end gutter only at md+ (where the spine is visible) while using the full width below md (where reading-spine defaults to hideBelow="md" and is hidden). Replaces the hand-rolled inline max-width: calc(100% - 12rem) / padding-right: 12rem reserve, which — being an inline style — could not [@media](https://github.com/media) and so stayed applied on phones, collapsing the article to a single character per line. Override the gutter with --reading-content-spine-gutter (default 12rem).

Fixed

  • <x-wirekit::table alpine-sort> now actually reorders rows when a header is clicked. The Alpine sort read the table body from the clicked <th> instead of the table root, so it found no <tbody> and bailed out — the sort-direction arrow flipped but the rows never moved. Clicking a sortable header now sorts the rows as expected.
  • <x-wirekit::table alpine-sort> sorts date and other non-numeric columns correctly. Numeric detection used parseFloat, which read a leading number out of any string — so every ISO date (2025-01-15) collapsed to its year (2025), making a whole date column compare as equal and never reorder. Values are now only compared numerically when the entire cell is a number; dates, sizes (12px), prices ($5), and counts (3 items) fall through to locale-aware comparison, which orders ISO dates chronologically.
  • <x-wirekit::data-list> no longer renders a doubled border under the last row. The inter-row separator was an inline border-bottom on every item, including the last — which doubled against the container's own bottom border into a 2px seam. The separator now omits the final row.
  • Muted text stays legible on <x-wirekit::hero> / <x-wirekit::cta> variant="dark" and variant="accent". A nested <x-wirekit::text variant="muted"> previously resolved against the light-theme muted colour on the near-black surface — invisible. It now reads a high-contrast muted colour scoped to the dark/accent surface, auto-adapting to any theme.
  • Raw <td> / <th> cells inside <x-wirekit::table> now receive readable padding. Previously a raw table (cells not wrapped in the table.* sub-components) rendered cramped flush-to-border; raw cells now get the same padding table.td uses, complementing the existing debug-mode warning. An edge-to-edge card.body pads a flush raw table's cells too.
  • <x-wirekit::range-slider> is more usable on touch devices. The thumb grows to a comfortable 28px on coarse pointers, touch-action: none stops a horizontal drag from scrolling the page, and discrete sliders (step > 1) with a readable step count render snap tick-marks.
  • wirekit:export-json manifest no longer false-positively flags class-side public properties as required template slots. Class-based components (currently <x-wirekit-chart>) expose public properties like $alpineComponent and $chartConfig that the Blade template references as {{ $name }}. The slot-detection layer treated these as <x-slot:...> requirements, so the manifest's chart entry emitted {name: alpineComponent, required: true} — leading AI tooling to wrap chart content in <x-slot:alpineComponent> and produce broken Blade. The exclude is applied to both wirekit:export-json and the .wirekit-schema.json file written by wirekit:install.
  • <x-wirekit::tabs> tab bar is now horizontally scrollable on narrow viewports. The tablist previously used a non-scrolling inline-flex, so a layout with five or more tabs overflowed the viewport on mobile with no way to reach the off-screen tabs. The tablist now caps at the container width and scrolls (the standard mobile tab-bar pattern); tabs keep their natural label width (shrink-0 + whitespace-nowrap) instead of squishing. Desktop rendering is unchanged. Keyboard access is preserved — role="tablist" with arrow-key navigation already owns its keyboard model.
  • <x-wirekit::data-list> labels and values wrap long single tokens instead of overflowing. A <dt> carried white-space: nowrap, so a long single-word label (e.g. a long compound noun, a URL, or a file path) bled past its 33% track and pushed the row wider than its container on narrow viewports. Both <dt> and <dd> now use overflow-wrap: anywhere + min-width: 0.
  • <x-wirekit::menubar> and <x-wirekit::navigation-menu> dropdown panels now open at their trigger even inside a transformed ancestor. Both teleport their position: fixed panels to <body> (like modal / drawer / context-menu / tooltip), so a menubar or navigation-menu nested inside any element with a transform / filter / perspective / contain (which establishes a containing block for fixed descendants) no longer opens its dropdown far from the trigger. Keyboard navigation, outside-click close, and hover behaviour are unchanged.
  • <x-wirekit::sparkline> no longer clips sibling cards in a multi-column KPI grid on mobile. A block-mode sparkline now carries min-width: 0; overflow: hidden so the ApexCharts canvas's fixed intrinsic width can't floor a surrounding CSS-grid track — previously a repeat(2, 1fr) KPI grid kept both columns at the chart's ~300px width and pushed the right-hand cells off-screen on narrow viewports.
  • <x-wirekit::toolbar> wraps to a second row on narrow viewports instead of cramming. The leading (search) cluster used min-w-0, so on a phone the search field collapsed toward zero width while the filter selects and action button squeezed beside it. The leading cluster now keeps a usable minimum width (min-w-[min(100%,14rem)], growing to fill spare space) and the default-slot wrapper carries flex-wrap justify-between, so the filters and actions drop to a second row once they no longer fit beside a usable search field. Desktop layout is unchanged.

Changed

  • <x-wirekit::reading-progress>, <x-wirekit::reading-spine>, <x-wirekit::reading-bookmark> boundary prop now accepts CSS-selector strings in addition to 'container'. Pass a selector like boundary="#article" and the Alpine init logs a console.warn when no ancestor matches — runtime verification that the developer's intended ancestor exists. The default (null) and 'container' shapes are unchanged.
  • <x-wirekit::reading-shell> gains a boundary prop that pass-through to every composed primitive — set once on the shell and the entire reading surface stays inside the parent positioned ancestor.
  • <x-wirekit::dropdown>, <x-wirekit::modal>, <x-wirekit::alert-dialog> <x-slot:trigger> named-slot path now covered by both Pest tests and Playwright regression tests so the optional named-slot affordance can't silently drift.
  • <x-wirekit::icon> accessibility hardening — when the underlying SVG package source ships a baked-in aria-hidden="true" on the SVG root (blade-heroicons does this), the component now strips that attribute and re-emits exactly one resolved aria-hidden value matching the developer's intent. Fixes a screen-reader regression where icons marked informative via aria-label / role="img" were silently skipped by assistive tech.
  • IconResolver fallthrough now correctly resolves bare heroicon-family names like <x-wirekit::icon name="pencil-square"> to heroicon-m-pencil-square. Previously the fallthrough probed a binding key that has never existed in blade-ui-kit/blade-icons, so every such call threw an "Unknown icon alias" exception.
  • wirekit:doctor (wirekit:verify) gains an environment-tier silent-typo log scan. Walks storage/logs/laravel*.log for WireKit [...] ERROR / WARNING lines emitted by the prop-validation fallback path and surfaces them as a WARN with example lines. SAFE-DEGRADE at every failure mode (missing log file, non-file LOG_CHANNEL, custom log channels) — the helper NEVER blocks the doctor's exit code. Opt-out via wirekit.doctor.scan_logs config or WIREKIT_DOCTOR_SCAN_LOGS=false.
  • wirekit:export-json manifest entries carry a new component_kind field (anonymous | class) so downstream AI tooling can branch on the value when generating composition shapes — anonymous components accept <x-slot:name>; class-based components accept only constructor-mapped props.
  • Hero docs ship a new "Tight mobile padding for size='lg'" subsection demonstrating the tightOnMobile prop with a preview, plus a new tightOnMobile row on the Props table.
  • Reading docs gain a new "Scoping a primitive to its parent" subsection with a contained-surface preview block and a 3-row behaviour matrix covering null / 'container' / '<css-selector>' boundary shapes.
  • Dropdown docs gain a new "Composition forms" section walking both the quick-form (<x-slot:trigger>) and the explicit-sub-component path with previews and a do-not-mix tip.
  • Icon docs + Variants & Intents docs carry explicit cross-reference notes documenting that the four status names (info, success, warning, danger) appear deliberately in both the icon-alias namespace and the canonical intent-value enum — the shared keyword is a feature, not a namespace collision.
  • Chart docs Troubleshooting section names the silent-failure mode (chart adapter bundle not loaded at all → Alpine references an unknown factory) and contrasts it with the in-DOM advisory panel rendered when the adapter bundle IS loaded but the underlying chart library (window.ApexCharts / window.Chart) is missing. Chart.js + ApexCharts missing-library console.error now emits ONCE per page-load (deduplicated) instead of once per chart instance.

v2.3.1

Patch release.

Fixed

  • .cursor/rules/wirekit.mdc section-heading polish. Several headings have been simplified to cleaner product copy. Developers running php artisan wirekit:cursor-rules after composer update pick up the polished rules file.

v2.3.0

Minor release. First-time-integrator audit follow-up. Five surface areas touched: form-input accessibility (multi-select, range-slider), shared-prop-vocabulary aliases (feature, card, button, heading, reveal), icon-system completeness (8 new base aliases per preset + bare-name fallthrough resolution), new main max-width default + button forceLoading, two new artisan-command capabilities (wirekit:show <parent>.<child>, wirekit:verify --fix), plus a new static-analysis a11y linter wirekit:doctor:a11y. Documentation site grows the unified /blueprints/ route with /blueprints/partials/* (8 marketing-section building blocks) and /blueprints/recipes/* (10 worked-example compositions) under a fresh index page.

Added

  • ariaLabel prop on <x-wirekit::multi-select>. Optional explicit override for the screen-reader-announced label of the component's internal combobox <input>. When unset, the component auto-derives a sensible label from (in order) any passthrough aria-label attribute, then the label prop, then the placeholder, then finally the name — so the internal input is never unlabelled even if the consuming code does not supply one explicitly.
  • forceLoading prop on <x-wirekit::button>. When true, renders the spinner unconditionally and disables the button regardless of any wire:loading gate. Useful for static documentation demos, non-Livewire contexts, or stories that want to preview the loading state without a backend request in flight. The existing loading semantics are unchanged — loading=true alone still auto-detects the wire-action context.
  • max prop on <x-wirekit::main> with default "2xl". Caps the inner content width to the matching --size-wk-container-* token tier. Default "2xl" matches the existing container component default and prevents dashboards from stretching edge-to-edge on 1900 px+ monitors. Opt out with max="none" to preserve the pre-2.3.0 unbounded behaviour, or pick another tier (sm/md/lg/xl/full). The config key wirekit.components.main.max overrides the default app-wide.
  • 8 new base icon aliases in every presethome, moon, sun, book-open, sign-out, megaphone, map, file-text. The four base presets (Heroicons, Lucide, Phosphor, Tabler) now ship 34 aliases each (was 26). Every common first-install icon (theme toggle, sidebar home, sign-out button, file row) resolves without an additional preset.
  • Icon-alias fallthrough resolution. When <x-wirekit::icon name="briefcase"> references an alias that isn't in the active preset, the resolver now tries the underlying blade-icons identifier (heroicon-m-briefcase, then -o-, then -s-) before throwing Unknown icon alias. Logs an INFO line so the developer can add the alias to their preset. Catches the bug class where a developer reaches for a heroicons name that ships in blade-ui-kit/blade-heroicons but wasn't aliased by WireKit.
  • php artisan wirekit:show <parent>.<child> resolves dotted sub-component names. Pre-fix, php artisan wirekit:show card advertised Sub-components: card.body but php artisan wirekit:show card.body returned "Unknown component" because the component registry tracks only top-level components. The artisan command now resolves sub-components by reading the nested Blade file directly and extracting props via the same parser the top-level command uses. Honours --as=json so AI tooling can introspect sub-components the same way as top-level components.
  • wirekit:verify --fix self-heals missing public assets. When the doctor finds public/vendor/wirekit/wirekit.css or wirekit.js missing AND --fix is set, it proactively runs vendor:publish --tag=wirekit-assets --force and re-tests the asset paths so the verify report comes back clean. Closes the fresh-clone-fails-the-doctor gap where the .gitignored public/vendor/wirekit/ directory is empty until the publish runs. Without --fix, the existing "Run: php artisan vendor:publish ..." hint is augmented with an "Or: php artisan wirekit:verify --fix" alternative.
  • wirekit:doctor:a11y — new static-analysis a11y linter. Scans every .blade.php under a given path for three high-value WCAG-AA bug classes: icon-only <x-wirekit::button> without aria-label, role="dialog"/"alertdialog" without aria-label/aria-labelledby, role="img" without aria-label. CLI: php artisan wirekit:doctor:a11y [path] [--fail-on=error|warning|none]. Exit 1 on any finding ≥ threshold. Pair with wirekit:verify in CI to gate both integration health AND a11y in one pass.
  • tag_alias field in wirekit:export-json for class-based components. Class-based components whose canonical tag uses the single-hyphen form (<x-wirekit-chart>) now also emit a tag_alias field carrying the double-colon form (<x-wirekit::chart>). Lets downstream tool integrators that grep against the historical shape still match. Normal anonymous components have no alias — the field is omitted entirely so the schema stays minimal for the common case.
  • library prop on <x-wirekit-chart> — per-instance chart-library override. Accepts the same values as config('wirekit.charts.library') — the built-in keys "chartjs" / "apexcharts", or a fully qualified class name implementing ChartAdapter. When null (default), the chart uses whatever the global config resolves to — matching pre-v2.3.0 behaviour. When set, this chart instance binds to the named library regardless of the app default, so two charts on the same page can use different libraries. Useful when a specific chart type requires a specific library (boxplot / candlestick / heatmap / treemap / column are ApexCharts-only and throw TypeNotSupportedException against the Chart.js adapter) inside an app whose default is the other library. Backwards compatible — existing charts that don't set the prop continue to read from config exactly as before. The wrapper components <x-wirekit::chart-mixed> and <x-wirekit::sparkline> accept and forward library to the underlying chart, so mixed-library pages work via either tag shape. Example: <x-wirekit-chart type="column" library="apexcharts" ... /> renders the ApexCharts column chart even when the app's default is chartjs.

Changed

  • <x-wirekit::feature> tone prop accepts the canonical intent vocabulary. primary is now accepted as an alias for accent (same colour role), and info as an alias for soft (same tinted-accent treatment). The pre-2.3.0 names (accent, neutral, soft, success, warning, danger) are unchanged. Developers copying intent="primary" from a <x-wirekit::button> no longer get a 500 on <x-wirekit::feature>.
  • <x-wirekit::heading> size prop accepts md / 4xl / 5xl. The size enum now reads sm | md | base | lg | xl | 2xl | 3xl | 4xl | 5xl. md is an alias for the existing base tier (matches the rest of the WireKit's middle-tier convention); 4xl (2.25 rem) and 5xl (3 rem) are new tiers for hero copy. The --text-wk-4xl and --text-wk-5xl design tokens are shipped in dist/wirekit.css.
  • <x-wirekit::card> accepts variant="outline" as alias for outlined. <x-wirekit::button> accepts surface="outlined" as alias for outline. Same visual treatment, two spellings — the WireKit's pre-2.3.0 vocabulary diverged. Both forms now work on both components; canonical spellings (outlined for card, outline for button) are unchanged.
  • <x-wirekit::profile> avatar prop accepts string | array{src?, initials?, alt?}. Pre-fix the prop required a URL string; passing an array crashed with a htmlspecialchars(): Argument #1 ($string) must be of type string error. Now matches the shape that message.author and the avatar primitive already accept. The initials fallback renders a rounded-full muted-background span with aria-label set to the profile's name so screen readers still announce who the profile belongs to.
  • <x-wirekit::reveal> preset prop accepts fade-up / fade-down / fade-left / fade-right. Aliases for slide-up-in / slide-down-in / slide-left-in / slide-right-in. Matches the Tailwind UI / Framer Motion naming convention designers reach for first. Existing slide-*-in callers unaffected.
  • animateIn prop on every marketing component accepts the same fade-* aliases. Card, feature, hero, stat, callout, alert, empty-state, cta, and footer all route through WireKit::resolveAnimateIn(), which now resolves the four Tailwind-UI shorthand aliases identically to <x-wirekit::reveal>. Pre-fix, <x-wirekit::card animateIn="fade-up"> rejected the alias as an unknown enum value (silent no-op in production, InvalidArgumentException in debug). Now byte-identical resolution: fade-upslide-up-in on every surface. Existing callers unaffected.
  • <x-wirekit::sidebar.item> consumes data-current as fallback for :active. Livewire 4 emits data-current automatically on wire:navigate links. Pre-fix, sidebar.item ignored the attribute and required the developer to manually pass :active="request()->is('posts*')", duplicating routing knowledge already encoded in routes/web.php. The explicit :active prop still wins; if unset and the attribute bag carries data-current="true" / data-current="page" / data-current="1", the item highlights automatically.
  • /blueprints/ documentation route — partials/* and recipes/* sub-sections. The /blueprints/ index page lists every entry with per-card previews and copy-paste guidance, organised under two sub-sections:
    • /blueprints/partials/* — 8 drop-in marketing-section building blocks: hero, features, pricing, testimonials, logo-cloud, FAQ, CTA, footer. Each partial is a single Blade snippet you paste into a parent layout and customise.
    • /blueprints/recipes/* — 10 worked examples composing several WireKit primitives into a complete UI shape: long-form article shell, documentation reader, on-page TOC, marketing landing page (with TOC variant), hero-with-code-aside, live KPI strip, stat-with-sparkline, feature-with-numbered-marker, toolbar-filter-bar.
  • <x-wirekit::brand> accepts <x-slot:logo> for custom logo markup. Pre-fix the prop required a URL string for <img src>; passing a slot stringified the slot HTML inside the <img src> attribute and produced broken DOM output (visible attribute-fragment text). The component now detects a ComponentSlot instance and renders the slot directly. URL-string callers unaffected.

Fixed

  • <x-wirekit::multi-select> internal combobox <input> now always carries an aria-label. Previously, the parent <x-wirekit::field label="…">'s emitted <label for="$id"> did not reach the internal combobox input (whose id is $id-input, not $id), so screen readers + axe's label rule reported an unlabelled form element. The component now synthesises an aria-label via the fallback chain above and emits it on the internal input. WCAG 2.1 AA-compliant for any developer that has been wrapping multi-select in a <x-wirekit::field> with a label — no developer-side change required. Backward compatible.
  • <x-wirekit::range-slider> thumb-circles now sit vertically centred ON the track line. Pre-fix, the two thumb <div>s were siblings of the track div, so their top: 50%; transform: translateY(-50%); resolved relative to the FULL component wrapper (track + edge labels + value display) and the thumbs visually dropped BELOW the track line. The thumbs now live INSIDE the track div, so their vertical centering resolves to the 8 px track height and the circles sit pixel-centred on the line.
  • <x-wirekit::range-slider> edge labels now show the slider BOUNDS, not the current values. Pre-fix, the bottom-of-component value display rendered minVal / maxVal (the current handle positions) at the container edges via justify-between. The result was that the visible label "20" sat at position 0 % of the container, while the thumb representing value 20 sat at its proportional position (e.g. 6.7 % on a 0–300 range), and the two looked disconnected — and when the values matched the bounds exactly, the label and the thumb collided at the same pixel. Now: the edge labels render the constant slider min / max ({{ $min }} and {{ $max }}), and the current values surface as a tooltip-style badge ABOVE each thumb that tracks the handle horizontally as it moves. Screen-reader users still get a live announcement of the current range via an aria-live="polite" text node ("Range: 20 to 200") that updates on drag.
  • Prop validation no longer crashes the whole page in dev HTTP requests. Pre-fix, <x-wirekit::heading size="4xl"> (or any other invalid prop value) threw InvalidArgumentException in APP_DEBUG=true and the whole blade view 500'd. Now: strict-mode is split into two paths. CLI / Pest / wirekit.validation.throw_on_invalid=true still throws (fail-fast — the signal a dev wants in a test or artisan command). HTTP dev requests log at ERROR level and render with the first-allowed fallback so a single prop typo doesn't take down the entire page. The dev still sees the typo loudly in the log; iteration continues without a request-response restart.
  • <x-wirekit::button> warns at log level on unknown prop keys. Pre-fix, <x-wirekit::button variant="ghost"> (the prop is surface, not variant) was silently swallowed — the button rendered with default surface="filled" and the developer got no signal that their intended ghost treatment didn't apply. The new WireKit::warnUnknownProps() helper logs a Levenshtein-ranked Did-you-mean warning for any unknown key not in the canonical Blade-passthrough allowlist (aria-*, data-*, wire:*, x-*, @*, :*, plus reserved HTML attrs). Public helper — opt-in for every component that wants the warning; button and main already adopt it.
  • <x-wirekit::reading-spine> hover-boundary flicker. Cursor jitter (1–2 px) at the spine's top or left edge fired alternating mouseenter/mouseleave events on every frame, toggling the spine expanded ↔ collapsed once per frame and producing a visible flicker. Hover-collapse now debounces by 120 ms — a re-entry within that window cancels the pending collapse, absorbing the jitter. Expand still fires immediately on initial hover, so the responsive direction is unchanged. Focus-driven expansion (expandOnFocus / collapseOnFocus) is unaffected.
  • <x-wirekit::reading-spine> implicit horizontal scrollbar inside the spine. The spine carried overflow-y-auto but no explicit overflow-x. Per CSS spec, setting one axis to auto while the other defaults to visible computes BOTH axes to auto — so long white-space: nowrap heading labels inside the spine produced a thin horizontal scrollbar gutter even though the ellipsis truncation already clipped them visually. Pinned to overflow-x-hidden overflow-y-auto for true vertical-only scroll.
  • Themed scrollbar (.wk-scrollbar) applied across every component with an internal scroll region. <x-wirekit::modal> body, <x-wirekit::drawer> body, <x-wirekit::alert-dialog>, <x-wirekit::code-block>, <x-wirekit::combobox> dropdown list, <x-wirekit::command-palette> overlay + inner list, <x-wirekit::kanban> (horizontal row) + <x-wirekit::kanban-column> (vertical list), <x-wirekit::main>, <x-wirekit::reading-spine>, and <x-wirekit::reading-toc> previously showed the OS-default scrollbar (thick on Windows, hidden-until-scroll on macOS). Each now applies WireKit's themed scrollbar — thin width, theme-aware thumb / track colours, dark-mode reactive. <x-wirekit::prose> <pre> children inherit the same treatment via a .wk-prose pre CSS rule (no template change required). <x-wirekit::multi-select>, <x-wirekit::scroll-area>, and <x-wirekit::table> already carried .wk-scrollbar and are unchanged. Override the colours / width via --color-wk-scrollbar-thumb, --color-wk-scrollbar-track, --color-wk-scrollbar-thumb-hover, --size-wk-scrollbar in :root {}.

v2.2.0

Minor release. Form-input upgrades (range-slider Livewire integration, input HTML5 props, number-input grid-snap), entrance-animation polish (zero-flash + delay-prop fix universal), AI-tooling schema fields (sub_components, slots required flag, [@example](https://github.com/example), ClassPropsExtractor for class-based components), two new artisan commands (wirekit:fonts, wirekit:icons), and three new docs pages (prop-naming-conventions, overlays/events, recipes/on-page-toc).

Added

  • <x-wirekit::range-slider> first-class wire:model integration. Pre-fix wire:model="priceRange" on the component tag was silently dropped into the outer <div>'s attribute bag — Livewire only watches input/select/textarea elements, not divs. Dragging the slider didn't update the Livewire property. Now: the component detects any wire:model* directive on the tag, strips it from the outer bag (no double-render), and re-emits it on the two hidden inputs as wire:model="propName.min" / wire:model="propName.max". Developer declares public array $priceRange = ['min' => 20, 'max' => 80];; dragging either handle updates $priceRange['min'] / $priceRange['max'] live. All modifiers (.live, .lazy, .debounce.500ms, .blur) flow through unchanged. docs/components/range-slider.md "Livewire Integration" section rewritten to show the array-property pattern.
  • docs/extending/prop-naming-conventions.md — reference page for the three semantic-modifier prop families (intent / variant / tone). Documents the contract behind each name, the canonical 7-value intent enum (primary / accent / neutral / danger / success / warning / info), and a per-family decision tree for component authors. Cross-component example table covers <x-wirekit::button> (intent), <x-wirekit::alert> / <x-wirekit::card> (variant), and <x-wirekit::feature> (tone). Single source of truth for "which prop name should my new component use?" — closes the historical gap where the three families were inferred from reading the source. Registered under the "Extending" group in the docs navigation.
  • docs.wirekit.app/recipes/on-page-toc — canonical recipe for the right-edge sticky sidebar Table-of-Contents pattern. Pairs <x-wirekit::reading-spine> and <x-wirekit::reading-progress> to document the in-page TOC shape used by Stripe, Linear, Vercel, and shadcn docs. Covers the sidebar shape end-to-end (canonical wiring, CSS-variable knobs for expanded width and sticky offset, integration with a sticky <x-wirekit::brand-bar>, breadcrumb composition); cross-links to the marketing-landing-toc recipe for the horizontal strip variant. Targets the <main> selector by default so H2 / H3 headings auto-populate the spine without manual wiring.
  • docs/overlays/events.md — canonical event-vocabulary reference page. Complete matrix of every overlay component's event names + payload shapes (modal / drawer / alert-dialog / command-palette / tour / toast-region — 6 overlays). Three sections: the matrix table, Alpine $dispatch + Livewire $this->dispatch() examples per event family, and "Conventions + footguns" enumerating the 4 historical irregularities (hyphen-not-colon, tour's name-in-event-name shape, toast's variant-vs-intent divergence, fire-and-forget show events). Single source of truth for AI / IDE tooling that previously had to discover this by reading resources/js/components/*.js. Registered under a new "Overlays" group in the docs navigation.
  • <x-wirekit::alert-dialog.cancel> sub-component — pre-wired close button for alert-dialogs. Analogous to <x-wirekit::modal.close>, but specific to the destructive-action confirmation dialog. The wrapping <div> carries x-on:click="close()" (the parent's wirekitAlertDialog Alpine x-data exposes the method); slot defaults to a <x-wirekit::button intent="neutral" surface="filled">Cancel</x-wirekit::button> so the typical case is a one-tag affair. Override the label by passing slot text (<x-wirekit::alert-dialog.cancel>Back</x-wirekit::alert-dialog.cancel>) or wrap your own <x-wirekit::button> for full control (custom variant / size / icon). Replaces the manual-wiring pattern (x-on:click="$dispatch('wirekit-alert-dialog-close', { name: '...' })") — that pattern still works but is no longer required.
  • wirekit:make recipe:<name> scaffolds for all 9 documented recipes. Each recipe: template generates a Livewire class + Blade view from a shipped stub mirroring the corresponding docs.wirekit.app/recipes/<name> page's structural composition. Class names derive PascalCase from the kebab-case recipe slug (marketing-landing-pageMarketingLandingPage). Available recipes: documentation-reader, feature-numbered-marker, hero-with-code-aside, live-kpi-strip, long-form-article, marketing-landing-page, marketing-landing-toc, stat-with-sparkline, toolbar-filter-bar. Each generated view ends with a comment cross-linking to docs.wirekit.app/recipes/<name> for the full reference. Unknown recipes fail with a Levenshtein-ranked Did-you-mean hint.
  • slots schema field now flags required slots. Slot entries in wirekit:export-json / .wirekit-schema.json / wirekit:show were previously a flat list of names — they didn't tell developers (or AI tooling) which slots were OPTIONAL (guarded by [@isset](https://github.com/isset)) and which were REQUIRED (referenced bare via {{ $name }}). The shape now reads [{name: 'trigger', required: true}, {name: 'header', required: false}, ...] per slot. Detection heuristic: an [@isset](https://github.com/isset)($name) / isset($name) guard marks the slot as optional; a bare {{ $name }} / {!! $name !!} / $name->method() reference without an enclosing guard marks it as required. The fix closes the popover / hover-card / context-menu gap — all three reference {{ $trigger }} directly and 500 with Undefined variable $trigger when developers omit the slot; the manifest now reports trigger as required: true.
  • Pushery\WireKit\Support\ClassPropsExtractor — public Reflection-based prop extractor for class-based Blade components. Companion to PropsParser. Class-based components like <x-wirekit-chart> (registered via loadViewComponentsAs(...)) don't have an [@props](https://github.com/props)([...]) block — the prop surface IS the constructor signature. ClassPropsExtractor::extract(string $className) walks the constructor parameters via Reflection and returns the same shape PropsParser emits, so downstream callers don't branch. Constructor parameter names are kebab-cased for Blade-attribute compatibility (wireStreamwire-stream); type hints stringify to their declared form (string, ?string, int, bool, array, union types). Default values stringify as PHP literals matching what PropsParser captures. ComponentRegistry::extractProps('chart') now returns 11 props (was empty). The schema entries from wirekit:export-json / .wirekit-schema.json / wirekit:show chart all carry the full constructor surface — AI tooling and IDE extensions see chart's type / labels / datasets / wire-stream / etc. instead of the previous empty props: [].
  • [@example](https://github.com/example) "..." annotations in [@props](https://github.com/props) blocks surface as a new examples field in the schema. PropsParser now reads // [@example](https://github.com/example) "value" annotations from the trailing same-line comment after a prop's comma and exposes them as a list<string> field on every schema entry next to name / default / comment. Use for props whose accepted value-shape is non-obvious from the default alone — the canonical first user is <x-wirekit::grid> cols, which accepts a Tailwind-style space-separated string ("1 md:2 lg:4"); the default 1 doesn't telegraph that. Multiple [@example](https://github.com/example) annotations per comment are all captured; backslash-escaped quotes inside the value are supported ([@example](https://github.com/example) "Press \\"Enter\\" to confirm"). Empty array (not null) when no annotation present, so the field is type-stable across the catalogue.
  • wirekit:icons artisan command — list every icon alias shipped with WireKit, grouped by preset. Each section shows the alias-count summary, an [active] / [opt-in] indicator against the current wirekit.icons.preset / wirekit.icons.presets config, and every alias → Blade-Icon identifier mapping. Same API shape as wirekit:list / wirekit:fonts (--preset=..., --as=count|presets|aliases|json, --format= alias, Levenshtein-ranked Did-you-mean on unknown preset). The --as=aliases flavour emits a unique alphabetised alias list across the (optionally filtered) preset set — useful for "do any presets define bolt?" lookups. JSON output per preset: {key, count, active, requires, aliases} so AI tooling sees the active-vs-opt-in distinction without re-reading config. The pre-existing IconResolver::availablePresets() PHP-side helper is unchanged; the CLI is a thin discovery wrapper. Also: the bolt example in docs/components/icon.md's Sizing section is replaced with search (which is in the default heroicons preset) — bolt lives only in heroicons-marketing and required opt-in to render.
  • wirekit:fonts artisan command — list every font preset shipped with WireKit, grouped by category. Mirrors wirekit:list's API surface (--category=sans|serif|mono, --as=count|slugs|categories|json, --format= alias, Levenshtein-ranked Did-you-mean on unknown category). Each row shows the preset key, label, and font-family; the keys map 1:1 to the values accepted by wirekit:install --font={key}. Previously the only way to discover the preset list was via tinker or a typo'd install-error path. New PHP-side surface: Pushery\WireKit\Fonts\FontRegistry::all() / category() / get() (already existed; the CLI now wraps it).
  • sub_components array on every entry in wirekit:export-json + .wirekit-schema.json. Previously only wirekit:show <name> surfaced the sibling-directory .blade.php files (e.g. card.body, card.header, card.footer). The JSON-manifest paths read by AI tooling, IDE extensions, and the .wirekit-schema.json written by wirekit:install now carry the same list — first-class field next to props and slots. Sorted alphabetically; skips index.blade.php (Laravel's anonymous-component index file). Components with no sibling directory get an empty array (not null) so the field is type-stable. Closes the "card looks empty because the body wrapper is invisible to tooling" gap.
  • <x-wirekit::tabs> accepts items as array-of-objects in addition to the legacy keyed-assoc form. Two list-input shapes now coexist on the component: ['profile' => 'Profile'] (compact, ergonomic for static inline data) AND [['key' => 'profile', 'label' => 'Profile']] (matches typical API responses, easier to compose from a Livewire [@computed](https://github.com/computed) property). The component normalises both at the template edge — pick whichever your data source already produces. Both shapes produce byte-identical rendered HTML (same aria-controls / id wiring, same Alpine bindings) so screen-reader behaviour is stable across the two. When the label field is missing in array-of-objects form, the key is used as the visible label.
  • <x-wirekit::input> HTML5 form-state props in [@props](https://github.com/props). Five props now declared explicitly: required, disabled, readonly, autocomplete, placeholder. Previously these only worked via attribute-bag passthrough (<x-wirekit::input required>) — invisible to the schema, AI tooling, and IDE autocomplete. The required prop additionally flows through to the associated <x-wirekit::label> so its required indicator (*) renders without manual wiring. Back-compat preserved: the attribute-bag passthrough still works exactly as before, and mixing the two forms (:required="true" required) produces a single required token on the underlying <input> — guarded by an explicit regression test.
  • <x-wirekit::profile> interactive prop for dropdown-trigger composition. Profile previously rendered a presentational <div> with no focusable child. Wrapping it in <x-wirekit::dropdown.trigger> (the canonical user-menu pattern) opened the menu on mouse-click but was unreachable by keyboard — Tab skipped past the profile entirely because the trigger's focusable-descendant search returned null. New interactive prop (default false) emits role="button", tabindex="0", Enter / Space keyboard handlers synthesising a click, plus a focus-visible ring matching the canonical button focus state. Default interactive=false preserves the pre-existing presentational <div> byte-for-byte. New "Interactive profile inside a dropdown trigger" preview block on the docs page shows the canonical composition.
  • <x-wirekit::brand> mobileLogo + mobileBreakpoint props for responsive logo swap. A wide wordmark (/brand/wirekit-wordmark.svg) on a brand-bar overflows narrow viewports — mobileLogo pairs with a compact mark (/brand/wirekit-mark.svg) that renders below the chosen Tailwind breakpoint, with the wide wordmark taking over at + breakpoint. mobileBreakpoint accepts sm / md / lg / xl, default sm (640 px). When mobileLogo is null, behaviour is unchanged (single <img> at every viewport — back-compat). Both images carry the same accessibility shape (alt="" + aria-hidden="true"); the <a>'s aria-label provides the accessible name. Invalid mobileBreakpoint values throw in debug, fall back to sm in production via StrictnessGate. Six tests cover back-compat (single-logo unchanged), default breakpoint swap, custom breakpoint, invalid value throw, mobileLogo-without-main-logo no-op, accessibility (two images both decorative + aria-label preserved).
  • Pushery\WireKit\Support\StrictnessGate — public single source of truth for runtime validation strictness. Central decision behind WireKit::validateProp() (component-level prop validation) and IconResolver (icon-alias / preset lookups): strict mode throws InvalidArgumentException with a Did-you-mean hint, lenient mode logs a warning (now WITH the actual fallback value annotated) and returns the first allowed value (or a caller-supplied override via the new fallback parameter). StrictnessGate::isStrict() returns the current decision; StrictnessGate::enforce($context, $key, $value, $allowed, fallback: null) is the canonical entry point; StrictnessGate::formatMessage(...) exposes the message-builder so callers that throw their own typed exception (like IconResolver) inherit identical wording.
  • wirekit.validation.strict config key (env WIREKIT_STRICT_VALIDATION) — explicit override of the strict-vs-lenient decision. null (default) keeps the historical behaviour (strict in APP_DEBUG=true, lenient in prod). true forces strict everywhere — useful for CI / staging hardening. false forces lenient everywhere — useful for snapshot CI runs that want to assert rendered output regardless of prop typos. Documented in config/wirekit.php with a per-value behaviour matrix.

Changed

  • Integration Guide URL moved from /integration to /getting-started/integration. The guide is conceptually a sub-page of Getting Started — installation walkthrough, optional dependencies, asset publishing, verification checklist — so the URL now reflects that hierarchy. Every in-docs cross-link has been updated in lockstep (10 inbound references across getting-started.md, cli-reference.md, livewire-starter-kit.md, components/resizable.md, extending/console-output-baseline.md, cli-reference/wirekit-doctor.md). External bookmarks pointing at the legacy /integration URL will need updating; the new URL is the canonical location going forward.
  • wirekit:doctor now WARNs when 'charts.library' => 'chartjs' is configured but resources/js/app.js is missing the Chart.register(...registerables) registration. Catches the #1 first-run Chart.js gotcha upstream: npm install chart.js + 'chartjs' in config IS NOT enough — the JS-side import { Chart, registerables } from 'chart.js'; Chart.register(...registerables); must be in app.js or the chart component renders + mounts but draws nothing (Chart.js logs a friendly console.error and gives up). The doctor's WARN prints the actionable snippet inline. Comments stripped before scanning so a // Missing: Chart.register(...) hint comment doesn't false-pass the detection. getting-started.md Optional Features section gains an explicit three-step callout (config + npm + JS-side registration) cross-linked to /integration#optional-dependencies.
  • <x-wirekit::table> warns in debug mode when plain HTML descendants (<thead> / <tbody> / <tr> / <th> / <td>) are passed inside the slot instead of the <x-wirekit::table.*> sub-components. Same composition-required-for-padding pattern as <x-wirekit::card>: the bare <table> only carries the outer styling — padding, row dividers, stripe + hover wiring live on the sub-components. Dropping raw HTML inside compiles cleanly but produces a visually-broken table with no error. The console.warn now surfaces the silent breakage with a link to the canonical composition. Production stays silent. All 5 sub-components (table.head / table.body / table.row / table.th / table.td) now emit a data-wk-table-* marker on their root element — used by the detector to distinguish "wrapped" from "raw". Markers are inert at runtime; they exist purely as detection signals.
  • <x-wirekit::tabs> warns in debug mode when wire:model* is passed on the component tag. Pre-fix the wire:model attribute was silently dropped into the outer <div>'s attribute bag (Livewire only watches <input>, <select>, <textarea> — not divs). Tabs are client-only Alpine state; clicking a tab mutates a private x-data="{ active: '...' }" scope and does NOT update Livewire state. The console.warn now surfaces the silent-breakage with a link to docs.wirekit.app/components/tabs for the named-slot contract. Production stays silent (no console.warn). The component's behaviour is unchanged — the warn is observability-only. Docs page rewritten with a "Contract: named slots, NOT wire:model" section under the H1 explaining the client-only state model, the named-slot pattern with a complete example, and the @computed-property workaround for server-side initial state.
  • <x-wirekit::alert-dialog> ESC key always closes, even when dismissible="false". Pre-fix the dialog was completely keyboard-locked when non-dismissible — ESC inert, backdrop click inert, Cancel button wasn't pre-wired. The only escape was page reload. Now: backdrop click stays inert on non-dismissible (the safety-strict half — don't approve destructive action by a stray click), but ESC always fires the close event. Keyboard users always have an escape hatch. Implementation: createOverlay gained an escapeAlwaysCloses option; alert-dialog passes true; focus-trap uses escapeDeactivates: dismissible || escapeAlwaysCloses. JS bundles rebuilt.
  • <x-wirekit::replay-button> removed from the README components table and the wk-replay-button row in the Public CSS API catalog no longer carries a docs link. The component remains fully callable in the package (the <x-wirekit::replay-button> tag still renders, the wk-replay-button CSS class still ships in dist/wirekit.css) — replay-button is preview-chrome infrastructure rather than a developer-pickable primitive, so it's no longer surfaced in the catalog tables.
  • IconResolver exception messages now include Did-you-mean hints on close-typo alias AND preset names. Previously Unknown icon alias 'clse' listed only the available aliases (long list); now reads Unknown icon alias 'clse'. Did you mean: close, eye, plus? Available aliases: … — same Levenshtein-ranked suggestion contract as WireKit::validateProp() and every wirekit:* CLI surface. Same enhancement for Unknown icon preset 'heroicns' (suggests heroicons). Behaviour-preserving — the InvalidArgumentException class and the leading message prefix are unchanged so existing exception-catching tests stay green.
  • WireKit::validateProp() lenient-mode log message now annotates the fallback value. The pre-existing log line read WireKit [button]: Invalid variant "purple". Allowed: primary, secondary. — readers had to know the fallback contract to interpret what the component actually rendered. Now reads … Falling back to "primary". so prod log readers see the concrete substitution without consulting the source. The new annotation flows automatically from StrictnessGate::enforce().
  • wirekit:show chart / wirekit:list --format=json / wirekit:export-json / wirekit:export-api-map now emit the class-based Blade tag form <x-wirekit-chart> (not the broken anonymous form <x-wirekit::chart>). The chart component is registered class-based via loadViewComponentsAs(...) so the working tag is the prefixed-class shape; the anonymous Blade file at resources/views/components/chart.blade.php exists for the class-based path to render through but reaching it via <x-wirekit::chart> hits an undefined $alpineComponent variable and 500s. New ComponentRegistry::tag(string $name): string helper is the single source of truth, with a small per-component override map (currently only chart). Every CLI surface that prints a "Tag:" field reads from it, so every "what tag do I use?" report now prints the WORKING form.

Fixed

  • <x-wirekit::file-upload> file list now visibly separates from the dropzone above it. Pre-fix the <ul> carried an inline style="list-style: none; margin: 0; padding: 0;" (required so the bullet-list cleanup works in the sandbox-iframe rendering context where Tailwind classes don't resolve) AND a class string with mt-[var(--padding-wk-y-sm)] (the intended dropzone gap). Inline margin: 0 wins over the class via CSS specificity, so the gap was always zero and the file list sat flush against the dropzone's bottom edge. The spacing BETWEEN file rows (driven by gap-[var(--space-wk-sm)] on the flex parent, not by per-child margin) was unaffected, which made the bug look like "spacing works between files but not against the dropzone above". The fix migrates the dropzone gap into the inline declaration (margin: var(--padding-wk-y-sm) 0 0 0), so the gap resolves in BOTH rendering contexts — developer apps and sandbox iframes both load dist/wirekit.css, so the CSS variable resolves identically.
  • <x-wirekit::reveal> delay prop silently dropped whenever the caller also passed an inline style="…" attribute. The template emitted TWO style= attributes on the root <div> — one from $attributes->merge(...) carrying the caller-passed value, and one from a separate [@if](https://github.com/if)($delayValue) style="..." directive carrying animation-delay: {value}. Per HTML5 parsing rules, duplicate attributes on the same element resolve to the FIRST and silently drop the second, so the animation-delay was never honoured whenever any caller-supplied inline style was present. Every gallery preview on docs/animations.md passes style="--wk-stagger-step: 0ms", so the bug was universal on the gallery and on every developer composition that passed inline style for any reason — delay="lg" / :delay="1000" / etc. all rendered but had zero visual effect. The fix composes caller-style + internal-animation-delay into a single attribute via a [@php](https://github.com/php)-block merge before the <div>, so the root now carries exactly one style="caller-styles; animation-delay: …" attribute and the delay actually takes effect.
  • Every <x-wirekit::reveal> and animateIn-driven entrance animation (<x-wirekit::hero animateIn>, <x-wirekit::card animateIn>, <x-wirekit::feature animateIn>, <x-wirekit::cta animateIn>, <x-wirekit::stat animateIn>, <x-wirekit::callout animateIn>, <x-wirekit::alert animateIn>, <x-wirekit::empty-state animateIn>, <x-wirekit::footer animateIn>) no longer flashes when the element is already in the viewport at page-load. Pre-fix the animation-fill-mode: both on [class*='wk-animate-'] included backwards, which snapped the element to its keyframe from state (opacity 0) the instant Alpine added the animation class. For elements not yet in viewport this was invisible — the element wasn't visible anyway. For elements ALREADY in viewport at page-load (the marketing-landing-page hero, for example), the IntersectionObserver fired almost immediately on Alpine init, the class arrived, and the browser visibly snapped the element from opacity: 1 (no class) to opacity: 0 (from-state) before animating back. User perceived a brief "flash to invisible → 300 ms fade-in" — the flicker. New CSS rule in dist/wirekit.css pre-hides any element with x-data="wirekitAnimate('*-in')" and no wk-animate-* class yet, scoped to viewport-trigger only (click and manual triggers are explicitly excluded so click-to-animate cards stay visible until clicked). The :not([class*="wk-animate-"]) clause deactivates the rule the moment Alpine fires; the keyframe from opacity matches the pre-hide opacity, producing a clean entrance with no snap. Pure CSS fix — no Blade or JS code changes — upgrades every existing reveal / animateIn usage transparently.
  • <x-wirekit::reading-toc> link horizontal padding now follows the design-token grid (--padding-wk-x-sm, 10px) instead of the hardcoded px-2 (8px) Tailwind atom. The link's px-2 py-1 declaration was a pre-token-system convention left over from the component's first authoring — never reconciled with the project's --padding-wk-x-* scale. The 2px offset misaligned the active-link highlight chrome with the surrounding content-padding column (every other prose container sits on the 10px grid), reading as "the strip is 2 px out". Now px-[var(--padding-wk-x-sm)] py-[var(--padding-wk-y-xs)] — 10px × 4px — so the link's left edge lands on the same vertical line as the prose paragraphs beneath the strip. Y-axis kept at the equivalent token (y-xs = 4px) to preserve the strip's overall height.
  • <x-wirekit::number-input> stepper buttons now snap to the step grid instead of skipping values when the starting value is off-grid. Pre-fix value=1.78 step=0.1 clicked + jumped to 1.9 (skipping 1.8) because the Alpine handler added step to the current value (1.78 + 0.1 = 1.88) then ran toFixed(precision) which rounded 1.88 → 1.9. The intermediate grid point 1.8 was unreachable. The fix snaps to the step grid anchored at min (falling back to 0 when no min is set): increase goes to the next grid point ABOVE the current value (1.78 → 1.8), decrease goes to the next grid point BELOW (1.78 → 1.7) — matching the W3C native <input type=number> stepper contract. A 1e-10 tolerance absorbs binary-float drift so on-grid values like 1.8 (which can be stored as 1.7999999998) still advance to the correct next step. Anti-regression test it('emits the precision/round helpers + grid-snap stepper wiring in Alpine x-data') pins the new shape.
  • <x-wirekit::file-upload> file-row layout now pushes size + remove button to the right edge with proper horizontal padding. Pre-fix the filename, size, and × remove button clustered on the LEFT of each row leaving a large empty area on the right, AND the row's all-sides p-[var(--padding-wk-y-xs)] declaration used the tight Y-axis token for horizontal padding too — content felt cramped against the row's left edge with no breathing room. Filename now carries flex-1 so it grows to fill available space (pushing the size + X to the right edge in the standard file-uploader UX shape), and padding split into px-[var(--padding-wk-x-sm)] py-[var(--padding-wk-y-xs)] so horizontal breathing room is generous while vertical stays compact. File rows carry variable-length content, not nav-chip text, so the asymmetric padding shape is correct.
  • <x-wirekit::stat animate animateIn="…"> race condition that left random counters stuck at "0" on hard refresh. When both animate and animateIn were set, the Blade template wrapped the inner counter root in an outer <div x-data="wirekitAnimate('…')"> carrying the entrance keyframe (e.g. wk-slide-up-in: translateY(1rem)translateY(0)). The entrance keyframe shifted the inner element's geometry for ~300 ms. The inner counter's own IntersectionObserver (threshold: 0.4) could fire while the entrance keyframe was active — the shifted bounding box pushed the inner below threshold, the callback returned without starting the counter, and the stat stayed at "0" indefinitely. The race was random per card because it depended on Alpine mount-order vs paint-timing; no console errors and no determinate reproduction in a static test environment. Now: when the entrance wrapper is present, the counter listens for animationend on the outer rather than running its own IntersectionObserver — the outer's wirekitAnimate plugin already owns scroll-into-view via its own observer, so the counter just needs a deterministic "start" signal that doesn't depend on transform-affected geometry. Standalone counters (no entrance wrapper, the dominant deployment shape) keep the IO behaviour unchanged. The counter-tick logic was factored into a shared runCounter closure so both paths call the identical ease-out-cubic-over-1.2 s tick.
  • <x-wirekit::tour.step> index prop now auto-assigns sequentially within its parent <x-wirekit::tour>. Pre-fix every step defaulted to index=0, so every step rendered with data-wk-tour-step="0". The tour's start() correctly counted 3 steps, next() incremented currentStep to 1, then _positionStep() queried [data-wk-tour-step="1"] — found nothing → all step divs hidden because none match currentStep === 0 anymore. Tour appeared to "break" after the first Next click — actually all steps just disappeared. Now: index defaults to null; when null, the step picks the next integer from a per-render counter (Pushery\WireKit\Support\TourStepCounter) that the parent tour component resets at the top of its [@php](https://github.com/php) block. Steps with explicit :index="N" bypass the counter (the developer's choice; the counter only advances on next() calls). Multiple tours on the same page each restart numbering from 0 — Alpine scopes isolate per-tour currentStep. The natural usage pattern — just list the steps, no :index props — now works correctly.
  • <x-wirekit::range-slider> value-display spans referenced undefined Alpine variables. The two <span x-text="minVal"> / <span x-text="maxVal"> elements rendered AFTER the </div> that closed the x-data scope — every page using the slider flooded the browser console with Alpine Expression Error: minVal is not defined (twice per render) and the displayed numbers never updated when the user dragged. The slider handles still rendered + dragged correctly because their bindings sit on Alpine-scoped attributes (:style, [@pointerdown](https://github.com/pointerdown)) within the x-data root; only the read-out spans were affected. Now restructured so the value-display row sits INSIDE the x-data root, between the max-thumb <div> and the scope-closing </div>. Regression test depth-counts <div> opens / closes through the rendered HTML and asserts both x-text substrings appear before the matching close — catches future structural regressions.
  • <x-wirekit::badge intent="accent"> threw Invalid intent "accent" in debug mode (broke live previews on docs.wirekit.app/recipes/marketing-landing-page + docs.wirekit.app/components/hero after v2.1.x). accent was missing from the badge's validateProp allowed list — a regression vs the canonical 7-value intent enum documented in docs/extending/prop-naming-conventions.md. Now restored as a first-class intent with a high-contrast filled rendering (bg-[var(--color-wk-accent)] + text-[color:var(--color-wk-accent-fg)]) — the "stand out against surrounding chrome" variant used on CTA hero pills, pricing-card "Most popular" markers, and marketing "New" eyebrows. Dot indicator uses accent-fg for contrast on the filled bg. Lockstep enforcement scans every <x-wirekit::badge intent="X"> literal in the docs and asserts X appears in the validateProp allowed list — catches future drift in either direction (intent enum change OR docs author using a fresh intent name).
  • Animation-emitting components now uniformly emit data-replayable="true" when their animateIn prop is set — <x-wirekit::card> / <x-wirekit::feature> / <x-wirekit::hero> / <x-wirekit::cta> / <x-wirekit::footer> previously emitted the x-data="wirekitAnimate(...)" hook without the replay marker, so docs.wirekit.app's preview-chrome replay button missed them. Existing emitters (<x-wirekit::reveal>, <x-wirekit::stat animate>, <x-wirekit::chart>) are unchanged. The attribute is otherwise inert; on developer apps without a replay surface it's a harmless no-op. New docs/animations.md subsection documents the same contract for utility-class animations (raw class="wk-animate-fade-in" on HTML elements — developer adds data-replayable="true" manually since there's no PHP render path). The contract is pinned across all 11 animation-emitting components: every one carries the attribute when the animateIn prop is set, and does NOT carry it when inactive.
  • <x-wirekit::hero> aside column overflowed the page on <lg viewports when fed wide unbreakable content. The most common shape: developer drops a <x-wirekit::code-block> in the <x-slot:aside>; <pre>'s white-space: pre keeps the longest line non-breakable, so a 40-char code line drives the aside column to ~425 px on a 393-px iPhone viewport — overflows the section's padding-x boundary, code-block hangs out of the right edge. The copy column carried the same theoretical risk (long URL in lede, ascii-art header). Both columns now carry min-w-0 w-full lg:w-auto on the flex-col / flex-row break: min-w-0 escapes flex-shrink's default min-content floor, w-full lg:w-auto takes parent's full inline-size on flex-col then reverts to content-sized on lg:flex-row. The code-block's own overflow-x-auto on its <pre> then handles intra-line horizontal scrolling within the contained column. Back-compat: existing layouts at lg+ keep the same flex-row content-sized columns.
  • <x-wirekit::button loading> without a wire:* action attribute was a visual no-op. The component emitted wire:loading on the spinner SVG + wire:loading.attr="disabled" on the button — both Livewire directives that only activate WHILE a Livewire request is in flight. So <x-wirekit::button :loading="true"> on a non-Livewire page (or a button with no wire:click / wire:submit) rendered an invisible spinner and a clickable button. The component now distinguishes two intents: (a) loading=true + any wire:* action present → unchanged behaviour (spinner + disabled while the Livewire request runs), (b) loading=true + NO wire:* action present → DECLARATIVE loading state, spinner always visible + native disabled attribute set. Developers who want "this button is in a loading state, period" now get the expected visual without wiring up a Livewire request.
  • Reading-family widgets (<x-wirekit::reading-spine> / <x-wirekit::reading-toc> / <x-wirekit::reading-minimap> / <x-wirekit::reading-progress>) silently rendered empty when the developer-supplied target selector matched no element. A typo'd id (target="#summry") or a renamed ancestor class produced a blank spine / empty TOC / non-rendering minimap with no console signal — the developer had to reverse-engineer the silent miss from the DOM. Each widget now emits a single console.warn at init time when target is non-default AND document.querySelector(target) returns null, naming the offending selector and the component tag form (Check the selector on <x-wirekit::reading-spine target="…" />). The default 'main, article' selector stays silent — a miss there is a page-shape concern rather than a developer typo. reading-progress is the special case: its default target is null (track viewport), so any supplied selector triggers the check.
  • <x-wirekit-chart> (or <x-wirekit::chart-mixed> / <x-wirekit::chart-spark>) crashed the whole page on a fresh install with a RuntimeException: Charts are disabled. Set 'charts.library' in config/wirekit.php … because the package ships 'charts' => ['library' => null] as the safe default (avoids forcing every developer into Chart.js installation). In APP_DEBUG=true the chart now renders a developer-visible placeholder div instead — names the missing config step inline (Set 'charts.library' => 'chartjs' in config/wirekit.php, then npm install chart.js) and preserves the developer-supplied height so layout stays stable when the developer fixes the config. In production (APP_DEBUG=false) the hard-throw stays — silent fallback would hide a real misconfiguration from end users. New wirekit:doctor check checkChartUsageWithoutAdapter() surfaces the same diagnostic at install time: WARN when any Blade file under resources/views/ references the chart tag and config('wirekit.charts.library') is null. Both diagnostics ignore Blade-comment references ({{-- <x-wirekit-chart> --}}) so docs pages don't false-positive.
  • wirekit:show <name> printed Docs: docs/components/<name>.md — but /docs is export-ignored from the published package, so the path never resolved on a developer install. The line now prints Docs: https://docs.wirekit.app/components/<name> (the live URL), matching what wirekit:export-json's docs_url field already emitted everywhere else.
  • <x-wirekit::chart-mixed> silently dropped six props that the underlying class-based chart delegate accepts: wireStream, wireStreamMode, wireStreamCap, annotations, inline, replayable. The anonymous-Blade wrapper only declared labels/datasets/options/height/scope, and Laravel's attribute-bag doesn't flow into a nested <x-wirekit-chart> tag unless the props are bound by name. Added all six to chart-mixed's [@props](https://github.com/props) and forwarded via explicit :wireStream="$wireStream" etc. Existing callers pass through unchanged; mixed charts with wire:stream bindings or annotations now work as documented.
  • wirekit:doctor warned about font assets on every fresh install because the package ships 'sans' => 'inter' as the default config and the matching asset directory isn't auto-published. The warning read as if something failed; it's the natural state of a bare install. Now distinguishes the package-default font from a developer override — default + assets-not-published emits a new INFO hint (still suggests vendor:publish --tag=wirekit-fonts for self-hosting; system-ui fallback works without it); a developer override (e.g. 'sans' => 'roboto') + assets-not-published stays WARN because the requested font genuinely won't render until shipped.
  • wirekit:doctor printed two FAIL lines for missing [@wirekitStyles](https://github.com/wirekitStyles) / [@wirekitScripts](https://github.com/wirekitScripts) directives on a fresh Laravel 12 install before the developer had a chance to add a layout file at the conventional path. The report read like the install broke when actually the next step is on the developer. The bare-install case (no resources/views/components/layouts/*.blade.php, no resources/views/layouts/*.blade.php, no resources/views/components/layout.blade.php, AND no [@import](https://github.com/import) 'wirekit.css' in resources/css/app.css) now emits a single INFO line — Layout file not yet created — \wirekit:install` injects @wirekitStyles + @wirekitScripts on a re-run once you add …— and skips the directive scan. Once any of those paths exists OR the@import` alternative is configured, the scan runs normally and FAILs on missing directives in real misconfiguration scenarios.
v2.1.1

Patch release. Accessibility hardening sweep across every public docs page rendered on docs.wirekit.app. Targets the WCAG 2.2 AA contract WireKit promises in README.md. No API changes, no behaviour changes, no token changes — every existing developer drops in v2.1.1 and observes a stricter screen-reader / keyboard-reader contract with no migration work.

Fixed

  • /recipes/toolbar-filter-bar filter controls had no accessible name. The two <x-wirekit::select> filters and the search <x-wirekit::input> on the toolbar recipe page shipped without a label, aria-label, or wrapping <x-wirekit::field> — axe-core's select-name rule fires critical for that shape because screen readers announce each filter as "blank, combobox" with no semantic context. Added aria-label="Filter by status" / aria-label="Filter by category" / aria-label="Search products" to both the rendered preview AND the developer-facing source snippet so copy-paste users inherit the fix automatically.
v2.1.0

Minor release. Strict-by-default install hygiene, full pre-flight validation, token-clobber detection, plus five new install flags (--no-strict, --force, --ignore-failed-flags, --diff, --rollback) for explicit developer control over the install lifecycle. Public CSS API catalog (/extending/public-css-api) + content-edge spine contract (/extending/spine-contract) ship as the canonical AI-tooling discovery surfaces for wk-* classes and layout participation. New <x-wirekit::spine-aware> helper wrapper + WireKit::spinePadding() static let developer-authored components opt into the page-edge spine with one line. ALSO ships marketing-primitive composition ergonomics: brand-bar gains container + max props, hero/cta gain responsive size props with mobile-tightened vertical padding, hero's gradient overlay is contained to the content wrapper (no more empty-dark-area class of bug on mobile), footer gains a max prop replacing the prior hardcoded xl inner width. A new canonical recipe at /recipes/marketing-landing-page shows the full composition, and a new explainer at /extending/composition-patterns documents the chrome-vs-content axis as a design contract. Command-surface consistency: every "Unknown X" error now emits a uniform Levenshtein-ranked Did-you-mean hint, wirekit:theme default succeeds (and removes any existing preset block) instead of returning FAILURE, wirekit:list --category=Marketing lists the four canonical conversion primitives (cta, feature, feature-grid, hero), wirekit:component my-button derives --base=button automatically, wirekit:doctor becomes a Symfony alias of wirekit:verify (both names resolve to the SAME command instance), and <x-wirekit::cta> / <x-wirekit::hero> runtime prop validation reuses the same suggestion contract. Two behaviour changes flagged below: wirekit:install now aborts (exit 2) on invalid flag values or token-clobber warnings, where v2.0.0 printed a warning and continued with exit 0; AND the cta / feature / feature-grid / hero components migrated from category Display to category Marketing — developers reading wirekit:export-json see the changed category strings. Developers who depended on the legacy "warn-and-proceed" semantics combine --no-strict --ignore-failed-flags for byte-stable v2.0.0 behaviour. Every other change is additive.

Added

  • Comprehensive mobile-viewport browser-test coverage across every category where viewport behaviour matters. Builds on the marketing-primitive mobile coverage with Tier-1 PR-gate Tier1MobileGateTest.php (6 cross-category representatives — hero, modal, sidebar, input, table, reading-spine) plus Tier-2 on-demand sweeps for Overlays (modal / drawer / popover / tooltip / dropdown / alert-dialog / command-palette / hover-card / context-menu), Navigation (navbar / sidebar / tabs / breadcrumb / stepper / pagination / menubar / navigation-menu / brand-bar + reading-toc hideBelow breakpoint test), Forms (input / select / combobox / textarea / checkbox / toggle / date-picker / time-picker / segmented-control / slider / file-upload / field — with WCAG 2.5.5 AAA 44×44 touch-target audit), Reading family (progress / spine / toc / minimap / bookmark / meta / shell), Data display (table / kanban / data-list / timeline / tree-view / stat / stats / sparkline / pagination / progress / ticker / price / calendar — with horizontal-overflow guard), AND a generic ComponentPreviewMobileSweepTest that scans every docs/components/** :::preview block at iPhone 14 Pro viewport. New shared AssertsMobileViewport trait centralises the matchMedia override + console-error capture + scroll-settle scaffold so per-test boilerplate stays minimal.
  • docs/extending/public-css-api.md — canonical Public CSS API catalog. Single source of truth for every wk-* class WireKit emits to developers — via dist/wirekit.css literal selectors AND via Blade-template static-string emissions. ~40 catalogued classes across four groups (Layout / chrome markers, Reading family, Animation / motion, Display / loading), each with a stability tier (Stable / Provisional / Internal-with-exception) and customisation contract. Drift-audit guard at tests/Feature/PublicCssApiDriftTest.php enforces both directions — every shipped class has a row, every row references a real shipped class. Surfaces the catalog as machine-readable JSON via wirekit:export-api-map's new css-classes group for AI tooling / IDE-extension consumption.
  • docs/extending/spine-contract.md — content-edge spine participation contract. Documents the per-component inline-padding spine — which components join --padding-wk-x-lg (brand-bar, main, container, header, footer, cta, hero outer-only, spine-aware) and why others (sidebar, reading-toc, section's non-lg tiers) deliberately don't. Hero's "outer-yes / inner-no" asymmetry is now explicit; developers no longer measure the discrepancy by hand. Drift-audit guard at tests/Feature/SpineParticipantDriftTest.php enforces a // wirekit:spine-participant marker comment on every documented participant — adding a new spine-aware component without the marker fails the build.
  • <x-wirekit::spine-aware> — NEW opt-in spine wrapper for developer-authored components. Pass any slot content; the outer <div> reads --padding-wk-x-lg so the content aligns with brand-bar / main / footer / etc. on the same vertical content edge. The tier prop (sm / md / lg / xl) reads a different --padding-wk-x-* tier when the developer wants to deliberately step off the spine. Helper-driven (uses the new WireKit::spinePadding() static) so a future spine-padding refactor propagates automatically.
  • Pushery\WireKit\WireKit::spinePadding(string $tier = 'lg'): string — NEW public helper. Returns the canonical Tailwind utility string for the named padding tier (px-[var(--padding-wk-x-{tier})]). Use directly in developer-authored Blade class strings to opt INTO spine participation with one helper call — protects against tier typos AND gives the drift-audit guard a single canonical detection target. Tier values mirror the --padding-wk-x-* token family.
  • wirekit:doctor --tier={package|environment} filter. Run only the package-tier checks (asset / config / directive / Tailwind-source / token-alignment / Alpine-cleanup) OR only the environment-tier checks (currently just compiled-view freshness). Default behaviour unchanged — running without --tier walks every check. Useful for interactive dev sessions where the developer cares about only one tier's signal.
  • wirekit:doctor compiled-views-freshness check. Detects when resources/views/ mtimes exceed storage/framework/views/ by ≥60 seconds (the canonical "your test asserts a new prop but the assertion fails" failure mode caused by Laravel's view-cache lag). Emits a WARN with the actionable php artisan view:clear fix, short-circuits the diagnostic chain to the real cause. Honours the 60-second buffer to avoid false-positives on fast file-edit cycles. Categorised under the new environment tier.
  • wirekit:export-api-map new css-classes group. Machine-readable enumeration of every public wk-* class — same inventory as docs/extending/public-css-api.md table, one entry per class with tier + docs_url. AI tooling consumes via php artisan wirekit:export-api-map --pretty | jq '.groups[] | select(.id == "css-classes")' for autocomplete / type-checker awareness.
  • --wk-stagger-step token default in :root {}. The CSS variable now ships with a 75ms default in dist/wirekit.css (previously developers needed to set it via the prop OR risk a silent 0ms cascade). Documented in docs/theming.md under the new "Motion tokens" subsection alongside the existing --motion-wk-* token family.
  • Marketing category in Pushery\WireKit\ComponentRegistry. Four conversion-focused primitives (cta, feature, feature-grid, hero) migrate from category Display to category Marketing. footer stays Layout (structural primitive), brand-bar stays Navigation (header chrome), reveal stays Display (generic animation). wirekit:list --category=Marketing now exits 0 and prints the four primitives; the canonical category enum becomes eight values (Form, Layout, Typography, Navigation, Overlay, Display, Marketing, System). See /extending/component-registry#categories for the per-category definition + usage criteria.
  • Pushery\WireKit\Theming\ThemePresetRegistry — public single source of truth for every theme preset. keys() / get(string $key) / all() / isValid(string $key) / isDefault(string $key) cover read access; register(string $key, array $preset) lets downstream packages add custom presets at runtime from a service-provider boot hook. wirekit:theme, wirekit:install --preset=, and wirekit:export-api-map themesGroup() all read from this class — drift between their lists is impossible.
  • Pushery\WireKit\Support\SuggestSimilar — public Levenshtein helper underpinning every CLI "Did you mean?" hint AND the runtime WireKit::validateProp() suggestion line. byLevenshtein(string $needle, array $haystack, int $max = 3, int $maxDistance = 3) returns up to N ranked candidates; byLevenshteinScored(...) exposes the distance scores for ranked-weighting developers (MCP servers, recently-used-bias suggestions); format(array $suggestions) pretty-prints the result. Cross-cutting use across wirekit:show, wirekit:theme, wirekit:list, wirekit:publish-icons, wirekit:component, wirekit:install, plus WireKit::validateProp().
  • wirekit:list --category=Marketing,Display multi-category filter. Comma-separated list returns the union of the per-category sets. Useful for "show me everything that could go on a landing page" queries.
  • wirekit:list --format=... alias for --as=.... Matches the --format=json convention used by other Laravel commands. Both flags accept the same value set (count|slugs|categories|json) and resolve to the same code path; passing both simultaneously with different values is rejected.
  • wirekit:component --interactive flag. Forces the --base chooser prompt even when TTY detection misfires (Herd / Docker / WSL). Pairs with the new derivation chain so wirekit:component customer-dashboard walks: right-segment strip → Levenshtein suggestion → interactive choice() prompt → fail-fast with actionable error when no candidate matches.
  • <x-wirekit::brand-bar> container + max props. When container=true, the brand-bar's CHROME (background, border, sticky behaviour) stays edge-to-edge while CONTENT (brand, tagline, actions) aligns with the body's container-wrapped column via the inner max-width wrapper. max accepts the six-value container enum (sm/md/lg/xl/2xl/full), default xl. Reads --size-wk-container-* tokens so brand-bar + body align on the same vertical content-edge spine. Default container=false preserves the v2.0.0 edge-to-edge render.
  • <x-wirekit::navbar> container + max props. Same shape as brand-bar's pair — when container=true, the navbar's outer <nav> (background, border, sticky behaviour) stays viewport-edge-to-viewport-edge while the inner flex row (brand, nav items, actions) AND the mobile-menu panel wrap to max-w-[var(--size-wk-container-{tier})] + mx-auto. max accepts sm/md/lg/xl/2xl/full, defaults to xl. Lets a marketing-landing-page navbar align its brand + nav items on the same content-edge spine as the body below without losing the edge-to-edge chrome. Default container=false preserves the v2.0.0 full-bleed content render.
  • <x-wirekit::hero> size prop + responsive vertical padding. size="sm|md|lg" (default lg) names the sm+ tier of --space-wk-section-*; the mobile viewport (< sm breakpoint) automatically drops one tier so the section never overshoots a small viewport. Closes the historical "14 rem vertical padding on every viewport" footgun without breaking the default lg semantic on tablet and up.
  • <x-wirekit::hero> gradient overlay containment. When gradient is set, the overlay is now anchored INSIDE the content wrapper (max-w-xl) instead of as a direct child of the <section>. The previous shape filled the entire section including the vertical-padding band, producing a visible-empty-dark-area below content on mobile under variant="dark" + gradient. The overlay carries pointer-events-none so it never intercepts clicks.
  • <x-wirekit::cta> size prop. Same shape as hero's size — three-tier responsive vertical padding (default md). Mobile drops one tier automatically.
  • <x-wirekit::footer> max prop. Replaces the prior hardcoded xl inner-content max-width. Same six-value enum as brand-bar / container. All three inner wrappers (columns grid, brand+legal row, default slot) read from a single shared value — set max once and every wrapper updates. Default xl. Hardcoded fallback values in var(--token, fallback) pairs are removed (per user direction "kein hardcoded") — the tokens ARE the source of truth.
  • Two new documentation pages under the "Extending" + "Recipes" sidebar groups: Composition Patterns — Chrome vs. Content (canonical design contract explainer for the chrome-vs-content composition axis across every page-edge primitive) and Marketing Landing Page (copy-paste-ready worked example composing brand-bar + hero + feature-grid + stats + cta + footer with full visual alignment on the same content-edge spine).
  • wirekit:install --diff dry-run mode. Reports what WOULD change in resources/css/app.css, the layout file, config/wirekit.php, and public/vendor/wirekit/ without writing any files. Read-only preview. Exits 0 after rendering. Use to preview an install before committing — pairs with code-review workflows.
  • wirekit:install --rollback mode. Reverses the most-recent install session by replaying recorded before-snapshots from .wirekit-install.log at the developer project root. Per-file restore. Returns 0 on full success, 1 on partial restore. Mutually exclusive with every other install flag.
  • wirekit:install --no-strict opt-out flag. Pre-flight warnings print but do not abort. Use only for legacy CI scripts that depended on the v2.0.0 warnings-as-success semantic. Mutually exclusive with --force.
  • wirekit:install --force flag. Bypasses pre-flight warnings (token clobber, hand-edited marker blocks). Errors still abort. Use only when you understand the consequences. Mutually exclusive with --no-strict.
  • wirekit:install --ignore-failed-flags flag. Per-flag failures report but do not abort the install. Other flags still apply. Exit code reflects partial failure (non-zero so CI still detects it). Requires --no-strict (since strict-default would abort first).
  • .wirekit-install.log written by wirekit:install on success. Append-only JSON-Lines audit trail at the developer project root. Each tracked side-effect records its file path + before-snapshot. Enables --rollback. Failed installs discard pending entries (don't pollute the log with partial state).
  • Token-clobber scan during pre-flight validation. When a --font* flag is set AND resources/css/app.css already declares the matching --font-wk-{category} token OUTSIDE the wirekit marker block, the install aborts (or warns under --no-strict/--force) with an actionable error message naming the conflicting declaration's line + value. Closes a long-standing silent-overwrite footgun where developer-set tokens lost cleanly to wirekit's marker block on cascade order.
  • Pre-flight validation pass in wirekit:install. Every flag is validated BEFORE any side-effect runs. Errors are aggregated in one pass — the user sees the full picture in one round-trip, not one error at a time. Filesystem is byte-stable across a failed validation. The pattern is codified for adoption by other multi-flag artisan commands in future plans.
  • CI / Deploy script discipline section in docs/integration.md — canonical fail-fast install shape, exit-code reference, GitHub Actions example, legacy opt-out recipe, dry-run preview pattern, rollback workflow.
  • wirekit:doctor cleanup-hygiene check for custom Alpine plugins. Static-analysis scan of the developer's resources/js/ tree that flags two anti-patterns: an IntersectionObserver / MutationObserver / ResizeObserver instantiation without a destroy() cleanup hook, and a disconnect() call inside an observer callback without a preceding null-guard against post-teardown fire. Heuristic-based — emits a soft warning with a docs cross-link and respects a // wirekit-doctor: cleanup-ok opt-out comment for intentional patterns the heuristic doesn't recognise.
  • Three new documentation pages under the "Extending" sidebar group: Authoring Custom Alpine Plugins (defensive-cleanup pattern + worked example for developer-authored Alpine plugins), Pest Browser-Test Setup (recipe for wiring pestphp/pest-plugin-browser into a WireKit-using Laravel app, including known-issue workarounds), and Console Output Baseline (catalog of what a clean WireKit install should emit on first paint, during interaction, and from optional-dependency warnings — helps developers triage unexpected output).
  • Pushery\WireKit\Support\BladeParser — companion to PropsParser for the broader Blade-content surface (named slots, directive enumeration, comment extraction, component-reference discovery). Replaces the inline slot-detection regex previously embedded in ExportJsonCommand.
  • wirekit:list --as=count|json|slugs|categories flag emits machine-readable output for script + AI consumption. --as=count produces a single integer with no decoration (WK_COUNT=$(php artisan wirekit:list --as=count)), --as=slugs newline-separated component names, --as=categories a JSON object mapping each category to its component count, --as=json the full per-component array with name + tag + category + description. Default human-readable output unchanged.
  • wirekit:show <name> --as=json flag emits the per-component structured schema as JSON to stdout (no decoration). Includes name, tag, category, description, docs_url, props[] (full prop records with default, default_normalized, comment), and sub_components[]. Tooling that needs one component's schema without loading the full export-json output.
  • wirekit:show <name> --validate-against=<path> flag lints a developer Blade file against the component's known prop set. Walks every <x-wirekit::name ...> usage, warns on unknown attributes with the closest matching prop name (Levenshtein-ranked) so typos like intnetintent surface immediately. Pre-runtime catch of "did I typo a prop?". Exits 1 on any unknown attribute — wire into pre-commit hooks for prop-spelling enforcement.
  • .wirekit-schema.json written by wirekit:install. Drop-in IDE-extension + AI-tool feeder file at the developer's project root. Contains the same JSON manifest as wirekit:export-json --pretty (every component's name + tag + category + description + full prop records + slots). Re-running wirekit:install regenerates the file — checked into git for stable autocomplete across teammates, or gitignored to keep installs fresh. Non-fatal on write-failure (read-only filesystem etc.).
  • Two new documentation pages under the "Extending" sidebar group: ComponentRegistry — Programmatic Discovery (canonical PHP-API for reading the component catalogue at runtime) and Authoring Custom Blade Components (eight tested [@props](https://github.com/props) syntax shapes WireKit's tooling understands, plus CLI tooling for custom components).

Changed

  • wirekit:install is now strict-by-default. Invalid font keys, category mismatches, unknown preset names, invalid --apex-license tiers, AND detected token-clobbers all abort with exit 2 (INVALID) before any filesystem mutation. v2.0.0 BEHAVIOUR CHANGE. Previously these conditions printed a warning to stderr and the install continued with exit 0 — a silent failure mode that masked install bugs from CI. To preserve the legacy semantic, combine --no-strict (lets pre-flight warnings pass) + --ignore-failed-flags (lets per-flag exceptions pass).
  • wirekit:install exit codes are now precise: 0 = success, 1 = runtime failure mid-install (verify failed, theme application failed, partial rollback), 2 = pre-flight validation rejected the input (no filesystem mutations happened). Wire these into CI as documented in docs/integration.md "CI / Deploy script discipline".
  • Four components recategorised from Display to Marketing. cta, feature, feature-grid, and hero carry their conversion-funnel intent in the category field now. API-VISIBLE for developers reading wirekit:export-json / wirekit:list --as=json / Pushery\WireKit\ComponentRegistry::all() — the four entries' category field changed value. Rendering, props, slots, and tags unchanged; no other API surface affected.
  • wirekit:doctor is now a Symfony alias of wirekit:verify. Both names resolve to the SAME command instance — running either walks the identical check pipeline. php artisan list wirekit still shows two rows (the alias appears with an [wirekit:doctor] annotation on the verify row), but the standalone DoctorCommand class file is removed and both invocations share one underlying handler. Internal API change for developers who imported Pushery\WireKit\Console\DoctorCommand directly in a custom service provider's command-registration array — switch to VerifyInstallationCommand (which now declares wirekit:doctor via setAliases()).
  • Pushery\WireKit\ComponentRegistry::extractProps() return shape. Was array<string, string> (flat name → default-string map). Now returns the structured list<array{name, default, default_normalized, type_hint, comment}> shape produced by PropsParser. BREAKING for developers of the prior shape. Migration: iterate the returned list, read each entry's name / default keys, and the inline-comment metadata (previously hidden) is now available via the comment field. No back-compat shim — the structured shape is the canonical surface going forward.

Fixed

  • Silent exit-code lie in wirekit:install --font=<invalid-key>. Previously the catch-block printed ✗ Unknown font key '...' to stderr but did NOT propagate a non-zero exit code; the install continued through publishing / theming / verification and returned self::SUCCESS. CI scripts wrapped in set -e saw exit 0 and continued. Now pre-flight aborts immediately with exit 2 and the actionable error message.
  • wirekit:theme default returned FAILURE. The default preset wasn't in ThemeCommand's inline PRESETS array, so running wirekit:theme default exited 1 with "Unknown preset" — despite wirekit:install --preset=default being a valid no-op. Symmetry restored: wirekit:theme default now exits 0, actively removes any existing wirekit:theme start/end block from app.css (returning to the bundled token values), and emits an actionable status line either way.
  • wirekit:show buttn (and every other "Unknown X" error) had unreliable suggestions. The historical str_contains bidirectional match in ShowComponentCommand produced suggestions only when the typo's characters were a substring of a real name (or vice versa). Single-char typos like buttn (distance 1 from button) never matched. The new Pushery\WireKit\Support\SuggestSimilar::byLevenshtein covers every "Unknown X" report uniformly across wirekit:show, wirekit:theme, wirekit:list, wirekit:publish-icons, wirekit:component, AND WireKit::validateProp() runtime messages.
  • wirekit:component <name> defaulted --base to <name> itself. When --base wasn't passed, the default pointed at the new component's own (non-existent) source — every invocation without explicit --base failed with "Unknown base component". The new derivation chain (right-segment strip → Levenshtein fallback → interactive prompt → fail-fast with actionable error) handles the common my-X → X pattern automatically (e.g. wirekit:component my-button derives --base=button and copies button.blade.php).
  • Two regex-based [@props](https://github.com/props)([...]) parsers consolidated into a single tokenizer-backed Pushery\WireKit\Support\PropsParser. The historical parsers (ComponentRegistry::extractProps and Console\ExportJsonCommand::extractProps) silently truncated prop defaults that contained commas inside function-call argument lists (config('x.y', null) was split into two phantom props) AND leaked trailing // comment blocks into the captured default value. Both bug classes broke wirekit:show output AND the wirekit:export-json manifest — every developer agent (Cursor / Claude Code / MCP servers) reading the export got incomplete prop catalogues. The replacement uses PHP's own token_get_all() tokenizer and handles every legitimate [@props](https://github.com/props) shape: config(...) defaults, multi-line array literals, match(...) expressions, mixed-quote keys, heredoc / nowdoc values. A drift-audit guard (tests/Feature/PropsParserCallerDriftTest.php) blocks new regex-based [@props](https://github.com/props) parsers from being added to src/.
  • Unguarded observer callbacks in wirekitStatAnimate and wirekitAnimate Alpine plugins. Browser-queued IntersectionObserver callbacks could fire AFTER Alpine teardown set this._observer = null (Livewire morph removing the host element pre-intersection is the canonical trigger), causing TypeError: Cannot read properties of null (reading 'disconnect') to log to the console. The page rendered correctly, but the silent error reded every developer's assertNoSmoke() / assertNoJavascriptErrors() browser-test against any WireKit page using <x-wirekit::stat animate> or <x-wirekit::reveal>. The fix adds a null-guard immediately before the in-callback disconnect() call in both plugins plus the reading-minimap initialisation IntersectionObserver that shared the bug shape. Sweep audit of the remaining observer instantiations across reading-spine, reading-toc, reading-minimap (4 sites), chart, and chart-apex confirmed they don't access the observer reference inside the callback, so no race exists.
v2.0.0

Major release. First publicly-cut tag after v1.6.3, aggregating every change since. Two breaking changes (both in ### Changed): custom Pushery\WireKit\Contracts\ChartAdapter implementations must add three new methods to satisfy the expanded interface, AND <x-wirekit::container> inline-padding tiers now read from the --padding-wk-x-* token family instead of --space-wk-* so a container nested in <x-wirekit::main> inherits the same content-edge spine. Consumers using only the built-in ChartJsAdapter or the new ApexChartsAdapter, and consumers using <x-wirekit::container> with default padding values, see no behavioural change beyond a 0.5-rem horizontal alignment correction. Every other change is additive and back-compatible.

Added

  • Optional ApexCharts chart adapter alongside the existing Chart.js adapter. Switch with one line in config/wirekit.php: 'charts' => ['library' => 'apexcharts']. Same <x-wirekit::chart> Blade tag for both libraries; zero breaking change. ApexCharts unlocks 9 chart types Chart.js does not ship natively — candlestick, boxplot, range-bar, range-area, heatmap, treemap, funnel, radial-bar, sparkline. Dedicated dist/wirekit-apex.js adapter bundle (~2 KB gzip glue, no ApexCharts library code; the consumer installs apexcharts via npm). Note: ApexCharts is not MIT-licensed — the free Community License covers organisations under $2M USD annual revenue; Commercial License required above. WireKit ships only the adapter glue (MIT). See docs/components/chart.md#license-apexcharts-only for the full terms.
  • <x-wirekit::sparkline> — NEW first-class component. Inline trend sparkline (axis-less line chart) for KPI strips and dashboard cells. Six props (data / trend / inline / height / scope). Auto-detects trend from first-vs-last data and tints green / red / muted accordingly; manual trend="up|down|neutral" override available. Inline mode renders at surrounding text height (1.25em / 4rem width); block mode at configurable height (default 2.5rem). Delegates to <x-wirekit::chart type="sparkline"> so both adapters render correctly — ApexCharts uses native sparkline mode, Chart.js falls back to a plain line.
  • <x-wirekit::chart-mixed> — NEW component for multi-axis dashboards. Each dataset declares its own type (line / bar / column / area) plus an optional yAxisID for multi-axis configurations. Both adapters consume the per-dataset type field natively — Chart.js via the per-dataset type field, ApexCharts via series[].type.
  • wireStream prop on <x-wirekit::chart> for real-time data updates. Subscribe a chart to a Livewire-emitted event and append each fired payload via the library's imperative API (chart.update('none') for Chart.js / chart.appendData() for ApexCharts). wireStreamMode="strict" (default) FIFO-trims at wireStreamCap points (default 100); wireStreamMode="stream" grows unbounded.
  • annotations prop on <x-wirekit::chart> for vertical lines / horizontal regions / point callouts. ApexCharts has annotations built-in; Chart.js requires chartjs-plugin-annotation (the Alpine factory emits a console.warn when annotations are supplied but the plugin is missing — graceful degradation).
  • Smooth dark-mode preset transitions on chart re-theming (~250 ms ease-out). Both adapters interpolate colours during .dark toggle instead of snapping. Collapsed to instant under prefers-reduced-motion: reduce.
  • Two new chart-system docs subhierarchies on docs.wirekit.app: /components/charts-chartjs/ (full Chart.js demo set — bar / line / area / pie-doughnut / scatter-bubble / radar-polar / advanced / theming) and /components/charts-apex/ (full ApexCharts demo set — line / area / bar / column / range-bar / pie-donut / radial-bar / radar / scatter-bubble / heatmap / treemap / candlestick / boxplot / funnel / timeline / sparklines / annotations / streaming / mixed / motion / theming). Every page ships realistic seed data drawn from B2B SaaS, e-commerce, DevOps, finance, marketing domains.
  • wirekit:install --apex-license=community|commercial|oem flag. Records the consumer's license tier into config/wirekit.php charts.apex_license after printing the License Notice once at install time. Suppresses the wirekit:doctor reminder for commercial and oem tiers.
  • wirekit:doctor ApexCharts checks. Detects apexcharts npm package presence, validates the active license tier, confirms dist/wirekit-apex.js is published.
  • wirekit-apex.js registered as publishable under both the wirekit-scripts and wirekit-assets tags. Running php artisan vendor:publish --tag=wirekit-assets now copies the adapter glue to public/vendor/wirekit/wirekit-apex.js alongside the other JS bundles. The route fallback at /wirekit/wirekit-apex.js also works for setups that prefer route-based serving over publishing. Eliminates the manual cp vendor/.../dist/wirekit-apex.js public/vendor/wirekit/ step that ApexCharts adopters previously had to run after every composer update.
  • <x-wirekit::reading-toc> — NEW primitive in the reading family. Horizontal sticky-strip TOC sibling to reading-spine. Same auto-build-from-headings + IntersectionObserver active-section model, different rendered shape (a flat row of links across the top or bottom of the article container). Use case: marketing landing pages with 3-4 anchored sections (Hero, Features, Pricing, FAQ) where a vertical sidebar feels excessive. Seven props (target, levels, position, offset, hideBelow, flush, scope). Defaults to levels="2" for landing-page flat structure; mobile-hidden via hideBelow="sm" since narrow viewports cannot host a horizontal strip without overflow. The flush prop zeroes the first link's left-edge padding and the last link's right-edge padding so the visible text aligns flush with the strip's content edges (use when the TOC sits directly under <x-wirekit::brand-bar> or <x-wirekit::main> and the consumer wants the first link on the same vertical content-edge spine as the surrounding brand text and h2 headings). Real <a href="#section-id"> links inside a <nav aria-label="Page sections"> landmark — keyboard-navigable + screen-reader native. prefers-reduced-motion: reduce collapses smooth-scroll to instant. Reuses spine's --reading-spine-color-idle / -active for theme consistency; adds 5 layout-specific tokens (--reading-toc-bg / -padding-y / -padding-x / -gap / -link-max-width) plus 2 colour aliases.
  • <x-wirekit::brand-bar> — NEW navigation primitive. Page-chrome wrapper for the canonical "logo + tagline + actions" header pattern. Three named slots: brand (typically a <x-wirekit::brand> primitive), tagline (secondary muted text), actions (right-anchored sign-in / theme-toggle / account-widget content via margin-left: auto). Props: as (header | nav), divider (bottom | none), padding (none/sm/md/lg/xl), sticky (pins to top: 0 during scroll with an opaque background fallback). Carries the content-edge spine via padding="lg" reading from --padding-wk-x-lg, so the brand visible-text aligns with <x-wirekit::main padding="lg">, <x-wirekit::reading-toc flush>, and the article h2 headings below on the same vertical X-coordinate — out of the box, no custom CSS.
  • <x-wirekit::prose :density> — NEW prop on the existing component. comfortable (default) keeps the long-form-article heading scale (h2 mt = 2.5 rem for generous section breaks, h1 = 1.875 rem). compact tightens for marketing pages (h2 mt = 0.75 rem, h1 one type-scale tier smaller, p mb = 0.5 rem). Eliminates the need to override prose typography with consumer-side .my-section h2 { margin: ... } rules when the prose lives inside a tight marketing layout. Default comfortable keeps backward-compatible rendering.
  • <x-wirekit::reading-shell :toc="true"> — opt-in toc toggle. Defaults to false in every density preset (comfortable / compact / minimal). Marketing landing pages explicitly opt in via :toc="true" :spine="false"; blog posts and docs pages keep the spine sidebar.
  • Marketing Landing TOC recipe at /recipes/marketing-landing-toc — sibling to long-form-article. Demonstrates the canonical Hero / Features / Pricing / FAQ landing-page pattern with the new sticky TOC strip, plus the offset prop pattern for layouts with a fixed nav above the strip.
  • Long-form Article recipe at /recipes/long-form-article — canonical Medium / Substack-style article layout: reading-progress bar at the top, reading-spine sidebar on the right, reading-bookmark resume pill, reading-meta time-to-read. Demonstrates the full reading-* family in one composition, both via the <x-wirekit::reading-shell> sugar wrapper and via direct primitive composition for power-user customisation.
  • Documentation Reader recipe at /recipes/documentation-reader — Stripe / Tailwind-docs-style article shell with reading-progress bar, dense-section reading-spine, fixed-nav offset on the TOC strip, reading-bookmark across multi-page-session reads. Shows the docs-rhythm pattern (heading-scale + dense per-paragraph code blocks) alongside the same reading-* primitives.
  • <x-wirekit::reading-minimap> — NEW primitive in the reading family. Every-item density overview of a scrollable container, with two rendering modes:
    • mode="stripes" (default) — every item matched by itemSelector renders as a 1–2 px stripe at proportional vertical position. itemStyle="block" upgrades to per-paragraph rectangles whose height tracks the source item's natural height (skeleton-style content texture).
    • mode="rendered" — PhpStorm / VS Code-style abstract content canvas. Walks every text-bearing node via TreeWalker, gets per-rendered-line rects via Range.getClientRects(), draws each as a rectangle on a DPR-aware canvas. Per-element-type colour palette (h1–h6 each its own alpha tier, code indigo, table rose, blockquote slate-bolder, image amber, WireKit wk-* components emerald, prose / default slate-muted). 13 --reading-minimap-color-{h1..h6,code,table,blockquote,prose,wirekit,image,default} tokens for full re-theming without JS.
    • Translucent viewport-overlay rectangle tracks the host's visible region; click stripe → smooth-scroll target so item is centered (instant under prefers-reduced-motion: reduce); drag overlay → pan host scroll position with browser-scrollbar-matched translation; hover stripe (non-touch) → tooltip following the cursor with the item label.
    • Two canonical use cases: long-form article density-overview (sibling to reading-spine) and sidebar navigation density-overview.
    • 5000-tag silent fallback to stripe mode on very long articles. No DOM clone in rendered mode → no parse step → no surface for HTML-injection vectors.
  • <x-wirekit::reading-meta perParagraph> — Medium-style inline annotations. Opt-in mode that injects small <span class="wk-reading-meta-paragraph">N min</span> annotations immediately before each <p> in the target with at least paragraphMinWords words (default 30). Annotations show estimated remaining-time FROM that paragraph onward, re-computed on scroll. aria-hidden="true" on every annotation — canonical SR text remains the total/remaining display. Default off; opt-in.
  • <x-wirekit::reading-progress variant="auto"> — theme-reactive fill mode. Falls back to currentColor when the consumer hasn't set --reading-progress-fill — useful for embedded contexts (iframes, browser extensions) where the bar should match the surrounding text colour. Joins the canonical 6-value variant set (primary | neutral | success | warning | danger | info).
  • <x-wirekit::reading-shell density> preset prop. Three values (comfortable | compact | minimal) adjust the shell's per-primitive defaults: progress-bar height, spine expand mode, which primitives render by default. Per-primitive toggles (:spine="false" etc.) win over the density preset.
  • 14 new design tokens for the reading- family*: 1 on reading-progress (--reading-progress-fill), 7 on the new reading-minimap (width, stripe-height, stripe-gap, color-idle, color-active, viewport-bg, viewport-border), 2 on reading-meta perParagraph mode (paragraph-color, paragraph-spacing). All themeable in :root {}.
  • <x-wirekit::stat> composes animate (counter count-up) with animateIn (entrance reveal). The Blade template emits an outer wrapper that carries the entrance reveal while the inner element keeps the counter handler — the two scopes layer cleanly. Single-flag usages render byte-identical.
  • <x-wirekit::cta> and <x-wirekit::footer> accept the animateIn prop. Both marketing primitives now match the existing animateIn surface on <x-wirekit::card>, <x-wirekit::feature>, <x-wirekit::hero>, and others. Passing animateIn="slide-up" (or any of the 11 base presets) wires <x-wirekit::reveal> semantics inline without an extra wrapper element. Honours prefers-reduced-motion: reduce.
  • stagger prop on <x-wirekit::feature-grid> and <x-wirekit::stats>. When set on the wrapper and an entrance preset is configured on the children, each child's animation fires with an incremental delay — produces a clean cascade instead of every card landing at once. Boolean form uses a 75ms step (stagger); integer form overrides for custom rhythm (:stagger="125"). Pure CSS via :nth-child rules with an index cap at 8 to bound delay on long lists; collapses to 0 under reduced motion. Zero JS bundle impact.
  • delay prop on <x-wirekit::reveal>. Holds the entrance for a beat before it begins — useful for sequencing cards or letting a hero-section heading land before the supporting copy follows. Accepts five named tokens (none, sm, md, lg, xl) mapping to themeable CSS variables, or a raw integer in milliseconds for one-offs. Composed via inline animation-delay on the existing wk-animate-{preset} class — no JS plugin change needed. Collapses to 0ms under prefers-reduced-motion: reduce.
  • Five new motion-delay tokens in dist/wirekit.css: --motion-wk-delay-none (0ms), --motion-wk-delay-sm (75ms), --motion-wk-delay-md (150ms), --motion-wk-delay-lg (300ms), --motion-wk-delay-xl (500ms). All consumer-overridable in :root {}.
  • CSS class .wk-stagger plus eight :nth-child rules and a cap-at-8 index ceiling. Drives the stagger prop on feature-grid and stats. Reduced-motion override zeroes per-child delays inside the existing [@media](https://github.com/media) (prefers-reduced-motion: reduce) block.
  • wirekit:doctor thirteenth check — :root vs .dark colour-token symmetry. The doctor now reads the consumer's resources/css/app.css, extracts the :root {} and .dark {} blocks, and emits a warning when --color-wk-* tokens are declared in one block but missing from the other. Asymmetric colour tokens produce theme drift the consumer sees as "looks wrong in dark mode" without an obvious cause.
  • <x-wirekit::reading-progress> — viewport-pinned reading-progress indicator for long-form articles. Bar (default) or dot (indicator="dot") variants; both fill 0 → 100% on scroll using compositor-only properties (transform: scaleX / stroke-dasharray). Five variant colours, three height tokens (sm/2px, md/3px, lg/5px), showAfter scroll threshold, target= selector, segments chapter markers, milestones events (wirekit:reading-progress:milestone Alpine dispatch fired ONCE per session at 25 / 50 / 75 / 100% boundaries). role="progressbar" + dynamic aria-valuenow. Zero-KB bundle impact.
  • <x-wirekit::reading-spine> — sidebar mini-TOC that auto-builds from page headings. Pins to the right edge at md+ breakpoints; tracks scroll position via IntersectionObserver; expands on hover or focus. Configurable target=, levels=, position=, expand= (hover / focus / always / always-md), offset=. Real anchor links so navigation works without JS. Five opt-in extensions: numbered, fillSections, backToTop, expand="always-md", and a wirekit:reading-spine:section-changed event. Filter slot composition pattern. Bundle impact: ~600 bytes gzip.
  • <x-wirekit::reading-bookmark> — persists scroll position to localStorage while reading; surfaces a "Resume reading where you left off?" pill on return-visit when conditions are met (previous-session dwell time ≥ minDwellSeconds, scroll moved past threshold * scrollHeight). Cross-tab consistency via storage event. try/catch wrapping every storage op — silently degrades on private-browsing / quota-exceeded. Required key prop (typically "article-{slug}").
  • <x-wirekit::reading-meta> — small text element showing ~12 min read (initial estimate) and optionally ~5 min remaining (live-tracking on scroll). Skips non-prose nodes (pre, code, figure, figcaption, img, picture, svg, [data-language]). CJK-aware: when more than 40% of text is CJK ideographs, falls back to character-based estimation with a configurable cjkCharsPerMinute baseline (default 500). wpm clamps to ≥ 50; totalMinutes floors to 1. role="status" aria-live="polite".
  • <x-wirekit::reading-shell> — composition wrapper that renders <x-wirekit::reading-progress> + <x-wirekit::reading-spine> + <x-wirekit::reading-bookmark> + <x-wirekit::reading-minimap> + <x-wirekit::reading-meta> around the slot content in one tag. Mirrors the <x-wirekit::app-shell> "one tag, full UX" pattern. Per-component opt-out via :progress="false" etc. Forwards every documented child prop via flat surface.
  • CSS variable tokens for the reading- family in dist/wirekit.css:* three height tokens for the bar (--reading-progress-height-{sm,md,lg}), the dot diameter (--reading-progress-dot-size), seven spine layout / colour tokens, three bookmark pill tokens, two reading-meta tokens.
  • [@media](https://github.com/media) print { display: none !important } rule for every reading-* primitive — only the article body prints, not the reading chrome.
  • prefers-reduced-motion: reduce gating for the entire reading- family* in the global [@media](https://github.com/media) (prefers-reduced-motion: reduce) block.
  • <x-wirekit::replay-button> — NEW companion primitive. Renders an icon-button that walks the closest [data-replay-target] ancestor and re-mounts it (Alpine.initTree) so its x-data="wirekitAnimate(...)" / counter / chart-motion fires again. Standardises the "re-play this animation" affordance every animation-capable component carries via the data-replayable="true" contract — consumers can place the button explicitly anywhere alongside a primitive, or rely on the docs-site preview chrome's auto-injection. Two props: label (button aria-label, default "Replay"), scope (named personalization scope).
  • replayable prop on <x-wirekit-chart>. Opt INTO the docs-site ↻ Replay button surface by emitting data-replayable="true" on the chart root. Set it explicitly when the entrance animation is worth re-watching (bar-grow / line-trace / slice-sweep on /components/charts-apex/motion), or rely on the auto-detect path: whenever wireStream is bound (every streaming chart on /components/charts-apex/streaming), the attribute is emitted automatically. Back-compat preserved: callers who already passed data-replayable raw via the attribute bag continue to work — the Blade view skips its own emission to avoid duplication.

Changed

  • Pushery\WireKit\Contracts\ChartAdapter interface expanded from 4 → 7 methods. Net-new: name() / rendersTo() / supportedTypes(). Existing scripts() / normalizeData() / defaultOptions() / alpineComponent() unchanged. Consumers using only built-in adapters see no impact (both ChartJsAdapter and ApexChartsAdapter ship the new methods). Custom adapter implementations need to add the three new methods to satisfy the interface. BREAKING for consumers with a hand-written ChartAdapter implementation.
  • <x-wirekit::container> inline-padding moved from --space-wk-* to --padding-wk-x-*. padding="sm|md|lg|xl" now reads from the --padding-wk-x-* content-edge spine instead of the --space-wk-* block-rhythm scale. Resolves a silent 0.5 rem horizontal drift when nesting <x-wirekit::container padding="lg"> inside <x-wirekit::main padding="lg"> — both now share the same content-edge X-coordinate. Consumers who depended on the wider previous value (e.g. padding="lg" was 1.5 rem, now 1 rem) should bump to padding="xl" (also 1.5 rem) for unchanged visual output. BREAKING for consumers relying on the exact pixel value of container's horizontal padding.
  • <x-wirekit::sidebar> inline-padding fixed. Was applying --padding-wk-y-sm (the y-axis token) on all four edges; now correctly splits into px-[var(--padding-wk-x-sm)] py-[var(--padding-wk-y-sm)] and moves the inter-item gap to --space-wk-sm (the canonical gap token). Sidebar items at the inboard edge now align with <x-wirekit::header> and <x-wirekit::main> rendered alongside on the same vertical content-edge spine. Back-compat: visual change is a few-pixel adjustment to the sidebar's internal padding; no API change.
  • Color-prop unification across <x-wirekit::message>, <x-wirekit::alert>, <x-wirekit::callout>, and <x-wirekit::progress>. Every component that exposes a color prop now accepts the same canonical six-value set (primary, neutral, success, warning, danger, info). progress adopts primary as the canonical name; the historical accent value continues to work as a back-compat alias. New Variants & Intents page documents the canonical set, the per-component prop tables, and the visual-synonym semantics in one place.
  • Bundle size: dist/wirekit.js grew from 25 → 26 KB gzip (83 → 86 KB raw) covering the new reading-* family and chart-adapter expansion. Stays within the ±2 KB drift budget per docs/dependencies.md. Core bundle (chart-only) unchanged at ~2 KB gzip / 11 KB raw. ESM bundle ~25 KB gzip / 82 KB raw.
  • .wk-reading-toc__list inline-padding ships without !important. Consumers running a typography prose-wrapper (e.g. a [@tailwindcss](https://github.com/tailwindcss)/typography body or any body.prose-style class) should carve out [class*="wk-"] from their ul/ol/li rules — same pattern they already use for the max-width: 75ch typography clamp. Without the carve-out the padding-left: 1.75rem in a typical prose stylesheet wins on specificity over .wk-reading-toc__list { padding-inline: 0.25rem } and pushes WireKit list-based components (reading-toc, list, breadcrumb) off the content-edge spine. Recommended typography rule shape: .prose ul:not([class*="wk-"]), .prose ol:not([class*="wk-"]) { padding-left: 1.75rem }.

Documentation

  • docs/integration.md — new "Performance — Conditional Asset Loading" section documenting a server-side flag pattern for emitting highlight.js and Chart.js <script> tags only on pages that actually use <x-wirekit::code-block> or <x-wirekit::chart>. Saves ~95 KB gzip on lightweight routes (landing pages, theming guide, recipes without code/charts) without breaking caching or first-paint correctness. Pattern is documentation-only; no helper class shipped — adapt the str_contains() detection to your DocsParser / class-naming conventions.
  • /theming/design-tokens — NEW comprehensive reference page for every WireKit CSS variable: colors, typography, motion, sizing, component-specific tokens, and the chart-theming palette. Single landing page where consumers can find any token by name before reaching for its component's docs page.
  • /getting-started/livewire-starter-kit — NEW recipe page for fitting WireKit into an existing Livewire Starter Kit project. Walks the four touchpoints where Starter-Kit defaults differ from a clean-room wirekit:install (theme preset, font stack, dark-mode flip, sidebar conflict resolution) so existing Starter-Kit consumers don't have to discover them ad-hoc.

v1.6.3

Patch release covering one consumer-facing dependency fix, one CI build-stability fix, and a README link-table cleanup. No new API surface; no breaking changes.

Fixed

  • livewire/livewire is now a direct require, not a require-dev. Running composer require pushery/wirekit previously installed the package without pulling Livewire — consumers who hadn't already added Livewire to their project then ran php artisan wirekit:install and hit confusing "class not found" errors at first component render. Livewire 4+ is a stated minimum requirement (per the README's stack badges and the integration guide); composer.json now reflects that contract and pulls Livewire into the consumer's project automatically. Existing consumers who already have Livewire in their composer.json see no change — composer will just keep their pinned version. Net effect: zero-step Livewire setup for new installs.

  • CI markdown lint stabilised against markdownlint-cli2 minor-version drift. The CI workflow runs npx markdownlint-cli2 which always pulls the latest published version, while the local dev environment was pinned to ^0.21.0. When 0.22.1 shipped a stricter MD038 rule interpretation, CI started failing on a markdown file that the local tooling considered clean. Bumped the local pin to ^0.22.1 so local + CI run the same lint version, and fixed the lingering MD038 violation surfaced by the new rule.

  • README documentation links no longer 404. The Components badge and the "Documentation" table linked to docs.wirekit.app/components and docs.wirekit.app/recipes, neither of which existed as index routes — both returned 404. Replaced with links to working pages: the docs root (docs.wirekit.app), the getting-started guide, and the theming guide. Pointing readers at routes that resolve.


v1.6.2

Patch release covering three small surface-polish fixes spotted in the v1.6.1 README + docs walk. No new API surface; no breaking changes; every component renders byte-identical to v1.6.1.

Changed

  • README.md — removed the "Tests" CI badge. The badge URL pointed at pushery/wirekit/actions/workflows/tests.yml, but the public Packagist repo doesn't carry the test suite (tests/ is export-ignore in .gitattributes), so the badge image returned 404 and rendered as broken on every README view. The actual test suite (1686 tests, all green) runs on the development repo. The badge will return in a future release once a public-facing CI surface exists; for now, removing the broken image is the honest choice.

Fixed

  • <x-wirekit::code-block> copy-button cursor. The toolbar copy button rendered without a cursor: pointer on hover, leaving keyboard / mouse users unsure whether it was clickable. Added cursor-pointer to the button class — hover state now signals interactivity unambiguously, matching every other interactive control in WireKit.

  • docs/cli/wirekit-doctor.md — "Common failures and fixes" section restructured. The four subsection headings previously embedded the literal doctor command output (with / i glyphs and inline-code backticks at H3 size), which rendered awkwardly on docs.wirekit.app — small icons forced to heading scale, monospace at heading weight. Each subsection now uses a descriptive title (e.g. "Sans font mismatch") and quotes the exact doctor line in a text code block in the body. Reads cleanly at every viewport.


v1.6.1

Patch release covering five consumer-visible fixes plus a README restructure and badge expansion. No new API surface; no breaking changes; every component renders byte-identical to v1.6.0 unless its specific bug applied.

Fixed

  • prefers-reduced-motion: reduce now actually honored on <x-wirekit::reveal> and wk-animate-* utilities (WCAG 2.3.3). The component documentation and helper docblock claimed the OS-level reduced-motion preference was respected, but the global [@media](https://github.com/media) (prefers-reduced-motion: reduce) block in dist/wirekit.css only gated [x-transition] selectors — the 22 keyframe animations driving every preset ran at full speed regardless of user preference. The block now gates [class*='wk-animate-'] as well, snapping animation-duration to 0.01ms so reduced-motion users see an instant snap-to-final state. Behavior now matches the documented contract.

  • Nested <x-wirekit::list> margins no longer accumulate inside typography wrappers. When the component is rendered inside a consumer's .prose / .docs-prose / [@tailwindcss](https://github.com/tailwindcss)/typography context, the wrapper's <ol> / <ul> / <li> rules injected margin: 1em 0 onto every level — and the previous Tailwind-utility approach was too low-specificity to win the cascade. Nested lists looked progressively more spaced out at deeper levels regardless of the spacing prop. The component now ships with a wk-list marker class plus dedicated wk-list-spacing-{none,sm,md} rules in dist/wirekit.css whose doubled-class selectors win on specificity alone (no !important, so consumers can still override with even-higher-specificity rules when they explicitly need to). The spacing prop is now the single source of truth for inter-item vertical rhythm at every nesting depth.

  • bounce and spring reveal presets — final state no longer fades back to invisible. The wk-bounce-in and wk-spring-in keyframes declared opacity: 1 only at the 50% / 60% peaks; the 70% and 100% frames left opacity unspecified. With animation-fill-mode: both, some browser implementations held opacity at 1 from the last specified frame, but others interpolated back toward the underlying inline opacity: 0 after the animation settled — the element became visible briefly, then vanished. All four bounce / spring keyframe blocks now declare opacity at every frame, so the final-state behavior is browser-independent.

  • Source-file comment cleanup. PHP, Blade, and Alpine JS files in src/, resources/views/, and resources/js/ no longer carry internal-only project terminology in their inline comments and strings. Consumer-facing source now uses consistent product-public language throughout. No functional changes to any component, command, or helper.

  • wirekit:generate-changelogs sanitizer — broader pattern coverage. The codegen's commit-subject sanitizer (GenerateChangelogsCommand::sanitizeSubject()) now strips a wider set of internal-only phrasing before writing per-component changelog entries, plus a verb-prefix early-return that drops bullets which would otherwise reduce to bare verb stubs (fix:, security:, etc.). See the method docblock for the exhaustive pattern list.

Changed

  • README.md restructured from a comprehensive standalone reference to a concise getting-started overview. Installation walkthrough, Quick Start example, and the browser-support table are retained on the GitHub landing page; extended component tables and the customisation reference now live at docs.wirekit.app where they're searchable and link-rich. Net effect: the README acts as a 30-second "what is this and how do I install it" landing page; docs.wirekit.app remains the authoritative reference for everything else.

  • README.md badge row expanded from 3 to 11 badges across two visual rows. Row 1 (project status): Packagist version, Total Downloads, GitHub Actions test status, MIT licence, GitHub Stars, component count. Row 2 (stack): PHP ≥ 8.4, Laravel 12+, Livewire 4+, Tailwind CSS v4, Alpine.js. The CI badge in particular gives evaluators an at-a-glance signal that the build is green on main. Component-count badge auto-validates against ComponentRegistry via a Pest sync guard so the displayed number can never drift from the actual registry size.


v1.6.0

Minor release covering a downstream field-test brief, the brief's deferred extensions, and a unified motion subsystem. Twelve chunks shipped together so the topic — fonts, doctor diagnostics, stat description options, and a real animation system — can be closed in one release. No breaking changes; every new flag, prop, opt-in surface defaults to off so v1.5.0 consumers see byte-identical output.

Added

  • wirekit:install --font=<sans-key>, --font-serif=<key>, --font-mono=<key> — three optional flags for choosing which bundled WireKit font your project uses. Resolves the key against the bundled FontRegistry (21 GDPR-compliant local font CSS files), validates the category, and idempotently injects an override block into resources/css/app.css setting BOTH --font-{cat} (drives Tailwind font-{cat} utilities) AND --font-wk-{cat} (drives WireKit chrome). The two stay aligned automatically — closes the foot­gun where Tailwind utilities and WireKit chrome rendered different families. All three flags combinable; re-running with the same key produces byte-identical output. Wrong-category passes throw with a list of valid keys for the right category. Local fonts only — nothing is fetched from a CDN.

  • wirekit:install Tailwind config writer detection — the install command detects whether your project uses CSS-first Tailwind v4 config ([@theme](https://github.com/theme) block in app.css) or the legacy JS-config (tailwind.config.js theme.extend.fontFamily) and writes the font override to the right destination. CSS-first wins on tie (Tailwind v4 deprecates JS config). Custom config shapes that the auto-edit can't safely match log an actionable manual-edit hint instead of risking AST corruption.

  • wirekit:install interactive mode — running the command without flags in an interactive TTY opens a guided setup prompting for theme preset + sans/serif/mono font selection. CI / --no-interaction / scripted contexts skip prompts and run with v1.5.0-identical defaults.

  • wirekit:doctor token-alignment diagnostic — new section comparing seven Tailwind tokens against their matching WireKit tokens: --font-sans/serif/mono, --color-accent, --color-accent-foreground, --radius, --shadow. Each pair emits ✓ aligned, ⚠ mismatch (with actionable fix hint), or i skipped (var() reference or unset). Surfaces token drift at install-time rather than letting it ship to production.

  • docs/cli/wirekit-doctor.md — dedicated documentation page for the doctor command. Covers all twelve diagnostic checks with example output, three "Common failures and fixes" walkthroughs (font mismatch, accent-colour mismatch, var() skip), CI integration recipes (GitHub Actions + Claude Code stop-hook).

  • <x-wirekit::stat animate> Three Description Options — three opt-in props govern how the description text behaves during the value count-up animation. Option A descriptionDeferred defers the description fade-in until the counter settles (200ms ease-out), with aria-hidden mirroring visibility for screen-reader contract. Option B (default, status quo) renders the description statically. Option C descriptionAnimate animates the description text colour from --color-wk-text-muted--color-wk-text synchronously with the value count-up. Options A and C are mutually exclusive — passing both throws. All three honour prefers-reduced-motion: reduce. New ### Animation Scope subsection in docs/components/stat.md documents the contract.

  • <x-wirekit::reveal> component — NEW thin Blade wrapper that animates its slot content into view. One preset prop selects from 11 bases × in/out variants (fade, slide-up, slide-down, slide-left, slide-right, scale, zoom, flip, rotate, bounce, spring). Three trigger modes: viewport (default, IntersectionObserver), click, manual (consumer dispatches wirekit:reveal event). Three duration tokens: fast (150ms), normal (300ms, default), slow (600ms). Full prefers-reduced-motion: reduce honoring — element snaps to final state. New docs page docs/components/reveal.md with eight live preview blocks.

  • docs/animations.md — NEW reference page covering the entire motion subsystem: six new design tokens, 22 keyframes (11 bases × in/out), 22 utility classes (.wk-animate-{preset}-{in|out} plus three duration modifiers .wk-animate-{fast|normal|slow}), the wirekitAnimate Alpine helper API, and the reduced-motion contract.

  • Six new motion design tokens in dist/wirekit.css: --motion-wk-duration-fast: 150ms, --motion-wk-duration-normal: 300ms, --motion-wk-duration-slow: 600ms, --motion-wk-easing-out (decelerating cubic), --motion-wk-easing-in (accelerating cubic), --motion-wk-easing-spring (overshoot). The legacy --transition-wk-duration is aliased to --motion-wk-duration-fast for back-compat.

  • animateIn prop on 7 marketing components<x-wirekit::card>, <x-wirekit::feature>, <x-wirekit::hero>, <x-wirekit::stat>, <x-wirekit::callout>, <x-wirekit::alert>, and <x-wirekit::empty-state> accept an optional animateIn prop that wires the wirekitAnimate Alpine helper to the root. Pass a base name (fade, slide-up, bounce, …) or a full preset name (fade-in, slide-up-out). Default null preserves v1.5.0 render exactly. Components with built-in entrance transitions (modal, drawer, toast, tooltip, dropdown, popover) deliberately skip this prop to avoid double-motion conflicts.

  • wirekit:export-api-map — new top-level helpers group covering Alpine helpers exposed by the WireKit JS bundle. wirekitAnimate lists its full 22-preset enum, three trigger modes, three duration tokens, reduced-motion contract, and Blade-wrapper hint. wirekitStatAnimate documents its reactive state catalog (value, animating, progress). MCP servers + AI tooling consume this map to suggest correct x-data="…" shapes without grepping the source.

  • <x-wirekit::list> four new ordered marker typeslower-roman (i, ii, iii), upper-roman (I, II, III), lower-alpha (a, b, c), upper-alpha (A, B, C) join the existing disc, decimal, none set. All four render as <ol> with Tailwind v4 arbitrary-value list-style-type utilities. Mix freely across nested levels for legal-contract / academic / spec-style outlines.

Changed

  • <x-wirekit::main> horizontal padding aligned with <x-wirekit::header>. Previously padding="lg" produced 1.5rem all around (via --space-wk-lg); now uses --padding-wk-x-{size} for the horizontal axis (1rem at lg) — matching the same-name token used by Header. A sibling Header + Main pair (the canonical app-shell layout) now shares one vertical alignment line. Vertical padding stays on the generic --space-wk-{size} scale for breathing room. Applies to all five sizes (none/sm/md/lg/xl); none unchanged.

  • <x-wirekit::app-shell> sidebar breathing room. Sidebar <aside> wrapper gained lg:mt-[var(--space-wk-md,1rem)] lg:ml-[var(--padding-wk-x-lg)] so the in-flow column position at lg+ no longer sits flush against the header divider. Mobile / off-canvas behavior unchanged.

  • dist/wirekit.js bundle grew by ~0.4 KB (the wirekitAnimate Alpine helper, ~1 KB minified). Full bundle is 78.0 KB. Core bundle (chart-only) unchanged.

  • docs/cli.md documents all new install flags + the interactive mode + the new doctor token-alignment section + a cross-link to the dedicated wirekit:doctor reference page.

  • docs/animations.md interactive preset gallery — fifteen click-triggered preview blocks covering all eleven <x-wirekit::reveal> presets (fade, slide-up, slide-down, slide-left, slide-right, scale, zoom, flip, rotate, bounce, spring) plus a duration comparison row (fast / normal / slow). Each block uses trigger="click" so the animation re-fires on demand instead of waiting for scroll-into-view. Reduced-motion users see the final state immediately.

  • docs/dependencies.md bundle sizes refreshed to reflect the v1.6.0 additions: full bundle now ~23 KB gzip (78 KB raw, +wirekitAnimate Alpine helper), core unchanged at ~2 KB gzip (8 KB raw), ESM ~23 KB gzip (76 KB raw). Verified via gzip -c | wc -c.

  • Public package archive no longer ships .gitattributes or .gitignore. Both were dev-tooling artifacts whose only purpose was to filter the source tree at archive time; once the package is installed via composer require pushery/wirekit, neither file has a downstream role. Their absence from the published tarball makes consumer installs marginally cleaner. v1.6.0 is the first release where this applies.

Fixed

  • <x-wirekit::stat animate> reduced-motion display formatting. Previously the reduced-motion code path snapped value to the raw data-target string (e.g. "12500"); the in-flight animation tick formatted via toLocaleString() (e.g. "12,500"). Result: a user with prefers-reduced-motion: reduce saw a different format than a user without. Both paths now share a formatValue() helper so display is locale-consistent regardless of motion preference. Suffix preservation ($, %, etc.) also unified across paths.

  • Sandbox code-block schema — language is now an enum with 20 highlight.js-aligned grammar values (bash, php, blade, html, javascript, typescript, python, ruby, go, rust, sql, yaml, markdown, dockerfile, …). The wirekit:export-api-map output and any Sandbox-driven UI now expose the discoverable allowed-values list instead of leaving consumers to guess what the syntax-highlighter accepts.

  • Sandbox PropsValidator — HTML-form scalar coercion for type: int / type: bool / type: float. HTML form submissions are always strings ("4" from a <select>, "" from an unchecked checkbox); previously the strict type check rejected with expected int, got string. The validator now conservatively coerces unambiguous string shapes into the declared scalar type before the type check, while preserving rejection for non-numeric strings against int. Makes every schema with a typed-int / typed-bool prop usable from a Live-Sandbox UI without consumers needing to coerce client-side.


v1.5.0

Minor release covering a consumer-polish wave from real-world field-testing on a downstream landing-page build. Eight items in scope: a code-block screen-reader announcement fix, a wirekit:doctor post-build CSS sanity check, a hero-row xl size on <x-wirekit::feature>, a counter-animation animate prop on <x-wirekit::stat>, an asideWidth ratio refinement on <x-wirekit::hero>, eight new marketing-copy semantic aliases on heroicons-marketing, plus the syntax-highlighter contract documented in docs/theming.md.

No breaking changes — fully backward-compatible with v1.4.0.

Added

  • <x-wirekit::stat animate> — opt-in counter-animation prop. When set, the value text wraps in an Alpine wirekitStatAnimate data handler that animates 0 → target over 1.2s (ease-out cubic) once the stat scrolls 40% into view (IntersectionObserver). Respects prefers-reduced-motion: reduce — the value snaps to target with no animation if the OS-level setting is enabled. Static value remains visible inside the <span x-text="value"> fallback so search engines, no-JS browsers, and Alpine-pre-init paint all see the real number. Default false preserves v1.4.x output. Numeric prefixes / suffixes ($, %, etc.) are preserved through the animation; toLocaleString() formats the in-flight value with locale-aware thousand-separators.

  • <x-wirekit::hero asideWidth> — opt-in copy:aside ratio refinement under layout="balanced". Five values: 1/3 (aside ⅓), 2/5 (aside ⅖), 1/2 (50/50, matches default), 3/5 (aside ⅗), 2/3 (aside ⅔). Under any non-balanced layout (lead / centered / stacked) asideWidth throws via WireKit::validateProp in debug mode and is silently ignored in production. Default null preserves v1.4.x balanced 50/50.

  • <x-wirekit::feature size="xl"> — fourth chip-size value for hero-row features. Renders a 64×64 chip with a 32×32 inner icon. Existing sm / md / lg values keep their semantics; md remains the default.

  • Eight semantic copy aliases on the heroicons-marketing presetlive (signal), pulse (arrow-path-rounded-square), a11y (finger-print), sparkle (sparkles), security (lock-closed), speed (bolt), open-source (code-bracket), ai (cpu-chip). Names map to landing-page bullet copy rather than to the underlying icon name. Anti-collision verified by existing test suite — none of these shadow a base or heroicons-app alias.

  • Post-build CSS sanity check on wirekit:doctor / wirekit:verify — final check after the existing source-side [@source](https://github.com/source) verification. If public/build/manifest.json exists, the doctor walks every CSS file in the manifest and looks for any --color-wk-* token reference. Fails with a "run npm run build" hint if no CSS bundle in the manifest references WireKit tokens — catches the silent-failure mode where a consumer adds the [@source](https://github.com/source) line to app.css but forgets to rebuild. Skips silently in environments without a manifest (dev / pre-build / package-test scenarios).

  • docs/theming.md "Syntax-highlighter contract" subsection — under Accessibility & Contrast. Tells consumers wiring their own highlight.js / Prism / Shiki theme that token-level contrast must hit ≥4.5:1 against the active --color-wk-bg-elevated, in BOTH light and dark mode, across every theme preset. Documents the two foot-guns (unscoped WCAG overrides bleeding cross-mode; per-theme bg-elevated variations breaking single-pair audits) with a sketch of a Playwright contrast-audit recipe.

Fixed

  • <x-wirekit::code-block copy> symmetric screen-reader announcements. The polite live region (<span role="status" aria-live="polite">) was already in DOM but only spoke on success; the error path was silent (clipboard.writeText() rejection on non-secure-context or denied-permission cases threw an unhandled rejection and the user heard nothing). Click handler now wraps the clipboard write in .then()/.catch() so success AND failure each set a distinct, polite SR-only string: "Code copied to clipboard" / "Copy failed". WCAG 2.2 SC 4.1.3 (Status Messages) — both code paths now satisfy.

v1.4.0

Minor release covering accessibility, sandbox-schema, and component-render fixes that surfaced after v1.3.0 shipped, plus three additive opt-in prop additions: <x-wirekit::action-bar mode="static">, <x-wirekit::toast-region eventScope>, and the new SandboxRenderer::BODY_WRAPPERS map.

No breaking changes — fully backward-compatible with v1.3.0.

Added

  • <x-wirekit::action-bar mode="static"> — second layout mode for the action bar. The default mode="floating" keeps the existing position: fixed + viewport-centring transforms (fully back-compat). The new mode="static" flows inline with surrounding content (drops the fixed positioning + the centring transforms; keeps the same chrome — border, shadow, padding, rounded corners). Useful when the bar is part of a card / panel / dashboard rather than a viewport-floating overlay.

  • <x-wirekit::toast-region eventScope="…"> — optional CSS selector that scopes incoming toast events by DOM containment. When set, only events whose dispatching element is inside an ancestor matching the selector are handled. Useful for "per-section toast surfaces" where multiple toast regions on the same page must not cross-talk. The existing name parameter (event-name routing) is unchanged and still works in parallel; eventScope is additive. Default null preserves the global-listener behaviour.

  • Pushery\WireKit\Sandbox\SandboxRenderer::BODY_WRAPPERS map — auto-wraps the sandbox body slot in a sub-component for primitives whose composition requires it. The Card schema now wraps its body in <x-wirekit::card.body> so consumer-side sandbox renders carry the full card chrome with padded body content instead of a bare rounded pill. Open extension point: future multi-slot primitives (tabs, accordion, …) can opt in by adding an entry.

Fixed

  • Resizable handle drag no longer flickers from text-selection in adjacent panels. The handle's pointerdown already called setPointerCapture(), but the body user-select was never disabled during drag — so the cursor crossing a sibling panel highlighted that text and the browser repainted the selection mid-drag (visible as a "flicker"). onPointerDown now captures the prior inline value of body.style.userSelect and sets it to 'none'; onPointerUp restores it. pointercancel already routes through onPointerUp so a tab-switch mid-drag also cleans up — no leak.

  • Vertical <x-wirekit::resizable> no longer collapses to 0 height when the wrapper has no explicit size. [data-wk-resizable][data-wk-direction="vertical"] carries contain: size, which requires an explicit container size — without one, the panels' percent heights resolve against a 0-height container and the whole component disappears. Added a default min-height: 16rem on the vertical wrapper so unstyled containers still render visibly. Authors with explicit inline style="min-height: …" keep their value (CSS specificity favours the inline rule).

  • WCAG 4.1.2 (Name, Role, Value) sweep across 11 interactive components. Alpine's reactive :aria-* bindings only emit the attribute after the JS boots — initial server-rendered HTML lacked the required aria-expanded (combobox, multi-select), aria-valuenow (image-compare, range-slider), or aria-checked (rating, segmented-control). Each affected component now ships a static aria-foo="default-value" immediately before the :aria-foo="reactive-binding" so server-rendered HTML is WCAG-complete from the first paint and Alpine overrides reactively after hydration. Plus: <x-wirekit::brand> in logo-only mode auto-injects aria-label="Home" (caller's aria-label still wins); <x-wirekit::toast-region> adds role="region" so its aria-label="Notifications" is permitted; <x-wirekit::date-picker>, <x-wirekit::color-picker>, <x-wirekit::slider> accept a new label prop and fall back to a sr-only label derived from name when no label / aria-label / aria-labelledby is provided; <x-wirekit::navigation-menu.item> link-mode now falls back to the default slot when no trigger prop is set so the canonical <x-wirekit::navigation-menu.item href="/x">Label</x-wirekit::navigation-menu.item> pattern produces a non-empty link.

  • Card sandbox schema rendered as a bare rounded pill instead of a proper card. The Card sandbox schema declared padded / bordered / elevated boolean props that the Card primitive never read in its [@props](https://github.com/props)([...]) block — the renderer emitted them as raw HTML attributes (silently no-op), and the body slot was never wrapped in <x-wirekit::card.body>, so slot text pressed flush against the rounded card edges. Schema rewritten to use the real variant prop (outlined / elevated / flat); body slot now auto-wraps via the new BODY_WRAPPERS map.

  • code-block sandbox schema declared lang, but the actual prop is language. The renderer emitted <x-wirekit::code-block lang="php"> which the component ignored — the syntax-highlight language class was never applied. Renamed the schema key to match the component contract.

  • text sandbox schema declared a muted boolean. The Text primitive has no muted prop; it uses a string variant enum with allowed values default / muted / subtle / accent / success / warning / danger. Replaced the boolean with the real variant enum.

  • Callout sandbox schema dropped the un-renderable title reference. title is a named slot in the Callout primitive ([@isset](https://github.com/isset)($title)), not a [@props](https://github.com/props) entry, so the renderer's string-prop-as-HTML-attribute path could never populate it. Schema now carries only the props the renderer can actually deliver. (alert keeps title because <x-wirekit::alert> declares title as a real [@props](https://github.com/props) entry — different shape.)

  • button and badge sandbox schemas: slot key labelbody. Earlier iterations declared label as the slot-content key, which the renderer treated as an HTML attribute and never inserted into the slot — sandbox renders produced empty buttons / badges. Renamed to the renderer's reserved body convention.

  • clipboard-button button-width regression on state change. The first iteration of the stable-width fix used x-show to toggle which label rendered inside the grid cell. x-show sets display: none on the inactive label, removing it from layout entirely; the grid cell then collapsed to whichever child was visible — defeating the stable-width goal. Replaced with :style="{ visibility: ... }" toggling so both labels stay in layout permanently and the grid cell sizes to the wider one. The copied-state span also carries a static style="visibility: hidden" so its layout slot is reserved before Alpine init evaluates the bindings — no flicker on first paint, no width jump on state change.

  • ticker rendered ++8.4% for already-signed-string deltas. The delta-formatting block prepended + to positive deltas, then interpolated the original input verbatim — inputs that already carried an explicit sign rendered with double prefix. Strip leading + from string deltas before re-deriving the sign from the numeric value. Both "8.4" and "+8.4" now render identically as +8.4%. Negative signed strings ("-1.2") keep their leading -. Numeric inputs (int / float) and unsigned strings remain unchanged.

  • app-shell defaulted to a width that didn't fill its parent. Added w-full to the shell's base classes so it correctly fills its parent container regardless of layout context.

  • code-block defensive styling against inherited inner <pre> / <code> background. Added bg-transparent and radius-none on the inner elements to prevent host-page CSS from bleeding through.

  • WCAG 1.4.3 contrast sweep for soft-bg foreground tokens. Round-2 polish on calendar week-view chips, finance cells, opacity-overlaid cards, and CTA buttons — every soft-tinted background now pairs with a foreground token that meets the 4.5:1 ratio in both light and dark mode. Plus a P0 fix to dist/wirekit.css (:root {} instead of [@theme](https://github.com/theme) {} so a plain <link rel="stylesheet"> load works without the build pipeline).

  • A11y polish on shipped components: center component fills its parent, callout becomes a <section> for proper landmark semantics, kanban-column scroll body gets the missing scroll-region a11y wiring.

Changed

  • README.md icon section now lists both stackable extension presets (heroicons-app, heroicons-marketing) consistently. Earlier the stackable presets only surfaced in the Available-Presets table near the bottom of the icon README block while top-of-page sections (Requirements tip, Configuration block, Switching Presets) referenced base presets only — discoverability gap closed.

v1.3.0

Minor release covering eight new blueprint primitive components, an Artisan command suite for scaffolding / diagnostics / asset publishing / AI-tooling integration / machine-readable manifests, the Livewire sandbox primitives library, and a security-hardening sweep across target="_blank" link rendering plus the JSON-encoder layer.

No breaking changes — fully backward-compatible with v1.2.x.

Added

  • Blueprint primitive components (8)<x-wirekit::price> (currency formatting with size variants), <x-wirekit::date-separator> (timeline / chat date divider), <x-wirekit::reaction> (emoji reaction button with count), <x-wirekit::ticker> (live data ticker with delta indicator), <x-wirekit::toolbar> (button group bar with slots), <x-wirekit::message> (chat / thread message bubble with alignment), <x-wirekit::kanban> and <x-wirekit::kanban-column> (kanban board with column composition). All use design tokens exclusively, support the Intent × Surface API where applicable, and follow WAI-ARIA patterns.

  • Sandbox primitives library (src/Sandbox/) — reusable security-hardened render pipeline for any consumer project that needs to render WireKit components from untrusted JSON props (live-preview iframes, prop-editor UIs, etc.):

    • SandboxRenderer::render($component, $props, $ip): RenderResult — main entry point. Validates → sanitizes → renders → audit-logs. Returns RenderResult (success or 422-shaped rejection); never throws.
    • PropsValidator — enforces per-component prop schema, type-checks, rejects strings >10 KB and arrays nested >5 deep (DoS defence), HTML-escapes every string defence-in-depth so even a slot using {!! !!} cannot surface raw payload content.
    • ComponentAllowlist — strict kebab-case regex + ComponentRegistry cross-check + sandbox-schema presence guard. Path-traversal characters, namespace separators, whitespace, uppercase — all rejected with 422-shape, never 500.
    • SandboxSchemaRegistry — in-memory registry of per-component prop allowlists with allowed_values enums. Initial coverage of 11 starter components (button, badge, callout, alert, card, code, code-block, kbd, heading, text, link); the renderer is functional with whatever schemas are seeded, full coverage will follow incrementally.
    • SandboxAuditLog — file-based daily-rotating log (storage/logs/sandbox/YYYY-MM-DD.log). IPs sha256-truncated to 16 chars so logs are useful for rate-pattern auditing but not for tracking individuals.
    • RenderResult / ValidationResult — immutable result objects with public-readable ok / violations / html / schema properties so consumer-side prop-editor UIs can read them via get_object_vars().
  • <x-wirekit::ticker> dark-mode contrast fix — switched the delta text from the bare --color-wk-success / --color-wk-danger foundation tokens to the *-text variants (which are calibrated for ≥4.5:1 WCAG 1.4.3 contrast against surface tokens in BOTH light and dark mode).

  • .cursor/rules/wirekit.mdc — single-file (~150-line) Cursor rules ruleset covering component invocation syntax, the Intent × Surface variant system, design tokens, icon usage, layout primitives, typography primitives, modal / drawer / dropdown trigger patterns, accessibility defaults, Livewire integration patterns, browser-support baseline, and the full CLI. Cursor / Codeium / other native .mdc editors pick up the rules automatically for every *.blade.php and *.css file in the project.

  • php artisan wirekit:component {name} — scaffolds a custom Blade component derived from a WireKit base into resources/views/components/custom/{name}.blade.php. --base flag picks the source (defaults to {name}); --force allows overwriting an existing custom file. Resolves both flat (button) and dotted (card.header) base names.

  • php artisan wirekit:publish-icons {preset} — targeted icon publishing. Copies a single preset's SVG directory from vendor/{package}/resources/svg/ to public/vendor/wirekit/icons/{preset}/. Refuses with a precise composer require ... fix line when the underlying icon-set package is not installed. Supports heroicons, heroicons-app, heroicons-marketing, lucide, phosphor, tabler.

  • php artisan wirekit:doctor — alias for wirekit:verify under the more conventional Laravel-ecosystem name. Both registrations stay in parallel for backward compatibility — existing CI scripts and docs that reference wirekit:verify keep working.

  • php artisan wirekit:cursor-rules — copies the package's .cursor/rules/wirekit.mdc into the consumer project's .cursor/rules/ directory. --force to overwrite an existing copy.

  • php artisan wirekit:export-api-map [--pretty] — emits an AI-friendly hierarchical sitemap covering eight groups: components, themes, fonts, icons, layouts, blueprints, recipes, and commands. Superset of wirekit:export-json. Output is XSS-safe via JSON_HEX_TAG. Designed for MCP servers and other AI tooling that need a single entry point to enumerate every WireKit surface.

  • php artisan wirekit:export-blocks [--pretty] — emits a machine-readable JSON manifest of every layout + blueprint with frontmatter metadata (category, tags, dependencies, responsive, dark_compatible) plus generated preview_url and source_url. Consumable by gallery UIs and AI tooling for filterable browsing.

  • php artisan wirekit:generate-changelogs [--dry-run] — regenerates per-component ## Changelog sections from filtered Git history, with full sanitization of internal-only references before writing public output.

  • Password input accessibility — toggle button has a static aria-label fallback for pre-Alpine-hydration accessibility scans.

  • Toggle auto-labeling — component auto-generates an aria-label from the name prop when neither label nor aria-label is provided.

  • Config defaultsconfig/wirekit.php ships size defaults for <x-wirekit::price> and <x-wirekit::ticker>.

Changed

  • Pushery\WireKit\Sandbox\RenderResult now carries a public-readable ?array $schema property in addition to ok / html / violations. SandboxRenderer::success() echoes the per-component schema back so consumer-side prop-editor UIs can render the editor without a second round-trip to the schema registry.

  • target="_blank" auto-protection hardened — the rel attribute injection in all 13 link-rendering components (Button, Dropdown Item, Command Palette Item, Menubar Item, Navigation Menu Item, Navigation Menu Link, Navbar Item, Sidebar Item, Link, Brand, Card) now uses an explicit override pattern that prevents caller-supplied rel values from silently defeating the noopener noreferrer protection. Coverage extended to four additional components that were previously missing the pattern: Link, Navbar Item, Navigation Menu Link, and Sidebar Item.

  • Chart.js dark-mode refreshchart.update() replaces chart.update('none') in the MutationObserver callback. Chart.js v4's 'none' mode skips the style-resolver pass when only color properties change, leaving stale colors rendered. The observer now watches both <html> and <body> for .dark class changes, supporting both mounting conventions.

Fixed

  • WCAG 1.4.3 dark-mode contrast on <x-wirekit::ticker> delta text — the --color-wk-success / --color-wk-danger foundation tokens previously yielded 3.66:1 / 4.05:1 against dark surface tokens (fails the 4.5:1 AA threshold for small text). Fix uses the *-text token variants which are calibrated for ≥4.5:1 in both modes.

  • CTA accent variant dark-mode contrast — the accent variant used a hardcoded text-white class that fails when --color-wk-accent inverts in dark mode. Now uses text-[var(--color-wk-accent-fg)] which auto-switches correctly.

  • Section accent background token — the accent background variant referenced a non-existent --color-wk-primary CSS variable. Replaced with --color-wk-accent background and --color-wk-accent-fg text color, matching the established pattern used by CTA and Badge.

  • Dropdown trigger ARIAaria-haspopup, aria-expanded, and aria-controls were placed on a non-interactive <div> wrapper. Moved to the inner interactive element via x-init.

  • Progress bar accessible name — component only wired aria-labelledby when the label prop was set. Usages without labels now receive a sensible "Progress" default.

  • App-shell sidebar backdrop — the mobile sidebar dim overlay was traversable by screen readers, defeating the focus-trap intent. Added aria-hidden="true".

  • dist/wirekit.css parses correctly when loaded via <link>. Previously the entire token palette lived inside a Tailwind v4 [@theme](https://github.com/theme) {} compiler block. Browsers correctly skip unknown at-rules per the CSS spec, which meant the documented "fastest path" — using the [@wirekitStyles](https://github.com/wirekitStyles) Blade directive that embeds the file via <link rel="stylesheet"> — left zero --color-wk-* variables defined in the CSSOM. Components rendered without color tokens. The file now emits a standard :root {} (light) and .dark {} (dark) block directly, so both consumption paths resolve identically: the [@wirekitStyles](https://github.com/wirekitStyles) directive AND [@import](https://github.com/import) from app.css. The [@custom-variant](https://github.com/custom-variant) dark (&:where(.dark, .dark *)); directive is preserved at the top of the file (harmless under <link>, useful under [@import](https://github.com/import) for Tailwind dark: variant support).

  • WCAG 1.4.3 contrast — light-mode *-text and text-muted tokens recalibrated. Three "soft-bg foreground" tokens were below the AA 4.5:1 threshold against the 12% color-mix() soft-tone backgrounds used by badge / alert / callout / feature / message / reaction / toast-region, and text-muted was below threshold on bg-muted:

    • --color-wk-success-text: green-700 → green-800. Was 4.33:1 on soft-success bg, now ~6.17:1.
    • --color-wk-danger-text: red-500 → red-700. Was 3.89:1 on soft-danger bg, now ~5.13:1.
    • --color-wk-warning-text: amber-700 → amber-800. Was 4.41:1 on soft-warning bg, now ~6.04:1.
    • --color-wk-text-muted: neutral-500 → neutral-550. Was 4.26:1 on bg-muted (#f7f7f7), now ~6.13:1. text-subtle and text-placeholder are unchanged (they only appear on white where neutral-500 already meets 4.74:1).
  • Bare text-[var(--color-wk-{success,warning,danger})] on text content swept to *-text variants. Affected component-internal usages: <x-wirekit::text> semantic variants (success / warning), <x-wirekit::stat> up trend, <x-wirekit::price> delta intent (success / danger), <x-wirekit::feature> soft tones (success / warning). Decorative aria-hidden SVG icons inside alert / callout / toast-region / code-block / rating left at the bare tone (graphic-element semantics, 3:1 threshold via WCAG 1.4.11).

  • <x-wirekit::calendar> event chips no longer render white text on green-500 / amber-500. Month-view and week-view event chips (background: var(--color-wk-success); color: var(--color-wk-bg)) yielded 3.21:1 / 2.13:1 — fail. Component now uses color: var(--color-wk-success-fg) / color: var(--color-wk-warning-fg) (zinc-900 on the tone bg — ~6.95:1 / ~9.03:1).

  • <x-wirekit::calendar> week-view chip subtexts collapsed. Opacity-blended subtexts (<span style="opacity: 0.8">…</span>) dropped contrast below 4.5:1 in light mode (4.23:1 against the tone bg). Replaced the two-line Primary + opacity-dimmed subtext pattern with a single-line Primary · Subtext middle-dot label that passes both modes at the full token contrast.

Security

  • /components.json JSON encoder hardened with JSON_HEX_TAG — brings wirekit:export-json in line with the existing wirekit:export-api-map and wirekit:export-blocks contracts. Without JSON_HEX_TAG, a component description containing </script> could break out of a <script type="application/ld+json"> block where the manifest is embedded.

v1.2.1

Patch release to fix the automated release pipeline.

Fixed

  • Release workflow tag pushgit push origin "${TAG_NAME}" failed with src refspec matches more than one when both the release branch and the new tag shared the same name. Changed to git push origin "refs/tags/${TAG_NAME}" to explicitly push the tag reference.

v1.2.0

Major feature release. WireKit now covers full-page composition: a 10-component layout primitives system, a 9-component typography primitives system, app-shell scaffolding, and a marketing-page toolkit — plus a new unified intent × surface variant API, a component registry, five new artisan commands, and a rewritten release pipeline.

Added

  • Layout primitives (10 components)<x-wirekit::container> (width-constrained wrapper with max/padding/center props), <x-wirekit::stack> (vertical flex), <x-wirekit::row> (horizontal flex), <x-wirekit::grid> (responsive column syntax cols="1 sm:2 lg:3"), <x-wirekit::section> (full-width with background and divider variants), <x-wirekit::spacer> (flex-grow), <x-wirekit::divider> (horizontal/vertical with label and variants), <x-wirekit::center> (flex centering), <x-wirekit::aspect-ratio> (native CSS aspect-ratio), and <x-wirekit::visually-hidden> (sr-only wrapper).
  • Typography primitives (9 components)<x-wirekit::heading> (h1–h6 with auto-sizing, accent, tracking), <x-wirekit::text> (body text with size, variant, weight, align, truncate, line-clamp), <x-wirekit::link> (styled anchor with external-link detection), <x-wirekit::code> (inline monospace), <x-wirekit::code-block> (multi-line with copy button and filename), <x-wirekit::kbd> (keyboard key indicator), <x-wirekit::list> (ul/ol with spacing), <x-wirekit::blockquote> (left-border with citation), and <x-wirekit::mark> (text highlight wrapper).
  • <x-wirekit::highlight> — typography helper that highlights query matches inside a block of text. Pairs with the Prose component's new variant prop.
  • App Shell components (6 components)<x-wirekit::app-shell> (full-page layout with responsive sidebar toggle), <x-wirekit::header> (sticky header with optional container), <x-wirekit::main> (content area with padding variants), <x-wirekit::brand> (logo + name link), <x-wirekit::profile> (avatar + name display), and <x-wirekit::sidebar.toggle> (hamburger button).
  • Marketing components (5 components)<x-wirekit::hero> (landing-page hero with variants, gradient, slots), <x-wirekit::feature-grid> (responsive feature-card grid), <x-wirekit::feature> (individual card), <x-wirekit::cta> (call-to-action banner with dark and accent variants), and <x-wirekit::footer> (columns, brand, legal slots).
  • Unified Intent × Surface variant system — new intent and surface props on Button and Badge. Six intents (neutral, accent, success, warning, danger, info) × five surfaces (filled, soft, outline, ghost, link) generate consistent combinations via a central VariantResolver. The legacy variant= API is preserved for full backward compatibility.
  • ComponentRegistry — central catalog of every WireKit component with category and description metadata. Anti-drift tests enforce registry ↔ filesystem consistency (every Blade file has an entry, no stale entries, valid categories, non-empty descriptions).
  • php artisan wirekit:list — lists all components grouped by category.
  • php artisan wirekit:show {name} — displays props, sub-components, and docs path for a given component.
  • php artisan wirekit:install — one-command setup: publishes config and assets, prints layout directive snippet, adds published assets to .gitignore.
  • php artisan wirekit:theme {preset} — injects a theme preset's CSS block into the consumer's app.css.
  • php artisan wirekit:make {name} — scaffolds a Livewire page pre-wired with WireKit components.
  • Design tokens — new spacing scale (--space-wk-xs through --space-wk-2xl), container max widths (--size-wk-container-sm through --size-wk-container-2xl), --text-wk-3xl, --font-wk-heading-line-height, and semantic colors (--color-wk-bg-inverse, --color-wk-text-inverse, --color-wk-border-strong, --color-wk-warning-bg) with dark-mode counterparts.
  • Accessibility sections in docs — Button, Input, Textarea, and Label pages document label pairing, aria-invalid/aria-describedby wiring, :user-invalid styling, focus-visible rings, disabled states, icon-only aria-label guidance, external-link auto-protection, and required-indicator semantics.
  • 162 new tests across the 19 layout and typography primitives, bringing the total test suite to 1316 tests (3712 assertions).

Changed

  • target="_blank" auto-protection — all link-rendering components (Button, Dropdown Item, Brand, Card, Navigation Menu Item, Link, Menubar Item, Command Palette Item) now automatically inject rel="noopener noreferrer" (tabnabbing prevention) and a <span class="sr-only">(opens in new tab)</span> screen-reader hint whenever target="_blank" is set. No consumer changes needed. Components with both button and link modes include a guard so the injection only fires when href is present.
  • <x-wirekit::prose> — gained a variant prop for tighter integration with the new typography primitives.
  • Release pipeline — the public-repo release workflow now opens a pull request on pushery/wirekit from a release branch, squash-merges it into main with the CHANGELOG section as the commit body, and tags the squashed commit. Result: one clean squash commit per release on public main with the full CHANGELOG embedded. Retries are safe.

Fixed

  • Grid and Feature Grid responsive classes missing from consumer bundlesgrid.blade.php and feature-grid.blade.php built Tailwind class names via runtime PHP string concatenation ("{$breakpoint}:grid-cols-{$value}"). Tailwind's content scanner only sees literal strings, so these classes were dropped from consumer CSS and previews rendered as stacked items instead of responsive grids. Replaced with a literal class map covering all valid breakpoint × column combinations (Grid: 72 entries; Feature Grid: 36 entries). Invalid tokens surface via WireKit::validateProp().
  • Layout-doc preview rendering — 40 <x-wirekit::card> occurrences in container, grid, stack, row, section, spacer, and divider docs wrapped raw content directly in the card without <x-wirekit::card.body>. Card provides border/radius/background but not padding — previews rendered as thin-bordered boxes with cramped text. All 40 occurrences now use card.body. Aspect-ratio preview refactored to a styled centering frame.
  • Feature component test and docs coverage — added 8 render tests and a dedicated docs/components/feature.md page with three previews. Registered in the DocsPreviewRenderTest minimum-previews guard.
  • Documentation sidebar coverage — five components (Code Block, Header, Main, Brand, Profile) now have their own docs pages and dedicated sidebar entries. Previously their README rows pointed to parent pages, leaving them invisible in the navigation.
  • README component links — the Feature row now links to its own docs page instead of Feature Grid. Highlight component added to the Typography table.
v1.1.1

Feature release with a complete theme overhaul, two new components, and accessibility improvements across the board.

Added

  • <x-wirekit::image-compare> — new before/after image comparison slider with horizontal and vertical orientation, pointer/touch drag, full WAI-ARIA Slider Pattern keyboard support, wire:model binding (deferred, live, and debounced), screen-reader live region, reduced-motion guard, and four personalization blocks. Ships in both the full and core JS bundle.
  • Liquid Glass extension — optional glassmorphism module installed via php artisan wirekit:glass install. Tier 1 provides frosted-glass backdrop-filter effects (all browsers). Tier 2 adds SVG feDisplacementMap refraction (Chrome/Chromium only). CSS classes: .wk-glass, .wk-glass-refract. Blade component <x-wirekit::glass /> for the layout head.
  • VT323 font — bundled as 21st locally served font (OFL 1.1 license).
  • Modal and drawer headers now auto-render a close button by default. The button respects the dismissible prop and can be disabled via :close="false" on the header. Personalizable via WireKit::personalize('modal.header', ['close' => '...']).

Changed

  • Complete theme overhaul — all 7 presets rewritten with WCAG 2.2 AA compliance verified for every text-on-surface, button-label, and semantic-message pair in both light and dark mode.
  • Default theme migrated from zinc to neutral palette (zero chroma, no blue cast). Inter font set as default sans. Updated shadows, letter-spacing, and dark-mode borders.
  • Cupertino theme — new Apple-aesthetic preset replacing Slick. Uses -apple-system font stack, iOS ease-out easing, Apple HIG dark colors, blue-600 accent, and 0.5px hairline borders.
  • Minimal theme — rewrite: 0px radius, 2px ring, neutral-200 input backgrounds for visible boundaries.
  • Soft theme — rewrite: DM Sans font, violet accent, wider blur shadows, ease-out easing.
  • Material theme — rewrite: M3 standard decelerate easing, indigo accent, Roboto font.
  • Brutalist theme — rewrite: neutral palette, JetBrains Mono font, explicit border-color tokens fixing WCAG 1.4.11 contrast failures.
  • Retro Terminal theme — rewrite with 3 WCAG fixes: ring width 1px→2px, danger-fg contrast fix, explicit success/warning foreground tokens.
  • Context menu panel now teleports to <body> by default, consistent with Modal, Drawer, and other overlay components. Opt-out via teleport="false".
  • Resizable panels now support symmetric pair-drag in 3+ panel layouts — dragging a handle resizes both adjacent panels proportionally, matching industry-standard splitter behavior (VSCode, Figma, split.js).

Fixed

  • Tour component overlay and step panels now teleport to <body> via Alpine's x-teleport, matching the pattern used by Modal and Drawer. This ensures correct Floating UI positioning regardless of ancestor CSS transforms or containing blocks. Steps use x-show instead of x-if for simpler DOM lifecycle.
  • Tour step no longer flickers at (0,0) on first show — a CSS fallback parks steps off-screen until Floating UI positions them.
  • Chart component style attribute collision — caller-supplied style now merges correctly with the component's own height/background declarations.
  • Timeline last-item trailing padding now correctly collapses when after="true" adds a continuation line.
  • Toggle OFF track now meets WCAG 1.4.11 non-text contrast (changed from bg-muted to border token).
  • Checkbox and radio hover border added for interactive discoverability on small 20×20 elements.
  • WCAG 2.2 AA contrast fixes across all base tokens: success-fg, warning-fg, text-subtle, text-placeholder, and dark-mode danger values corrected.
  • Scroll-to-top preview — button was invisible in documentation previews because x-cloak + x-show hid it before scroll events could fire. Added forceVisible prop that disables the scroll listener and keeps the button permanently visible.
  • Range slider — added missing pointercancel event listener cleanup to prevent listener accumulation on interrupted drag gestures.
  • External doc link — Liquid Glass cross-reference in theming guide changed from absolute URL to internal path for environment portability.

Removed

  • Slick theme — merged into Default. The theme set is now: Default, Minimal, Soft, Material, Brutalist, Retro Terminal, Cupertino.

v1.1.0

See CHANGELOG for details.

v1.0.1

See CHANGELOG for details.

v1.0.0

First stable release of WireKit — a free, MIT-licensed UI component library for Laravel Livewire built on Tailwind CSS v4, Alpine.js, and PHP 8.4+.

Added

  • 71 Blade components spanning form controls, overlays, navigation, display, feedback, and specialized categories — every component uses design tokens exclusively (zero hardcoded colors), auto-switches between light and dark mode via .dark class, and supports personalization via WireKit::personalize().
  • Form controls: Input, Textarea, Select, Checkbox, Toggle, Radio Group, Range Slider, Color Picker, Date Picker, File Upload, Pin Input, OTP Input, Multi-Select, Combobox, Rich Text Editor (Tiptap).
  • Overlay components: Modal, Drawer, Dropdown, Popover, Tooltip, Hover Card, Command Palette, Context Menu, Toast (with toast region), Confirm Dialog, Tour.
  • Navigation: Navigation Menu, Breadcrumb, Pagination, Tabs, Stepper, Scroll-to-Top.
  • Display components: Avatar, Badge, Card, Accordion, Timeline, Carousel, Image Compare, Scroll Area, Skeleton, Prose, QR Code, Clipboard Button, Collapsible, Separator, Resizable Panels, Tree View, Calendar, Stat Card, Data Table.
  • Feedback: Alert, Progress Bar, Rating, Spinner.
  • Typography & layout: Fonts (20 GDPR-compliant Google Fonts served locally), Icon system (4 presets: Heroicons, Lucide, Phosphor, Tabler with 26 semantic aliases), Chart (via Chart.js adapter).
  • Theming system with ~80 CSS custom properties, 7 theme presets (Default, Minimal, Soft, Material, Brutalist, Retro, Slick), all WCAG 2.2 AA compliant in both light and dark mode.
  • Font system — 20 locally bundled Google Fonts (9 sans, 5 serif, 6 mono), zero external requests, configurable via config/wirekit.php, served from the app's own domain for CSP and GDPR compliance.
  • Icon system — pluggable SVG icon presets with resolved aliases, caching, and a <x-wirekit::icon> component that renders any icon by name.
  • Chart system — adapter-based architecture with a Chart.js adapter, a class-based <x-wirekit-chart> component, and dark-mode-aware color tokens.
  • JavaScript bundles: full bundle (Floating UI + focus-trap, ~76 KB), core bundle (chart + image-compare only, ~7.5 KB), and ESM bundle for tree-shaking.
  • Accessibility: every interactive component follows WAI-ARIA Authoring Practices — proper role, aria-* attributes, keyboard navigation, focus management, screen-reader announcements via aria-live regions. Tour and modal use role="dialog", combobox implements full ARIA 1.2 combobox pattern, all form components support error and description associations.
  • Personalization system — three levels of customization: CSS variable overrides (theme presets), WireKit::personalize() for global class overrides, and WireKit::scope() for per-instance class overrides.
  • Reduced-motion support[@media](https://github.com/media) (prefers-reduced-motion: reduce) disables all WireKit animations and transitions, including skeleton pulse, progress bar indeterminate, and Alpine x-transition durations.
  • 27 layout recipes (application shells, auth pages, dashboards, error pages, marketing pages) as copy-paste Blade templates.
  • 59 blueprint pages across 10 verticals (CRM, Project Management, Chat, Mail, Finance, Analytics, Helpdesk, HR, Calendar, E-Commerce) demonstrating real-world page compositions.
  • 967 automated tests covering component rendering, accessibility contracts, design token compliance, and documentation preview rendering.

Browser Support

Chrome 111+, Edge 111+, Safari 16.4+, Firefox 128+ — matching the Tailwind CSS v4 browser baseline.

v0.0.1-alpha
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
aimeos/prisma
besmartand-pro/php-quality-config
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky
spatie/mailcoach-vapor
spatie/laravel-javascript-views