@rx-ventures/quiz-runtime
v0.5.4
Published
Renders a published Atlas quiz. One component, one stylesheet, no Tailwind in your repo.
Readme
@rx-ventures/quiz-runtime
A published Atlas quiz, rendered. One component, one stylesheet, no Tailwind in your repo.
Public on the registry so two private repositories can both install it. That is
a distribution decision, not a licence — UNLICENSED, no rights granted, and
it is built for one product's data model rather than for general use.
npm i @rx-ventures/quiz-runtimeimport { QuizRunner, fetchAtlasQuiz } from "@rx-ventures/quiz-runtime";
import "@rx-ventures/quiz-runtime/styles.css";
import { notFound } from "next/navigation";
export default async function Page({
params,
}: {
params: Promise<{ handle: string }>;
}) {
const { handle } = await params;
const quiz = await fetchAtlasQuiz({
base: process.env.ATLAS_URL!,
org: process.env.ATLAS_ORG!,
brand: process.env.ATLAS_BRAND!,
key: process.env.ATLAS_KEY!,
handle,
});
if (!quiz) notFound();
return (
<main className="flex min-h-svh flex-col">
<QuizRunner quiz={quiz.document} theme={quiz.theme} />
</main>
);
}That is the whole integration. The min-h-svh column is the one thing worth
copying: the quiz fills the height it is given, so a page that gives it none
leaves the footer under the last question instead of at the foot of the screen.
Why it lives in Atlas's repository
Atlas is the builder. It is also a consumer of this package — its Simulate tab
and its /run page import from here exactly as a storefront does.
That is not tidiness. A builder and a renderer that disagree produce a quiz that previews one way and runs another, and the only structural defence is for there to be one renderer with the builder importing it. If Atlas ever stops importing this package, that defence is gone.
What ships
| Entry point | What |
| -------------------------------- | -------------------------------------------- |
| @rx-ventures/quiz-runtime | QuizRunner, fetchAtlasQuiz, QuizDocument, AtlasTheme |
| @rx-ventures/quiz-runtime/styles.css| The compiled, scoped stylesheet |
| @rx-ventures/quiz-runtime/document | The quiz model — types, schemas, predicates. No React. |
| @rx-ventures/quiz-runtime/react | The renderer in pieces, for Atlas's builder |
| @rx-ventures/quiz-runtime/quiz.css | The quiz's own rules, uncompiled, for a Tailwind consumer |
The first two are the promise. Everything in index.ts can be added to and
nothing taken away. The other three move with the renderer — depend on them the
way Atlas does, in step, or not at all.
QuizRunner({ quiz, theme })
Takes both, because a document without a theme is the right words at the wrong
size in the wrong typeface. It injects the @font-face rules and the type
scale itself, so there is nothing to remember to mount alongside it.
fetchAtlasQuiz({ base, org, brand, key, handle })
Resolves the pointer, then the document and theme together. Returns null when
nothing is published at that address — a 404 you should answer with your own —
and throws on anything else, because a 500 rendered as "not found" sends people
looking for something that is in fact there.
The cache hints mirror Atlas's own Cache-Control: 30s on the pointer and the
theme, forever on the document, which is written once at a version and never
edited.
key is server-side only. Issue it in Atlas under Settings → Brands; it is
shown once and stored there only as a hash, so a lost key is re-issued rather
than recovered. It both authenticates the request and identifies the brand —
Atlas resolves the brand from the key and refuses if org/brand disagree, so
a key cannot read a brand it was not issued for.
Call this from a server component, a route handler or a build step. Never from code that reaches a browser: anything in a client bundle is public. Atlas sends no CORS headers at all, so a browser cannot make the request even holding a key — that is deliberate, not an oversight to work around.
The stylesheet
styles.css is compiled from Tailwind and then scoped: every selector is
prefixed with .ay-quiz, :root and html are rewritten to it, and the
cascade layers are flattened so the rules compete on specificity rather than
losing to any unlayered rule in your page. Preflight is included, scoped the
same way, so the quiz gets the reset its utilities were designed against
without touching anything outside it.
You do not need Tailwind, @base-ui/react, a --muted-foreground, or a cn.
scripts/check-dist.mjs asserts the scoping and the surviving "use client"
directives on every build, because both failures are invisible here and only
appear in your repository.
Isolation runs both ways. Scoping stops the package's rules escaping; a second
set of resets on .ay-quiz stops the host's typography inheriting IN, and a
:where() reset handles host rules that name an element directly — a base
layer setting letter-spacing on h1–h6 reaches quiz headings without
inheriting through anything. Both are kept at one class of specificity, so the
quiz's own utilities still win.
Three things stay global, because CSS gives them no scope:
- the
@font-facefamilies a brand registers — a host with the same family name and different files is a coin toss; @property --tw-*registrations, which a host running Tailwind v4 also declares (identically, today);text-decorationset on a host ancestor, which propagates to descendants and cannot be removed by them.
@keyframes would be a fourth and is namespaced ay-quiz-* instead.
Dark mode is off. A quiz renders in its brand's palette; following the host page's night mode would be the brand rendering as something else.
Version skew
An author can publish a block kind older runtimes have never heard of. That renders as a placeholder and the rest of the quiz carries on — the unknown block captures nothing, so nothing waits on an answer that cannot be given. Keeping up to date is still the right thing; it just isn't load-bearing.
Working on it
pnpm --filter @rx-ventures/quiz-runtime build # tsc, then the scoped CSS, then the checks
pnpm --filter @rx-ventures/quiz-runtime dev # tsc --watch, for editing alongside AtlasAtlas consumes dist, so a change here needs a build before Atlas sees it —
pnpm dev and pnpm build at the repository root do that for you.
Atlas does not load styles.css. It scans this package's source with its
own Tailwind (@source in src/app/globals.css) so the builder's chrome, its
tokens and its theme toggle stay coherent around the quiz. The rules no utility
expresses live in quiz.css, which both sides import — one copy, no drift.
