Files
livewire-material/docs/plans/livewire-material.md
T
Andreas Reinhold / reiniandClaude Opus 5 9b53891a8e
tests / lint (push) Successful in 1m14s
tests / feature (8.4) (push) Successful in 56s
tests / feature (8.5) (push) Successful in 56s
tests / browser (chrome, chromium) (push) Successful in 2m9s
tests / browser (firefox, firefox) (push) Successful in 1m34s
tests / browser (safari, webkit) (push) Successful in 1m41s
Scaffold the Livewire Material package
The skeleton for nonameweb/livewire-material: service provider and config
(unprefixed components, blade-icons' own <x-icon> switched off, an opt-in
showcase at /material), the Testbench Workbench with its Vite build, the
CSS and JS entry points applications import, the Boost guideline and skill
with a drift test, and Gitea CI running Pint, feature tests on PHP 8.4 and
8.5, and browser tests in Chrome, Firefox and Safari.

The plan moved here from SealShare: docs/plans/livewire-material.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 04:59:21 +02:00

28 KiB
Raw Blame History

Livewire Material: a shared Material 3 Expressive component library

Split on 2026-09-13 from SealShare's docs/plans/livewire-material.md, where the plan was made. SealShare's adoption (Phase 11) stays in SealShare's copy.

Goal

ReStride has just left maryUI and daisyUI for Material 3 Expressive on its own Blade components (ReStride/docs/plans/material-expressive.md, done 2026-09-10). Doing that again, by hand, in every Laravel + Livewire app is what this package avoids.

nonameweb/livewire-material carries the whole current M3 Expressive component catalogue as Blade components for Livewire, together with everything around them — colour scheme generation, tokens, the theme script, Material Symbols, Google Sans Flex, motion, error pages, a mail theme, a showcase, test helpers and AI guidelines. SealShare converts to it once it is complete (1.0.0); ReStride adopts it later, in a plan of its own.

Context

Stacks. SealShare: Laravel 13.31, Livewire 4.4, maryUI 2.9.10 (no prefix), daisyUI 5.7, Tailwind 4.3, Pest 5.1, Octane on FrankenPHP, PHP 8.5; public on GitHub under MIT, image published to ghcr.io/surtic86/sealshare. ReStride: same Laravel / Livewire / Tailwind / Pest, private on gitea.nonameweb.ch, CI through Gitea act_runner.

What ReStride already solved (and the package generalises):

  • 45 anonymous components in resources/views/components/, maryUI's names and props, M3 styling; class components only where PHP earns it.
  • resources/css/material/{color,shape,type,motion,elevation,field,menu,table,list}.css; @theme (never inline) mapping M3 roles to utilities; --color-*: initial so only tokens compile; :root dark + [data-theme=light], each with its own color-scheme.
  • Material Symbols Rounded (400/0/24) as local SVGs registered as blade-icons sets (ms/msf), config/blade-icons.php turning off blade-icons' own <x-icon>.
  • Google Sans Flex, a 63 KB Latin subset (weight 400700, ROND 0100).
  • Spring motion as CSS linear() curves; press shape-morph; Expressive shapes generated by formula into resources/svg/shapes.
  • App\Livewire\Concerns\Toasts (protected success|error|warning|info, dispatching a window event) and a snackbar <x-toast>.
  • ~30 component tests ($this->blade()), browser tests (Pest browser plugin), and guard tests (DesignLanguageTest, MaterialTokensTest, MaterialSymbolsTest).
  • The Livewire traps, recorded in ../ReStride/.ai/rules/ui.md: wire:ignore.self on a showModal() dialog; never pass hidden or a position to a component (wrap it); Blade directives do not compile inside a component tag's attributes; wire('model')->value() is false, not null, without wire:model; a field's states are CSS selectors (:has()), marked with data-invalid / data-floated / data-readonly where the control cannot carry them; the customizable select's traps (../ReStride/.ai/rules/components-views-components.md).
  • Mail on M3 (../ReStride/docs/plans/material-mail.md): one theme CSS inlined onto bare tags, light only, because CssToInlineStyles strips @media.

What in ReStride is app-specific and does not move: the doctrine (two button weights, no tertiary / primary-container — enforced by its build), restride-theme, $store.install / $store.connection in the shell, --bottom-bar set by its layout, sport / zone / route tokens, training-row, map-shell, map-chip, product-shot, star-rating, share-button, the unDraw repaint, the Ace code editor.

Constraints found.

  • gitea.nonameweb.ch requires sign-in to view anything (/explore/repos → login, API 403). A "public" repo there cannot be installed anonymously until that changes. (Resolved in step 1.)
  • Laravel's Markdown mail accepts a namespaced view as theme (Illuminate/Mail/Markdown.php:114), so a package can render the theme CSS from data.
  • Laravel replaces the errors view namespace at render time with config('view.paths') + /errors and the framework's own (Illuminate/Foundation/Exceptions/RegisterErrorViewPaths.php), so error views a package adds with addNamespace('errors', …) are wiped; only a path in view.paths survives.
  • M3 Expressive deprecates bottom app bar, navigation drawer, the original navigation bar, segmented button, small FAB and medium/large top app bar (material-components-android docs).
  • CSS anchor positioning is in Chrome 125+, Firefox 147+, Safari 18.4+ (flip via @position-try from Safari 18.4).
  • Boost loads a package's resources/boost/guidelines/core.blade.php and its skills on boost:install / boost:update --discover.

Decisions

  • A shared Composer package, SealShare its first consumer; ReStride adopts later in its own plan — SealShare's small surface proves the API; ReStride's three-day-old migration is not put at risk.
  • Public repo on gitea.nonameweb.ch, MIT — SealShare is public; its CI, Docker build, manual install and fork PRs must install the package without credentials. SealShare requires it through a vcs repository entry. Material Symbols (Apache-2.0), Google Sans Flex (OFL) and material-web token values (Apache-2.0) are compatible; attributions ship in NOTICE.
  • Prerequisite: Gitea allows anonymous reads ([service] REQUIRE_SIGNIN_VIEW = false, or expensive on 1.23+) — otherwise "public" is not installable.
  • Name nonameweb/livewire-material, namespace NoNameWeb\LivewireMaterial, config/livewire-material.php, views livewire-material::, commands material:* — says what it is and what it is for.
  • Components unprefixed by default, prefix configurable (maryUI's model, via Blade::anonymousComponentPath($path, $prefix)) — ReStride's call sites keep their names; SealShare's maryUI tags keep theirs. The provider sets blade-icons.components.default to null, or blade-icons' class <x-icon> beats ours.
  • CSS and JS imported from vendor/ (@import, @source, import) — one dependency, one version; consuming Dockerfiles install Composer packages before the Vite build. @source covers the package's views and PHP, never its SVG folders.
  • The full Material Symbols Rounded set (400 / 0 / 24, outlined and filled, ~7,800 SVGs, ~34 MB), produced by a maintenance script in the package repo, not fetched at runtime — any name works in any app; icons:cache keeps lookups cheap under Octane.
  • Google Sans Flex bundled (ReStride's subset) via @font-face in the package CSS; an app overrides --font-sans.
  • Full M3, not ReStride's doctrine — every variant, colour role (tertiary and primary-container included) and container is available; each app enforces its own rules through a configurable guard helper the package ships.
  • variant + color propsvariant="filled|tonal|outlined|text|elevated", color="primary|secondary|tertiary|error|success|warning|info", default text in primary; both validated against fixed lists, unknown values fall back to the default. Shorthands kept: primary = filled primary, danger = filled error, caution = filled warning. The same pattern for badge, alert, chip, icon button and FAB (tone stays an alias of color where ReStride used it).
  • php artisan material:scheme {seed} --variant= — runs Google's material-color-utilities from a single prebundled Node script inside the package; writes the app's resources/css/material-scheme.css (dark and light --md-sys-color-*, plus success/warning/info custom colours, harmonisation off) and resources/css/material-scheme.json (the light hexes, for the mail theme). Output is committed.
  • Theme: a head script component, light | dark | system — sets data-theme before paint; CSS keys only on the attribute and never asks prefers-color-scheme; only the script reads the OS (and follows its changes while system). $store.theme is the one state every toggle shares. Config: theme.default, theme.storage_key, theme.legacy_keys (adopted once, then removed).
  • The whole M3 Expressive catalogue, current components only — the six deprecated ones are skipped; their names alias where free (<x-group> renders a connected button group, FAB size="sm" renders medium).
  • Plus the non-M3 pieces apps need — data table, sort header, pagination views, file input, password, stat, alert, empty state, collapse, section nav, account menu, theme toggle, an adaptive app-shell composition, error pages, mail theme.
  • Modern browser floor, no third-party JS — Chrome 125+, Firefox 147+, Safari 18.4+: native <dialog>, Popover API, CSS anchor positioning, customizable <select> as a progressive enhancement; Alpine (bundled with Livewire) for behaviour. Date and time pickers, carousel and search are our own.
  • PHP ^8.4, Laravel ^13, Livewire ^4. No request state in singletons (Octane).
  • Strings through __() with an en file, publishable and overridable.
  • Accessibility: WAI-ARIA Authoring Practices patterns, WCAG 2.2 AA contrast, full keyboard.
  • Showcase in the package, mounted twice — by the Workbench for development and browser tests, and opt-in inside an app (showcase.enabled, default only when APP_ENV=local) so an app sees every component in its own scheme. Every variant, colour, size and state, a light/dark/system switch, and the Blade snippet beside each example.
  • Tests: render tests for every component, browser tests against the Workbench showcase in Chromium, Firefox and WebKit; no screenshot diffs.
  • Error pages and a mail theme in the package — the mail theme is the namespaced view livewire-material::mail.theme, rendering CSS from the app's material-scheme.json, so mail colours cannot drift from the app.
  • Boost guideline + livewire-material-development skill in the package, and a test that fails when a component is missing from the skill.
  • Versioning: 0.x per wave, 1.0.0 when the catalogue is complete.
  • Semantic ink and line utilities carried over from ReStride (text-body, text-meta, text-quiet, border-structure, border-chrome, border-divider) — cheap @theme names ReStride's templates already use, derived from M3 roles.

Out of scope

  • ReStride adopting the package — its own plan, after 1.0.0.
  • A brand colour chosen at runtime (e.g. in SealShare's admin), dynamic colour, a PHP port of the colour maths.
  • M3 Expressive's deprecated components (bottom app bar, navigation drawer, original navigation bar, segmented button, small FAB, medium/large top app bar).
  • Screenshot / visual regression tests.
  • Blaze optimisation of the components — a follow-up if rendering gets slow.
  • ReStride-specific components and tokens (training row, maps, product shot, star rating, share button, sport/zone/route tokens, unDraw repaint, Ace code editor).
  • Publishing on Packagist — the vcs repository is enough; revisit if others adopt it.
  • Translations beyond en.
  • Browsers below the floor.
  • Non-Livewire stacks (Inertia, React, Vue).

Implementation steps

Phase 0 — Prerequisites

  1. Gitea. Done 2026-09-13. GITEA__service__REQUIRE_SIGNIN_VIEW=false in the Gitea compose service (Gitea 1.26.4); /explore/repos and the API answer anonymously, and no other repo or org is public. The package lives in the public org noNameWEB: https://gitea.nonameweb.ch/noNameWEB/livewire-material.git (public, empty, main, Actions on). SealShare's vcs repository entry uses that URL.
  2. Split the plan. Done 2026-09-13. This file; SealShare keeps its adoption steps.

Phase 1 — Package skeleton (0.1.0)

  1. Repository. composer.json (nonameweb/livewire-material, PSR-4 NoNameWeb\LivewireMaterial\, requires php ^8.4, laravel/framework ^13, livewire/livewire ^4, blade-ui-kit/blade-icons ^1.10; dev: orchestra/testbench, pestphp/pest, pestphp/pest-plugin-laravel, pestphp/pest-plugin-browser, laravel/pint), LICENSE (MIT), NOTICE, pint.json, phpunit.xml, .gitattributes excluding workbench/, tests/, bin/, docs/ and the Node/Vite dev files from dist archives.
  2. Service provider LivewireMaterialServiceProvider: merge config; register the anonymous component path with the configured prefix; loadViewsFrom (livewire-material), loadTranslationsFrom; set blade-icons.components.default to null in register() (before blade-icons boots and registers its component); register the icon and shape sets; append the package's error-view root (a directory holding only errors/) to view.paths after the app's own, so Laravel's namespace replacement keeps it and an app's resources/views/errors still wins; publishable config, lang, error views; commands; the showcase routes when enabled. No request state anywhere in the container.
  3. Config config/livewire-material.php: prefix, theme.default, theme.storage_key, theme.legacy_keys, showcase.enabled (env('MATERIAL_SHOWCASE', app()->isLocal())), showcase.path (material), showcase.middleware (['web']).
  4. Workbench (orchestra/workbench): a Laravel app with Livewire, Vite and the package CSS/JS built, serving the showcase at /; composer serve starts it.
  5. CI on Gitea act_runner: Pint, Pest render tests, browser tests in Chromium, Firefox and WebKit (Playwright installed with deps), on pushes and pull requests.
  6. Boost resources skeleton: resources/boost/guidelines/core.blade.php and resources/boost/skills/livewire-material-development/SKILL.md; the drift test (every component file under resources/views/components is named in the skill).

Phase 1 is done (2026-09-13). What changed from the steps above:

  • Testbench 11.2 runs the Workbench (testbench.yaml: providers listed, start: /material, MATERIAL_SHOWCASE=true). Vite builds into workbench/public/build; composer serve links that into the skeleton's public/build through the Workbench sync option, and tests/TestCase.php points public_path() at workbench/public so browser tests find the manifest without the command. Verified: composer serve answers /material and its CSS.
  • The showcase is a plain view with @extends, not a component: <x-livewire-material::…> resolves under components/, which would have made the layout a public component.
  • blade-icons' components.default is set in a booting callback, not in register(): mergeConfigFrom is shallow, so a nested key written before blade-icons merges its defaults wipes the rest of its components array. The test fails with the call removed.
  • The showcase switch is tested by rebooting with MATERIAL_SHOWCASE in the environment: Testbench's per-test attributes do not reach Pest closures.
  • The Boost guideline is wrapped in @verbatim: it is rendered as Blade, and a <x-mary-*> in its prose would compile as a component tag.
  • phpunit.xml trap: a comment containing a double hyphen (--browser) is invalid XML and PHPUnit refuses the file; Pest then reports an unrelated Pest\Plugins\Tia error on shutdown.
  • CI (.github/workflows/tests.yml): Pint, feature tests on PHP 8.4 and 8.5, browser tests per engine — Pest's --browser chrome|firefox|safari, Playwright's chromium|firefox|webkit. Locally only Chromium was run; Firefox and WebKit first run on CI.
  • NOTICE moves to Phase 2, when the first third-party assets (Symbols, font, tokens, colour utilities) arrive — a notice with nothing to attribute would be wrong.
  • The scheme script is not in bin/: bin/ is export-ignored (maintenance scripts only), but applications run the scheme script, so it lives at resources/node/scheme.mjs.
  • Found for Phase 2: blade-icons calls Factory::registerComponents() whenever the view factory resolves, registering one Blade component per icon unless components.disabled — and without icons:cache it scans the folders first. With ~7,800 symbols that is per request under PHP-FPM. Decided before step 13.

Phase 2 — Foundation (0.2.0)

  1. Scheme command. resources/node/scheme.mjs built once in the package repo (esbuild bundle of @material/material-color-utilities, committed, Apache-2.0 header); material:scheme {seed} {--variant=tonal-spot|vibrant|expressive|fidelity|content|neutral|monochrome} {--success=} {--warning=} {--info=} {--output=} runs it through node and writes resources/css/material-scheme.css and resources/css/material-scheme.json. Fails with a clear message when node is missing. A default scheme (M3 baseline #6750A4) ships inside the package so a fresh install renders before the command is run.
  2. Tokens. resources/css/material.css as the single entry, importing tokens/shape.css, type.css (typescale utilities incl. emphasized), motion.css (six spring linear() curves — spatial/effects × fast/default/slow — and reduced-motion overrides), elevation.css, state.css (state layers, focus ring), and the @theme block mapping every M3 role (primary/secondary/tertiary and their containers, surfaces, outlines, inverse, error, custom success/warning/info with inverse-*) plus the semantic ink and line utilities; --color-*: initial with white and black re-added. Each theme block declares color-scheme. Values from material-web's token files, attributed.
  3. Font. resources/fonts/google-sans-flex/ (woff2 + OFL), @font-face and --font-sans in the entry CSS.
  4. Theme. <x-theme-script /> (inline, before @vite): reads storage_key, adopts legacy_keys (their values may be JSON-encoded — maryUI's $persist stores "dark" with quotes), resolves system through matchMedia, writes data-theme; $store.theme in resources/js/material.js with set() and a matchMedia listener while system.
  5. Icons. bin/fetch-symbols (maintenance only) downloads Material Symbols Rounded 400/0/24 outlined and filled from google/material-design-icons into resources/svg/symbols/{outlined,filled} with fill="currentColor" and no width/height; <x-icon name="…" filled> resolves ms/msf; throws on an unknown name.
  6. Shapes. The Expressive shape set generated by formula (ReStride's components/shape.blade.php method) into resources/svg/shapes; <x-shape>.
  7. JS entry resources/js/material.js: theme store, snackbar listener, data-list-row rows, x-figure count-up directive, shared keyboard helpers. Imported by an app's app.js.
  8. Toasts concern NoNameWeb\LivewireMaterial\Concerns\Toasts: protected success|error|warning|info(string $title, ?string $description = null, ?int $timeout = null, ?string $redirectTo = null), dispatching a browser event and flashing across redirectTo.
  9. Guard helpers (NoNameWeb\LivewireMaterial\Testing\DesignGuard, Pest-friendly): scan given paths for maryUI tags, daisyUI component classes, colour utilities not declared by the compiled tokens, unknown icon names, and app-configured banned roles/variants.
  10. Showcase shell: layout, section navigation, theme switch, snippet renderer; a section per foundation piece (colour roles in both themes, type scale, shapes, motion, icons search).

Phase 3 — Actions (0.3.0)

  1. Primitives the actions need: loading (M3 Expressive loading indicator, contained and not), plain tooltip (from a fine pointer only, not laid out while hidden), menu / menu-item / menu-separator (Popover API + anchor positioning, Expressive vertical menu, APG menu keyboard).
  2. button — five variants × colours × sizes xs|sm|md|lg|xl, icon, icon-right, label, link (+ wire:navigate unless external / no-wire-navigate), spinner, responsive, tooltip*, disabled on links (aria-disabled), press shape-morph.
  3. icon-button behaviour inside button (icon, no label): standard/filled/tonal/outlined, selected toggle (aria-pressed), widths.
  4. button-group (standard and connected; <x-group> alias with wire:model options), split-button, fab (medium default, large; size="sm" → medium), extended FAB, and the responsive fab prop (extended FAB below sm, filled header button above — one element), fab-menu.

Phase 4 — Communication (0.4.0)

  1. badge (dot, count, label; variant/colour), progress (linear, circular, wavy, determinate and indeterminate), toast (M3 snackbar, action, timeout, stacked), rich tooltip, alert (tinted container, icon, actions slot), stat (figure with x-figure), empty-state.

Phase 5 — Containment (0.5.0)

  1. card (elevated, filled, outlined; title, subtitle, actions slot; clickable row contract), divider, list / list-item (one-, two-, three-line; leading/trailing; selectable), modal (native <dialog>, showModal(), wire:ignore.self, writes back false/null, fullscreen below sm, basic dialog with icon/headline/actions), bottom-sheet (modal and standard, drag handle), drawer (side sheet: modal and standard; pane for list-detail from xl; width prop), carousel (multi-browse, uncontained, hero, full-screen on CSS scroll-snap), collapse.

Phase 6 — Text inputs and selection (0.6.0)

  1. form, field (the shared shell: outlined and filled, floating label via :has(), notch, hint replaced by error, aria-invalid / aria-describedby, data-* state marks), input (prefix/suffix, icons, copyable trailing button with a snackbar), password (reveal toggle), textarea (auto-grow), select (native, customizable-select enhancement with ReStride's traps), checkbox (incl. indeterminate), radio, toggle (M3 switch, icons), file (native input inside the field, batch and per-file errors).
  2. chip (assist, filter, input, suggestion), choices (filter chips, or a searchable combobox with a menu when searchable), slider (standard, centered, range; native range inputs, value label), search (search bar and search view, results through a Livewire property).

Phase 7 — Pickers (0.7.0)

  1. datepicker (docked, modal, modal input; Intl month/day names and week start from the app locale; min/max; single and range; wire:model stores Y-m-d), timepicker (dial and input; 12/24h from locale; stores H:i). APG grid keyboard for the calendar.

Phase 8 — Navigation (0.8.0)

  1. app-bar (small, center-aligned, medium flexible, large flexible, search app bar; sticky, scroll-elevation), navigation-bar (flexible), navigation-rail (collapsed, expanded, modal; badges), tabs / tab (primary and secondary; server-rendered tablist, roving tabindex), toolbar (docked and floating), section-nav (secondary tabs from sm, menu picker below), account-menu (avatar trigger, slot for items, theme row), theme-toggle (cycles or picks light/dark/system through $store.theme).
  2. app-shell — a slot-based adaptive composition: app bar + navigation bar below sm, rail smlg, expanded collapsible rail from lg (state in the store, applied before paint by the theme script), content region with wire:transition.navigate, snackbar host. Nothing app-specific inside; apps pass destinations and extra chrome as slots.

Phase 9 — Data, pages, mail (0.9.0)

  1. table (.data-table, descendant selectors, fine-pointer density, position: relative), sort-header (sortBy array shape, aria-sort), Livewire and Laravel pagination views (current page in secondary-container, "Page 2 of 7" on a phone).
  2. Error pages: a layout and 403, 404, 419, 429, 500, 503 in the package's error-view root (wired through view.paths in step 4); publishable for per-app wording.
  3. Mail theme: livewire-material::mail.theme renders CSS from the app's material-scheme.json (falling back to the default scheme); html/header and html/message overrides; typescale on bare tags; filled primary button.

Phase 10 — 1.0.0

  1. Showcase complete (every component, variant, colour, size and state, both themes); the in-app mount verified inside a fresh Laravel app; Boost guideline and skill complete (drift test green); README (install, CSS/JS wiring, scheme, theme, prefix, showcase, guard, Docker ordering note); tag 1.0.0.

Phase 11 — SealShare 2.0.0

Tracked in SealShare's docs/plans/livewire-material.md, after 1.0.0.

Testing

  • Render tests (tests/Feature/Components/*Test.php, Testbench, $this->blade()), per component: each variant × color renders its classes and falls back on an unknown value; shorthands (primary, danger, caution, tone) map correctly; sizes; ARIA (aria-pressed, aria-expanded, aria-current, aria-invalid + aria-describedby, labelled dialogs, aria-sort); link adds wire:navigate unless external; wire:model normalisation (falsenull); prefix config renames the tags.
  • Foundation tests: every --md-sys-color-* role defined in both theme blocks and different between them; each block declares color-scheme; no prefers-color-scheme in the CSS; @theme never inline; the scheme command writes both files for a known seed (snapshot of a few roles) and fails cleanly without Node; the theme script lands before @vite and resolves system; <x-icon> throws on an unknown name; every shape fills the 100-unit box with currentColor only; Toasts dispatches and survives redirectTo; DesignGuard catches each forbidden pattern on fixtures; the skill drift test; the mail theme renders the JSON's hexes; a 404 renders the package's error view and an app's own errors/404.blade.php still wins; the showcase route is 404 when disabled and 200 when enabled; blade-icons' own <x-icon> is not registered.
  • Browser tests against the Workbench showcase in Chromium, Firefox and WebKit: dialog (open, Esc, morph survival, write-back), bottom and side sheets and the pane from xl, menu and select (keyboard, anchoring, flip), tabs (arrows, Home/End), date and time pickers (keyboard grid, locale, wire:model value), carousel (snap, keyboard), sliders, chips and choices, search, snackbar timing and action, theme (light/dark/system, legacy adoption, OS change followed), app shell at 393 / 768 / 1024 / 1512px, focus rings on keyboard focus, and reduced motion leaving no running animations.

Risks and open questions

  • Scope and time. The whole catalogue (~45 components plus extras) comes before any app uses it. Mitigation: waves tagged 0.x, each reviewed in the showcase.
  • Accessibility is entirely ours — menus, pickers, carousel, sheets. Mitigation: APG patterns, native elements first, ARIA in render tests, keyboard in browser tests across three engines.
  • Date and time pickers without a library are the largest single components. Mitigation: Intl for all locale data; the modal-input variant as the accessible baseline.
  • Full M3 invites inconsistency per app. Mitigation: the configurable DesignGuard; each app records its own rules.
  • Livewire morphing against Alpine state inside components. Mitigation: ReStride's recorded traps become package rules in the skill, with tests pinning each.
  • ~7,800 SVGs make dist archives and vendor/ larger (~4 MB) and a cold icon lookup slower. Mitigation: .gitattributes keeps dev files out, icons:cache in deploys, @source never scans the SVG folders.
  • Maintenance of a public package is one person's job; breaking changes need semver discipline once ReStride also depends on it.
  • Runner scope. The package's CI needs an act_runner registered for the instance or the noNameWEB org, not only for ReStride — check before the first push in Phase 1.