@hermesihq/react 0.1.0 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/core/subscriberToken.ts","../src/core/types.ts","../src/core/HermsClient.ts","../src/react/HermsProvider.tsx","../src/react/useUnreadCount.ts","../src/react/useInbox.ts","../src/react/usePreferences.ts","../src/react/HermsInbox.tsx","../src/react/locale.ts"],"sourcesContent":["/**\r\n * `@hermesihq/react`: the embeddable inbox SDK (spec §8.5, F-INB-5).\r\n *\r\n * Two layers, exported from the one entry point:\r\n * - `./core`: `HermsClient`, framework-agnostic, no React import. The piece\r\n * a future Vue/vanilla wrapper would reuse as-is.\r\n * - `./react`: `HermsProvider`, `useUnreadCount`, `useInbox`, `<HermsInbox />`.\r\n * The React bindings this package ships today.\r\n */\r\nexport * from './core'\r\nexport * from './react'\r\n","/**\r\n * Pure, dependency-free helpers for the *reading* half of F-INB-2's subscriber\r\n * token (§6.2). `HermsClient` never mints or verifies a signature (it never has\r\n * the secret key hash to do either), it only needs to read the `exp` claim back\r\n * out of a token the host app already handed it, to drive `onTokenExpiring`.\r\n *\r\n * Token shape (verbatim, `hermesi.core.security`'s \"F-INB-2 subscriber token\"\r\n * section): `base64url(payload) + \".\" + base64url(hmac_sha256(...))`, where\r\n * `payload` is a plain JSON object `{sub, env, exp}`. No JOSE/JWT header segment.\r\n */\r\n\r\n/** `atob`/`TextDecoder` are DOM/web-platform globals, also present in every\r\n * modern Node runtime (≥16). They are never touched at module-eval time, only from\r\n * inside this function when a caller actually decodes a token, so this stays\r\n * SSR-safe per §8.5 without any `typeof window` guard needed here specifically. */\r\nfunction base64UrlDecodeToString(segment: string): string {\r\n const base64 = segment.replace(/-/g, '+').replace(/_/g, '/')\r\n const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4)\r\n const binary = atob(padded)\r\n const bytes = new Uint8Array(binary.length)\r\n for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i)\r\n return new TextDecoder().decode(bytes)\r\n}\r\n\r\n/**\r\n * Returns the token's `exp` claim (unix seconds) or `null` for anything that\r\n * isn't a well-formed `<payload>.<signature>` token with a numeric `exp`. A\r\n * malformed value never throws, it just means `onTokenExpiring` won't be\r\n * scheduled for this particular token.\r\n */\r\nexport function decodeSubscriberTokenExp(token: string): number | null {\r\n const [payloadSegment] = token.split('.')\r\n if (!payloadSegment) return null\r\n try {\r\n const parsed: unknown = JSON.parse(base64UrlDecodeToString(payloadSegment))\r\n if (\r\n typeof parsed === 'object' &&\r\n parsed !== null &&\r\n 'exp' in parsed &&\r\n typeof (parsed as { exp: unknown }).exp === 'number'\r\n ) {\r\n return (parsed as { exp: number }).exp\r\n }\r\n return null\r\n } catch {\r\n return null\r\n }\r\n}\r\n","/**\r\n * Shared, framework-agnostic shapes for `HermsClient` and every React binding\r\n * built on top of it. Camel-cased on purpose. The wire (§6.6 `/v1/client/inbox/*`)\r\n * is snake_case; `HermsClient` is the one place that translation happens, the same\r\n * posture the dashboard's own hand-written API layer takes (`frontend/src/api/*.ts`)\r\n * until generated OpenAPI types exist for either surface.\r\n */\r\n\r\nexport interface HermsClientOptions {\r\n /** `hm_pk_<env>_<random>`, safe to embed in a browser bundle (§6.2: \"grant\r\n * nothing by themselves\"). */\r\n publicKey: string\r\n /** Base URL of the client API, e.g. `https://api.hermesi.dev/v1/client` or,\r\n * same-origin behind a dev proxy, `/v1/client`. No trailing slash required:\r\n * normalised internally. */\r\n apiBaseUrl: string\r\n /**\r\n * Host-app-supplied. This SDK never has the secret key and never could\r\n * (F-INB-2: a subscriber token is minted server-side, by the host's own\r\n * backend). Called fresh before *every* request; never cached past what the\r\n * host itself returns, since refreshing ahead of expiry is explicitly the\r\n * host's job, not this client's (see `onTokenExpiring`).\r\n */\r\n getSubscriberToken: () => string | Promise<string>\r\n /**\r\n * Called proactively, once, some seconds before the *currently held* token's\r\n * own `exp` claim (read back out of the token's payload segment client-side:\r\n * it's HMAC-signed, not encrypted, so this never requires verifying the\r\n * signature). This is a hook for the host app to refresh ahead of expiry rather\r\n * than reactively after a `401`. Best-effort: a token whose payload can't be\r\n * decoded (malformed, or a host app testing with a garbage string) simply\r\n * never schedules a call.\r\n */\r\n onTokenExpiring?: () => void\r\n}\r\n\r\nexport type HermsInboxReadStatus = 'read' | 'unread'\r\n\r\nexport interface HermsInboxCategory {\r\n key: string\r\n name: string\r\n}\r\n\r\nexport interface HermsInboxItem {\r\n id: string\r\n title: string\r\n body: string\r\n actionUrl: string | null\r\n category: HermsInboxCategory | null\r\n seenAt: string | null\r\n readAt: string | null\r\n createdAt: string\r\n}\r\n\r\nexport interface HermsInboxCounts {\r\n unread: number\r\n unseen: number\r\n}\r\n\r\nexport interface HermsInboxPage {\r\n items: HermsInboxItem[]\r\n hasMore: boolean\r\n nextCursor: string | null\r\n}\r\n\r\nexport interface HermsInboxListParams {\r\n /** Maps to `GET /inbox?read=<bool>`: `'read'` -> `read=true`, `'unread'` ->\r\n * `read=false`, omitted -> both. */\r\n status?: HermsInboxReadStatus\r\n /** Category key(s), `?category=` (repeatable server-side). */\r\n category?: string | string[]\r\n cursor?: string\r\n limit?: number\r\n}\r\n\r\n/**\r\n * Every channel Hermesi can hold an identity for.\r\n *\r\n * **Written out, and checked against the API by a test rather than trusted.** This\r\n * union said `'push' | 'sms' | 'email' | 'in_app'` while the platform had grown to\r\n * nine, so an integrator could not register a Telegram or Slack identity at all:\r\n * TypeScript refused it at their call site, and no test here or in the dashboard had\r\n * any reason to notice, because the demo page never registers a channel.\r\n *\r\n * It cannot be generated: this package is published standalone and must not depend on\r\n * the repository's OpenAPI snapshot at build time. So `channelParity.test.ts` compares\r\n * it to that snapshot's `Channel` enum instead. The list stays hand-written, and\r\n * drifting from the server is what fails.\r\n *\r\n * `push` is included because the API accepts it. No adapter delivers it today, which is\r\n * a deployment fact rather than a reason to refuse the identity.\r\n *\r\n * Exported as a value, not only a type: a host building a preference centre needs to\r\n * iterate the channels at runtime, and deriving the type from the array means the two\r\n * cannot disagree.\r\n */\r\nexport const HERMS_CHANNELS = [\r\n 'in_app',\r\n 'email',\r\n 'push',\r\n 'sms',\r\n 'whatsapp',\r\n 'telegram',\r\n 'slack',\r\n 'teams',\r\n 'discord',\r\n] as const\r\n\r\nexport type HermsChannel = (typeof HERMS_CHANNELS)[number]\r\n\r\nexport interface HermsRegisterChannelParams {\r\n channel: HermsChannel\r\n identifier: string\r\n metadata?: Record<string, unknown>\r\n}\r\n\r\n/** One channel's setting, either globally or inside a category. `null` means the\r\n * subscriber has expressed no preference and the category's or platform's default\r\n * applies. It is not the same as `false`, and collapsing the two would silently\r\n * opt somebody out of something they never declined. */\r\nexport interface HermsChannelPreference {\r\n channel: HermsChannel\r\n enabled: boolean | null\r\n}\r\n\r\nexport interface HermsCategoryPreference {\r\n categoryId: string\r\n key: string\r\n name: string\r\n /** A critical category is always delivered on every live channel and cannot be turned\r\n * off. Render it, but not as a control: an affordance that refuses is worse than\r\n * none. */\r\n isCritical: boolean\r\n channels: HermsChannelPreference[]\r\n emailEnabled: boolean | null\r\n inAppEnabled: boolean | null\r\n}\r\n\r\nexport interface HermsPreferences {\r\n /** For addressing the subscriber in a preference page's own copy. `null` when the\r\n * application never supplied one. */\r\n firstName: string | null\r\n /** Settings that apply across every category. */\r\n globalChannels: HermsChannelPreference[]\r\n /** `email` and `in_app` again, as scalars.\r\n *\r\n * Carried rather than dropped because the API publishes them, and dropping a\r\n * published field is the defect `HermsApiError` had. They are the same values as\r\n * the matching entries in `globalChannels`; read whichever suits, but do not treat\r\n * a disagreement between them as meaningful. */\r\n globalEmailEnabled: boolean | null\r\n globalInAppEnabled: boolean | null\r\n categories: HermsCategoryPreference[]\r\n}\r\n\r\n/** One setting to change. Omit `categoryId` to change the global setting for that\r\n * channel; supply it to change the setting inside one category. */\r\nexport interface HermsPreferenceUpdate {\r\n channel: HermsChannel\r\n enabled: boolean\r\n categoryId?: string\r\n}\r\n\r\nexport interface HermsItemCreatedEvent {\r\n type: 'item.created'\r\n data: { id: string; title: string; created_at: string }\r\n}\r\n\r\nexport interface HermsCountsChangedEvent {\r\n type: 'counts.changed'\r\n data: HermsInboxCounts\r\n}\r\n\r\nexport type HermsRealtimeEvent = HermsItemCreatedEvent | HermsCountsChangedEvent\r\n\r\nexport type HermsEventListener = (event: HermsRealtimeEvent) => void\r\n\r\n/**\r\n * One specific problem inside an error.\r\n *\r\n * Open, not closed, because the API says so: most entries name a request field\r\n * (`field` + `issue`), some name a dependency instead, and its own schema declares\r\n * `additionalProperties` rather than publishing a shape a client would be right to\r\n * reject on. Typing it closed here would reintroduce exactly that.\r\n */\r\nexport type HermsErrorDetail = Record<string, unknown>\r\n\r\n/** Thrown for any failed request to the client API. It mirrors the server's error\r\n * envelope where one was returned, and falls back to a generic shape when the body was\r\n * not JSON at all, which is usually an upstream proxy's own HTML error page.\r\n *\r\n * `detail` and `docUrl` were missing for a long time while the API published both as\r\n * required fields. A 422's per-field `detail` reached a consumer as a bare sentence,\r\n * and `doc_url` (the one thing that lets an integrator resolve an error without\r\n * asking anybody) never arrived at all. */\r\nexport class HermsApiError extends Error {\r\n readonly status: number\r\n readonly code: string\r\n readonly type: string\r\n readonly requestId: string\r\n /** Per-problem specifics. Possibly empty, never absent: the API's own words. */\r\n readonly detail: readonly HermsErrorDetail[]\r\n /** Documentation for this `code`, or `''` when the server sent none. */\r\n readonly docUrl: string\r\n\r\n constructor(status: number, body: unknown) {\r\n const envelope =\r\n typeof body === 'object' && body !== null && 'error' in body\r\n ? (body as {\r\n error?: {\r\n type?: unknown\r\n code?: unknown\r\n message?: unknown\r\n request_id?: unknown\r\n detail?: unknown\r\n doc_url?: unknown\r\n }\r\n }).error\r\n : undefined\r\n const message = typeof envelope?.message === 'string' ? envelope.message : `Hermesi client API request failed with status ${status}.`\r\n super(message)\r\n this.name = 'HermsApiError'\r\n this.status = status\r\n this.code = typeof envelope?.code === 'string' ? envelope.code : 'unknown_error'\r\n this.type = typeof envelope?.type === 'string' ? envelope.type : 'unknown_error'\r\n this.requestId = typeof envelope?.request_id === 'string' ? envelope.request_id : ''\r\n // `[]` rather than `undefined` when absent: the API documents this as possibly\r\n // empty and never absent, so a consumer should be able to iterate it without a\r\n // guard whatever actually arrived.\r\n this.detail = Array.isArray(envelope?.detail)\r\n ? (envelope.detail as HermsErrorDetail[]).filter(\r\n (entry): entry is HermsErrorDetail => typeof entry === 'object' && entry !== null,\r\n )\r\n : []\r\n this.docUrl = typeof envelope?.doc_url === 'string' ? envelope.doc_url : ''\r\n }\r\n}\r\n","import { decodeSubscriberTokenExp } from './subscriberToken'\r\nimport {\r\n HermsApiError,\r\n type HermsChannel,\r\n type HermsClientOptions,\r\n type HermsEventListener,\r\n type HermsInboxCounts,\r\n type HermsInboxItem,\r\n type HermsInboxListParams,\r\n type HermsInboxPage,\r\n type HermsPreferences,\r\n type HermsPreferenceUpdate,\r\n type HermsRegisterChannelParams,\r\n} from './types'\r\n\r\n/**\r\n * `HermsClient` is the framework-agnostic core of `@hermesihq/react` (§8.5: \"so a\r\n * Vue or vanilla wrapper is later a thin layer, not a rewrite\"). Deliberately\r\n * has **no import from `react` anywhere in this file**. Every React binding in\r\n * `../react/*` is a thin wrapper around an instance of this class, never a\r\n * second implementation of any of this logic.\r\n *\r\n * SSR-safe per §8.5 (\"no `window` at import time\"): nothing at module scope or\r\n * in the constructor touches `window`/`EventSource`/`document`. Those are only\r\n * ever read inside `subscribe()`, and only once it actually runs (i.e. once a\r\n * consumer mounts on a real client).\r\n */\r\n\r\n// §6.6's own sanctioned degraded mode: \"clients that cannot hold an SSE\r\n// connection fall back to polling `/counts` every 60s.\"\r\nconst POLL_INTERVAL_MS = 60_000\r\n\r\n// After this many *consecutive*, immediate (no successful `onopen` in between)\r\n// SSE failures, stop reconnecting into what's very likely a hard failure (an\r\n// invalid/expired public key or subscriber token, a network that blocks SSE\r\n// entirely) and lean on the polling fallback instead, still retrying SSE in\r\n// the background at an increasingly long interval, never fully giving up.\r\nconst MAX_FAST_RECONNECT_ATTEMPTS = 3\r\nconst BASE_RECONNECT_DELAY_MS = 1_000\r\nconst MAX_RECONNECT_DELAY_MS = 30_000\r\n\r\n// `onTokenExpiring` fires this many seconds *before* the currently held\r\n// token's own `exp`, giving a host app a real window to refresh ahead of it\r\n// actually expiring rather than reacting to a `401`.\r\nconst TOKEN_EXPIRY_BUFFER_SECONDS = 60\r\n\r\nfunction backoffDelayMs(attempt: number): number {\r\n const exponential = BASE_RECONNECT_DELAY_MS * 2 ** Math.max(0, attempt - 1)\r\n return Math.min(MAX_RECONNECT_DELAY_MS, exponential)\r\n}\r\n\r\nfunction buildQueryString(params: Record<string, string | string[] | number | undefined>): string {\r\n const search = new URLSearchParams()\r\n for (const [key, value] of Object.entries(params)) {\r\n if (value === undefined) continue\r\n if (Array.isArray(value)) {\r\n for (const item of value) search.append(key, item)\r\n } else {\r\n search.append(key, String(value))\r\n }\r\n }\r\n const qs = search.toString()\r\n return qs ? `?${qs}` : ''\r\n}\r\n\r\ninterface WireInboxCategory {\r\n key: string\r\n name: string\r\n}\r\n\r\ninterface WireInboxItem {\r\n id: string\r\n title: string\r\n body: string\r\n action_url: string | null\r\n category: WireInboxCategory | null\r\n seen_at: string | null\r\n read_at: string | null\r\n created_at: string\r\n}\r\n\r\ninterface WireInboxListResponse {\r\n data: WireInboxItem[]\r\n has_more: boolean\r\n next_cursor: string | null\r\n}\r\n\r\ninterface WireInboxCountsResponse {\r\n unread: number\r\n unseen: number\r\n}\r\n\r\ninterface WireUpdatedCountResponse {\r\n updated: number\r\n}\r\n\r\nfunction fromWireItem(wire: WireInboxItem): HermsInboxItem {\r\n return {\r\n id: wire.id,\r\n title: wire.title,\r\n body: wire.body,\r\n actionUrl: wire.action_url,\r\n category: wire.category ? { key: wire.category.key, name: wire.category.name } : null,\r\n seenAt: wire.seen_at,\r\n readAt: wire.read_at,\r\n createdAt: wire.created_at,\r\n }\r\n}\r\n\r\ninterface WireChannelPreference {\r\n channel: HermsChannel\r\n enabled: boolean | null\r\n}\r\n\r\ninterface WireCategoryPreference {\r\n category_id: string\r\n key: string\r\n name: string\r\n is_critical: boolean\r\n channels: WireChannelPreference[]\r\n email_enabled: boolean | null\r\n in_app_enabled: boolean | null\r\n}\r\n\r\ninterface WirePreferencesResponse {\r\n first_name: string | null\r\n global_channels: WireChannelPreference[]\r\n global_email_enabled: boolean | null\r\n global_in_app_enabled: boolean | null\r\n categories: WireCategoryPreference[]\r\n}\r\n\r\nfunction fromWirePreferences(wire: WirePreferencesResponse): HermsPreferences {\r\n return {\r\n firstName: wire.first_name,\r\n globalChannels: wire.global_channels.map((c) => ({ channel: c.channel, enabled: c.enabled })),\r\n globalEmailEnabled: wire.global_email_enabled,\r\n globalInAppEnabled: wire.global_in_app_enabled,\r\n categories: wire.categories.map((category) => ({\r\n categoryId: category.category_id,\r\n key: category.key,\r\n name: category.name,\r\n isCritical: category.is_critical,\r\n channels: category.channels.map((c) => ({ channel: c.channel, enabled: c.enabled })),\r\n emailEnabled: category.email_enabled,\r\n inAppEnabled: category.in_app_enabled,\r\n })),\r\n }\r\n}\r\n\r\nexport class HermsClient {\r\n private readonly options: HermsClientOptions\r\n private readonly baseUrl: string\r\n private expiryTimer: ReturnType<typeof setTimeout> | null = null\r\n private lastScheduledExp: number | null = null\r\n\r\n constructor(options: HermsClientOptions) {\r\n this.options = options\r\n this.baseUrl = options.apiBaseUrl.replace(/\\/+$/, '')\r\n }\r\n\r\n // --- Token handling ----------------------------------------------------\r\n\r\n /** Every request goes through here: always a *fresh* call to\r\n * `getSubscriberToken()` (never cached past what the host itself returns,\r\n * per this class's own contract), which also re-arms `onTokenExpiring`\r\n * against whatever token comes back. */\r\n private async getFreshToken(): Promise<string> {\r\n const token = await this.options.getSubscriberToken()\r\n this.scheduleExpiryHook(token)\r\n return token\r\n }\r\n\r\n private scheduleExpiryHook(token: string): void {\r\n const { onTokenExpiring } = this.options\r\n if (!onTokenExpiring) return\r\n const exp = decodeSubscriberTokenExp(token)\r\n if (exp === null || exp === this.lastScheduledExp) return\r\n if (this.expiryTimer !== null) clearTimeout(this.expiryTimer)\r\n this.lastScheduledExp = exp\r\n const delayMs = Math.max(0, exp * 1000 - TOKEN_EXPIRY_BUFFER_SECONDS * 1000 - Date.now())\r\n this.expiryTimer = setTimeout(() => {\r\n onTokenExpiring()\r\n }, delayMs)\r\n }\r\n\r\n /** Stops the scheduled `onTokenExpiring` timer. Called from\r\n * `HermsProvider`'s unmount cleanup so a torn-down client never fires the\r\n * hook after the app has stopped caring. */\r\n destroy(): void {\r\n if (this.expiryTimer !== null) clearTimeout(this.expiryTimer)\r\n this.expiryTimer = null\r\n }\r\n\r\n // --- Transport -----------------------------------------------------------\r\n\r\n /**\r\n * `expectsBody` is how this knows a missing response body is a failure rather than a\r\n * normal outcome. It defaults to `true` because most calls here read a payload; the\r\n * methods that resolve to `void` pass `false`.\r\n *\r\n * Without it the success path did no checking at all. A `200` carrying a proxy's HTML\r\n * page, or a `204` where a body was expected, left `parsed` null and returned it as\r\n * `T`, so `listInbox()` rejected with `TypeError: Cannot read properties of null\r\n * (reading 'data')` and the stack pointed at this SDK instead of at whatever was\r\n * actually in the way. The *error* path already handled exactly that case carefully;\r\n * only the success path did not.\r\n */\r\n private async request<T>(path: string, init: { method?: string; body?: unknown; query?: Record<string, string | string[] | number | undefined>; expectsBody?: boolean } = {}): Promise<T> {\r\n const token = await this.getFreshToken()\r\n const qs = init.query ? buildQueryString(init.query) : ''\r\n const headers: Record<string, string> = {\r\n Authorization: `Bearer ${this.options.publicKey}`,\r\n 'X-Hermesi-Subscriber-Token': token,\r\n Accept: 'application/json',\r\n }\r\n if (init.body !== undefined) headers['Content-Type'] = 'application/json'\r\n\r\n const response = await fetch(`${this.baseUrl}${path}${qs}`, {\r\n method: init.method ?? 'GET',\r\n headers,\r\n body: init.body !== undefined ? JSON.stringify(init.body) : undefined,\r\n })\r\n\r\n const expectsBody = init.expectsBody ?? true\r\n\r\n if (response.status === 204) {\r\n if (expectsBody) throw new HermsApiError(response.status, null)\r\n return undefined as T\r\n }\r\n\r\n const rawText = await response.text()\r\n let parsed: unknown = null\r\n if (rawText) {\r\n try {\r\n parsed = JSON.parse(rawText)\r\n } catch {\r\n parsed = null\r\n }\r\n }\r\n\r\n if (!response.ok) {\r\n throw new HermsApiError(response.status, parsed)\r\n }\r\n // A 2xx that carries nothing usable where something was expected. Reported as a\r\n // `HermsApiError` like every other failure, so a consumer has one thing to catch.\r\n // It does not become their problem to distinguish a transport fault from a bug in\r\n // their own code, which a raw `TypeError` makes it.\r\n if (expectsBody && parsed === null) {\r\n throw new HermsApiError(response.status, null)\r\n }\r\n return parsed as T\r\n }\r\n\r\n // --- Inbox reads -----------------------------------------------------------\r\n\r\n async listInbox(params: HermsInboxListParams = {}): Promise<HermsInboxPage> {\r\n const wire = await this.request<WireInboxListResponse>('/inbox', {\r\n query: {\r\n limit: params.limit,\r\n cursor: params.cursor,\r\n read: params.status === undefined ? undefined : params.status === 'read' ? 'true' : 'false',\r\n category: params.category,\r\n },\r\n })\r\n return {\r\n items: wire.data.map(fromWireItem),\r\n hasMore: wire.has_more,\r\n nextCursor: wire.next_cursor,\r\n }\r\n }\r\n\r\n async getCounts(): Promise<HermsInboxCounts> {\r\n const wire = await this.request<WireInboxCountsResponse>('/inbox/counts')\r\n return { unread: wire.unread, unseen: wire.unseen }\r\n }\r\n\r\n // --- Inbox writes ------------------------------------------------------\r\n\r\n async markRead(id: string): Promise<HermsInboxItem> {\r\n const wire = await this.request<WireInboxItem>(`/inbox/${encodeURIComponent(id)}/read`, { method: 'POST' })\r\n return fromWireItem(wire)\r\n }\r\n\r\n async markAllRead(): Promise<{ updated: number }> {\r\n const wire = await this.request<WireUpdatedCountResponse>('/inbox/read-all', { method: 'POST' })\r\n return { updated: wire.updated }\r\n }\r\n\r\n async markSeen(ids: string[]): Promise<{ updated: number }> {\r\n if (ids.length === 0) return { updated: 0 }\r\n const wire = await this.request<WireUpdatedCountResponse>('/inbox/seen', { method: 'POST', body: { ids } })\r\n return { updated: wire.updated }\r\n }\r\n\r\n async archive(id: string): Promise<HermsInboxItem> {\r\n const wire = await this.request<WireInboxItem>(`/inbox/${encodeURIComponent(id)}/archive`, { method: 'POST' })\r\n return fromWireItem(wire)\r\n }\r\n\r\n async delete(id: string): Promise<void> {\r\n await this.request<void>(`/inbox/${encodeURIComponent(id)}`, { method: 'DELETE', expectsBody: false })\r\n }\r\n\r\n // --- Channels (device-token self-registration) --------------------------\r\n //\r\n // `POST/DELETE /v1/client/channels`: landing alongside this SDK, not yet\r\n // live in every deployment at the time this client was written. Implemented\r\n // against the documented shape (§8.5/§6.10) so the SDK's public surface is\r\n // complete; a backend that hasn't shipped the route yet will 404, surfaced\r\n // to the caller as an ordinary `HermsApiError`, same as any other endpoint.\r\n\r\n async registerChannel(params: HermsRegisterChannelParams): Promise<void> {\r\n await this.request<void>('/channels', {\r\n method: 'POST',\r\n expectsBody: false,\r\n body: { channel: params.channel, identifier: params.identifier, metadata: params.metadata },\r\n })\r\n }\r\n\r\n // --- Preferences -----------------------------------------------------------\r\n\r\n /**\r\n * The subscriber's own notification settings, for rendering a preference centre.\r\n *\r\n * There is deliberately no `unsubscribe()` beside this, although the API exposes\r\n * `POST /v1/client/unsubscribe`. That route is the target of a `List-Unsubscribe`\r\n * one-click link (RFC 8058): its token is minted by Hermesi and embedded in an\r\n * outgoing email's headers, and the API's own description says it is invoked by a\r\n * mail client rather than by application code. Wrapping it here would invite a host\r\n * to build an unsubscribe button on a token it cannot obtain. The equivalent here\r\n * is `updatePreference({ channel: 'email', enabled: false })`.\r\n */\r\n async getPreferences(): Promise<HermsPreferences> {\r\n return fromWirePreferences(await this.request<WirePreferencesResponse>('/preferences'))\r\n }\r\n\r\n /**\r\n * Change one setting, and receive the whole updated state back.\r\n *\r\n * The server answers `PATCH` with the full preferences rather than an\r\n * acknowledgement, so a caller never has to refetch, and never renders a view\r\n * assembled from its own optimistic guess plus whatever else has changed since.\r\n * Omit `categoryId` for the global setting; supply it to change one category.\r\n */\r\n async updatePreference(update: HermsPreferenceUpdate): Promise<HermsPreferences> {\r\n const wire = await this.request<WirePreferencesResponse>('/preferences', {\r\n method: 'PATCH',\r\n body: {\r\n channel: update.channel,\r\n enabled: update.enabled,\r\n // Sent as `null` rather than omitted: the API reads its absence and its null\r\n // the same way, and being explicit keeps \"global\" visible in a request log.\r\n category_id: update.categoryId ?? null,\r\n },\r\n })\r\n return fromWirePreferences(wire)\r\n }\r\n\r\n /**\r\n * **Path parameters, not a query string.** This sent\r\n * `DELETE /channels?channel=...&identifier=...` for as long as the method has existed,\r\n * and there is no `DELETE` on `/v1/client/channels` at all: the route is\r\n * `/v1/client/channels/{channel}/{identifier}`, so every call 404'd. Nothing caught\r\n * it: the reference integration never deregisters, and the package had no tests.\r\n *\r\n * `identifier` is encoded because it is somebody's address (an email, a phone\r\n * number, a chat conversation id), and those carry `@`, `+` and `:`.\r\n */\r\n async deregisterChannel(channel: HermsChannel, identifier: string): Promise<void> {\r\n await this.request<void>(`/channels/${encodeURIComponent(channel)}/${encodeURIComponent(identifier)}`, {\r\n method: 'DELETE',\r\n expectsBody: false,\r\n })\r\n }\r\n\r\n // --- Real-time -----------------------------------------------------------\r\n\r\n /**\r\n * Opens a live connection for real-time inbox events and returns an\r\n * unsubscribe function. SSR-safe: `window`/`EventSource` are only read here,\r\n * at call time, never at module or constructor time.\r\n *\r\n * - Prefers SSE (`GET /inbox/stream`) when `EventSource` exists.\r\n * - Falls back to polling `getCounts()` every 60s (§6.6) when `EventSource`\r\n * doesn't exist at all, or after `MAX_FAST_RECONNECT_ATTEMPTS` consecutive\r\n * immediate reconnect failures (an invalid/expired token, or a network\r\n * that blocks SSE outright). It still retries SSE in the background with\r\n * exponential backoff rather than abandoning it permanently, so a fixed\r\n * token/network problem self-heals without a page reload.\r\n */\r\n subscribe(onEvent: HermsEventListener): () => void {\r\n let stopped = false\r\n let eventSource: InstanceType<typeof EventSource> | null = null\r\n let pollTimer: ReturnType<typeof setInterval> | null = null\r\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\r\n let consecutiveFailures = 0\r\n\r\n const hasEventSource = typeof window !== 'undefined' && typeof EventSource !== 'undefined'\r\n\r\n const startPolling = () => {\r\n if (pollTimer !== null) return\r\n pollTimer = setInterval(() => {\r\n this.getCounts()\r\n .then((counts) => onEvent({ type: 'counts.changed', data: counts }))\r\n .catch(() => {\r\n /* best-effort: try again next tick */\r\n })\r\n }, POLL_INTERVAL_MS)\r\n }\r\n\r\n const stopPolling = () => {\r\n if (pollTimer !== null) {\r\n clearInterval(pollTimer)\r\n pollTimer = null\r\n }\r\n }\r\n\r\n const scheduleReconnect = () => {\r\n if (stopped) return\r\n consecutiveFailures += 1\r\n if (consecutiveFailures >= MAX_FAST_RECONNECT_ATTEMPTS) startPolling()\r\n const delay = backoffDelayMs(consecutiveFailures)\r\n reconnectTimer = setTimeout(() => {\r\n void connect()\r\n }, delay)\r\n }\r\n\r\n const connect = async () => {\r\n if (stopped || !hasEventSource) return\r\n let token: string\r\n try {\r\n token = await this.getFreshToken()\r\n } catch {\r\n scheduleReconnect()\r\n return\r\n }\r\n if (stopped) return\r\n\r\n const qs = buildQueryString({ public_key: this.options.publicKey, subscriber_token: token })\r\n const source = new EventSource(`${this.baseUrl}/inbox/stream${qs}`)\r\n eventSource = source\r\n\r\n source.addEventListener('item.created', (evt) => {\r\n consecutiveFailures = 0\r\n try {\r\n const data = JSON.parse((evt as MessageEvent<string>).data) as { id: string; title: string; created_at: string }\r\n onEvent({ type: 'item.created', data })\r\n } catch {\r\n /* malformed frame: ignore, next event still gets through */\r\n }\r\n })\r\n source.addEventListener('counts.changed', (evt) => {\r\n consecutiveFailures = 0\r\n try {\r\n const data = JSON.parse((evt as MessageEvent<string>).data) as HermsInboxCounts\r\n onEvent({ type: 'counts.changed', data })\r\n } catch {\r\n /* malformed frame: ignore */\r\n }\r\n })\r\n source.onopen = () => {\r\n consecutiveFailures = 0\r\n stopPolling()\r\n }\r\n source.onerror = () => {\r\n source.close()\r\n if (eventSource === source) eventSource = null\r\n if (!stopped) scheduleReconnect()\r\n }\r\n }\r\n\r\n if (hasEventSource) {\r\n void connect()\r\n } else {\r\n startPolling()\r\n }\r\n\r\n return () => {\r\n stopped = true\r\n stopPolling()\r\n if (reconnectTimer !== null) clearTimeout(reconnectTimer)\r\n eventSource?.close()\r\n }\r\n }\r\n}\r\n","import { createContext, useContext, useEffect, useMemo, useRef, type ReactNode } from 'react'\r\nimport type { HermsClient } from '../core/HermsClient'\r\nimport type { HermsEventListener } from '../core/types'\r\n\r\ninterface HermsContextValue {\r\n client: HermsClient\r\n /** Registers `listener` against the *one* `client.subscribe(...)` connection\r\n * this provider owns for the whole subtree, returning an unsubscribe\r\n * function. Every `useUnreadCount`/`useInbox` consumer calls this instead\r\n * of opening its own `EventSource`/poll loop. */\r\n addEventListener: (listener: HermsEventListener) => () => void\r\n}\r\n\r\nconst HermsContext = createContext<HermsContextValue | null>(null)\r\n\r\nexport interface HermsProviderProps {\r\n client: HermsClient\r\n children: ReactNode\r\n}\r\n\r\n/**\r\n * `<HermsProvider client={new HermsClient({...})}>`: one `HermsClient`\r\n * instance for the whole subtree. Opens the client's real-time subscription\r\n * once on mount and tears it (and the client's own `onTokenExpiring` timer)\r\n * down on unmount; every listener registered via `addEventListener` below\r\n * shares that single connection rather than each hook paying for its own.\r\n *\r\n * `client` is expected to be a stable reference (constructed once, e.g. in\r\n * `useMemo`/module scope in the host app). Passing a new instance on every\r\n * render tears down and reopens the underlying connection every render.\r\n */\r\nexport function HermsProvider({ client, children }: HermsProviderProps) {\r\n const listenersRef = useRef<Set<HermsEventListener>>(new Set())\r\n\r\n useEffect(() => {\r\n const unsubscribe = client.subscribe((event) => {\r\n for (const listener of listenersRef.current) listener(event)\r\n })\r\n return () => {\r\n unsubscribe()\r\n client.destroy()\r\n }\r\n }, [client])\r\n\r\n const value = useMemo<HermsContextValue>(\r\n () => ({\r\n client,\r\n addEventListener: (listener) => {\r\n listenersRef.current.add(listener)\r\n return () => listenersRef.current.delete(listener)\r\n },\r\n }),\r\n [client],\r\n )\r\n\r\n return <HermsContext.Provider value={value}>{children}</HermsContext.Provider>\r\n}\r\n\r\n/** Every hook/component in this package throws this same error outside a\r\n * `<HermsProvider>`: there is no sensible default client to fall back to. */\r\nexport function useHermsContext(): HermsContextValue {\r\n const ctx = useContext(HermsContext)\r\n if (!ctx) {\r\n throw new Error('@hermesihq/react: this hook/component must be rendered inside <HermsProvider client={...}>.')\r\n }\r\n return ctx\r\n}\r\n","import { useEffect, useState } from 'react'\r\nimport type { HermsInboxCounts } from '../core/types'\r\nimport { useHermsContext } from './HermsProvider'\r\n\r\nexport interface UseUnreadCountResult extends HermsInboxCounts {\r\n isLoading: boolean\r\n}\r\n\r\n/**\r\n * Headless. `{unread, unseen, isLoading}`, kept live via the provider's\r\n * shared subscription: one `getCounts()` call on mount, then recomputed on\r\n * every `counts.changed` event (real-time push, or the client's own 60s\r\n * polling fallback) for as long as this component stays mounted.\r\n */\r\nexport function useUnreadCount(): UseUnreadCountResult {\r\n const { client, addEventListener } = useHermsContext()\r\n const [counts, setCounts] = useState<HermsInboxCounts>({ unread: 0, unseen: 0 })\r\n const [isLoading, setIsLoading] = useState(true)\r\n\r\n useEffect(() => {\r\n let cancelled = false\r\n setIsLoading(true)\r\n client\r\n .getCounts()\r\n .then((fresh) => {\r\n if (!cancelled) setCounts(fresh)\r\n })\r\n .catch(() => {\r\n /* best-effort initial read: the badge just stays at its last known\r\n * value (0 on first mount) until a `counts.changed` event or a\r\n * remount succeeds. */\r\n })\r\n .finally(() => {\r\n if (!cancelled) setIsLoading(false)\r\n })\r\n return () => {\r\n cancelled = true\r\n }\r\n }, [client])\r\n\r\n useEffect(\r\n () =>\r\n addEventListener((event) => {\r\n if (event.type === 'counts.changed') setCounts(event.data)\r\n }),\r\n [addEventListener],\r\n )\r\n\r\n return { ...counts, isLoading }\r\n}\r\n","import { useCallback, useEffect, useRef, useState } from 'react'\r\nimport type { HermsInboxItem, HermsInboxReadStatus } from '../core/types'\r\nimport { useHermsContext } from './HermsProvider'\r\n\r\nexport interface UseInboxOptions {\r\n status?: HermsInboxReadStatus\r\n category?: string\r\n}\r\n\r\nexport interface UseInboxResult {\r\n items: HermsInboxItem[]\r\n isLoading: boolean\r\n isLoadingMore: boolean\r\n error: Error | null\r\n hasMore: boolean\r\n loadMore: () => void\r\n markRead: (id: string) => Promise<void>\r\n markAllRead: () => Promise<void>\r\n archive: (id: string) => Promise<void>\r\n remove: (id: string) => Promise<void>\r\n refetch: () => void\r\n}\r\n\r\n/**\r\n * Headless, cursor-paginated inbox list state: thin wrappers around\r\n * `HermsClient`'s own methods that also patch local state, so a consumer\r\n * never has to manually refetch the whole list after every mutating action.\r\n *\r\n * Live-updated on `item.created`: the new item is prepended immediately from\r\n * the (deliberately minimal: `{id, title, created_at}`, §6.6) SSE payload so\r\n * the UI reacts instantly, then reconciled against a fresh first page fetched\r\n * in the background so the authoritative row (body, category, action_url)\r\n * lands without a full list reload/flicker.\r\n */\r\nexport function useInbox(options: UseInboxOptions = {}): UseInboxResult {\r\n const { client, addEventListener } = useHermsContext()\r\n const { status, category } = options\r\n\r\n const [items, setItems] = useState<HermsInboxItem[]>([])\r\n const [isLoading, setIsLoading] = useState(true)\r\n const [isLoadingMore, setIsLoadingMore] = useState(false)\r\n const [error, setError] = useState<Error | null>(null)\r\n const [hasMore, setHasMore] = useState(false)\r\n const cursorRef = useRef<string | null>(null)\r\n const requestIdRef = useRef(0)\r\n\r\n const fetchFirstPage = useCallback(() => {\r\n const requestId = ++requestIdRef.current\r\n setIsLoading(true)\r\n setError(null)\r\n client\r\n .listInbox({ status, category })\r\n .then((page) => {\r\n if (requestIdRef.current !== requestId) return\r\n setItems(page.items)\r\n setHasMore(page.hasMore)\r\n cursorRef.current = page.nextCursor\r\n })\r\n .catch((err: unknown) => {\r\n if (requestIdRef.current !== requestId) return\r\n setError(err instanceof Error ? err : new Error('Failed to load inbox.'))\r\n })\r\n .finally(() => {\r\n if (requestIdRef.current === requestId) setIsLoading(false)\r\n })\r\n }, [client, status, category])\r\n\r\n useEffect(() => {\r\n fetchFirstPage()\r\n }, [fetchFirstPage])\r\n\r\n const loadMore = useCallback(() => {\r\n if (isLoadingMore || !hasMore || cursorRef.current === null) return\r\n setIsLoadingMore(true)\r\n client\r\n .listInbox({ status, category, cursor: cursorRef.current })\r\n .then((page) => {\r\n setItems((prev) => [...prev, ...page.items])\r\n setHasMore(page.hasMore)\r\n cursorRef.current = page.nextCursor\r\n })\r\n .catch((err: unknown) => {\r\n setError(err instanceof Error ? err : new Error('Failed to load more of the inbox.'))\r\n })\r\n .finally(() => setIsLoadingMore(false))\r\n }, [client, status, category, isLoadingMore, hasMore])\r\n\r\n // Reconcile a just-prepended, partial (`id`/`title`/`createdAt`-only) item\r\n // against a fresh first page once it lands. Replaces any row whose id is\r\n // in the fresh page with its authoritative version, and folds in any other\r\n // brand-new rows that arrived out of order, without disturbing rows loaded\r\n // from subsequent pages that the first page never touches.\r\n const reconcileFirstPage = useCallback(() => {\r\n client\r\n .listInbox({ status, category })\r\n .then((page) => {\r\n setItems((prev) => {\r\n const freshById = new Map(page.items.map((item) => [item.id, item]))\r\n const merged = prev.map((item) => freshById.get(item.id) ?? item)\r\n const knownIds = new Set(merged.map((item) => item.id))\r\n const additions = page.items.filter((item) => !knownIds.has(item.id))\r\n return [...additions, ...merged]\r\n })\r\n })\r\n .catch(() => {\r\n /* best-effort reconciliation: the optimistic partial row (or the\r\n * next `item.created`/manual refetch) is the fallback. */\r\n })\r\n }, [client, status, category])\r\n\r\n useEffect(\r\n () =>\r\n addEventListener((event) => {\r\n if (event.type !== 'item.created') return\r\n const partial: HermsInboxItem = {\r\n id: event.data.id,\r\n title: event.data.title,\r\n body: '',\r\n actionUrl: null,\r\n category: null,\r\n seenAt: null,\r\n readAt: null,\r\n createdAt: event.data.created_at,\r\n }\r\n setItems((prev) => (prev.some((item) => item.id === partial.id) ? prev : [partial, ...prev]))\r\n reconcileFirstPage()\r\n }),\r\n [addEventListener, reconcileFirstPage],\r\n )\r\n\r\n const markRead = useCallback(\r\n async (id: string) => {\r\n const updated = await client.markRead(id)\r\n setItems((prev) => prev.map((item) => (item.id === id ? updated : item)))\r\n },\r\n [client],\r\n )\r\n\r\n const markAllRead = useCallback(async () => {\r\n await client.markAllRead()\r\n const now = new Date().toISOString()\r\n setItems((prev) => prev.map((item) => (item.readAt ? item : { ...item, readAt: now })))\r\n }, [client])\r\n\r\n const archive = useCallback(\r\n async (id: string) => {\r\n await client.archive(id)\r\n // F-INB-3: archiving is an exit from the default feed, not a toggle within it.\r\n setItems((prev) => prev.filter((item) => item.id !== id))\r\n },\r\n [client],\r\n )\r\n\r\n const remove = useCallback(\r\n async (id: string) => {\r\n await client.delete(id)\r\n setItems((prev) => prev.filter((item) => item.id !== id))\r\n },\r\n [client],\r\n )\r\n\r\n return {\r\n items,\r\n isLoading,\r\n isLoadingMore,\r\n error,\r\n hasMore,\r\n loadMore,\r\n markRead,\r\n markAllRead,\r\n archive,\r\n remove,\r\n refetch: fetchFirstPage,\r\n }\r\n}\r\n","import { useCallback, useEffect, useState } from 'react'\r\nimport type { HermsPreferences, HermsPreferenceUpdate } from '../core/types'\r\nimport { useHermsContext } from './HermsProvider'\r\n\r\nexport interface UsePreferencesResult {\r\n /** `null` until the first read resolves. */\r\n preferences: HermsPreferences | null\r\n isLoading: boolean\r\n /** The failure of the last read or write, or `null`. Kept rather than thrown so a\r\n * preference centre can show it beside the controls instead of unmounting them. */\r\n error: Error | null\r\n /** Change one setting. Resolves once the server's new state has been applied. */\r\n setPreference: (update: HermsPreferenceUpdate) => Promise<void>\r\n /** Re-read, for a retry button. */\r\n reload: () => Promise<void>\r\n}\r\n\r\n/**\r\n * Headless preferences, the counterpart to `useInbox`.\r\n *\r\n * **No optimistic update, deliberately.** A toggle here is not a like button: turning\r\n * off a channel is a consent decision, and showing it as done before the server agreed\r\n * is showing somebody they have opted out when they may not have. The server answers\r\n * `PATCH` with the *whole* updated state, so the correct view arrives with the write\r\n * and there is nothing to reconcile. A per-row pending state in the host is a better\r\n * answer to the latency than a lie.\r\n *\r\n * **`error` is state, not an exception.** `setPreference` still rejects, so a caller\r\n * can `await` it and react per-control, but the hook also holds the failure so a\r\n * preference centre can render it without every consumer writing the same try/catch.\r\n * A failed write leaves the last known-good state on screen rather than an empty page.\r\n *\r\n * Critical categories arrive with `isCritical: true` and are always delivered. Render\r\n * them (a subscriber should see what they are receiving), but not as a control: an\r\n * affordance that refuses is worse than none.\r\n */\r\nexport function usePreferences(): UsePreferencesResult {\r\n const { client } = useHermsContext()\r\n const [preferences, setPreferences] = useState<HermsPreferences | null>(null)\r\n const [isLoading, setIsLoading] = useState(true)\r\n const [error, setError] = useState<Error | null>(null)\r\n\r\n const load = useCallback(async () => {\r\n setIsLoading(true)\r\n try {\r\n const fresh = await client.getPreferences()\r\n setPreferences(fresh)\r\n setError(null)\r\n } catch (caught) {\r\n setError(caught instanceof Error ? caught : new Error(String(caught)))\r\n } finally {\r\n setIsLoading(false)\r\n }\r\n }, [client])\r\n\r\n useEffect(() => {\r\n let cancelled = false\r\n setIsLoading(true)\r\n client\r\n .getPreferences()\r\n .then((fresh) => {\r\n if (cancelled) return\r\n setPreferences(fresh)\r\n setError(null)\r\n })\r\n .catch((caught: unknown) => {\r\n // Guarded on `cancelled` like the success path: a client swap or an unmount\r\n // mid-flight must not write the old client's failure into the new one's state.\r\n if (cancelled) return\r\n setError(caught instanceof Error ? caught : new Error(String(caught)))\r\n })\r\n .finally(() => {\r\n if (!cancelled) setIsLoading(false)\r\n })\r\n return () => {\r\n cancelled = true\r\n }\r\n }, [client])\r\n\r\n const setPreference = useCallback(\r\n async (update: HermsPreferenceUpdate) => {\r\n try {\r\n // The response is the whole new state, so this replaces rather than patches.\r\n setPreferences(await client.updatePreference(update))\r\n setError(null)\r\n } catch (caught) {\r\n setError(caught instanceof Error ? caught : new Error(String(caught)))\r\n // Rethrown as well as held: a host that awaits this needs to know its one\r\n // toggle failed, and a host that does not still gets `error` for the page.\r\n throw caught\r\n }\r\n },\r\n [client],\r\n )\r\n\r\n return { preferences, isLoading, error, setPreference, reload: load }\r\n}\r\n","import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent } from 'react'\r\nimport * as Popover from '@radix-ui/react-popover'\r\nimport type { HermsInboxItem } from '../core/types'\r\nimport { useHermsContext } from './HermsProvider'\r\nimport { useInbox } from './useInbox'\r\nimport { useUnreadCount } from './useUnreadCount'\r\nimport { getHermsInboxStrings, type HermsLocale } from './locale'\r\nimport './HermsInbox.css'\r\n\r\nexport type HermsInboxPlacement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'\r\n\r\nexport interface HermsInboxTheme {\r\n accent?: string\r\n radius?: string\r\n}\r\n\r\nexport interface HermsInboxProps {\r\n /** Popover position relative to the bell trigger. Default `bottom-end`. */\r\n placement?: HermsInboxPlacement\r\n /** Called when an item is activated (click, or Enter on a focused row).\r\n * Lets a host app drive its own router (§8.5's own example:\r\n * `onItemClick={(item) => navigate(item.actionUrl)}`) instead of a hard\r\n * page navigation. When omitted, activating an item with a non-null\r\n * `actionUrl` does a plain `window.location.assign` to it. Either way, the\r\n * item is marked read first. */\r\n onItemClick?: (item: HermsInboxItem) => void\r\n /** A couple of the most commonly re-themed CSS custom properties, settable\r\n * without a host app writing any CSS of its own. Anything else is\r\n * reachable by overriding `--herms-color-*`/`--herms-radius` variables\r\n * directly (see `HermsInbox.css`). */\r\n theme?: HermsInboxTheme\r\n /** `'auto'` (default) follows the visitor's OS `prefers-color-scheme`;\r\n * `'light'`/`'dark'` force one regardless of it. */\r\n colorScheme?: 'auto' | 'light' | 'dark'\r\n locale?: HermsLocale\r\n className?: string\r\n}\r\n\r\nconst placementToSideAlign: Record<HermsInboxPlacement, { side: 'top' | 'bottom'; align: 'start' | 'end' }> = {\r\n 'bottom-start': { side: 'bottom', align: 'start' },\r\n 'bottom-end': { side: 'bottom', align: 'end' },\r\n 'top-start': { side: 'top', align: 'start' },\r\n 'top-end': { side: 'top', align: 'end' },\r\n}\r\n\r\nfunction BellIcon() {\r\n return (\r\n <svg className=\"herms-inbox__bell\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\r\n <path d=\"M6 8a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6\" />\r\n <path d=\"M10 21a2 2 0 0 0 4 0\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction CloseIcon() {\r\n return (\r\n <svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden=\"true\">\r\n <path d=\"M18 6 6 18M6 6l12 12\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction ArchiveIcon() {\r\n return (\r\n <svg viewBox=\"0 0 24 24\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\r\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"4\" rx=\"1\" />\r\n <path d=\"M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8\" />\r\n <path d=\"M10 13h4\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction AlertIcon() {\r\n return (\r\n <svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\r\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\r\n <path d=\"M12 8v5M12 16h.01\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction relativeTime(iso: string, locale: HermsLocale): string {\r\n const then = new Date(iso).getTime()\r\n if (Number.isNaN(then)) return ''\r\n const diffSeconds = Math.max(0, Math.round((Date.now() - then) / 1000))\r\n const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })\r\n if (diffSeconds < 60) return formatter.format(0, 'minute')\r\n const diffMinutes = Math.round(diffSeconds / 60)\r\n if (diffMinutes < 60) return formatter.format(-diffMinutes, 'minute')\r\n const diffHours = Math.round(diffMinutes / 60)\r\n if (diffHours < 24) return formatter.format(-diffHours, 'hour')\r\n const diffDays = Math.round(diffHours / 24)\r\n return formatter.format(-diffDays, 'day')\r\n}\r\n\r\n/**\r\n * `<HermsInbox />`: bell trigger + dropdown panel (§4.8/F-INB-5). Built\r\n * entirely on the headless `useInbox`/`useUnreadCount` hooks; every state\r\n * (loading, empty, error, populated) renders here, none of it invented by a\r\n * host app. Accessible: a real `<button>` trigger with `aria-label`/\r\n * `aria-expanded`, the panel as `role=\"menu\"` with roving-tabindex arrow-key\r\n * navigation, `Home`/`End`, `Escape`-to-close (native to Radix `Popover`,\r\n * which also returns focus to the trigger), and `Enter`/`Space` (native\r\n * `<button>` behavior) to activate the focused item.\r\n */\r\nexport function HermsInbox({ placement = 'bottom-end', onItemClick, theme, colorScheme = 'auto', locale, className }: HermsInboxProps) {\r\n const { client } = useHermsContext()\r\n const { unread, unseen } = useUnreadCount()\r\n const { items, isLoading, isLoadingMore, error, hasMore, loadMore, markRead, markAllRead, archive, refetch } = useInbox()\r\n const [open, setOpen] = useState(false)\r\n const [activeIndex, setActiveIndex] = useState(0)\r\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([])\r\n const seenIdsRef = useRef<Set<string>>(new Set())\r\n const strings = getHermsInboxStrings(locale)\r\n\r\n // F-INB-1/§9.4: \"seen\" (the bell was opened) is distinct from \"read.\" Marks\r\n // every currently loaded, not-yet-seen id seen once the panel is open and\r\n // has items, including any additional ids that arrive later via\r\n // `loadMore`/live updates while it stays open.\r\n useEffect(() => {\r\n if (!open || items.length === 0) return\r\n const toMark = items.filter((item) => !seenIdsRef.current.has(item.id)).map((item) => item.id)\r\n if (toMark.length === 0) return\r\n for (const id of toMark) seenIdsRef.current.add(id)\r\n void client.markSeen(toMark)\r\n }, [open, items, client])\r\n\r\n useEffect(() => {\r\n if (open) setActiveIndex(0)\r\n }, [open])\r\n\r\n const focusItem = useCallback((index: number) => {\r\n setActiveIndex(index)\r\n itemRefs.current[index]?.focus()\r\n }, [])\r\n\r\n const handleListKeyDown = useCallback(\r\n (event: KeyboardEvent<HTMLUListElement>) => {\r\n if (items.length === 0) return\r\n switch (event.key) {\r\n case 'ArrowDown':\r\n event.preventDefault()\r\n focusItem(Math.min(activeIndex + 1, items.length - 1))\r\n break\r\n case 'ArrowUp':\r\n event.preventDefault()\r\n focusItem(Math.max(activeIndex - 1, 0))\r\n break\r\n case 'Home':\r\n event.preventDefault()\r\n focusItem(0)\r\n break\r\n case 'End':\r\n event.preventDefault()\r\n focusItem(items.length - 1)\r\n break\r\n default:\r\n break\r\n }\r\n },\r\n [items.length, activeIndex, focusItem],\r\n )\r\n\r\n const handleActivate = useCallback(\r\n (item: HermsInboxItem) => {\r\n if (!item.readAt) void markRead(item.id)\r\n if (onItemClick) {\r\n onItemClick(item)\r\n } else if (item.actionUrl && typeof window !== 'undefined') {\r\n window.location.assign(item.actionUrl)\r\n }\r\n },\r\n [markRead, onItemClick],\r\n )\r\n\r\n const rootStyle = useMemo<CSSProperties>(() => {\r\n const style: CSSProperties & Record<string, string> = {}\r\n if (theme?.accent) style['--herms-color-accent'] = theme.accent\r\n if (theme?.radius) style['--herms-radius'] = theme.radius\r\n return style\r\n }, [theme?.accent, theme?.radius])\r\n\r\n const { side, align } = placementToSideAlign[placement]\r\n const rootClassName = ['herms-inbox', className].filter(Boolean).join(' ')\r\n\r\n return (\r\n <div className={rootClassName} style={rootStyle} data-herms-color-scheme={colorScheme === 'auto' ? undefined : colorScheme}>\r\n <span className=\"herms-inbox__visually-hidden\" role=\"status\" aria-live=\"polite\">\r\n {unread > 0 ? strings.bellLabelWithUnread(unread) : ''}\r\n </span>\r\n <Popover.Root open={open} onOpenChange={setOpen}>\r\n <Popover.Trigger asChild>\r\n <button\r\n type=\"button\"\r\n className=\"herms-inbox__trigger\"\r\n aria-label={unread > 0 ? strings.bellLabelWithUnread(unread) : strings.bellLabel}\r\n aria-expanded={open}\r\n >\r\n <BellIcon />\r\n {unseen > 0 && (\r\n <span className=\"herms-inbox__badge\" aria-hidden=\"true\">\r\n {unseen > 99 ? '99+' : unseen}\r\n </span>\r\n )}\r\n </button>\r\n </Popover.Trigger>\r\n <Popover.Portal>\r\n <Popover.Content\r\n className=\"herms-inbox__panel\"\r\n side={side}\r\n align={align}\r\n sideOffset={8}\r\n onOpenAutoFocus={(event) => {\r\n // Radix's default is to focus the content wrapper itself; we\r\n // want the first item (or the header when there are none)\r\n // focused instead, so keyboard users land somewhere useful.\r\n if (items.length > 0) {\r\n event.preventDefault()\r\n itemRefs.current[0]?.focus()\r\n }\r\n }}\r\n >\r\n <div className=\"herms-inbox__header\">\r\n <p className=\"herms-inbox__title\">{strings.panelTitle}</p>\r\n <div className=\"herms-inbox__header-actions\">\r\n <button type=\"button\" className=\"herms-inbox__text-button\" onClick={() => void markAllRead()} disabled={unread === 0}>\r\n {strings.markAllRead}\r\n </button>\r\n <Popover.Close asChild>\r\n <button type=\"button\" className=\"herms-inbox__icon-button\" aria-label={strings.close}>\r\n <CloseIcon />\r\n </button>\r\n </Popover.Close>\r\n </div>\r\n </div>\r\n\r\n {isLoading ? (\r\n <div role=\"status\" aria-label={strings.loading}>\r\n {[0, 1, 2, 3].map((i) => (\r\n <div key={i} className=\"herms-inbox__skeleton-line\" style={{ width: i % 2 === 0 ? '85%' : '65%' }} />\r\n ))}\r\n </div>\r\n ) : error ? (\r\n <div className=\"herms-inbox__state\" role=\"alert\">\r\n <AlertIcon />\r\n <p className=\"herms-inbox__state-title\">{strings.errorTitle}</p>\r\n <button type=\"button\" className=\"herms-inbox__text-button\" onClick={() => refetch()}>\r\n {strings.retry}\r\n </button>\r\n </div>\r\n ) : items.length === 0 ? (\r\n <div className=\"herms-inbox__state\">{strings.empty}</div>\r\n ) : (\r\n <>\r\n <ul className=\"herms-inbox__list\" role=\"menu\" aria-label={strings.panelTitle} onKeyDown={handleListKeyDown}>\r\n {items.map((item, index) => (\r\n <li key={item.id} className=\"herms-inbox__list-row\" role=\"none\">\r\n <button\r\n type=\"button\"\r\n role=\"menuitem\"\r\n ref={(el) => {\r\n itemRefs.current[index] = el\r\n }}\r\n tabIndex={index === activeIndex ? 0 : -1}\r\n className=\"herms-inbox__item\"\r\n data-unread={!item.readAt}\r\n onFocus={() => setActiveIndex(index)}\r\n onClick={() => handleActivate(item)}\r\n >\r\n <span className=\"herms-inbox__item-title\">{item.title}</span>\r\n {item.body && <span className=\"herms-inbox__item-body\">{item.body}</span>}\r\n <span className=\"herms-inbox__item-meta\">{relativeTime(item.createdAt, locale ?? 'en')}</span>\r\n </button>\r\n <button\r\n type=\"button\"\r\n className=\"herms-inbox__archive-button\"\r\n aria-label={`${strings.archive}: ${item.title}`}\r\n onClick={(event) => {\r\n event.stopPropagation()\r\n void archive(item.id)\r\n }}\r\n >\r\n <ArchiveIcon />\r\n </button>\r\n </li>\r\n ))}\r\n </ul>\r\n {hasMore && (\r\n <button type=\"button\" className=\"herms-inbox__load-more\" onClick={loadMore} disabled={isLoadingMore}>\r\n {isLoadingMore ? strings.loadingMore : strings.loadMore}\r\n </button>\r\n )}\r\n </>\r\n )}\r\n </Popover.Content>\r\n </Popover.Portal>\r\n </Popover.Root>\r\n </div>\r\n )\r\n}\r\n","/**\r\n * `<HermsInbox />`'s own tiny, self-contained EN/FR string table.\r\n *\r\n * Deliberately **not** i18next: this package ships to arbitrary host apps\r\n * (§8.5: \"zero hard dependency on the host app's state library\") that may run\r\n * their own i18n stack, a different one, or none at all. Pulling i18next in\r\n * as a real dependency here would be exactly the kind of host-app-shaped\r\n * assumption the SDK is supposed to avoid, for a handful of short strings.\r\n * `locale` mirrors §8.5's own `<HermsProvider locale=\"fr\">` example prop.\r\n */\r\n\r\nexport type HermsLocale = 'en' | 'fr'\r\n\r\nexport interface HermsInboxStrings {\r\n bellLabel: string\r\n bellLabelWithUnread: (count: number) => string\r\n panelTitle: string\r\n markAllRead: string\r\n close: string\r\n loading: string\r\n empty: string\r\n errorTitle: string\r\n retry: string\r\n loadMore: string\r\n loadingMore: string\r\n archive: string\r\n unreadDot: string\r\n}\r\n\r\nconst en: HermsInboxStrings = {\r\n bellLabel: 'Notifications',\r\n bellLabelWithUnread: (count) => `Notifications, ${count} unread`,\r\n panelTitle: 'Notifications',\r\n markAllRead: 'Mark all as read',\r\n close: 'Close',\r\n loading: 'Loading notifications…',\r\n empty: \"You're all caught up\",\r\n errorTitle: \"Couldn't load notifications.\",\r\n retry: 'Retry',\r\n loadMore: 'Load more',\r\n loadingMore: 'Loading…',\r\n archive: 'Archive',\r\n unreadDot: 'Unread',\r\n}\r\n\r\nconst fr: HermsInboxStrings = {\r\n bellLabel: 'Notifications',\r\n bellLabelWithUnread: (count) => `Notifications, ${count} non lues`,\r\n panelTitle: 'Notifications',\r\n markAllRead: 'Tout marquer comme lu',\r\n close: 'Fermer',\r\n loading: 'Chargement des notifications…',\r\n empty: 'Vous êtes à jour',\r\n errorTitle: \"Impossible de charger les notifications.\",\r\n retry: 'Réessayer',\r\n loadMore: 'Charger plus',\r\n loadingMore: 'Chargement…',\r\n archive: 'Archiver',\r\n unreadDot: 'Non lu',\r\n}\r\n\r\nconst dictionaries: Record<HermsLocale, HermsInboxStrings> = { en, fr }\r\n\r\nexport function getHermsInboxStrings(locale: HermsLocale | undefined): HermsInboxStrings {\r\n return dictionaries[locale ?? 'en'] ?? en\r\n}\r\n"],"mappings":"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","names":["import_react","import_react","import_react","import_react","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/HermsProvider.tsx","../src/useUnreadCount.ts","../src/useInbox.ts","../src/usePreferences.ts","../src/HermsInbox.tsx","../src/locale.ts"],"sourcesContent":["/**\n * `@hermesihq/react`: React bindings for Hermesi's in-app inbox.\n *\n * Two parts, from the one entry point:\n *\n * - **The client surface**, re-exported from `@hermesihq/js` so that\n * `import { HermsClient } from '@hermesihq/react'` keeps working. It is exactly the\n * names this package has always exported from there and no more: the stores and the\n * session are `@hermesihq/js`'s to export, and a React host has the hooks.\n * - **`HermsProvider`, `useUnreadCount`, `useInbox`, `usePreferences` and\n * `<HermsInbox />`.** The hooks are a thin binding over `@hermesihq/js`'s stores; the\n * component is a ready bell and panel built on them.\n */\nexport { HermsClient, HermsApiError, HERMS_CHANNELS, decodeSubscriberTokenExp } from '@hermesihq/js'\nexport type {\n HermsCategoryPreference,\n HermsChannel,\n HermsChannelPreference,\n HermsClientOptions,\n HermsErrorDetail,\n HermsEventListener,\n HermsInboxCategory,\n HermsInboxCounts,\n HermsInboxItem,\n HermsInboxListParams,\n HermsInboxPage,\n HermsInboxReadStatus,\n HermsPreferences,\n HermsPreferenceUpdate,\n HermsRealtimeEvent,\n HermsRegisterChannelParams,\n} from '@hermesihq/js'\n\nexport { HermsProvider, useHermsContext, type HermsProviderProps } from './HermsProvider'\nexport { useUnreadCount, type UseUnreadCountResult } from './useUnreadCount'\nexport { useInbox, type UseInboxOptions, type UseInboxResult } from './useInbox'\nexport { usePreferences, type UsePreferencesResult } from './usePreferences'\nexport { HermsInbox, type HermsInboxProps, type HermsInboxPlacement, type HermsInboxTheme } from './HermsInbox'\nexport { type HermsLocale } from './locale'\n","import { createContext, useContext, useEffect, useMemo, type ReactNode } from 'react'\r\nimport type { HermsClient } from '@hermesihq/js'\r\nimport { HermsSession, type HermsStoreHost } from '@hermesihq/js'\r\n\r\n/**\r\n * What every hook in this package reads from context. Exactly the shape the stores take\r\n * as their host, so a hook hands this straight to the store it creates.\r\n */\r\ntype HermsContextValue = HermsStoreHost\r\n\r\nconst HermsContext = createContext<HermsContextValue | null>(null)\r\n\r\nexport interface HermsProviderProps {\r\n client: HermsClient\r\n children: ReactNode\r\n}\r\n\r\n/**\r\n * `<HermsProvider client={new HermsClient({...})}>`: one `HermsClient` instance for the\r\n * whole subtree. Opens the client's real-time subscription once on mount and tears it\r\n * (and the client's own `onTokenExpiring` timer) down on unmount; every listener\r\n * registered via `addEventListener` shares that single connection rather than each hook\r\n * paying for its own.\r\n *\r\n * The connection itself is `HermsSession`, which knows nothing about React: this\r\n * component is the few lines that tie its lifetime to a mount. That is deliberate, and it\r\n * is the same session a Vue or vanilla page uses.\r\n *\r\n * `client` is expected to be a stable reference (constructed once, e.g. in\r\n * `useMemo`/module scope in the host app). Passing a new instance on every render tears\r\n * down and reopens the underlying connection every render.\r\n */\r\nexport function HermsProvider({ client, children }: HermsProviderProps) {\r\n const session = useMemo(() => new HermsSession(client), [client])\r\n\r\n // `connect()` returns its own release, which is the effect's cleanup. Reference-counted\r\n // in the session, so Strict Mode's connect, release, connect comes out right.\r\n useEffect(() => session.connect(), [session])\r\n\r\n const value = useMemo<HermsContextValue>(\r\n () => ({ client, addEventListener: session.addEventListener }),\r\n [client, session],\r\n )\r\n\r\n return <HermsContext.Provider value={value}>{children}</HermsContext.Provider>\r\n}\r\n\r\n/** Every hook/component in this package throws this same error outside a\r\n * `<HermsProvider>`: there is no sensible default client to fall back to. */\r\nexport function useHermsContext(): HermsContextValue {\r\n const ctx = useContext(HermsContext)\r\n if (!ctx) {\r\n throw new Error('@hermesihq/react: this hook/component must be rendered inside <HermsProvider client={...}>.')\r\n }\r\n return ctx\r\n}\r\n","import { useEffect, useMemo, useSyncExternalStore } from 'react'\r\nimport { CountsStore, type CountsState } from '@hermesihq/js'\r\nimport { useHermsContext } from './HermsProvider'\r\n\r\nexport type UseUnreadCountResult = CountsState\r\n\r\n/**\r\n * Headless. `{unread, unseen, isLoading}`, kept live via the provider's shared\r\n * subscription: one `getCounts()` call on mount, then recomputed on every\r\n * `counts.changed` event (real-time push, or the client's own 60s polling fallback) for\r\n * as long as this component stays mounted.\r\n *\r\n * A binding over `CountsStore`, which holds all of that. The store's constructor does\r\n * nothing, so React discarding a memoised one costs nothing: it is the `connect()` in the\r\n * effect that starts work.\r\n */\r\nexport function useUnreadCount(): UseUnreadCountResult {\r\n const host = useHermsContext()\r\n const store = useMemo(() => new CountsStore(host), [host])\r\n\r\n useEffect(() => store.connect(), [store])\r\n\r\n // The initial state is also the server snapshot: nothing connects during a server\r\n // render, so this is exactly what the server would produce.\r\n return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)\r\n}\r\n","import { useEffect, useMemo, useSyncExternalStore } from 'react'\r\nimport { InboxStore } from '@hermesihq/js'\r\nimport type { HermsInboxItem, HermsInboxReadStatus } from '@hermesihq/js'\r\nimport { useHermsContext } from './HermsProvider'\r\n\r\nexport interface UseInboxOptions {\r\n status?: HermsInboxReadStatus\r\n category?: string\r\n}\r\n\r\nexport interface UseInboxResult {\r\n items: HermsInboxItem[]\r\n isLoading: boolean\r\n isLoadingMore: boolean\r\n error: Error | null\r\n hasMore: boolean\r\n loadMore: () => void\r\n markRead: (id: string) => Promise<void>\r\n markAllRead: () => Promise<void>\r\n archive: (id: string) => Promise<void>\r\n remove: (id: string) => Promise<void>\r\n refetch: () => void\r\n}\r\n\r\n/**\r\n * Headless, cursor-paginated inbox list state: thin wrappers around `HermsClient`'s own\r\n * methods that also patch local state, so a consumer never has to manually refetch the\r\n * whole list after every mutating action.\r\n *\r\n * Live-updated on `item.created`: the new item is prepended immediately from the\r\n * (deliberately minimal: `{id, title, created_at}`) SSE payload so the UI reacts\r\n * instantly, then reconciled against a fresh first page fetched in the background so the\r\n * authoritative row (body, category, action_url) lands without a full list\r\n * reload/flicker.\r\n *\r\n * All of that lives in `InboxStore`; this is the binding. The filter is handed to the\r\n * store rather than being part of what identifies it, so changing tabs reloads the list\r\n * in place, keeping the rows on screen, instead of discarding the store and blanking the\r\n * panel.\r\n */\r\nexport function useInbox(options: UseInboxOptions = {}): UseInboxResult {\r\n const host = useHermsContext()\r\n const { status, category } = options\r\n\r\n // Only the first render's filter is used to create the store. Later changes go through\r\n // `setFilter` below, so this memo is keyed on the host alone.\r\n const store = useMemo(() => new InboxStore(host, { status, category }), [host])\r\n\r\n useEffect(() => store.connect(), [store])\r\n // A no-op when the filter is the one the store already has, which is the case on the\r\n // first render: `connect()` above has just loaded it.\r\n useEffect(() => {\r\n store.setFilter({ status, category })\r\n }, [store, status, category])\r\n\r\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)\r\n\r\n return useMemo(\r\n () => ({\r\n ...state,\r\n // Arrow-function fields on the store: the same references for its whole life, so a\r\n // consumer's `useCallback`/`memo` dependencies do not churn as the state changes.\r\n loadMore: store.loadMore,\r\n markRead: store.markRead,\r\n markAllRead: store.markAllRead,\r\n archive: store.archive,\r\n remove: store.remove,\r\n refetch: store.refetch,\r\n }),\r\n [state, store],\r\n )\r\n}\r\n","import { useEffect, useMemo, useSyncExternalStore } from 'react'\r\nimport { PreferencesStore } from '@hermesihq/js'\r\nimport type { HermsPreferences, HermsPreferenceUpdate } from '@hermesihq/js'\r\nimport { useHermsContext } from './HermsProvider'\r\n\r\nexport interface UsePreferencesResult {\r\n /** `null` until the first read resolves. */\r\n preferences: HermsPreferences | null\r\n isLoading: boolean\r\n /** The failure of the last read or write, or `null`. Kept rather than thrown so a\r\n * preference centre can show it beside the controls instead of unmounting them. */\r\n error: Error | null\r\n /** Change one setting. Resolves once the server's new state has been applied. */\r\n setPreference: (update: HermsPreferenceUpdate) => Promise<void>\r\n /** Re-read, for a retry button. */\r\n reload: () => Promise<void>\r\n}\r\n\r\n/**\r\n * Headless preferences, the counterpart to `useInbox`.\r\n *\r\n * **No optimistic update, deliberately.** A toggle here is not a like button: turning\r\n * off a channel is a consent decision, and showing it as done before the server agreed is\r\n * showing somebody they have opted out when they may not have. The server answers `PATCH`\r\n * with the *whole* updated state, so the correct view arrives with the write and there is\r\n * nothing to reconcile. A per-row pending state in the host is a better answer to the\r\n * latency than a lie.\r\n *\r\n * **`error` is state, not an exception.** `setPreference` still rejects, so a caller can\r\n * `await` it and react per-control, but the hook also holds the failure so a preference\r\n * centre can render it without every consumer writing the same try/catch. A failed write\r\n * leaves the last known-good state on screen rather than an empty page.\r\n *\r\n * Critical categories arrive with `isCritical: true` and are always delivered. Render\r\n * them (a subscriber should see what they are receiving), but not as a control: an\r\n * affordance that refuses is worse than none.\r\n *\r\n * A binding over `PreferencesStore`, which holds the behaviour above.\r\n */\r\nexport function usePreferences(): UsePreferencesResult {\r\n const host = useHermsContext()\r\n const store = useMemo(() => new PreferencesStore(host), [host])\r\n\r\n useEffect(() => store.connect(), [store])\r\n\r\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)\r\n\r\n return useMemo(\r\n () => ({ ...state, setPreference: store.setPreference, reload: store.reload }),\r\n [state, store],\r\n )\r\n}\r\n","import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent } from 'react'\r\nimport * as Popover from '@radix-ui/react-popover'\r\nimport type { HermsInboxItem } from '@hermesihq/js'\r\nimport { useHermsContext } from './HermsProvider'\r\nimport { useInbox } from './useInbox'\r\nimport { useUnreadCount } from './useUnreadCount'\r\nimport { getHermsInboxStrings, type HermsLocale } from './locale'\r\nimport './HermsInbox.css'\r\n\r\nexport type HermsInboxPlacement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'\r\n\r\nexport interface HermsInboxTheme {\r\n accent?: string\r\n radius?: string\r\n}\r\n\r\nexport interface HermsInboxProps {\r\n /** Popover position relative to the bell trigger. Default `bottom-end`. */\r\n placement?: HermsInboxPlacement\r\n /** Called when an item is activated (click, or Enter on a focused row).\r\n * Lets a host app drive its own router (for example\r\n * `onItemClick={(item) => navigate(item.actionUrl)}`) instead of a hard\r\n * page navigation. When omitted, activating an item with a non-null\r\n * `actionUrl` does a plain `window.location.assign` to it. Either way, the\r\n * item is marked read first. */\r\n onItemClick?: (item: HermsInboxItem) => void\r\n /** A couple of the most commonly re-themed CSS custom properties, settable\r\n * without a host app writing any CSS of its own. Anything else is\r\n * reachable by overriding `--herms-color-*`/`--herms-radius` variables\r\n * directly (see `HermsInbox.css`). */\r\n theme?: HermsInboxTheme\r\n /** `'auto'` (default) follows the visitor's OS `prefers-color-scheme`;\r\n * `'light'`/`'dark'` force one regardless of it. */\r\n colorScheme?: 'auto' | 'light' | 'dark'\r\n locale?: HermsLocale\r\n className?: string\r\n}\r\n\r\nconst placementToSideAlign: Record<HermsInboxPlacement, { side: 'top' | 'bottom'; align: 'start' | 'end' }> = {\r\n 'bottom-start': { side: 'bottom', align: 'start' },\r\n 'bottom-end': { side: 'bottom', align: 'end' },\r\n 'top-start': { side: 'top', align: 'start' },\r\n 'top-end': { side: 'top', align: 'end' },\r\n}\r\n\r\nfunction BellIcon() {\r\n return (\r\n <svg className=\"herms-inbox__bell\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\r\n <path d=\"M6 8a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6\" />\r\n <path d=\"M10 21a2 2 0 0 0 4 0\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction CloseIcon() {\r\n return (\r\n <svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden=\"true\">\r\n <path d=\"M18 6 6 18M6 6l12 12\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction ArchiveIcon() {\r\n return (\r\n <svg viewBox=\"0 0 24 24\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\r\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"4\" rx=\"1\" />\r\n <path d=\"M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8\" />\r\n <path d=\"M10 13h4\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction AlertIcon() {\r\n return (\r\n <svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\r\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\r\n <path d=\"M12 8v5M12 16h.01\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction relativeTime(iso: string, locale: HermsLocale): string {\r\n const then = new Date(iso).getTime()\r\n if (Number.isNaN(then)) return ''\r\n const diffSeconds = Math.max(0, Math.round((Date.now() - then) / 1000))\r\n const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })\r\n if (diffSeconds < 60) return formatter.format(0, 'minute')\r\n const diffMinutes = Math.round(diffSeconds / 60)\r\n if (diffMinutes < 60) return formatter.format(-diffMinutes, 'minute')\r\n const diffHours = Math.round(diffMinutes / 60)\r\n if (diffHours < 24) return formatter.format(-diffHours, 'hour')\r\n const diffDays = Math.round(diffHours / 24)\r\n return formatter.format(-diffDays, 'day')\r\n}\r\n\r\n/**\r\n * `<HermsInbox />`: bell trigger + dropdown panel. Built\r\n * entirely on the headless `useInbox`/`useUnreadCount` hooks; every state\r\n * (loading, empty, error, populated) renders here, none of it invented by a\r\n * host app. Accessible: a real `<button>` trigger with `aria-label`/\r\n * `aria-expanded`, the panel as `role=\"menu\"` with roving-tabindex arrow-key\r\n * navigation, `Home`/`End`, `Escape`-to-close (native to Radix `Popover`,\r\n * which also returns focus to the trigger), and `Enter`/`Space` (native\r\n * `<button>` behavior) to activate the focused item.\r\n */\r\nexport function HermsInbox({ placement = 'bottom-end', onItemClick, theme, colorScheme = 'auto', locale, className }: HermsInboxProps) {\r\n const { client } = useHermsContext()\r\n const { unread, unseen } = useUnreadCount()\r\n const { items, isLoading, isLoadingMore, error, hasMore, loadMore, markRead, markAllRead, archive, refetch } = useInbox()\r\n const [open, setOpen] = useState(false)\r\n const [activeIndex, setActiveIndex] = useState(0)\r\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([])\r\n const seenIdsRef = useRef<Set<string>>(new Set())\r\n const strings = getHermsInboxStrings(locale)\r\n\r\n // \"Seen\" (the bell was opened) is distinct from \"read.\" Marks\r\n // every currently loaded, not-yet-seen id seen once the panel is open and\r\n // has items, including any additional ids that arrive later via\r\n // `loadMore`/live updates while it stays open.\r\n useEffect(() => {\r\n if (!open || items.length === 0) return\r\n const toMark = items.filter((item) => !seenIdsRef.current.has(item.id)).map((item) => item.id)\r\n if (toMark.length === 0) return\r\n for (const id of toMark) seenIdsRef.current.add(id)\r\n void client.markSeen(toMark)\r\n }, [open, items, client])\r\n\r\n useEffect(() => {\r\n if (open) setActiveIndex(0)\r\n }, [open])\r\n\r\n const focusItem = useCallback((index: number) => {\r\n setActiveIndex(index)\r\n itemRefs.current[index]?.focus()\r\n }, [])\r\n\r\n const handleListKeyDown = useCallback(\r\n (event: KeyboardEvent<HTMLUListElement>) => {\r\n if (items.length === 0) return\r\n switch (event.key) {\r\n case 'ArrowDown':\r\n event.preventDefault()\r\n focusItem(Math.min(activeIndex + 1, items.length - 1))\r\n break\r\n case 'ArrowUp':\r\n event.preventDefault()\r\n focusItem(Math.max(activeIndex - 1, 0))\r\n break\r\n case 'Home':\r\n event.preventDefault()\r\n focusItem(0)\r\n break\r\n case 'End':\r\n event.preventDefault()\r\n focusItem(items.length - 1)\r\n break\r\n default:\r\n break\r\n }\r\n },\r\n [items.length, activeIndex, focusItem],\r\n )\r\n\r\n const handleActivate = useCallback(\r\n (item: HermsInboxItem) => {\r\n if (!item.readAt) void markRead(item.id)\r\n if (onItemClick) {\r\n onItemClick(item)\r\n } else if (item.actionUrl && typeof window !== 'undefined') {\r\n window.location.assign(item.actionUrl)\r\n }\r\n },\r\n [markRead, onItemClick],\r\n )\r\n\r\n const rootStyle = useMemo<CSSProperties>(() => {\r\n const style: CSSProperties & Record<string, string> = {}\r\n if (theme?.accent) style['--herms-color-accent'] = theme.accent\r\n if (theme?.radius) style['--herms-radius'] = theme.radius\r\n return style\r\n }, [theme?.accent, theme?.radius])\r\n\r\n const { side, align } = placementToSideAlign[placement]\r\n const rootClassName = ['herms-inbox', className].filter(Boolean).join(' ')\r\n\r\n return (\r\n <div className={rootClassName} style={rootStyle} data-herms-color-scheme={colorScheme === 'auto' ? undefined : colorScheme}>\r\n <span className=\"herms-inbox__visually-hidden\" role=\"status\" aria-live=\"polite\">\r\n {unread > 0 ? strings.bellLabelWithUnread(unread) : ''}\r\n </span>\r\n <Popover.Root open={open} onOpenChange={setOpen}>\r\n <Popover.Trigger asChild>\r\n <button\r\n type=\"button\"\r\n className=\"herms-inbox__trigger\"\r\n aria-label={unread > 0 ? strings.bellLabelWithUnread(unread) : strings.bellLabel}\r\n aria-expanded={open}\r\n >\r\n <BellIcon />\r\n {unseen > 0 && (\r\n <span className=\"herms-inbox__badge\" aria-hidden=\"true\">\r\n {unseen > 99 ? '99+' : unseen}\r\n </span>\r\n )}\r\n </button>\r\n </Popover.Trigger>\r\n <Popover.Portal>\r\n <Popover.Content\r\n className=\"herms-inbox__panel\"\r\n side={side}\r\n align={align}\r\n sideOffset={8}\r\n onOpenAutoFocus={(event) => {\r\n // Radix's default is to focus the content wrapper itself; we\r\n // want the first item (or the header when there are none)\r\n // focused instead, so keyboard users land somewhere useful.\r\n if (items.length > 0) {\r\n event.preventDefault()\r\n itemRefs.current[0]?.focus()\r\n }\r\n }}\r\n >\r\n <div className=\"herms-inbox__header\">\r\n <p className=\"herms-inbox__title\">{strings.panelTitle}</p>\r\n <div className=\"herms-inbox__header-actions\">\r\n <button type=\"button\" className=\"herms-inbox__text-button\" onClick={() => void markAllRead()} disabled={unread === 0}>\r\n {strings.markAllRead}\r\n </button>\r\n <Popover.Close asChild>\r\n <button type=\"button\" className=\"herms-inbox__icon-button\" aria-label={strings.close}>\r\n <CloseIcon />\r\n </button>\r\n </Popover.Close>\r\n </div>\r\n </div>\r\n\r\n {isLoading ? (\r\n <div role=\"status\" aria-label={strings.loading}>\r\n {[0, 1, 2, 3].map((i) => (\r\n <div key={i} className=\"herms-inbox__skeleton-line\" style={{ width: i % 2 === 0 ? '85%' : '65%' }} />\r\n ))}\r\n </div>\r\n ) : error ? (\r\n <div className=\"herms-inbox__state\" role=\"alert\">\r\n <AlertIcon />\r\n <p className=\"herms-inbox__state-title\">{strings.errorTitle}</p>\r\n <button type=\"button\" className=\"herms-inbox__text-button\" onClick={() => refetch()}>\r\n {strings.retry}\r\n </button>\r\n </div>\r\n ) : items.length === 0 ? (\r\n <div className=\"herms-inbox__state\">{strings.empty}</div>\r\n ) : (\r\n <>\r\n <ul className=\"herms-inbox__list\" role=\"menu\" aria-label={strings.panelTitle} onKeyDown={handleListKeyDown}>\r\n {items.map((item, index) => (\r\n <li key={item.id} className=\"herms-inbox__list-row\" role=\"none\">\r\n <button\r\n type=\"button\"\r\n role=\"menuitem\"\r\n ref={(el) => {\r\n itemRefs.current[index] = el\r\n }}\r\n tabIndex={index === activeIndex ? 0 : -1}\r\n className=\"herms-inbox__item\"\r\n data-unread={!item.readAt}\r\n onFocus={() => setActiveIndex(index)}\r\n onClick={() => handleActivate(item)}\r\n >\r\n <span className=\"herms-inbox__item-title\">{item.title}</span>\r\n {item.body && <span className=\"herms-inbox__item-body\">{item.body}</span>}\r\n <span className=\"herms-inbox__item-meta\">{relativeTime(item.createdAt, locale ?? 'en')}</span>\r\n </button>\r\n <button\r\n type=\"button\"\r\n className=\"herms-inbox__archive-button\"\r\n aria-label={`${strings.archive}: ${item.title}`}\r\n onClick={(event) => {\r\n event.stopPropagation()\r\n void archive(item.id)\r\n }}\r\n >\r\n <ArchiveIcon />\r\n </button>\r\n </li>\r\n ))}\r\n </ul>\r\n {hasMore && (\r\n <button type=\"button\" className=\"herms-inbox__load-more\" onClick={loadMore} disabled={isLoadingMore}>\r\n {isLoadingMore ? strings.loadingMore : strings.loadMore}\r\n </button>\r\n )}\r\n </>\r\n )}\r\n </Popover.Content>\r\n </Popover.Portal>\r\n </Popover.Root>\r\n </div>\r\n )\r\n}\r\n","/**\r\n * `<HermsInbox />`'s own tiny, self-contained EN/FR string table.\r\n *\r\n * Deliberately **not** i18next: this package ships to arbitrary host apps\r\n * (with zero hard dependency on the host app's state library) that may run\r\n * their own i18n stack, a different one, or none at all. Pulling i18next in\r\n * as a real dependency here would be exactly the kind of host-app-shaped\r\n * assumption the SDK is supposed to avoid, for a handful of short strings.\r\n * `locale` is a prop of `<HermsInbox />`.\r\n */\r\n\r\nexport type HermsLocale = 'en' | 'fr'\r\n\r\nexport interface HermsInboxStrings {\r\n bellLabel: string\r\n bellLabelWithUnread: (count: number) => string\r\n panelTitle: string\r\n markAllRead: string\r\n close: string\r\n loading: string\r\n empty: string\r\n errorTitle: string\r\n retry: string\r\n loadMore: string\r\n loadingMore: string\r\n archive: string\r\n unreadDot: string\r\n}\r\n\r\nconst en: HermsInboxStrings = {\r\n bellLabel: 'Notifications',\r\n bellLabelWithUnread: (count) => `Notifications, ${count} unread`,\r\n panelTitle: 'Notifications',\r\n markAllRead: 'Mark all as read',\r\n close: 'Close',\r\n loading: 'Loading notifications…',\r\n empty: \"You're all caught up\",\r\n errorTitle: \"Couldn't load notifications.\",\r\n retry: 'Retry',\r\n loadMore: 'Load more',\r\n loadingMore: 'Loading…',\r\n archive: 'Archive',\r\n unreadDot: 'Unread',\r\n}\r\n\r\nconst fr: HermsInboxStrings = {\r\n bellLabel: 'Notifications',\r\n bellLabelWithUnread: (count) => `Notifications, ${count} non lues`,\r\n panelTitle: 'Notifications',\r\n markAllRead: 'Tout marquer comme lu',\r\n close: 'Fermer',\r\n loading: 'Chargement des notifications…',\r\n empty: 'Vous êtes à jour',\r\n errorTitle: \"Impossible de charger les notifications.\",\r\n retry: 'Réessayer',\r\n loadMore: 'Charger plus',\r\n loadingMore: 'Chargement…',\r\n archive: 'Archiver',\r\n unreadDot: 'Non lu',\r\n}\r\n\r\nconst dictionaries: Record<HermsLocale, HermsInboxStrings> = { en, fr }\r\n\r\nexport function getHermsInboxStrings(locale: HermsLocale | undefined): HermsInboxStrings {\r\n return dictionaries[locale ?? 'en'] ?? en\r\n}\r\n"],"mappings":"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","names":["import_js","import_react","import_js","import_react","import_js","import_react","import_js","import_react","import_jsx_runtime"]}
package/dist/index.css CHANGED
@@ -1,4 +1,4 @@
1
- /* src/react/HermsInbox.css */
1
+ /* src/HermsInbox.css */
2
2
  .herms-inbox {
3
3
  --herms-color-accent: #5b5bd6;
4
4
  --herms-color-accent-foreground: #ffffff;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react/HermsInbox.css"],"sourcesContent":["/*\r\n * `<HermsInbox />`'s styles: scoped entirely under `.herms-inbox` and driven\r\n * by CSS custom properties, never Tailwind utility classes (§8.5: \"styles\r\n * scoped ... so Tailwind in the host app cannot bleed into the widget\"). This\r\n * package has no Tailwind dependency at all; a host dashboard's own Tailwind\r\n * build never touches these rules, and these rules never leak into the host's\r\n * own layout beyond `.herms-inbox`'s own box.\r\n *\r\n * Every visual value is a `--herms-*` custom property with a sensible\r\n * default here, overridable by a host app either globally (setting the\r\n * variable higher up the DOM) or per-instance via `<HermsInbox theme={...}>`,\r\n * which sets a few of these inline on the root element.\r\n *\r\n * Dark mode: `data-herms-color-scheme=\"dark\"` on the root element (set by the\r\n * `colorScheme` prop) forces dark values; leaving it unset (`colorScheme=\"auto\"`,\r\n * the default) follows the visitor's OS-level `prefers-color-scheme` instead.\r\n * A host app's own dark-mode mechanism (e.g. Hermesi dashboard's `data-theme`\r\n * attribute) is a different, unrelated attribute and never bleeds in here.\r\n */\r\n\r\n.herms-inbox {\r\n --herms-color-accent: #5b5bd6;\r\n --herms-color-accent-foreground: #ffffff;\r\n --herms-color-bg: #ffffff;\r\n --herms-color-surface: #ffffff;\r\n --herms-color-border: #e5e3df;\r\n --herms-color-text: #1c1b18;\r\n --herms-color-muted: #6f6b64;\r\n --herms-color-danger: #b03a2a;\r\n --herms-color-unread-dot: #b03a2a;\r\n --herms-color-hover: #f4f3f1;\r\n --herms-radius: 10px;\r\n --herms-font-family:\r\n -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\r\n\r\n position: relative;\r\n display: inline-block;\r\n font-family: var(--herms-font-family);\r\n color: var(--herms-color-text);\r\n line-height: 1.4;\r\n}\r\n\r\n@media (prefers-color-scheme: dark) {\r\n .herms-inbox:not([data-herms-color-scheme='light']) {\r\n --herms-color-bg: #201f1a;\r\n --herms-color-surface: #201f1a;\r\n --herms-color-border: #38362f;\r\n --herms-color-text: #f3f1ec;\r\n --herms-color-muted: #a6a19a;\r\n --herms-color-hover: #2b2a24;\r\n }\r\n}\r\n\r\n.herms-inbox[data-herms-color-scheme='dark'] {\r\n --herms-color-bg: #201f1a;\r\n --herms-color-surface: #201f1a;\r\n --herms-color-border: #38362f;\r\n --herms-color-text: #f3f1ec;\r\n --herms-color-muted: #a6a19a;\r\n --herms-color-hover: #2b2a24;\r\n}\r\n\r\n.herms-inbox * {\r\n box-sizing: border-box;\r\n}\r\n\r\n.herms-inbox__trigger {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 36px;\r\n height: 36px;\r\n border-radius: 999px;\r\n border: none;\r\n background: transparent;\r\n color: var(--herms-color-text);\r\n cursor: pointer;\r\n}\r\n\r\n.herms-inbox__trigger:hover {\r\n background: var(--herms-color-hover);\r\n}\r\n\r\n.herms-inbox__trigger:focus-visible {\r\n outline: 2px solid var(--herms-color-accent);\r\n outline-offset: 2px;\r\n}\r\n\r\n.herms-inbox__bell {\r\n width: 20px;\r\n height: 20px;\r\n}\r\n\r\n.herms-inbox__badge {\r\n position: absolute;\r\n top: 4px;\r\n right: 4px;\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 999px;\r\n background: var(--herms-color-unread-dot);\r\n color: #ffffff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n}\r\n\r\n.herms-inbox__panel {\r\n width: 380px;\r\n max-width: calc(100vw - 24px);\r\n max-height: 480px;\r\n display: flex;\r\n flex-direction: column;\r\n border-radius: var(--herms-radius);\r\n border: 1px solid var(--herms-color-border);\r\n background: var(--herms-color-surface);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.16);\r\n overflow: hidden;\r\n z-index: 50;\r\n}\r\n\r\n.herms-inbox__header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 8px;\r\n padding: 12px 14px;\r\n border-bottom: 1px solid var(--herms-color-border);\r\n}\r\n\r\n.herms-inbox__title {\r\n font-size: 14px;\r\n font-weight: 600;\r\n margin: 0;\r\n}\r\n\r\n.herms-inbox__header-actions {\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n}\r\n\r\n.herms-inbox__text-button {\r\n border: none;\r\n background: transparent;\r\n color: var(--herms-color-accent);\r\n font-size: 12px;\r\n font-weight: 500;\r\n cursor: pointer;\r\n padding: 4px 6px;\r\n border-radius: 6px;\r\n}\r\n\r\n.herms-inbox__text-button:hover {\r\n background: var(--herms-color-hover);\r\n}\r\n\r\n.herms-inbox__text-button:disabled {\r\n opacity: 0.5;\r\n cursor: not-allowed;\r\n}\r\n\r\n.herms-inbox__icon-button {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 26px;\r\n height: 26px;\r\n border: none;\r\n background: transparent;\r\n color: var(--herms-color-muted);\r\n border-radius: 6px;\r\n cursor: pointer;\r\n}\r\n\r\n.herms-inbox__icon-button:hover {\r\n background: var(--herms-color-hover);\r\n color: var(--herms-color-text);\r\n}\r\n\r\n.herms-inbox__icon-button:focus-visible,\r\n.herms-inbox__text-button:focus-visible,\r\n.herms-inbox__item:focus-visible {\r\n outline: 2px solid var(--herms-color-accent);\r\n outline-offset: -2px;\r\n}\r\n\r\n.herms-inbox__list {\r\n list-style: none;\r\n margin: 0;\r\n padding: 4px;\r\n overflow-y: auto;\r\n flex: 1 1 auto;\r\n}\r\n\r\n.herms-inbox__list-row {\r\n display: flex;\r\n align-items: stretch;\r\n gap: 2px;\r\n}\r\n\r\n.herms-inbox__item {\r\n flex: 1 1 auto;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n text-align: left;\r\n border: none;\r\n background: transparent;\r\n border-radius: 8px;\r\n padding: 8px 10px;\r\n cursor: pointer;\r\n color: inherit;\r\n font: inherit;\r\n}\r\n\r\n.herms-inbox__item:hover {\r\n background: var(--herms-color-hover);\r\n}\r\n\r\n.herms-inbox__item[data-unread='true'] .herms-inbox__item-title::before {\r\n content: '';\r\n display: inline-block;\r\n width: 6px;\r\n height: 6px;\r\n margin-right: 6px;\r\n border-radius: 999px;\r\n background: var(--herms-color-accent);\r\n}\r\n\r\n.herms-inbox__item-title {\r\n font-size: 13px;\r\n font-weight: 600;\r\n display: flex;\r\n align-items: center;\r\n}\r\n\r\n.herms-inbox__item-body {\r\n font-size: 12.5px;\r\n color: var(--herms-color-muted);\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n display: -webkit-box;\r\n -webkit-line-clamp: 2;\r\n -webkit-box-orient: vertical;\r\n}\r\n\r\n.herms-inbox__item-meta {\r\n font-size: 11px;\r\n color: var(--herms-color-muted);\r\n}\r\n\r\n.herms-inbox__archive-button {\r\n flex: 0 0 auto;\r\n align-self: center;\r\n width: 28px;\r\n height: 28px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border: none;\r\n background: transparent;\r\n color: var(--herms-color-muted);\r\n border-radius: 6px;\r\n cursor: pointer;\r\n}\r\n\r\n.herms-inbox__archive-button:hover {\r\n background: var(--herms-color-hover);\r\n color: var(--herms-color-danger);\r\n}\r\n\r\n.herms-inbox__state {\r\n padding: 32px 16px;\r\n text-align: center;\r\n color: var(--herms-color-muted);\r\n font-size: 13px;\r\n}\r\n\r\n.herms-inbox__state-title {\r\n color: var(--herms-color-text);\r\n font-weight: 600;\r\n margin: 0 0 6px;\r\n}\r\n\r\n.herms-inbox__load-more {\r\n display: block;\r\n width: calc(100% - 8px);\r\n margin: 4px;\r\n padding: 8px;\r\n border-radius: 8px;\r\n border: 1px dashed var(--herms-color-border);\r\n background: transparent;\r\n color: var(--herms-color-accent);\r\n font-size: 12.5px;\r\n font-weight: 500;\r\n cursor: pointer;\r\n}\r\n\r\n.herms-inbox__load-more:hover {\r\n background: var(--herms-color-hover);\r\n}\r\n\r\n.herms-inbox__load-more:disabled {\r\n opacity: 0.6;\r\n cursor: not-allowed;\r\n}\r\n\r\n.herms-inbox__visually-hidden {\r\n position: absolute;\r\n width: 1px;\r\n height: 1px;\r\n padding: 0;\r\n margin: -1px;\r\n overflow: hidden;\r\n clip: rect(0, 0, 0, 0);\r\n white-space: nowrap;\r\n border: 0;\r\n}\r\n\r\n.herms-inbox__skeleton-line {\r\n height: 12px;\r\n border-radius: 4px;\r\n background: var(--herms-color-hover);\r\n margin: 6px 10px;\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/HermsInbox.css"],"sourcesContent":["/*\r\n * `<HermsInbox />`'s styles: scoped entirely under `.herms-inbox` and driven\r\n * by CSS custom properties, never Tailwind utility classes, so that Tailwind in the\r\n * host app cannot bleed into the widget. This\r\n * package has no Tailwind dependency at all; a host app's own Tailwind\r\n * build never touches these rules, and these rules never leak into the host's\r\n * own layout beyond `.herms-inbox`'s own box.\r\n *\r\n * Every visual value is a `--herms-*` custom property with a sensible\r\n * default here, overridable by a host app either globally (setting the\r\n * variable higher up the DOM) or per-instance via `<HermsInbox theme={...}>`,\r\n * which sets a few of these inline on the root element.\r\n *\r\n * Dark mode: `data-herms-color-scheme=\"dark\"` on the root element (set by the\r\n * `colorScheme` prop) forces dark values; leaving it unset (`colorScheme=\"auto\"`,\r\n * the default) follows the visitor's OS-level `prefers-color-scheme` instead.\r\n * A host app's own dark-mode mechanism (e.g. a `data-theme`\r\n * attribute) is a different, unrelated attribute and never bleeds in here.\r\n */\r\n\r\n.herms-inbox {\r\n --herms-color-accent: #5b5bd6;\r\n --herms-color-accent-foreground: #ffffff;\r\n --herms-color-bg: #ffffff;\r\n --herms-color-surface: #ffffff;\r\n --herms-color-border: #e5e3df;\r\n --herms-color-text: #1c1b18;\r\n --herms-color-muted: #6f6b64;\r\n --herms-color-danger: #b03a2a;\r\n --herms-color-unread-dot: #b03a2a;\r\n --herms-color-hover: #f4f3f1;\r\n --herms-radius: 10px;\r\n --herms-font-family:\r\n -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\r\n\r\n position: relative;\r\n display: inline-block;\r\n font-family: var(--herms-font-family);\r\n color: var(--herms-color-text);\r\n line-height: 1.4;\r\n}\r\n\r\n@media (prefers-color-scheme: dark) {\r\n .herms-inbox:not([data-herms-color-scheme='light']) {\r\n --herms-color-bg: #201f1a;\r\n --herms-color-surface: #201f1a;\r\n --herms-color-border: #38362f;\r\n --herms-color-text: #f3f1ec;\r\n --herms-color-muted: #a6a19a;\r\n --herms-color-hover: #2b2a24;\r\n }\r\n}\r\n\r\n.herms-inbox[data-herms-color-scheme='dark'] {\r\n --herms-color-bg: #201f1a;\r\n --herms-color-surface: #201f1a;\r\n --herms-color-border: #38362f;\r\n --herms-color-text: #f3f1ec;\r\n --herms-color-muted: #a6a19a;\r\n --herms-color-hover: #2b2a24;\r\n}\r\n\r\n.herms-inbox * {\r\n box-sizing: border-box;\r\n}\r\n\r\n.herms-inbox__trigger {\r\n position: relative;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 36px;\r\n height: 36px;\r\n border-radius: 999px;\r\n border: none;\r\n background: transparent;\r\n color: var(--herms-color-text);\r\n cursor: pointer;\r\n}\r\n\r\n.herms-inbox__trigger:hover {\r\n background: var(--herms-color-hover);\r\n}\r\n\r\n.herms-inbox__trigger:focus-visible {\r\n outline: 2px solid var(--herms-color-accent);\r\n outline-offset: 2px;\r\n}\r\n\r\n.herms-inbox__bell {\r\n width: 20px;\r\n height: 20px;\r\n}\r\n\r\n.herms-inbox__badge {\r\n position: absolute;\r\n top: 4px;\r\n right: 4px;\r\n min-width: 16px;\r\n height: 16px;\r\n padding: 0 4px;\r\n border-radius: 999px;\r\n background: var(--herms-color-unread-dot);\r\n color: #ffffff;\r\n font-size: 10px;\r\n font-weight: 600;\r\n line-height: 16px;\r\n text-align: center;\r\n}\r\n\r\n.herms-inbox__panel {\r\n width: 380px;\r\n max-width: calc(100vw - 24px);\r\n max-height: 480px;\r\n display: flex;\r\n flex-direction: column;\r\n border-radius: var(--herms-radius);\r\n border: 1px solid var(--herms-color-border);\r\n background: var(--herms-color-surface);\r\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.16);\r\n overflow: hidden;\r\n z-index: 50;\r\n}\r\n\r\n.herms-inbox__header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 8px;\r\n padding: 12px 14px;\r\n border-bottom: 1px solid var(--herms-color-border);\r\n}\r\n\r\n.herms-inbox__title {\r\n font-size: 14px;\r\n font-weight: 600;\r\n margin: 0;\r\n}\r\n\r\n.herms-inbox__header-actions {\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n}\r\n\r\n.herms-inbox__text-button {\r\n border: none;\r\n background: transparent;\r\n color: var(--herms-color-accent);\r\n font-size: 12px;\r\n font-weight: 500;\r\n cursor: pointer;\r\n padding: 4px 6px;\r\n border-radius: 6px;\r\n}\r\n\r\n.herms-inbox__text-button:hover {\r\n background: var(--herms-color-hover);\r\n}\r\n\r\n.herms-inbox__text-button:disabled {\r\n opacity: 0.5;\r\n cursor: not-allowed;\r\n}\r\n\r\n.herms-inbox__icon-button {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 26px;\r\n height: 26px;\r\n border: none;\r\n background: transparent;\r\n color: var(--herms-color-muted);\r\n border-radius: 6px;\r\n cursor: pointer;\r\n}\r\n\r\n.herms-inbox__icon-button:hover {\r\n background: var(--herms-color-hover);\r\n color: var(--herms-color-text);\r\n}\r\n\r\n.herms-inbox__icon-button:focus-visible,\r\n.herms-inbox__text-button:focus-visible,\r\n.herms-inbox__item:focus-visible {\r\n outline: 2px solid var(--herms-color-accent);\r\n outline-offset: -2px;\r\n}\r\n\r\n.herms-inbox__list {\r\n list-style: none;\r\n margin: 0;\r\n padding: 4px;\r\n overflow-y: auto;\r\n flex: 1 1 auto;\r\n}\r\n\r\n.herms-inbox__list-row {\r\n display: flex;\r\n align-items: stretch;\r\n gap: 2px;\r\n}\r\n\r\n.herms-inbox__item {\r\n flex: 1 1 auto;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n text-align: left;\r\n border: none;\r\n background: transparent;\r\n border-radius: 8px;\r\n padding: 8px 10px;\r\n cursor: pointer;\r\n color: inherit;\r\n font: inherit;\r\n}\r\n\r\n.herms-inbox__item:hover {\r\n background: var(--herms-color-hover);\r\n}\r\n\r\n.herms-inbox__item[data-unread='true'] .herms-inbox__item-title::before {\r\n content: '';\r\n display: inline-block;\r\n width: 6px;\r\n height: 6px;\r\n margin-right: 6px;\r\n border-radius: 999px;\r\n background: var(--herms-color-accent);\r\n}\r\n\r\n.herms-inbox__item-title {\r\n font-size: 13px;\r\n font-weight: 600;\r\n display: flex;\r\n align-items: center;\r\n}\r\n\r\n.herms-inbox__item-body {\r\n font-size: 12.5px;\r\n color: var(--herms-color-muted);\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n display: -webkit-box;\r\n -webkit-line-clamp: 2;\r\n -webkit-box-orient: vertical;\r\n}\r\n\r\n.herms-inbox__item-meta {\r\n font-size: 11px;\r\n color: var(--herms-color-muted);\r\n}\r\n\r\n.herms-inbox__archive-button {\r\n flex: 0 0 auto;\r\n align-self: center;\r\n width: 28px;\r\n height: 28px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border: none;\r\n background: transparent;\r\n color: var(--herms-color-muted);\r\n border-radius: 6px;\r\n cursor: pointer;\r\n}\r\n\r\n.herms-inbox__archive-button:hover {\r\n background: var(--herms-color-hover);\r\n color: var(--herms-color-danger);\r\n}\r\n\r\n.herms-inbox__state {\r\n padding: 32px 16px;\r\n text-align: center;\r\n color: var(--herms-color-muted);\r\n font-size: 13px;\r\n}\r\n\r\n.herms-inbox__state-title {\r\n color: var(--herms-color-text);\r\n font-weight: 600;\r\n margin: 0 0 6px;\r\n}\r\n\r\n.herms-inbox__load-more {\r\n display: block;\r\n width: calc(100% - 8px);\r\n margin: 4px;\r\n padding: 8px;\r\n border-radius: 8px;\r\n border: 1px dashed var(--herms-color-border);\r\n background: transparent;\r\n color: var(--herms-color-accent);\r\n font-size: 12.5px;\r\n font-weight: 500;\r\n cursor: pointer;\r\n}\r\n\r\n.herms-inbox__load-more:hover {\r\n background: var(--herms-color-hover);\r\n}\r\n\r\n.herms-inbox__load-more:disabled {\r\n opacity: 0.6;\r\n cursor: not-allowed;\r\n}\r\n\r\n.herms-inbox__visually-hidden {\r\n position: absolute;\r\n width: 1px;\r\n height: 1px;\r\n padding: 0;\r\n margin: -1px;\r\n overflow: hidden;\r\n clip: rect(0, 0, 0, 0);\r\n white-space: nowrap;\r\n border: 0;\r\n}\r\n\r\n.herms-inbox__skeleton-line {\r\n height: 12px;\r\n border-radius: 4px;\r\n background: var(--herms-color-hover);\r\n margin: 6px 10px;\r\n}\r\n"],"mappings":"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","names":[]}