WireKit 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, ClassPropsExtractor for class-based components), two new artisan commands (wirekit:fonts, wirekit:icons), and three new docs pages (prop-naming-conventions, overlays/events, recipes/).
Added
<x-wirekit::first-classrange-slider> wire:modelintegration. Pre-fixwire:model="priceon the component tag was silently dropped into the outerRange" <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 anywire:model*directive on the tag, strips it from the outer bag (no double-render), and re-emits it on the two hidden inputs aswire:model="propName./min" wire:model="propName.. Developer declaresmax" public array $priceRange = ['min' => 20, 'max' => 80];; dragging either handle updates$price/Range['min'] $pricelive. All modifiers (Range['max'] .live,.lazy,.debounce.500ms,.blur) flow through unchanged.docs/"Livewire Integration" section rewritten to show the array-property pattern.components/ range-slider. md docs/— reference page for the three semantic-modifier prop families (extending/ prop-naming-conventions. md intent/variant/tone). Documents the contract behind each name, the canonical 7-valueintentenum (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.— canonical recipe for the right-edge sticky sidebar Table-of-Contents pattern. Pairswirekit. app/ recipes/ on-page-toc <x-wirekit::andreading-spine> <x-wirekit::to document the in-page TOC shape common to modern documentation sites. Covers the sidebar shape end-to-end (canonical wiring, CSS-variable knobs for expanded width and sticky offset, integration with a stickyreading-progress> <x-wirekit::, breadcrumb composition); cross-links to thebrand-bar> marketing-landing-tocrecipe for the horizontal strip variant. Targets the<main>selector by default soH2/H3headings auto-populate the spine without manual wiring.docs/— 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, Alpineoverlays/ events. md $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 readingresources/. Registered under a new "Overlays" group in the docs navigation.js/ components/*.js <x-wirekit::sub-component — pre-wired close button for alert-dialogs. Analogous toalert-dialog. cancel> <x-wirekit::, but specific to the destructive-action confirmation dialog. The wrappingmodal. close> <div>carriesx-on:click="close()"(the parent'swirekitAlpine x-data exposes the method); slot defaults to aAlert Dialog <x-wirekit::so the typical case is a one-tag affair. Override the label by passing slot text (button intent="neutral" surface="filled">Cancel</x-wirekit:: button> <x-wirekit::) or wrap your ownalert-dialog. cancel>Back</x-wirekit:: alert-dialog. cancel> <x-wirekit::for full control (custom variant / size / icon). Replaces the manual-wiring pattern (button> 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. Eachrecipe:template generates a Livewire class + Blade view from a shipped stub mirroring the correspondingdocs.page's structural composition. Class names derive PascalCase from the kebab-case recipe slug (wirekit. app/ recipes/<name> marketing-landing-page→Marketing). Available recipes:Landing Page 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 todocs.for the full reference. Unknown recipes fail with a Levenshtein-ranked Did-you-mean hint.wirekit. app/ recipes/<name> slotsschema field now flags required slots. Slot entries inwirekit:export-json/.wirekit-schema./json wirekit:showwere previously a flat list of names — they didn't tell developers (or AI tooling) which slots were OPTIONAL (guarded by@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($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 withUndefined variable $triggerwhen developers omit the slot; the manifest now reportstriggerasrequired: true.Pushery\— public Reflection-based prop extractor for class-based Blade components. Companion toWireKit\ Support\ Class Props Extractor PropsParser. Class-based components like<x-wirekit-chart>(registered viaload) don't have anView Components As(...) @props([...])block — the prop surface IS the constructor signature.Classwalks the constructor parameters via Reflection and returns the same shapeProps Extractor:: extract(string $className) PropsParseremits, 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.Componentnow returns 11 props (was empty). The schema entries fromRegistry:: extract Props('chart') wirekit:export-json/.wirekit-schema./json wirekit:show chartall carry the full constructor surface — AI tooling and IDE extensions see chart'stype/labels/datasets/wire-stream/ etc. instead of the previous emptyprops: [].@example "..."annotations in@propsblocks surface as a newexamplesfield in the schema.PropsParsernow reads// @example "value"annotations from the trailing same-line comment after a prop's comma and exposes them as alist<string>field on every schema entry next toname/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 default1doesn't telegraph that. Multiple@exampleannotations per comment are all captured; backslash-escaped quotes inside the value are supported (@example "Press \\"Enter\\" to confirm"). Empty array (not null) when no annotation present, so the field is type-stable across the catalog.wirekit:iconsartisan 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 currentwirekit./icons. preset wirekit.config, and every alias → Blade-Icon identifier mapping. Same API shape asicons. presets wirekit:list/wirekit:fonts(--preset=...,--as=count|presets|aliases|json,--format=alias, Levenshtein-ranked Did-you-mean on unknown preset). The--as=aliasesflavor emits a unique alphabetised alias list across the (optionally filtered) preset set — useful for "do any presets definebolt?" 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-existingIconResolver::PHP-side helper is unchanged; the CLI is a thin discovery wrapper. Also: theavailable Presets() boltexample indocs/'s Sizing section is replaced withcomponents/ icon. md search(which is in the defaultheroiconspreset) —boltlives only inheroicons-marketingand required opt-in to render.wirekit:fontsartisan command — list every font preset shipped with WireKit, grouped by category. Mirrorswirekit: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 bywirekit:install --font={key}. Previously the only way to discover the preset list was viatinkeror 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_componentsarray on every entry inwirekit:export-json+.wirekit-schema.. Previously onlyjson wirekit:show <name>surfaced the sibling-directory.blade.phpfiles (e.g.card.body,card.header,card.footer). The JSON-manifest paths read by AI tooling, IDE extensions, and the.wirekit-schema.written byjson wirekit:installnow carry the same list — first-class field next topropsandslots. Sorted alphabetically; skipsindex.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::acceptstabs> itemsas 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@computedproperty). The component normalizes both at the template edge — pick whichever your data source already produces. Both shapes produce byte-identical rendered HTML (samearia-controls/idwiring, same Alpine bindings) so screen-reader behavior is stable across the two. When thelabelfield is missing in array-of-objects form, thekeyis used as the visible label.<x-wirekit::HTML5 form-state props ininput> @props. Five props now declared explicitly:required,disabled,readonly,autocomplete,placeholder. Previously these only worked via attribute-bag passthrough (<x-wirekit::) — invisible to the schema, AI tooling, and IDE autocomplete. Theinput required> requiredprop additionally flows through to the associated<x-wirekit::so its required indicator (label> *) 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 singlerequiredtoken on the underlying<input>— guarded by an explicit regression test.<x-wirekit::profile> interactiveprop for dropdown-trigger composition. Profile previously rendered a presentational<div>with no focusable child. Wrapping it in<x-wirekit::(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. Newdropdown. trigger> interactiveprop (defaultfalse) emitsrole="button",tabindex="0", Enter / Space keyboard handlers synthesizing a click, plus a focus-visible ring matching the canonical button focus state. Defaultinteractive=falsepreserves 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+mobileprops for responsive logo swap. A wide wordmark (Breakpoint /brand/) on a brand-bar overflows narrow viewports —wirekit-wordmark. svg mobileLogopairs with a compact mark (/brand/) that renders below the chosen Tailwind breakpoint, with the wide wordmark taking over at + breakpoint.wirekit-mark. svg mobileacceptsBreakpoint sm/md/lg/xl, defaultsm(640 px). WhenmobileLogois null, behavior is unchanged (single<img>at every viewport — back-compat). Both images carry the same accessibility shape (alt=""+aria-hidden="true"); the<a>'saria-labelprovides the accessible name. Invalidmobilevalues throw in debug, fall back toBreakpoint smin production viaStrictnessGate. 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\— public single source of truth for runtime validation strictness. Central decision behindWireKit\ Support\ StrictnessGate WireKit::(component-level prop validation) andvalidateProp() IconResolver(icon-alias / preset lookups): strict mode throwsInvalidwith 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 newArgument Exception fallbackparameter).StrictnessGate::returns the current decision;isStrict() StrictnessGate::is the canonical entry point;enforce($context, $key, $value, $allowed, fallback: null) StrictnessGate::exposes the message-builder so callers that throw their own typed exception (likeformatMessage(...) IconResolver) inherit identical wording.wirekit.config key (envvalidation. strict WIREKIT_) — explicit override of the strict-vs-lenient decision.STRICT_ VALIDATION null(default) keeps the historical behavior (strict inAPP_DEBUG=true, lenient in prod).trueforces strict everywhere — useful for CI / staging hardening.falseforces lenient everywhere — useful for snapshot CI runs that want to assert rendered output regardless of prop typos. Documented inconfig/with a per-value behavior matrix.wirekit. php
Changed
- Integration Guide URL moved from
/integrationto/getting-started/. 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 acrossintegration getting-started.,md cli-reference.,md livewire-starter-kit.,md components/,resizable. md extending/,console-output-baseline. md cli-reference/). External bookmarks pointing at the legacywirekit-doctor. md /integrationURL will need updating; the new URL is the canonical location going forward. wirekit:doctornow WARNs when'charts.is configured butlibrary' => 'chartjs' resources/is missing thejs/ app. js Chart.registration. Catches the #1 first-run Chart.js gotcha upstream:register(...registerables) npm install chart.js+'chartjs'in config IS NOT enough — the JS-sideimport { Chart, registerables } from 'chart.js'; Chart.must be inregister(...registerables); app.jsor 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.hint comment doesn't false-pass the detection.register(...) getting-started.Optional Features section gains an explicit three-step callout (config + npm + JS-side registration) cross-linked tomd /integration#optional-dependencies.<x-wirekit::warns in debug mode when plain HTML descendants (table> <thead>/<tbody>/<tr>/<th>/<td>) are passed inside the slot instead of the<x-wirekit::sub-components. Same composition-required-for-padding pattern astable.*> <x-wirekit::: the barecard> <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 adata-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::warns in debug mode whentabs> 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 privatex-data="{ active: '...' }"scope and does NOT update Livewire state. The console.warn now surfaces the silent-breakage with a link todocs.for the named-slot contract. Production stays silent (no console.warn). The component's behavior 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.wirekit. app/ components/ tabs <x-wirekit::ESC key always closes, even whenalert-dialog> 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:createOverlaygained anescapeoption; alert-dialog passesAlways Closes true; focus-trap usesescape. JS bundles rebuilt.Deactivates: dismissible || escape Always Closes <x-wirekit::removed from the README components table and thereplay-button> wk-replay-buttonrow in the Public CSS API catalog no longer carries a docs link. The component remains fully callable in the package (the<x-wirekit::tag still renders, thereplay-button> wk-replay-buttonCSS class still ships indist/) — replay-button is preview-chrome infrastructure rather than a developer-pickable primitive, so it's no longer surfaced in the catalog tables.wirekit. css IconResolverexception messages now include Did-you-mean hints on close-typo alias AND preset names. PreviouslyUnknown icon alias 'clse'listed only the available aliases (long list); now readsUnknown icon alias 'clse'. Did you mean: close, eye, plus? Available aliases: …— same Levenshtein-ranked suggestion contract asWireKit::and everyvalidateProp() wirekit:*CLI surface. Same enhancement forUnknown icon preset 'heroicns'(suggestsheroicons). Behavior-preserving — theInvalidclass and the leading message prefix are unchanged so existing exception-catching tests stay green.Argument Exception WireKit::lenient-mode log message now annotates the fallback value. The pre-existing log line readvalidateProp() 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 fromStrictnessGate::.enforce() wirekit:show chart/wirekit:list --format=json/wirekit:export-json/wirekit:export-api-mapnow emit the class-based Blade tag form<x-wirekit-chart>(not the broken anonymous form<x-wirekit::). The chart component is registered class-based viachart> loadso the working tag is the prefixed-class shape; the anonymous Blade file atView Components As(...) resources/exists for the class-based path to render through but reaching it viaviews/ components/ chart. blade. php <x-wirekit::hits an undefinedchart> $alpinevariable and 500s. NewComponent Componenthelper is the single source of truth, with a small per-component override map (currently onlyRegistry:: tag(string $name): string 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 list now visibly separates from the dropzone above it. Pre-fix thefile-upload> <ul>carried an inlinestyle="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 withmt-[var(--padding-wk-y-sm)](the intended dropzone gap). Inlinemargin: 0wins 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 bygap-[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 loaddist/, so the CSS variable resolves identically.wirekit. css <x-wirekit::reveal> delayprop silently dropped whenever the caller also passed an inlinestyle="…"attribute. The template emitted TWOstyle=attributes on the root<div>— one from$attributes->merge(...)carrying the caller-passed value, and one from a separate@if($delaydirective carryingValue) style="..." animation-delay: {value}. Per HTML5 parsing rules, duplicate attributes on the same element resolve to the FIRST and silently drop the second, so theanimation-delaywas never honored whenever any caller-supplied inline style was present. Every gallery preview ondocs/passesanimations. md 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-block merge before the<div>, so the root now carries exactly onestyle="caller-styles; animation-delay: …"attribute and the delay actually takes effect.- Every
<x-wirekit::andreveal> 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::) no longer flashes when the element is already in the viewport at page-load. Pre-fix thefooter animateIn> animation-fill-mode: bothon[class*='wk-animate-']includedbackwards, which snapped the element to its keyframefromstate (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 fromopacity: 1(no class) toopacity: 0(from-state) before animating back. User perceived a brief "flash to invisible → 300 ms fade-in" — the flicker. New CSS rule indist/pre-hides any element withwirekit. css x-data="wirekitand noAnimate('*-in')" 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 keyframefromopacity 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::link horizontal padding now follows the design-token grid (reading-toc> --padding-wk-x-sm, 10px) instead of the hardcodedpx-2(8px) Tailwind atom. The link'spx-2 py-1declaration 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". Nowpx-[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::stepper buttons now snap to the step grid instead of skipping values when the starting value is off-grid. Pre-fixnumber-input> value=1.78 step=0.1clicked+jumped to1.9(skipping1.8) because the Alpine handler addedstepto the current value (1.78 + 0.1 = 1.88) then rantowhich roundedFixed(precision) 1.88 → 1.9. The intermediate grid point1.8was unreachable. The fix snaps to the step grid anchored atmin(falling back to0when nominis 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. A1e-10tolerance absorbs binary-float drift so on-grid values like1.8(which can be stored as1.7999999998) still advance to the correct next step. Anti-regression testit('emits the precision/round helpers + grid-snap stepper wiring in Alpine x-data')pins the new shape.<x-wirekit::file-row layout now pushes size + remove button to the right edge with proper horizontal padding. Pre-fix the filename, size, andfile-upload> ×remove button clustered on the LEFT of each row leaving a large empty area on the right, AND the row's all-sidesp-[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 carriesflex-1so it grows to fill available space (pushing the size + X to the right edge in the standard file-uploader UX shape), and padding split intopx-[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::race condition that left random counters stuck at "0" on hard refresh. When bothstat animate animateIn="…"> animateandanimateInwere set, the Blade template wrapped the inner counter root in an outer<div x-data="wirekitcarrying the entrance keyframe (e.g.Animate('…')"> wk-slide-up-in:translate→Y(1rem) translateY(0)). The entrance keyframe shifted the inner element's geometry for ~300 ms. The inner counter's ownIntersection(Observer 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 foranimationendon the outer rather than running its ownIntersection— the outer'sObserver wirekitAnimateplugin 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 behavior unchanged. The counter-tick logic was factored into a sharedrunCounterclosure so both paths call the identical ease-out-cubic-over-1.2 s tick.<x-wirekit::tour. step> indexprop now auto-assigns sequentially within its parent<x-wirekit::. Pre-fix every step defaulted totour> index=0, so every step rendered withdata-wk-tour-step="0". The tour'sstart()correctly counted 3 steps,next()incrementedcurrentStepto 1, then_positionStep()queried[data-wk-tour-step="1"]— found nothing → all step divs hidden because none matchcurrentStep === 0anymore. Tour appeared to "break" after the first Next click — actually all steps just disappeared. Now:indexdefaults tonull; when null, the step picks the next integer from a per-render counter (Pushery\) that the parent tour component resets at the top of itsWireKit\ Support\ TourStepCounter @phpblock. Steps with explicit:index="N"bypass the counter (the developer's choice; the counter only advances onnext()calls). Multiple tours on the same page each restart numbering from 0 — Alpine scopes isolate per-tourcurrentStep. The natural usage pattern — just list the steps, no:indexprops — now works correctly.<x-wirekit::value-display spans referenced undefined Alpine variables. The tworange-slider> <span x-text="min/Val"> <span x-text="maxelements rendered AFTER theVal"> </div>that closed the x-data scope — every page using the slider flooded the browser console withAlpine 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) 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 bothx-textsubstrings appear before the matching close — catches future structural regressions.<x-wirekit::threwbadge intent="accent"> Invalid intent "accent"in debug mode (broke live previews ondocs.+wirekit. app/ recipes/ marketing-landing-page docs.after v2.1.x).wirekit. app/ components/ hero accentwas missing from the badge'svalidatePropallowed list — a regression vs the canonical 7-value intent enum documented indocs/. Now restored as a first-class intent with a high-contrast filled rendering (extending/ prop-naming-conventions. md 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::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).badge intent="X"> - Animation-emitting components now uniformly emit
data-replayable="true"when theiranimateInprop is set —<x-wirekit::/card> <x-wirekit::/feature> <x-wirekit::/hero> <x-wirekit::/cta> <x-wirekit::previously emitted thefooter> x-data="wirekithook without the replay marker, soAnimate(...)" docs.'s preview-chrome replay button missed them. Existing emitters (wirekit. app <x-wirekit::,reveal> <x-wirekit::,stat animate> <x-wirekit::) are unchanged. The attribute is otherwise inert; on developer apps without a replay surface it's a harmless no-op. Newchart> docs/subsection documents the same contract for utility-class animations (rawanimations. md class="wk-animate-fade-in"on HTML elements — developer addsdata-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 theanimateInprop is set, and does NOT carry it when inactive. <x-wirekit::aside column overflowed the page onhero> <lgviewports when fed wide unbreakable content. The most common shape: developer drops a<x-wirekit::in thecode-block> <x-slot:aside>;<pre>'swhite-space: prekeeps 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 carrymin-w-0 w-full lg:w-autoon the flex-col / flex-row break:min-w-0escapes flex-shrink's defaultmin-contentfloor,w-full lg:w-autotakes parent's full inline-size on flex-col then reverts to content-sized onlg:flex-row. The code-block's ownoverflow-x-autoon its<pre>then handles intra-line horizontal scrolling within the contained column. Back-compat: existing layouts atlg+keep the sameflex-rowcontent-sized columns.<x-wirekit::without abutton loading> wire:*action attribute was a visual no-op. The component emittedwire:loadingon the spinner SVG +wire:loading.on the button — both Livewire directives that only activate WHILE a Livewire request is in flight. Soattr="disabled" <x-wirekit::on a non-Livewire page (or a button with nobutton :loading="true"> wire:click/wire:submit) rendered an invisible spinner and a clickable button. The component now distinguishes two intents: (a)loading=true+ anywire:*action present → unchanged behavior (spinner + disabled while the Livewire request runs), (b)loading=true+ NOwire:*action present → DECLARATIVE loading state, spinner always visible + nativedisabledattribute 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::) silently rendered empty when the developer-suppliedreading-progress> targetselector 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 singleconsole.warnat init time whentargetis non-default ANDdocument.returns null, naming the offending selector and the component tag form (query Selector(target) Check the selector on <x-wirekit::). The defaultreading-spine target="…" /> '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 defaulttargetis null (track viewport), so any supplied selector triggers the check. <x-wirekit-chart>(or<x-wirekit::/chart-mixed> <x-wirekit::) crashed the whole page on a fresh install with achart-spark> Runtimebecause the package shipsException: Charts are disabled. Set 'charts. library' in config/ wirekit. php … 'charts' => ['library' => null]as the safe default (avoids forcing every developer into Chart.js installation). InAPP_DEBUG=truethe chart now renders a developer-visible placeholder div instead — names the missing config step inline (Set 'charts.) and preserves the developer-suppliedlibrary' => 'chartjs' in config/ wirekit. php, then npm install chart.js heightso 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. Newwirekit:doctorcheckchecksurfaces the same diagnostic at install time: WARN when any Blade file underChart Usage Without Adapter() resources/references the chart tag andviews/ config('wirekit.is null. Both diagnostics ignore Blade-comment references (charts. library') {{-- <x-wirekit-chart> --}}) so docs pages don't false-positive.wirekit:show <name>printedDocs: docs/— butcomponents/<name>.md /docsis export-ignored from the published package, so the path never resolved on a developer install. The line now printsDocs: https://docs.(the live URL), matching whatwirekit. app/ components/<name> wirekit:export-json'sdocs_urlfield already emitted everywhere else.<x-wirekit::silently dropped six props that the underlying class-based chart delegate accepts:chart-mixed> wireStream,wireStreamMode,wireStreamCap,annotations,inline,replayable. The anonymous-Blade wrapper only declaredlabels/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 tochart-mixed's@propsand forwarded via explicit:wireetc. Existing callers pass through unchanged; mixed charts withStream="$wire Stream" wire:streambindings or annotations now work as documented.wirekit:doctorwarned 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 suggestsvendor:publish --tag=wirekit-fontsfor 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:doctorprinted two FAIL lines for missing@wirekitStyles/@wirekitScriptsdirectives 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 (noresources/, noviews/ components/ layouts/*.blade. php resources/, noviews/ layouts/*.blade. php resources/, AND noviews/ components/ layout. blade. php @import 'wirekit.css'inresources/) now emits a single INFO line —css/ app. css 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@importalternative is configured, the scan runs normally and FAILs on missing directives in real misconfiguration scenarios.