feat: the guided tour library - #1
Merged
Merged
Conversation
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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Everything: the library itself, the example app, the docs website and the release
tooling.
mainis 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 aview 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, andlets you replace the card with
renderCardso the tour looks like the app instead oflike a library.
How it was verified
yarn lint— zero errors and zero warningsyarn typecheckyarn test— 99 tests, 15 suitesThe website builds for both locales (
en,pt-BR), checked by serving the build andloading both.
If it touches the tour surface
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 notreach the published package
Bob's module target runs with
configFile: trueon purpose, andtsc-aliasdoes thesame for the
.d.tsfiles. With the defaultfalseit ignoresbabel.config.jsandships
import '@/constants'to consumers.Notes for the reviewer
taste. The one deliberate divergence is documented in
resolveCardLayout: theprototype centres the card on the screen, which only works because every target in
it happens to be central.
GitHub Actions or the
Docsworkflow fails on first run, and the first npm publishhas to be manual before OIDC can take over.