Weave 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

A free, open-source UI component library for Laravel Livewire, built on Tailwind CSS v4 and Alpine.js. Forms, navigation, overlays, layout, marketing and data display components — themeable, accessible and dark-mode ready.

View on GitHub
Deep Wiki
Context7
v2.64.0

Minor. A component keeps working when a Livewire update renders it with new values, a URL written into a component tag as href="{{ $url }}" keeps every query parameter, and the components that keep their value out of sight send it under wire:model.blur and wire:model.change, as a rating, a range slider and a calendar now send the value just chosen. A field's label names the combobox, the date picker or the multi-select inside it, and buttons and fields show their focus ring under the minimal, retro-terminal and brutalist presets. Glass looks like liquid glass, in four levels from clear to opaque. Info has tokens of its own, a code block can wrap its lines, a one-time code can fill its width, and x-wk-scroll-into-view brings a result into view when an action makes it appear far from its button. A form says when it holds unsaved changes and can ask before the reader leaves, a modal or a drawer asks before the reader discards what they typed into it, every field can explain itself in a tooltip from a question mark beside its label, a select with nothing to choose says what is missing and where it is created, a toggle can name its state in words and take a color for each state, a click on a range slider's track brings the nearer handle there, a resizable handle takes clicks on two step controls, a kanban card moves with two clicks in reorder mode, an app rail item can say what its counter counts, and the entries of an overflow nav take a class block of their own. A search field in a page's header opens the command palette with what was typed, a page header can stay at the top and set itself apart once the content scrolls under it, text keeps to a reading width, and a data list can set each label above its value. A field you tab to, or a fragment link's target, comes to rest beside a pinned bar instead of under it. A click inside a dialog reaches the page's own listeners, Back returns to where a page was left from an open overlay, the open list of a combobox or a multi-select closes when Tab moves on and a choice made with the mouse leaves the focus in its field, containers pad evenly, an invalid field stays red under the pointer and in its focus ring, a link button sits flush with its sentence, the clear button of a field stands as deep in its frame as an icon, and the knob of a switch and a counter on the current rail module stay visible.

An input and a textarea count their characters as the reader types, and a combobox, a multi-select and a segmented control take the language of an option, so a list of languages names each one in its own voice. Every tab stop the kit renders draws the kit's focus ring, and three classes put that ring on a tab stop of your own. A menubar, a navigation menu and a popover close when a press outside them ends rather than when it begins, and a phone field's country picker, an OTP input's first box and an attachment's upload state reach a screen reader under their own words.

Security

  • The font route serves only what lies inside the font directory. It compared the resolved path with the directory's own path, so a file beside the directory whose name begins with fonts, such as resources/fonts-extra/, would have passed as if it were inside. The package ships no such file, so nothing outside the font directory was reachable. The route now compares against the directory and a separator, and the asset routes make the same check.

Added

  • A modal or a drawer asks before the reader discards what they typed, and does so by default. Once the reader has changed a field in it, Escape, a click beside it and its close button first ask "Discard your changes?" in the reader's language, and it stays open unless they agree. A field x-wk-unsaved marks as unsaved counts as a change too. A Cancel button and a close your page makes ask nothing, and neither does an overlay the reader has not changed, one your server filled after it opened included. :confirm-discard="false" turns the question off for one overlay, 'confirm-discard' => false under components.modal or components.drawer in the config for all of them, discard-question sets your own question, and data-wk-discard-ignore leaves a field such as a filter box out.
  • Every field can explain itself with help. A small question mark beside the label of an input, a select, a textarea, a checkbox, a toggle or any other field with a label shows the text in a tooltip on hover and on keyboard focus, and Escape hides it. A screen reader hears the same text as the field's description. The button sits beside the label rather than inside it, so the field's name stays the label's text, and a row with help is as tall as one without, so fields side by side keep their controls on one line. With a visually hidden label there is no button and the text still describes the field. In a field wrapper the help's id is offered for the control in the slot, as the hint's is. A click on the question mark, and Enter or Space on it, dispatches wirekit:field-help with the field's name and label, so a page can open its own modal with the whole explanation; Livewire hears it with #[On('wirekit:field-help')]. A field without help renders as before.
  • An input and a textarea count their characters as the reader types: counter. The count shows under the field, "12 / 60" with a maxlength and the number on its own without one, and it moves with every key in the browser, with no request to the server, so a count under a field no longer needs wire:model.live. A string instead of true is the sentence the counter shows, with :count, :max and :remaining. A screen reader hears the count in words once typing pauses, and past the limit how far past it the text is. It runs under Alpine's CSP build.
  • A combobox, a multi-select and a segmented control take the language of an option: lang. A list of languages names each one in its own language, "Deutsch" or "日本語", and with the option's lang a screen reader says it in that language's voice instead of the page's. The combobox and the multi-select read it from the option array, beside label, and the multi-select carries it to the pill and the list of chosen values too. The segmented control takes an option as an array with label and lang, the shape a select already takes. A lang that is not a language tag throws an InvalidArgumentException that names the option.
  • A search field opens the command palette and hands it what is typed. <x-wirekit::command-palette.trigger> sits in a page's header; a click, Enter or the Down arrow opens the palette, and so does typing, with the text in the palette's own field. Keys typed while the palette opens arrive too, so a word typed in one go arrives whole, and text an input method is still composing goes when the composition ends. The show event takes query as well: $dispatch('wirekit-command-palette-show', { query: 'invoice' }) opens the palette searching for it, with the cursor after the text, and reports the query at once.
  • Text keeps to a reading width with measure. measure="default" stops a paragraph at about 65 characters and measure="wide" at about 78, as on prose, so an introduction under a page's title does not run across a wide column. Without the prop, text runs as wide as its container, as before. The description of a page header keeps to the wide measure on its own.
  • A page header can stay at the top: sticky. It sticks to the top of the box the page scrolls in, below an announcement banner strip when there is one, with as much space above its content as below it. Once it rests there and the content scrolls under it, a bottom border fades in and sets the two apart; in the flow of the page it draws none, and under reduced motion the border appears without fading. The resting state is data-wk-stuck on the header, for a style of your own. --wk-page-header-rest-gap sets the space above and below, and --wk-page-header-top the line it sticks to, so a header in a box with a padding of its own can cover that padding. Without sticky the header is unchanged.
  • A field you tab to, or a fragment link's target, comes to rest beside a pinned bar instead of under it. A sticky navbar, header, toolbar, shell bar, brand bar, page header or announcement banner, a bottom navigation and a floating action bar tell the page, or the box they stick in, how much of its edge they cover, and it keeps that much clear as scroll-padding. Tabbing back up a form under a sticky navbar used to leave fields entirely under the bar, and tabbing down past a bottom navigation hid them behind it. Put data-wk-scroll-inset on a bar of your own for the same (keeping the focus clear of a pinned bar); a scroll-padding you set yourself wins.
  • A click or tap on a range slider's track brings the nearer handle to that point. Dragging was the only way to set a value with a pointer. The handle that moves takes the focus, so an arrow key fine-tunes from there, and the track takes the click across a band as tall as a handle's target although it is drawn 8px tall. The click that ends a drag does not count, so releasing a handle over the track never moves the other one.
  • A resizable handle resizes with a click as well as a drag. Two step controls beside the grip move it a few pixels per click, little enough that the control is still under the pointer for the next one. They show while the handle is hovered or focused, and always on a touch screen. The handle keeps its keyboard model, and the controls take no focus.
  • A card on a sortable kanban board moves with two clicks. Each sortable column has a Reorder button in its header. Pressed, a click picks a card up and the next click puts it in another card's place, in its own column or, on a cross-column board, in another one. A drag and the keyboard were the only ways to move a card, and a pointer that clicks but cannot drag had none. The move reports once, through the same event a drag sends, and while the mode is on a click does not reach the links and handlers inside a card.
  • Glass in four levels: wk-glass-clear, wk-glass-regular, wk-glass-frosted and wk-glass-opaque. Set on the glass element or on any ancestor, such as <html>, a level decides how much of the backdrop the liquid glass shows, from the clearest pane to the theme's opaque surface. Six --glass-wk-* tokens carry each level's fill, rim, light, edge and shadow for light and dark, and a rule in your own stylesheet overrides them without editing the published file.
  • Glass answers Increase Contrast. Under prefers-contrast: more, which Safari reports, a glass surface turns opaque with a strong border.
  • A form says when it holds unsaved changes: x-wk-unsaved and the unsaved hint. Put x-wk-unsaved on a form or a card. Every field inside it with a deferred wire:model that differs from the value its Livewire component last received is framed in --color-wk-border-unsaved, a blue beside the red of an error and the green of a success. A field with a prefix, a clear button or pills takes it on the frame around them, and the frame is drawn as an outline as well, so it shows under a theme whose border width is 0px and the field does not move. <x-wirekit::unsaved-hint> beside the save button says so in the reader's language, as a polite live region. x-wk-unsaved.confirm also asks before the page is left with an unsaved field: the browser's own question on a reload or a closed tab, and the hint's question before a wire:navigate visit. A save that redirects is not held, a step through the history is not held, and an invalid field keeps its error border. wirekit:doctor:a11y --theme-contrast checks the new border at 3:1 against the field.
  • A select says what is missing when it has nothing to choose: empty-text, empty-href and empty-action. A select filled from your records showed only its placeholder until the first record existed. With empty-text and no option, it is disabled, the sentence below it says what is missing, and empty-href adds a link to the screen that creates it, named by empty-action. The select names that line in aria-describedby, so a screen reader on the field hears why it is empty. The empty slot takes your own control instead. With an option in options or in the slot, nothing changes.
  • A toggle can show its state in words and take a color for each state. on-label and off-label write a word beside the switch, after it and its label or before it with state-position="start". The word follows the click at once, before anything is saved, and a screen reader hears the switch's own state instead. on-intent and off-intent color the track with accent, success, danger or neutral, so "green on, red off" is on-intent="success" off-intent="danger" rather than a replaced track.
  • badge-label on an app rail item says what its counter counts. A dot in the narrow rail says that something waits, and digits beside a name say how many, but neither says what. The sentence, such as "3 vouchers wait for review", shows in a tooltip on hover and focus in every mode, under the name where the name is a tooltip too, and it becomes the link's description for a screen reader, so the bare count leaves the name.
  • Info has tokens of its own: --color-wk-info-tone, --color-wk-info-fg and --color-wk-info-hover, beside --color-wk-info-text. Every info surface reads them: an alert, a callout, a toast, a badge, a banner, a progress bar, a map pin and an info button among them. They alias the accent's tokens, so nothing changes until you set them. When your accent reads as success, a green brand for one, give info a color of its own: every informational notice follows, and success stays green. An alert, a callout and every toast also name their intent in data-intent. On a feature chip, intent="info" follows the info color while the older tone="soft" keeps the accent's tint.
  • Three classes draw the kit's focus ring on a tab stop of your own: wk-focus-ring, wk-focus-ring-offset and wk-focus-ring-inset. A link, a button or an element with tabindex="0" that you write yourself takes the ring the components draw, on :focus-visible only and from the ring tokens: at the edge the way a link draws it, past a band of --color-wk-ring-offset the way a button draws it, for an image or a swatch, or inside the box for a scroll region. See design tokens.
  • A result that an action makes appear far from the button comes into view. Put x-wk-scroll-into-view on a form that opens at the top of the page, a key shown once or a message above a long list, and render a data-wk-scroll-key that is new with every result: the region comes into view and takes the focus when it appears and whenever the key changes, and a page that loads with it does not move. Under reduced motion it jumps instead of gliding. See bringing a result into view.
  • A code block can wrap its lines: wrap. It breaks long lines instead of scrolling them, and since nothing scrolls then, the block is no tab stop of its own. Without wrap a long line scrolls inside the block, which stays keyboard-reachable as before. A page of wrapped examples saves a keyboard reader one press per block.
  • A copy tells the page. The code block's copy button and the clipboard button dispatch wirekit:copied once the write has succeeded or failed, with event.detail.succeeded and the block or the button as the target. The event bubbles, so one listener on document counts every copy on the page, a second copy with the same result included.
  • A one-time code can fill its width: justify="stretch" on the otp-input. The cells share the control's width, so a PIN row ends flush with the sentence above it and the button below. Every cell stays square and grows from the touch target, or from its size when that is larger, up to twice its size; where every cell has reached that, the row sits centered. With group, each group takes its share by its number of cells and still breaks at its boundary.
  • The list item of an overflow nav entry has a class block of its own: item. It holds the item around an entry's link and its action in the rows, so a personalization can frame the two as one, as a tab bar of open records does, and reach the current entry through its data-wk-overflow-current attribute. It is empty by default, and an entry with an action sets its link and its button in a line, which a closure receives as the vendor block.

Changed

  • wk-glass looks like liquid glass. It was a milky surface, a 72% white wash under a 20px blur that hid its backdrop. It is now the regular level of the liquid glass: a 6px blur, a thin fill, and an edge that catches the light, so the backdrop comes through softly. For the previous look, add wk-glass-frosted, which keeps its blur and fill and gains the edge. The published stylesheet does not update itself: once any edits you made to it live in your own stylesheet as tokens, run php artisan wirekit:glass install --force.
  • Containers pad evenly. The body, header and footer of a card, an alert, a callout, an alert dialog, the header of a drawer, an action bar, an announcement banner, the panel of an accordion, a stat, a stage card, an empty state and a sticky panel have the same padding on every side now, the value their sides already had, so a button at the end sits as far from the bottom edge as from the side. They grow a little taller, and nothing moves sideways. A component's block can still be personalized to the flatter padding, and the theming guide gives the pair for your own containers.
  • The current link of an overflow nav is set in --font-wk-selected-weight. The token follows the heading weight until a theme sets it, so nothing changes by default, and a theme that marks a choice without bold no longer has to replace the whole link block. The link's fill still marks it, and under forced colors, which replace the fill, it is framed in the system highlight color.
  • The MCP server answers with an example from each page documented under a component. The worked examples of the command palette include its search field and a remote search, and the map has examples on both of its engines, where it had none.

Fixed

  • A component keeps working when a Livewire update renders it with new values. Many components, among them the modal, the dropdown and the combobox, went on acting on a copy of themselves that nothing displayed once an update changed a value they were rendered with, such as the server's answer to an optimistic choice in a combobox rendered with :value="$country": the combobox cleared its field when the answer arrived, and its arrow keys stopped moving the active option. Affected since 2.57.0.
  • A URL with more than one query parameter reaches its target when you write it into the tag as href="{{ $url }}". A link component, a button, a card, a dropdown item and the other components that take a URL as a prop wrote such a value escaped twice, so the browser followed ?a=1&amp;b=2 and your application read a parameter named amp;b: a signed link lost its signature and answered 403. The same happened when a component of your own passed its attributes on to one of them. A URL bound with :href was always right. The src of an avatar, an image and a flag had the same fault.
  • wire:model.blur and wire:model.change reach the server from every component that keeps its value out of sight. Livewire 4 applies both modifiers on the bound element itself, and the phone field, the rating, the segmented control, the one-time-code input, the editor, the calendar, the color picker in its popover, the range slider, the filter builder, the status matrix, the notification center, the pricing table's interval toggle and the data table's selection keep their value in a field the reader never focuses. None of them sent it with .blur or .change, and the number input lost a value changed only with its steppers. Each now fires change when a value is committed and blur when focus leaves the component, as a native field does.
  • A rating and a range slider bound with wire:model send the value just chosen. Both told wire:model about a change before the value had reached the field it reads, so a rating sent the star chosen before the last one, and a range slider stayed one step behind every key press and every move of a drag. That held for wire:model and for wire:model.live. A plain form was not affected.
  • A calendar bound with wire:model sends the date chosen. Livewire 4 hears wire:model only on the element that carries it, and the calendar carried it on its outer element while the date came from a field inside it, so neither wire:model nor wire:model.live ever reached the server. A plain form was not affected.
  • A combobox or multi-select bound with wire:model keeps its choice when the server changes its options. The options and the value were part of the expression the combobox and the multi-select start from, so a Livewire render with a new list reset them to that expression: the choice went to nothing, and wire:model.live sent the empty value back, replacing the choice on the server too. Both now travel on attributes of their own, and the choice stays while the list takes the new options.
  • A modal, a drawer and an alert dialog bound with wire:model follow the property when the server changes it. After the first render, setting the property to true opened nothing, and setting it to false, as a save action does to close its modal, left the open drawer or alert dialog standing. Both directions work now. A close made this way announces no dismissal, as the modal page describes.
  • An x-model on a command palette or a map is dropped, and a warning says so, as on the other components that hand no value over. On a palette that renders in place the binding received the text of its search field, and on a map nothing.
  • A click inside a modal, a drawer, an alert dialog or a command palette reaches the page. Each of them stopped every click inside its panel, so a listener your page delegates to the document heard nothing from inside them, and a dropdown, popover or combobox open inside one stayed open when you clicked elsewhere in the dialog. A click beside the panel still closes the dialog, and a click inside still does not. The clear and toggle buttons of a combobox and the remove button on a multi-select pill let their clicks through as well.
  • A vertical swipe that starts on a row of a table or a data table scrolls the page. The region a table scrolls sideways in could also scroll up and down by the few pixels the cells' line boxes can leave under the table, so on a touch screen it took a vertical swipe that began on a row, moved by those pixels, and the page stayed where it was. A mouse wheel over a row did the same. The region scrolls only sideways now; with sticky-header it still scrolls the rows under the header.
  • Removing a chosen value from a multi-select leaves the focus in the field. A press on a pill's remove button took the focus away with the pill, so the next Tab started again at the top of the page. The focus now moves to the pill that takes the removed one's place, to the new last pill, or to the search field once nothing is chosen, from the keyboard and with the mouse. The list of options stays closed through a removal: it opens when you tab or click into the field, type or press an arrow key. The remove buttons of layout="list" already kept the focus, except with optimistic set, and now keep it there too.
  • The open list of a combobox or a multi-select closes when Tab moves the focus on. Both closed their list on a click outside and on Escape only, so a reader going through a form with the keyboard left an open list behind each of these fields, and two multi-selects in a row left two lists standing. Tab in the field now closes the list and chooses nothing, and the focus moves on as before.
  • A choice made with the mouse in a combobox leaves the focus in its field. An option cannot take the focus, so the press handed it to the page, and in Chrome and Edge the next Tab started again at the top of the page. A press on the list with a mouse or a pen no longer moves the focus, in the combobox and in a multi-select with optimistic. A tap is left as it was, so the on-screen keyboard still closes over the choice.
  • The panel of a notification center and the popover of a filter builder close in Safari when Tab leaves them. Both close when the reader tabs past their last control or back past their first, and they found those ends in a list that counts buttons and links. Safari passes buttons and links by unless it is told otherwise, so Tab left the panel from an earlier control, and the panel stayed open over the page. A Tab that carries the focus out now closes the panel in every browser, and the focus goes on from the bell or the trigger.
  • Characters typed right after the command palette's hotkey reach its search. The palette's field takes the focus a moment after it opens, and keys typed in that moment were lost, or landed in the field of the page that had the focus. They now go to the palette's search, Backspace included.
  • Back returns to where you were after a link in an open drawer, modal or alert dialog leaves the page. While one of them is open the page is held still, and a plain link or form inside it left the page in that state, so the browser remembered the top of the page and Back went there. They close as the page is left now, which gives the position back first, and the command palette and the lightbox do the same. A link that opens a new tab leaves them open.
  • A range slider counts its steps from min under the pointer as well. On a slider from 5 with a step of 10, a drag set 10, 20 and 30 while the arrow keys set 5, 15 and 25, and the minimum itself could not be reached with a pointer. A drag and a click on the track now land on the grid the keys walk.
  • A range slider with a decimal step writes the value the step means. A step of 0.1 set 0.30000000000000004 where 0.3 was meant, by pointer and by keyboard alike, and that number showed in the badge above the handle, was announced and was submitted with the form. Values are now rounded to the decimal places of the slider's own numbers.
  • A resizable handle ends a drag where the pointer was released. A pointer move takes effect in the next screen frame, and a release that came before that frame dropped it: a drag lost its last stretch, and a quick one did not move the handle at all.
  • A card lifted in a sortable kanban column looks lifted. A card lifted with the keyboard to be moved looked exactly like a focused one, and a focused card wore the browser's own outline. A focused card now wears the focus ring, and a lifted card a dashed outline in the accent with a raised shadow.
  • A menubar, a navigation menu and a popover close when a press outside them ends, not when it begins. They closed as soon as the button went down beside them, so a reader who pressed by mistake and slid back onto the open menu lost it anyway (WCAG 2.2, 2.5.2). They now close on the release, and only when it happens outside too, as the browser's own popovers do; a touch that turns into a scroll closes nothing. The scope switcher, the overflow nav and the source popovers of the assistant message follow the popover. A click on another control still closes the popover and leaves the focus on that control.
  • A navigation menu panel opened from the keyboard stays open in Chrome. It could close itself a moment after Enter when the mouse pointer happened to rest near the bottom left of a short page, without the mouse being touched.
  • Every star of an interactive rating is at least 24 by 24 pixels. At size="sm" the stars were 16 pixel buttons 18 pixels apart, below the minimum target size of WCAG 2.2. Each small star is a 24 pixel button now, with the icon in its middle; the medium and large stars were that size already.
  • The "+N more" button of an event calendar month cell is 24 pixels tall. It was one line of small text 18 pixels tall, right under an event pill. A day cell that shows it grows by the 6 pixels the button needs.
  • A page header with actions and no description is as tall as its title. It took the height of its tallest button, so the content below started further down than under a header without actions. While the actions share the title's line they now sit centered on the title and add nothing to the header's height; once they wrap under it, the header lays them out as before.
  • A module of the icon-only app rail is read once. Its tooltip shows the module's name and also described the link with it, so a screen reader said the name twice. The tooltip no longer describes the link.
  • An invalid field stays red under the pointer, and an invalid checkbox or radio is red at all. The hover border of an input, a textarea, a select, a password input and a time picker outranked the error and success borders, so a field turned gray exactly while the reader pointed at it to correct it; it now belongs to the resting state alone. An invalid checkbox or radio kept its gray resting border even with the pointer away, because the error border lost to it; the box now takes one border for its state.
  • An invalid field shows a red focus ring. An input, a textarea, a select, a password input, a time picker and a number input in their error state drew the resting focus ring when they took the keyboard focus: each carried the resting ring color and the error one, and the stylesheet put the resting one last. Each state now names one ring color, and an input with a prefix or suffix draws the error and success rings on its frame as well. A field in its success state showed its green ring before, but only because of how the two colors happened to sort.
  • A text or a heading that keeps a user's line breaks starts at its first line. Their templates set the content on a line of its own, indented, so with whitespace-pre-line an empty line stood above the text, and with whitespace-pre-wrap its first line was indented as well. The content now sits directly between the tags.
  • An input, a select and a textarea line up in a row with align-fields on their own. The row's three tracks for labels, controls and messages were taken only by a field wrapped in field; an input with its own label, hint or error landed on the middle track as a whole, so a button beside it stood a label higher and moved when an error appeared. They now hand their label, control and message to the tracks themselves, with an empty placeholder where one is missing, and leave them to a field around them.
  • The distance below a horizontal tab bar is a knob. Set --wk-tabs-panel-gap to the distance between your cards, in your theme or on one tab set, and the first card sits as far from the bar as two cards from each other. Unset, the distance is --padding-wk-y-md, as before, and the panel block no longer has to be replaced for it, which also took the vertical layout with it.
  • A sidebar row and an app rail module can be the current location rather than the current page. current="location" on an active entry writes aria-current="location", for a row that leads the section the page lies in, and current="true" is there too; page stays the default. A screen reader no longer announces "current page" on an entry whose link leads elsewhere. The highlight is the same for every value, under forced colors as well.
  • Taking the last file out of a file upload empties the bound property. Livewire ignores a change that brings no file, so after the reader removed the last file with its remove button the earlier upload stayed on the property, and the next save stored a file the reader had taken out. The component now sets the property to null, or to an empty array on a multiple upload.
  • A message from a sender without a picture shows their initials. The avatar of a message, a message group and a typing indicator fell back to the same gray silhouette for everyone although it had the name, so every sender without a picture looked alike. It now shows the first letter of the first and of the last word of the name, in the color an avatar with from-initials gives those letters, and initials in the author record replaces them. A screen reader still hears the name.
  • A data list with layout="stacked" sets each label above its value, and layout="grid" goes to one column on a narrow screen, as the documentation says. A stacked item stood side by side like the default layout, so a long value became a right-aligned paragraph beside a label wrapping in a third of the row; its value now starts under the label, from the same edge. A grid kept its two columns on a phone; below a 40rem screen it now has one.
  • The clear and copy buttons of an input stand as deep in the frame as an icon in front of the value. They kept 10px to the frame's edge while an icon in the leading or trailing slot keeps 12px, so a field with a search icon in front and the clear button behind sat 2px off center. The buttons keep 12px now, and two of them stand 12px apart.
  • A button with surface="link" sits flush with its sentence. It kept the horizontal padding of its size, 12px on each side at the default size, beside the link's own p-0, because the padding of one side outranks the padding of all four. A link button now takes neither the size's padding nor its fixed height, and keeps its text size.
  • A popover with padded="false" has no padding. The panel kept its 12px, so did the panel of every scope switcher, which asks for an unpadded one. The padding now comes out of the panel's classes, also out of a block you personalized.
  • An icon sized with a class takes that size. A height or width utility in an icon's class now replaces the icon's own size on that axis. Before, class="h-4 w-4" beside the default h-5 w-5 rendered at 20px, while a larger utility won only by sorting later. The size prop stays the way to size an icon. The icons in tabs, the theme controller and the scope switcher are 16px, as written.
  • The avatar placeholder of a skeleton is round. skeleton.avatar drew a square with rounded corners, because the radius of its text lines outranked the circle's.
  • A strip announcement banner is as tall as --wk-strip-height. The banner's vertical padding stayed beside the strip's py-0 and made it 32px tall against a 27.5px token at the default text size, while every sticky bar below a strip is placed at the token's height.
  • The create action of a scope switcher is in the accent color, which sets it apart from the rows above it. The text color of the rows outranked it.
  • A number input takes a width from class. A width utility on a number input now replaces its own w-16. A narrower one, such as w-12, lost to it before.
  • The knob of a switch stands out from its track when it is off. The neutral track of a toggle and of the theme controller's switch was filled with --color-wk-border, and the knob, which is what shows whether the switch is on, stood at 1.3:1 against it in light mode and 1.42:1 in dark. The track is filled with --color-wk-border-strong now: the knob reaches 3:1 against it and the track 3:1 against the page in both modes, as WCAG 1.4.11 asks of a state indicator. A toggle with on-intent="neutral" takes the same fill when on.
  • A counter on the current module of an app rail with tone="accent" stays visible. On the accent tone the current module's pill and the fill under the pointer are --color-wk-accent-fg, and so was the counter, so a counter on either sat in its own color and disappeared. A counter on a filled item now takes --color-wk-rail-badge-on-fill, with its digits in --color-wk-rail-badge-fg-on-fill: on the accent tone they invert with the label, and on every other tone they repeat the counter the column shows. Set them like the other rail roles to give that counter a color of your own.
  • The Livewire simple pager of the pagination works with a cursor paginator. A component whose paginationSimpleView() returns wirekit::pagination.livewire-simple ended its request with a stack overflow at the first links() of a cursorPaginate() result, and so did a <x-wirekit::pagination :paginator> bound to a cursor paginator inside such a component: Blade escapes a bound paginator by rendering it through its default view, and Livewire had made that view this pager. The pager now renders once.
  • The page-size choice of a pagination outside Livewire waits for its Apply button. Picking a size sent the form at once, and on Windows and Linux the arrow keys change a closed select's value, so every key loaded the list again and put the reader at the top of a new page. The form shows its Apply button now and sends from it alone; bound with wire:model, the choice works as before.
  • The label of a field names the control inside it, and a click on it reaches that control. The label pointed its for at the field's name, the id only a plain input carries, while the combobox, the multi-select, the tags input, the date picker, the file upload, the color picker, the slider, the range slider, the editor and the inline edit give their input an id of their own. So the visible label named nothing and a click on it did nothing: a combobox in a field had no name at all, and a multi-select or a tags input was announced by its placeholder. Each of them now takes the field's label as its name, through for or, where there is no input a label can name, by reference. A control with a label or an aria-label of its own keeps that name, and a for you pass to the field keeps your wiring. A click on the label of a range slider, its own or a field's, focuses the first thumb.
  • The first box of an OTP input carries the words of its label. The label points at the first box, and that box was named "Digit 1 of 6", so no control answered to the label's words and a reader who uses speech input reached nothing by saying them. With a label, the first box is "Verification Code, digit 1 of 6" now; the other boxes, and a field without a label, keep their names.
  • Every tab stop the kit renders draws the kit's focus ring. A link component, a brand, a reaction, a tooltip trigger that is a stop of its own, a scroll area, a tab panel, the copy button of a code block, the links in prose, a card that links or is a button, an attachment that is a link and the link cells of a data table showed only the browser's own ring. That ring follows neither the ring tokens nor a preset, and in Safari it stands at about 2:1 on a light surface.
  • Buttons and fields show their focus ring under the minimal, retro-terminal and brutalist presets. Those presets set shadow tokens to none, and a button or a field writes its shadow and its focus ring into one box-shadow, where none is valid only on its own: the whole declaration was dropped, and the element drew nothing when it took the keyboard focus. The preset pages, php artisan wirekit:theme and the first example on the theming page say 0 0 0 0 transparent now, which looks the same. If you copied one of those blocks, change none to 0 0 0 0 transparent in --shadow-wk-sm, --shadow-wk-md, --shadow-wk-lg and --shadow-wk-none.
  • A filled surface points the focus ring at its foreground. On the accent and status fills of an announcement banner, a CTA and a hero, the ring had the color of the fill in the default preset, so the banner's dismiss button showed no ring at all. These surfaces now draw the ring in their foreground color, without an offset band.
  • Focus rings read their width and offset from the tokens everywhere. The dismiss button of an announcement banner, a row of an attachment group, a conversation, a skip link and the floating action button drew a 2px ring at a 2px offset whatever the preset said.
  • The focus ring of the image compare handle stands out on any picture. It is drawn in two bands now, the page's color beside the handle and the ring outside it, where a ring of one color vanished on a picture of that color.
  • The code inside a code block shows its focus ring. The block's frame clips to its rounded border, and the code it holds, which scrolls and is a stop for the Tab key, drew its ring outside its own edges, where the frame cut it away: in Safari nothing on screen changed when the keyboard reached the code, in Chrome and Edge a few pixels at its corners did, and under a filename or a copy button a thin line was all that was left. The ring is drawn inside the code's edges now and shows on all four sides, in a tool call, in prose and in a code mockup as well.
  • The rows of a sidebar that scrolls and the links in a shell bar's strip show their whole focus ring. The sidebar's scrolling list and the shell bar's strip clip what overflows them, and the rows and links filled them edge to edge: a focused row showed a line above and below it and nothing at its sides, and a link in the strip showed two brackets around its text. Both leave room for the ring now, as much as a button's ring reaches with its offset, without moving anything, and a row the keyboard scrolls into view stops short of the edge, so its ring stays in view. A shell bar with padding="none" has no padding to lend its strip, so there the room is above and below the links only, and in the foot of a folded sidebar the room stays inside the strip, which makes the control in it narrower by that much. This holds for a sidebar with a header or a footer, the sidebar of an app shell, and whatever you put in those slots.
  • The field of a number input shows its whole focus ring. The increase button sits flush against the field and was drawn over the right side of its ring, which read as a bracket around the number. The focused field now rises above both buttons.
  • The first card of a sortable kanban column shows its focus outline at the top. The column's card list scrolls and clips, and its first card sat flush with the top, so the outline a focused card wears, a little apart from the card, lost its top edge. The list leaves room for it now without moving the cards.
  • A kanban column and a conversation keep the focus ring of what they hold whole. The board cut the ring of its first column, and a conversation cut the ring of a control the focus scrolled to its edge, and its own ring in a pane that clips.
  • A focused tab shows its whole focus ring. A row of tabs scrolls sideways when it is too wide for its column, and the bar clips what reaches past it: the ring of a focused tab, drawn around the tab, lost its top and bottom, and on the first tab its start side as well, which left a single vertical line beside the label. The ring is drawn inside the tab now, in every variant and in a bar built from tabs.list and tabs.tab, and on the filled selected tab of the bordered bar in the color of its label, so it stands out from the fill.
  • The selected tab of an underline tabs bar shows its underline. Its 3 pixel edge in the accent color lost to the transparent edge every tab carries, so the selected tab differed from the others in its text color alone. Horizontal and vertical bars both draw it again; the pills and bordered variants are unchanged.
  • A hover card opened from the keyboard leaves its trigger's focus ring in view. The card was placed against the line around its trigger rather than the button in it, and opened less than a pixel below the button, over the lower edge of the button's focus ring. It keeps its full distance from the button now, below it and above it.
  • The scroll region of a table shows its whole focus ring. The region drew its ring inside its edges and beneath its own cells, so the header row covered the ring's top edge on every table, a striped or footer row its bottom edge, a frozen first column its left side, and a sticky header kept the top covered while the rows scrolled. The ring is drawn above the cells now, on all four sides, while the region has the keyboard focus.
  • A context menu trigger that the keyboard reaches shows its whole focus indication, whatever it wraps. When the trigger holds no control of its own, its wrapper takes the focus, and its ring was cut off at both sides when the wrapper filled a table with rounded, clipped corners, as the example with table rows does. The wrapper marks the focus with an outline set just inside its edges now, which stays visible in a clipped table and above content that has a background of its own.
  • A tooltip around a button, a link or a field is no second tab stop. The tooltip made its trigger a stop by default, so around a control the reader met an unnamed stop before the control itself. focusable-trigger now reads the slot: around a control the control is the stop, around an icon or a word the trigger still is. true and false keep their meaning. Around a control the control carries the tooltip's description, and keeps it when a Livewire update renders the tooltip again.
  • The list of a combobox, a multi-select, a scope switcher and a command palette is no tab stop of its own in Chrome. Chrome makes a list that scrolls a stop, so Tab from a field that was the last stop on the page could land on a list that was closing.
  • A chart takes no tab stop inside its hidden drawing. ApexCharts 7 navigates a chart's data points with the keyboard, and in Chrome its drawing stayed a tab stop inside the part hidden from screen readers. The component leaves that navigation switched off.
  • A carousel's Tab order is Previous, slides, Next. Both arrows came after the slides, so Tab went to the slides and then back to the arrow on their left. The arrows stand where they stood.
  • A refracting glass surface turns opaque for a reader who asked for no transparency. Under reduced transparency, forced colors and in print, wk-glass-refract stayed transparent with its displacement layer in front of it wherever the extension's script had prepared the page for refraction, because the refraction's own rule outweighed the reader settings. Each setting now reaches it.
  • A name given to a component reaches assistive technology as written. Many components took their aria-label out of the attributes and escaped it a second time, so a name with an apostrophe, an ampersand or a quote, bound from your data or written with entities, was read as Tom&#039;s orders &amp; returns: among them the chart, the editor, the drawer, the alert dialog, the progress bar, the rating, the tags input, the file upload and the two thumbs of a range slider. Each reads the name as the text it stands for now.
  • A radio and a checkbox put a lang on their label. It went on the field, while the words sat beside it in the label, so a label in another language than the page was read in the page's voice.
  • wire:model.live.blur sends when the reader leaves, and wire:model.live.change when they choose, on the combobox, the multi-select, the tags input and the branch switcher. These four bind on their outer element, which never received a blur or a change, so a choice bound with .live.blur was sent only with the next request, and an updated hook meant to validate on leaving never ran.
  • The list of a combobox is named after its label. It carried an empty aria-label unless you passed one, so a combobox with a visible label opened a list without a name. It takes its name from the label now, or from the field around it, or from your aria-label.
  • The country picker of a phone field has a name of its own. It took the field's label, so a screen reader heard "Phone number" for the country and again for the number, and a field without a label left the picker unnamed. It is "Phone number, country" now, or "Country" without a label, translated in every language file the kit ships.
  • An attachment announces a change of its upload state. When your component moves state to done or error in a Livewire update, a screen reader now says "Uploaded" or "Upload failed" without the reader moving to the card.
  • An open notification center stays open when a notification arrives from the server. Its list was part of the expression the notification center starts from, so every render with a new notification, such as one a poll brings, reset it to that expression and closed the panel the reader was looking at. The list now travels on an attribute of its own, and the panel keeps its state while the new notification appears in it.
  • An event calendar stays on the week or month the reader moved to when the server changes its events. The events, the day markers, the view and the date were part of the expression the event calendar starts from, so a render with a changed event, such as one saved from a modal, reset it to that expression and sent the reader back to the week it opened on. All four now travel on attributes of their own: new events appear where the reader is, and a view or a date the server changes still moves the calendar.
  • A component that Livewire renders again with new values stops repeating itself. The watchers a component sets up when it starts piled up with every such render, so an opening of a combobox announced itself once more after every answer from the server. Only the watchers of the latest start act now.
  • An open dropdown, popover, menubar menu or color picker stays at its trigger when Livewire updates the page. An update removed the panel's placement while it stayed open, and nothing put it back: measured on a refresh, a dropdown moved from 8px below its trigger to 144px, a menubar menu to the end of the page and a color picker 1773px down. A popover that holds only text also dropped the focus to the page. All four keep their place now, and the popover keeps the focus.
  • Enter in a combobox chooses only from an open list. On a closed list it chose the active option anyway: the first option in a field nobody had touched, or the suggestion a reader had just dismissed with Escape, neither of them seen. Enter now opens a closed list at the current choice and chooses nothing, as the select-only combobox and the multi-select already did. With optimistic, nothing is sent until a choice is made, and a choice made with Enter closes the list, as a click does. The country list of the phone field behaves the same way.
  • A field whose id carries a dot, a colon or brackets is no longer talked over when an optimistic save fails. The optimistic layer stays quiet wherever the field's own error message is already being announced, and it found that message through a selector written from the field's id as it stood. A field named form.email, the name a Livewire form object's messages carry, gets the id form.email, and #form.email-error reads as an id and a class and matches nothing, so the generic "Could not save" was announced over the field's own message. On a date picker named with brackets the selector was invalid, and the failure stopped before anything was announced. The selector now escapes the id the way the browser's CSS.escape() does.
  • A menubar labeled in Chinese, Japanese, Korean, Hebrew or Thai opens one menu at a time, and a scope switcher's groups named in those scripts each carry their own name. Both took an identity from a slug of the text, and such text slugs to nothing: every menu of a menubar shared one name, so a click opened them all, and every group of a scope switcher shared one id, so each group was announced with the first group's name. Text without a slug, symbols included, is counted now.
  • A short event in the event calendar's week grid shows its time in the tip once enlarged text spacing pushes the time past the block's edge. With a larger line height or letter spacing, the time under a short event's title ran past the bottom of its block, and a time with a descender, such as "9:00 p.m." in Mexican Spanish or "9:00 μ.μ." in Greek, lost the descender. The tip that names a cut-off title now also opens for such a block, and it carries the time with the title.
  • The menu of the theme controller is named after what it does and the mode it shows. Its trigger shows the current mode, such as "System", and was named "Dark mode" alone: speech input could not reach it by the word on it, and a screen reader never heard the mode. It is named "Dark mode, System" now, after the control's label and the mode.
  • Links and other accent text on a callout or an alert reach 4.5:1 in the dark mode of the Cupertino preset. Callouts and alerts tint their surface with a color, and on those tints Cupertino's dark accent text, blue-400, measured as low as 3.82:1. It is blue-300 now, 6:1 on the accent callout. If you copied the preset into your stylesheet, change --color-wk-accent-content in its .dark block to var(--color-blue-300). The guaranteed contrast pairs now include accent text on these surfaces.
  • A chart's aria-label follows a Livewire update. Since 2.56.0 a chart takes the new labels and series of a Livewire update, and its name stayed the one it first rendered with: a name that summarizes the data, as the chart page asks for, went on describing the period the page loaded with while the chart showed another. The name now travels with the data and changes with it.
  • The tooltip of an ApexCharts chart reads enabledOnSeries, inverseOrder and hideEmptySeries, and the value and name formatters set for every chart. With library="apexcharts", the tooltip WireKit draws for a chart listed every series in their order and wrote each value as it came, whatever these options said: a tooltip limited to one series listed both, inverseOrder changed nothing, and a value of zero kept its row under hideEmptySeries. A tooltip.y.formatter or a yaxis.labels.formatter set through window.Apex wrote the values of ApexCharts' own tooltip and not of this one, and a tooltip.y.title.formatter the names in front of them. It reads all six now, as ApexCharts does, and writes what a formatter returns as text. valueDecimals, valuePrefix and valueSuffix come first where a chart sets them.
  • A scope switcher row whose address is only a fragment, such as #, no longer reloads the page. Livewire turns every same-origin link into a navigation, so choosing such a row fetched the page again and left the reader at its top without the focus. A fragment stays with the browser now, in the rows and in the footer action; a real address still navigates with wire:navigate.
  • A map draws with a library that lands on window after it has rendered. A map looked for MapLibre or Leaflet when it started and afterwards waited only for registerMapEngine(), so a library whose script ran later, as one can on a page reached through wire:navigate, was never used: the marker list stayed, the canvas did not come, and the console said no library had been found. A map now keeps looking for its library the way a chart does and draws the moment it arrives, and the console hint no longer speaks for a library that is there.
  • The calendar button of a wide date field sits at the end of the field in Chrome and Edge. In a date, time or date-and-time field wider than its value, as a grid column or a full-width form gives it, the button stood right behind the value and the rest of the field was empty. The field laid its parts out in a row, and Chrome and Edge shrink the box that holds the value and the button to their width in a row. The field is a grid now, which stretches that box across it. Safari draws no button inside the field, and its value sits where it did.
  • The wirekit:doctor:a11y linter reads a view whose [@php](https://github.com/php) blocks share one statement. Blade compiles every block of a view into one file, so a try may open in one block and close in the next. The linter checked each block as PHP of its own, stopped on such a view with a parse error and reported nothing for the whole directory it was given. It reads those views now, and still skips the comments inside them.
  • An editor mounts a factory that arrives after it has started. An application that loads its editor only on the pages that have one defines window.wirekitEditor from a script that, after a wire:navigate visit, runs after Alpine has started the editor. The editor looked once and stayed a plain textarea with a console error. It waits now the way the charts and the map wait for their library: the content the server rendered stays until the factory arrives, the textarea and the error come a few seconds after the page has loaded if it does not, and a factory that arrives even later still takes over a textarea nobody has typed into.
  • A read-only editor without a factory keeps showing its document. With :editable="false" there is no form field to fall back to, and the fallback hid the content the server had rendered anyway, so the reader saw an empty box. The content stays now, and the console still reports the missing factory.
  • Escape folds a reading spine that hover or focus opened. The expanded list of a reading spine lies over the text beside it, and it could only be closed by moving the pointer or the focus away. Escape folds it now with both left where they are, and it stays folded until they leave and come back. A spine the page keeps open lets the key pass.
  • A command palette with a remote source no longer says "nothing found" before it has searched. The query goes out 300 ms after the last keystroke, and until your host answered, the empty state showed under the results of an older input and its status region read it out after every keystroke. A palette with a loading slot, or one whose host has reported loading once, now keeps the empty state for the answer to the newest input. A palette without a remote source, and a host that only reports failures, behave as before.
  • The label of an inline edit with your own control names it. With the editor slot the label pointed its for at an element the slot never renders, so it named nothing and a click on it did nothing. It now names the part of your control that takes focus when the editor opens, unless that part names itself, and a click on it focuses the control while the editor is open.
  • A click on the label of an open inline edit keeps what was typed in Safari. WebKit took the focus from the field when its label was pressed, and the editor read that as leaving: it closed and dropped the draft, in Safari and in every browser on an iPhone. The editor stays open now, and the click puts the focus in the field.
  • A click on the label of an editor focuses it. The label pointed its for at an element that cannot take a label, so the click did nothing. It focuses the editor now, and a click while you are writing leaves the focus where it is.

Documentation

  • The scroll area page says how much room a control at its edge needs for its focus ring. The area clips whatever reaches past its edges, and a focus ring reaches a little past the control it surrounds, so a button flush against the edge loses that side of its ring. The page header example with a header resting at the top of a scroll area gives its button that room now, on the sides only, so the header still rests at the top.
  • Every tab stop an example writes shows the kit's focus ring. The scroll regions of the toolbar and context menu examples use a scroll area, the consent links of the checkbox and the sign-in link of the brand bar use the link component, and the examples' own links, buttons and scroll regions draw it with these classes.
  • The images in the examples carry their width and height. The first example on the image page showed an image whose place was only known once it had loaded, and a copy of it would have done the same; the page says now that the browser reserves the box from the two values before the file arrives. The logo cloud and card examples and the logo cloud and marketing landing page blueprints carry them too.
  • The range slider shows how to bind a stored range with wire:model. Its example bound wire:model alone, which starts the thumbs at the ends of the scale, follows no range the server sets, and replaces the stored range on the first move. It now passes the range to min-value and max-value as well and says why.
  • The slider page says what wire:model.live sends during a drag. It said a drag fires a change on every step and sends a request per step. A drag fires input on every step and one change at release, and Livewire sends a .live field once its input pauses: a quick drag sends once, a drag that stops along the way sends at each stop. The page now points to wire:model.change for sending only at release.
  • The phone field, calendar and segmented control pages pass the bound value as :value beside wire:model, as the rating and color picker pages already did. These components keep their value in their own state, seeded from value: bound without it they show their default instead of a stored value and do not follow a value the server sets. The pages now say so, and the pricing table's example binds the same way.
  • The aspect-ratio page tells you to give a column that holds a box min-width: 0. In a row or a grid of columns, Safari and every other browser on iOS keep such a column at the width it had before the window narrowed, so turning a phone upright left the columns of the page's own examples wider than the screen. The examples carry min-width: 0 now, and the page says when a layout of your own needs it. Columns from the grid's cols prop need nothing. A column with a caption under its box gets a floor that carries the caption instead, min-width: 8rem in the example, so the caption no longer runs past a phone's screen.
  • Two further-reading links point at pages that exist again. The one-time-code input linked the Web OTP page Chrome has moved, and the icon page linked an Authoring Practices pattern that does not exist; it now links the img role in WAI-ARIA 1.2.
  • The label page no longer lists --font-wk-body-weight as the label's weight. A label is medium weight, and the token never reached it: the weight beside it decided. The page and the component now say the same.
  • The multi-select page describes its open list as it is. It said a chosen option shows a check and the selected weight in the list, while a chosen option leaves the list and shows as a pill in the field, as it always has; every row of the list says aria-selected="false". The mark is gone from the markup, and with it the option-selected and option-check class blocks and the empty column each row kept for the check, so a long option label has that room now. --font-wk-selected-weight no longer names the multi-select among its readers.
  • The word inside a sentence that opens a hover card keeps its focus outline. The example reset its button with all: unset, which also took away the outline a keyboard user finds it by. It now resets the background, the border, the padding and the margin by name, and sits in the sentence as before.
  • Three examples work for every reader. The tabs example with a purple accent reads in dark mode, the reveal example with a click trigger plays by keyboard, and the stacked hero example keeps its heading outline.

Components

Every component this release changes, each linked to its page: Accordion · Action Bar · Alert · Alert Dialog · Announcement Banner · App Rail · App Shell · Assistant Message · Attachment · Avatar · Badge · Bottom Nav · Branch Switcher · Brand · Brand Bar · Button · Calendar · Callout · Card · Carousel · Chart · Checkbox · Clipboard Button · Code Block · Collapsible · Color Picker · Combobox · Command Palette · Context Menu · Conversation · Countdown · CTA · Data List · Data Table · Date Picker · Drawer · Dropdown · Editor · Empty State · Event Calendar · FAB · Feature · Field · File Upload · Filter Builder · Flag · Header · Heading · Hero · Hover Card · Icon · Image · Image Compare · Inline Edit · Input · Kanban · Label · Lightbox · Link component · Liquid Glass · Map · Menubar · Message · Message Group · Message Typing · Modal · Multi-Select · Navbar · Navigation Menu · Notification Center · Number Input · OTP Input · Overflow Nav · Page Header · Page Progress · Pagination · Password Input · Phone · Popover · Pricing Table · Product Card · Progress · Progress Radial · Prose · QR Code · Radio · Range Slider · Rating · Reaction · Reading · Resizable · Scope Switcher · Scroll Area · Segmented Control · Select · Shell Bar · Sidebar · Skeleton · Skip Link · Slider · Spinner · Stage Card · Stat · Status Matrix · Status Tiles · Step Marker · Sticky Panel · Strength Meter · Table · Tabs · Tags Input · Team Section · Testimonial · Text · Textarea · Theme Controller · Time Picker · Toast · Toggle · Toggle Button · Toolbar · Tooltip · Tour · Unsaved Hint

v2.63.0

Minor. A value handed to a component stays data when the application has turned off double encoding in Blade, the labels, series names and goals of an ApexCharts chart reach its legend and tooltip as text, AlpinePayload::json() is new for a value your own script reads back, and a countdown and a message write their times in the reader's zone with timezone. A breadcrumb and a stepper check the link targets of their items, a color picker draws a preset only as a color, a list prop takes a Collection, a record goes into the page with only the keys its component reads, a field you build yourself asks for the touch text floor, a Tab in Safari stays inside an open dialog, a calendar, a kanban column and a data table's toolbar fit a 320px screen, the keys of an input method stay with it while it converts, a list field's name written with [] keeps one, and an empty list announces nothing. A Chart.js chart draws the data a Livewire update gives it and the points streamed into it, an ApexCharts chart keeps its data through a theme change, its tooltip titles a point on a date axis with its date, lists only the series on the hovered date and reads a candle in every form ApexCharts takes, a date of any class keeps its instant, a dialog from a touch region keeps its sizes through a Livewire update, wirekit:verify notices an update installed from a release, and a scroll a component starts under reduced motion is a jump also on a page whose root scrolls smoothly.

Security

  • The labels, series names and goals of an ApexCharts chart reach its legend and its tooltip as text. With library="apexcharts", a chart wrote the labels and the names of its datasets into its legend and its tooltip as HTML, even behind the tooltip WireKit draws, so a name from your data that held markup ran in the page when the chart was drawn or hovered. In every chart type a name with < or & now shows those characters, and the dates of a date axis keep their format. The name, the value and the color of a goal on a bar went into the tooltip the same way, the color inside a style attribute; they reach ApexCharts as text too, when the chart is drawn, when a Livewire update changes its data and when a point is streamed in, and a valid color draws as before. A formatter set for every chart through window.Apex stays in place, and it has to escape what it returns.
  • A value handed to a component stays data when your application has called Blade::withoutDoubleEncoding(). After that call {{ }} leaves a character reference such as &quot; or &#106; as it is, and the browser decodes it inside the attribute. In a directive, the decoded quote ended the string it sat in: a label, an option or a step name that held &quot; followed by an expression ran that expression when the page loaded, in every component that hands a text or a list to Alpine. In the state a script parses, the rows of a data table or the list of a filter builder, it could add keys. And a link target written as &#106;avascript: passed the scheme check and reached the browser as javascript:. A decoded reference now stays inside its string, and a link target is checked with its references decoded as well. With Laravel's standard echo none of this was reachable, and nothing changes in what a component reads. If you use AlpinePayload::from() in a directive of your own, the fix applies there. If you read its output back with JSON.parse(), write that value with AlpinePayload::json() instead: from() writes a JavaScript literal, and one that holds a character reference is not JSON.
  • A record handed to an event calendar, a map or a notification center goes into the page with only the keys of its shape. These components write their list into the page for their script, and each record went with every key it had, so a record from a query carried its other columns into the page's source as well. The events and day markers of an event calendar, the markers of a map and the notifications of a notification center now carry the keys their page lists, and an attendee its name and avatar. A script of your own that read another key from one of these components' state no longer finds it.
  • The rows of a data table go into the page with only the fields the table reads. Each row went with every key it had, so the rows of a query shown as a name and an email carried each customer's other columns into the page's source. A row now carries its row-key, the sort-key the table starts on and the keys its columns name. With a rowActions slot the rows still go whole, because the slot has the row in scope; the page shows how to hand such a table only the fields it may show.
  • A crumb whose target comes from your data can no longer run script. The items of a breadcrumb put their href into the link unchecked, and the breadcrumb's structured data carried it as the step's URL. They now keep http, https, mailto, tel and relative URLs, like every other component that reads a link target from a list. A crumb whose target is refused renders like one without href: as text, in the trail and in the structured data.
  • A step whose target comes from your data can no longer run script. The steps of a stepper put their href into the link unchecked as well, and keep the same targets now. A step whose target is refused renders like one without href: as a button when it carries a wire:click and as text when not. A step with an empty href is no link either; it linked to its own page before.
  • A color picker preset is drawn only as a color. Each entry of presets on a color picker went into its swatch's style as written, so an entry that carried further declarations behind a color styled the swatch itself: it could lie over the whole page as an invisible field while the popover was open. A preset is now a hex color, rgb(), hsl() or oklch(), the forms the picker reads. Any other entry draws no swatch and is reported like every rejected prop value, a color name such as red included; a click on such a swatch never picked a color. The recent colors a picker keeps in the browser's storage are checked the same way.
  • A step of a stepper takes its action from wire:click alone. The component also read it from a key named action, which was never documented. Livewire evaluates that value as an expression when the step is clicked, so a list of steps built from your rows turned a column named action into a button that ran its value. If you used that key, write wire:click. An empty wire:click no longer renders a button that does nothing.
  • An entry of an overflow nav takes its target from href alone. An href among the entry's attributes reached the link as written, and when the entry's own target had been refused it was the only one the link had. That key is left out of the attributes now; everything else in them still reaches the anchor.

Added

  • AlpinePayload::json() writes a value your script reads back with JSON.parse(). A data- attribute or a hidden field is not a directive, and AlpinePayload::from() is the encoder for a directive. json() writes JSON that stays one value whichever way your application echoes it. The CLI reference shows the two side by side.
  • A field you build yourself takes the touch text floor with wk-touch-text-floor. On a touch screen wk-field lifts a field's text to 16px through its text token. A select, input or textarea of your own whose size is a literal such as text-[15px], or inherited, has no token to lift: it stayed below 16px on a phone, and mobile Safari zooms the page when such a field takes focus. With the marker the field shows at least --text-wk-touch-floor there, and at least the size of the text around it. Until 2.61.0 wk-field set every field to exactly 16px, so a hand-built field with a literal size below that was lifted then and has not been since; it needs the marker now. The integration guide shows both cases.
  • A countdown and a message write their times in the reader's zone with timezone. A name such as Europe/Berlin converts the deadline a countdown names in its accessible name, and the timestamp and the status time of a message, before they are written. Without it, a time stays in the zone of the value, which for a date from the database is usually UTC.

Fixed

  • A list prop takes a Collection. The trail of a breadcrumb, the steps of a stepper and a wizard, the selected values of a tags input and a multi-select, the levels of a strength meter, the items of an overflow nav, the countries of a phone field, the data of a sparkline and the labels and datasets of a chart threw on a Collection or rendered as if nothing had been passed, so a tags input or a multi-select started empty although the values were there. Each takes an array or a Collection now, a lazy Collection and a paginator included, so $user->skills->pluck('id') goes in as it is. The integration guide says it once for every list prop.
  • A map, a record and a paginator go into a component as they are. The options a pluck('label', 'value') returns made a segmented control throw, and so did the marks of a slider as a Collection. The intervals of a pricing table, the names behind a reaction, the value texts of a slider and a range slider and the hidden columns of a data table were read as no value when they arrived as a Collection. A data table, an event calendar, a notification center and a status matrix read a paginator or a lazy Collection as the object's own properties, so a table given a paginator showed its page size and its path as rows. A message whose author was a model wrote the whole record into the author's name. Each takes an array, a Collection, a lazy Collection and a paginator now, and reads a model as the record it holds.
  • The items of a list may be models. The result of a query is a Collection of models, and a component that prints one field of each item took a model for a plain text, which for a model is the JSON of the whole record. A stepper and a wizard wrote it as the step's label, a breadcrumb as the crumb, an image gallery and a lightbox as the image's address, quick replies as the reply's value, a combobox and a multi-select as the option's value, and the sources of an assistant message as the chip's label, although that page names models as an input. A scope switcher and a chart threw. Each reads a model as the record it holds now, so only the fields a component reads reach the page.
  • A filtered or sorted Collection is read as the list it is. filter(), where() and sortBy() keep the keys of what they return, and five places read such a list by its keys. The labels of a chart and the data of a series reached the chart as an object: ApexCharts drew nothing, and Chart.js put the bars under new categories named after the keys, or threw. A sparkline threw on the missing first key, a stepper numbered its steps by their keys and marked the wrong one as current, and a select field of a filter builder offered no options. Each reads its items in the order the Collection has them now, so a sorted list also stays sorted.
  • A list field's name written with [] keeps one. name="skills[]", the way a native multiple select is written, submitted skills[][] from a multi-select, a tags input and a multiple file upload, which PHP reads as a list of one-item lists. Both spellings submit skills[] now.
  • A Chart.js chart draws the data a Livewire update gives it. Since 2.56.0 a chart under Chart.js took the new labels and series of a Livewire update and went on showing the old ones: the update reached the chart through Alpine's reactive wrapper and threw Maximum call stack size exceeded, and the component caught the error, so nothing reported it. A change of the reduced-motion setting while the page is open failed the same way. Both redraw the chart now. A chart that is removed from the page is also let go by Chart.js, whose animation queue kept every removed chart in memory.
  • A point streamed into a Chart.js chart is drawn. With wireStream under Chart.js, each point that arrived threw Maximum call stack size exceeded in the console and the chart went on drawing the points it already had, because the stream updated it through Alpine's reactive wrapper. The point is added and drawn now.
  • An ApexCharts chart keeps the data a Livewire update or a stream gave it when the theme changes. With library="apexcharts", a change between light and dark, by a toggle or by the reader's system while the page is open, drew the chart again with the data it was first drawn with: a report filtered to another period went back to the one it loaded with, and streamed points disappeared. It keeps what it shows now, in the colors of the new theme.
  • The tooltip of an ApexCharts chart on a date axis titles a point with its date. A point whose x is a timestamp, given as an object or as a pair, was titled with the number (1767225600000), one whose x is a date string with the string itself, and tooltip.x.format was not read, although the date previews on the chart pages set it. The title is now the date in that format, ApexCharts' own dd MMM until you set one, with names in your application's locale, in UTC as the axis is labeled unless xaxis.labels.datetimeUTC is false. The two ends of a range on a timeline follow the same format and zone. They were a short date with the year in the reader's zone, which put a range that starts at midnight UTC on the day before for a reader west of UTC, and an end before 1970 stayed a negative number. A timeline without a format of its own now shows dd MMM there as well; 'tooltip' => ['x' => ['format' => 'MMM dd, yyyy']] keeps the year. A range column or a range area writes the ends of its ranges as the values they are, where an end above ten billion read as a date.
  • A shared tooltip of an ApexCharts chart lists the series on the hovered date. With library="apexcharts", the tooltip of a chart whose series have dates of their own listed the value every series holds at the hovered position: a value of January 1st stood under January 11th, and a series that ends earlier, or one switched off in the legend, read undefined. A gap in a series read null. A series on another date is left out now, and a series without a value at the hovered point has no row.
  • A candle given as one flat list is read as a candle. ApexCharts also takes a candlestick point as [x, open, high, low, close], and the tooltip wrote such a point as a boxplot whose minimum was the timestamp. A candle and a boxplot now show the numbers ApexCharts read, in each form it takes: flat, nested, or as x and y, the form the candlestick page shows.
  • A formatter set for every chart titles a point on a date axis. A tooltip.x.formatter or an xaxis.labels.formatter set through window.Apex decides the title of ApexCharts' own tooltip, and the tooltip WireKit draws wrote the date in tooltip.x.format instead. It takes its title from that formatter now, and writes what the formatter returns as text.
  • A countdown counts to the instant an immutable date holds. A CarbonImmutable, which every model hands over in an application that has called Date::use(CarbonImmutable::class), was written out without its offset and read in the application's zone, so a deadline handed over in Berlin counted an hour or two too long. Any date object now counts to the instant it holds.
  • A date separator shows an immutable date in its timezone, and leaves the date it is handed as it was. An immutable date kept its own zone, so a message sent at 23:30 UTC stood under yesterday for a reader in Berlin, and a mutable Carbon was converted in place, so your own variable came back in the reader's zone.
  • A Tab inside an open dialog no longer takes the focus out of the page in Safari. By default, Safari on a Mac stops a Tab on text fields and pop-up menus and passes buttons, links, checkboxes and radios by. In a modal, a drawer or any other overlay that traps the focus, a Tab from the last control Safari stops on found nothing further inside: the focus went to the page's body, and the dialog stayed open without the keyboard in it. The control Safari passed by takes the focus now, so a dialog that ends on its buttons reaches them, and one that begins on its close button reaches that with Shift+Tab.
  • The keys of an input method stay with it while it converts. The Enter that confirms a Japanese, Chinese or Korean conversion added the half-converted text as a tag in a tags input and picked an option in a combobox and a multi-select, and an Escape that abandoned a conversion in a field of a modal or a drawer closed the dialog. WireKit leaves Enter, the arrow keys and Escape to the input method while it converts, in its own fields and in fields you write inside its dialogs. The localization page says what that covers.
  • A dialog or a drawer opened from inside data-wk-touch keeps the touch sizes while a Livewire update runs. The panel took the touch sizes when it opened, and the first server round trip with it open, from a field bound with wire:model.live for one, put its fields and buttons back to the mouse sizes on a till with a mouse plugged in. It keeps them now for as long as it is open, as touch sizes on any pointer promises.
  • A calendar fits a container down to 222px. A day was 36px wide whatever its container, so the grid needed 280px and the panel 306px, and on a 320px screen, which is also the width a desktop reader has at 400% zoom, the last column was cut off. A day now shrinks with its column down to 24px, the smallest target WCAG 2.5.8 allows, and keeps its 36px wherever there is room, so a calendar in a container that sizes to its content is as wide as before. With selectable-header on a touch screen the year moves under the month where the two do not fit beside the arrows.
  • A kanban column fills a board narrower than 280px instead of standing out past it, in a vertical board on a phone and in a narrow horizontal one.
  • The toolbar of a data table wraps its density toggle and its column button where the two do not fit beside each other.
  • A multi-select and a tags input fit a narrow container in Safari. WebKit took the default width of their text input as its minimum, so on a 320px screen the field stood 13px past a container Chromium filled exactly.
  • An empty list announces nothing. An empty breadcrumb trail, as on an application's home page, rendered a navigation landmark around an empty list; the landmark is hidden now unless the actions slot holds a control. A stepper without steps hides its list, and a tabs row without tabs carries no tablist role.
  • wirekit:verify notices an update installed from a release. Composer keeps the release archive's times on every file it unpacks, so after a composer update the checks for a build, a compiled view cache and a generated AI catalog older than the installed package compared them with the day of the release, and stayed quiet about one made between the release and the update. They date the install by the package directory now. A deploy that copies a vendor directory onto a server dates the install by the copy there; run the check where the build runs.
  • Under reduced motion a scroll a component starts is a jump, also on a page whose root scrolls smoothly. Scroll to top, the reading primitives, a tour, a conversation and a carousel answered reduced motion with the browser's default scroll behavior, and that follows a scroll-behavior: smooth on the page's root, as Tailwind's scroll-smooth sets: the jump glided again. They jump now.
  • The scrollToMessage() of a conversation finds a message whose id holds a quote or a backslash. The id went into a selector as written, and the selector was invalid.
  • A link target with a long run of spaces inside it no longer stalls the render. On a PHP without the PCRE JIT, checking such a target took quadratic time, over a second for 16,000 spaces. The check is linear now, and it reads an upper-case scheme and trims a target the same way under every locale; under two legacy locales it had dropped every link.
  • A link target given as a backed enum, as 0 or as a number works in every list component. An overflow nav and a scope switcher threw on a backed enum, and a breadcrumb and status tiles drew an entry whose target is 0 without its link.
  • A trail built with Schema::breadcrumbList() treats an address the breadcrumb would not link as a step without a page. The builder kept any address as the step's item, a javascript: one included, while the component left that step out. Both read the address by the same rule now: http, https, mailto, tel or relative.

Documentation

  • The CSS API page says what wk-field does on a touch screen. It still described the fixed 16px that 2.61.0 replaced. The class lifts the field's text tokens to at least 16px, and a size written as a literal on the field is not lifted.
  • Five examples wrap on a 320px screen instead of running past their frame: on the pages of the link component, the lightbox, the spacer, the file upload and the scope switcher.
  • The marketing landing page recipe shows the sparkline of its third figure. The line was drawn in the color of the card it sits on and could not be seen in either theme.
  • Five pages say which link targets are kept. The pages of the overflow nav, status tiles, scope switcher, notification center and assistant message said a target is dropped when its scheme could run script. The rule is a list: http, https, mailto, tel and relative addresses become links, and every other scheme is dropped.
  • The scroll to top page no longer says that browsers turn smooth scrolling off under reduced motion on their own. They do not; the button decides it, and the page says how.

Components

Every component this release changes, each linked to its page: Accordion · Alert Dialog · App Shell · Breadcrumb · Button · Calendar · Carousel · Chart · Chart Mixed · Checkbox · Collapsible · Color Picker · Combobox · Command Palette · Conversation · Countdown · Data Table · Date Picker · Date Separator · Drawer · Dropdown · Editor · Event Calendar · File Upload · Filter Builder · Hover Card · Image Gallery · Inline Edit · Input · Kanban · Lightbox · Map · Message · Message Group · Modal · Multi-Select · Notification Center · Number Input · OTP Input · Overflow Nav · Pagination · Password Input · Phone · Popover · Pricing Table · Profile · Quick Replies · Range Slider · Rating · Reaction · Reading · Scope Switcher · Scroll to Top · Segmented Control · Select · Slider · Sparkline · Stat · Status Matrix · Status Tiles · Stepper · Stream · Strength Meter · Tabs · Tags Input · Textarea · Theme Controller · Time Picker · Toggle · Toggle Button · Tooltip · Tour · Wizard

v2.35.0

Minor. The stylesheet you serve is a fifth of the size it was, the ES module can finally be tree-shaken, and a handful of controls now do what they always said they did — a rich-text editor that stops zooming iOS, a checkbox whose third state survives a server round trip, a sidebar rail that no longer shifts the page when it collapses.

Added

  • --z-wk-chrome — a layer for page chrome that must stay above overlays opened in page content. <x-wirekit::header sticky> and <x-wirekit::navbar sticky> sit on it, so a popover in an article no longer paints over the site header. It stays below --z-wk-modal, and a panel anchored inside the header is unaffected: top chrome opens downward and never overlaps the bar it hangs from.
  • --reading-progress-segment — retint the chapter dividers <x-wirekit::reading-progress :segments="…"> draws, independently of the bar itself.
  • previous-label and next-label on <x-wirekit::pagination> — name the two direction controls. "Previous" and "Next" are misleading on a reverse-chronological list, where moving "next" goes backward in time; on a changelog or an activity feed, previous-label="Newer" says what the button does. Unset keeps the translated defaults.
  • label on <x-wirekit::drawer> and <x-wirekit::alert-dialog> — name the dialog without a header slot. A drawer built from body content alone previously had no accessible name and no way to give it one.
  • The undo icon alias — a real undo glyph in all four presets, so the name keeps meaning the same thing when a project changes preset.
  • Per-component ES module exports. Every Alpine factory is exported by name and register() takes a chosen subset, so a bundler can drop what you never render. Measured with esbuild: one component 26 KB raw / 10 KB gzip against 235 KB / 66 KB for the whole library. The default export is unchanged.

Changed

  • [@wirekitStyles](https://github.com/wirekitStyles) links a minified stylesheet. The readable, fully commented dist/wirekit.css is still published beside it — it is the source of truth for what every token means — but two thirds of it is comments, and it is render-blocking. The linked file is now 12 KB gzip instead of 74 KB, verified to contain every token, component class and at-rule the readable one does.
  • wirekit:install --font=<key> publishes that family only. It copied the whole 5.8 MB font tree for a single flag, once per --font* flag, and again on every composer install where the publish is wired into post-autoload-dump.
  • <x-wirekit::prose> wraps long lines in a <pre> instead of scrolling them. A horizontal scroll region has to be reachable by keyboard, and Prose styles markup it did not author, so it cannot put a tab stop on your <pre>. Author one yourself — <pre tabindex="0" role="region" aria-label="…"> — and Prose gives that block its horizontal scrolling back.
  • wirekit:doctor checks the stylesheet the page actually links. It reported the readable copy as published while the linked one was missing, which is a green tick over an unstyled page.
  • Sidebar rows keep one height whether the rail is expanded or collapsed. They were sized by the label when it showed and by the icon when it did not — 2.75px per row on the shipped type ramp, so a three-item rail moved everything below it by 8.25px on every toggle.

Fixed

  • <x-wirekit::editor> no longer makes iOS zoom on focus. Its typing surface computed to 14px: the 16px floor was written for form controls, and a rich-text editor types into a contenteditable, which no input/textarea selector reaches.
  • A caller's aria-label reaches the control on inline-edit, editor, command-palette, drawer and alert-dialog. It was landing on a wrapper with no role, so the element the user operates had no accessible name at all.
  • Every labeled <x-wirekit::inline-edit> had a control with no accessible name — the label's for and the control's id were two different strings.
  • <x-wirekit::checkbox indeterminate> follows a change made after the first render. The third state is a DOM property with no HTML attribute, and it was applied once; a Livewire round trip left the box reading "none selected" while something was selected.
  • <x-wirekit::collapsible> and <x-wirekit::tabs> keep their content across a Livewire round trip. Both built ids that changed on every render, so the morph replaced the region: focus left the slot, anything typed into an unbound input was discarded, and an open panel flashed hidden.
  • <x-wirekit::menubar.menu> and <x-wirekit::navigation-menu.item> keep the classes you pass them. They emitted class twice, and the browser discards the second.
  • <x-wirekit::code-block> and <x-wirekit::shell-bar> scroll regions are reachable by keyboard. The shell bar's wiring depended on its optional label, so the common usage shipped an unreachable scroller.
  • <x-wirekit::message> reveals its actions on touch. The default reveal used hover and focus, and a phone has neither in a discoverable form — reply, react and delete were invisible with no way to summon them.
  • Bottom-anchored floating controls clear the iOS home indicator. action-bar, scroll-to-top, reading-bookmark and the <x-wirekit::reading-progress indicator="dot"> badge sat inside the gesture strip, which swallows taps.
  • <x-wirekit::drawer> measures against the dynamic viewport, so the strip it reserves is not consumed by the mobile browser's own chrome.
  • Reveals no longer flash before they animate, and a click-triggered one is visible enough to click. The rule that pre-hides an element until its animation runs was spelled for one authoring form in its anchor and for the other in its exclusions, so each was half-covered in opposite directions: a <x-wirekit::reveal> painted at full opacity before sliding in, while a wirekitAnimate('…', { trigger: 'click' }) written by hand was hidden into a target nobody could aim at. Measured across all seven combinations.
  • A <x-wirekit::stat animate> counter can be started again. It fires once when the stat scrolls into view — correctly, since a stat that re-counts on every pass would be distracting — but nothing could ask for a restart: the observer disconnects itself after the first intersection, and the page said to scroll the stat out of view and back, which for that reason could never have worked. Dispatch wirekit:stat-replay on the stat's root. Under prefers-reduced-motion: reduce the value is already settled and the event does nothing.
  • <x-wirekit::mark> renders its themed background again.
  • reading-progress chapter dividers are visible in dark mode — they were a fixed near-black on a transparent strip.
  • The sliders shrink with a narrow parent instead of overflowing it. Their minimum width was a hard floor, against a comment promising the opposite.
  • <x-wirekit::glass /> is documented for the start of the body, not the head. It emits an <svg>, and an SVG in the head ends head parsing — every metadata tag after it is reparented into the body, where a crawler does not look.
  • Config defaults that nothing read are wired or removed — stat.animate, editor.format, editor.toolbar and editor.size are read from config now; date-picker.format is gone, since a native date input renders in the viewer's locale and cannot be told otherwise.
  • as is validated before it becomes a tag name. A value containing a space or an = was rendered into the opening tag as an attribute.
  • wirekit:install --rollback no longer runs out of memory on a large install log, and a second run reports that the session is already undone instead of silently replaying it.
  • <x-wirekit::fonts> documents its one prop. nonce was explained in prose but never listed as a prop, so it was absent from the one place a developer looks for the component's surface.
  • The wirekit:doctor page shows the glyphs the command prints. Five of its six output examples used ⚠ for a warning, which the command has never emitted — it prints ! — and the page asserted the wrong glyph in its own legend. Four of the quoted lines also carried labels and a shape the command does not produce. A developer comparing their terminal against the page was matching it to output that does not exist.
  • wirekit:glass install tells you to put the component in the body, where it belongs. The command and both pages that show the placement still put it in the <head> — the placement this release announced as corrected. An SVG there ends head parsing, so every metadata tag after it is reparented into the body.
  • wirekit:icons --audit says what it looked at when it finds nothing. It reads two surfaces — the <x-wirekit::icon> tag and the icon prop on every component that takes one — but both of its refusals named only the tag, so a project whose icons are props was told no icon tags were found and sent looking for a scanning path that was not the problem. The bound-value line said :name="…" where a prop binds as :icon.
  • wirekit:class-by-area looks for your application's own compiled CSS, and says so when it finds none.

Documentation

  • The ES module page explains how to import a subset, with measured sizes.
  • wirekit:doctor's example output is transcribed once, on the command's own page.
  • Fifty-seven blueprint code blocks that name a destination .php file now open with <?php.
  • Thirteen page-to-page links that pointed at a fragment no heading generates are corrected.
  • Every publicly served page has an authored meta description within the serving cap, and no two share a title.

v2.34.2

Patch. The collapse control on a sidebar moves to the bottom of the column, where this library's own documentation already said it was.

Fixed

  • The collapse control sits below the navigation. It was the first child of the column, so it rendered at the top — while the page describing it told the reader to click the chevron at the bottom, and while the app rail's expander really is down there. One gesture, two components, two places to look for it.
  • It no longer shares an edge with the row beside it. The column carried no row gap, so the control's hover surface ended at exactly the pixel where the adjacent sidebar.item's began. Two surfaces of the same gray touching read as one smudged block rather than as two controls — and on a collapsed rail, where both are icon-sized squares, nothing else tells them apart. The column now spaces its rows by the token they were already spaced by.
v2.33.0

Minor — this release is about being told what you actually have. wirekit:doctor names every personalized class block your application owns, wirekit:icons --audit separates the icon names under contract from the ones that merely work today, the MCP server hands an assistant real worked examples and the full component signature instead of a name and a category, and a scroll area fades only the edges its content really continues past.

Added

  • wirekit:icons --audit tells you which of YOUR icon names are under contract. <x-wirekit::icon> renders any name your icon set knows, so a glyph name works — and looks exactly like a declared alias right up until somebody switches preset, at which point every one of them breaks at once. The audit reads your views and separates the two, with the file and line of each fall-through. It never calls a glyph name an error (some glyphs have no alias and never will) and never suggests a replacement (checked across ten such pairs, ten pointed at a different character). Names bound at runtime are counted separately rather than quietly dropped, and a run that finds no icon usage at all exits non-zero — "nothing was measured" and "nothing is wrong" are different answers.

  • The MCP server gained get_component_examples — worked examples instead of assembled markup. php artisan wirekit:mcp-serve could tell an editor what props a component accepts; it could not show one being used. A prop list says what is allowed, and an assistant filling one in guesses at the composition — which sub-component wraps which, which props are set together, what the canonical shape actually is. The new tool answers that from real, reviewed usage: 439 examples covering every documented component, and a sub-component (card.body) resolves to the page where it is shown inside its parent. Ask for it before writing markup. Documented in the AI tooling guide.

  • get_component now answers with the whole component, not a summary of it. The MCP server described a component as a name, a category, a description and three fields per prop. An assistant reading that could not tell an enum from free text, saw config('wirekit.components.button.intent', 'primary') as the default and had to guess what may be passed, and — worse — was never told that card.body exists, which is the one composition rule the shipped guidance spends a paragraph on. It now returns the same picture the JSON manifest carries: the documentation URL, whether the component is anonymous or class-based, every declared slot with whether it is required, every sub-component with its own props, and the full prop signature including type hints, the resolved default behind a config(...) call, and the example values the docblock names. A test compares the two surfaces field by field, in both directions, so one can no longer learn something the other does not. Documented in the AI tooling guide.

  • Three registry helpers became public API, because the manifest and the MCP server were each deriving the same answers privately and had begun to disagree: ComponentRegistry::slotsOf(…) returns a component's declared slots with their required flag, ComponentRegistry::describeSubComponentsOf(…) returns its sub-components with their props, and ComponentRegistry::existingBladeFilePath(…) resolves a component's template — or null when it has none, which is the answer the path-returning companion cannot give.

    The examples are extracted when the package is built rather than read at runtime, because the documentation is not part of what gets installed — a server that read it would answer correctly in WireKit's own repository and "no examples" in yours.

  • scroll-area gained fade="auto" — an edge fade that measures before it masks. The named edges (both, start, end) are unconditional CSS, which is what makes them free and also what limits them: they fade the top edge while the reader is already at the top, the bottom edge at the bottom, and both edges on an area whose content fits and cannot scroll at all — taking ink off text that is entirely visible, to signal something that is not true. auto masks only an edge the content continues past, and follows content that arrives later, which is the case worth having it for: a transcript appending a message, a list a search filters down, a panel that opens. Nothing to call, nothing to refresh. It is the one value that needs JavaScript, and it fails toward no mask at all rather than the wrong one — a missing hint instead of dissolved text. The named edges are untouched and stay pure CSS, so nothing that exists today changes, and the depth is still the --fade-wk-size token.

  • wirekit:doctor names every personalized block that replaces the shipped one. WireKit::personalize() takes two value shapes per block, and they differ in a consequence nothing reported: a finished class string REPLACES the block, while a closure receiving the vendor default extends it. A replacement is a valid choice — it also ends the flow of later WireKit changes to that block, permanently and without a word, so the personalization keeps looking like a decision somebody made long after it has stopped inheriting improvements. The check reports replacements as a warning with the block names and offers the closure form for the case where only a delta was wanted. It stays silent when every block extends. A new WireKit::personalizedComponents() returns the names of the personalized components; the map could be read per component but never enumerated, and a diagnostic cannot guess names it has no way to list.

  • wirekit:doctor:props gained --require-in-scope. A run that scans real templates and finds none of them using a WireKit component has two honest readings, and which one is right depends on the application rather than on the linter. If you do not use WireKit in that tree, nothing in scope is correct and the default still succeeds. If you use it everywhere, the same result means the walk found the wrong tree — a second view path, a renamed directory, an argument pointing somewhere empty — and a green run is the last thing you want. The flag is how you say which application you are. Reported by a developer whose only handle on that state was matching the success sentence in a shell script, which a reword would have deleted silently.

Fixed

  • The tab bar's active indicator was missing from the compiled CSS. A tab bar's appearance moved into PHP in 2.31.0 — a good refactor, and one Tailwind's [@source](https://github.com/source) glob never looks at, so six classes stopped compiling. They were not decoration: they are the active-tab indicator and the margins that pull it onto the container edge. The bar rendered, the tabs worked, the ARIA was correct, and the selected tab was simply not marked. Reported from a project that attributes its built stylesheet byte for byte — 123702 → 123169 bytes, six selectors gone and none added. Fixed through the safelist mechanism that already exists for this class of bug, listing all 48 emitted classes rather than only the six that went missing: the other 42 survive today because some unrelated view happens to use flex or gap-1, which is a coincidence and not a guarantee.

  • <x-wirekit::fonts> overwrote the shipped font tokens with weaker ones. The component wrote all three --font-wk-* variables unconditionally, standing in a hardcoded stack for a category nobody had configured — and those stand-ins are shorter than what the package ships. Both declarations sit unlayered at equal specificity, so document order decided it, and placed after [@wirekitStyles](https://github.com/wirekitStyles) the monospace stack silently lost two families. Nothing threw and the markup was identical either way; it showed only to a reader who had those fonts installed. An unconfigured sans or mono is now simply not declared, so the stylesheet's value stands from any position. Serif is deliberately still written, because the stylesheet does not declare it and omitting it would drop every serif surface to the browser default.

  • The fonts.fallbacks example gave a real family another font's numbers. The configuration stub and the fonts page both showed a named family with measurements that belong to a different one — directly below a line reading "measure the four values, do not estimate them". A developer whose font really was that family read the block as already measured and pasted it. Both examples now use a placeholder family with blank placeholders: a blank cannot be copied, a plausible number can.

  • wirekit:doctor told developers to delete configuration that was working. The config-drift check compared key names against the shipped stub and reported anything the stub does not carry as an option "this version no longer offers". Reported from a project where ten keys were named and all ten were wrong, in three shapes — keys whose names belong to the developer rather than to the stub, a feature whose stub value is an empty array so no correct use could ever match, and leaves sitting under a branch the stub does carry. Developer-keyed nodes are now exempt, a path that is a prefix of a stub key is not an orphan, and the wording no longer asserts that nothing reads them: a diff is evidence, not a verdict. The list also prints in full, because the truncation hid half of a finding whose whole point was which keys were named.

  • An underscore-spelled regional locale resolved to the wrong variety. pt_BR and pt-BR are one locale wearing two separators, but only the base-language half of that was handled. The underscore spelling never reached the regional catalog this package ships: it missed, the base fallback answered, and a Brazilian-Portuguese application quietly rendered European Portuguese. Nothing threw — the strings were all present, just from the wrong catalog.

  • The component manifest said nothing about glass. Of the whole catalog, exactly one component carries no props, no slots and no sub-components, and its emptiness is real. In a manifest that is indistinguishable from a component whose props could not be parsed — and the wrong reading is the expensive one, because a tool that assumes a parse failure will invent an API. Its description now says so, and a guard requires any component with nothing to declare to declare that.

  • list reported that it accepts no content. Every machine-readable surface — the JSON manifest, the project-root schema file, and every tool fed by them — listed the component with an empty slot array while its template renders {{ $slot }} on its last line. A developer asking the manifest how to use it was told to write an empty tag. The cause was a second Blade-path resolver that knew the flat and dotted filenames but not the directory-index form list is written in, so the file was never found and "no template" read as "no slots" — indistinguishable from the components that genuinely have none. Both surfaces resolve through one path now, and a test fails whenever a component that renders a default slot fails to report one.

  • Two siblings of the same overlay no longer stand open at once. Opening a second popover, dropdown, hover-card, menubar or navigation-menu menu on the same page left the first one open behind it, and the two panels overlapped. Nothing reported it — no console error, no changed markup — because the only symptom is what a reader sees. context-menu and combobox had each solved it separately; all seven now share one mechanism, so the next overlay inherits it instead of copying it. Opening a dropdown still does not close a popover: the coordination is per component family, which is the behavior that existed before and is not a question a patch release should answer differently.

  • The CSP advice about method names was far wider than the rule it described, and the extra width cost real renames. Two pages and wirekit:csp-audit itself said that a Livewire method "whose name is a JavaScript keyword" needs index access — $wire['delete'](...) instead of $wire.delete(...). Measured against the parser that decides it, that is true of ten names and false of the forty-two other reserved and future-reserved words, because a reserved word after a dot has been an ordinary property name since ES5. A developer auditing their own component against the old sentence renames public action names — for, class, return — that were never affected, and every one of those renames is reachable from templates and tests. All three places now print the set itself: delete, false, in, instanceof, new, null, true, typeof, undefined and void, with a named counter-example so the list reads as complete rather than as a sample. The set is no longer written by hand anywhere — it is checked against the tokenizer's own table on every test run, so a future change to that table fails the build instead of leaving three pages quietly wrong.

Documentation

  • Two PHP entry points that were only findable in the source are now on a page. WireKit::avatarPaletteFor(...) returns the same background/foreground pair <x-wirekit::avatar from-initials> derives, so a custom chip can match an avatar without rendering one. WireKit::defaultsFor(...) reads back what WireKit::defaults([...]) registered — with the distinction stated on the page, because it reports the runtime record rather than the value in effect.

  • Four tables across three pages rendered as raw text and now render as tables. A blank line, a callout and a paragraph had each been placed inside a table, and one font-size table carried no header row at all. Markdown ends a table at the first interruption rather than resuming it afterwards, so every row below the break was published as pipe-separated body text. Affected the bundles table on the dependencies page, the App Shell prop table, and the motion and font-size token tables.

  • wirekit:verify --tier examples corrected. The commented check ranges beside the two --tier examples still described the numbering from before a check was added, and the environment-tier comment pointed at a package-tier check.

  • The gap scale is now reachable from the components that use it. --gap-wk-* and --space-wk-* do not run on the same ladder, and the design-token page has said so since 2.28 with three guards keeping the table honest. It was still reported twice from two applications eight days apart, and the second report was measured against a version that already carried the table. That is a placement problem rather than a documentation one: measured, the words gap-wk, design-tokens, ladder and rung appeared zero times on the row, stack and grid pages, so a developer typing gap="lg" had no path to the paragraph that prevents the mistake — which also had no heading, so it was neither linkable nor in the page's contents. It has one now, and the five components that let a developer name a rung link to it: row, stack, grid, bento-grid and feature-grid.

  • The customization page no longer says nothing warns you. Its section on adjusting a block rather than replacing it explained that taking ownership silently stops later improvements from reaching it, and closed with "nothing warns you, because nothing is broken". The doctor check in this release is that warning, so the sentence became false in the same release that made it obsolete. It now names the check.

  • The PHP discovery surface is documented where it claims to be. The ComponentRegistry page opens by calling itself the canonical surface for discovering every component, and documented five of its thirteen entry points — subComponentsOf, tag, tagAlias, resolve, componentClass, isSubComponent, subComponents and extractAwareProps were all shipped and described nowhere. All of them are there now, with the three this release adds. Two claims on the page were also wrong: type_hint was said to be always null where fifteen props carry one, and the prop-record table listed five fields where the record has six.

  • The theme-preset registry had only its write side on a page. ThemePresetRegistry::register() was documented; the four reads that make it useful were not — which is exactly the set a theme picker needs. all(), keys(), get() and isValid() are documented together now, with isDefault() called out separately because it is the one a picker gets wrong: default is not a preset with variables, it is the instruction to remove the block, and treating it as a normal preset writes an empty one.

  • WireKit::cspNonce() and WireKit::prefix() are documented — the first in the integration guide's nonce section, with its resolution order and the fact that null means "no policy" rather than a failed lookup; the second in Getting Started, where code that builds a tag name should ask rather than assume, because the prefix is a setting and wirekit is only its default.

  • Localization no longer reads as base-language only. The page described the shipped catalogs without saying that a regional variety is a catalog of its own, which is the half a developer needs before choosing a locale string.

v2.32.1

Patch — a component and your own style no longer overwrite each other, a seeded value survives a Livewire morph, today is announced in the calendar, and two catalog strings say what they mean.

Fixes only. Nothing here adds or removes API, and every change is backward compatible — though two of them change what a page renders, which is the point: in both cases it was rendering the wrong thing quietly.

Fixed

  • A component and a passed-in style attribute no longer cancel each other out. Several components wrote their own inline style beside the attribute bag. As soon as a style was passed in, the element carried two of them — and a browser keeps the first and drops the rest, with no error and no invalid-markup complaint. Which half disappeared depended only on the order they were written in. aspect-ratio lost its ratio to anyone who also set a background; data-list and its items discarded the passed-in style instead. Both halves now survive as one declaration list, with your declaration last so a deliberate override still wins. The same shape is fixed in scroll-area, stepper, header, badge and every other component that had it.

  • A value seeded into x-data now survives a Livewire morph. segmented-control, rating, pricing-table and tabs interpolated a server-driven value into their x-data seed while also observing it through a data attribute. A morph rewrites x-data, so Alpine re-initializes on a DOM node that survived, and an effect queued against the pre-morph scope writes the old value last — the control reverted to its initial state after a server round-trip. The seed is now read from the attribute at init instead.

  • event-calendar announces today. The current day was distinguished by an accent pill and a heavier weight and nothing else, so "this is today" reached a screen reader only as a color. It now carries aria-current="date" in the month, week and agenda views.

  • Spanish: a status tile read as an instruction. On the success tile of status-tiles, OK was translated as Aceptar — the verb on a confirm button — where the key is a status word. Beside a green check it asked the operator to confirm something. It is now Correcto, matching Advertencia and Crítico in the same catalog.

  • Popover shipped untranslated in every catalog. The key was present with the English term as its value, so a completeness comparison found nothing missing while a reader saw English in an otherwise translated interface. It is now translated in Spanish, French, Italian and Portuguese; German and Dutch keep the established loanword deliberately.

  • wirekit:csp-audit pointed at the shape it does not hit. Its hint about Js::from() said the encoder wraps its payload for "any non-empty string, array or object". It wraps a non-empty array or object; a string of any length — apostrophes and non-ASCII included — comes back a quoted literal, as do numbers, booleans, null, [] and {}. Acting on the old advice for a string meant replacing a correct encoder with hand-written interpolation that the next apostrophe breaks. The report now names the real trigger and exempts the safe shapes explicitly.

Documentation

  • CLI reference: the wirekit:csp-audit section states the real trigger, says outright that a string is not the case to fix, and recommends AlpinePayload::from() for handing a composed payload to an Alpine directive — a helper the documentation had never mentioned.
  • event-calendar: the accessibility section documents the aria-current="date" contract across all three views.

v2.32.0

Minor — a shared icon vocabulary that resolves the same way on every preset, an icon package you can actually install, a prop-naming reference that was wrong about six things, and a prop linter that no longer fails a build over a test selector.

Nothing here changes what an unchanged call site renders. One command reports less than it used to, and it is the reporting that was wrong rather than the rule.

Added

  • Eleven words the shared icon vocabulary was missing. chart-bar, trend-up, percent, coins, gift, map-pin, sliders, list-checks, list-bullets, lock-key and broadcast — reporting, money, settings and location, the vocabulary a back-office reaches for. Each resolves on all four base presets, and each target was checked against the installed icon set rather than assumed. Two more were asked for and are deliberately absent: Heroicons has no gauge and no handshake, and an alias that changes the drawing when you switch presets is worth less than no alias at all — for those two, name the glyph directly. See Icon.

Changed

  • chart-bar moved from the marketing extension into the shared vocabulary. While the extension owned the name it meant chart-bar-square on Heroicons and nothing at all on Lucide, Phosphor or Tabler — so the one property a shared alias has to keep, that it means the same thing whichever preset you install, was the property it did not have. On heroicons-marketing the glyph is now the plain bar chart the other three presets draw. If you want the framed variant, name heroicon-m-chart-bar-square directly.

  • wirekit:doctor:props no longer reports a browser-test selector as an unknown prop. dusk is written on a component precisely so that it reaches the rendered HTML, where a browser suite selects on it. The linter counted it as a mistake, and in a project whose quality gate runs the linter under set -euo pipefail that exit code ended the stage before the tests ran — seventy of one project's findings were this one attribute. The line is what the attribute does: an unknown prop is an instruction that disappears, a test selector is one that arrives where it was aimed. The same change covers the runtime warning, since both read one verdict. Valid HTML attributes and the aria- / data- / wire: / x- / on prefixes were already passthrough and are unchanged. CLI Reference lists what is never reported.

Fixed

  • The tabler icon preset named a package you cannot install. ryangjchandler/blade-tabler-icons is marked abandoned and its newest release requires Laravel 10 or 11, while WireKit requires 12 or 13 — so composer require refused, for every supported installation. The preset was documented, listed in the preset table, and unreachable. It now names secondnetwork/blade-tabler-icons, the maintained successor: same tabler- prefix, same alias vocabulary, ~7,200 icons, and it tracks Tabler's own version line. Nothing changes in your templates — <x-wirekit::icon name="user"> on the tabler preset resolves exactly as before. Icon names the new package.

  • megaphone was broken on the tabler preset. It mapped to tabler-megaphone, which does not exist — Tabler calls that glyph speakerphone. Blade Icons throws on an unknown name, so the page broke rather than degraded. It survived because the preset's package could not be installed, which meant nothing could check its 94 targets; all four presets are verified now, and every other alias across them was already correct.

  • wirekit:publish-icons lucide reported success while publishing the wrong thing. The command looked for SVGs in a directory the package emptied when it reorganized — the folder still exists, so the check answered yes and the copy produced icons/lucide/icons/… where every other preset gives you icons/lucide/…. The source directory is now located by looking for the files rather than by a written-down path, so an upstream reorganization cannot silently change what you get.

Documentation

  • The CLI reference documented an exit code no command returns. It described a three-value convention and gave exit 2 its own row and its own meaning, plus two more mentions under individual commands. Every wirekit:* command exits 0 or 1 — including on rejected input — and has done since that was standardized. A CI step written as if [ $? -eq 2 ] could never fire, and 1 read as narrower than it is. The reference now states the two-value contract and says outright that there is no exit 2. CLI Reference.

  • A recipe's dark band did not follow the theme. The marketing landing-page recipe painted its hero with raw hex — a near-black gradient with white text, plus three sparkline strokes in literal red, green and gray. Copied into an application, that band stayed exactly as light in dark mode as it had been in light mode while everything around it moved. It is tokens now, so the band belongs to whichever theme is active.

  • The prop-naming reference was wrong about six things, on the page that exists to settle them. It named a component this package does not have, filed two components under a prop they do not declare, classified alert and callout as surface-treatment while both resolve variant into the color axis, promised a stable seven-value intent enum that button does not accept, and described a v3 plan whose canonical surface axis was the wrong name. It also named one component as keeping a back-compat alias while seven do — which is how a reader plans a one-component migration for a seven-component change. The page now carries the full alias matrix, the accepted values per component, and the v3 mapping split into the rows that are a plain prop rename and the rows that also rename a value. The release date stays open. Prop Naming Conventions.

  • The integration guide says that x-cloak is covered. Alpine hides nothing for that attribute — it removes it once initialized, and the hiding has to come from CSS. WireKit ships the rule and [@wirekitStyles](https://github.com/wirekitStyles) delivers it, which was true and written down nowhere. The guide now also says why looking for it comes up empty: the stylesheet arrives as its own <link>, so the rule is never in your Vite bundle and grepping app-*.css finds nothing whether your setup is right or not. Integration.

v2.31.0

Minor — a Brazilian Portuguese catalog, a font of your own that no longer moves the page, a CSP audit you can act on without checking it first, and six pages that were teaching the version before this one.

Nothing here changes what an unchanged call site renders. Two commands report more than they used to: wirekit:csp-audit gained a warning class that leaves your exit code alone, and wirekit:verify compares every published bundle instead of four.

Added

  • A tab bar the server drives, with no panels — <x-wirekit::tabs.list> and <x-wirekit::tabs.tab>. tabs assumes the browser already holds each panel's content, which is the wrong shape for the arrangement a Livewire application reaches for first: a row of tabs above content the server renders, where choosing one is a round trip. Two of the things the full component does are actively wrong there — it holds the selection your server has already decided, and it emits aria-controls pointing at panels that do not exist, sending a screen-reader user somewhere there is nothing. The bar on its own holds nothing at all: selected is a plain server-side boolean that drives both aria-selected and the roving tabindex, and the keyboard handlers resolve the tabs from the DOM on every keypress, so Livewire replacing the markup cannot leave focus pointing at an element that no longer exists. Activation is manual — arrows move focus, Enter or Space commits — because selection following focus would fire one request per arrow key and render four pages nobody asked to see. Both bars are styled from one source, so variant and orientation mean the same thing in either. See Tabs.

  • Brazilian Portuguese. pt is European Portuguese, and regional locales resolve through their base language — so pt-BR was rendering fluent Portuguese of the wrong variety. A Brazilian reader saw (abre num novo separador) where they expect (abre em uma nova aba), and A carregar where they expect Carregando. Nothing failed: the page rendered, the screen reader read it out, and only a native speaker looking at the right string would ever have caught it. pt-BR now ships as a delta over pt — it holds only the strings the two varieties spell differently, so the shared wording stays in one place and cannot drift apart. Localization names the variety of every catalog, so you can decide deliberately rather than inherit one.

  • A font you host yourself can get a metric-matched fallback. Every bundled family ships one — a local system font registered under the family's own name with the web font's measured metrics, so text painted before the swap occupies the same box as text painted after it. Your own family got none of that, which is the setup null is for. Declare the four measured values in wirekit.fonts.fallbacks and WireKit emits the face. Empty by default and nothing is invented: a guessed size-adjust moves the layout in the other direction and looks deliberate while doing it. The method, including the two parts that are easy to get wrong, is on the Fonts page.

  • The font component's inline <style> can carry a CSP nonce. It is the only inline style WireKit emits, and it has to stay inline — the three custom properties come from your font configuration, not from our build. Without a nonce you cannot drop 'unsafe-inline' from style-src, and CSP Level 2 makes that a cliff rather than a slope: a nonce anywhere in a directive makes the browser ignore 'unsafe-inline' in that same directive, so this block loses its permission the moment you add a nonce for anything else. Silently — the page renders and your typography falls back to the system font. The value resolves itself from a csp-nonce container binding or Vite::cspNonce(), so an application that already has one needs no configuration; an explicit :nonce still wins.

  • status-tiles items can state the word they report. The visible status word was derived from intent, which has five values — so a domain with more states folds two onto one word, and the pair it folds is usually the one carrying the information. A health check that ran and found a problem and one that crashed are both danger, and both read "Critical": the first points at your application, the second at your monitoring. Pass status per item and the tile says your word, in the caption and in the screen-reader text alike.

  • stream gained replace(text). push() cannot express a rendering derived from the whole stream rather than accumulated from its parts — masked text where a placeholder breaks across a chunk boundary, incremental Markdown where a closing fence changes what came before it, anything that diffs. Setting text directly looks like the workaround and quietly skips the reduced-motion buffer; replace() takes the same path a push takes.

  • The sidebar item's icon size can be personalized. It was a literal in the render call, so the only way to resize it was to take over the surrounding block — and a taken-over block stops inheriting improvements silently, which is the one outcome the closure form exists to avoid.

Changed

  • wirekit:csp-audit no longer reports a bare PASS for an expression it did not fully measure. Blade renders before Alpine reads, so an attribute carrying {{ … }} is checked with an identifier standing in for the part the audit cannot see. That substitution was invisible, and it hid the case that matters: Js::from() — Laravel's attribute-safe encoder, and the documented way to hand data to Alpine — renders to JSON.parse('…'), and JSON is precisely the name the CSP evaluator cannot resolve. The audit rejected that call written out by hand and passed it written the way everybody writes it, in the same file on the same line. A run now counts the expressions resting on a substitution, names the ones that call Js::from() or [@js](https://github.com/js)() so you can look at those first, and qualifies its verdict instead of claiming everything resolves in scope. Your exit code does not change — the same encoder emits a plain literal for a number, a boolean, [] or {}, and a violation that turns out to be nothing costs the next hundred their credibility. See CLI reference.

  • wirekit:csp-audit warns when an expression resolves but may not evaluate. Alpine's CSP evaluator refuses a value, not a name: it throws on any property access whose result is one of globalThis's own. So a chain can parse, resolve every identifier, run, and be rejected the moment it touches something global — $el.ownerDocument.location reaches window.location by another route. Reported as a warning that leaves your exit code alone, because the rule approximates a question about runtime values. The output also names what it measured, so a pass is not read as more than it is, and the CLI reference documents the supported shape.

  • wirekit:verify compares every published bundle, and reports a config option by name. The freshness check compared four; every bundle added since inherited nothing, so an upgrade could leave six stale files behind a clean report. It is derived from the package now. The config check also names the missing options and prints each default instead of counting them, and reports an option your file still carries that this version no longer offers.

Fixed

  • wirekit:csp-audit read a bare : on a <livewire:…> tag as an Alpine expression. On a component tag that is Blade's prop binding and its value is PHP, finished on the server. The scanner knew that for <x-…> only. Measured in a real application: 13 of 18 reported violations were false, and 12 of those were this one construct.

  • wirekit:csp-audit judged a Livewire action as written, not as Livewire presents it. wire:click="confirm" is rewritten to a call on the component proxy before Alpine sees it, so a method whose name collides with a browser global was reported as a dead handler while working perfectly.

  • countdown's locale prop did nothing. The x-data object declared the key twice, and a JavaScript object literal keeps the last one — so the line that read the prop was dead and locale="fr-FR" on a German page counted in German.

  • The unknown-prop check now reaches sub-components. It covered the top level and none of the 76 sub-component views — accordion.item, table.th, list.item, field.*, dropdown.panel and the rest, which are exactly the tags you write in a loop.

  • An overlay container you write yourself is finished, not just adopted. WireKit creates #wk-overlay-root and names it in the page's language; one already in your markup was handed straight back without a role or a label. Writing it yourself is the sturdier route — it sits in the markup Livewire morphs — so the localization was reaching everyone except the readers who had built the better setup. A label you wrote is left alone.

  • wirekit:csp-audit said "bridge" when it meant "node". A missing executable does not throw, so the branch written for it never ran and the message never contained the word node. It now says so before the process starts, and the dependency is named in the command's own description.

Documentation

  • Six pages were teaching the version before the one that linked them. All six had the same shape: the reference table was maintained and the prose around it was not.

  • Fonts recommended writing your own font-display rules — the exact hand workaround wirekit.fonts.display had replaced, and rules that land on the same faces as the metric-matched fallbacks that are the actual fix.

  • The CLI reference framed the CSP audit as a grammar check alone, which is precisely the half that does not explain why a passing build had started failing. It now says what the command looks at, and what it deliberately does not.

  • The script-order rule named the wrong invariant. [@wirekitScripts](https://github.com/wirekitScripts) must RUN before Alpine starts — which its own defer guarantees in a Livewire layout, so the order of the two directives is not the lever. The troubleshooting table sent you to check a tag order that was already correct, ruling out the wrong hypothesis and keeping the right one.

  • Scroll area documents that start and end are the reading edges, so a horizontal fade follows the document direction.

  • Table documents the edge hint that tells a reader the row continues before they touch it.

  • Integration documents the overlay container, the data-wk-overlay-label attribute, and wirekit.assets.middleware — the last of which the previous release's own entry had linked to a page that never named it.


v2.30.0

Minor — a personalization that keeps inheriting, an error message that finally names its own cause, and asset replies that are safe to cache where they claim to be.

Nothing here changes what an unchanged call site renders. The asset routes change which middleware they run; see the entry below if your application relies on web applying to them.

Added

  • A mistyped prop now tells you, on nearly every component instead of a handful. Blade folds a name a component does not declare into the attribute bag, where it renders as a literal HTML attribute nothing reads — the page looks finished and nothing fails. WireKit has warned about that in development since v2.6, with a did-you-mean hint, but only a handful of components asked for the check. It now covers essentially the whole catalog. The warning is development-only and silent in production, and the list of accepted names is read from the component itself, so it cannot fall behind the component it describes.

    Two things it deliberately does not flag: any valid HTML attribute, and framework wiring (aria-*, data-*, wire:, x-/@/:, v-, and the on* event family). A name a component reads through [@aware](https://github.com/aware) — announce-errors on any form control, for instance — counts as known too. That last part is also a fix: those calls were reported as typos before, on the components that already had the check.

    ComponentRegistry::extractAwareProps() exposes the [@aware](https://github.com/aware) half for tooling that needs it. It stays separate from extractProps(), which continues to mean "the props this component declares". Customization

  • A class block can state its delta instead of taking the block over. Personalizing a block has always meant restating it in full, and from that moment your application owns it — every later improvement WireKit makes to that block stops arriving, silently, because nothing is broken. A block value may now be a closure, and it receives the class string WireKit ships:

    WireKit::personalize('sidebar.item', [
        'base' => fn (string $vendor): string => $vendor.' rounded-none',
    ]);
    

    You are not limited to appending — the point is that you can see what you are overriding, so you can swap or drop one utility and keep the rest. Works on personalize() and scope(). A plain string still replaces the block outright, unchanged, and is still the right choice when you do mean to own the slot. Not available for class overrides written in config/wirekit.php: a closure there cannot survive config:cache, so it would work in development and vanish on the first cached deploy. Customization

Fixed

  • The scroll-area edge fade pointed the wrong way in right-to-left layouts. fade="start" and fade="end" name the reading start and end, but the horizontal mask was emitted with physical directions, so in an Arabic or Hebrew document the two were inverted: the fade sat on the edge the reader had already passed while the edge that actually scrolls stayed hard. Nothing failed and nothing logged — the hint simply pointed backwards. fade="both" is symmetric and was never affected, which is why the most-viewed example could not show it. Scroll Area

  • The asset routes no longer set a cookie on a response they ask a cache to keep for a year. The routes serving WireKit's own CSS, JS and fonts were registered inside the web middleware group. Their handlers read a file from the package directory — no session, no CSRF token, no authentication, no route-model binding — so the group gave them nothing, while StartSession added the session cookie to every reply. Those replies declare public, max-age=31536000, immutable: a shared cache is invited to keep them for a year and may hand one to the next visitor with the cookie still attached. That most shared caches refuse a response carrying Set-Cookie is a per-vendor default you do not control, not a property of what is sent. The practical cost was the reverse of what the header was written for — a CDN could not cache the files the year-long directive exists to let it cache, and every stylesheet hit paid for a session read and write.

    The group is now taken from wirekit.assets.middleware, which defaults to none. If your application applies its own middleware everywhere — a security-header layer, HTTPS enforcement — name it there; anything that starts a session brings the cookie back with it. Published assets served from public/vendor/wirekit/ never used these routes and are unaffected. Integration

  • A bundle that loads too late now says so, instead of leaving you with someone else's error. If the WireKit bundle is loaded after Alpine has already started, every component already on the page never receives its Alpine data. Those elements cannot be revived by registering afterwards, and what reached you instead was the browser complaining about the bindings inside them — startShadow is not defined, a property of a component you never wrote, from a file you did not author. Because tables are responsive by default, one ordinary <x-wirekit::table> was enough to produce it on a page whose author had never heard of a sticky panel. Every bundle now reports the real cause once, with the remedy, and only when there is actually affected markup on the page. Integration

Documentation

  • Design Tokens is now actually complete. The page opens by calling itself the complete CSS-variable surface, and 69 of the 237 declared tokens were not on it — among them --space-wk-md, the whole --size-wk-container-* ladder, both halves of the inverse color pair, every --shimmer-wk-*, and 26 tokens of the reading family. Looking one up returned nothing, with no way to tell "not a token" from "not written down". All of them now have a row, and a check keeps the claim honest as tokens are added.

  • Customization explains when to adjust a block rather than replace it, and why the choice affects whether you keep receiving upstream changes to that block.


v2.29.0

Minor — the navigation that took the page with it, and four checks that could not fail.

Nothing here changes what an unchanged call site renders. The additions are things the browser and the screen reader were told nothing about until now.

Added

  • Native widgets follow your dark theme. The <select> popup, scrollbars, number spinners, date and color pickers, autofill and the spellcheck menu are painted by the browser rather than by any stylesheet, and the browser had no way to know the page was dark — so it painted them light, on a page that was not. WireKit now declares color-scheme, so they follow. The native <select> is the one this matters most for, because using it is a deliberate choice here: it costs no JavaScript and gives a real picker on a real phone, and its popup was the one surface a screenshot of our own components would never show. Theming

  • A regional locale reaches its language. pt-PT, pt-BR, de-AT, es-MX, fr-CA — every regional variant previously saw none of the shipped translations, because the JSON loader matches a locale's filename exactly and there is no step from pt-PT to pt. An application running regional locales got English chrome inside a fully translated page, with nothing to indicate why. They now resolve through the base language, and your own catalog still wins per key, so you can reword one regional phrase without re-translating everything around it. Localization

  • The overlay landmark is announced in the reader's language. Every teleported panel lives inside one labeled region, and that label was the English word Overlays in every locale — invisible on screen, and therefore visible only to the readers the landmark was built for. It now follows the page's locale. A language WireKit does not yet ship keeps the English name rather than losing it: a region announced as "region" and nothing else helps nobody.

  • Two words the commerce vocabulary was missing. stack, and cash-register — the till the receipts come from, next to receipt, barcode and truck which were all already there. A name outside the vocabulary resolves through whichever icon set happens to be installed, so it works until the day you change one; stack did not even do that, because two of the four sets ship no glyph of that name and the icon package throws on a name it does not know. Icon

  • Self-hosted fonts no longer move the page when they arrive. Every bundled family now ships a metric-matched fallback face: a local system font registered under its own name with the web font's own measurements, named in the stack ahead of the generic families. Measured on a 400px column of prose, the unadjusted fallback rendered the block a full line shorter than the web font — that shift is now zero. The four override percentages are read out of the shipped font files rather than estimated, on both sides of every ratio. Fonts

  • font-display is yours to choose. wirekit.fonts.display sets what every bundled [@font-face](https://github.com/font-face) is served with; the default stays swap, which now costs no layout shift. Pick optional if you would rather some readers never see the web font than see it arrive late — that is a real trade, and it is your application's to make. It reaches the package route and wirekit:publish-fonts; a plain vendor:publish copies files verbatim, so when that leaves a published copy disagreeing with your config, the fonts component says so in the page source and wirekit:verify reports it.

  • <x-wirekit::tabs> can be driven from the server. The new active prop is the active tab as the server sees it, and unlike default it keeps arriving: a tab restored from a URL, a validation error whose field lives in another panel, or a permission that just changed can all move the tablist. Previously the only route was to hand-build the widget, which meant re-implementing its keyboard and ARIA behavior. Tabs

  • <x-wirekit::pricing-table> tells your application which interval was chosen. A hidden input carries the choice into a surrounding form, wire:model binds it, and the new interval prop lets the server set it — so a checkout decided server-side can both read the choice and correct it. Pricing table

  • <x-wirekit::stream> can render your markup instead of its own. The new output slot replaces the component's output block while keeping the state machine and the announcement contract, so a chat bubble or a syntax-highlighted pane can use it as a pure transport. The default slot is unchanged and still additive. Stream

  • <x-wirekit::status-tiles> can let a caption wrap. wrapMeta turns the one-line clamp on the meta line into wrapping text, for the case where the caption is the message rather than a two-word count. Off by default, because a wrapping caption grows the whole grid row. Status tiles

  • Seven words the icon vocabulary was missing. system names the third theme state beside sun and moon; code, key, rocket-launch, chart-line, folders and phone fill gaps that showed up as a name with no answer. key and rocket-launch were previously reachable only by stacking an extension preset, so they are now available on every icon set. Icon

  • Disclosure animations work outside Livewire. Four components ask for Alpine's collapse directive and nothing registered it, so in an Alpine-only application the region snapped open instead of animating and each element logged a warning. It is now registered by every full-catalog bundle, at a cost of roughly 600 gzipped bytes.

Changed

  • wirekit:csp-audit now checks more, and will fail builds it used to pass. Read this one before upgrading: nothing about your templates has changed, but the command was reporting a pass over two things it never looked at.

    It checked whether an expression parses under Alpine's grammar, and stopped there. Under a strict policy an identifier is resolved against the Alpine scope alone — there is no window fallback — so an expression can be flawless grammar and still throw on the first name it reads. JSON.parse('…'), which is what Laravel's Js::from() emits for anything but a scalar, is exactly that shape: it passed the audit and threw in the browser, leaving the element with an empty scope and every directive on it silently dead. It also scanned no wire: attributes at all, though Livewire hands most of their values to the same evaluator.

    Both are now covered. A pipeline that is green today can go red on upgrade — it was green about something that does not run.

  • wirekit:doctor:props and wirekit:doctor:a11y refuse to pass over nothing. A mistyped path produced "no issues found across 0 templates" and exit 0. Reading zero files is now a failure, because a linter that read nothing and reported clean is worse than no linter — you stop looking. Templates that exist but use no WireKit component are a different, honest result and still succeed, saying the count out loud.

Fixed

  • A wire:navigate no longer takes the whole page with it. Since 2.27.0 every overlay panel teleports into a landmark container built by the bundle. A navigation replaces the document body, the container left with it, and the framework treats a teleport whose target is missing as fatal — so initialization stopped at the first overlay on the arriving page and everything after it was never wired up. The result is the expensive kind of broken: the page renders completely, every control is visible, and not one of them does anything. There is no error a reader would see and nothing that looks wrong in a screenshot.

    The obvious repair does not work, and finding that out was most of the fix. The event that announces a completed navigation fires after the framework has already walked the new document, so rebuilding the container there restores it for the next overlay and leaves the failure exactly where it was. The container is now rebuilt in the window between the body swap and that walk. Covered by a case that navigates twice and asserts the arriving page is still interactive, not merely quiet.

  • A page-wide script failure could no longer start with a color. Several places assumed a document body was present before touching it — the overlay container, the chart adapters' theme probe, its color-variable resolver, the optimistic announcer, and two class-attribute observers. An uncaught error there does not cost the thing that failed; it ends the evaluation of the bundle, and everything registered after it stops existing. Each of these now yields a sensible default and stays quiet, and a check holds the whole class rather than the sites that were reported.

  • A menu that stayed dead after an update, in three more components. 2.28.0 fixed this for the dropdown. The same shape survived on the tooltip, the combobox and the multi-select: a teleported panel identified by an id that changes on every render is replaced rather than patched by a Livewire update, the replacement has lost its component scope, and the expression deciding whether it is open resolves against the browser's global object — where open is a real function. Calling it raises Illegal invocation from a stack that names nothing. All four panels now carry a stable key, and a check fails the build on a teleported panel that has an id and no key.

  • wirekit:doctor no longer reports a token asymmetry that is not there — and can now report one that is. The symmetry check located :root and .dark by searching for the text, so .dark also matched inside html.dark and .dark-mode. An application that writes its theme class on <html> — the usual arrangement for setting it before the first paint — was told all of its color tokens were missing from dark mode, and advised to duplicate declarations it already had. Worse in the other direction: the [@custom-variant](https://github.com/custom-variant) dark line the integration guide asks every application to write also matched, and made the two sides look identical, so a real gap could never be reported at all. The selector is now matched as a selector, a shared :root, .dark { … } head counts for both sides, and a rule nested in a layer or a media query is still found. The check also says when it skipped, instead of falling silent in a way that reads like a pass.

  • wirekit:csp-audit no longer crashes on an ordinary comment — and no longer misses expressions because of one. An apostrophe inside a Blade comment placed among a tag's attributes was read as the start of a quoted value. With an odd number the scan ran past the end of the file and the command failed with an internal error, before it could even tell you the CSP build was not installed. With an even number there was no error at all: the span between them was swallowed and every Alpine expression inside it went unreported — silence from a command whose entire purpose is finding failures that are silent. Both halves came from one hand-written scanner that a sister scanner had already been fixed for; there is now one, and all three places that read Blade tags use it. wirekit:show --validate-against was the third, and it was quietly wrong in its own way: it read words out of expression text as if they were attributes.

  • vendor:publish --tag=wirekit-lang hands over every catalog. It named English and German because those were the two that existed when it was written; five more shipped and it kept handing over two, while the documentation promised seven. Nothing failed — a publish list cannot notice a file it was never told about. It is now derived from what ships, so the next language needs no edit.

  • A blank reserved for a validation message is no longer selectable. The spacer that keeps a field from shoving its neighbors around, and the reserved message row on input, select and textarea, each render a non-breaking space that exists only to hold height. Dragging a selection across a form collected one of them per reserved field and carried them into the clipboard. They are marked unselectable now, which is the pointer half of the decision that already hid them from screen readers.

  • wirekit:csp-audit reads what the browser reads, not what Blade left behind. Server-side constructs are removed before the expression is parsed — a comment outright, an escaped or raw echo and [@js](https://github.com/js)(…) as a placeholder, with the parentheses of [@js](https://github.com/js)(…) balanced across strings so a payload containing ) does not truncate the rest. Before this, [@js](https://github.com/js)(…) — the pattern this very command recommends in its own advice — was reported as blocked with a syntax error at the @, and a raw echo carrying JSON was reported as a broken object literal. Neither is anything a reader could act on: by the time Alpine sees the attribute, the server text is gone.

    Two more corrections in the same area. x-teleport is no longer scanned at all: its value goes straight to document.querySelector(), so it is a CSS selector, and a selector beginning with # was being reported as an expression beginning with an operator. And a rejection now counts as a finding only when the parser saw what the browser sees — an expression the audit could not read is listed separately and does not fail the run, because "the tool could not check this" and "Alpine will refuse this" are different facts and only one of them is yours to fix. CLI reference

  • The setup guide listed five CSS features as safe to rely on that are not. [@starting-style](https://github.com/starting-style), field-sizing: content, text-wrap: balance, native CSS nesting and round() all require browsers newer than the supported floor, so an application taking the sentence at face value would ship something a reader on a supported browser does not get. They are now named for what they are — worth using behind a feature test — and where WireKit uses one itself, it already sits behind one. Every version in the correction was read from browser-compatibility data rather than recalled. Integration

  • Two field-composition recipes in the documentation did not do what they said. The pattern for putting a button beside a labeled field described a wrapper that produces a different vertical rhythm than the one the field itself uses, so following it left the control a hair off. Corrected, and pinned by a check so the page and the component cannot drift apart again. Field

  • A table that scrolls sideways shows its edge hint before you scroll. The hint is the only sign that more columns exist, and it was appearing only after the first scroll — after the moment it is for. Its two markers sat on the wrong axis. Table

  • wirekit:doctor:props no longer invents props that are not there. A Blade echo inside a quoted attribute value — the shape any translated attribute built from a variable takes — was read as more attributes, so a translation key surfaced as a prop the component never declared.

  • wirekit:verify recognizes every real form of the ApexCharts global assignment. window.ApexCharts ??= …, globalThis.ApexCharts = … and the bracket form were reported as missing, TypeScript entry points were not read at all, and a commented-out line counted. In the other direction, window.ApexCharts == null was read as an assignment — a feature detect passing as a completed setup.

  • wirekit:verify no longer recommends putting a chart library on every page. Its ApexCharts advice named only the global entry point, which is roughly 850 KB on pages without a chart. It now names the route-scoped alternative too, with the weight attached.

  • Six documentation pages named icons that do not exist on a default installation. They rendered a hole for anyone following them without an extension preset. Two of them wanted a telephone, which is why phone is now a word.

  • The design-token reference credited the wrong spacing ladder. The gap prop of the layout primitives resolves to the --space-wk-* scale, not --gap-wk-* — so overriding the latter to widen a grid changed nothing, and the page said to look there. Design tokens

  • A 2.27.0 note credited wirekit:verify with a check that lives elsewhere. Duplicate adapter registration is reported — by the adapter, as a browser console warning — so running the command and seeing nothing meant nothing. The entry now says where the report comes from.

v2.28.0

Minor — the dropdown defect 2.27.0 shipped with, and the tooling that could not see it.

Nothing about how you write a dropdown changes; the fix is entirely inside the component.

Fixed

  • One more deep link in the public API map pointed at nothing. The Alpine animation helper's entry aimed at while the page renders a heading that slugs to . Same shape as the eleven corrected in 2.27.0, one group over — which is why the check now reads every fragment the manifest publishes rather than one group of them.

  • A dropdown trigger announces a panel that is actually there — and writing that relationship no longer paints a menu nobody opened. Both halves were broken at once, and neither could be fixed alone.

    Since 2.27.0 the panel's aria-controls pointed at an element that is not in the document until the panel first opens, so a screen reader following the reference landed nowhere. The obvious repair — write the id — made a Livewire update replace the panel with a copy that had lost its Alpine scope, at which point the expression deciding whether the panel is visible resolved against the browser's global object instead. open is a real function there, a function is truthy, and a menu nobody had opened appeared on the page.

    The panel now carries a stable key of its own, so a Livewire update patches it in place rather than replacing it, and the id can be written safely. Both halves are covered by tests that were watched failing before the fix and passing after. Dropdown

v2.27.0

Minor — the whole triage, and what the tickets did not know.

Every addition here is opt-in: an unchanged call site renders exactly what it did in 2.26.1.

Added

  • <x-wirekit::step-marker> — the numbered chip a sequence of steps was being hand-rolled for. No existing primitive fits, and each near-miss misses differently: a badge is a pill, and a pill reads as a label about something — a status, a count, a tag attached to its neighbor. A step marker is not about the step, it is the step, which is what the square corners carry and why the shape is not a prop. It is filled always, and each intent's fill is paired with that intent's own foreground token in one place, because that pairing is precisely the part a call site has no way to get right: a number on a colored square is legible or it is not.

  • The three accessibility hooks can be renamed to match your application. data-reduce-motion, data-contrast and --font-scale-wk are defaults, not fixed names — motion_attribute, contrast_attribute and font_scale_property in config/wirekit.php set what WireKit reads. An application that already has its own preference attributes keeps them instead of carrying a second set. Theming

  • safeObserver, for the observer that keeps firing after the component is gone. An observer's callback runs once more after Livewire morphs the node away and Alpine tears the component down — into a this whose fields are already null. The page fills with Cannot read properties of null pointing at your plugin rather than at the teardown that happened, and your browser tests go red for a reason that is not yours. safeObserver gives you an observer whose callback simply does not run once you stop it, including for a batch the browser had already queued. It ships with the Composer package rather than from npm, so you import it by path — there is nothing to install. It does not replace destroy() — nothing can — it removes the guard you would otherwise write in every callback. Authoring custom Alpine plugins

  • A type-size preference, which completes the accessibility trio. Motion and contrast each got an override in this release; type size had none, so an application offering its own "larger text" setting had nothing to set. The browser zoom is not the same thing — it scales the whole page and reflows a dense interface into something harder to use. One custom property now moves the whole ramp: <html style="--font-scale-wk: 1.25">. The default is 1 and multiplying by one changes nothing, so an application that sets nothing renders exactly as before. Integration

  • <x-wirekit::field.spacer> — the other half of reserve-message. That one holds the space below a control so a field does not shove its neighbors down when validation fires; this one holds the space above, so a button beside a labeled field starts where the input starts instead of a label-height too high. Neither items-end nor items-start solves it — the first follows the field as it grows, the second lines up with the label. It renders the real label rather than a copy of its classes, so it stays exactly as tall when the typography changes. Field

  • php artisan wirekit:csp-audit — checks every Alpine expression in your own Blade views against Alpine's real CSP grammar. Under a policy without 'unsafe-eval' an expression outside that grammar is never evaluated: nothing throws, nothing logs, and the page looks correct while the control is dead — so there is no symptom to notice and the check has to be mechanical. The verdict comes from Alpine's own parser rather than a pattern list, because the grammar is wider than it looks and judging by eye over-reports badly. It exits non-zero when it found nothing to scan, too: an audit that measured nothing must not report a pass. CLI reference

  • optimisticArgs on every component that supports optimistic UI. The optimistic layer always spread args into the action call and no component exposed it — so the commonest optimistic surface there is, a list of identical controls with one per row, had no way to say which row it meant, and the only path was to hand-mount the factory and give up the component. Optimistic UI

  • start(payload) and setBody() on <x-wirekit::stream>. The fetch request body was read once at mount and could not be changed, so a stream could only ever send the payload it was born with — while the case this transport exists for rebuilds its body every run. startMessage / readyMessage / stoppedMessage / failedMessage are now props as well.

  • Seven locales ship, not two. Spanish, French, Italian, Dutch and Portuguese join English and German. Set your app's locale to any of them and every string WireKit renders on your behalf follows — no publishing step, no configuration. Authored rather than machine-translated, and checked against the reference catalog key by key: nothing missing, no dropped :placeholder, no broken plural form. See Localization.

  • An application can ask for increased contrast, or decline it. data-contrast="more" on <html> lifts the muted end of the palette — helper text, placeholders, borders, the values a reader with low vision loses first — and "no-preference" holds the default even when the OS asks for more. Without the attribute the OS decides, as before. This completes the set: motion, color scheme and contrast can now each be stated by the application rather than only by the system.

  • New icon aliases, so a page can name what it means instead of a glyph: envelope, user-add, user-remove, building, webhook, arrow-left, clock, lock, and a commerce set — store, cart, receipt, truck, package, barcode. Every alias resolves in all four icon sets. See Icon.

  • WireKit::isIconAlias() and WireKit::iconVocabulary() answer a question WireKit::icon() cannot: whether a name is one WireKit declares, rather than one that merely renders. icon() always returns something — an unknown name falls through to the icon set's own naming — so it tells you what will appear, never whether the name was recognized. Useful for editor completion and for checking a design system's names against the ones that exist.

  • error (and hint) on slider, range-slider, rating and segmented-control. All four took a label and none took an error, so a validation message passed to them was silently dropped into the attribute bag. They now render it, announce it politely, and wire aria-invalid + aria-describedby to the element the message is about.

  • wirekit:doctor:props finds a misspelled prop before a page renders. An unknown prop is not an error at render time — Blade passes it through and the component uses its default — so <x-wirekit::button intnet="danger"> renders a perfectly good button in the wrong intent. The linter is static, so it covers templates nobody has opened yet.

  • variant="flush" on table.head drops the header fill and keeps the divider and the sticky behavior. Previously the fill could only be removed by overriding the whole class string, which discarded both.

  • zoneInset on sidebar stops the head and foot zones from insetting content that already insets itself — a sidebar.item in a zone was indented twice.

  • name on dropdown pins the panel id, so it survives a Livewire round trip verbatim.

Fixed

  • A segmented-control inside a form submitted nothing while the page showed a choice. Its hidden input was filled from JavaScript, so between the server render and Alpine booting the field was empty with the selected segment highlighted right beside it — and a submit in that window sent no value at all. Under a Content-Security-Policy that blocks Alpine's evaluator, that window is the entire session: nothing 404s, nothing throws, and the form is silently empty every time. The field now carries its value from the server render, read from the same expression that seeds the client state so the two cannot disagree.

  • The command-palette reopened showing the previous search. Closing and reopening cleared the visible input but told the host nothing, so a server-driven palette kept the last query alive: type "strategy", close, reopen, and the field is empty while the results still answer a word the reader can no longer see. The empty query is now announced at the moment the empty field appears. Its results list also takes a height override like every other part of the panel — the 288px it was fixed at is a reasonable default and a poor ceiling for ranked results on a large screen.

  • Opening a WireKit asset URL directly no longer garbles its text. The nine dist routes did not declare their character set, so a browser hitting /wirekit/wirekit.css on its own fell back to a legacy single-byte encoding and every em-dash rendered as mojibake. Loaded from a page the file inherits the document's encoding, which is why this was invisible in every application and visible only on the direct hit.

  • wirekit:verify now checks the ApexCharts adapter wherever it is switched on. The check only ran when charts.library was apexcharts, while scripts.apex emits the adapter independently — so the configuration that ships the adapter with the library set elsewhere went entirely unexamined, and every chart stayed blank while the installation was reported healthy. It also asks the harder question now: finding the package in package.json proves it can be resolved, not that it reaches the page. Registering it twice is reported too.

  • Eleven CLI deep links in the public API map pointed at nothing, and one command in it was never part of WireKit. The anchors were built from the command id rather than from the heading the page renders, so wirekit:show pointed at #wirekitshow while the page offers #wirekitshow-name — and a fragment that matches no heading does not fail: the page loads and simply does not scroll. Separately, the export kept every command whose name began with wirekit:, which is a naming choice rather than a proof of ownership, so a command belonging to the host application could appear in a manifest describing this package. Ownership is now decided by the class.

  • The public API map listed every recipe twice, and its blueprints group held no blueprints. The group scanned its directory recursively and swept up the pages belonging to two other groups, so a tool reading the manifest saw duplicate entries under two different names and a group whose contents did not match what it is called. Every page now appears under exactly one group.

  • Picking a date range with the keyboard now shows the same preview a pointer gets. Moving through the grid after choosing one end left the shading — and the aria-selected that says "commit here and this is your range" — untouched, because the preview was wired to hover alone. There is no second way to learn what a range will be before making it, so for anyone not using a pointer the feature was not harder to reach, it was absent. Calendar

  • A dropdown inside a Livewire component no longer throws on every round trip. The panel read its id out of the Alpine scope, which is right across the teleport and wrong across a morph — so every re-render raised panelId is not defined. That is worth more than console noise: a JavaScript error during a morph ends evaluation at that point, so whatever the same pass would have done next silently does not happen, and nothing turns red. The id is written from the component now instead of bound, leaving no scope-dependent expression on the teleported node. Reported against 2.25 and 2.26 alike, so it was not new — only unnamed. Dropdown

  • Streaming over fetch now works for an endpoint that sends more than one kind of frame. The reader kept only data: lines and dropped the event: name, so a stream of named events — which is how Laravel's own SSE helper frames them — arrived as one run-on string with a refusal indistinguishable from a token, and not one error anywhere. Name the frame that carries text with event-name, and every other named frame is dispatched as a wirekit-stream-event on the element (with its JSON payload decoded) instead of being pasted into the output. A stream that names nothing is unaffected. See Named events.

  • A failed stream no longer discards what had already arrived. stop() and completion both revealed the buffered text under prefers-reduced-motion; the failure path did not — so a response that died at 90% showed the reader nothing at all. The one terminal state where a partial answer is worth most, and the reader affected was the one who had asked for less motion.

  • Stream failures can be announced in the application's language. The failure announcement and all five reasons behind it were English literals in the JavaScript with no way through __() — so an interface translated everywhere else announced its failures in English, in the moment the reader most needs to understand what happened. All six are now in the shipped catalogs (seven languages) and overridable via failed-message.

  • A date range now previews backwards. Dragging toward an earlier day showed nothing at all until the click, while dragging forward shaded as expected. The gesture was always accepted — only the preview refused to show it.

  • calendar's range caps follow the ordered ends, so the rounding no longer lands on the wrong day while the pointer sits before the start.

  • Ten numeric props stopped discarding a deliberate 0. A toast asked not to auto-dismiss vanished after five seconds; a dropdown or popover asked to sit flush against its trigger sat eight pixels away. Also carousel's interval, hover-card's delays and offset, and reading-meta's two reading speeds.

  • Nine labels the catalog already knew were rendered in English regardless of locale — the data table's screen-reader column header, the event calendar's view switcher, the status matrix legend, two calendar labels, the combobox empty state, and the alert dialog's cancel.

  • An upgrade's new assets are served instead of the old published copy. Staleness was decided by which file was written later, and a vendor:publish from the previous version can easily carry the later timestamp — so the old bytes shipped, with a fresh-looking cache-buster.

  • A dropdown inside a Livewire component survives a round trip. Its panel teleports out of the morphed subtree while the id was regenerated per render, so the trigger and the panel stopped naming each other.

  • wirekit:verify no longer calls a correct setting invalid. scripts.bundle = 'csp' — the one correct choice under a strict Content-Security-Policy — was reported as an invalid value with advice to undo it.

  • wirekit:doctor's stale-views advice is no longer a loop. Running the suggested view:clear produced exactly the state the warning reported.

  • wirekit:doctor's typo scan can go green again. It read the whole log history, so a typo fixed weeks ago kept the warning yellow for the life of the file. It now looks at a configurable window, 24h by default — WIREKIT_DOCTOR_SCAN_LOGS_WINDOW_HOURS sets it.

  • Every teleported overlay panel now lives inside a landmark. Dropdowns, tooltips, popovers, comboboxes and menus lift their panel out of the document flow so an overflow: hidden ancestor cannot clip it — which also placed them outside every region on the page. An accessibility audit reported it on every one, and a developer could not fix it, because the markup is WireKit's. They now teleport into a named region WireKit creates.

  • A responsive table shows its horizontal scroll. On a phone there is no scrollbar until you are already dragging, so a cut-off column looked like the end of the table.

Documentation

  • A recipe for numbering a feature list. Replacing the auto-generated icon chip with a numbered marker via iconSlot is the composition the new step marker exists for, and it is now written down rather than left as something to work out from two component pages. Feature with numbered marker

  • The one-time-code page no longer reads as a rule about one-time codes. Every preview on it used a trimmed character set, and the section above them explained why a recovery code drops I and O — so the page looked like a statement that the component removes ambiguous characters. It does not: alphabet is the developer's declaration, and it accepts lowercase, symbols and non-ASCII alike. A second preview now shows a permissive set beside the conventional one. Two previews, two audiences: one shows the convention, the other shows the range. OTP input

  • A named Content-Security-Policy route for a Livewire app. The page sent an app with a strict policy toward the self-contained CSP bundle and warned in the same breath that a Livewire app must not load it — leaving the one stack WireKit is built for with no route at all. The route is simpler than either warning suggests: the default bundle carries no evaluator of its own, so switching Livewire to its own CSP distribution is enough and WireKit needs no change. Also documents that a Livewire method named like a JavaScript keyword needs index access. Integration

  • grid explains that template is not responsive and that a strict style-src can drop it entirely — where other components lose a shade, the grid loses its whole column definition. Pass cols alongside to declare the fallback.

  • app-shell names the coupling that silently disables a sticky header: headerPlacement="content" puts the header inside a column that never scrolls, and nothing sticks to a container that does not move.

  • inline-edit already carried its click-away note from 2.26.1; the reduced-motion attribute is now documented as fixed rather than configurable, because setting it never had any effect.

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-inset — false 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 xs…2xl 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.
  • Input — inputmode 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 1–6 (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 preset — settings, 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 heatFrom → heatTo (a cold→hot color interpolation), and each heat value rides in a contrast-checked chip so it stays readable at any point on the ramp.

  • New <x-wirekit::notification-center> — 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 preset — home, 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-up → slide-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-page → MarketingLandingPage). 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 (wireStream → wire-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 intnet → intent 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 types — lower-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 preset — live (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 label → body. 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 defaults — config/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 refresh — chart.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 ARIA — aria-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 push — git 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 bundles — grid.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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo