letmesplain

Splain adapters — architecture & expansion

Splain is built as a framework-agnostic engine with thin adapters, so it can grow from "Filament plugin" to "guidance for anything a Laravel shop ships" without rewriting the hard part. This doc is the contract an adapter must satisfy and the roadmap for the adapters we want.

The shape: one engine, many adapters

                        ┌───────────────────────────────────────────┐
                        │  THE ENGINE  (resources/js/*.ts, pure TS)  │
                        │  driver.js spotlight · flow DAG · privacy  │
                        │  knows NOTHING about Filament/Tailwind/    │
                        │  any host design system (splain.ts:84)     │
                        └───────────────────────────────────────────┘
                             ▲ reads a documented page contract ▲
        ┌────────────────────┼─────────────────────┬────────────────────┐
   ┌────┴─────┐        ┌─────┴──────┐        ┌──────┴─────┐        ┌─────┴──────┐
   │ Filament │        │ pure-TS /  │        │  Livewire  │        │  Inertia / │
   │ v3 (shipped)      │ standalone │        │ (no Filament)       │  Vue/Blade │
   └──────────┘        └────────────┘        └────────────┘        └────────────┘
     each adapter only has to: put the payload on the page · render the launcher ·
                    deliver the assets · signal SPA navigation

The engine is already the framework-agnostic core the design intended — its own header says it "knows NOTHING about Filament, Tailwind or any host design system." An adapter is deliberately small.

The adapter contract (what any adapter must provide)

1. The payload — a single script tag the engine reads (readPayload()):

<script type="application/json" data-splain-payload>
  { "guides": [ … ], "masks": [ … ], "ui": { … } }
</script>

The shape is the guide serialization documented in schema.md / playback.md. How you produce it is the adapter's business — the Filament adapter is a Livewire component (SplainPlayback::payload()); a pure-TS adapter could fetch it from a JSON endpoint or inline it server-side.

2. The launcher DOM — the helper dot and its panel, marked with the contract attributes the engine wires behaviour onto ([data-splain-dot], [data-splain-panel], per-guide [data-splain-guide], the privacy toggle [data-splain-privacy]). The engine styles nothing — the adapter renders these in the host's own design system (the Filament adapter wears Filament's compiled CSS).

3. Asset delivery — serve splain.js + splain.css. Filament uses FilamentAsset; another adapter can ship a Vite entry or a plain <script>.

4. The SPA navigation signal — the engine's only lifecycle coupling, and it is framework-neutral. It binds two abstract signals:

Signal Meaning Default binding
navigating SPA is leaving the page → tear down an active walkthrough livewire:navigating
navigated SPA arrived on a new page → boot livewire:navigated

Livewire's events are wired by default (Filament needs zero setup). Any non-Livewire adapter drives the identical lifecycle by dispatching the framework-neutral events instead:

document.dispatchEvent(new Event('splain:navigating')); // before your SPA swaps the DOM
document.dispatchEvent(new Event('splain:navigated'));   // after it settles

That is the whole seam. A static (non-SPA) site needs neither — the engine also boots on load.

Roadmap

Adapter Status Notes
Filament v3 shipped the reference adapter; playback + Studio
Filament v4 shipped + verified (playback AND Studio hub) Constraint ^3.2 || ^4.0, CI runs the full suite on BOTH majors. Verified on a fresh Laravel 13 + Filament v4.11 app: install, all splain:* commands, in-browser playback, and the FULL Studio hub (guide inventory, track builder with ordered-guides repeater + assignment manager, onboarding report). The hub is version-split — SplainStudioHubV3/V4 roots outside composer's autoload (a runtime prefix autoloader in StudioServiceProvider), selected by installed major in StudioPlugin — because v4's Schema unification made v3's form(Form)/infolist(Infolist) signatures unloadable under v4 and vice versa. Proving ground: ~/projects/splain-v4-test.
Filament v5 not yet exists (5.x); deliberately outside the constraint until v4 is fully served.
pure TypeScript / standalone shipped (v0.1, reference) Splain with no Filament and no PHP: resources/dist/standalone.js renders the launcher/checklist chrome (neutral skin) from a payload object and boots the engine. Browser-verified end-to-end (launcher, checklist, tour, walkthrough spotlight, and decision branching) on a plain HTML page with window.Livewire/window.filament both undefined. See below + examples/standalone/. The Studio (authoring) stays a separate Laravel-side concern.
Livewire without Filament designed same engine; the adapter renders the launcher in the host's own Blade/Livewire components. Livewire events already drive it.
Inertia / Vue, plain Blade designed dispatch splain:navigated from the router's after-navigate hook; render the launcher as a Vue component / Blade partial.

Guiding principle: keep Laravel at the core first — it's where the depth and the daily proving happens — but design every seam so that anything a Laravel shop puts in front of their app can eventually get in-perimeter, code-anchored guidance. The goal is to add to Laravel's industry strength, not to fork away from it.

YAGNI guard: we do not build speculative adapters before there's a real consumer. What we do now is keep the seam clean and documented (this file) and never let a new feature re-couple the engine to Livewire/Filament specifics — the navigation signal above is the model: bind the framework's event, but always behind a framework-neutral one.

Standalone adapter (non-Filament) — usage

Ship two files — the engine and the standalone adapter — then hand it a payload:

<link rel="stylesheet" href="/path/to/splain.css">      <!-- driver.js theme + popovers -->
<script src="/path/to/splain.js"></script>              <!-- the engine (framework-agnostic) -->
<script src="/path/to/standalone.js"></script>          <!-- the neutral-skin adapter -->
<script>
  SplainStandalone.mount({
    guides: [{ slug, title, genre: 'walkthrough'|'tour', version, steps: [...] }],
    tracks: [{ slug, title, guides: [{ slug, title, version, url }] }],   // optional checklist
    progress: { enabled: false },                                          // optional
  });
</script>

mount(payload, { root?, skin? }) renders the dot + launcher (its own neutral, prefers-color-scheme-aware skin — pass skin: false to supply your own CSS), injects the payload script, and boots the engine. For an SPA, call mount() again after a route change (it replaces the prior chrome) and/or dispatch splain:navigated. steps are the same shape the Filament serializer emits (see docs/generation-design.md and the payload snapshot); a server or a static build produces the payload however it likes — the adapter only cares about the shape.

What's proven vs. what a first adopter hardens: the engine is the same core the production-shaped Filament proving host exercises daily — playback, decisions, advance-on, cross-page hand-off all shared. The standalone adapter is a thin (~4.5kb) chrome renderer, hermetically tested (resources/js/standalone.test.ts) and browser-verified via examples/standalone/. What it hasn't had is a real third-party SPA with real content and users — that's a first-adopter milestone, and shipping this as a reference adapter is how you attract one. Run the harness locally: serve the repo root and open examples/standalone/index.html.


Rendered from splain@b6ea439. The documentation is rendered from the package repository — the same files that ship with Splain — so the site can't drift from the code.