{
  "markdown": "# Elaan JavaScript SDKs\n\nClient SDKs for [Elaan](https://elaan.io) — drop-in notification inbox,\npreferences, and push-token management for your users' frontends.\n\nThis is the **JavaScript/TypeScript** family. Swift and Kotlin/Android live in\ntheir own repos (`elaan-swift`, `elaan-kotlin`).\n\n## Packages\n\n| Package | What it is | Registry |\n|---|---|---|\n| [`@elaanio/core`](./core) | Framework-agnostic foundation — API client, types, observable inbox/preferences stores, and a realtime-transport interface. No UI, no framework. | npm |\n| [`@elaanio/react-core`](./react-core) | React bindings only (no DOM) — `ElaanProvider` + hooks (`useNotifications`, `useUnreadCount`, `usePreferences`, `usePush`, `useBrowserPush`) over the core stores. Shared by web and native. | npm |\n| [`@elaanio/react`](./react) | React (web) components — notification bell, feed, and preferences UI, plus real-time updates over SSE-on-fetch. | npm |\n| [`@elaanio/react-native`](./react-native) | React Native components over the same hooks; realtime over SSE (`react-native-sse`) with polling fallback. | npm |\n| [`@elaanio/vue`](./vue) | Vue 3 components + composables — bell, feed, and preferences, with fetch-SSE realtime. | npm |\n| [`@elaanio/svelte`](./svelte) | Svelte stores (headless) — reactive inbox, unread count, and preferences; bring your own markup. | npm |\n| [`@elaanio/elements`](./elements) | Framework-agnostic Web Components — `<elaan-bell>` / `<elaan-feed>` / `<elaan-preferences>`. Drop into any page or framework. | npm |\n\n### How they fit together\n\n```\n@elaanio/core                  vanilla TS: client · stores · realtime transport\n   ├─ @elaanio/react-core           React provider + hooks (framework, no DOM)\n   │    ├─ @elaanio/react                web components + fetch-SSE realtime\n   │    └─ @elaanio/react-native         RN components (SSE via react-native-sse)\n   ├─ @elaanio/vue                  Vue 3 components + composables\n   ├─ @elaanio/svelte               Svelte stores (headless)\n   └─ @elaanio/elements             Web Components (works anywhere)\n```\n\nThe non-visual logic lives once in `@elaanio/core`; each framework package is a\nthin adapter over the core client + observable stores (React goes through the\nshared `@elaanio/react-core` hooks; Vue and Svelte bind the stores to their own\nreactivity). Adding another framework means a new binding over `@elaanio/core`,\nnot a reimplementation of the client or stores.\n\n## Authentication (all packages)\n\nThe SDK never sees your API key. Your backend mints a short-lived **contact\ntoken** for the signed-in user (`POST /v1/contacts/tokens` with your service\nkey, by `external_id`) and returns it to the client. You pass the SDK a\n`tokenProvider` callback that fetches a fresh token from your own endpoint; it\nrefreshes automatically on expiry. See [`@elaanio/react`](./react#readme) for the\nfull token flow.\n\n## Quick start — React (web)\n\n```tsx\nimport { ElaanProvider, NotificationBell, Preferences } from \"@elaanio/react\";\nimport \"@elaanio/react/styles.css\";\n\nasync function tokenProvider() {\n  const res = await fetch(\"/api/elaan-token\"); // your endpoint\n  const { token, contact_id } = await res.json();\n  return { token, contactId: contact_id };\n}\n\nexport function App() {\n  return (\n    <ElaanProvider apiBase=\"https://api.elaan.io/v1\" tokenProvider={tokenProvider}>\n      <NotificationBell />\n      <Preferences />\n    </ElaanProvider>\n  );\n}\n```\n\n## Quick start — React Native\n\nSame provider and hooks; the components render with React Native primitives\n(`View`/`FlatList`/`Switch`/`Modal`) instead of DOM, and there's no stylesheet\nto import. Realtime works over SSE via [`react-native-sse`](https://www.npmjs.com/package/react-native-sse)\n(RN can't stream `fetch`, but its XHR-based EventSource can send the auth\nheader), wired into the RN provider by default and falling back to polling when\nthe deployment has realtime off. Pass `realtime={null}` for polling only.\n\n```tsx\nimport {\n  ElaanProvider,\n  NotificationBell,\n  NotificationFeed,\n  Preferences,\n} from \"@elaanio/react-native\";\n\nasync function tokenProvider() {\n  const res = await fetch(\"https://yourapp.com/api/elaan-token\", {\n    headers: { Authorization: `Bearer ${yourSessionToken}` },\n  });\n  const { token, contact_id } = await res.json();\n  return { token, contactId: contact_id };\n}\n\nexport default function App() {\n  return (\n    <ElaanProvider apiBase=\"https://api.elaan.io/v1\" tokenProvider={tokenProvider}>\n      {/* A bell + badge that opens the inbox in a modal card */}\n      <NotificationBell />\n\n      {/* …or the inbox inline as a full screen */}\n      <NotificationFeed emptyText=\"Nothing here yet.\" />\n\n      {/* per-type × channel preference switches */}\n      <Preferences />\n    </ElaanProvider>\n  );\n}\n```\n\n### Registering a device token (Expo / FCM)\n\n```tsx\nimport { usePush } from \"@elaanio/react-native\";\n\nfunction useRegisterPush(expoToken: string) {\n  const { register } = usePush();\n  useEffect(() => {\n    // provider: \"expo\" | \"fcm\" | \"apns\" | \"onesignal\"\n    register(expoToken, \"expo\", \"ios\");\n  }, [expoToken]);\n}\n```\n\n### Browser push (the `web_push` channel)\n\nA separate channel from mobile push, not a provider under it — a contact's opt-out\nis keyed by `(type, channel)`, so \"no browser nags, keep my phone alerts\" has to be\nexpressible.\n\nThere is no token to hand in here, only a handshake to perform, so `useBrowserPush`\ndoes the whole thing: permission, service worker registration, `subscribe()`, and\nconverting the subscription's two keys into what the API stores.\n\n```tsx\nimport { useBrowserPush } from \"@elaanio/react\";\nimport type { BrowserPushResult } from \"@elaanio/react\";\n\nfunction PushToggle() {\n  const push = useBrowserPush({ serviceWorkerUrl: \"/sw.js\" });\n  const [note, setNote] = useState<string | null>(null);\n  if (!push.supported) return null;\n\n  // Handle the result, and catch: the hook rethrows genuine faults after setting\n  // `push.error`, so passing `push.subscribe` straight to onClick turns one into an\n  // unhandled rejection.\n  const onClick = async () => {\n    try {\n      if (push.subscribed) {\n        setNote((await push.unsubscribe()) ? null : \"Nothing to turn off.\");\n      } else {\n        setNote(explain(await push.subscribe()));\n      }\n    } catch {\n      setNote(push.error?.message ?? \"Something went wrong.\");\n    }\n  };\n\n  return (\n    <>\n      <button onClick={onClick} disabled={push.busy || push.permission === \"denied\"}>\n        {push.subscribed ? \"Turn off\" : \"Turn on\"} browser notifications\n      </button>\n      {note && <p>{note}</p>}\n    </>\n  );\n}\n\nfunction explain(result: BrowserPushResult): string | null {\n  if (result.ok) return null;\n  switch (result.reason) {\n    case \"denied\":\n      return \"Notifications are blocked for this site. Change it in site settings — the browser won't ask again.\";\n    case \"dismissed\":\n      return \"No problem — you can turn these on any time.\";\n    case \"not-configured\":\n      return \"Browser notifications aren't set up for this account yet.\";\n    case \"unsupported\":\n      return \"This browser can't do notifications.\";\n  }\n}\n```\n\n`subscribe()` resolves to a result rather than throwing, because the failures are\nstates to render: `unsupported`, `denied`, `dismissed`, `not-configured`. `denied`\nis the one to handle deliberately — the browser will not prompt again, so the only\nway forward is site settings.\n\nYour service worker must show the notification. If it doesn't, the browser\nsubstitutes its own \"site has been updated in the background\" notice:\n\n```js\n// sw.js — bundle this; a classic service worker can't `import`.\nimport { handlePush, handleNotificationClick } from \"@elaanio/core/service-worker\";\n\nself.addEventListener(\"push\", handlePush);\nself.addEventListener(\"notificationclick\", handleNotificationClick);\n```\n\nThe account needs a VAPID keypair (Push Transport in the console). Without one\n`subscribe()` returns `not-configured` — the SDK checks before it prompts, so a\none-shot permission isn't spent on an account that can't send.\n\nIt also needs a **browser push template** for each notification type you send, but\nthe SDK cannot check that and does not claim to: subscribing succeeds and the sends\nthen fail server-side with \"no resolvable template\". If notifications never arrive\nfor a browser that reports itself subscribed, that is the first thing to check —\nthe delivery log in the console names it directly.\n\nA runnable version of all of this is in\n[`examples/web-push-demo`](./examples/web-push-demo).\n\n## Theming & styling\n\n**Web (`@elaanio/react`)** ships a stylesheet driven entirely by CSS variables, so\nyou theme it without touching component internals. Import the stylesheet once,\nthen override the variables on `:root` (or any ancestor of the components):\n\n```css\n:root {\n  --elaan-accent: #7c3aed;   /* brand color: badges, links, active states */\n  --elaan-accent-ink: #fff;  /* text/icon on top of the accent */\n  --elaan-bg: #ffffff;\n  --elaan-bg-hover: #f4f6f9;\n  --elaan-text: #1a1d23;\n  --elaan-muted: #6b7280;\n  --elaan-border: #e5e7eb;\n  --elaan-danger: #ef4444;\n  --elaan-radius: 10px;\n  --elaan-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.3);\n  --elaan-z: 1000;           /* popover stacking order */\n  /* --elaan-font is unset by default, so the components inherit your\n     app's type stack. Name a stack here to pin one instead. */\n}\n```\n\nEvery component also takes `className` and any other DOM attribute, applied to\nits root element, so you can scope the variables to a subtree instead of\n`:root`:\n\n```tsx\n<NotificationBell className={styles.scope} />\n```\n\n**Dark mode** follows the OS by default and can be driven by your app instead:\na `dark` class or `data-theme=\"dark\"` on any ancestor switches the palette, so\na manual theme toggle (the Tailwind and shadcn default) works without you\nredeclaring the variables. Override the variables inside your own media query\nor under your own selector to customize either palette.\n\nThe rules are wrapped in `@layer elaan`, so your own unlayered CSS beats them\nwithout having to out-specify anything. Each element carries a stable `elaan-*`\nclass (`.elaan-bell`, `.elaan-feed`, `.elaan-item`, …), and a row in the feed\ncarries `data-unread` while it is unread, if you need finer control.\n\n**React Native (`@elaanio/react-native`)** components use `StyleSheet` with a\nsmall built-in palette (accent, background, text, muted, border). There are no\nCSS variables in RN, so for anything beyond the defaults — brand fonts, custom\nrow layouts, dark-mode palettes — build your own components on the hooks (next\nsection). That's the intended path for heavy RN customization.\n\n## Building your own components\n\nThe packaged components are deliberately thin. When the defaults don't fit —\nyour own markup, a design system, a different layout, or a framework we don't\nship yet — drop down a layer. Pick the lowest one you need:\n\n**1. Same framework, your own UI → use the hooks.** `@elaanio/react` and\n`@elaanio/react-native` both re-export the hooks from `@elaanio/react-core`. Render\nwhatever you like; the hook owns loading, polling, realtime, and optimistic\nupdates:\n\n```tsx\nimport { useNotifications, usePreferences } from \"@elaanio/react\"; // or /react-native\n\nfunction MyInbox() {\n  const { notifications, unreadCount, loading, markRead, markAllRead, remove } =\n    useNotifications();\n\n  if (loading && notifications.length === 0) return <Spinner />;\n  return (\n    <MyList\n      items={notifications}\n      onOpen={(n) => markRead(n.id)}\n      onDismiss={(n) => remove(n.id)}\n      onClearAll={markAllRead}\n    />\n  );\n}\n\nfunction MyPrefs() {\n  const { preferences, setPreference, clearPreference } = usePreferences();\n  // preferences: [{ notification_type_key, channels: [{ channel, enabled, overridden }] }]\n  // setPreference(typeKey, channel, enabled) / clearPreference(typeKey, channel)\n}\n```\n\nAvailable hooks: `useNotifications`, `useUnreadCount`, `usePreferences`,\n`usePush` — all require an ancestor `<ElaanProvider>`.\n\n**2. A different framework (Vue, Svelte, Solid, vanilla) → use `@elaanio/core`.**\nThe core exposes the same logic as framework-agnostic observable stores. This is\nexactly what `@elaanio/react-core` is built on, so a new binding is small:\n\n```ts\nimport { ElaanClient, createInboxStore } from \"@elaanio/core\";\n\nconst client = new ElaanClient(\"https://api.elaan.io/v1\", tokenProvider);\nconst inbox = createInboxStore(client, { pollInterval: 30000 });\n\ninbox.subscribe(() => render(inbox.getState())); // getState() / getUnreadCount()\ninbox.markRead(id);                              // + markUnread / markAllRead / remove / refresh\n// inbox.destroy() when you tear down\n```\n\nWire `store.subscribe` + `store.getState` into your framework's reactivity\n(Vue `ref`, Svelte store contract, `useSyncExternalStore`, …). For one-off calls\nthat don't need a store, `ElaanClient` has every endpoint directly. If you build\na binding for another framework, a PR adding an `@elaanio/<framework>` package is\nvery welcome.\n\n## Development\n\nThis repo is a [pnpm](https://pnpm.io) workspace.\n\n```bash\npnpm install     # install all packages\npnpm -r build    # build every package (topological order)\npnpm -r typecheck\n```\n\n## Releasing\n\nVersioning is **manual semver, per package**. To cut a release:\n\n1. Bump `\"version\"` in the `package.json` of each package you're releasing.\n2. Commit, then push a tag:\n   ```bash\n   git tag v0.2.0 && git push origin v0.2.0\n   ```\n3. The [`Publish`](./.github/workflows/publish.yml) workflow builds, typechecks,\n   and runs `pnpm -r publish` — which publishes each package in dependency order\n   (rewriting `workspace:*` to real versions) and **skips any version already on\n   npm**, so releasing a subset just works.\n\nOne-time setup (repo owner):\n\n- Own the **`@elaan` scope/org** on [npmjs.com](https://www.npmjs.com/) (the\n  packages are scoped `@elaanio/*`).\n- Add an **`NPM_TOKEN`** repository secret (Settings → Secrets and variables →\n  Actions) — an npm **Automation** token with publish rights to the scope.\n\nPackages publish as public via each one's `publishConfig.access`, with npm\nprovenance attested from the workflow.\n\n## License\n\nMIT — see [LICENSE](./LICENSE).\n",
  "bytes": 14193,
  "sha": "77b52bc7365c710ba1aa94b9f18c05984ed11fe4a36acb846701ec1e59f0fc7a",
  "repo_slug": "thingsidoforlove/elaan-js",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_elaan_mcp_3be425b9/readme"
}