Skip to main content

WireKit v2.2.0

Minor release

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/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 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 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) 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 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([...]) 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 "..." annotations in @props blocks surface as a new examples field in the schema. PropsParser now reads // @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 annotations 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: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 flavor 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 property). The component normalizes 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 behavior 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. 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 synthesizing 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, behavior 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 behavior (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 behavior 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 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.
  • <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). Behavior-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($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 honored 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-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 behavior 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 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) 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 behavior (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 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 / @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 '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.