@atomicstrata/themes
Shared, framework-independent theming for AtomicStrata products: semantic CSS tokens, four canonical themes, self-hosted fonts, pre-paint theme selection, and reusable contract checks. Products keep their own layouts, components, navigation and state.
Releases before 0.4.0 remain MIT licensed (see "Licence"). 0.5.0 adds theme
families with a shared System/Light/Dark preference and a Scientific Clay Dark
palette, opt-in: a consumer that does not ask for them gets exactly 0.4.0's
behaviour. 0.5.1 writes the design-system compiler's ledger ratios truncated
to 10 decimal places, so the Node versions measured write the same ledger
(docs/design-systems.md, "Ledger ratios"); --check reports a ledger
written by 0.5.0 or earlier stale once, and compiling again rewrites it. 0.6.0
adds an optional compact appearance switch for family pages
(@atomicstrata/themes/appearance-switch, AppearanceSwitch in the React
entry, css/appearance-switch.css): a sun/moon button with an attached
chevron tab opening a System/Light/Dark and theme panel, mounted inline in a
slot of the page's top bar or title row (recommended) or fixed to a viewport
corner, sized from the root font size. It is opt-in: importing it changes no
page, and a consumer that does not mount it gets exactly 0.5.1's behaviour.
Its words are English, a corner is fixed to the viewport only when no
ancestor of its host is transformed, and its 44px targets reach a little past
what is seen (docs/reference.md, "Compact appearance switch"). 0.7.0
colours the switch's product preview from a design system compiled with
--bind-product-slot (compile again after updating), keeps an earlier
switch working when a remount is refused, and has the System hint name the
device's scheme. 0.8.0 adds external themes, opt-in: a compiler
(@atomicstrata/themes/theme-compiler, the atomicstrata-theme command)
that compiles token-only ext- theme sources, with optional materials, into
checked stylesheets, manifests and catalogs; a catalog run time for family
pages (@atomicstrata/themes/catalog, @atomicstrata/themes/catalog/prepaint,
useThemeCatalog); the authoring JSON Schemas; and css/materials.css, the
material role sheet (docs/external-themes.md, docs/reference.md §8 and
§9). A page that uses none of them keeps 0.7.0's behaviour, apart from the
compact switch's corrections listed on the documentation site's Versions
page. 0.8.1 fixes the switch's open panel under large text in a narrow
viewport: nothing in it scrolls sideways. 0.9.0 adds the
atomicstrata-visual-compare command (Live preview, Screenshot comparison and
inspect, below), and makes every opted-in material card one stacking context
from rest onward (docs/reference.md §9). The
full reference (token roles, API and runtime assumptions, versioning, limits
of the checks) is docs/reference.md, which ships in the package.
docs/integration-acceptance.md, also shipped, says when a product's
integration is finished: its intent, the material setup and load order, a
cascade audit, a coverage matrix and four separate evidence tracks
(appearance with material fidelity, accessibility, persistence and first
paint, application state). Installed packages and theme menu entries alone
never complete an integration.
skills/atomicstrata-theme-integration/SKILL.md is a short agent skill that
applies it; npm installs it with the package but not as an agent skill, so
copy or link it into the agent's skills directory and refresh it after an
upgrade.
Install
npm install @atomicstrata/themes
The package has no runtime dependencies (React is an optional peer, used
only by the @atomicstrata/themes/react subpath). Its JavaScript is ES modules plus
two classic scripts, and its stylesheets and fonts are plain files: a bundler
resolves the exports in the table below, and a page with no build step serves
copies of the installed dist/ and css/ directories (serving the fonts'
.woff2 files as binary, font/woff2), as the example under
"Using it" does from /assets/themes/.
What is in the package
| Export | What it is |
|---|---|
@atomicstrata/themes | createThemeController, getThemeController, bindThemeSelect, the registry constants and the token-name lists |
@atomicstrata/themes/prepaint | prepaintSource(config): a classic script, as a string, for a same-origin file or an inline splice |
@atomicstrata/themes/prepaint.global.js | the prebuilt classic script, configured by data-* attributes on its own tag |
@atomicstrata/themes/prepaint-mode.global.js | the same with the optional System/Light/Dark preference (data-mode-storage-key) |
@atomicstrata/themes/prepaint-family.global.js | 0.5.0: the same with theme families (Scientific Clay, Minimal, Nebula, the product) sharing that preference (data-product-modes) |
@atomicstrata/themes/checks | colour arithmetic, a token-stylesheet reader and contract checks that take their policy as arguments |
@atomicstrata/themes/theme.css | ordered entrypoint: fonts, safe defaults, the four theme maps, focus and reduced-motion foundations |
@atomicstrata/themes/css/* | every stylesheet and font by path, including the optional layers below and theme.bundle.css, the entrypoint flattened with no @import |
@atomicstrata/themes/design-system | design-system scope helpers: designSystemAttributes, applyDesignSystemScope, recipeAttributes and the attribute and layer names |
@atomicstrata/themes/design-system/compiler | Node, build time: profile validation, the compiler and pinned acquisition (also the bin atomicstrata-design-system) |
@atomicstrata/themes/catalog | optional, 0.8.0, external themes (see below): createThemeCatalogController, getThemeCatalogController, the external-theme catalog controller for family pages; see docs/external-themes.md |
@atomicstrata/themes/catalog/prepaint | 0.8.0, external themes, build time: prepaintCatalogSource, the catalog page's generated pre-paint script (external file or inline) |
@atomicstrata/themes/theme-compiler | 0.8.0, external themes, Node, build time: compile token-only external theme sources (ext-… ids) into stylesheets, manifests and catalogs, and check them (also the bin atomicstrata-theme); see docs/external-themes.md |
(repository only) examples/theme-packs/ | nine example external theme packs, each light and dark with a reference brief, compiled into one example catalog: the three curated candidates (Editorial, Graphite and Instrument, unfinished) and six expressive packs after named styles; authored with docs/theme-authoring.md, not packed and not published |
(its own package) packages/theme-packs/ | @atomicstrata/theme-packs: the six expressive packs compiled into the catalog expressive by the published 0.8.0 compiler, with an author starter; data only, documented in its README and in docs/theme-pack-distribution.md |
@atomicstrata/themes/schemas/theme-source.schema.json, …/theme-manifest.schema.json, …/theme-catalog.schema.json | 0.8.0, external themes: JSON Schemas for editing the three documents, as editor aids (the validators decide) |
@atomicstrata/themes/appearance-switch | optional, 0.6.0: mountAppearanceSwitch, a compact sun/moon switch with a theme and appearance panel for family pages, inline in a slot of the page's top bar or title row, or fixed to a viewport corner (top right by default) |
@atomicstrata/themes/react | optional: DesignSystemScope, useDesignSystemAttributes, (0.5.0) useThemeAppearance, (0.6.0) AppearanceSwitch and, for external themes, (0.8.0) useThemeCatalog |
@atomicstrata/themes/design-systems/strata/profile.json | the Strata design-system profile |
@atomicstrata/themes/design-systems/strata/recipes.json | the Strata component recipe document (package-authored, opt-in) |
(bin) atomicstrata-visual-compare | 0.9.0, Node, local: one loopback address (serve; preview is the same command) with two modes, a Live preview of one synthetic dashboard under two independently chosen themes, optionally with a compiled catalog's themes, which is reference HTML, not evidence; and a Screenshot comparison gallery for your own saved screenshots across themes, OS modes and versions (side by side, a reveal slider and an exact pixel difference), with a check that they are still the captured bytes (init, verify, serve --config); inspect drives your own running app in Chromium, with your own Playwright, and writes a dataset the gallery shows; not an export, and nothing in the runtime imports it; see docs/visual-comparison.md |
Visual comparison. With the package installed in your project
(npm install @atomicstrata/themes, above), npx atomicstrata-visual-compare serve needs nothing else: it opens one address with two modes. In a project
without the package, npx does not find this bin; it looks for a different
package named atomicstrata-visual-compare. Live preview serves one synthetic
dashboard live in two panes, each with its own theme and appearance
(--catalog <index> adds a compiled catalog's themes, such as an installed
@atomicstrata/theme-packs); it is reference HTML, not screenshot evidence.
Screenshot comparison shows your own saved screenshots once a configuration
names them: npx atomicstrata-visual-compare init visual-review creates a
project with a synthetic example dataset (four generated images, not product
evidence), and serve --config visual-review/visual-compare.config.json shows
it beside Live preview. You supply your own screenshots, captured with your own
tools; the gallery counts pixel differences and makes no accessibility or
approval judgement. docs/visual-comparison.md, which ships in the package,
covers both modes, the configuration, the dataset format and capturing.
External themes (the catalog, catalog/prepaint, theme-compiler and
schemas rows, useThemeCatalog and the atomicstrata-theme bin) are new
in 0.8.0: installing 0.7.0 or earlier from npm does not supply them. The
documentation site's Versions page records each release's status.
Design systems (an external vocabulary such as the Strata registry's, its
brands and a one-way bridge onto the contract tokens) are described in
docs/design-systems.md, which also ships in the package. A system can
also be shown in the theme appearance (data-design-appearance="theme"):
its unchanged components take the canonical theme in force, beside the
native appearance. From 0.4.0, an os system may also allow a pinned
System/Light/Dark choice (designMode on the controller, §6 of the guide),
and a product's own markup can opt into a system's component recipes
(data-design-recipe: actions, fields, cards, status and text roles, with
checked contrast and labelled accessibility overrides, §7), compiled for the
product root and native regions only unless asked otherwise.
A whole application in one design system (for example Strata, offered in the theme menu beside Scientific Clay and Minimal) is supported with no system-specific theme id. In outline:
- Compile the system with
--bind-product-slot. - Write
data-design-system,data-design-brandanddata-design-appearance="theme"on<html>in the served markup. - Offer the
productid, labelled with the system's name (data-product-label="Strata"). Keep the existing storage key, and state the default explicitly. - Declare the cascade layer order before any other stylesheet.
Under product the root is the native system; under a canonical theme its
components adapt to that theme. The full recipe is "A whole application in one
system" in §5 of docs/design-systems.md. It also covers fonts, popups, and
what stays the product's work: spacing, sizes, hardcoded CSS values and the
brand's known contrast exceptions. A system on the root without
data-design-appearance="theme" under a canonical theme is the mixed state
of §4, not the consistent whole-application recipe.
Optional stylesheets, never loaded by theme.css:
css/typography-root.css: the themed body font at 16px/1.55 on the root.css/control.css: material for the labelled theme<select>.css/appearance-switch.css(0.6.0): material for the compact appearance switch.css/materials.css(0.8.0): the material role sheet, for elements markeddata-theme-material="<role>"(docs/reference.md§9). Link it aftertheme.cssand before any statically linked theme stylesheet;docs/integration-acceptance.mdcovers the order, opt-ins and evidence.css/ambient/scientific-clay.css: Scientific Clay's ambient glow, on elements markeddata-theme-ambient="page"anddata-theme-ambient="frame".css/adapters/llmwiki-public.css: the llmwiki viewer's public names (--bg-shell,--fg-body,--accent,--ok…), defined from semantic tokens, with the Nebula shades that have no semantic name kept as literals. Do not load it beside another vocabulary that uses--accentor--border.css/profiles/commonware-workbench.css: the Commonware workbench's values for Scientific Clay and Minimal.
Themes and tokens
Theme ids are scientific-clay, minimal, nebula-light and nebula-dark,
stamped as data-theme on the document root. Only minimal follows the
operating system's light or dark setting. A product may add one more id,
product, for its own palette, which it declares in its own stylesheet under
[data-theme="product"]; the package ships no such rule. Ids are stored in
people's browsers and are permanent API.
Every theme defines every token in TOKEN_NAMES. Each status has three roles:
--status-<s> is a decorative accent for marks, bars and icons and carries no
contrast promise; --status-<s>-text is the ink for words; --status-<s>-soft
is the tinted fill that ink sits on. Product token families (graph, gate,
category, workflow) stay with the product, declared under the same
[data-theme="…"] selectors.
Using it
<!-- before the first stylesheet -->
<script src="/assets/themes/prepaint.global.js" data-storage-key="acme.app.theme.v1"></script>
<link rel="stylesheet" href="/assets/themes/css/theme.css">
<label class="theme-control"><span>Theme</span><select data-theme-select></select></label>
import { bindThemeSelect, getThemeController } from "@atomicstrata/themes";
const release = bindThemeSelect(document, getThemeController());
Script attributes: data-storage-key="…" or the bare data-session-only
(exactly one), and optionally data-themes="scientific-clay minimal",
data-product-label="Acme paper", data-default="minimal".
With a build step, generate the script instead. This form also supports a one-time migration from an older preference key; every migration target must be one of the offered themes:
import { prepaintSource } from "@atomicstrata/themes/prepaint";
const script = prepaintSource({
storageKey: "acme.app.theme.v1",
themes: ["scientific-clay", "minimal", "nebula-light", "nebula-dark"],
legacyPreference: { key: "acme-old-theme", values: { light: "nebula-light", dark: "nebula-dark" } },
});
Write the string to a same-origin .js file, or splice it inline right after
<head> and allow it by hash. All three deliveries are built from the same
functions in src/boot-core.ts.
Behaviour worth knowing:
storageKeyis always stated: a non-empty string you choose, ornullfor a choice that lasts for the page. The package never invents a key.- An invalid configuration throws before storage is read or the document is touched; the page then renders on the stylesheet's safe
:rootdefaults. - A stored id that is unknown, too long or not offered resolves to the default. Storage that throws is treated as unusable: the default applies, nothing is migrated and nothing is written.
- Selecting a theme sets
data-themeon the root and writes one key. It fetches nothing, navigates nowhere and does not reach into frames. bindThemeSelectreturns a release function, and binding the same select again replaces its earlier binding. For React, usecontroller.subscribewithuseSyncExternalStore.- Without a bundler, serve the whole
dist/directory: the module entry imports its sibling files by relative path. A page that serves a closed list of files instead needs every moduledist/index.jsimports, listed indocs/reference.md("Runtime assumptions"); from 0.5.0 that includesdist/family-core.js, even on a page that never asks for families. Each classic script (prepaint.global.js,prepaint-mode.global.js,prepaint-family.global.js) is a single file. Thecss/directory must keep its structure too, because fonts are addressed relative to their stylesheets.
Embedded exported documents
Theming never styles a frame's contents. A page that embeds a fixed-appearance
document should say so itself: give the document its own
<meta name="color-scheme">, and set color-scheme on the iframe element
(for a light export, color-scheme: light), so the application's scheme does
not reach the embedded canvas or native controls. That rule belongs to the
embedding product; the playground's content.css shows it.
Develop
Needs Node 22.18 or later (tests run TypeScript directly).
npm install
npm test # build, then node --test over tests/*.test.ts
npm run typecheck
npm run codehealth # tests with measured coverage, then fallow dead-code, dupes and health
npm run vendor:fonts # recopy fonts and regenerate notices from the pinned @fontsource packages
cp .env.example .env # playground host and port; no defaults are built in
npm run playground # http://127.0.0.1:4319/
npm run playground:screenshots # needs the server running; writes playground/screenshots/
npm run fixtures:prepare # pack the package and build the two consumer fixtures from that tarball
npm run test:browser # Playwright acceptance against the built fixtures
npm run visual:gallery # saved screenshot comparison gallery, http://127.0.0.1:4320/
npm run visual:check-distribution # pack, install in a scratch consumer, drive the installed gallery in Chromium
npm run design-systems:acquire # pinned private Strata brand files via gh (organisation access)
npm run test:strata # acquire, then the Strata profile against the pinned files
npm run design-systems:prepare # after fixtures:prepare: build the plain and React galleries from the tarball
npm run design-systems:serve plain # http://127.0.0.1:4351/ (react: 4352)
npm run test:design-systems:browser # both galleries in Chromium, Firefox and WebKit
npm run docs:dev # documentation site (docs-site/, Mintlify) preview, http://localhost:4360/
npm run docs:check # generated reference pages current, mint validate and broken links
The React gallery also needs the registry's components:
node fixtures/design-systems/react/scripts/vendor-registry.mjs (gh, pinned
commit; bytes stay git-ignored).
npm run visual:gallery serves the repository's own gallery of saved product
screenshots across themes and theme versions, starting with the llmwiki
migration evidence; see tools/visual-comparison/README.md. That evidence and
the capture harnesses stay in the repository and are not packed. The gallery
itself ships as the atomicstrata-visual-compare command, described above.
The playground shows one content fragment in three layouts (reader, workbench,
product), each loading the package through a different pre-paint delivery
under a strict content security policy. The fixtures under fixtures/ install
the packed tarball into a plain-JavaScript site and a React/Vite build.
Verification
Three layers, each run from this repository:
- Unit and contract tests (
npm test): the resolver's behaviour as one table run against the module entry, the generated script and the prebuilt script; rejected configurations leaving the page untouched; the select binding; every theme complete and its text roles readable by arithmetic; reference direction; the viewer drop-in comparison against unmodified upstream stylesheets; font bytes and notices; and the documentation's own examples. - Packed-artifact fixtures (
npm run fixtures:prepare): one tarball is packed and installed into a plain-JavaScript site and a React/Vite production build. Neither imports from this source tree. - Browser acceptance (
npm run test:browser): Playwright against both built fixtures, in the Chromium, Firefox and WebKit builds it pins. - Design-system galleries (
npm run test:design-systems:browser): the same tarball in a plain-HTML Strata and Lumen site and a React/Tailwind site with the Strata registry's twelve components in every brand, compared with registry-only reference pages, in the same three engines.
Results are not written into this file, because it ships inside the package it
would be describing. In the repository, the dated report under
evidence/verification/ records the commands run, their results, the tarball
digest and what was left unverified. The repository's tech-debt.md lists
known compromises. Neither is part of the published package.
Licence
From version 0.4.0, including this version (0.9.0), @atomicstrata/themes is free for noncommercial use under
the PolyForm Noncommercial License 1.0.0
(LICENSE, the official text, unmodified). The licensor is Supernet Inc.,
doing business as Atomic Strata.
Required Notice: Copyright (c) 2026 Supernet Inc., doing business as Atomic Strata
- Noncommercial use is permitted on the licence's terms, which define
it: any noncommercial purpose, personal uses without any anticipated
commercial application (its "Personal Uses" section), and use by a
charitable organisation, educational institution, public research
organisation, public safety or health organisation, environmental
protection organisation or government institution regardless of funding
(its "Noncommercial Organizations" section). Read
LICENSEfor the exact wording. - Commercial use outside the permitted purposes above requires a separate written, paid agreement with Supernet Inc., doing business as Atomic Strata. Contact [email protected]. Installing the package or making a payment without that agreement does not grant additional commercial rights.
- Earlier releases stay MIT. Every version published before 0.4.0 (0.0.0-stage, 0.1.0 and 0.3.0) was released under the MIT License and remains under it. Material as published in those versions, including where it appears unchanged in 0.4.0 or later, stays available under those MIT terms. Supernet Inc.'s original contributions first published in 0.4.0 or later are offered under PolyForm Noncommercial unless separately licensed, including through a commercial agreement. Third-party material retains its own terms.
- Third-party MIT material. The theme values and mechanisms derived from
the MIT-licensed llm-wiki-compiler viewer keep that project's MIT notice.
It and this package's earlier MIT licence are the same text byte for byte,
shipped in full as
LEGACY-MIT-LICENSE.txt. - Fonts stay under the SIL Open Font License 1.1, as below.
The license field in package.json is
(PolyForm-Noncommercial-1.0.0 AND MIT AND OFL-1.1): one package containing
material under each of the three. This section describes the arrangement; it
is not legal advice.
Provenance
The theme values and mechanisms derive from the MIT
llm-wiki-compiler viewer; tests/upstream/llm-wiki-compiler/ holds unmodified
copies of its theme stylesheets, which tests/viewer-drop-in.test.ts compares
against. Fonts are SIL Open Font License 1.1, copied unmodified from pinned
@fontsource packages; css/fonts/manifest.json records each file's source
and digest, THIRD_PARTY_NOTICES.txt carries each package's full licence
file, and a test checks the thirteen files are byte-identical to the viewer's. Source
commits and digests for the design review are in
evidence/source-snapshots/2026-10-11-design/SOURCES.md.
Cross-product architecture and migration plans live in am-research-internal.