Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
d18fd86
chore: point the package at the Salve-Software org
eumaninho54 Aug 27, 2026
abedb1a
fix(jest): render the unresolved source-condition template
eumaninho54 Aug 27, 2026
8d837e5
build: replace lefthook with husky and lint-staged
eumaninho54 Aug 27, 2026
249e0e6
style: move prettier config out of package.json
eumaninho54 Aug 27, 2026
b1678eb
build: resolve the @/ alias across babel, tsc and jest
eumaninho54 Aug 27, 2026
785b2cc
ci(eslint): ban relative parent imports and widen the glob
eumaninho54 Aug 27, 2026
e710c88
ci: check formatting in the pipeline
eumaninho54 Aug 27, 2026
8fe3960
chore: ignore the example web build output
eumaninho54 Aug 27, 2026
a5f73b5
build(deps): take reanimated as the only peer dependency
eumaninho54 Aug 27, 2026
17b5580
feat(types): declare the public tour API
eumaninho54 Aug 27, 2026
188c491
feat(constants): port the palette and geometry from the prototype
eumaninho54 Aug 27, 2026
ad44390
feat(utils): resolve tour geometry, placement and scrolling
eumaninho54 Aug 27, 2026
d8fcf3e
feat(storage): persist which tours were already seen
eumaninho54 Aug 27, 2026
0f8958e
feat(spotlight): dim the screen and cut the hole over the target
eumaninho54 Aug 27, 2026
e0ee0e5
feat(card): render the default tour card
eumaninho54 Aug 27, 2026
5e4c92d
feat(target): register targets and reach the ones below the fold
eumaninho54 Aug 27, 2026
fed2685
feat(provider): drive tour state and step measurement
eumaninho54 Aug 27, 2026
559187f
feat: expose the Cicerone namespace as the public entry point
eumaninho54 Aug 27, 2026
e520295
build(example): add reanimated and safe-area to the demo
eumaninho54 Aug 27, 2026
29a2a25
build(example): alias the demo sources as ~/
eumaninho54 Aug 27, 2026
55e66d9
feat(example): demo static, scrolled and highlight steps
eumaninho54 Aug 27, 2026
127f68d
docs: document the tour API in the readme
eumaninho54 Aug 27, 2026
1442c6b
docs: add project rules for AI agents
eumaninho54 Aug 27, 2026
71d14ff
style: apply prettier to the scaffold files
eumaninho54 Aug 27, 2026
68b93e6
chore: update the lockfile
eumaninho54 Aug 27, 2026
eebc79b
fix(example): resolve the aliases through Metro instead of babel
eumaninho54 Aug 27, 2026
72cf42e
docs: record the Metro alias resolution gotcha
eumaninho54 Aug 27, 2026
0bf8332
fix(deps): pin reanimated and worklets to the SDK versions
eumaninho54 Aug 27, 2026
9da2732
docs: warn against copying dependency versions
eumaninho54 Aug 27, 2026
63d01eb
fix(card): slide the card to its new position between steps
eumaninho54 Aug 27, 2026
9484472
perf(spotlight): cut the per-frame work of a step transition
eumaninho54 Aug 27, 2026
7402d6c
fix(card): stop stretching the card when it sits above the target
eumaninho54 Aug 27, 2026
f006ca8
feat: fade the overlay out when the tour ends
eumaninho54 Aug 27, 2026
fc914a9
fix: stop restarting animations on every render
eumaninho54 Aug 27, 2026
952b89b
docs: warn about easings rebuilt every render
eumaninho54 Aug 27, 2026
bebdbfa
fix(spotlight): cover the screen corners, not just the edges
eumaninho54 Aug 27, 2026
9ce7f20
build(deps): take react-native-svg as a peer dependency
eumaninho54 Aug 27, 2026
63ea22c
perf(spotlight): draw the scrim as an even-odd SVG path
eumaninho54 Aug 27, 2026
b274fb9
fix(card): stop restarting the slide when the card re-measures
eumaninho54 Aug 27, 2026
6d69bf0
fix(card): stop fading the card out on every step
eumaninho54 Aug 27, 2026
81b342b
chore: ignore the example native projects
eumaninho54 Aug 27, 2026
753b233
refactor(types): give every hook prop type its own file
eumaninho54 Aug 27, 2026
a657605
docs: drop the last comments that restate the code
eumaninho54 Aug 27, 2026
8740e76
refactor(providers): move CiceroneProvider out of components
eumaninho54 Aug 27, 2026
9c96d90
docs: record where providers live
eumaninho54 Aug 27, 2026
2701545
refactor: rename the entry point to .ts
eumaninho54 Aug 27, 2026
d3c32e0
chore: publish under the @salve-software scope
eumaninho54 Aug 27, 2026
80233f2
docs: use the scoped package name
eumaninho54 Aug 27, 2026
4646032
fix(types): let a theme override carry a partial palette
eumaninho54 Aug 27, 2026
8331249
build(example): add Storybook on device
eumaninho54 Aug 27, 2026
2ee7128
feat(example): browse the tour through Storybook
eumaninho54 Aug 27, 2026
a7e892b
build(example): register the datetimepicker config plugin
eumaninho54 Aug 27, 2026
033bb4d
fix(example): keep the demo inside its provider
eumaninho54 Aug 27, 2026
2f1052a
test(hooks): cover useCicerone outside a provider
eumaninho54 Aug 27, 2026
51b25f0
fix(test): drop the unused React import
eumaninho54 Aug 27, 2026
4fa6628
fix(example): make the story stage usable
eumaninho54 Aug 27, 2026
26f0772
docs: warn that a Target takes part in layout
eumaninho54 Aug 27, 2026
00648c7
feat(utils): translate geometry into the overlay's own box
eumaninho54 Aug 27, 2026
df52351
fix(overlay): stop assuming the provider sits at the window origin
eumaninho54 Aug 27, 2026
7ea3166
fix(card): measure the card against the overlay box too
eumaninho54 Aug 27, 2026
cc944c1
docs: add the pull request template CONTRIBUTING already pointed at
eumaninho54 Aug 27, 2026
01f68e0
chore: start from 0.0.0 so the first release lands on 0.1.0
eumaninho54 Aug 27, 2026
6d2c53f
docs: cut the readme down to the essentials
eumaninho54 Aug 27, 2026
e95b26b
build: keep the docs site out of the library's toolchain
eumaninho54 Aug 27, 2026
92c935f
docs: add the documentation site
eumaninho54 Aug 27, 2026
d498c11
ci: deploy the docs site to GitHub Pages
eumaninho54 Aug 27, 2026
18f1d1e
docs: rewrite the copy in plainer language
eumaninho54 Aug 27, 2026
cc0cbc9
docs: put the banner at the top of the readme
eumaninho54 Aug 27, 2026
e5bbef3
feat(website): wire up i18n with a locale switch
eumaninho54 Aug 27, 2026
8474041
docs(website): translate the site to pt-BR
eumaninho54 Aug 27, 2026
0c32c6a
feat(website): use the real logo and icons
eumaninho54 Aug 27, 2026
f0e0327
style: drop the trailing dot from the wordmark
eumaninho54 Aug 27, 2026
6c517d8
refactor(example): make the demo generic
eumaninho54 Aug 27, 2026
737935d
docs(example): rewrite the story copy
eumaninho54 Aug 27, 2026
688415c
docs: drop the product name from constants
eumaninho54 Aug 27, 2026
810b335
style(example): extract the custom card styles
eumaninho54 Aug 27, 2026
62f39de
fix(example): respect the top safe area
eumaninho54 Aug 27, 2026
4451e98
docs: add the tour recording to the readme
eumaninho54 Aug 27, 2026
ddd8a05
feat(website): add the demo video section
eumaninho54 Aug 27, 2026
9e5771a
fix(ci): track the generated storybook file
eumaninho54 Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# @salve-software/react-native-cicerone

Guided onboarding tours for React Native. Published open source under the
[Salve-Software](https://github.com/Salve-Software) org.

The library ships **no native code**: it measures with `measureInWindow`, dims with plain
views, and animates with Reanimated.

## Rules

Read these before writing code. They are not suggestions.

- [Architecture](.claude/rules/architecture.md) — where each thing lives
- [Components](.claude/rules/components.md) — the layered anatomy every component follows
- [Imports](.claude/rules/imports.md) — the `@/` alias, and why `../` is banned
- [Comments](.claude/rules/comments.md) — English, one line or nothing
- [Lint and formatting](.claude/rules/lint.md) — zero errors, zero warnings
- [The prototype](.claude/rules/prototype.md) — where the magic numbers come from

## Commands

```sh
yarn lint # zero errors and zero warnings, always
yarn typecheck
yarn test
yarn format # prettier --write .
yarn prepare # builds lib/ through bob, then resolves aliases
yarn example start # the demo app
```

## Non-negotiables

- **`yarn lint` is zero errors and zero warnings.** Not "mostly clean".
- **Every view model has a test.** No exceptions worth arguing about.
- **The published `lib/` must not contain `@/`.** `yarn prepare` resolves it; if you change
the build, verify with `grep -r '@/' lib/`.
- **Never commit to `main`.** Branch first.
68 changes: 68 additions & 0 deletions .claude/rules/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Architecture

A published library, not an app. Everything under `src/` ends up in someone else's bundle.

```
src/
index.tsx the public API — the only file consumers import from
types/ one type per file
constants/ named `as const` groups
context/ React contexts
storage/ the "already seen" adapter
utils/ one pure function per file
hooks/ cross-cutting hooks (useCicerone)
providers/ one folder per provider, re-exported by its index
components/ the tour surface

example/ demo app, its own workspace
```

## Where each thing lives

- Public type consumers touch → `src/types/`, re-exported by `src/index.tsx`
- Internal-only type → still `src/types/`; the barrel is cheap and the split is not worth it
- Pure function with no React → `src/utils/`, with a test
- Value from the prototype → the owning `constants/` folder
- Anything that owns state for the tree below it → `src/providers/`, not `components/`
- Component used by more than one component → `src/components/`
- Subcomponent of one component → `components/` **inside** that component's folder
- Component state → `hooks/use<Name>ViewModel/` next to the component
- Animated style → `hooks/useReanimatedStyles/` next to the component

## Dependency rules

- **Reanimated is the only peer dependency.** Adding another is a decision, not a detail:
every consumer pays for it. `react-native-svg` was deliberately avoided — the scrim is a
giant rounded border, which needs no SVG.
- **Nothing in `src/` may import from `example/`.** The example depends on the library, never
the reverse.
- **Never copy dependency versions from another project.** Run `npx expo install --fix` in
`example/` and let the SDK pick; then pin the root devDependency to the _same exact_
version so the workspace hoists one copy. Reanimated and `react-native-worklets` are
version-locked to each other and to the SDK's native binary — a mismatch does not fail the
bundle, it crashes at startup with `Exception in HostFunction` in `NativeWorklets`.
`npx expo install --check` is the fast way to confirm.
- **No design system, no i18n, no storage engine.** The library takes a `theme`, `labels` and
a `storage` adapter instead. It has no opinion about which ones you use.

## The Target wrapper

`Target` renders a `View` around its child, so it is not layout-transparent. In a column it
stretches, and then `measureInWindow` returns the wrapper instead of the thing the user sees
— a circular button ends up with a full-width ring. Consumers fix it with
`style={{ alignSelf: 'flex-start' }}`, which is why `Target` takes a `style` prop at all.

Changing the default to hug would break the opposite case, where the target is meant to span
its container. This stays documented rather than "fixed".

## The overlay

`CiceroneProvider` owns state and measurement, and renders `CiceroneOverlay` as a sibling of
its children, so the overlay paints on top without a Modal.

The dim is one view with a very wide border and a rounded hollow centre — the hollow is the
hole. The ring sits `padding` px outside it, leaving that band dimmed; that band is the halo.

Touch is decoupled from the visuals: by default one layer covers the screen and a press
advances; with `allowTargetInteraction`, four strips surround the hole instead so the
highlighted element stays pressable.
62 changes: 62 additions & 0 deletions .claude/rules/comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Comments

## English

This is an open source library. Comments, identifiers and commit messages are all English —
contributors outside the team have to read it.

This overrides the global "comments in Portuguese" rule, which still holds for the Rotuz app.

## As lean as possible — one line or nothing

The test: **would deleting this make someone break the code?** If not, delete it.

A comment that stays true when pasted onto any other thing of its kind says nothing.
`"Presentation component for the screen"` fits anything; `"Android reports 0x0 until the view
has been through layout"` fits exactly one place.

```ts
// ❌ restates the name
/** Which side of the target the card is anchored to. */
export type ICiceronePlacement = 'top' | 'bottom';

// ✅ says something the code does not
// Android reports 0x0 until the view has been through layout.
if (width === 0 && height === 0) return resolve(null);
```

## Paragraph blocks are banned

If it does not fit on one line, what is left over is decision context — that belongs in a
rule file or the commit message, not in the source.

## What earns an inline comment

| Type | Example in this repo |
| -------------------------------------------- | ------------------------------------------------------------ |
| Looks like a bug, is not | Android's 0x0 measurement in `measureInWindow` |
| Deliberate absence | `// Mount only: changing steps mid-tour must not restart it` |
| A value that must not diverge from elsewhere | letter spacing derived from the prototype's em values |
| An invisible external constraint | the giant border technique in the scrim |

Narrating the next block, repeating the function name, or explaining the pattern instead of
this instance: all out.

## JSDoc — allowed, short

One line, on a component, hook, function or type, saying what **this** one is or why it
exists. Keep it when it carries an API contract the signature cannot:

```ts
/** `'circle'` rounds by half of the shortest side. */
radius?: number | 'circle';
```

## Values from the prototype

The origin goes **once**, at the top of the `constants/` file — not repeated on every field.

```ts
/** Geometry taken from the Rotuz clickable prototype. */
export const CICERONE = { ... } as const;
```
82 changes: 82 additions & 0 deletions .claude/rules/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Components

Every component follows the same anatomy:

```
<Name>/
index.tsx view — composition and JSX only
styles.ts useStyles
types/ one type per file, plus index.ts
constants/ named `as const` groups
hooks/
use<Name>ViewModel/
index.ts state, handlers, derivations
__tests__/ the view model's test
useReanimatedStyles/ animated styles, when there are any
components/ subcomponents local to this one
```

## The view has no logic

`index.tsx` does composition and JSX. It does not declare functions, derive values, hold
state, or compute conditionals beyond rendering what the view model already resolved.

Only `useStyles()` and animated-style hooks belong in the view — they are presentation.

## Every component has a view model

Even when it looks like too little. The view model is where a rule becomes testable without
mounting the tree.

The exception is a component with no state, no effect, no handler and no derivation —
`CardArrow` builds JSX from props and constants, and a view model there would be an empty
function with an empty test. The moment a `useState`, `useEffect`, `useRef`, handler or
derivation appears, the view model becomes mandatory.

## Every view model has a test

`hooks/use<Name>ViewModel/__tests__/use<Name>ViewModel.test.ts`. One `describe` per exposed
function, and the cases that matter: the happy path, the path that must **not** happen, and
the edge.

`@testing-library/react-native` 14 is **async** — `renderHook`, `act` and `unmount` all need
`await`. Without it, `result` is `undefined` or the assertion reads stale state:

```ts
const { result } = await renderHook(() => useThingViewModel(props));
await act(async () => result.current.next());
await unmount();
```

A variable used inside `jest.mock()` needs a `mock` prefix — jest hoists the call.

## Build easings at module scope

`Easing.bezier(...)` returns a new object on every render. Listed in a dependency array,
it makes the effect re-run every render — an entrance animation reset over and over reads
as the card blinking. Build it once outside the hook.

```ts
// ❌ new identity every render
const easing = Easing.bezier(0.22, 1, 0.36, 1);
useEffect(() => {
entry.value = withTiming(1, { easing });
}, [index, easing]);

// ✅
const EASE_OUT_EXPO = Easing.bezier(0.22, 1, 0.36, 1);
```

`react-hooks/exhaustive-deps` does not catch this: the dependency is declared correctly, it
is the value that is unstable.

## Do not over-nest

`Sparkle` is a sibling of `Sparkles` under `Spotlight/components/`, not a child of it. Four
levels of component folders means eight `../` to reach `src/` — and that is a signal the
structure is wrong, not a path detail to work around.

## Constants

A magic number does not live in `styles.ts` or in the view. It goes to `constants/`, named,
grouped in an `as const` object.
60 changes: 60 additions & 0 deletions .claude/rules/imports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Imports

## No `../` — ever

`import/no-relative-parent-imports` is an **error**. A chain like `../../../../constants`
hides where a module actually lives, and it breaks the moment a file moves.

```ts
// ❌
import { CICERONE } from '../../../../constants';

// ✅
import { CICERONE } from '@/constants';
```

`./` is fine and preferred inside a component folder — `./styles`, `./types`,
`./hooks/useThingViewModel` all stay relative.

## The two aliases

| alias | points at | used by |
| ----- | -------------- | --------------- |
| `@/` | `src/` | the library |
| `~/` | `example/src/` | the example app |

**They must stay distinct.** The example's babel config also transforms library files, so a
shared prefix makes `@/context` in the library resolve to `example/src/context`. Both aliases
are declared with absolute paths for the same reason.

Three places have to agree, and all three are already wired:

- `tsconfig.json` → `paths`, for the editor and `yarn typecheck`
- `babel.config.js` → `module-resolver`, for the library build only
- `example/tsconfig.json` → `paths`, which Metro reads directly
- `package.json` → `jest.moduleNameMapper`, for tests

**The example resolves through Metro, not babel.** Expo enables `tsconfigPaths` by
default, so `example/tsconfig.json` declares its `paths` outright instead of inheriting
them — Metro does not follow `extends`. A `module-resolver` plugin there as well would be
redundant, and worse: `api.cache(true)` freezes the babel config, so a stale transform
cache resurfaces as `Unable to resolve module ~/components/...` on a device long after the
config was fixed. If you ever see that error, `yarn example start --clear` proves whether
it is a cache.

## The alias must not reach the published package

Babel rewrites `@/` to a relative path at build time, and `tsc-alias` does the same for the
`.d.ts` files. `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.

After touching anything in the build, verify:

```sh
yarn prepare && grep -r '@/' lib/ && echo "LEAK" || echo "clean"
```

## Order

`import/order` groups: types first, then packages, then `@/` and `~/`, then relative. No blank
lines between groups. `--fix` handles it.
48 changes: 48 additions & 0 deletions .claude/rules/lint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Lint and formatting

**Target: zero errors and zero warnings.** An accumulated warning becomes noise, and the next
real error slips past it.

```sh
yarn lint # eslint over js,jsx,cjs,mjs,ts,tsx
yarn lint:fix
yarn format # prettier --write .
yarn format:check
```

## Prettier and ESLint do not compete

`eslint-config-prettier` is the **last** entry in the array and turns off every formatting
rule. Style is Prettier's job; ESLint handles correctness and convention. Do not add a
formatting rule to ESLint.

Prettier config lives in `.prettierrc.json`, not in `package.json`.

## Rules that are not the default, and why

**`import/no-relative-parent-imports`.** See [imports.md](./imports.md).

**`no-void` with `allowAsStatement: true`.** `void somePromise()` is how a deliberately
un-awaited call is marked. The default rule bans it outright; only the expression form stays
banned.

**`@react-native` config scoped to `js,jsx,ts,tsx`.** Its parser cannot handle `import.meta`,
so `.mjs`/`.cjs` config files must keep the default parser. Without the scope,
`eslint.config.mjs` fails to parse itself — and note the lint glob covers `.mjs`, precisely
so that failure is visible.

**`example/dist/**` ignored.** The exported web bundle is 1.3MB of minified JS and produces
hundreds of meaningless findings.

## Hooks

`lint-staged` runs `prettier --write` then `eslint --fix` on staged files, via husky's
`pre-commit`. `commit-msg` runs commitlint. `pre-push` runs `tsc --noEmit` — type-checking on
every commit is too slow to be worth it.

## When touching the lint config

- A new rule needs a reason. If it is taste, it is Prettier's business.
- Running `--fix` across the repo and committing it alongside a logic change hides the diff
that matters. Formatting goes in its own commit.
- Suppressing with `eslint-disable` requires a comment saying why.
Loading
Loading