WireKit 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/) + content-edge spine contract (/extending/) ship as the canonical AI-tooling discovery surfaces for wk-* classes and layout participation. New <x-wirekit:: helper wrapper + WireKit:: 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/ shows the full composition, and a new explainer at /extending/ 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:: / <x-wirekit:: runtime prop validation reuses the same suggestion contract. Two behavior 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 behavior. Every other change is additive.
Added
- Comprehensive mobile-viewport browser-test coverage across every category where viewport behavior matters. Builds on the marketing-primitive mobile coverage with Tier-1 PR-gate
Tier1Mobile(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 genericGate Test. php Componentthat scans everyPreview Mobile Sweep Test docs/components/** :::previewblock at iPhone 14 Pro viewport. New sharedAssertstrait centralizes theMobile Viewport matchMediaoverride + console-error capture + scroll-settle scaffold so per-test boilerplate stays minimal. docs/— canonical Public CSS API catalog. Single source of truth for everyextending/ public-css-api. md wk-*class WireKit emits to developers — viadist/literal selectors AND via Blade-template static-string emissions. ~40 catalogd classes across four groups (Layout / chrome markers, Reading family, Animation / motion, Display / loading), each with a stability tier (Stable / Provisional / Internal-with-exception) and customization contract. The catalog enforces lockstep both directions — every shipped class has a row, every row references a real shipped class. Surfaces as machine-readable JSON viawirekit. css wirekit:export-api-map's newcss-classesgroup for AI tooling / IDE-extension consumption.docs/— content-edge spine participation contract. Documents the per-component inline-padding spine — which components joinextending/ spine-contract. md --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. Every documented participant carries a// wirekit:spine-participantmarker comment in source so adding a new spine-aware component without the marker fails the build upstream.<x-wirekit::— NEW opt-in spine wrapper for developer-authored components. Pass any slot content; the outerspine-aware> <div>reads--padding-wk-x-lgso the content aligns with brand-bar / main / footer / etc. on the same vertical content edge. Thetierprop (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 newWireKit::static) so a future spine-padding refactor propagates automatically.spinePadding() Pushery\— NEW public helper. Returns the canonical Tailwind utility string for the named padding tier (WireKit\ WireKit:: spine Padding(string $tier = 'lg'): string 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 behavior unchanged — running without--tierwalks every check. Useful for interactive dev sessions where the developer cares about only one tier's signal.wirekit:doctorcompiled-views-freshness check. Detects whenresources/mtimes exceedviews/ storage/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 actionableframework/ views/ php artisan view:clearfix, short-circuits the diagnostic chain to the real cause. Honors the 60-second buffer to avoid false-positives on fast file-edit cycles. Categorized under the newenvironmenttier.wirekit:export-api-mapnewcss-classesgroup. Machine-readable enumeration of every publicwk-*class — same inventory asdocs/table, one entry per class withextending/ public-css-api. md tier+docs_url. AI tooling consumes viaphp artisan wirekit:export-api-map --pretty | jq '.groups[] | select(.id == "css-classes")'for autocomplete / type-checker awareness.--wk-stagger-steptoken default in:root {}. The CSS variable now ships with a75msdefault indist/(previously developers needed to set it via the prop OR risk a silentwirekit. css 0mscascade). Documented indocs/theming.mdunder the new "Motion tokens" subsection alongside the existing--motion-wk-*token family.Marketingcategory inPushery\. Four conversion-focused primitives (WireKit\ Component Registry cta,feature,feature-grid,hero) migrate from categoryDisplayto categoryMarketing.footerstaysLayout(structural primitive),brand-barstaysNavigation(header chrome),revealstaysDisplay(generic animation).wirekit:list --category=Marketingnow exits 0 and prints the four primitives; the canonical category enum becomes eight values (Form,Layout,Typography,Navigation,Overlay,Display,Marketing,System). See/extending/for the per-category definition + usage criteria.component-registry#categories Pushery\— public single source of truth for every theme preset.WireKit\ Theming\ Theme Preset Registry keys()/get(string $key)/all()/isValid(string $key)/iscover read access;Default(string $key) register(string $key, array $preset)lets downstream packages add custom presets at runtime from a service-provider boot hook.wirekit:theme,wirekit:install --preset=, andwirekit:export-api-map themesGroup()all read from this class — drift between their lists is impossible.Pushery\— public Levenshtein helper underpinning every CLI "Did you mean?" hint AND the runtimeWireKit\ Support\ SuggestSimilar WireKit::suggestion line.validateProp() byreturns up to N ranked candidates;Levenshtein(string $needle, array $haystack, int $max = 3, int $maxDistance = 3) byexposes the distance scores for ranked-weighting developers (MCP servers, recently-used-bias suggestions);Levenshtein Scored(...) format(array $suggestions)pretty-prints the result. Cross-cutting use acrosswirekit:show,wirekit:theme,wirekit:list,wirekit:publish-icons,wirekit:component,wirekit:install, plusWireKit::.validateProp() wirekit:list --category=Marketing,Displaymulti-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=jsonconvention 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 --interactiveflag. Forces the--basechooser prompt even when TTY detection misfires (Herd / Docker / WSL). Pairs with the new derivation chain sowirekit:component customer-dashboardwalks: right-segment strip → Levenshtein suggestion → interactive choice() prompt → fail-fast with actionable error when no candidate matches.<x-wirekit::brand-bar> container+maxprops. Whencontainer=true, the brand-bar's CHROME (background, border, sticky behavior) stays edge-to-edge while CONTENT (brand, tagline, actions) aligns with the body's container-wrapped column via the inner max-width wrapper.maxaccepts the six-value container enum (sm/), defaultmd/ lg/ xl/2xl/ full xl. Reads--size-wk-container-*tokens so brand-bar + body align on the same vertical content-edge spine. Defaultcontainer=falsepreserves the v2.0.0 edge-to-edge render.<x-wirekit::navbar> container+maxprops. Same shape as brand-bar's pair — whencontainer=true, the navbar's outer<nav>(background, border, sticky behavior) stays viewport-edge-to-viewport-edge while the inner flex row (brand, nav items, actions) AND the mobile-menu panel wrap tomax-w-[var(--size-wk-container-{tier})]+mx-auto.maxacceptssm/, defaults tomd/ lg/ xl/2xl/ full 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. Defaultcontainer=falsepreserves the v2.0.0 full-bleed content render.<x-wirekit::hero> sizeprop + responsive vertical padding.size="sm|md|lg"(defaultlg) names thesm+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 defaultlgsemantic on tablet and up.<x-wirekit::gradient overlay containment. Whenhero> gradientis 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 undervariant="dark"+gradient. The overlay carriespointer-events-noneso it never intercepts clicks.<x-wirekit::cta> sizeprop. Same shape as hero'ssize— three-tier responsive vertical padding (defaultmd). Mobile drops one tier automatically.<x-wirekit::footer> maxprop. Replaces the prior hardcodedxlinner-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 — setmaxonce and every wrapper updates. Defaultxl. Hardcoded fallback values invar(--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) andMarketing 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 --diffdry-run mode. Reports what WOULD change inresources/, the layout file,css/ app. css config/, andwirekit. php public/without writing any files. Read-only preview. Exits 0 after rendering. Use to preview an install before committing — pairs with code-review workflows.vendor/ wirekit/ wirekit:install --rollbackmode. Reverses the most-recent install session by replaying recorded before-snapshots from.wirekit-install.at the developer project root. Per-file restore. Returns 0 on full success, 1 on partial restore. Mutually exclusive with every other install flag.log wirekit:install --no-strictopt-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 --forceflag. 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-flagsflag. 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.written bylog wirekit:installon 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 ANDresources/already declares the matchingcss/ app. css --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. - CI / Deploy script discipline section in
docs/— canonical fail-fast install shape, exit-code reference, GitHub Actions example, legacy opt-out recipe, dry-run preview pattern, rollback workflow.integration. md wirekit:doctorcleanup-hygiene check for custom Alpine plugins. Static-analysis scan of the developer'sresources/js/tree that flags two anti-patterns: anIntersection/Observer Mutation/Observer ResizeObserverinstantiation without adestroy()cleanup hook, and adisconnect()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-okopt-out comment for intentional patterns the heuristic doesn't recognize.- 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 wiringpestphp/into a WireKit-using Laravel app, including known-issue workarounds), andpest-plugin-browser 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\— companion toWireKit\ Support\ BladeParser PropsParserfor the broader Blade-content surface (named slots, directive enumeration, comment extraction, component-reference discovery). Replaces the inline slot-detection regex previously embedded inExport.Json Command wirekit:list --as=count|json|slugs|categoriesflag emits machine-readable output for script + AI consumption.--as=countproduces a single integer with no decoration (WK_COUNT=$(php artisan wirekit:list --as=count)),--as=slugsnewline-separated component names,--as=categoriesa JSON object mapping each category to its component count,--as=jsonthe full per-component array with name + tag + category + description. Default human-readable output unchanged.wirekit:show <name> --as=jsonflag emits the per-component structured schema as JSON to stdout (no decoration). Includesname,tag,category,description,docs_url,props[](full prop records withdefault,default_,normalized comment), andsub_. Tooling that needs one component's schema without loading the full export-json output.components[] wirekit:show <name> --validate-against=<path>flag lints a developer Blade file against the component's known prop set. Walks every<x-wirekit::usage, warns on unknown attributes with the closest matching prop name (Levenshtein-ranked) so typos likename ...> intnet→intentsurface 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.written byjson wirekit:install. Drop-in IDE-extension + AI-tool feeder file at the developer's project root. Contains the same JSON manifest aswirekit:export-json --pretty(every component's name + tag + category + description + full prop records + slots). Re-runningwirekit:installregenerates 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:
Component(canonical PHP-API for reading the component catalog at runtime) andRegistry — Programmatic Discovery Authoring Custom Blade Components(eight tested@propssyntax shapes WireKit's tooling understands, plus CLI tooling for custom components).
Changed
wirekit:installis now strict-by-default. Invalid font keys, category mismatches, unknown preset names, invalid--apex-licensetiers, AND detected token-clobbers all abort with exit 2 (INVALID) before any filesystem mutation. v2.0.0 BEHAVIOR 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:installexit 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
DisplaytoMarketing.cta,feature,feature-grid, andherocarry their conversion-funnel intent in the category field now. API-VISIBLE for developers readingwirekit:export-json/wirekit:list --as=json/Pushery\— the four entries'WireKit\ Component Registry:: all() categoryfield changed value. Rendering, props, slots, and tags unchanged; no other API surface affected. wirekit:doctoris now a Symfony alias ofwirekit:verify. Both names resolve to the SAME command instance — running either walks the identical check pipeline.php artisan list wirekitstill shows two rows (the alias appears with an[wirekit:doctor]annotation on the verify row), but the standaloneDoctorCommandclass file is removed and both invocations share one underlying handler. Internal API change for developers who importedPushery\directly in a custom service provider's command-registration array — switch toWireKit\ Console\ DoctorCommand Verify(which now declaresInstallation Command wirekit:doctorviasetAliases()).Pushery\return shape. WasWireKit\ Component Registry:: extractProps() array<string, string>(flat name → default-string map). Now returns the structuredlist<array{name, default, default_shape produced bynormalized, type_hint, comment}> PropsParser. BREAKING for developers of the prior shape. Migration: iterate the returned list, read each entry'sname/defaultkeys, and the inline-comment metadata (previously hidden) is now available via thecommentfield. 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 returnedself::SUCCESS. CI scripts wrapped inset -esaw exit 0 and continued. Now pre-flight aborts immediately with exit 2 and the actionable error message. wirekit:theme defaultreturned FAILURE. Thedefaultpreset wasn't inThemeCommand's inline PRESETS array, so runningwirekit:theme defaultexited 1 with "Unknown preset" — despitewirekit:install --preset=defaultbeing a valid no-op. Symmetry restored:wirekit:theme defaultnow exits 0, actively removes any existingwirekit:theme start/endblock fromapp.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 historicalstr_containsbidirectional match inShowproduced suggestions only when the typo's characters were a substring of a real name (or vice versa). Single-char typos likeComponent Command buttn(distance 1 frombutton) never matched. The newPushery\covers every "Unknown X" report uniformly acrossWireKit\ Support\ SuggestSimilar:: byLevenshtein wirekit:show,wirekit:theme,wirekit:list,wirekit:publish-icons,wirekit:component, ANDWireKit::runtime messages.validateProp() wirekit:component <name>defaulted--baseto<name>itself. When--basewasn't passed, the default pointed at the new component's own (non-existent) source — every invocation without explicit--basefailed with "Unknown base component". The new derivation chain (right-segment strip → Levenshtein fallback → interactive prompt → fail-fast with actionable error) handles the commonmy-X → Xpattern automatically (e.g.wirekit:component my-buttonderives--base=buttonand copiesbutton.).blade. php - Two regex-based
@props([...])parsers consolidated into a single tokenizer-backedPushery\. The historical parsers (WireKit\ Support\ PropsParser ComponentandRegistry:: extractProps Console\) silently truncated prop defaults that contained commas inside function-call argument lists (Export Json Command:: extractProps config('x.y', null)was split into two phantom props) AND leaked trailing// commentblocks into the captured default value. Both bug classes brokewirekit:showoutput AND thewirekit:export-jsonmanifest — IDE tooling and MCP servers reading the export got incomplete prop catalogs. The replacement uses PHP's owntoken_get_all()tokenizer and handles every legitimate@propsshape:config(...)defaults, multi-line array literals,match(...)expressions, mixed-quote keys, heredoc / nowdoc values. New regex-based@propsparsers insrc/are blocked from being added going forward. - Unguarded observer callbacks in
wirekitandStat Animate wirekitAnimateAlpine plugins. Browser-queuedIntersectioncallbacks could fire AFTER Alpine teardown setObserver this._observer = null(Livewire morph removing the host element pre-intersection is the canonical trigger), causingTypeError: Cannot read properties of null (reading 'disconnect')to log to the console. The page rendered correctly, but the silent error reded every developer'sassertNoSmoke()/assertbrowser-test against any WireKit page usingNo Javascript Errors() <x-wirekit::orstat animate> <x-wirekit::. The fix adds a null-guard immediately before the in-callbackreveal> disconnect()call in both plugins plus thereading-minimapinitializationIntersectionthat shared the bug shape. Sweep audit of the remaining observer instantiations acrossObserver reading-spine,reading-toc,reading-minimap(4 sites),chart, andchart-apexconfirmed they don't access the observer reference inside the callback, so no race exists.