playground
The workbench for the shared web components and the web app's route map, on port 3004.
apps/statshub-playground is where @statshub/ui-web is
looked at rather than used. It renders every shared component against its own
previews, and it holds a screenshotted map of every route in
web.
bun run dev:playground # http://localhost:3004
bun run build:playground
bun run birds-eye # re-shoot the route mapTwo tools, both live once it is running: the component workbench and Bird's Eye View. Those links resolve through the app registry, so they point wherever this environment's playground actually is.
What it does
Every shared component, rendered
One entry per file in packages/ui-web/src/components, each with the
previews that file exports. If a component is in the package, it is on this
page.
Live controls
A preview declares its controls, and the panel builds itself from them —
so a variant is checked by changing it, not by editing a file and waiting
for HMR.
Source, docs and examples beside the preview
Four tabs per component. /api/components/[slug] reads the real file off
disk at request time, so what you read is what ships.
Bird's eye view
Every route in web, screenshotted and wired into a map — the root page
first, then a chip per tool category, then each route's screenshot hanging
off the group it belongs to. It is how you see the shape of the product
without clicking through it. Click a card for what the run recorded: HTTP
status, how long the page took to settle, and the console errors it logged
while it rendered.
What is missing from the set
The catalog carries the shadcn component list beside ours, so a primitive the design system has not built yet shows up as a gap rather than as nothing at all.
How it is built
A component without previews fails the build
prebuild runs check:previews, which walks
packages/ui-web/src/components and requires exactly one
export const <name>Previews per file, with globally unique preview ids.
The gate is what stops the inventory quietly going stale as the package
grows.
The registry is generated, not written
predev and prebuild run gen-preview-registry.mjs, which writes
src/lib/component-previews.generated.ts — the static import list the
bundler needs, since a dynamic import of a workspace path would not be
traced.
The manifest is the whole contract
src/lib/birds-eye.ts is imported by two things that may never import each
other: scripts/shoot-web-routes.ts, which drives Playwright from Node, and
birds-eye-view.tsx, which draws in the browser. So the file touches
neither node:fs nor React.
A JSON file on disk, not a build artefact
The shot manifest is read at runtime, so a fresh screenshot run shows up on reload rather than after a rebuild.
Tools are addressable
src/lib/playground-tools.ts is the list. It held one thing for long enough
that the shell and the tool were the same component; adding a second one
means a route, a card in that list, and nothing else.
Layout
| Path | What is in it |
|---|---|
src/app/page.tsx | The component workbench |
src/app/birds-eye-view | The route map |
src/app/api/components/[slug] | Reads a component's source, docs and examples off disk |
src/lib/component-catalog.ts | Our components, and the shadcn set to compare against |
src/lib/birds-eye.ts | The manifest shape both the shooter and the canvas read |
scripts/shoot-web-routes.ts | Drives Playwright over web and writes the manifest |
Shooting the route map
bun run birds-eye opens every route in web once and writes
public/birds-eye/manifest.json beside the screenshots. It takes its default
target from the app registry, so it follows
WEB_ORIGIN or DEV_HOST without a flag — and --base-url still wins when you
want a preview or production.
bun run birds-eye --list # the route plan, opening nothing
bun run birds-eye --theme dark --full-page
bun run birds-eye --only /refereesThe route list is discovered
Every page.tsx under apps/statshub-web/src/app, minus the parallel-route
slots. A list written down here would go stale the first time somebody adds
a board, and the failure is a map that quietly omits it.
One entity page stands in for all of them
[view] and [scope] enumerate from the page's own generateStaticParams,
unioned with the catalogue in statshub-config.
Entity routes cannot enumerate, so each takes one sample off the sitemap the
API already serves — one team, one player, one fixture, one referee.
A build error is not a screenshot
The dev overlay paints instantly, holds still and contains no skeletons, so every settle check passes it. The first full run returned forty green ticks and forty identical pictures of "Module not found". The shooter now reads the overlay's shadow root after the shutter and marks those failed.
It imports the package, it does not copy it
@statshub/ui-web is a workspace dependency. A component is added to
the package with its previews, and it appears here —
there is nothing to register.