@thelol3882/sapar 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +380 -2
  3. package/dist/adapter-Y9yB3-q3.d.ts +14 -0
  4. package/dist/adapter-fFZPsINL.d.ts +14 -0
  5. package/dist/chunk-7B66QEMY.js +536 -0
  6. package/dist/chunk-7B66QEMY.js.map +1 -0
  7. package/dist/chunk-FM64UDKJ.js +893 -0
  8. package/dist/chunk-FM64UDKJ.js.map +1 -0
  9. package/dist/chunk-VC6XLWM3.js +176 -0
  10. package/dist/chunk-VC6XLWM3.js.map +1 -0
  11. package/dist/chunk-ZBVQGMR4.js +583 -0
  12. package/dist/chunk-ZBVQGMR4.js.map +1 -0
  13. package/dist/cli/index.js +1335 -0
  14. package/dist/core/index.d.ts +273 -0
  15. package/dist/core/index.js +71 -0
  16. package/dist/core/index.js.map +1 -0
  17. package/dist/expo/index.d.ts +189 -0
  18. package/dist/expo/index.js +39 -0
  19. package/dist/expo/index.js.map +1 -0
  20. package/dist/expo-router/index.d.ts +18 -0
  21. package/dist/expo-router/index.js +46 -0
  22. package/dist/expo-router/index.js.map +1 -0
  23. package/dist/next/index.d.ts +18 -0
  24. package/dist/next/index.js +49 -0
  25. package/dist/next/index.js.map +1 -0
  26. package/dist/presentation-CClsPRT6.d.ts +12 -0
  27. package/dist/react/index.d.ts +35 -0
  28. package/dist/react/index.js +29 -0
  29. package/dist/react/index.js.map +1 -0
  30. package/dist/rtk/index.d.ts +751 -0
  31. package/dist/rtk/index.js +114 -0
  32. package/dist/rtk/index.js.map +1 -0
  33. package/dist/stepCount-CTYYFWiV.d.ts +57 -0
  34. package/dist/types-CHD2pB5X.d.ts +192 -0
  35. package/dist/web/index.d.ts +112 -0
  36. package/dist/web/index.js +34 -0
  37. package/dist/web/index.js.map +1 -0
  38. package/package.json +147 -3
  39. package/templates/expo/AnnouncementModal.tsx +102 -0
  40. package/templates/expo/TourCard.tsx +112 -0
  41. package/templates/expo/WhatsNewButton.tsx +87 -0
  42. package/templates/expo/WhatsNewPanel.tsx +167 -0
  43. package/templates/mantine/AnnouncementModal.tsx +66 -0
  44. package/templates/mantine/TourCard.tsx +84 -0
  45. package/templates/mantine/WhatsNewButton.tsx +62 -0
  46. package/templates/mantine/WhatsNewPanel.tsx +129 -0
  47. package/templates/tailwind/AnnouncementModal.tsx +100 -0
  48. package/templates/tailwind/TourCard.tsx +69 -0
  49. package/templates/tailwind/WhatsNewButton.tsx +56 -0
  50. package/templates/tailwind/WhatsNewPanel.tsx +174 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dias Shabarov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,381 @@
1
- # Temporary Holding Version
1
+ # Sapar
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ *Sapar* (сапар) is Kazakh for a journey. This one takes a person on a short trip through your app, and tells them what is new.
4
+
5
+ Headless product tours and a "What's New" feed for React, Next.js and Expo: `@thelol3882/sapar`.
6
+
7
+ - Spotlight tours, announcements (silent, feed or modal) and an unread badge.
8
+ - Headless parts plus copy-in Tailwind templates: you own the markup.
9
+ - Runs on an in-memory source (definitions in code, state in `localStorage`) or against a backend that speaks the Sapar HTTP contract (see Backend compatibility), over `fetch` or RTK Query.
10
+ - Framework-free core with platform adapters for the web (Next.js) and Expo / React Native.
11
+ - A `sapar` CLI: `init` scaffolds the setup, `add` copies UI templates (Tailwind, Mantine, Expo), `check` is the CI gate for anchors.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add @thelol3882/sapar
17
+ ```
18
+
19
+ Peer dependencies: `react >= 18.3`. Optional: `next >= 15` (for `/next`), `@reduxjs/toolkit >= 2` and `react-redux >= 9` (for `/rtk`).
20
+
21
+ ## Quick start (Next.js App Router + Tailwind)
22
+
23
+ Define a tour and its anchors once:
24
+
25
+ ```ts
26
+ // tours/definitions.ts
27
+ import { defineAnchors, defineTour } from '@thelol3882/sapar';
28
+
29
+ export const anchors = defineAnchors(['nav.menu', 'billing.invoices']);
30
+
31
+ export const onboarding = defineTour({
32
+ key: 'onboarding',
33
+ title: 'Welcome',
34
+ trigger: { route: '/dashboard' },
35
+ steps: [
36
+ { key: 'welcome', title: 'Welcome', body: 'A quick look around.' },
37
+ { key: 'menu', anchor: 'nav.menu', title: 'Navigation', body: 'Everything lives here.' },
38
+ ],
39
+ });
40
+ ```
41
+
42
+ Mount the provider in a client component under your root layout:
43
+
44
+ ```tsx
45
+ // app/tours-provider.tsx
46
+ 'use client';
47
+
48
+ import { memorySource } from '@thelol3882/sapar';
49
+ import { NextSaparProvider } from '@thelol3882/sapar/next';
50
+ import { localStorageStore } from '@thelol3882/sapar/web';
51
+ import type { ReactNode } from 'react';
52
+ import { onboarding } from '../tours/definitions';
53
+
54
+ const source = memorySource({ tours: [onboarding], storage: localStorageStore() });
55
+
56
+ export function AppSaparProvider({ children }: { children: ReactNode }) {
57
+ return <NextSaparProvider source={source}>{children}</NextSaparProvider>;
58
+ }
59
+ ```
60
+
61
+ Create the source once (module scope or `useMemo`): the provider recreates its controller whenever the `source` identity changes.
62
+
63
+ Mark what a tour can point at, and render a card copied from `templates/tailwind` once inside the provider:
64
+
65
+ ```tsx
66
+ 'use client';
67
+
68
+ import { useTourAnchor } from '@thelol3882/sapar/web';
69
+ import { TourCard } from '@/components/tours/TourCard'; // copied from templates/tailwind
70
+
71
+ export function Menu() {
72
+ return <nav {...useTourAnchor('nav.menu')}>...</nav>;
73
+ }
74
+
75
+ export function AppShell({ children }: { children: ReactNode }) {
76
+ return (
77
+ <AppSaparProvider>
78
+ {children}
79
+ <TourCard />
80
+ </AppSaparProvider>
81
+ );
82
+ }
83
+ ```
84
+
85
+ Server components can mark anchors with a plain attribute instead: `<section data-sapar="billing.invoices">`.
86
+
87
+ Start a tour on demand with `useSapar().startTour('onboarding')`.
88
+
89
+ ## Defining tours in code
90
+
91
+ ```ts
92
+ import { defineAnchors, defineAnnouncement, defineTour } from '@thelol3882/sapar';
93
+
94
+ export const release = defineAnnouncement({
95
+ key: 'release_2026_03',
96
+ tier: 'modal', // 'silent' | 'feed' | 'modal'
97
+ title: { en: 'Faster invoices', ru: 'Быстрее счета' },
98
+ body: { en: 'Invoices now load instantly.', ru: 'Счета загружаются мгновенно.' },
99
+ publishedAt: '2026-03-01T00:00:00Z',
100
+ tourKey: 'onboarding',
101
+ });
102
+ ```
103
+
104
+ Keys are lowercase, digits and underscores, dot-separated. Text is a string or a per-locale map. Changing a tour's `revision` re-offers it.
105
+
106
+ `defineAnchors` keeps anchor names typed. Wrap `useTourAnchor` once so a typo fails to compile:
107
+
108
+ ```ts
109
+ import type { AnchorKey } from '@thelol3882/sapar';
110
+ import { useTourAnchor } from '@thelol3882/sapar/web';
111
+ import { anchors } from './definitions';
112
+
113
+ export const useAnchor = (key: AnchorKey<typeof anchors>) => useTourAnchor(key);
114
+ ```
115
+
116
+ ## Sources
117
+
118
+ A source is where tours, announcements and per-user state come from (`SaparSource`).
119
+
120
+ **In memory.** Definitions live in code, state in a `KeyValueStore`:
121
+
122
+ ```ts
123
+ import { memorySource } from '@thelol3882/sapar';
124
+ import { localStorageStore } from '@thelol3882/sapar/web';
125
+
126
+ const source = memorySource({
127
+ tours: [onboarding],
128
+ announcements: [release],
129
+ storage: localStorageStore(),
130
+ locale: () => 'en',
131
+ policy: { cooldownHours: 12 },
132
+ namespace: `app.sapar.${userId}`,
133
+ registeredAt: user.createdAt,
134
+ });
135
+ ```
136
+
137
+ - **`namespace`** prefixes every storage key. Give each user their own (`app.sapar.${userId}`): on a shared computer, people otherwise share one "seen" state.
138
+ - **`registeredAt`** drives the new-user grace period. Without it the grace period is skipped and a brand-new account can be interrupted on its first visit, so pass the account's creation date.
139
+
140
+ ### Texts from your i18n library
141
+
142
+ Definitions take a string or a per-locale map (`{ en, ru }`). If your texts live in next-intl, i18next or similar, put message keys in the definitions and resolve them with `translate`, which runs every time a text is read:
143
+
144
+ ```tsx
145
+ 'use client';
146
+
147
+ function AppSaparProvider({ children }: { children: ReactNode }) {
148
+ const t = useTranslations('tours');
149
+ const latestT = useRef(t);
150
+ useEffect(() => {
151
+ latestT.current = t;
152
+ }, [t]);
153
+
154
+ // Created once: `translate` reads the latest `t`, so a locale switch does not change the source.
155
+ const [source] = useState(() =>
156
+ memorySource({ tours, storage: localStorageStore(), translate: (key) => latestT.current(key) }),
157
+ );
158
+ return <NextSaparProvider source={source}>{children}</NextSaparProvider>;
159
+ }
160
+ ```
161
+
162
+ Keep the source's identity stable. The provider recreates its controller whenever `source` changes: a running tour is closed, and the inbox is fetched again. After a locale switch, call `useSaparController().refresh()` so the What's New items are re-read; a tour that is already running keeps its texts until it is started again.
163
+
164
+ **HTTP.** The fetch-based client for the backend:
165
+
166
+ ```ts
167
+ import { httpSource } from '@thelol3882/sapar';
168
+
169
+ const source = httpSource({
170
+ baseUrl: '/api/tours',
171
+ credentials: 'include',
172
+ headers: () => ({ Authorization: `Bearer ${getToken()}` }),
173
+ locale: () => document.documentElement.lang,
174
+ });
175
+ ```
176
+
177
+ **RTK Query.** Reuse your own `createApi` slice, so auth, refresh and the base URL stay in one place. No `tagTypes` edits needed: the `Sapar` tag type is added for you.
178
+
179
+ ```ts
180
+ import { createSaparRtk } from '@thelol3882/sapar/rtk';
181
+ import { baseApi } from './baseApi';
182
+
183
+ export const tours = createSaparRtk(baseApi, { basePath: '/tours' });
184
+ export const { useGetSaparInboxQuery, useGetAnnouncementsQuery } = tours;
185
+ ```
186
+
187
+ ```tsx
188
+ function AppSaparProvider({ children }: { children: ReactNode }) {
189
+ const source = tours.useSaparSource(); // inside the react-redux <Provider>
190
+ return <NextSaparProvider source={source}>{children}</NextSaparProvider>;
191
+ }
192
+ ```
193
+
194
+ The RTK source always refetches (the controller owns timing) and invalidates `Tours/inbox` after seen, dismiss and progress writes, so components using `useGetSaparInboxQuery` directly stay in sync. A missing tour resolves to `null`, as with `httpSource`. `createSource(dispatch)` builds the same source outside React.
195
+
196
+ ## Styling: Tailwind, Mantine, your own
197
+
198
+ The package ships behavior, not CSS. `TourOverlay`, `TourSpotlight` and `TourPopover` are headless; state is exposed as data-attributes you can style with Tailwind variants (for example `data-[placement=top]:...`). `TourStep.Root`, `Title`, `Body`, `Progress`, `Next`, `Back`, `Skip` and `Close` compose your own card, rendered inside `<TourOverlay>`:
199
+
200
+ ```tsx
201
+ <TourStep.Root className="w-80 rounded-xl bg-white p-4 shadow-xl">
202
+ <TourStep.Title className="font-semibold" />
203
+ <TourStep.Body className="mt-1 text-sm text-slate-600" />
204
+ <TourStep.Progress className="mt-3 text-xs" />
205
+ <TourStep.Back />
206
+ <TourStep.Next />
207
+ <TourStep.Skip />
208
+ </TourStep.Root>
209
+ ```
210
+
211
+ Buttons take a `render` prop for design-system components, e.g. `<TourStep.Next render={(props) => <Button {...props} />} />`, and `TourOverlay` takes `spotlightStyle` / `popoverStyle` as well as class names.
212
+
213
+ Two things to know when you restyle:
214
+
215
+ - **Focus.** `TourStep.Root` receives focus on every step, so keyboard and screen-reader users land on the new step. Your global `:focus-visible` outline will draw around the card; the templates replace it with a ring that follows the card's radius. Do the same in your own card rather than removing focus.
216
+ - **Tokens.** The templates' colours are a starting point. Every colour class sits in one `styles` object at the top of each Tailwind template (one `palette` object in the Expo ones), so swapping in your design tokens is a single edit per file.
217
+
218
+ `templates/tailwind`, `templates/mantine` and `templates/expo` each hold `TourCard`, `WhatsNewButton`, `WhatsNewPanel` and `AnnouncementModal` with the same props; `sapar add` copies them into your app, then edit freely. Use `useTourStep`, `useWhatsNew`, `useWhatsNewPanel` and `useAnnouncementInterruption` to build your own. `useWhatsNewPanel(open)` marks what was unread as seen once per opening, while its `isNew` flag keeps those items marked until the panel closes. The panels take a `formatDate` prop so dates can go through your own helper.
219
+
220
+ ## CLI
221
+
222
+ ```bash
223
+ npx sapar init # detect Next/Expo/React, Tailwind/Mantine, RTK; write sapar.json, definitions, provider, TourCard
224
+ npx sapar add --list # TourCard, WhatsNewButton, WhatsNewPanel, AnnouncementModal
225
+ npx sapar add whats-new-panel announcement-modal
226
+ npx sapar check # CI gate: exit 1 on errors, --strict fails on warnings, --format json for tooling
227
+ ```
228
+
229
+ `sapar.json` holds the choices (`framework`, `style`, `source`, `componentsDir`, `toursDir`, `importBase`, and anchor scan roots per platform under `platforms`). `add` rewrites template imports to `importBase`: the package name, or the alias of a copied `src/` folder such as `@/shared/tours`. Existing files are never overwritten without `--force`.
230
+
231
+ `check` parses your sources with the TypeScript compiler (a peer dependency) and reports, per platform: step anchors no element declares (with "did you mean"), anchors missing from `defineAnchors`, invalid route patterns, invalid or duplicate keys, and catalogue anchors nothing declares. A platform with no scanned files is a warning, never a silent pass. Mark a tour `platforms: ['web']` when it only exists on one platform.
232
+
233
+ `init` flags: `--framework`, `--style`, `--source`, `--components-dir`, `--tours-dir` (for layered apps, e.g. `--tours-dir src/shared/tours`), `--import-base`, `--yes`. When your project has Prettier, `init` and `add` format the files they write with your config (`--no-format` to skip).
234
+
235
+ `check` reads string literals, and `const`s declared in the same file (`const BOARD_TOUR = 'board.intro'`). A value imported from another file cannot be verified and is reported as a warning: keep keys as literals or local consts.
236
+
237
+ From a checkout: `node dist/cli/index.js init --cwd ../your-app` (after `pnpm build`).
238
+
239
+ ## Expo / React Native
240
+
241
+ Start from the Expo Router template. At the time of writing `create-expo-app` 5.x fails with `Cannot find module '@expo/env'`; `npx create-expo-app@3.5.3` works and still pulls the latest template.
242
+
243
+ ```bash
244
+ npx expo install react-native-svg @react-native-async-storage/async-storage
245
+ npx sapar init --framework expo --style native --source memory --tours-dir src/shared/tours
246
+ ```
247
+
248
+ `init` generates a per-user provider. Mount it near the root, then render the card last so its overlay sits above the app:
249
+
250
+ ```tsx
251
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
252
+ import { AppSaparProvider } from '@/shared/tours/SaparProvider';
253
+ import { TourCard } from '@/components/tours/TourCard';
254
+
255
+ const TAB_BAR_HEIGHT = 49; // your tab bar's height
256
+
257
+ export default function RootLayout() {
258
+ const insets = useSafeAreaInsets();
259
+ const { user } = useSession(); // your auth hook
260
+ return (
261
+ <AppSaparProvider userId={user.id} registeredAt={user.createdAt}>
262
+ <Stack />
263
+ <TourCard insets={{ ...insets, bottom: insets.bottom + TAB_BAR_HEIGHT }} colors={{ surface: '#0B1F3A', accent: '#F26B1D' }} />
264
+ </AppSaparProvider>
265
+ );
266
+ }
267
+ ```
268
+
269
+ Mark anchors with `useTourAnchor`, and wrap scrollable screens in `TourScrollView` so a step can scroll its anchor into view and the spotlight follows when the user scrolls:
270
+
271
+ ```tsx
272
+ <TourScrollView>
273
+ <Pressable {...useTourAnchor('jobs.on_my_way')} onPress={startDriving} />
274
+ </TourScrollView>
275
+ ```
276
+
277
+ A view that already has its own `ref` or `onTouchEnd` passes them in, so spreading does not overwrite them: `useTourAnchor('jobs.card', { ref: cardRef, onTouchEnd })`. A `Pressable`'s `onPress` needs nothing: with `advanceOn: 'press'` the button's own handler runs and the tour advances.
278
+
279
+ ### Card or sheet: `presentation`
280
+
281
+ `TourOverlay`, `TourPopover` and the `TourCard` templates take `presentation`, read on every render:
282
+
283
+ | Value | Native | Web |
284
+ | --- | --- | --- |
285
+ | `'auto'` (default) | The card next to the element when it fits above or below it; a bottom sheet only when it does not (a tall card, a tiny window, an anchor that fills the screen) | A bottom sheet below `compactBreakpointPx` (768), otherwise the card next to the element |
286
+ | `'anchored'` | Always next to the element (centred for a step without one) | Same |
287
+ | `'sheet'` | Always a bottom sheet | Same |
288
+
289
+ `compactBreakpointPx` on the adapter is only an extra threshold for `'auto'`: windows narrower than it always get the sheet. Its native default is 0 (off).
290
+
291
+ <!-- Screenshots: anchored card and bottom sheet on a phone (to be added from a device run). -->
292
+
293
+ **Safe areas, tab bar, keyboard.** `insets` is the space kept clear at each edge, in both modes. Pass the safe-area insets and add your tab bar's height to `bottom`, so a card near the bottom flips above the element instead of under the tab bar. While the keyboard is open the bottom inset becomes the keyboard's height plus a small gap (the keyboard already covers the tab bar), and it returns when the keyboard hides. The card is placed again after rotation and while a `TourScrollView` scrolls.
294
+
295
+ **Colours.** The Expo templates take `colors` (`surface`, `text`, `muted`, `accent`, `onAccent`, `pressed`, ...), applied over their light and dark palettes, so you can pass your brand without editing the file. `WhatsNewButton` also takes `panelColors` for the panel it opens.
296
+
297
+ **Storage.** `asyncStorageStore(AsyncStorage)` works with `@react-native-async-storage/async-storage` 2.x (Expo SDK 58 pins 2.2.0) and 3.x; `mmkvStore(mmkv)` works with `react-native-mmkv` before and after v4.
298
+
299
+ The Android back button skips a running tour. `/expo` does not need expo-router; only `/expo-router` does. The session cap lasts one app launch; stored progress and the cooldown survive restarts.
300
+
301
+ ## When a tour starts, and what it waits for
302
+
303
+ - **Started by the user** (`startTour`): step 1 shows as soon as its own anchor is found. Each later step is resolved when the runner reaches it, so an element that unmounted since the start is skipped, never spotlighted stale.
304
+ - **Missing anchors**: on a page that is already rendered the runner waits `presentAnchorTimeoutMs` (300 ms) and skips the step; right after navigating to a step's `route` it waits `anchorTimeoutMs` (3 s) for the page to load. A missing step marked `required` ends the run silently, and the tour stays offerable.
305
+ - **Started automatically** (route trigger): the tour waits up to `autoStartTimeoutMs` (10 s) for the anchor of its first anchored step, so it never opens on a loading screen. That anchor is your "page ready" signal: render it only once the data is there, e.g. `data-sapar={loaded ? 'board.new_job' : undefined}`. If it never appears, nothing is shown and the session's interruption is not spent.
306
+ - **Progress**: `useTourStep()` gives a 1-based `position` and a `total`; steps whose anchors are not on the page right now are left out of both, so "2 / 4" stays honest.
307
+
308
+ ## Interruption rules
309
+
310
+ Unbidden tours and modals are limited by a policy, applied by the backend or, for `memorySource`, locally:
311
+
312
+ | Rule | Default | Meaning |
313
+ | --- | --- | --- |
314
+ | `newUserGraceDays` | 3 | No interruptions for young accounts |
315
+ | `cooldownHours` | 24 | Minimum gap between automatic interruptions |
316
+ | `dismissalBackoffDays` | 30 | A dismissed tour returns after this long (0 means immediately) |
317
+
318
+ On top of that the client allows one automatic interruption per session (`sessionCap: false` turns it off). Tours the user starts themselves are never capped.
319
+
320
+ ## `advanceOn: 'press'`
321
+
322
+ A step with `advanceOn: 'press'` moves on when the user presses the anchored element itself, instead of a Next button:
323
+
324
+ ```ts
325
+ { key: 'open_menu', anchor: 'nav.menu', title: 'Open the menu', body: 'Tap it.', advanceOn: 'press' }
326
+ ```
327
+
328
+ ## Backend compatibility
329
+
330
+ A server speaks the contract `httpSource` and `createSaparRtk` use (`inbox/`, `tours/<key>/`, `tours/<key>/progress/`, `seen/`, `dismiss/`, `announcements/`, `announcements/<key>/click/`). Anchors must stay greppable so the backend's `tours_check` and `tours_anchors` commands keep working: spell them as literals, either `useTourAnchor('key')` or `data-sapar="key"`, never built dynamically.
331
+
332
+ Pass `locales` to the provider if your routes carry a locale prefix; it must match the backend's setting.
333
+
334
+ ## Testing
335
+
336
+ `createTestAdapter` is a scriptable platform adapter. Pair it with a fake source and the controller:
337
+
338
+ ```ts
339
+ import { createTestAdapter, createSaparController, memorySource, memoryStore } from '@thelol3882/sapar';
340
+
341
+ const adapter = createTestAdapter({ anchors: ['nav.menu'], route: '/dashboard' });
342
+ const controller = createSaparController({
343
+ source: memorySource({ tours: [onboarding], storage: memoryStore() }),
344
+ adapter,
345
+ });
346
+
347
+ controller.setRoute('/dashboard');
348
+ ```
349
+
350
+ `delayedAnchors` plus `showAnchor` simulate anchors that mount late.
351
+
352
+ ## Migrating from 0.1
353
+
354
+ Every change that alters what a value means also changes its type, so the compiler finds what to update. The full list with one-line migrations is in [CHANGELOG.md](CHANGELOG.md#020); in short:
355
+
356
+ - Step numbers are one-based and passed as objects: `useTourStep().position` (was `index`), `announceStep={({ position, total, title }) => …}` (was `announce(index, …)`), `<TourStep.Progress>{({ position, total }) => …}` (was `(index, total)`). Drop every `+ 1`.
357
+ - Remove `anchorTimeoutMs` workarounds: missing anchors are skipped after `presentAnchorTimeoutMs` (300 ms) and auto-started tours wait for their first anchor (`autoStartTimeoutMs`, 10 s).
358
+ - On Expo, phones now get the card next to the element; pass `presentation="sheet"` for the old bottom sheet, and drop `compactBreakpointPx: 0`. Brand colours go through `colors`.
359
+ - `sapar init --tours-dir` chooses where definitions and the provider go; generated memory providers take `userId` and `registeredAt`.
360
+ - Re-run `sapar add <Name> --force` to pick up the template fixes, then re-apply your styles.
361
+
362
+ ## Entry points
363
+
364
+ | Import | Contents |
365
+ | --- | --- |
366
+ | `@thelol3882/sapar` | Core: types, `defineTour`, `defineAnnouncement`, `defineAnchors`, `createSaparController`, `memorySource`, `httpSource`, `createTestAdapter` |
367
+ | `@thelol3882/sapar/react` | `SaparProvider`, `useSapar`, `useTourStep`, `useWhatsNew`, `useWhatsNewPanel`, `useAnnouncementInterruption`, `useSaparController`, `useSaparSnapshot` (no DOM: shared by web and Expo) |
368
+ | `@thelol3882/sapar/web` | `useTourAnchor`, `BodyPortal`, `createWebAdapter`, `localStorageStore`, `browserSessionStore`, `TourOverlay`, `TourSpotlight`, `TourPopover`, `TourStep.*` |
369
+ | `@thelol3882/sapar/next` | `NextSaparProvider`, `useNextSaparAdapter` |
370
+ | `@thelol3882/sapar/rtk` | `createSaparRtk` |
371
+ | `@thelol3882/sapar/expo` | `useTourAnchor`, `createExpoAdapter`, `TourScrollView`, `asyncStorageStore`, `mmkvStore`, `TourOverlay`, `TourSpotlight`, `TourPopover`, `TourStep.*`, `computeCardPosition` |
372
+ | `@thelol3882/sapar/expo-router` | `ExpoSaparProvider`, `useExpoRouterAdapter` |
373
+ | `sapar` (bin) | `init`, `add`, `check` |
374
+
375
+ ## Maintenance
376
+
377
+ Sapar is a side project, built for my own apps and shared as is. I improve it when my projects need something, so there is no schedule and no promise of support or fixes. Issues and pull requests are welcome; I read them when I can. MIT licensed: use it, fork it, change it.
378
+
379
+ ## License
380
+
381
+ [MIT](LICENSE)
@@ -0,0 +1,14 @@
1
+ import { p as SessionStore, b as PlatformAdapter } from './types-CHD2pB5X.js';
2
+
3
+ interface WebAdapterOptions {
4
+ /** Push navigation, e.g. `router.push`. */
5
+ navigate: (route: string) => void | Promise<void>;
6
+ /** The route the host router currently reports. */
7
+ currentRoute: () => string;
8
+ /** Viewports narrower than this are compact. Default 768. */
9
+ compactBreakpointPx?: number;
10
+ session?: SessionStore;
11
+ }
12
+ declare function createWebAdapter(options: WebAdapterOptions): PlatformAdapter;
13
+
14
+ export { type WebAdapterOptions as W, createWebAdapter as c };
@@ -0,0 +1,14 @@
1
+ import { p as SessionStore, b as PlatformAdapter } from './types-CHD2pB5X.js';
2
+
3
+ interface ExpoAdapterOptions {
4
+ /** Push navigation, e.g. `router.push`. */
5
+ navigate: (route: string) => void | Promise<void>;
6
+ /** The route the host router currently reports. */
7
+ currentRoute: () => string;
8
+ /** With `presentation="auto"`, windows narrower than this always get the bottom sheet. Default 0: never. */
9
+ compactBreakpointPx?: number;
10
+ session?: SessionStore;
11
+ }
12
+ declare function createExpoAdapter(options: ExpoAdapterOptions): PlatformAdapter;
13
+
14
+ export { type ExpoAdapterOptions as E, createExpoAdapter as c };