Architecture
hintbeam is built in layers. Each layer depends only on the ones below it, so a new platform, router, path style or add-on is an addition — never a fork.
┌────────────────────────────────────────────────────────────────────────────┐
│ plugins analytics · targeting · path styles · add-ons │
├────────────────────────────────────────────────────────────────────────────┤
│ routers next · react-router · expo-router · react-navigation · own │
├──────────────────────────────┬─────────────────────────────────────────────┤
│ web platform │ native platform │
│ React DOM, Next.js, Vite, │ React Native, Expo │
│ Expo web │ │
├──────────────────────────────┴─────────────────────────────────────────────┤
│ react TourProvider factory · useTarget · useTour · useTourStep │
├────────────────────────────────────────────────────────────────────────────┤
│ core tours & validation · targets · finding · player · │
│ progress · path styles · card layout · plugin host │
│ (pure TypeScript, no React, no platform) │
└────────────────────────────────────────────────────────────────────────────┘core
Pure TypeScript with injected time, storage and measurement. Every rule — validation, the four-way "where is the target" decision, how steps end, skip and resume, the progress merge, path geometry, card placement — is unit-tested here without a screen.
ElementRegistryholds refs, never positions. Measuring happens only when a step asks.resolveTargetturns a target name intovisible/offscreen/otherScreen/notFound.TourPlayeris the state machine: start, next, back, skip, stop, tap, arrive, event, timeout.- Path styles are pure functions
(from, to) => Route; every renderer draws any route.
react
createTourProvider(platform) builds a TourProvider for a platform. The hooks are shared: they
only ever talk to the core and to the platform interface.
platforms
A platform implements TourPlatform:
interface TourPlatform {
name: string;
measurable(ref): Measurable; // where is this element, in viewport coordinates
defaultScrollArea?(): ScrollArea | null; // the page, on the web
TapObserver: Component<{ onTap(point) }>; // see taps without blocking them
StepUI: Component<{ view, theme, path }>; // the built-in card and light
useLayoutChanges(invalidate): void; // resize, rotation
useKeyboard?(actions): void; // shortcuts
}hintbeam resolves to the right platform automatically through the package's exports
conditions (react-native → native, everything else → web). To add a platform — Vue, Svelte, a
canvas — implement this interface (or reuse hintbeam/core directly with your framework's own
reactivity).
routers
Each adapter is a few lines over useRouterAdapter(screen, navigate). Router packages are optional
peer dependencies: an app installs only the one it uses.
plugins
composePlugins merges plugins into one host: path styles are added to the registry; events fan out
to every plugin; canStart requires every plugin to agree. A failing plugin is reported and ignored —
the open-source behaviour always wins.
Design rules
- Measure, never remember. Positions are read when needed and re-read when anything moves.
- Never point at nothing. Every outcome of finding a target has an honest UI.
- A companion, not a modal. The app stays usable; tapping the real button is a valid step.
- Data first. Tours are validated data, the same in code and in JSON.
- Add, don't fork. Platforms, routers, paths and plugins plug in; none edits the core.