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
28 KiB
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(neverinline) mapping M3 roles to utilities;--color-*: initialso only tokens compile;:rootdark +[data-theme=light], each with its owncolor-scheme.- Material Symbols Rounded (400/0/24) as local SVGs registered as
blade-iconssets (ms/msf),config/blade-icons.phpturning off blade-icons' own<x-icon>. - Google Sans Flex, a 63 KB Latin subset (weight 400–700, ROND 0–100).
- Spring motion as CSS
linear()curves; press shape-morph; Expressive shapes generated by formula intoresources/svg/shapes. App\Livewire\Concerns\Toasts(protectedsuccess|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.selfon ashowModal()dialog; never passhiddenor a position to a component (wrap it); Blade directives do not compile inside a component tag's attributes;wire('model')->value()isfalse, notnull, withoutwire:model; a field's states are CSS selectors (:has()), marked withdata-invalid/data-floated/data-readonlywhere 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, becauseCssToInlineStylesstrips@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.chrequires 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
errorsview namespace at render time withconfig('view.paths')+/errorsand the framework's own (Illuminate/Foundation/Exceptions/RegisterErrorViewPaths.php), so error views a package adds withaddNamespace('errors', …)are wiped; only a path inview.pathssurvives. - 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-tryfrom Safari 18.4). - Boost loads a package's
resources/boost/guidelines/core.blade.phpand its skills onboost: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 avcsrepository entry. Material Symbols (Apache-2.0), Google Sans Flex (OFL) and material-web token values (Apache-2.0) are compatible; attributions ship inNOTICE. - Prerequisite: Gitea allows anonymous reads (
[service] REQUIRE_SIGNIN_VIEW = false, orexpensiveon 1.23+) — otherwise "public" is not installable. - Name
nonameweb/livewire-material, namespaceNoNameWeb\LivewireMaterial,config/livewire-material.php, viewslivewire-material::, commandsmaterial:*— 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 setsblade-icons.components.defaulttonull, 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.@sourcecovers 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,
~3–4 MB), produced by a maintenance script in the package repo, not fetched at runtime —
any name works in any app;
icons:cachekeeps lookups cheap under Octane. - Google Sans Flex bundled (ReStride's subset) via
@font-facein 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+colorprops —variant="filled|tonal|outlined|text|elevated",color="primary|secondary|tertiary|error|success|warning|info", defaulttextin 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 (tonestays an alias ofcolorwhere ReStride used it).php artisan material:scheme {seed} --variant=— runs Google'smaterial-color-utilitiesfrom a single prebundled Node script inside the package; writes the app'sresources/css/material-scheme.css(dark and light--md-sys-color-*, plussuccess/warning/infocustom colours, harmonisation off) andresources/css/material-scheme.json(the light hexes, for the mail theme). Output is committed.- Theme: a head script component,
light | dark | system— setsdata-themebefore paint; CSS keys only on the attribute and never asksprefers-color-scheme; only the script reads the OS (and follows its changes whilesystem).$store.themeis 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, FABsize="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 anenfile, 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 whenAPP_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'smaterial-scheme.json, so mail colours cannot drift from the app. - Boost guideline +
livewire-material-developmentskill in the package, and a test that fails when a component is missing from the skill. - Versioning:
0.xper wave,1.0.0when 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@themenames 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
vcsrepository 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
- Gitea. Done 2026-09-13.
GITEA__service__REQUIRE_SIGNIN_VIEW=falsein the Gitea compose service (Gitea 1.26.4);/explore/reposand the API answer anonymously, and no other repo or org is public. The package lives in the public orgnoNameWEB:https://gitea.nonameweb.ch/noNameWEB/livewire-material.git(public, empty,main, Actions on). SealShare'svcsrepository entry uses that URL. - Split the plan. Done 2026-09-13. This file; SealShare keeps its adoption steps.
Phase 1 — Package skeleton (0.1.0)
- Repository.
composer.json(nonameweb/livewire-material, PSR-4NoNameWeb\LivewireMaterial\, requiresphp ^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,.gitattributesexcludingworkbench/,tests/,bin/,docs/and the Node/Vite dev files from dist archives. - Service provider
LivewireMaterialServiceProvider: merge config; register the anonymous component path with the configured prefix;loadViewsFrom(livewire-material),loadTranslationsFrom; setblade-icons.components.defaulttonullinregister()(before blade-icons boots and registers its component); register the icon and shape sets; append the package's error-view root (a directory holding onlyerrors/) toview.pathsafter the app's own, so Laravel's namespace replacement keeps it and an app'sresources/views/errorsstill wins; publishable config, lang, error views; commands; the showcase routes when enabled. No request state anywhere in the container. - 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']). - Workbench (
orchestra/workbench): a Laravel app with Livewire, Vite and the package CSS/JS built, serving the showcase at/;composer servestarts it. - 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.
- Boost resources skeleton:
resources/boost/guidelines/core.blade.phpandresources/boost/skills/livewire-material-development/SKILL.md; the drift test (every component file underresources/views/componentsis 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 intoworkbench/public/build;composer servelinks that into the skeleton'spublic/buildthrough the Workbenchsyncoption, andtests/TestCase.phppointspublic_path()atworkbench/publicso browser tests find the manifest without the command. Verified:composer serveanswers/materialand its CSS. - The showcase is a plain view with
@extends, not a component:<x-livewire-material::…>resolves undercomponents/, which would have made the layout a public component. - blade-icons'
components.defaultis set in abootingcallback, not inregister():mergeConfigFromis shallow, so a nested key written before blade-icons merges its defaults wipes the rest of itscomponentsarray. The test fails with the call removed. - The showcase switch is tested by rebooting with
MATERIAL_SHOWCASEin 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.xmltrap: a comment containing a double hyphen (--browser) is invalid XML and PHPUnit refuses the file; Pest then reports an unrelatedPest\Plugins\Tiaerror 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'schromium|firefox|webkit. Locally only Chromium was run; Firefox and WebKit first run on CI. NOTICEmoves 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 atresources/node/scheme.mjs. - Found for Phase 2: blade-icons calls
Factory::registerComponents()whenever the view factory resolves, registering one Blade component per icon unlesscomponents.disabled— and withouticons:cacheit 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)
- Scheme command.
resources/node/scheme.mjsbuilt 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 throughnodeand writesresources/css/material-scheme.cssandresources/css/material-scheme.json. Fails with a clear message whennodeis missing. A default scheme (M3 baseline#6750A4) ships inside the package so a fresh install renders before the command is run. - Tokens.
resources/css/material.cssas the single entry, importingtokens/shape.css,type.css(typescale utilities incl. emphasized),motion.css(six springlinear()curves — spatial/effects × fast/default/slow — and reduced-motion overrides),elevation.css,state.css(state layers, focus ring), and the@themeblock mapping every M3 role (primary/secondary/tertiary and their containers, surfaces, outlines, inverse, error, custom success/warning/info withinverse-*) plus the semantic ink and line utilities;--color-*: initialwith white and black re-added. Each theme block declarescolor-scheme. Values from material-web's token files, attributed. - Font.
resources/fonts/google-sans-flex/(woff2 + OFL),@font-faceand--font-sansin the entry CSS. - Theme.
<x-theme-script />(inline, before@vite): readsstorage_key, adoptslegacy_keys(their values may be JSON-encoded — maryUI's$persiststores"dark"with quotes), resolvessystemthroughmatchMedia, writesdata-theme;$store.themeinresources/js/material.jswithset()and amatchMedialistener whilesystem. - Icons.
bin/fetch-symbols(maintenance only) downloads Material Symbols Rounded 400/0/24 outlined and filled fromgoogle/material-design-iconsintoresources/svg/symbols/{outlined,filled}withfill="currentColor"and no width/height;<x-icon name="…" filled>resolvesms/msf; throws on an unknown name. - Shapes. The Expressive shape set generated by formula (ReStride's
components/shape.blade.phpmethod) intoresources/svg/shapes;<x-shape>. - JS entry
resources/js/material.js: theme store, snackbar listener,data-list-rowrows,x-figurecount-up directive, shared keyboard helpers. Imported by an app'sapp.js. ToastsconcernNoNameWeb\LivewireMaterial\Concerns\Toasts: protectedsuccess|error|warning|info(string $title, ?string $description = null, ?int $timeout = null, ?string $redirectTo = null), dispatching a browser event and flashing acrossredirectTo.- 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. - 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)
- Primitives the actions need:
loading(M3 Expressive loading indicator, contained and not), plaintooltip(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). button— five variants × colours × sizesxs|sm|md|lg|xl,icon,icon-right,label,link(+wire:navigateunlessexternal/no-wire-navigate),spinner,responsive,tooltip*,disabledon links (aria-disabled), press shape-morph.icon-buttonbehaviour insidebutton(icon, no label): standard/filled/tonal/outlined,selectedtoggle (aria-pressed), widths.button-group(standard and connected;<x-group>alias withwire:modeloptions),split-button,fab(medium default, large;size="sm"→ medium), extended FAB, and the responsivefabprop (extended FAB belowsm, filled header button above — one element),fab-menu.
Phase 4 — Communication (0.4.0)
badge(dot, count, label; variant/colour),progress(linear, circular, wavy, determinate and indeterminate),toast(M3 snackbar, action, timeout, stacked), richtooltip,alert(tinted container, icon, actions slot),stat(figure withx-figure),empty-state.
Phase 5 — Containment (0.5.0)
card(elevated, filled, outlined;title,subtitle,actionsslot; clickable row contract),divider,list/list-item(one-, two-, three-line; leading/trailing; selectable),modal(native<dialog>,showModal(),wire:ignore.self, writes backfalse/null,fullscreenbelowsm, basic dialog with icon/headline/actions),bottom-sheet(modal and standard, drag handle),drawer(side sheet: modal and standard;panefor list-detail fromxl;widthprop),carousel(multi-browse, uncontained, hero, full-screen on CSS scroll-snap),collapse.
Phase 6 — Text inputs and selection (0.6.0)
form,field(the shared shell: outlined and filled, floating label via:has(), notch,hintreplaced by error,aria-invalid/aria-describedby,data-*state marks),input(prefix/suffix, icons,copyabletrailing 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).chip(assist, filter, input, suggestion),choices(filter chips, or a searchable combobox with a menu whensearchable),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)
datepicker(docked, modal, modal input;Intlmonth/day names and week start from the app locale;min/max; single and range;wire:modelstoresY-m-d),timepicker(dial and input; 12/24h from locale; storesH:i). APG grid keyboard for the calendar.
Phase 8 — Navigation (0.8.0)
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 fromsm, menu picker below),account-menu(avatar trigger, slot for items, theme row),theme-toggle(cycles or picks light/dark/system through$store.theme).app-shell— a slot-based adaptive composition: app bar + navigation bar belowsm, railsm–lg, expanded collapsible rail fromlg(state in the store, applied before paint by the theme script), content region withwire:transition.navigate, snackbar host. Nothing app-specific inside; apps pass destinations and extra chrome as slots.
Phase 9 — Data, pages, mail (0.9.0)
table(.data-table, descendant selectors, fine-pointer density,position: relative),sort-header(sortByarray shape,aria-sort), Livewire and Laravel pagination views (current page insecondary-container, "Page 2 of 7" on a phone).- Error pages: a layout and
403, 404, 419, 429, 500, 503in the package's error-view root (wired throughview.pathsin step 4); publishable for per-app wording. - Mail theme:
livewire-material::mail.themerenders CSS from the app'smaterial-scheme.json(falling back to the default scheme);html/headerandhtml/messageoverrides; typescale on bare tags; filled primary button.
Phase 10 — 1.0.0
- 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: eachvariant×colorrenders 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);linkaddswire:navigateunlessexternal;wire:modelnormalisation (false→null); prefix config renames the tags. - Foundation tests: every
--md-sys-color-*role defined in both theme blocks and different between them; each block declarescolor-scheme; noprefers-color-schemein the CSS;@themeneverinline; 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@viteand resolvessystem;<x-icon>throws on an unknown name; every shape fills the 100-unit box withcurrentColoronly;Toastsdispatches and survivesredirectTo;DesignGuardcatches 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 ownerrors/404.blade.phpstill 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:modelvalue), 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:
Intlfor 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:.gitattributeskeeps dev files out,icons:cachein deploys,@sourcenever 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
noNameWEBorg, not only for ReStride — check before the first push in Phase 1.