Skip to content

feat(playground): a local app for running every hook against a real project - #17

Merged
Timonwa merged 11 commits into
mainfrom
feat/demo-app
Sep 1, 2026
Merged

Timonwa merged 11 commits into
mainfrom
feat/demo-app

Conversation

@Timonwa

@Timonwa Timonwa commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Adds apps/playground — a Next app that runs all 20 auth hooks against a live Firebase project, so a change to the library can be exercised before a PR goes up. Unit tests mock firebase/auth, so they prove orchestration and nothing about Firebase itself; this covers the other half.

It is never deployed. Config comes from .env.local only. Bring your own project.

What it looks like

Three panes per hook: the sidebar is the site map, the middle column explains the hook and shows the code, and the right column is a console you run it in — the hook's live loading and error, then the value it resolved to.

Options are settable

Every hook's own options are controls in an Options panel, and the snippet regenerates from them, so the code shown is the config the result came from. sendVerificationEmail, recaptchaSize, actionCodeSettings, storageKey, the OAuth provider, and formatErrorMessage are all live.

Callback options (onIdToken, onBeforeSignOut, onBeforeDelete) can be set to throw — the operation aborts, which is the whole reason they run inside the flow rather than after it.

Structure

One page per service at /auth, groups as sections on it. Each hook is its own component under components/auth/ — 20 files, averaging 68 lines. A new service is an entry in lib/hooks-map.ts plus a page at /[slug]; the shell reads that map and needs no other change.

Docs

Separately, getting-started never showed where auth comes from — no page in the docs called initializeApp or getAuth, and the example imported it from a file it never showed. Fixed, along with the provider section, which led with app-wide defaults and buried the actual reason the provider exists.

Checks

pnpm verify green — 109 tests, typecheck, lint, build, publint, attw. Docs and playground both build. CI builds the playground on every PR, so a broken export surfaces before publish rather than after.

No changeset: nothing here touches the published package.

The visitor supplies their own web config, kept in localStorage. usePhoneSignIn
sends real SMS and the email hooks consume quota, so a demo on the maintainer's
project would be a standing invitation to run up their bill.
@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
firebase-hooks Skipped Skipped Sep 1, 2026 11:14am UTC

Renames `apps/demo` to `apps/playground` and rewrites it as a
three-pane API-reference harness: sidebar site map, an explanation
and snippet in the centre, and a sticky console on the right showing
the hook's live `loading`, `error`, and resolved value.

The app is never deployed. It exists so a contributor can exercise a
change against a real Firebase project before opening a PR, which
unit tests cannot cover because they mock `firebase/auth`. Config
comes from `.env.local` only.

- one page per docs group, `useSignup` first — a fresh project has no
  accounts, so nothing else can succeed on a first run
- Shiki highlighting and a single token-based theme on
  `prefers-color-scheme`, replacing the mix of `dark:` and an unset
  `.dark` class
- `/auth/callback` and `/auth/action` for the flows Firebase redirects
  back into
- CONTRIBUTING gains a section on running it, and CI builds it so a
  broken export surfaces before publish
None of these were supported by anything — no measurement, no cited
source. Replaced with what the code or the page can actually show.

The playground carried a related one, a stale "roughly eighteen
lines" against a snippet the docs put at 21; that went with the
preceding commit.
The console showed what a hook returned but gave no way to change how
it was called, so the prose could describe an option with nothing on
screen to set it. Each hook now has an Options panel wired to its real
options, and the snippet beside it regenerates from those settings —
the code shown is the config the result came from.

Callback options can be set to throw, which is the behaviour worth
seeing: they run inside the flow, so throwing aborts the operation.

The error panel used to show three renderings of a failure with no
indication of how to get any of them. Each row is now labelled with
the formatErrorMessage config that produces it, and that option is a
control above it.

Also:

- wrap is one page-wide setting rather than per-response, and covers
  snippets too; responses are highlighted as JSON
- "Format errors" reaches the sidebar at last — the provider state
  existed and the README documented it, but nothing ever rendered it
- the sidebar tracks the section being read, via a scroll listener;
  IntersectionObserver only fires when the intersecting set changes,
  which leaves the highlight still through a tall section
- only the centre column sticks now. A sticky column taller than the
  viewport can never scroll to its own bottom, and the console is the
  one that grows with the result
The four group pages became one page at /auth, with the groups as
sections on it. A service is now an entry in lib/hooks-map.ts plus a
page at /[slug] — the sidebar reads that map, so adding Firestore or
Storage later touches nothing else.

Each hook is its own file under components/auth/ rather than a
function inside a growing page: 20 components averaging 68 lines.

Also:

- theme toggle — light, dark, or system. data-theme on <html> rather
  than prefers-color-scheme, since overriding the system is the point;
  an inline script resolves it before first paint, which an effect
  cannot do without flashing the wrong theme
- settings moved out of the sidebar's foot into a bar across the top.
  Below the hook list there was nothing to announce them, and no
  reason to scroll a nav you had already read
- the sidebar's brand block is fixed; only the list scrolls
- useOAuthSignIn gets provider and method selects. The provider is an
  argument to signIn(), not an option on the hook, so it belongs in
  Options with the snippet showing which one you picked
Not one of the 21 pages showed initializeApp or getAuth. Getting
started imported `auth` from '@/lib/firebase' behind a comment saying
"your initialised Auth instance" — pointing at a file it never showed,
which assumes the reader already knows the thing they came to learn.

Getting started now opens with that file, the console screen the four
config values come from, and why they are safe in client code. How
hooks work says outright that `auth` comes from the firebase package
rather than this one, for readers who land on a hook page first.

Also rewrites the provider section. It led with "optional, but it's
where app-wide defaults live" — the bonus first and the reason buried.
The reason is that nothing else subscribes to auth state, so without
it a sign-in on one page cannot tell a navbar on another. It now
leads with that and shows useAuth beside it.
@Timonwa Timonwa changed the title feat: demo app exercising every hook feat(playground): a local app for running every hook against a real project Sep 1, 2026
`pnpm typecheck` is recursive and the playground resolves the library
through its exports map, so on a clean checkout it fails with 27
"cannot find module @timonwa/firebase-hooks/auth" errors — dist does
not exist yet. Locally it passed only because a previous build had
left dist behind.

That made CONTRIBUTING's promise false: a green `pnpm verify` locally
did not mean a green PR, and a contributor cloning fresh would hit it
on their first run.

Verified by moving dist aside and running verify from clean.
The bar had five things competing for attention — project, status,
two labelled checkboxes with descriptions, and a three-button theme
group — so it was two rows tall and read as heavier than the page
under it. The project name now carries the weight, since every button
below it mutates that project; everything else recedes.

- settings move behind a gear popover, which gives the hints room to
  explain that a hook's own Options overrides the provider
- one theme button cycling light → dark → system. The label names the
  current state and the next, because an icon cannot say what a click
  will do
- lucide icons rather than ☀ ☾ ◐, whose optical weights differ by font
- the bar mirrors <main>'s box: padding outside the width cap, not
  inside it. Sharing an element, its padding ate into the cap and
  every item sat 24px inboard of the content below
- project and account stack below lg. Inline, truncation ate the
  account — the half you need before pressing Delete account

Below lg the sidebar is now an off-canvas drawer, opened from the bar
and closed by the ✕, the backdrop, Escape, or following any link.
Links close it themselves: they are in-page anchors, so the pathname
never changes and the drawer would otherwise sit over the section you
just jumped to.

It goes `invisible` when closed rather than only translating away —
a drawer parked off-screen still holds its links in the tab order.

Motion is one `--ease-panel` token shared by the drawer, backdrop, and
popover, on transform and opacity only, with a global
prefers-reduced-motion rule the app had been missing.
Format errors and Wrap code were plain useState, so every reload threw
them away — only the theme was stored.

The write lives in the setter rather than a second effect. Keyed on the
value, that effect runs in the same commit as the read with the
fallback still in hand, so it writes the fallback over what was stored;
Strict Mode then re-runs the read, which picks up the value it just
clobbered. It would have half-worked in a build and looked completely
dead in dev.

Reading stays in an effect: these pages are prerendered, and reading
storage during render would disagree with the server's HTML.
@Timonwa
Timonwa merged commit 583ede3 into main Sep 1, 2026
4 checks passed
@Timonwa
Timonwa deleted the feat/demo-app branch September 1, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant