VelaWind · Projects

Lodestar

Interactive astrophysics, explained in seven layers you choose to open.
Live site · Source

A dark simulation page at mid-transit: a planet’s black disc sits centred on a pale star while the marker on the light curve below rests at the bottom of a 1.01% dip, beside sliders for the star’s mass and size, the planet’s size and its orbit, and readouts for transit depth, duration and orbital period.

The problem

An astrophysics topic has to reach two readers who share almost no vocabulary. Someone who has never studied physics needs an analogy and a picture before a symbol means anything at all. Someone who works in the field wants the derivation and the assumptions behind it, and stops trusting a page the moment it waves a hand at something they know is harder than that. Lodestar covers seven topics, and each one has to serve both readers from a single page.

Why the obvious approach did not work

The obvious answer is two versions of every topic, a beginner page and an advanced page, chosen by a toggle. Lodestar does not do that, because parallel texts drift. Every correction has to be made twice, the second copy is the one that gets missed, and after a few rounds of editing the two versions contradict each other on the same facts. The failure is quiet: nothing breaks, the pages just stop agreeing.

So there is one text per topic, written once, structured as seven layers that run from a one-sentence hook down to the open research questions. The depth setting in the header decides which layers start open, and that is all it does. It never changes a word. The mathematics is on every page, folded shut by default, which means the beginner is never lied to and the specialist never has to go looking for a different version of the site.

One trade-off, and what it cost

The glossary panel is portalled out of the React tree onto document.body and positioned fixed. That is not the natural way to build a tooltip, and it was forced: every layer body sits inside the accordion's overflow-hidden box, which is what makes the height animation read as a collapse, and it clips horizontally as well. A panel rendered next to the word it defines gets sliced off at the panel edge, and an absolutely positioned one is at the mercy of any ancestor that later gains a transform. Leaving the tree entirely is the only placement that cannot be clipped.

The cost is that the panel is no longer positioned or ordered relative to the word it belongs to, and both halves of that had to be paid for separately. Because it is pinned to the viewport rather than to the term, it has to re-measure on every scroll event and only treats a scroll as "the reader has moved on" once the page has been still for 200 milliseconds, which is also how it survives the smooth scroll that Tab uses to bring a term into view. Because it renders at the end of <body>, where nobody is reading, a screen reader would hear the expanded state and then nothing, so the definition is spoken through one persistent aria-live region created before the text exists and shared by all the terms on the page. A visual fix turned into an accessibility obligation, and the second part is the part that is easy to miss.

The rest of this page is fetched from the Lodestar README at build time and rendered here, so it is maintained in the repository and not in this site. It covers the architecture, the stack, how the project is verified, and, at the end under "Known limits", what the project still owes.

Architecture

Stack

Vite 5, React 18, React Router 6, TypeScript (strict, plus noUnusedLocals and noUnusedParameters), Tailwind 3, Zustand 5, Framer Motion 11, KaTeX, Canvas 2D. Deployed on Vercel.

Tests

npm test is 324 Vitest tests across 8 files, in a Node environment:

.github/workflows/ci.yml runs typecheck, lint, test and build in that order on every push and pull request to main, on Node 22.

The browser suite is separate: playwright.config.ts defines five projects (Chromium at two viewports, WebKit, Firefox, mobile WebKit) covering tooltip journeys, figures, axe accessibility passes, keyboard operability and the not-found route. It starts no dev server, so baseURL defaults to the live site and E2E_BASE_URL retargets it at a local npm run preview.

Known limits

Some files are very large and heavily commented: sanity.ts is 964 lines and the escape-velocity simulation 758, which is awkward for a second contributor to navigate. No unit test renders a React component, so component behaviour is covered only by the Playwright suite, which needs a live deployment; a component regression is invisible to npm test and to CI. Two topics named in Connections layers were never written.