Sim-specific context for AI assistants. General SceneryStack guidance: OpenPhysics/.github/CLAUDE.md.
Reusable SceneryStack template (one or N screens) and canonical accessibility reference for
OpenPhysics sims. Prefer Baton/scripts/create-sim.sh (or GitHub Use this template +
npm run rename + npm run scaffold-screens) to fork it. For multi-screen sims, see
doc/multi-screen.md.
| File | Purpose |
|---|---|
src/SimColors.ts |
All ProfileColorProperty instances |
src/SimConstants.ts |
Named numeric constants (layout px, physics SI units) |
src/SimNamespace.ts |
Namespace for color property names |
src/i18n/StringManager.ts |
Singleton localized string accessor |
src/sim-screen/SimScreen.ts |
Screen wrapper |
src/sim-screen/model/SimModel.ts |
Simulation state and logic |
src/sim-screen/view/SimScreenView.ts |
Visual nodes, layout, screenSummaryContent + pdomOrder |
src/sim-screen/view/SimScreenSummaryContent.ts |
Accessible screen summary (reference a11y pattern) |
src/sim-screen/view/SimKeyboardHelpContent.ts |
Keyboard-help dialog content |
src/common/SimPanel.ts |
Pre-themed Panel wrapper (uses SimColors automatically) |
src/common/SimButtonOptions.ts |
Flat button-appearance option bundles + light-control-surface combo-box options |
src/common/TimeModel.ts |
Composable play/pause + elapsed-time model for animated sims |
scripts/generate-icons.ts |
PNG icons from public/icons/icon.svg |
scripts/rename-sim.ts |
Sim-level fork/rename (package id + metadata, Colors, Constants, Panel, ButtonOptions, Preferences) |
scripts/scaffold-screens.ts |
Emit N screen packages + wire main/strings/icons |
Every control panel and info box in the sim should use SimPanel so that
default/projector color switching is automatic:
import { SimPanel } from "../../common/SimPanel.js";
const panel = new SimPanel(content); // uses SimColors defaults
const panel = new SimPanel(content, { xMargin: 20 }); // override any PanelOptionFor simulations with animation, compose TimeModel into your screen model:
import { TimeModel } from "../../common/TimeModel.js";
export class MyModel implements TModel {
public readonly timer = new TimeModel(); // starts paused; pass true to auto-play
public step(dt: number): void {
this.timer.step(dt);
// use this.timer.timeProperty.value for physics
}
public reset(): void { this.timer.reset(); /* … */ }
}Wire the view to TimeControlNode from scenerystack/scenery-phet binding on
model.timer.isPlayingProperty.
SceneryStack's push/round buttons default to a 3-D/beveled look; every button in the sim should be flat instead. Spread these into the relevant options object:
import { FLAT_RESET_ALL_BUTTON_OPTIONS, FLAT_RECTANGULAR_BUTTON_OPTIONS } from "../../common/SimButtonOptions.js";
const resetAllButton = new ResetAllButton({ ...FLAT_RESET_ALL_BUTTON_OPTIONS, listener: () => {...} });
const exampleButton = new RectangularPushButton({ ...FLAT_RECTANGULAR_BUTTON_OPTIONS, content, listener });FLAT_PLAY_PAUSE_STEP_BUTTON_OPTIONS spreads into TimeControlNode's playPauseStepButtonOptions;
TIME_CONTROL_SPEED_RADIO_OPTIONS fixes TimeControlNode's speed-radio label color, which
otherwise defaults to black text on the sim's dark default-mode panels. SIM_COMBO_BOX_OPTIONS
themes a ComboBox's button/list chrome to the light control surface below; pair item labels
with LIGHT_SURFACE_TEXT_FILL (not SimColors.textColorProperty, which is for panel-fill text).
SimColors.ts backs this with a "light control surfaces" section —
controlSurfaceColorProperty, controlSurfaceDisabledColorProperty,
controlSurfaceTextColorProperty — identical white/dark-text values in both default and
projector profiles, so any component that must stay light regardless of theme (combo boxes,
flat buttons, editable fields) keeps readable contrast automatically.
This template is the canonical accessibility reference for OpenPhysics sims. It ships with
the three required layers wired up: PDOM names, a SimScreenSummaryContent, and an explicit
pdomOrder + SimKeyboardHelpContent. A11y strings live under the a11y key in each locale
JSON, exposed via StringManager.getA11yStrings(). When building a real sim, make
currentDetailsContent a live DerivedProperty over model state and add accessibleNames to
every interactive node. Full convention and checklist: Baton/ACCESSIBILITY.md.
A clean fork of this template rarely needs compliance carve-outs — root SimConstants.ts,
*Colors.ts, *Namespace.ts, standard screen layout, and full a11y wiring pass Baton's
compliance check out of the box. Document carve-outs in the forked sim's CLAUDE.md only when
you introduce a deliberate deviation (nested constants, hardcoded interaction fills, etc.).
JSON cannot carry comments, so the rationale for forced transitive pins lives here. Prefer
tilde (~) or exact versions — caret (^) lets minors drift under what is meant to be a
hard pin. Dependabot ignores these three names (see .github/dependabot.yml) so it does not
open PRs that fight the overrides. Revisit when SceneryStack drops or re-pins them upstream.
| Override | Pin | Why |
|---|---|---|
lodash |
~4.18.1 |
SceneryStack declares ~4.17.12. Bump clears Dependabot/npm advisories patched in 4.18.x (e.g. GHSA-r5fr-rjxr-66jc, GHSA-f23m-r3pf-42rh). |
three |
~0.125.2 |
SceneryStack declares ^0.104.0. Floor is 0.125.0 for GHSA-fq6p-x6j3-cmmq (ReDoS). Staying on the 0.125 line avoids a larger API jump; 0.125.x still has open CVEs (e.g. XSS GHSA-7vvq-7r29-5vg3, fixed only in ≥0.137.0). Remove this override if/when SceneryStack stops depending on three or pins a patched line itself. |
brace-expansion |
~5.0.9 |
Transitive via vite-plugin-pwa / Workbox. Clears npm audit (originally GHSA-mh99-v99m-4gvg; keep ≥5.0.9 for GHSA-rgw5-rvv9-x895). |
Fleet-standard Vitest layout (keep when forking):
| Path | Purpose |
|---|---|
vitest.config.ts |
happy-dom environment; setupFiles: ["./tests/setup.ts"]; execArgv: ["--expose-gc"] |
tests/setup.ts |
Canvas / AudioContext mocks + init({ name: "…" }) before SceneryStack imports |
tests/TimeModel.test.ts |
Sample model unit tests — replace with real physics tests |
tests/memory-leak.test.ts |
WeakRef + forceGC dispose regression (fleet pattern) |
tests/fuzz/fuzz.spec.ts |
Optional Playwright fuzz smoke via joist ?fuzz |
playwright.config.ts |
Chromium project + Vite webServer for fuzz |
- Put unit tests only under root
tests/, mirroringsrc/(never co-locate or use__tests__/). - Change the
namepassed toinit()intests/setup.tsto matchpackage.jsonafternpm run rename. - Run
npm test. CI runs the suite when atestscript is present. - Expand
memory-leak.test.tsfor any component that adds/removes nodes or links Properties at runtime (see OpticsLab for a deep suite). - Optional:
npm run test:fuzz/test:fuzz:quick/test:fuzz:long(not part of default CI). Duration is 30s by default; override withnpm run test:fuzz -- 90orFUZZ_DURATION=90.
npm run lint && npm run check && npm run build && npm test| Command | Description |
|---|---|
npm start / npm run dev |
Vite dev server |
npm run build |
Type-check + production build |
npm run build:single |
Single-file build mode |
npm run check |
TypeScript (tsc --noEmit + scripts project) |
npm run lint / npm run fix |
Biome check / auto-fix |
npm test |
Vitest unit tests |
npm run test:fuzz |
Playwright fuzz smoke (?fuzz&ea, 30s; npm run test:fuzz -- 90 to change) |
npm run test:fuzz:quick |
10s fuzz |
npm run test:fuzz:long |
300s fuzz |
npm run icons |
Regenerate PWA icons (+ placeholder screenshots) |
npm run rename |
Sim-level fork/rename (--id, --name) |
npm run scaffold-screens |
Emit N screens (--screens Intro,Lab) |
npm run release |
check && lint && build, then version patch + push tags |
npm run release intentionally skips npm test — template tests are samples. Real sims should append && npm test before the version bump.
Baton/scripts/create-sim.sh --repo Friction --name "Friction" --screens Intro,Lab --shared-model --onboardnpm install
npm run rename -- --id friction --name "Friction"
npm run scaffold-screens -- --screens Intro,Lab --shared-model
# omit --screens for one screen named after the sim; omit --shared-model for independent models
npm run fix # required: both scripts reorder imports, which Biome then sorts
npm run checkrename updates package id and metadata, display name, and every sim-level Sim*
(Colors, Constants, Namespace, Panel, ButtonOptions, Preferences, query parameters).
scaffold-screens owns screen folders (fleet naming: src/intro/, not intro-screen/).
After both steps no Sim* identifier should remain — grep -rn '\bSim[A-Z_]' src to confirm.
- Rename — replace
scenerystack-template/SceneryStack Template/Simprefix ininit.ts,brand.ts,package.json(name, description, keywords, repository.url), Colors/Constants/Namespace/Panel/ButtonOptions/Preferences - Screens — run
scaffold-screensor mirrorsim-screen/into kebab folders - Locale — add
strings_XX.json, register inStringManager, add locale toinit.tsavailableLocales - Icon — edit
public/icons/icon.svg, runnpm run icons; match theme color inindex.html/vite.config.ts - Colors — edit
*Colors.ts(default+projectorprofiles per property)
Full guide: doc/multi-screen.md
Summary:
- Prefer
npm run scaffold-screens -- --screens Intro,Lab(add--shared-modelfor a root model) - Or create a screen folder mirroring
src/sim-screen/for each screen (kebab names, no-screensuffix) - Add screen-name keys to all locale JSON files; nest
a11yper screen - Expose new getters in
StringManager.getScreenNames()/get{Screen}A11yStrings() - Shared state:
--shared-model→common/model/SharedModel.tscomposed per screen (rename to a domain type) - Add
src/common/{SimName}ScreenIcons.tswithcreate{Screen}Icon()factories; wirehomeScreenIcon+navigationBarIconon each Screen - Register all screens in the
screensarray inmain.ts
| Approach | When to use |
|---|---|
Baton/scripts/create-sim.sh |
Agents / fleet — create repo, rename, scaffold N screens |
| GitHub template ("Use this template") | Humans starting a sim in the browser |
npm run rename + scaffold-screens |
Same, after cloning the template |
| npm workspace / monorepo | Managing a suite of sims with shared tooling |
| git subtree for pulling updates | Keeping forks in sync with template improvements |
See doc/multi-screen.md → "Using this template beyond a direct copy" for details.
After npm run build, the sim is installable offline via Workbox (dist/manifest.webmanifest).