Skip to main content

WireKit v2.1.0

Minor release

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

Changed

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

Fixed

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