{"_id":"@apollovisionlabs/guide-core","_rev":"5-8bb743a5fc01a6ee0c052b1d3e1c8f0b","name":"@apollovisionlabs/guide-core","dist-tags":{"latest":"0.3.1"},"versions":{"0.1.0":{"name":"@apollovisionlabs/guide-core","version":"0.1.0","keywords":["react","onboarding","product-tour","walkthrough","mui","accessibility"],"license":"MIT","_id":"@apollovisionlabs/guide-core@0.1.0","maintainers":[{"name":"remydeme","email":"contact@apollovisionlabs.com"}],"homepage":"https://github.com/apollovisionlabs/guide#readme","bugs":{"url":"https://github.com/apollovisionlabs/guide/issues"},"dist":{"shasum":"54cbcae9eb1b02ada2c92a377c4bf471bb92a064","tarball":"https://registry.npmjs.org/@apollovisionlabs/guide-core/-/guide-core-0.1.0.tgz","fileCount":9,"integrity":"sha512-wje3pCVUAebwZ+z2+86k6kOXsZfSMR01lE9M4vspEu+Pf6vl2yAEXcxsLyIWZu8Tp6St081lQL0yBAIHNYtang==","signatures":[{"sig":"MEUCIBAK07PJVT+5AqzApt7feNa//XdaNNryDLBNAFX4KZa3AiEAoxJdrSCNm9J6B2uu8bJN8FHIGTeMN1dK5wZT4GMN2Bo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":136513},"main":"./dist/index.cjs","type":"module","_from":"file:apollovisionlabs-guide-core-0.1.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"remydeme","email":"contact@apollovisionlabs.com"},"_resolved":"/private/var/folders/7q/5_vym4v57b55kmnn8xmw9zrm0000gn/T/e9f1eed5a32e9e8a1b24f979c356199e/apollovisionlabs-guide-core-0.1.0.tgz","_integrity":"sha512-wje3pCVUAebwZ+z2+86k6kOXsZfSMR01lE9M4vspEu+Pf6vl2yAEXcxsLyIWZu8Tp6St081lQL0yBAIHNYtang==","repository":{"url":"git+https://github.com/apollovisionlabs/guide.git","type":"git"},"_npmVersion":"10.9.4","description":"`guide` is a headless React library for building in-app product tours. `@apollovisionlabs/guide-core` owns the state machine, target resolution, routing, persistence and accessibility concerns, and exposes them as hooks with no rendering opinion. `@apollo","directories":{},"sideEffects":false,"_nodeVersion":"22.21.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@types/react":"^19.2.0"},"peerDependencies":{"react":"^19"},"_npmOperationalInternal":{"tmp":"tmp/guide-core_0.1.0_1788366688848_0.9424221537236133","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@apollovisionlabs/guide-core","version":"0.1.1","keywords":["react","onboarding","product-tour","walkthrough","mui","accessibility"],"license":"MIT","_id":"@apollovisionlabs/guide-core@0.1.1","maintainers":[{"name":"remydeme","email":"contact@apollovisionlabs.com"}],"homepage":"https://github.com/apollovisionlabs/guide#readme","bugs":{"url":"https://github.com/apollovisionlabs/guide/issues"},"dist":{"shasum":"37604286267e9b6ee6ff4fb5ecfa5364c671b041","tarball":"https://registry.npmjs.org/@apollovisionlabs/guide-core/-/guide-core-0.1.1.tgz","fileCount":9,"integrity":"sha512-mrtoFfvCPs1M4c11v/Be4B3XyJt8oNx+PhVbVKnukyoLGzPww2jL7NJhHzO7TC0GBgFHuagH2JT/N42Gs48+Ig==","signatures":[{"sig":"MEUCIQCx8jp67T+J4zqOHymOchQRQJVTLnjSIrnjc0WFpseLDwIgaR94CvJuigq5QBt89ikGjELctTdE8U0G8VNkMk7ayko=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIHyCHHvM34BDZ3lDMy/jH/pN4uGI8mLFuDbPIQeeppLqAiEA27rhjmXEzRr7NmkZ7qWH/cWIfLxFDbdJgBgcHx7LgiE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@apollovisionlabs%2fguide-core@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":136513},"main":"./dist/index.cjs","type":"module","_from":"file:/tmp/tmp.uG4uTcTSm3/apollovisionlabs-guide-core-0.1.1.tgz","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:10ad7011-ddff-41f4-930a-e71ed4bea3ff"}},"_resolved":"/tmp/tmp.uG4uTcTSm3/apollovisionlabs-guide-core-0.1.1.tgz","_integrity":"sha512-mrtoFfvCPs1M4c11v/Be4B3XyJt8oNx+PhVbVKnukyoLGzPww2jL7NJhHzO7TC0GBgFHuagH2JT/N42Gs48+Ig==","repository":{"url":"git+https://github.com/apollovisionlabs/guide.git","type":"git"},"_npmVersion":"12.0.2","description":"`guide` is a headless React library for building in-app product tours. `@apollovisionlabs/guide-core` owns the state machine, target resolution, routing, persistence and accessibility concerns, and exposes them as hooks with no rendering opinion. `@apollo","directories":{},"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@types/react":"^19.2.0"},"peerDependencies":{"react":"^19"},"_npmOperationalInternal":{"tmp":"tmp/guide-core_0.1.1_1788385519691_0.7300234495264124","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@apollovisionlabs/guide-core","version":"0.2.0","keywords":["react","onboarding","product-tour","walkthrough","mui","accessibility"],"license":"MIT","_id":"@apollovisionlabs/guide-core@0.2.0","maintainers":[{"name":"remydeme","email":"contact@apollovisionlabs.com"}],"homepage":"https://github.com/apollovisionlabs/guide#readme","bugs":{"url":"https://github.com/apollovisionlabs/guide/issues"},"dist":{"shasum":"b0c5c82c114de13fc67d7794de534c9b77441a86","tarball":"https://registry.npmjs.org/@apollovisionlabs/guide-core/-/guide-core-0.2.0.tgz","fileCount":9,"integrity":"sha512-ZvcxjJiohd8g3m96X4PknA04gfIqzn19GK7WdEh1KAv9b8Wj8hqkonpMsqNDxSya3L1Az2sFH2bMR8+SNO9Sgg==","signatures":[{"sig":"MEQCIFFzpS3CPwRpTWsJfo3VGdygTLUy631KAjOZyP9h7arVAiA9fZqgk+SZkkL1ErKXGvoDTZYaPFLVLwZ19Tgnl56ZFA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQCxcDaW0wWQU6u7hyCA164oGQCV8KRmIRKh0+60YqYYxgIhAIISiAZfLFaNgO0Oyw5fe3xDelDdZugwqd3I87bHZPOV","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@apollovisionlabs%2fguide-core@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":220487},"main":"./dist/index.cjs","type":"module","_from":"file:/tmp/tmp.DQOjCujyZ3/apollovisionlabs-guide-core-0.2.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:10ad7011-ddff-41f4-930a-e71ed4bea3ff"}},"_resolved":"/tmp/tmp.DQOjCujyZ3/apollovisionlabs-guide-core-0.2.0.tgz","_integrity":"sha512-ZvcxjJiohd8g3m96X4PknA04gfIqzn19GK7WdEh1KAv9b8Wj8hqkonpMsqNDxSya3L1Az2sFH2bMR8+SNO9Sgg==","repository":{"url":"git+https://github.com/apollovisionlabs/guide.git","type":"git"},"_npmVersion":"12.0.2","description":"`guide` is a headless React library for building in-app product tours. `@apollovisionlabs/guide-core` owns the state machine, target resolution, routing, persistence and accessibility concerns, and exposes them as hooks with no rendering opinion. `@apollo","directories":{},"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@types/react":"^19.2.0"},"peerDependencies":{"react":"^19"},"_npmOperationalInternal":{"tmp":"tmp/guide-core_0.2.0_1788452061534_0.28400568551507255","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@apollovisionlabs/guide-core","version":"0.3.0","keywords":["react","onboarding","product-tour","walkthrough","mui","accessibility"],"license":"MIT","_id":"@apollovisionlabs/guide-core@0.3.0","maintainers":[{"name":"remydeme","email":"contact@apollovisionlabs.com"}],"homepage":"https://github.com/apollovisionlabs/guide#readme","bugs":{"url":"https://github.com/apollovisionlabs/guide/issues"},"dist":{"shasum":"b7bf64b6858cce4b2fe472068e32089b3580d2f4","tarball":"https://registry.npmjs.org/@apollovisionlabs/guide-core/-/guide-core-0.3.0.tgz","fileCount":9,"integrity":"sha512-J0Fcj1WdpEkQW2ylvNwsgXJuFCQBXvgWe2xxbhOJouxFjEHWSogH5KdbJAcZIDimUqFsH6H+Ys83kWszvm+QNw==","signatures":[{"sig":"MEQCIDPmqaym7F4ORCGGD8lD5CvcQjwRB7pjAEktrwXNJK0uAiAcNVtZfnpsBVEOlAAZ7bjvflGIzWATOG+hcfxkKMpXSw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDXqUPmnWJpXY5ORZq10WxsgfxDk6nBdkhiariSrFZnVwIgENwQvhBBMf3nlgXwAuQretpwborBZFf2SIWbPui7NrQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@apollovisionlabs%2fguide-core@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":297766},"main":"./dist/index.cjs","type":"module","_from":"file:/tmp/tmp.sGRlmJeaJk/apollovisionlabs-guide-core-0.3.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:10ad7011-ddff-41f4-930a-e71ed4bea3ff"}},"_resolved":"/tmp/tmp.sGRlmJeaJk/apollovisionlabs-guide-core-0.3.0.tgz","_integrity":"sha512-J0Fcj1WdpEkQW2ylvNwsgXJuFCQBXvgWe2xxbhOJouxFjEHWSogH5KdbJAcZIDimUqFsH6H+Ys83kWszvm+QNw==","repository":{"url":"git+https://github.com/apollovisionlabs/guide.git","type":"git"},"_npmVersion":"12.0.2","description":"`guide` is a headless React library for building in-app product tours. `@apollovisionlabs/guide-core` owns the state machine, target resolution, routing, persistence and accessibility concerns, and exposes them as hooks with no rendering opinion. `@apollo","directories":{},"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@types/react":"^19.2.0"},"peerDependencies":{"react":"^19"},"_npmOperationalInternal":{"tmp":"tmp/guide-core_0.3.0_1788535671174_0.5622583907278982","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"_id":"@apollovisionlabs/guide-core@0.3.1","bugs":{"url":"https://github.com/apollovisionlabs/guide/issues"},"dist":{"shasum":"9ea73b8d2b2d53de8838e6dc85673c1ff8f2cbc3","tarball":"https://registry.npmjs.org/@apollovisionlabs/guide-core/-/guide-core-0.3.1.tgz","fileCount":9,"integrity":"sha512-z0ZwD1+pqr/ohKoqe322diZeoAwNO/4FY92I9sNqiJd8bibWfUx9q1js2UyavVq9rhA9k94OpBL09ZxCElVR9w==","signatures":[{"sig":"MEQCIDM2x3fcWDKnniObdNPEFbcStz4ae2YWOheOn45Zlu1fAiBgIC6bCtsPIIY4G3q02Hj5ucmI4f5kUTkJ9fhLEHWQkg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDNab9bqe6wDKDff62YHVdJnG8YW7EeYWUFxEDYXuj1EgIgPts59u6yrGPbpsIDQUtMBdVw9n9IILj+9m34sIY0h2Q="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@apollovisionlabs%2fguide-core@0.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":298028},"main":"./dist/index.cjs","name":"@apollovisionlabs/guide-core","type":"module","_from":"file:/tmp/tmp.SqaVQ1TZtt/apollovisionlabs-guide-core-0.3.1.tgz","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"license":"MIT","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit"},"version":"0.3.1","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:10ad7011-ddff-41f4-930a-e71ed4bea3ff"}},"homepage":"https://github.com/apollovisionlabs/guide#readme","keywords":["react","onboarding","product-tour","walkthrough","mui","accessibility"],"_resolved":"/tmp/tmp.SqaVQ1TZtt/apollovisionlabs-guide-core-0.3.1.tgz","_integrity":"sha512-z0ZwD1+pqr/ohKoqe322diZeoAwNO/4FY92I9sNqiJd8bibWfUx9q1js2UyavVq9rhA9k94OpBL09ZxCElVR9w==","repository":{"url":"git+https://github.com/apollovisionlabs/guide.git","type":"git"},"_npmVersion":"12.0.2","description":"`guide` is a headless React library for building in-app product tours. `@apollovisionlabs/guide-core` owns the state machine, target resolution, routing, persistence and accessibility concerns, and exposes them as hooks with no rendering opinion. Two pack","directories":{},"maintainers":[{"name":"remydeme","email":"contact@apollovisionlabs.com"}],"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@types/react":"^19.2.0"},"peerDependencies":{"react":"^19"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/guide-core_0.3.1_1788636343889_0.3071407708229017"}}},"time":{"created":"2026-09-02T16:31:28.572Z","modified":"2026-09-05T19:25:44.355Z","0.1.0":"2026-09-02T16:31:28.997Z","0.1.1":"2026-09-02T21:45:19.781Z","0.2.0":"2026-09-03T16:14:21.632Z","0.3.0":"2026-09-04T15:27:51.303Z","0.3.1":"2026-09-05T19:25:44.002Z"},"bugs":{"url":"https://github.com/apollovisionlabs/guide/issues"},"license":"MIT","homepage":"https://github.com/apollovisionlabs/guide#readme","keywords":["react","onboarding","product-tour","walkthrough","mui","accessibility"],"repository":{"url":"git+https://github.com/apollovisionlabs/guide.git","type":"git"},"description":"`guide` is a headless React library for building in-app product tours. `@apollovisionlabs/guide-core` owns the state machine, target resolution, routing, persistence and accessibility concerns, and exposes them as hooks with no rendering opinion. Two pack","maintainers":[{"name":"remydeme","email":"contact@apollovisionlabs.com"}],"readme":"# guide\n\n`guide` is a headless React library for building in-app product tours. `@apollovisionlabs/guide-core` owns the\nstate machine, target resolution, routing, persistence and accessibility concerns, and exposes\nthem as hooks with no rendering opinion. Two packages render on top of those hooks out of the box:\n`@apollovisionlabs/guide-mui` with [MUI](https://mui.com) components (a spotlight overlay and a\npopover), and `@apollovisionlabs/guide-unstyled` with the same parts as plain DOM elements and an\noptional stylesheet. You can also render your own UI on top of `@apollovisionlabs/guide-core`\ndirectly.\n\nThis repository includes a runnable demo. From the repo root, run `pnpm --filter demo dev` and\nopen `http://localhost:5173` to try a three-page tour end to end.\n\n## Installation\n\n```bash\npnpm add @apollovisionlabs/guide-core @apollovisionlabs/guide-mui @mui/material @emotion/react @emotion/styled\n```\n\n`@apollovisionlabs/guide-mui` depends on `@apollovisionlabs/guide-core`, so both are needed to render the default UI. If you only\nwant the state machine and plan to render your own popover and spotlight, `@apollovisionlabs/guide-core` alone is\nenough.\n\n## Minimal example\n\n```tsx\nimport { GuideProvider, type Tour } from '@apollovisionlabs/guide-core'\nimport { GuideTour } from '@apollovisionlabs/guide-mui'\n\nconst tour: Tour = {\n  id: 'welcome',\n  steps: [\n    {\n      target: 'sidebar.projects',\n      title: 'Your projects',\n      body: 'Everything you create is grouped under a project.',\n    },\n  ],\n}\n\nfunction App() {\n  return (\n    <GuideProvider tours={[tour]}>\n      <Sidebar />\n      <GuideTour />\n    </GuideProvider>\n  )\n}\n\nfunction Sidebar() {\n  // The target string is matched against the data-guide attribute, not a CSS selector or a ref.\n  return <nav data-guide=\"sidebar.projects\">Projects</nav>\n}\n```\n\nStart the tour from anywhere under the provider with `useTour('welcome').start()`.\n\nDeclare tours as module constants, as above, rather than as literals built inside a component.\nThe provider compares step objects by identity, so a tour rebuilt on every render prevents the\nmissing-target policy below from ever firing.\n\n## `GuideTour` props\n\n| Prop | Type | Default | Description |\n| --- | --- | --- | --- |\n| `labels` | `Partial<{ next, previous, finish, close, awaitingAction }>` | `{ next: 'Next', previous: 'Back', finish: 'Finish', close: 'Close', awaitingAction: 'Click the highlighted element to continue.' }` | The popover's button labels. Defaults are English; override any subset. See \"Translations\". |\n| `zIndex` | `number` | `theme.zIndex.modal` | Stacking level of the spotlight; the popover sits one above it. |\n| `padding` | `number` | `8` | Margin, in pixels, between the highlighted element and the edge of the spotlight hole. |\n| `radius` | `number` | `8` | Corner radius, in pixels, of the spotlight hole. |\n\n## `GuideProvider` props\n\n| Prop | Type | Default | Description |\n| --- | --- | --- | --- |\n| `tours` | `Tour[]` | none | The tours available to `start()`. Tour ids must be unique. |\n| `children` | `ReactNode` | none | Your application. |\n| `navigate` | `(path: string) => void` | none | Called when a step declares a page it needs. Required for multi-page tours. |\n| `location` | `string` | none | The current pathname, used to decide whether a step's target should be on screen. Required for multi-page tours. |\n| `storage` | `GuideStorage` | none | Persists tour progress. See \"Persistence\". |\n| `translate` | `(key: string) => string` | none | Resolves `titleKey` / `bodyKey` on steps. See \"Translations\". |\n| `onEvent` | `(event: GuideEvent) => void` | none | Called for every lifecycle event. See \"Events\". |\n| `onMissingTarget` | `'skip' \\| 'wait' \\| 'error'` | `'wait'` | Default policy when a step's target never appears. Overridable per step. |\n| `targetTimeoutMs` | `number` | `5000` | How long to wait for a target before applying the missing-target policy. |\n\n## Multi-page tours\n\nA step can declare the route it belongs to and, when it isn't the current one, where to navigate:\n\n```tsx\n{\n  target: 'projects.create',\n  route: '/projects',\n  navigateTo: '/projects',\n  title: 'Create a project',\n  body: 'This step lives on another page, and you were moved here automatically.',\n}\n```\n\n`route` accepts `:param` segments and a trailing `*` wildcard, and is only used to decide whether\nthe current page already satisfies the step. `navigateTo` is the concrete path passed to\n`navigate`; when it is omitted and `route` is a literal path (no `:` or `*`), that route is used as\nthe destination.\n\n## Persistence\n\n`GuideStorage` is the two-method interface both `GuideProvider` and `ChecklistProvider` read from\nand write to. It is generic over the stored value, so one storage serves tour progress and\nchecklist progress under different keys:\n\n```ts\ninterface GuideStorage {\n  read<T>(key: string): Promise<T | null>\n  write<T>(key: string, value: T): Promise<void>\n}\n```\n\n`GuideProvider` reads and writes tour progress under `tour:<id>`. `ChecklistProvider` reads and\nwrites checklist progress under `checklist:<id>`. `HotspotProvider` reads and writes which\nhotspots have been opened under the single key `hotspots:seen`. See\n[ADR 0016](docs/adr/0016-one-storage-contract-for-tours-and-checklists.md) for why they share one\ninterface.\n\n`@apollovisionlabs/guide-core` ships `createMemoryStorage()` for tests and `createBrowserStorage(namespace?)` for\n`localStorage`. Neither talks to a server. An implementation backed by your own API looks like\nthis:\n\n```ts\nimport type { GuideStorage } from '@apollovisionlabs/guide-core'\n\nfunction createServerStorage(): GuideStorage {\n  return {\n    async read<T>(key: string) {\n      const response = await fetch(`/api/guide/${key}`)\n      if (!response.ok) return null\n      return (await response.json()) as T\n    },\n    async write<T>(key: string, value: T) {\n      await fetch(`/api/guide/${key}`, {\n        method: 'PUT',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(value),\n      })\n    },\n  }\n}\n```\n\nPass it as the `storage` prop on either provider. `GuideProvider` reads on `start()` (unless an\nexplicit `from` step index is passed, or `resume: false` is passed) and writes whenever a running\ntour advances or completes. `ChecklistProvider` reads once on mount and writes whenever an item is\nticked, completed or the checklist is dismissed.\n\nA value read back from storage is validated before it is trusted (`isTourProgress`,\n`isChecklistProgress`, `isHotspotsProgress`, all exported from `@apollovisionlabs/guide-core`): a\nvalue that does not\nmatch the expected shape, from a hand-edited store or an older version of this library, is treated\nthe same as nothing stored, rather than crashing or resuming into a broken state.\n\n## Translations\n\nEvery step's own text is supplied by the consumer. A step can set `title` / `body` directly, or\n`titleKey` / `bodyKey` plus a `translate` function on `GuideProvider`; the key is passed through\nyour translation library and the result is displayed. When a key is set without a `translate`\nprop, the raw key is shown instead, so wiring `translate` is required for `titleKey` / `bodyKey`\nto resolve to real strings.\n\nThe popover's own chrome is the one exception: `@apollovisionlabs/guide-mui` ships English default labels\n(`Next`, `Back`, `Finish`, `Close`), so the buttons read correctly out of the box. Every one of\nthem is overridable through the `labels` prop on `GuideTour`: pass the labels in your language\nand nothing English remains:\n\n```tsx\n<GuideTour labels={{ next: 'Suivant', previous: 'Retour', finish: 'Terminer', close: 'Fermer' }} />\n```\n\n## Events\n\n`onEvent` on `GuideProvider`, `ChecklistProvider` and `HotspotProvider` each receive their own\nlifecycle events, as a discriminated union of `GuideEvent`:\n\n| Event | Payload | When |\n| --- | --- | --- |\n| `tour:start` | `{ tourId, stepIndex }` | `start()` is called. |\n| `tour:complete` | `{ tourId }` | `next()` is called on the last step. |\n| `tour:stop` | `{ tourId, stepIndex }` | The tour is stopped before completion. |\n| `step:show` | `{ tourId, stepIndex, target }` | A step's target is resolved and the step becomes visible. |\n| `target:missing` | `{ tourId, stepIndex, target }` | A step's target didn't appear within `targetTimeoutMs`. |\n| `checklist:item-complete` | `{ checklistId, itemId }` | An item is completed, by finishing its linked tour or by a manual tick. Not emitted for an item already complete. |\n| `checklist:complete` | `{ checklistId }` | The last incomplete item in a checklist is completed. Fires on every transition into the complete state, so unticking an item and reticking it emits a second time. Deduplicate downstream if you count completions. |\n| `checklist:dismiss` | `{ checklistId }` | `dismiss()` is called. |\n| `hotspot:show` | `{ hotspotId }` | A hotspot's marker is actually drawn on screen. Emitted once per hotspot per mount. |\n| `hotspot:open` | `{ hotspotId }` | The hotspot's bubble is opened, which also marks it seen. Not emitted again for a bubble that is already open. |\n\n## Accessibility\n\n- The current step position is announced through a visually-hidden `aria-live=\"polite\"` region,\n  so screen reader users hear \"2 / 4\" as the tour advances.\n- The step popover traps keyboard focus and is exposed as `role=\"dialog\"` with\n  `aria-labelledby` / `aria-describedby`, except for a step marked `interactive: true`, which\n  deliberately does **not** trap focus, so the user can tab or click past the popover to reach the\n  element they're asked to interact with.\n- `Escape` stops the tour, `ArrowRight` advances, `ArrowLeft` goes back. All three are ignored\n  while focus is in a text input, so typing isn't hijacked.\n- The highlighted element receives `aria-describedby`, pointing at the step's body text.\n- The spotlight respects `prefers-reduced-motion` and disables its transition when set.\n\n## Missing targets\n\nEach step is checked against its `target` (matched by a `data-guide` attribute) for up to\n`targetTimeoutMs` (default 5 seconds, per-provider). If the target never appears, the step's own\n`onMissingTarget` (or the provider's `onMissingTarget`, which defaults to `'wait'`) decides what\nhappens: `'skip'` moves to the next step, `'error'` stops the tour, and `'wait'` (the default)\npauses and resumes automatically if the target appears later, for instance after a slow async\nrender.\n\n## Advancing on an action\n\nA step can declare `advanceOn: 'click'` instead of ending on the popover's button:\n\n```ts\n{\n  target: 'project.share',\n  title: 'Share it',\n  body: 'Click the button yourself, this step is interactive.',\n  advanceOn: 'click',\n}\n```\n\nThe step advances when the user clicks the target, not the popover. `advanceOn` implies\n`interactive`: a step that waits for a click has to let the click through, so `GuideProvider`\nderives both `interactive` and `awaitsAction` on `ActiveStep` from `advanceOn`, rather than\nrequiring both to be set by hand. Read `activeStep.interactive` / `activeStep.awaitsAction`, not\n`step.interactive`, which is left `undefined` on a step that only sets `advanceOn`. See\n[ADR 0017](docs/adr/0017-advancing-on-an-action-implies-an-interactive-step.md).\n\n`@apollovisionlabs/guide-mui`'s popover reflects `awaitsAction`: no primary button, and the\n`awaitingAction` label in its place (see the `labels` row above). `ArrowRight` is ignored while a\nstep awaits its action, since letting it through would be a way around the very thing the step is\nasking for; `Escape` and `ArrowLeft` still work.\n\nThe click listener is attached, in the bubble phase, to the element resolved when the step\nopened, without `preventDefault` or `stopPropagation`, so your own click handler on the target\nstill runs.\n\nIf your application replaces that DOM node afterward, for instance by re-rendering a list, the\nlistener goes with it and the step stops advancing. Nothing notices: the target was found once,\nso the timeout was already cleared, no `target:missing` is emitted and no `wait`, `skip` or\n`error` policy runs. The tour simply sits on that step. The consequence is specific to\n`advanceOn`, even though the cause is not: an `advanceOn` step offers no primary button and\nignores `ArrowRight`, so a replaced node leaves the tour with `Escape` as its only exit. If the\nelement a step points at can be re-created under it, either give it a stable target that survives\nthe re-render or use an ordinary step with a Next button.\n\n## Checklist\n\nA checklist is a separate feature from the tour: a fixed list of items, each completed by\nfinishing a linked tour or by a manual tick. An item can also carry an `href`, which navigates\nand nothing more. `ChecklistProvider` holds\nits state the way `GuideProvider` holds tour state, and nests inside it so that an item can launch\na tour:\n\n```tsx\nimport { GuideProvider, ChecklistProvider, type Tour, type Checklist } from '@apollovisionlabs/guide-core'\nimport { GuideTour } from '@apollovisionlabs/guide-mui'\nimport { ChecklistLauncher } from '@apollovisionlabs/guide-mui'\n\nconst tour: Tour = {\n  id: 'welcome',\n  steps: [{ target: 'sidebar.projects', title: 'Your projects', body: 'Grouped under a project.' }],\n}\n\nconst onboarding: Checklist = {\n  id: 'onboarding',\n  items: [\n    { id: 'tour', title: 'Take the tour', body: 'Two minutes.', tourId: 'welcome' },\n    { id: 'projects', title: 'Open your projects', body: 'See the list.', href: '/projects' },\n    { id: 'profile', title: 'Set your name', body: 'Manual, ticked by hand.' },\n  ],\n}\n\nfunction App() {\n  return (\n    <GuideProvider tours={[tour]} navigate={(path) => router.push(path)}>\n      <ChecklistProvider checklists={[onboarding]} navigate={(path) => router.push(path)}>\n        <Sidebar />\n        <GuideTour />\n        <ChecklistLauncher checklistId=\"onboarding\" title=\"Get started\" />\n      </ChecklistProvider>\n    </GuideProvider>\n  )\n}\n```\n\nAn item with an `href`, or with neither `tourId` nor `href`, is completed by a manual tick only:\nactivating an `href` item navigates and stops there, since arriving on a page is not evidence that\nanyone did anything on it. An item with `tourId`\nis completed automatically when that tour is finished (`next()` called on its last step); it can\nalso be ticked by hand before that. Completion is idempotent: ticking an already-complete item, or\nfinishing a tour whose item is already ticked, does nothing and emits no event.\n\n### `ChecklistProvider` props\n\n| Prop | Type | Default | Description |\n| --- | --- | --- | --- |\n| `checklists` | `Checklist[]` | none | The checklists available to `useChecklist`. |\n| `children` | `ReactNode` | none | Your application. |\n| `storage` | `GuideStorage` | none | Persists checklist progress under `checklist:<id>`. See \"Persistence\". |\n| `translate` | `(key: string) => string` | none | Resolves `titleKey` / `bodyKey` on items. See \"Translations\". |\n| `navigate` | `(path: string) => void` | none | Called when an item with `href` is activated. |\n| `onEvent` | `(event: GuideEvent) => void` | none | Called for `checklist:item-complete`, `checklist:complete`, `checklist:dismiss`. See \"Events\". |\n\n### `useChecklist(checklistId)`\n\nReturns `{ items, completedCount, total, isComplete, dismissed, restored, activate, toggle, complete, dismiss, reset }`.\n`items` is `ResolvedChecklistItem[]`: `{ id, title, body, completed, tourId?, href? }`, with\n`title` / `body` already resolved through `translate`. `activate(itemId)` runs an item's default\naction (start its tour, navigate to its `href`, or toggle it if it has neither); `toggle` and\n`complete` change completion directly; `dismiss()` and `reset()` act on the whole checklist.\n`restored` is whether this checklist's own initial read from storage has settled: `true`\nimmediately with no `storage` prop (there is nothing to wait for), and `true` once this\nchecklist's own read has resolved or rejected. It settles independently per checklist, so a\n`ChecklistProvider` holding several checklists never lets a slow or hung read for one hold\nanother one's `restored` false; each checklist's read runs concurrently with the others.\n\n### `Checklist` and `ChecklistLauncher` (`@apollovisionlabs/guide-mui`)\n\n`Checklist` renders the list inline: a progress bar, one row per item with a checkbox and a\ndismiss button. `ChecklistLauncher` wraps it behind a floating action button showing\n`completedCount/total`, opened as a popover. The popover stays open while items are ticked one\nafter another, and closes when an item hands off to something that needs the screen: launching a\ntour or navigating to an `href`. It does not close on a plain tick.\n\n```tsx\nimport { Checklist, ChecklistLauncher } from '@apollovisionlabs/guide-mui'\n\n<Checklist checklistId=\"onboarding\" title=\"Get started\" />\n// or, as a floating launcher:\n<ChecklistLauncher checklistId=\"onboarding\" title=\"Get started\" placement=\"bottom-right\" />\n```\n\nWith a `storage` prop configured on `ChecklistProvider`, both `Checklist` and `ChecklistLauncher`\nwait for their own checklist's restore to settle (`useChecklist(checklistId).restored`) before\ndrawing anything, rather than rendering their empty initial state (nothing completed, not\ndismissed) for one paint. The tradeoff: with a slow storage backend, a checklist now appears later\nthan it used to, instead of appearing at once and then jumping. A slow or broken read for one\nchecklist never holds a different checklist back; each restores on its own.\n\n## Hotspots\n\nA hotspot marks one element outside any tour: a small marker that opens a short explanation, and\noptionally a button that starts a tour. Unlike a tour step, a hotspot has no route and no order;\nit just sits at its target until opened. `HotspotProvider` nests inside `GuideProvider`, the same\nway `ChecklistProvider` does, so a hotspot naming a `tourId` can start it:\n\n```tsx\nimport {\n  GuideProvider,\n  HotspotProvider,\n  type Hotspot,\n  type Tour,\n} from '@apollovisionlabs/guide-core'\nimport { GuideTour, Hotspots } from '@apollovisionlabs/guide-mui'\n\nconst welcomeTour: Tour = {\n  id: 'welcome',\n  steps: [\n    { target: 'projects.create', title: 'Create a project', body: 'Start here.' },\n    { target: 'project.share', title: 'Share it', body: 'Send a link to your team.' },\n  ],\n}\n\nconst hotspots: Hotspot[] = [\n  {\n    id: 'create',\n    target: 'projects.create',\n    title: 'Start a project',\n    body: 'Everything else in here hangs off a project.',\n  },\n  {\n    id: 'share',\n    target: 'project.share',\n    title: 'Share a project',\n    body: 'Send a link to anyone on your team.',\n    // Named tours must exist on the GuideProvider above, or starting one warns and does nothing.\n    tourId: 'welcome',\n  },\n]\n\nfunction App() {\n  return (\n    <GuideProvider tours={[welcomeTour]}>\n      <HotspotProvider hotspots={hotspots}>\n        <YourApplication />\n        <GuideTour />\n        <Hotspots />\n      </HotspotProvider>\n    </GuideProvider>\n  )\n}\n```\n\nWithout a `GuideProvider` above it, starting a hotspot's tour warns once in the console and does\nnothing.\n\n### `HotspotProvider` props\n\n| Prop | Type | Default | Description |\n| --- | --- | --- | --- |\n| `hotspots` | `Hotspot[]` | none | The hotspots to render. Ids must be unique. |\n| `children` | `ReactNode` | none | Your application. |\n| `storage` | `GuideStorage` | none | Persists which hotspots have been opened, under `hotspots:seen`. See \"Persistence\". |\n| `translate` | `(key: string) => string` | none | Resolves `titleKey` / `bodyKey` on hotspots. See \"Translations\". |\n| `onEvent` | `(event: GuideEvent) => void` | none | Called for `hotspot:show` and `hotspot:open`. See \"Events\". |\n\n### `useHotspots()`\n\nReturns `{ hotspots, restored, open, startTour, reset, notifyShown }`.\n\n- `hotspots` is `ResolvedHotspot[]`: every hotspot, each carrying its own `seen`, with `title` /\n  `body` already resolved through `translate`. It lists the seen ones too, rather than only the\n  unseen ones, because a renderer that keeps a marker mounted while its own bubble closes needs\n  the seen one as well; filtering to unseen-only is one line at the call site.\n- `restored` is whether the initial read from storage has settled: `true` immediately with no\n  `storage` prop (there is nothing to wait for), and `true` once the read resolves or rejects.\n  Wait for it before drawing any marker, or a hotspot already seen in storage can flash on screen\n  once before the restore lands.\n- `open(hotspotId)` marks a hotspot seen and emits `hotspot:open`.\n- `startTour(hotspotId)` starts the tour named by the hotspot's `tourId`, if it has one.\n- `reset()` clears the seen state for every hotspot.\n- `notifyShown(hotspotId)` is for renderers: call it once a marker is actually drawn on screen, so\n  `hotspot:show` fires once per hotspot per mount. `@apollovisionlabs/guide-mui`'s `Hotspots`\n  already calls it.\n\n### `Hotspots` (`@apollovisionlabs/guide-mui`)\n\nRenders a marker at each unseen hotspot's target; clicking it opens a bubble with the hotspot's\ntitle, body, and, when it names a `tourId`, a button that starts that tour.\n\n| Prop | Type | Default | Description |\n| --- | --- | --- | --- |\n| `labels` | `Partial<{ marker, startTour, close }>` | see below | Wording. `marker` is a function of the hotspot's title, not a fixed string, because word order around a name varies by language. |\n| `placement` | `Placement` | `'bottom'` | Where the bubble opens relative to the marker. Overridable per hotspot through `Hotspot.placement`. |\n| `zIndex` | `number` | `theme.zIndex.drawer + 1` | Stacking level of the marker; the bubble sits one above it. |\n\nDefault labels: `` { marker: (title) => `Show what is new: ${title}`, startTour: 'Show me', close: 'Close' } ``.\n\nNo marker is drawn while a tour is running or paused. A hotspot is an ambient hint and must not\ncompete with a guided flow the user is already in: a marker over the element a step points at\nwould take the click meant for that step, and one over a non-interactive step would be drawn\nbright and pulsing yet inert behind the spotlight. The markers come back when the tour ends,\nunchanged: this suppresses them, it does not mark them seen. `Hotspots` reads the tour state\nthrough context and tolerates its absence, so hotspots work with no `GuideProvider` in the tree.\n\n`paused` counts, because a paused tour is waiting for its target rather than finished. Note what\nthat implies at the edge: a tour paused on a target that never appears, the default `wait`\npolicy, draws nothing itself and now hides every hotspot too, for as long as it stays paused,\nwith `Escape` as the only way out and nothing on screen to suggest it. If your steps point at\ntargets that may never mount, prefer the `skip` or `error` missing-target policy over `wait`.\n\nThe default `zIndex` sits below `theme.zIndex.modal`, the level a running tour's spotlight uses,\nso a hotspot whose target lives inside your own modal dialog is covered by it. Raise `zIndex` on\n`Hotspots` to bring the marker above that dialog.\n\nA marker is drawn only for a target that has actual size on screen. An element that is in the DOM\nbut not rendered, `display: none` for instance, measures an all-zero rectangle; that draws no\nmarker and emits no `hotspot:show`, so a hotspot cannot be retired before the user has seen what\nit explains.\n\nClicking a marker whose bubble is already open closes the bubble, and emits no second\n`hotspot:open`.\n\nWith a `storage` prop configured on `HotspotProvider`, `Hotspots` waits for the initial restore to\nsettle (`useHotspots().restored`) before drawing any marker.\n\n## Compatibility\n\n| | Supported |\n| --- | --- |\n| React | 19 |\n| MUI (`@apollovisionlabs/guide-mui` only) | 7, 9 |\n| `@apollovisionlabs/guide-unstyled` | no UI toolkit; peers on `react` and `react-dom` only |\n| Rendering | ESM and CommonJS, with `\"use client\"` for Next's App Router |\n\n## Prior art\n\nThe spotlight-and-popover approach is inspired by [driver.js](https://driverjs.com) (MIT), as are\n[react-joyride](https://github.com/gilbarbara/react-joyride) (MIT) and\n[reactour](https://github.com/elrumordelaluz/reactour) (MIT). `guide` differs from all three mainly\nin splitting the state machine (`@apollovisionlabs/guide-core`) from rendering, which ships as two\npackages, `@apollovisionlabs/guide-mui` and `@apollovisionlabs/guide-unstyled`, so the same logic\nrenders through a design system or through plain DOM. Contributors must also read the licence discipline in\n`CONTRIBUTING.md` before looking at any other tour library.\n\n## Documentation\n\nThis file is the public API reference. Everything else lives in the repository:\n\n- [`ARCHITECTURE.md`](ARCHITECTURE.md): how the packages are layered and how the mechanisms work.\n- [`CONTRIBUTING.md`](CONTRIBUTING.md): prerequisites, commands, conventions, licence discipline.\n- [`INFRA.md`](INFRA.md): build, continuous integration, and the state of the release path.\n- [`SECURITY.md`](SECURITY.md): reporting, supported versions, what the packages touch.\n- [`docs/index.md`](docs/index.md): the full documentation map: playbooks, decisions, references.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","readmeFilename":"README.md"}