Skip to content

feat: the guided tour library - #1

Merged
eumaninho54 merged 80 commits into
mainfrom
feat/guided-tour
Aug 27, 2026
Merged

feat: the guided tour library#1
eumaninho54 merged 80 commits into
mainfrom
feat/guided-tour

Conversation

@eumaninho54

Copy link
Copy Markdown
Member

What changed

Everything: the library itself, the example app, the docs website and the release
tooling. main is a bare initial commit, so this branch is the whole project.

The tour dims the screen, cuts a hole around one target and puts a card next to it.
No native code of our own: measurement goes through measureInWindow, the dim is a
view with a very wide border, and the animation is Reanimated.

Why

The tour libraries that already exist were written before the New Architecture and
most still call findNodeHandle, which is deprecated. This one is built for it, and
lets you replace the card with renderCard so the tour looks like the app instead of
like a library.

How it was verified

  • yarn lint — zero errors and zero warnings
  • yarn typecheck
  • yarn test — 99 tests, 15 suites
  • Ran the example on the iOS simulator (iPhone 17 Pro, iOS 26.5)

The website builds for both locales (en, pt-BR), checked by serving the build and
loading both.

If it touches the tour surface

  • Every view model that changed still has a test covering the new behaviour
  • Screenshots or a clip, since layout and animation do not show up in a diff

The clip is in the README and on the homepage of the site.

If it touches the build

  • yarn prepare && grep -r '@/' lib/ comes back empty — the alias must not
    reach the published package

Bob's module target runs with configFile: true on purpose, and tsc-alias does the
same for the .d.ts files. With the default false it ignores babel.config.js and
ships import '@/constants' to consumers.

Notes for the reviewer

  • Geometry, colours and durations come from a clickable prototype rather than from
    taste. The one deliberate divergence is documented in resolveCardLayout: the
    prototype centres the card on the screen, which only works because every target in
    it happens to be central.
  • Two things have to happen outside this PR. GitHub Pages needs its source set to
    GitHub Actions or the Docs workflow fails on first run, and the first npm publish
    has to be manual before OIDC can take over.

bob's module target runs with configFile: true on purpose — with the default
false it ignores babel.config.js and ships `import '@/constants'` to consumers.
tsc-alias does the same for the emitted .d.ts files.
The @react-native config is scoped to js/ts because its parser cannot read
import.meta; the lint glob now covers .mjs so eslint.config.mjs is checked too.
eslint-plugin-prettier is gone, so `yarn lint` no longer catches formatting.
react-native-svg was deliberately avoided: the scrim is a giant rounded border,
which needs no SVG, and every peer dependency is a cost the consumer pays.
Values read out of the Rotuz clickable prototype; the origin is noted once per
constants file rather than on every field.
The hole hugs the target and the ring sits `padding` px outside it, so the band
between the two stays dimmed — that band is the halo.

Unlike the prototype, which centres the card on the screen, the card follows the
target and the arrow is repositioned when clamping to the edge separates them.
The dim is one view with a very wide border and a rounded hollow centre, so the
cut-out gets real rounded corners without pulling in SVG.

Touch is decoupled from the visuals: one full-screen layer by default, or four
strips around the hole when the target has to stay pressable.
A Target inside Cicerone.ScrollView finds it through context, so there is no ref
to wire up by hand.
The example's babel config also transforms library files, so a shared prefix
would make @/context resolve to example/src/context. Both aliases are absolute
for the same reason.
Covers a circular target, a target below the fold that gets scrolled to, a card
that flips above its target, and the highlight variant.
Metro reads example/tsconfig.json paths directly, and it does not follow
`extends`, so the paths are declared there rather than inherited.

Dropping module-resolver from the example removes the failure mode behind
"Unable to resolve module ~/components/DemoSection": api.cache(true) freezes the
babel config, so a stale transform cache kept emitting the bare alias.
The versions were copied from another project instead of letting Expo pick, so
the example ran reanimated 4.5.4 against a dev client whose native worklets is
0.7.4, crashing at startup with "Exception in HostFunction" in NativeWorklets.

The root devDependency is pinned to the same exact version so the workspace
hoists one copy: two reanimated instances would reintroduce the same class of
failure. babel-preset-expo already applies react-native-worklets/plugin on its
own, so no babel change is needed.
Position came in as a static style, so advancing a step teleported the card
while the hole slid under it. left/top/bottom are now shared values driven with
the same easing and duration as the hole.

The entrance is replayed on every step, which is what the prototype is doing by
alternating rtzBalA and rtzBalB. Crossing the target switches anchor between top
and bottom, so that case snaps instead of sliding.
eumaninho54 and others added 29 commits August 27, 2026 12:04
Pulled in by @storybook/addon-ondevice-controls through
react-native-modal-datetime-picker; without it the bundle fails to resolve.
Rewriting App for the Storybook switcher dropped the Cicerone.Provider that
wrapped DemoScreen, so the screen threw on useCicerone at startup.
The example crashed on exactly this and nothing failed in CI, because the hook
had no test of its own.
Three things were wrong with it:

- The circular target measured full width, because a Target is a View and a
  View in a column stretches. It hugs now.
- A press ended the one-step tour and left the stage blank, so stories no
  longer react to presses and carry a Replay button instead.
- The card had no room; the stage lost some padding.
Targets are measured in window coordinates, so anything between the provider
and the window origin shifts every hole by that much.
The overlay is absolutely positioned within whatever contains the provider, but
it placed holes using raw window coordinates. With the provider at the root the
two happen to agree, which is why the demo looked right; inside Storybook the
ring landed 98px below its target.

It measures its own box now and works in that space — the panels and touch
strips too, which were reading the window directly.
Moving the overlay into its own coordinate space left the card behind: it kept
converting its bottom anchor with useWindowDimensions while the layout it was
given had been computed against the overlay's box. The gap between the two is
whatever sits below the overlay — in the example, the mode switcher — so every
card that hung above its target landed that far too low and covered it.

The container height comes down as a prop now, so both ends measure the same
box. Only the pre-measurement fallback still reads the window.
CONTRIBUTING told contributors to follow a template that the scaffold never
created. The checklist asks what was actually run, and carries the two checks
that a diff cannot show: the alias must not reach lib/, and layout or animation
changes need a screenshot.
release-it applies the recommended bump to the version in package.json, and the
commits on this branch recommend a minor. From 0.1.0 that would publish 0.2.0.

A v0.0.0 tag would not have fixed it: the tag sets where the changelog starts,
not the arithmetic, and pointing it at HEAD would leave the release notes empty.
It had grown into a full manual: theming, persistence, the API tables, how the
overlay is built. That belongs on the docs site, where it can be navigated.

What stays is what someone decides with — the problem, one example, the feature
list, and what to install.
website/ is a separate Docusaurus project with its own tsconfig and its own
rules — web code, not React Native — so the root eslint and tsc skip it, and
its build output is ignored everywhere.
Six pages: getting started, targets, theming, recipes, the API reference, and
how the overlay actually works. The homepage carries the pitch, the three
pieces, and the feature grid.

It is a standalone yarn project rather than a workspace, which is why it has an
empty lockfile of its own. Root scripts docs:dev and docs:build drive it.
The prose had drifted into a register nobody writes in: em dashes everywhere,
semicolons joining clauses, and sentences arranged for rhythm instead of for
being read once and understood.

Also drops the horizontal rules from the readme.
Uses the raw GitHub URL rather than a relative path, since assets/ is not in
the package files and npm would render a broken image otherwise.

Same file doubles as the site's social card, so links unfurl with it.
Adds pt-BR alongside English and a localeDropdown in the navbar. The homepage
strings moved into Translate so write-translations can pick them up, which is
why the components now carry ids instead of bare text.
All six pages plus the navbar, footer and homepage strings.

Anchors in the translated docs point at the accented slugs Docusaurus actually
generates. The {#custom-id} syntax would have been cleaner but MDX v3 reads the
braces as a JS expression and fails to parse.
The wordmark goes in the navbar with a light and dark variant, so the title
text is gone. It was saying the same thing twice.

The mark sits above the hero, and favicon.svg replaces the placeholder .ico.
The screen was a copy of an internal app, down to the product name and its
score widget. Someone opening the example to learn the library had to decode
a domain that has nothing to do with tours.

Every target is now a plain piece of UI and each step explains the case it
covers: the two hole shapes, the scroll, the flip, the highlight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same reason as the demo screen: the stories described a product instead of
the knob they exercise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These comments ship to consumers and named a private app they cannot see.
The prototype the values came from is documented in .claude/rules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six no-inline-styles warnings, from before this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stage started at y 0, so the wordmark sat under the notch. The bottom bar
already handled its inset; the top had nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The recording came in at 79MB, full simulator window: the device chrome, the
toolbar and the desktop behind it. Cropped to the phone and re-encoded at
300px, 12fps, 96 colours, which lands at 2.3MB and still reads.

The uncropped file stays out of git, ignored as assets/*-source.gif, so the
GIF can be re-cut without another recording.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The site gets the same recording as mp4 and webm instead of the GIF: 726KB
and 672KB against 2.3MB, at full 560px width rather than 300px.

It sits between the hero and the anatomy, so the tour is seen before it is
explained. Reduced-motion pauses it and reveals the controls; the check runs
in an effect because autoPlay has to match the server render.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
storybook.requires.ts is written by the metro plugin, so it only existed on
machines that had run the example. CI cloned, ran tsc and failed on the import
in .storybook/index.tsx, and so would any contributor typechecking a fresh
clone.

Committing generated code is the lesser evil here: the file is derived from
.storybook/main.ts alone, since require.context resolves the stories at
runtime. Adding a story does not change it. Prettier and ESLint were already
ignoring it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@eumaninho54
eumaninho54 merged commit 21ea469 into main Aug 27, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant