@hermesihq/react 0.2.0 → 0.2.1

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.
package/CHANGELOG.md CHANGED
@@ -1,120 +1,138 @@
1
- # Changelog
2
-
3
- All notable changes to `@hermesihq/react`.
4
-
5
- This file describes what a consumer gets, not how it was built. The repository's
6
- history is where the reasoning lives.
7
-
8
- ## Versioning
9
-
10
- **`0.x` means the public API can still change.** A minor bump (`0.1` → `0.2`) may
11
- contain a breaking change; a patch bump will not. That is the ordinary `0.x` reading
12
- of semver and it is chosen deliberately rather than by default: this SDK's shape is
13
- still being learned from the first integrations, and promising stability before anyone
14
- has built against it would be a promise made to nobody and broken later.
15
-
16
- `1.0.0` is the commitment that a breaking change requires a major bump. It waits until
17
- at least one real integration exists.
18
-
19
- Each release lists breaking changes first, because that is the only section that
20
- decides whether an upgrade is a decision or a formality.
21
-
22
- ## 0.2.0 (2026-09-30)
23
-
24
- No breaking changes, and nothing to change in your code: everything this package exported
25
- before it exports now, from the same place.
26
-
27
- **The client and the state moved into a new package, `@hermesihq/js`, which this one now
28
- depends on.** The list, count and preference state that lived inside the hooks is now held
29
- in framework-free stores, and the hooks are a thin binding over them. That is what lets a
30
- Vue, Angular or plain JavaScript page use the same code. You do not need to install or
31
- import `@hermesihq/js` yourself; `HermsClient`, `HermsApiError`, `HERMS_CHANNELS` and the
32
- rest are still exported from here. If you also import `@hermesihq/js` directly, npm
33
- installs one shared copy, so both see the same `HermsClient` class.
34
-
35
- Four things a consumer can notice:
36
-
37
- - **A further page for the previous filter no longer lands in the new list.** With
38
- `useInbox`, switching tabs while "load more" was in flight could append the old tab's
39
- rows to the new tab's list. Any response that arrives after the list was reloaded, the
40
- filter changed or the hook unmounted is now discarded.
41
- - **A load's result and its loading flag now change together.** The rows, or the error,
42
- used to be published one update before `isLoading` cleared, so a component that
43
- renders on every update could briefly show "failed and still loading" or "here are
44
- your rows and still loading".
45
- - **The functions `useInbox` and `usePreferences` return keep their identity.**
46
- `loadMore`, `markRead`, `archive` and the rest used to be recreated whenever
47
- `isLoadingMore` or `hasMore` changed, so a `useCallback` or `memo` listing one re-ran
48
- on every page.
49
- - **Two providers over one client no longer close each other's connection.** The
50
- real-time stream is reference-counted: it opens for the first and closes with the last.
51
-
52
- A listener that throws is now reported to the host without stopping the other listeners
53
- or corrupting the state it was notified about.
54
-
55
- ## 0.1.0 (2026-09-28)
56
-
57
- The first published release. Nothing precedes it, so there is nothing to migrate from
58
- and no deprecations; the entries below describe the surface rather than changes to one.
59
-
60
- ### The inbox
61
-
62
- - `HermsClient`: the framework-agnostic core. Reads and writes the subscriber's inbox,
63
- opens a realtime stream for new items and unread counts, and falls back to polling
64
- when the runtime has no `EventSource` or the stream keeps failing. It imports nothing
65
- from React, so a Vue or vanilla wrapper is a thin layer rather than a second
66
- implementation.
67
- - `HermsProvider` / `useHermsContext`: one shared connection per provider, fanned out
68
- to every hook beneath it. Mounting three components does not open three streams.
69
- - `useInbox`: the item list, with filtering, cursor pagination, and mark-read,
70
- mark-all-read, archive and delete.
71
- - `useUnreadCount`: the badge, kept live off the same stream.
72
- - `HermsInbox`: a rendered bell and panel, keyboard-navigable, in English or French.
73
- Optional: the hooks above are the whole API if you would rather build your own.
74
-
75
- ### Preferences
76
-
77
- - `client.getPreferences()` and `client.updatePreference({ channel, enabled, categoryId })`,
78
- plus the `usePreferences` hook.
79
- - `PATCH` answers with the whole updated state, so a caller never refetches. The hook
80
- does **not** update optimistically: turning off a channel is a consent decision, and
81
- showing it as done before the server agreed tells somebody they have opted out when
82
- they may not have.
83
- - A setting of `null` means no preference was expressed and the default applies. It is
84
- not `false`. Categories with `isCritical: true` are always delivered. Render them so
85
- a subscriber can see what they receive, but not as a control.
86
- - There is deliberately no `unsubscribe()`. `POST /v1/client/unsubscribe` is the target
87
- of a `List-Unsubscribe` one-click link (RFC 8058) whose token is minted into an
88
- outgoing email's headers and invoked by a mail client, not by application code. The
89
- equivalent here is `updatePreference({ channel: 'email', enabled: false })`.
90
-
91
- ### Identities and errors
92
-
93
- - `client.registerChannel` / `client.deregisterChannel`, and `HERMS_CHANNELS`: the
94
- channels an identity can be registered for, exported as an array so a preference
95
- centre can iterate them, with `HermsChannel` derived from it.
96
- - `HermsApiError`: every failed request rejects with it, carrying the API's `type`,
97
- `code`, `message`, `request_id`, `detail` and `docUrl`. A response that is not JSON
98
- (a proxy's error page, an empty body where one was expected) arrives as this too,
99
- rather than as a `TypeError` from inside the SDK.
100
- - `decodeSubscriberTokenExp`: reads a subscriber token's expiry so a host can refresh
101
- ahead of it. It parses; it does not validate a signature and cannot, since this
102
- package never holds a key.
103
-
104
- ### Requirements
105
-
106
- React 18 or newer, as a peer dependency with no upper bound. That is deliberate and
107
- the opposite of how this repository pins its own dependencies: an application locks its
108
- versions so two builds of one commit are identical, whereas a library that bounds a
109
- peer range forces a warning on every consumer the day React ships a major, whether or
110
- not anything actually broke. You control React; this package should not have an opinion
111
- about which one you run.
112
-
113
- Ships ESM and CommonJS builds with type declarations for both, and
114
- `@hermesihq/react/styles.css` for `HermsInbox`.
115
-
116
- ### Known gaps
117
-
118
- - No `<HermsPreferences />` component. The headless surface for one is here; the
119
- rendered interface is not, because it wants a design pass rather than an invention.
120
- - No Vue or vanilla wrapper yet.
1
+ # Changelog
2
+
3
+ All notable changes to `@hermesihq/react`.
4
+
5
+ This file describes what a consumer gets, not how it was built. The repository's
6
+ history is where the reasoning lives.
7
+
8
+ ## Versioning
9
+
10
+ **`0.x` means the public API can still change.** A minor bump (`0.1` → `0.2`) may
11
+ contain a breaking change; a patch bump will not. That is the ordinary `0.x` reading
12
+ of semver and it is chosen deliberately rather than by default: this SDK's shape is
13
+ still being learned from the first integrations, and promising stability before anyone
14
+ has built against it would be a promise made to nobody and broken later.
15
+
16
+ `1.0.0` is the commitment that a breaking change requires a major bump. It waits until
17
+ at least one real integration exists.
18
+
19
+ Each release lists breaking changes first, because that is the only section that
20
+ decides whether an upgrade is a decision or a formality.
21
+
22
+ ## 0.2.1 (2026-10-01)
23
+
24
+ ### Fixed
25
+
26
+ - **The panel had no styling.** `<HermsInbox />` renders its panel in a portal under `<body>`, so
27
+ the panel is not inside the bell's root, and every colour variable was defined only on that
28
+ root. Measured in a browser on 0.2.0: the panel was transparent, had no border and square
29
+ corners, and its text stayed black in dark mode. The `theme` and `colorScheme` props put their
30
+ values on the root, so they could never reach the panel either. The panel now carries the
31
+ variables, the forced colour scheme and the `theme` values itself, and forcing light mode on a
32
+ dark system works. If you worked around this with your own CSS on `.herms-inbox__panel`, it may
33
+ now be redundant.
34
+ - `className` is now applied to the panel as well as to the bell, so one rule themes both.
35
+ - The panel is sized with `box-sizing: border-box`. Its 380px width now includes its border; it
36
+ used to render at 382px.
37
+ - The dialog has an accessible name, taken from its title. It used to be announced as just
38
+ "dialog".
39
+
40
+ ## 0.2.0 (2026-09-30)
41
+
42
+ No breaking changes, and nothing to change in your code: everything this package exported
43
+ before it exports now, from the same place.
44
+
45
+ **The client and the state moved into a new package, `@hermesihq/js`, which this one now
46
+ depends on.** The list, count and preference state that lived inside the hooks is now held
47
+ in framework-free stores, and the hooks are a thin binding over them. That is what lets a
48
+ Vue, Angular or plain JavaScript page use the same code. You do not need to install or
49
+ import `@hermesihq/js` yourself; `HermsClient`, `HermsApiError`, `HERMS_CHANNELS` and the
50
+ rest are still exported from here. If you also import `@hermesihq/js` directly, npm
51
+ installs one shared copy, so both see the same `HermsClient` class.
52
+
53
+ Four things a consumer can notice:
54
+
55
+ - **A further page for the previous filter no longer lands in the new list.** With
56
+ `useInbox`, switching tabs while "load more" was in flight could append the old tab's
57
+ rows to the new tab's list. Any response that arrives after the list was reloaded, the
58
+ filter changed or the hook unmounted is now discarded.
59
+ - **A load's result and its loading flag now change together.** The rows, or the error,
60
+ used to be published one update before `isLoading` cleared, so a component that
61
+ renders on every update could briefly show "failed and still loading" or "here are
62
+ your rows and still loading".
63
+ - **The functions `useInbox` and `usePreferences` return keep their identity.**
64
+ `loadMore`, `markRead`, `archive` and the rest used to be recreated whenever
65
+ `isLoadingMore` or `hasMore` changed, so a `useCallback` or `memo` listing one re-ran
66
+ on every page.
67
+ - **Two providers over one client no longer close each other's connection.** The
68
+ real-time stream is reference-counted: it opens for the first and closes with the last.
69
+
70
+ A listener that throws is now reported to the host without stopping the other listeners
71
+ or corrupting the state it was notified about.
72
+
73
+ ## 0.1.0 (2026-09-28)
74
+
75
+ The first published release. Nothing precedes it, so there is nothing to migrate from
76
+ and no deprecations; the entries below describe the surface rather than changes to one.
77
+
78
+ ### The inbox
79
+
80
+ - `HermsClient`: the framework-agnostic core. Reads and writes the subscriber's inbox,
81
+ opens a realtime stream for new items and unread counts, and falls back to polling
82
+ when the runtime has no `EventSource` or the stream keeps failing. It imports nothing
83
+ from React, so a Vue or vanilla wrapper is a thin layer rather than a second
84
+ implementation.
85
+ - `HermsProvider` / `useHermsContext`: one shared connection per provider, fanned out
86
+ to every hook beneath it. Mounting three components does not open three streams.
87
+ - `useInbox`: the item list, with filtering, cursor pagination, and mark-read,
88
+ mark-all-read, archive and delete.
89
+ - `useUnreadCount`: the badge, kept live off the same stream.
90
+ - `HermsInbox`: a rendered bell and panel, keyboard-navigable, in English or French.
91
+ Optional: the hooks above are the whole API if you would rather build your own.
92
+
93
+ ### Preferences
94
+
95
+ - `client.getPreferences()` and `client.updatePreference({ channel, enabled, categoryId })`,
96
+ plus the `usePreferences` hook.
97
+ - `PATCH` answers with the whole updated state, so a caller never refetches. The hook
98
+ does **not** update optimistically: turning off a channel is a consent decision, and
99
+ showing it as done before the server agreed tells somebody they have opted out when
100
+ they may not have.
101
+ - A setting of `null` means no preference was expressed and the default applies. It is
102
+ not `false`. Categories with `isCritical: true` are always delivered. Render them so
103
+ a subscriber can see what they receive, but not as a control.
104
+ - There is deliberately no `unsubscribe()`. `POST /v1/client/unsubscribe` is the target
105
+ of a `List-Unsubscribe` one-click link (RFC 8058) whose token is minted into an
106
+ outgoing email's headers and invoked by a mail client, not by application code. The
107
+ equivalent here is `updatePreference({ channel: 'email', enabled: false })`.
108
+
109
+ ### Identities and errors
110
+
111
+ - `client.registerChannel` / `client.deregisterChannel`, and `HERMS_CHANNELS`: the
112
+ channels an identity can be registered for, exported as an array so a preference
113
+ centre can iterate them, with `HermsChannel` derived from it.
114
+ - `HermsApiError`: every failed request rejects with it, carrying the API's `type`,
115
+ `code`, `message`, `request_id`, `detail` and `docUrl`. A response that is not JSON
116
+ (a proxy's error page, an empty body where one was expected) arrives as this too,
117
+ rather than as a `TypeError` from inside the SDK.
118
+ - `decodeSubscriberTokenExp`: reads a subscriber token's expiry so a host can refresh
119
+ ahead of it. It parses; it does not validate a signature and cannot, since this
120
+ package never holds a key.
121
+
122
+ ### Requirements
123
+
124
+ React 18 or newer, as a peer dependency with no upper bound. That is deliberate and
125
+ the opposite of how this repository pins its own dependencies: an application locks its
126
+ versions so two builds of one commit are identical, whereas a library that bounds a
127
+ peer range forces a warning on every consumer the day React ships a major, whether or
128
+ not anything actually broke. You control React; this package should not have an opinion
129
+ about which one you run.
130
+
131
+ Ships ESM and CommonJS builds with type declarations for both, and
132
+ `@hermesihq/react/styles.css` for `HermsInbox`.
133
+
134
+ ### Known gaps
135
+
136
+ - No `<HermsPreferences />` component. The headless surface for one is here; the
137
+ rendered interface is not, because it wants a design pass rather than an invention.
138
+ - No Vue or vanilla wrapper yet.
package/README.md CHANGED
@@ -1,120 +1,122 @@
1
- # @hermesihq/react
2
-
3
- React bindings for Hermesi's in-app inbox: a ready-made bell and panel, and the hooks it is
4
- built on if you would rather build your own.
5
-
6
- ```sh
7
- npm install @hermesihq/react
8
- ```
9
-
10
- Requires React 18 or newer. It depends on [`@hermesihq/js`](https://github.com/hermesihq/sdk/tree/main/packages/js),
11
- which npm installs for you. You do not import it yourself unless you want the stores directly.
12
-
13
- ## Before you start
14
-
15
- Three things: a **public key** (`hm_pk_...`, safe to ship in a bundle), your **API base URL
16
- ending in `/v1/client`**, and a **subscriber token minted by your own backend**. The token
17
- proves which subscriber this page acts for. Your backend signs it with your secret key; this
18
- package never sees that key and cannot mint a token. You give the client a function that
19
- asks your backend for a fresh one, and it calls that function before every request.
20
-
21
- The base URL is the client API, not the host root: every request is this string plus a path
22
- such as `/inbox`. A base URL without `/v1/client` makes every request a 404.
23
-
24
- ## Quick start
25
-
26
- ```tsx
27
- import { HermsClient, HermsInbox, HermsProvider } from '@hermesihq/react'
28
- import '@hermesihq/react/styles.css'
29
-
30
- const client = new HermsClient({
31
- publicKey: 'hm_pk_prod_...',
32
- apiBaseUrl: 'https://your-hermesi-host/v1/client',
33
- getSubscriberToken: () => fetch('/api/hermesi-token').then((response) => response.text()), // calls *your* backend
34
- })
35
-
36
- export function AppHeader() {
37
- return (
38
- <HermsProvider client={client}>
39
- <HermsInbox />
40
- </HermsProvider>
41
- )
42
- }
43
- ```
44
-
45
- Two lines are easy to miss. The stylesheet import is required: **the bundle does not load its
46
- own CSS**, so without it the panel renders with no styling. And `HermsProvider` must sit above
47
- anything that uses a hook, or the hook throws an error that names the missing provider.
48
-
49
- Create the client **once**, at module scope or in a `useMemo`, not on every render. A new
50
- client each render closes and reopens the real-time connection each render.
51
-
52
- ## `<HermsInbox />`
53
-
54
- A real `<button>` with an accessible name that includes the unread count, and a panel with
55
- arrow-key navigation, `Home` and `End`, and `Escape` to close.
56
-
57
- | Prop | Default | |
58
- |---|---|---|
59
- | `placement` | `'bottom-end'` | `'bottom-start'`, `'bottom-end'`, `'top-start'` or `'top-end'` |
60
- | `onItemClick` | | Called with the item when one is activated. Use it to drive your own router. Without it, an item with an `actionUrl` navigates there. Either way the item is marked read first. |
61
- | `theme` | | `{ accent, radius }`, the two most re-themed values |
62
- | `colorScheme` | `'auto'` | `'auto'` follows the visitor's OS setting; `'light'` or `'dark'` force one |
63
- | `locale` | `'en'` | `'en'` or `'fr'` |
64
- | `className` | | Added to the root element |
65
-
66
- Everything else is reachable by overriding these CSS custom properties: `--herms-color-accent`,
67
- `--herms-color-accent-foreground`, `--herms-color-bg`, `--herms-color-border`,
68
- `--herms-color-danger`, `--herms-color-hover`, `--herms-color-muted`, `--herms-color-surface`,
69
- `--herms-color-text`, `--herms-color-unread-dot`, `--herms-font-family` and `--herms-radius`.
70
- The stylesheet is scoped under `.herms-inbox` and uses no Tailwind, so your build cannot bleed
71
- into the widget or the reverse.
72
-
73
- ## Hooks
74
-
75
- Use these when the bell and panel are not what you want. Same data, no chrome.
76
-
77
- | Hook | Returns |
78
- |---|---|
79
- | `useUnreadCount()` | `{ unread, unseen, isLoading }`, kept live |
80
- | `useInbox({ status?, category? })` | `{ items, isLoading, isLoadingMore, error, hasMore, loadMore, markRead, markAllRead, archive, remove, refetch }` |
81
- | `usePreferences()` | `{ preferences, isLoading, error, setPreference, reload }` |
82
-
83
- They are a thin binding over the stores in `@hermesihq/js`, which is where the behaviour is
84
- documented. The parts worth knowing here:
85
-
86
- - The functions a hook returns keep the same identity for as long as it is mounted, so they
87
- are safe in a `useCallback` or `memo` dependency list.
88
- - Changing `status` or `category` reloads the list and keeps the previous rows on screen,
89
- flagged as loading, until the new ones arrive. A response for the old filter that lands
90
- late is discarded.
91
- - Mutations go to the server first and patch local state with what it answered. A failed one
92
- rejects.
93
- - `usePreferences` does not update optimistically. Turning off a channel is a consent
94
- decision, and showing it as done before the server agreed tells somebody they have opted
95
- out when they may not have.
96
- - A setting of `null` means no preference was expressed and the default applies. It is not
97
- `false`, and treating them as the same would opt somebody out of something they never
98
- declined. Categories with `isCritical: true` are always delivered: show them, but not as a
99
- control.
100
-
101
- ## Server rendering
102
-
103
- Importing this package touches no DOM, and a component using the hooks renders its loading
104
- state on the server. Nothing connects and no request is made until it mounts in the browser.
105
-
106
- ## Also exported
107
-
108
- `HermsClient`, `HermsApiError`, `HERMS_CHANNELS` and `decodeSubscriberTokenExp`, with their
109
- types, are re-exported from `@hermesihq/js`, so an existing import from this package keeps
110
- working. Every failed request rejects with a `HermsApiError`; quote its `requestId` in a
111
- support conversation.
112
-
113
- ## Not included
114
-
115
- A rendered preference centre. The headless `usePreferences()` is here; the interface is not,
116
- because it wants a design pass rather than an invention.
117
-
118
- ## License
119
-
120
- MIT
1
+ # @hermesihq/react
2
+
3
+ React bindings for Hermesi's in-app inbox: a ready-made bell and panel, and the hooks it is
4
+ built on if you would rather build your own.
5
+
6
+ ```sh
7
+ npm install @hermesihq/react
8
+ ```
9
+
10
+ Requires React 18 or newer. It depends on [`@hermesihq/js`](https://github.com/hermesihq/sdk/tree/main/packages/js),
11
+ which npm installs for you. You do not import it yourself unless you want the stores directly.
12
+
13
+ ## Before you start
14
+
15
+ Three things: a **public key** (`hm_pk_...`, safe to ship in a bundle), your **API base URL
16
+ ending in `/v1/client`**, and a **subscriber token minted by your own backend**. The token
17
+ proves which subscriber this page acts for. Your backend signs it with your secret key; this
18
+ package never sees that key and cannot mint a token. You give the client a function that
19
+ asks your backend for a fresh one, and it calls that function before every request.
20
+
21
+ The base URL is the client API, not the host root: every request is this string plus a path
22
+ such as `/inbox`. A base URL without `/v1/client` makes every request a 404.
23
+
24
+ ## Quick start
25
+
26
+ ```tsx
27
+ import { HermsClient, HermsInbox, HermsProvider } from '@hermesihq/react'
28
+ import '@hermesihq/react/styles.css'
29
+
30
+ const client = new HermsClient({
31
+ publicKey: 'hm_pk_prod_...',
32
+ apiBaseUrl: 'https://your-hermesi-host/v1/client',
33
+ getSubscriberToken: () => fetch('/api/hermesi-token').then((response) => response.text()), // calls *your* backend
34
+ })
35
+
36
+ export function AppHeader() {
37
+ return (
38
+ <HermsProvider client={client}>
39
+ <HermsInbox />
40
+ </HermsProvider>
41
+ )
42
+ }
43
+ ```
44
+
45
+ Two lines are easy to miss. The stylesheet import is required: **the bundle does not load its
46
+ own CSS**, so without it the panel renders with no styling. And `HermsProvider` must sit above
47
+ anything that uses a hook, or the hook throws an error that names the missing provider.
48
+
49
+ Create the client **once**, at module scope or in a `useMemo`, not on every render. A new
50
+ client each render closes and reopens the real-time connection each render.
51
+
52
+ ## `<HermsInbox />`
53
+
54
+ A real `<button>` with an accessible name that includes the unread count, and a panel with
55
+ arrow-key navigation, `Home` and `End`, and `Escape` to close.
56
+
57
+ | Prop | Default | |
58
+ |---|---|---|
59
+ | `placement` | `'bottom-end'` | `'bottom-start'`, `'bottom-end'`, `'top-start'` or `'top-end'` |
60
+ | `onItemClick` | | Called with the item when one is activated. Use it to drive your own router. Without it, an item with an `actionUrl` navigates there. Either way the item is marked read first. |
61
+ | `theme` | | `{ accent, radius }`, the two most re-themed values |
62
+ | `colorScheme` | `'auto'` | `'auto'` follows the visitor's OS setting; `'light'` or `'dark'` force one |
63
+ | `locale` | `'en'` | `'en'` or `'fr'` |
64
+ | `className` | | Added to the root element **and to the panel**, so one rule themes both |
65
+
66
+ Everything else is reachable by overriding these CSS custom properties: `--herms-color-accent`,
67
+ `--herms-color-accent-foreground`, `--herms-color-bg`, `--herms-color-border`,
68
+ `--herms-color-danger`, `--herms-color-hover`, `--herms-color-muted`, `--herms-color-surface`,
69
+ `--herms-color-text`, `--herms-color-unread-dot`, `--herms-font-family` and `--herms-radius`.
70
+ Set them on a rule that matches the `className` you pass: it is applied to the bell and to the
71
+ panel. The panel is rendered under `<body>`, not inside the bell, so a rule on an ancestor of the
72
+ bell does not reach it. The stylesheet is scoped under `.herms-inbox` and uses no Tailwind, so
73
+ your build cannot bleed into the widget or the reverse.
74
+
75
+ ## Hooks
76
+
77
+ Use these when the bell and panel are not what you want. Same data, no chrome.
78
+
79
+ | Hook | Returns |
80
+ |---|---|
81
+ | `useUnreadCount()` | `{ unread, unseen, isLoading }`, kept live |
82
+ | `useInbox({ status?, category? })` | `{ items, isLoading, isLoadingMore, error, hasMore, loadMore, markRead, markAllRead, archive, remove, refetch }` |
83
+ | `usePreferences()` | `{ preferences, isLoading, error, setPreference, reload }` |
84
+
85
+ They are a thin binding over the stores in `@hermesihq/js`, which is where the behaviour is
86
+ documented. The parts worth knowing here:
87
+
88
+ - The functions a hook returns keep the same identity for as long as it is mounted, so they
89
+ are safe in a `useCallback` or `memo` dependency list.
90
+ - Changing `status` or `category` reloads the list and keeps the previous rows on screen,
91
+ flagged as loading, until the new ones arrive. A response for the old filter that lands
92
+ late is discarded.
93
+ - Mutations go to the server first and patch local state with what it answered. A failed one
94
+ rejects.
95
+ - `usePreferences` does not update optimistically. Turning off a channel is a consent
96
+ decision, and showing it as done before the server agreed tells somebody they have opted
97
+ out when they may not have.
98
+ - A setting of `null` means no preference was expressed and the default applies. It is not
99
+ `false`, and treating them as the same would opt somebody out of something they never
100
+ declined. Categories with `isCritical: true` are always delivered: show them, but not as a
101
+ control.
102
+
103
+ ## Server rendering
104
+
105
+ Importing this package touches no DOM, and a component using the hooks renders its loading
106
+ state on the server. Nothing connects and no request is made until it mounts in the browser.
107
+
108
+ ## Also exported
109
+
110
+ `HermsClient`, `HermsApiError`, `HERMS_CHANNELS` and `decodeSubscriberTokenExp`, with their
111
+ types, are re-exported from `@hermesihq/js`, so an existing import from this package keeps
112
+ working. Every failed request rejects with a `HermsApiError`; quote its `requestId` in a
113
+ support conversation.
114
+
115
+ ## Not included
116
+
117
+ A rendered preference centre. The headless `usePreferences()` is here; the interface is not,
118
+ because it wants a design pass rather than an invention.
119
+
120
+ ## License
121
+
122
+ MIT
package/dist/index.cjs CHANGED
@@ -210,6 +210,7 @@ function HermsInbox({ placement = "bottom-end", onItemClick, theme, colorScheme
210
210
  const itemRefs = (0, import_react5.useRef)([]);
211
211
  const seenIdsRef = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
212
212
  const strings = getHermsInboxStrings(locale);
213
+ const titleId = (0, import_react5.useId)();
213
214
  (0, import_react5.useEffect)(() => {
214
215
  if (!open || items.length === 0) return;
215
216
  const toMark = items.filter((item) => !seenIdsRef.current.has(item.id)).map((item) => item.id);
@@ -269,7 +270,9 @@ function HermsInbox({ placement = "bottom-end", onItemClick, theme, colorScheme
269
270
  }, [theme?.accent, theme?.radius]);
270
271
  const { side, align } = placementToSideAlign[placement];
271
272
  const rootClassName = ["herms-inbox", className].filter(Boolean).join(" ");
272
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: rootClassName, style: rootStyle, "data-herms-color-scheme": colorScheme === "auto" ? void 0 : colorScheme, children: [
273
+ const panelClassName = ["herms-inbox__panel", className].filter(Boolean).join(" ");
274
+ const colorSchemeAttribute = colorScheme === "auto" ? void 0 : colorScheme;
275
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: rootClassName, style: rootStyle, "data-herms-color-scheme": colorSchemeAttribute, children: [
273
276
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "herms-inbox__visually-hidden", role: "status", "aria-live": "polite", children: unread > 0 ? strings.bellLabelWithUnread(unread) : "" }),
274
277
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Popover.Root, { open, onOpenChange: setOpen, children: [
275
278
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
@@ -288,7 +291,10 @@ function HermsInbox({ placement = "bottom-end", onItemClick, theme, colorScheme
288
291
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Popover.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
289
292
  Popover.Content,
290
293
  {
291
- className: "herms-inbox__panel",
294
+ className: panelClassName,
295
+ style: rootStyle,
296
+ "data-herms-color-scheme": colorSchemeAttribute,
297
+ "aria-labelledby": titleId,
292
298
  side,
293
299
  align,
294
300
  sideOffset: 8,
@@ -300,7 +306,7 @@ function HermsInbox({ placement = "bottom-end", onItemClick, theme, colorScheme
300
306
  },
301
307
  children: [
302
308
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "herms-inbox__header", children: [
303
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "herms-inbox__title", children: strings.panelTitle }),
309
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "herms-inbox__title", id: titleId, children: strings.panelTitle }),
304
310
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "herms-inbox__header-actions", children: [
305
311
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "herms-inbox__text-button", onClick: () => void markAllRead(), disabled: unread === 0, children: strings.markAllRead }),
306
312
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Popover.Close, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "herms-inbox__icon-button", "aria-label": strings.close, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CloseIcon, {}) }) })
@@ -1 +1 @@
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"]}
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, useId, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent } from 'react'\nimport * as Popover from '@radix-ui/react-popover'\nimport type { HermsInboxItem } from '@hermesihq/js'\nimport { useHermsContext } from './HermsProvider'\nimport { useInbox } from './useInbox'\nimport { useUnreadCount } from './useUnreadCount'\nimport { getHermsInboxStrings, type HermsLocale } from './locale'\nimport './HermsInbox.css'\n\nexport type HermsInboxPlacement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'\n\nexport interface HermsInboxTheme {\n accent?: string\n radius?: string\n}\n\nexport interface HermsInboxProps {\n /** Popover position relative to the bell trigger. Default `bottom-end`. */\n placement?: HermsInboxPlacement\n /** Called when an item is activated (click, or Enter on a focused row).\n * Lets a host app drive its own router (for example\n * `onItemClick={(item) => navigate(item.actionUrl)}`) instead of a hard\n * page navigation. When omitted, activating an item with a non-null\n * `actionUrl` does a plain `window.location.assign` to it. Either way, the\n * item is marked read first. */\n onItemClick?: (item: HermsInboxItem) => void\n /** A couple of the most commonly re-themed CSS custom properties, settable\n * without a host app writing any CSS of its own. Anything else is\n * reachable by overriding `--herms-color-*`/`--herms-radius` variables\n * directly (see `HermsInbox.css`). */\n theme?: HermsInboxTheme\n /** `'auto'` (default) follows the visitor's OS `prefers-color-scheme`;\n * `'light'`/`'dark'` force one regardless of it. */\n colorScheme?: 'auto' | 'light' | 'dark'\n locale?: HermsLocale\n className?: string\n}\n\nconst placementToSideAlign: Record<HermsInboxPlacement, { side: 'top' | 'bottom'; align: 'start' | 'end' }> = {\n 'bottom-start': { side: 'bottom', align: 'start' },\n 'bottom-end': { side: 'bottom', align: 'end' },\n 'top-start': { side: 'top', align: 'start' },\n 'top-end': { side: 'top', align: 'end' },\n}\n\nfunction BellIcon() {\n return (\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\">\n <path d=\"M6 8a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6\" />\n <path d=\"M10 21a2 2 0 0 0 4 0\" />\n </svg>\n )\n}\n\nfunction CloseIcon() {\n return (\n <svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden=\"true\">\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n )\n}\n\nfunction ArchiveIcon() {\n return (\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\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"4\" rx=\"1\" />\n <path d=\"M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8\" />\n <path d=\"M10 13h4\" />\n </svg>\n )\n}\n\nfunction AlertIcon() {\n return (\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\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M12 8v5M12 16h.01\" />\n </svg>\n )\n}\n\nfunction relativeTime(iso: string, locale: HermsLocale): string {\n const then = new Date(iso).getTime()\n if (Number.isNaN(then)) return ''\n const diffSeconds = Math.max(0, Math.round((Date.now() - then) / 1000))\n const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })\n if (diffSeconds < 60) return formatter.format(0, 'minute')\n const diffMinutes = Math.round(diffSeconds / 60)\n if (diffMinutes < 60) return formatter.format(-diffMinutes, 'minute')\n const diffHours = Math.round(diffMinutes / 60)\n if (diffHours < 24) return formatter.format(-diffHours, 'hour')\n const diffDays = Math.round(diffHours / 24)\n return formatter.format(-diffDays, 'day')\n}\n\n/**\n * `<HermsInbox />`: bell trigger + dropdown panel. Built\n * entirely on the headless `useInbox`/`useUnreadCount` hooks; every state\n * (loading, empty, error, populated) renders here, none of it invented by a\n * host app. Accessible: a real `<button>` trigger with `aria-label`/\n * `aria-expanded`, the panel as `role=\"menu\"` with roving-tabindex arrow-key\n * navigation, `Home`/`End`, `Escape`-to-close (native to Radix `Popover`,\n * which also returns focus to the trigger), and `Enter`/`Space` (native\n * `<button>` behavior) to activate the focused item.\n */\nexport function HermsInbox({ placement = 'bottom-end', onItemClick, theme, colorScheme = 'auto', locale, className }: HermsInboxProps) {\n const { client } = useHermsContext()\n const { unread, unseen } = useUnreadCount()\n const { items, isLoading, isLoadingMore, error, hasMore, loadMore, markRead, markAllRead, archive, refetch } = useInbox()\n const [open, setOpen] = useState(false)\n const [activeIndex, setActiveIndex] = useState(0)\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([])\n const seenIdsRef = useRef<Set<string>>(new Set())\n const strings = getHermsInboxStrings(locale)\n const titleId = useId()\n\n // \"Seen\" (the bell was opened) is distinct from \"read.\" Marks\n // every currently loaded, not-yet-seen id seen once the panel is open and\n // has items, including any additional ids that arrive later via\n // `loadMore`/live updates while it stays open.\n useEffect(() => {\n if (!open || items.length === 0) return\n const toMark = items.filter((item) => !seenIdsRef.current.has(item.id)).map((item) => item.id)\n if (toMark.length === 0) return\n for (const id of toMark) seenIdsRef.current.add(id)\n void client.markSeen(toMark)\n }, [open, items, client])\n\n useEffect(() => {\n if (open) setActiveIndex(0)\n }, [open])\n\n const focusItem = useCallback((index: number) => {\n setActiveIndex(index)\n itemRefs.current[index]?.focus()\n }, [])\n\n const handleListKeyDown = useCallback(\n (event: KeyboardEvent<HTMLUListElement>) => {\n if (items.length === 0) return\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n focusItem(Math.min(activeIndex + 1, items.length - 1))\n break\n case 'ArrowUp':\n event.preventDefault()\n focusItem(Math.max(activeIndex - 1, 0))\n break\n case 'Home':\n event.preventDefault()\n focusItem(0)\n break\n case 'End':\n event.preventDefault()\n focusItem(items.length - 1)\n break\n default:\n break\n }\n },\n [items.length, activeIndex, focusItem],\n )\n\n const handleActivate = useCallback(\n (item: HermsInboxItem) => {\n if (!item.readAt) void markRead(item.id)\n if (onItemClick) {\n onItemClick(item)\n } else if (item.actionUrl && typeof window !== 'undefined') {\n window.location.assign(item.actionUrl)\n }\n },\n [markRead, onItemClick],\n )\n\n const rootStyle = useMemo<CSSProperties>(() => {\n const style: CSSProperties & Record<string, string> = {}\n if (theme?.accent) style['--herms-color-accent'] = theme.accent\n if (theme?.radius) style['--herms-radius'] = theme.radius\n return style\n }, [theme?.accent, theme?.radius])\n\n const { side, align } = placementToSideAlign[placement]\n const rootClassName = ['herms-inbox', className].filter(Boolean).join(' ')\n // The panel is rendered by Radix in a portal under <body>, so it is not inside the root\n // above and inherits nothing from it. Whatever the root carries so that the stylesheet can\n // theme it, the panel has to carry as well: the theme's inline variables, the forced colour\n // scheme, and the host's class (so that one rule on `className` themes both).\n const panelClassName = ['herms-inbox__panel', className].filter(Boolean).join(' ')\n const colorSchemeAttribute = colorScheme === 'auto' ? undefined : colorScheme\n\n return (\n <div className={rootClassName} style={rootStyle} data-herms-color-scheme={colorSchemeAttribute}>\n <span className=\"herms-inbox__visually-hidden\" role=\"status\" aria-live=\"polite\">\n {unread > 0 ? strings.bellLabelWithUnread(unread) : ''}\n </span>\n <Popover.Root open={open} onOpenChange={setOpen}>\n <Popover.Trigger asChild>\n <button\n type=\"button\"\n className=\"herms-inbox__trigger\"\n aria-label={unread > 0 ? strings.bellLabelWithUnread(unread) : strings.bellLabel}\n aria-expanded={open}\n >\n <BellIcon />\n {unseen > 0 && (\n <span className=\"herms-inbox__badge\" aria-hidden=\"true\">\n {unseen > 99 ? '99+' : unseen}\n </span>\n )}\n </button>\n </Popover.Trigger>\n <Popover.Portal>\n <Popover.Content\n className={panelClassName}\n style={rootStyle}\n data-herms-color-scheme={colorSchemeAttribute}\n // The dialog needs a name of its own. Only the list, the bell and the loading\n // state were named; a dialog without one is announced as just \"dialog\".\n aria-labelledby={titleId}\n side={side}\n align={align}\n sideOffset={8}\n onOpenAutoFocus={(event) => {\n // Radix's default is to focus the content wrapper itself; we\n // want the first item (or the header when there are none)\n // focused instead, so keyboard users land somewhere useful.\n if (items.length > 0) {\n event.preventDefault()\n itemRefs.current[0]?.focus()\n }\n }}\n >\n <div className=\"herms-inbox__header\">\n <p className=\"herms-inbox__title\" id={titleId}>{strings.panelTitle}</p>\n <div className=\"herms-inbox__header-actions\">\n <button type=\"button\" className=\"herms-inbox__text-button\" onClick={() => void markAllRead()} disabled={unread === 0}>\n {strings.markAllRead}\n </button>\n <Popover.Close asChild>\n <button type=\"button\" className=\"herms-inbox__icon-button\" aria-label={strings.close}>\n <CloseIcon />\n </button>\n </Popover.Close>\n </div>\n </div>\n\n {isLoading ? (\n <div role=\"status\" aria-label={strings.loading}>\n {[0, 1, 2, 3].map((i) => (\n <div key={i} className=\"herms-inbox__skeleton-line\" style={{ width: i % 2 === 0 ? '85%' : '65%' }} />\n ))}\n </div>\n ) : error ? (\n <div className=\"herms-inbox__state\" role=\"alert\">\n <AlertIcon />\n <p className=\"herms-inbox__state-title\">{strings.errorTitle}</p>\n <button type=\"button\" className=\"herms-inbox__text-button\" onClick={() => refetch()}>\n {strings.retry}\n </button>\n </div>\n ) : items.length === 0 ? (\n <div className=\"herms-inbox__state\">{strings.empty}</div>\n ) : (\n <>\n <ul className=\"herms-inbox__list\" role=\"menu\" aria-label={strings.panelTitle} onKeyDown={handleListKeyDown}>\n {items.map((item, index) => (\n <li key={item.id} className=\"herms-inbox__list-row\" role=\"none\">\n <button\n type=\"button\"\n role=\"menuitem\"\n ref={(el) => {\n itemRefs.current[index] = el\n }}\n tabIndex={index === activeIndex ? 0 : -1}\n className=\"herms-inbox__item\"\n data-unread={!item.readAt}\n onFocus={() => setActiveIndex(index)}\n onClick={() => handleActivate(item)}\n >\n <span className=\"herms-inbox__item-title\">{item.title}</span>\n {item.body && <span className=\"herms-inbox__item-body\">{item.body}</span>}\n <span className=\"herms-inbox__item-meta\">{relativeTime(item.createdAt, locale ?? 'en')}</span>\n </button>\n <button\n type=\"button\"\n className=\"herms-inbox__archive-button\"\n aria-label={`${strings.archive}: ${item.title}`}\n onClick={(event) => {\n event.stopPropagation()\n void archive(item.id)\n }}\n >\n <ArchiveIcon />\n </button>\n </li>\n ))}\n </ul>\n {hasMore && (\n <button type=\"button\" className=\"herms-inbox__load-more\" onClick={loadMore} disabled={isLoadingMore}>\n {isLoadingMore ? strings.loadingMore : strings.loadMore}\n </button>\n )}\n </>\n )}\n </Popover.Content>\n </Popover.Portal>\n </Popover.Root>\n </div>\n )\n}\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,5 +1,6 @@
1
1
  /* src/HermsInbox.css */
2
- .herms-inbox {
2
+ .herms-inbox,
3
+ .herms-inbox__panel {
3
4
  --herms-color-accent: #5b5bd6;
4
5
  --herms-color-accent-foreground: #ffffff;
5
6
  --herms-color-bg: #ffffff;
@@ -18,14 +19,17 @@
18
19
  Helvetica,
19
20
  Arial,
20
21
  sans-serif;
21
- position: relative;
22
- display: inline-block;
23
22
  font-family: var(--herms-font-family);
24
23
  color: var(--herms-color-text);
25
24
  line-height: 1.4;
26
25
  }
26
+ .herms-inbox {
27
+ position: relative;
28
+ display: inline-block;
29
+ }
27
30
  @media (prefers-color-scheme: dark) {
28
- .herms-inbox:not([data-herms-color-scheme=light]) {
31
+ .herms-inbox:not([data-herms-color-scheme=light]),
32
+ .herms-inbox__panel:not([data-herms-color-scheme=light]) {
29
33
  --herms-color-bg: #201f1a;
30
34
  --herms-color-surface: #201f1a;
31
35
  --herms-color-border: #38362f;
@@ -34,7 +38,8 @@
34
38
  --herms-color-hover: #2b2a24;
35
39
  }
36
40
  }
37
- .herms-inbox[data-herms-color-scheme=dark] {
41
+ .herms-inbox[data-herms-color-scheme=dark],
42
+ .herms-inbox__panel[data-herms-color-scheme=dark] {
38
43
  --herms-color-bg: #201f1a;
39
44
  --herms-color-surface: #201f1a;
40
45
  --herms-color-border: #38362f;
@@ -42,7 +47,9 @@
42
47
  --herms-color-muted: #a6a19a;
43
48
  --herms-color-hover: #2b2a24;
44
49
  }
45
- .herms-inbox * {
50
+ .herms-inbox *,
51
+ .herms-inbox__panel,
52
+ .herms-inbox__panel * {
46
53
  box-sizing: border-box;
47
54
  }
48
55
  .herms-inbox__trigger {
@@ -1 +1 @@
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":";AAoBA,CAAC;AACC,wBAAsB;AACtB,mCAAiC;AACjC,oBAAkB;AAClB,yBAAuB;AACvB,wBAAsB;AACtB,sBAAoB;AACpB,uBAAqB;AACrB,wBAAsB;AACtB,4BAA0B;AAC1B,uBAAqB;AACrB,kBAAgB;AAChB;AAAA,IACE,aAAa;AAAA,IAAE,UAAU;AAAA,IAAE,MAAM;AAAA,IAAE,SAAS;AAAA,IAAE,KAAK;AAAA,IAAE;AAEvD,YAAU;AACV,WAAS;AACT,eAAa,IAAI;AACjB,SAAO,IAAI;AACX,eAAa;AACf;AAEA,QAAO,sBAAuB;AAC5B,GAvBD,WAuBa,KAAK,CAAC;AAChB,sBAAkB;AAClB,2BAAuB;AACvB,0BAAsB;AACtB,wBAAoB;AACpB,yBAAqB;AACrB,yBAAqB;AACvB;AACF;AAEA,CAjCC,WAiCW,CAAC;AACX,oBAAkB;AAClB,yBAAuB;AACvB,wBAAsB;AACtB,sBAAoB;AACpB,uBAAqB;AACrB,uBAAqB;AACvB;AAEA,CA1CC,YA0CY;AACX,cAAY;AACd;AAEA,CAAC;AACC,YAAU;AACV,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,SAAO;AACP,UAAQ;AACR,iBAAe;AACf,UAAQ;AACR,cAAY;AACZ,SAAO,IAAI;AACX,UAAQ;AACV;AAEA,CAdC,oBAcoB;AACnB,cAAY,IAAI;AAClB;AAEA,CAlBC,oBAkBoB;AACnB,WAAS,IAAI,MAAM,IAAI;AACvB,kBAAgB;AAClB;AAEA,CAAC;AACC,SAAO;AACP,UAAQ;AACV;AAEA,CAAC;AACC,YAAU;AACV,OAAK;AACL,SAAO;AACP,aAAW;AACX,UAAQ;AACR,WAAS,EAAE;AACX,iBAAe;AACf,cAAY,IAAI;AAChB,SAAO;AACP,aAAW;AACX,eAAa;AACb,eAAa;AACb,cAAY;AACd;AAEA,CAAC;AACC,SAAO;AACP,aAAW,KAAK,MAAM,EAAE;AACxB,cAAY;AACZ,WAAS;AACT,kBAAgB;AAChB,iBAAe,IAAI;AACnB,UAAQ,IAAI,MAAM,IAAI;AACtB,cAAY,IAAI;AAChB,cAAY,EAAE,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AACrC,YAAU;AACV,WAAS;AACX;AAEA,CAAC;AACC,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,OAAK;AACL,WAAS,KAAK;AACd,iBAAe,IAAI,MAAM,IAAI;AAC/B;AAEA,CAAC;AACC,aAAW;AACX,eAAa;AACb,UAAQ;AACV;AAEA,CAAC;AACC,WAAS;AACT,eAAa;AACb,OAAK;AACP;AAEA,CAAC;AACC,UAAQ;AACR,cAAY;AACZ,SAAO,IAAI;AACX,aAAW;AACX,eAAa;AACb,UAAQ;AACR,WAAS,IAAI;AACb,iBAAe;AACjB;AAEA,CAXC,wBAWwB;AACvB,cAAY,IAAI;AAClB;AAEA,CAfC,wBAewB;AACvB,WAAS;AACT,UAAQ;AACV;AAEA,CAAC;AACC,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,SAAO;AACP,UAAQ;AACR,UAAQ;AACR,cAAY;AACZ,SAAO,IAAI;AACX,iBAAe;AACf,UAAQ;AACV;AAEA,CAbC,wBAawB;AACvB,cAAY,IAAI;AAChB,SAAO,IAAI;AACb;AAEA,CAlBC,wBAkBwB;AACzB,CAvCC,wBAuCwB;AACzB,CAAC,iBAAiB;AAChB,WAAS,IAAI,MAAM,IAAI;AACvB,kBAAgB;AAClB;AAEA,CAAC;AACC,cAAY;AACZ,UAAQ;AACR,WAAS;AACT,cAAY;AACZ,QAAM,EAAE,EAAE;AACZ;AAEA,CAAC;AACC,WAAS;AACT,eAAa;AACb,OAAK;AACP;AAEA,CAnBC;AAoBC,QAAM,EAAE,EAAE;AACV,WAAS;AACT,kBAAgB;AAChB,OAAK;AACL,cAAY;AACZ,UAAQ;AACR,cAAY;AACZ,iBAAe;AACf,WAAS,IAAI;AACb,UAAQ;AACR,SAAO;AACP,QAAM;AACR;AAEA,CAlCC,iBAkCiB;AAChB,cAAY,IAAI;AAClB;AAEA,CAtCC,iBAsCiB,CAAC,kBAAoB,CAAC,uBAAuB;AAC7D,WAAS;AACT,WAAS;AACT,SAAO;AACP,UAAQ;AACR,gBAAc;AACd,iBAAe;AACf,cAAY,IAAI;AAClB;AAEA,CAVwC;AAWtC,aAAW;AACX,eAAa;AACb,WAAS;AACT,eAAa;AACf;AAEA,CAAC;AACC,aAAW;AACX,SAAO,IAAI;AACX,YAAU;AACV,iBAAe;AACf,WAAS;AACT,sBAAoB;AACpB,sBAAoB;AACtB;AAEA,CAAC;AACC,aAAW;AACX,SAAO,IAAI;AACb;AAEA,CAAC;AACC,QAAM,EAAE,EAAE;AACV,cAAY;AACZ,SAAO;AACP,UAAQ;AACR,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,UAAQ;AACR,cAAY;AACZ,SAAO,IAAI;AACX,iBAAe;AACf,UAAQ;AACV;AAEA,CAfC,2BAe2B;AAC1B,cAAY,IAAI;AAChB,SAAO,IAAI;AACb;AAEA,CAAC;AACC,WAAS,KAAK;AACd,cAAY;AACZ,SAAO,IAAI;AACX,aAAW;AACb;AAEA,CAAC;AACC,SAAO,IAAI;AACX,eAAa;AACb,UAAQ,EAAE,EAAE;AACd;AAEA,CAAC;AACC,WAAS;AACT,SAAO,KAAK,KAAK,EAAE;AACnB,UAAQ;AACR,WAAS;AACT,iBAAe;AACf,UAAQ,IAAI,OAAO,IAAI;AACvB,cAAY;AACZ,SAAO,IAAI;AACX,aAAW;AACX,eAAa;AACb,UAAQ;AACV;AAEA,CAdC,sBAcsB;AACrB,cAAY,IAAI;AAClB;AAEA,CAlBC,sBAkBsB;AACrB,WAAS;AACT,UAAQ;AACV;AAEA,CAAC;AACC,YAAU;AACV,SAAO;AACP,UAAQ;AACR,WAAS;AACT,UAAQ;AACR,YAAU;AACV,QAAM,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AACpB,eAAa;AACb,UAAQ;AACV;AAEA,CAAC;AACC,UAAQ;AACR,iBAAe;AACf,cAAY,IAAI;AAChB,UAAQ,IAAI;AACd;","names":[]}
1
+ {"version":3,"sources":["../src/HermsInbox.css"],"sourcesContent":["/*\n * `<HermsInbox />`'s styles: scoped entirely under `.herms-inbox` and driven\n * by CSS custom properties, never Tailwind utility classes, so that Tailwind in the\n * host app cannot bleed into the widget. This\n * package has no Tailwind dependency at all; a host app's own Tailwind\n * build never touches these rules, and these rules never leak into the host's\n * own layout beyond `.herms-inbox`'s own box.\n *\n * Every visual value is a `--herms-*` custom property with a sensible\n * default here, overridable by a host app either globally (setting the\n * variable higher up the DOM) or per-instance via `<HermsInbox theme={...}>`,\n * which sets a few of these inline on the root element.\n *\n * Dark mode: `data-herms-color-scheme=\"dark\"` on the root element (set by the\n * `colorScheme` prop) forces dark values; leaving it unset (`colorScheme=\"auto\"`,\n * the default) follows the visitor's OS-level `prefers-color-scheme` instead.\n * A host app's own dark-mode mechanism (e.g. a `data-theme`\n * attribute) is a different, unrelated attribute and never bleeds in here.\n */\n\n/*\n * The bell's root and the panel are NOT parent and child. Radix renders the panel in a portal\n * under <body>, so nothing the panel needs can be inherited from `.herms-inbox`. Everything the\n * root declares for its descendants (the variables, the text properties, the colour-scheme\n * rules, box-sizing) is therefore declared on both, in the same rule, so the two cannot\n * disagree. Before this the panel was transparent, borderless and square, and `theme` and\n * `colorScheme` could never reach it.\n */\n.herms-inbox,\n.herms-inbox__panel {\n --herms-color-accent: #5b5bd6;\n --herms-color-accent-foreground: #ffffff;\n --herms-color-bg: #ffffff;\n --herms-color-surface: #ffffff;\n --herms-color-border: #e5e3df;\n --herms-color-text: #1c1b18;\n --herms-color-muted: #6f6b64;\n --herms-color-danger: #b03a2a;\n --herms-color-unread-dot: #b03a2a;\n --herms-color-hover: #f4f3f1;\n --herms-radius: 10px;\n --herms-font-family:\n -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n\n font-family: var(--herms-font-family);\n color: var(--herms-color-text);\n line-height: 1.4;\n}\n\n.herms-inbox {\n position: relative;\n display: inline-block;\n}\n\n@media (prefers-color-scheme: dark) {\n .herms-inbox:not([data-herms-color-scheme='light']),\n .herms-inbox__panel:not([data-herms-color-scheme='light']) {\n --herms-color-bg: #201f1a;\n --herms-color-surface: #201f1a;\n --herms-color-border: #38362f;\n --herms-color-text: #f3f1ec;\n --herms-color-muted: #a6a19a;\n --herms-color-hover: #2b2a24;\n }\n}\n\n.herms-inbox[data-herms-color-scheme='dark'],\n.herms-inbox__panel[data-herms-color-scheme='dark'] {\n --herms-color-bg: #201f1a;\n --herms-color-surface: #201f1a;\n --herms-color-border: #38362f;\n --herms-color-text: #f3f1ec;\n --herms-color-muted: #a6a19a;\n --herms-color-hover: #2b2a24;\n}\n\n.herms-inbox *,\n.herms-inbox__panel,\n.herms-inbox__panel * {\n box-sizing: border-box;\n}\n\n.herms-inbox__trigger {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border-radius: 999px;\n border: none;\n background: transparent;\n color: var(--herms-color-text);\n cursor: pointer;\n}\n\n.herms-inbox__trigger:hover {\n background: var(--herms-color-hover);\n}\n\n.herms-inbox__trigger:focus-visible {\n outline: 2px solid var(--herms-color-accent);\n outline-offset: 2px;\n}\n\n.herms-inbox__bell {\n width: 20px;\n height: 20px;\n}\n\n.herms-inbox__badge {\n position: absolute;\n top: 4px;\n right: 4px;\n min-width: 16px;\n height: 16px;\n padding: 0 4px;\n border-radius: 999px;\n background: var(--herms-color-unread-dot);\n color: #ffffff;\n font-size: 10px;\n font-weight: 600;\n line-height: 16px;\n text-align: center;\n}\n\n.herms-inbox__panel {\n width: 380px;\n max-width: calc(100vw - 24px);\n max-height: 480px;\n display: flex;\n flex-direction: column;\n border-radius: var(--herms-radius);\n border: 1px solid var(--herms-color-border);\n background: var(--herms-color-surface);\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.16);\n overflow: hidden;\n z-index: 50;\n}\n\n.herms-inbox__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--herms-color-border);\n}\n\n.herms-inbox__title {\n font-size: 14px;\n font-weight: 600;\n margin: 0;\n}\n\n.herms-inbox__header-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.herms-inbox__text-button {\n border: none;\n background: transparent;\n color: var(--herms-color-accent);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n padding: 4px 6px;\n border-radius: 6px;\n}\n\n.herms-inbox__text-button:hover {\n background: var(--herms-color-hover);\n}\n\n.herms-inbox__text-button:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.herms-inbox__icon-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n border: none;\n background: transparent;\n color: var(--herms-color-muted);\n border-radius: 6px;\n cursor: pointer;\n}\n\n.herms-inbox__icon-button:hover {\n background: var(--herms-color-hover);\n color: var(--herms-color-text);\n}\n\n.herms-inbox__icon-button:focus-visible,\n.herms-inbox__text-button:focus-visible,\n.herms-inbox__item:focus-visible {\n outline: 2px solid var(--herms-color-accent);\n outline-offset: -2px;\n}\n\n.herms-inbox__list {\n list-style: none;\n margin: 0;\n padding: 4px;\n overflow-y: auto;\n flex: 1 1 auto;\n}\n\n.herms-inbox__list-row {\n display: flex;\n align-items: stretch;\n gap: 2px;\n}\n\n.herms-inbox__item {\n flex: 1 1 auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n text-align: left;\n border: none;\n background: transparent;\n border-radius: 8px;\n padding: 8px 10px;\n cursor: pointer;\n color: inherit;\n font: inherit;\n}\n\n.herms-inbox__item:hover {\n background: var(--herms-color-hover);\n}\n\n.herms-inbox__item[data-unread='true'] .herms-inbox__item-title::before {\n content: '';\n display: inline-block;\n width: 6px;\n height: 6px;\n margin-right: 6px;\n border-radius: 999px;\n background: var(--herms-color-accent);\n}\n\n.herms-inbox__item-title {\n font-size: 13px;\n font-weight: 600;\n display: flex;\n align-items: center;\n}\n\n.herms-inbox__item-body {\n font-size: 12.5px;\n color: var(--herms-color-muted);\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n\n.herms-inbox__item-meta {\n font-size: 11px;\n color: var(--herms-color-muted);\n}\n\n.herms-inbox__archive-button {\n flex: 0 0 auto;\n align-self: center;\n width: 28px;\n height: 28px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: transparent;\n color: var(--herms-color-muted);\n border-radius: 6px;\n cursor: pointer;\n}\n\n.herms-inbox__archive-button:hover {\n background: var(--herms-color-hover);\n color: var(--herms-color-danger);\n}\n\n.herms-inbox__state {\n padding: 32px 16px;\n text-align: center;\n color: var(--herms-color-muted);\n font-size: 13px;\n}\n\n.herms-inbox__state-title {\n color: var(--herms-color-text);\n font-weight: 600;\n margin: 0 0 6px;\n}\n\n.herms-inbox__load-more {\n display: block;\n width: calc(100% - 8px);\n margin: 4px;\n padding: 8px;\n border-radius: 8px;\n border: 1px dashed var(--herms-color-border);\n background: transparent;\n color: var(--herms-color-accent);\n font-size: 12.5px;\n font-weight: 500;\n cursor: pointer;\n}\n\n.herms-inbox__load-more:hover {\n background: var(--herms-color-hover);\n}\n\n.herms-inbox__load-more:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.herms-inbox__visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.herms-inbox__skeleton-line {\n height: 12px;\n border-radius: 4px;\n background: var(--herms-color-hover);\n margin: 6px 10px;\n}\n"],"mappings":";AA4BA,CAAC;AACD,CAAC;AACC,wBAAsB;AACtB,mCAAiC;AACjC,oBAAkB;AAClB,yBAAuB;AACvB,wBAAsB;AACtB,sBAAoB;AACpB,uBAAqB;AACrB,wBAAsB;AACtB,4BAA0B;AAC1B,uBAAqB;AACrB,kBAAgB;AAChB;AAAA,IACE,aAAa;AAAA,IAAE,UAAU;AAAA,IAAE,MAAM;AAAA,IAAE,SAAS;AAAA,IAAE,KAAK;AAAA,IAAE;AAEvD,eAAa,IAAI;AACjB,SAAO,IAAI;AACX,eAAa;AACf;AAEA,CArBC;AAsBC,YAAU;AACV,WAAS;AACX;AAEA,QAAO,sBAAuB;AAC5B,GA3BD,WA2Ba,KAAK,CAAC;AAAA,EAClB,CA3BD,kBA2BoB,KAAK,CAAC;AACvB,sBAAkB;AAClB,2BAAuB;AACvB,0BAAsB;AACtB,wBAAoB;AACpB,yBAAqB;AACrB,yBAAqB;AACvB;AACF;AAEA,CAtCC,WAsCW,CAAC;AACb,CAtCC,kBAsCkB,CAAC;AAClB,oBAAkB;AAClB,yBAAuB;AACvB,wBAAsB;AACtB,sBAAoB;AACpB,uBAAqB;AACrB,uBAAqB;AACvB;AAEA,CAhDC,YAgDY;AACb,CAhDC;AAiDD,CAjDC,mBAiDmB;AAClB,cAAY;AACd;AAEA,CAAC;AACC,YAAU;AACV,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,SAAO;AACP,UAAQ;AACR,iBAAe;AACf,UAAQ;AACR,cAAY;AACZ,SAAO,IAAI;AACX,UAAQ;AACV;AAEA,CAdC,oBAcoB;AACnB,cAAY,IAAI;AAClB;AAEA,CAlBC,oBAkBoB;AACnB,WAAS,IAAI,MAAM,IAAI;AACvB,kBAAgB;AAClB;AAEA,CAAC;AACC,SAAO;AACP,UAAQ;AACV;AAEA,CAAC;AACC,YAAU;AACV,OAAK;AACL,SAAO;AACP,aAAW;AACX,UAAQ;AACR,WAAS,EAAE;AACX,iBAAe;AACf,cAAY,IAAI;AAChB,SAAO;AACP,aAAW;AACX,eAAa;AACb,eAAa;AACb,cAAY;AACd;AAEA,CAjGC;AAkGC,SAAO;AACP,aAAW,KAAK,MAAM,EAAE;AACxB,cAAY;AACZ,WAAS;AACT,kBAAgB;AAChB,iBAAe,IAAI;AACnB,UAAQ,IAAI,MAAM,IAAI;AACtB,cAAY,IAAI;AAChB,cAAY,EAAE,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AACrC,YAAU;AACV,WAAS;AACX;AAEA,CAAC;AACC,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,OAAK;AACL,WAAS,KAAK;AACd,iBAAe,IAAI,MAAM,IAAI;AAC/B;AAEA,CAAC;AACC,aAAW;AACX,eAAa;AACb,UAAQ;AACV;AAEA,CAAC;AACC,WAAS;AACT,eAAa;AACb,OAAK;AACP;AAEA,CAAC;AACC,UAAQ;AACR,cAAY;AACZ,SAAO,IAAI;AACX,aAAW;AACX,eAAa;AACb,UAAQ;AACR,WAAS,IAAI;AACb,iBAAe;AACjB;AAEA,CAXC,wBAWwB;AACvB,cAAY,IAAI;AAClB;AAEA,CAfC,wBAewB;AACvB,WAAS;AACT,UAAQ;AACV;AAEA,CAAC;AACC,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,SAAO;AACP,UAAQ;AACR,UAAQ;AACR,cAAY;AACZ,SAAO,IAAI;AACX,iBAAe;AACf,UAAQ;AACV;AAEA,CAbC,wBAawB;AACvB,cAAY,IAAI;AAChB,SAAO,IAAI;AACb;AAEA,CAlBC,wBAkBwB;AACzB,CAvCC,wBAuCwB;AACzB,CAAC,iBAAiB;AAChB,WAAS,IAAI,MAAM,IAAI;AACvB,kBAAgB;AAClB;AAEA,CAAC;AACC,cAAY;AACZ,UAAQ;AACR,WAAS;AACT,cAAY;AACZ,QAAM,EAAE,EAAE;AACZ;AAEA,CAAC;AACC,WAAS;AACT,eAAa;AACb,OAAK;AACP;AAEA,CAnBC;AAoBC,QAAM,EAAE,EAAE;AACV,WAAS;AACT,kBAAgB;AAChB,OAAK;AACL,cAAY;AACZ,UAAQ;AACR,cAAY;AACZ,iBAAe;AACf,WAAS,IAAI;AACb,UAAQ;AACR,SAAO;AACP,QAAM;AACR;AAEA,CAlCC,iBAkCiB;AAChB,cAAY,IAAI;AAClB;AAEA,CAtCC,iBAsCiB,CAAC,kBAAoB,CAAC,uBAAuB;AAC7D,WAAS;AACT,WAAS;AACT,SAAO;AACP,UAAQ;AACR,gBAAc;AACd,iBAAe;AACf,cAAY,IAAI;AAClB;AAEA,CAVwC;AAWtC,aAAW;AACX,eAAa;AACb,WAAS;AACT,eAAa;AACf;AAEA,CAAC;AACC,aAAW;AACX,SAAO,IAAI;AACX,YAAU;AACV,iBAAe;AACf,WAAS;AACT,sBAAoB;AACpB,sBAAoB;AACtB;AAEA,CAAC;AACC,aAAW;AACX,SAAO,IAAI;AACb;AAEA,CAAC;AACC,QAAM,EAAE,EAAE;AACV,cAAY;AACZ,SAAO;AACP,UAAQ;AACR,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,UAAQ;AACR,cAAY;AACZ,SAAO,IAAI;AACX,iBAAe;AACf,UAAQ;AACV;AAEA,CAfC,2BAe2B;AAC1B,cAAY,IAAI;AAChB,SAAO,IAAI;AACb;AAEA,CAAC;AACC,WAAS,KAAK;AACd,cAAY;AACZ,SAAO,IAAI;AACX,aAAW;AACb;AAEA,CAAC;AACC,SAAO,IAAI;AACX,eAAa;AACb,UAAQ,EAAE,EAAE;AACd;AAEA,CAAC;AACC,WAAS;AACT,SAAO,KAAK,KAAK,EAAE;AACnB,UAAQ;AACR,WAAS;AACT,iBAAe;AACf,UAAQ,IAAI,OAAO,IAAI;AACvB,cAAY;AACZ,SAAO,IAAI;AACX,aAAW;AACX,eAAa;AACb,UAAQ;AACV;AAEA,CAdC,sBAcsB;AACrB,cAAY,IAAI;AAClB;AAEA,CAlBC,sBAkBsB;AACrB,WAAS;AACT,UAAQ;AACV;AAEA,CAAC;AACC,YAAU;AACV,SAAO;AACP,UAAQ;AACR,WAAS;AACT,UAAQ;AACR,YAAU;AACV,QAAM,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AACpB,eAAa;AACb,UAAQ;AACV;AAEA,CAAC;AACC,UAAQ;AACR,iBAAe;AACf,cAAY,IAAI;AAChB,UAAQ,IAAI;AACd;","names":[]}
package/dist/index.js CHANGED
@@ -76,7 +76,7 @@ function usePreferences() {
76
76
  }
77
77
 
78
78
  // src/HermsInbox.tsx
79
- import { useCallback, useEffect as useEffect5, useMemo as useMemo5, useRef, useState } from "react";
79
+ import { useCallback, useEffect as useEffect5, useId, useMemo as useMemo5, useRef, useState } from "react";
80
80
  import * as Popover from "@radix-ui/react-popover";
81
81
 
82
82
  // src/locale.ts
@@ -167,6 +167,7 @@ function HermsInbox({ placement = "bottom-end", onItemClick, theme, colorScheme
167
167
  const itemRefs = useRef([]);
168
168
  const seenIdsRef = useRef(/* @__PURE__ */ new Set());
169
169
  const strings = getHermsInboxStrings(locale);
170
+ const titleId = useId();
170
171
  useEffect5(() => {
171
172
  if (!open || items.length === 0) return;
172
173
  const toMark = items.filter((item) => !seenIdsRef.current.has(item.id)).map((item) => item.id);
@@ -226,7 +227,9 @@ function HermsInbox({ placement = "bottom-end", onItemClick, theme, colorScheme
226
227
  }, [theme?.accent, theme?.radius]);
227
228
  const { side, align } = placementToSideAlign[placement];
228
229
  const rootClassName = ["herms-inbox", className].filter(Boolean).join(" ");
229
- return /* @__PURE__ */ jsxs("div", { className: rootClassName, style: rootStyle, "data-herms-color-scheme": colorScheme === "auto" ? void 0 : colorScheme, children: [
230
+ const panelClassName = ["herms-inbox__panel", className].filter(Boolean).join(" ");
231
+ const colorSchemeAttribute = colorScheme === "auto" ? void 0 : colorScheme;
232
+ return /* @__PURE__ */ jsxs("div", { className: rootClassName, style: rootStyle, "data-herms-color-scheme": colorSchemeAttribute, children: [
230
233
  /* @__PURE__ */ jsx2("span", { className: "herms-inbox__visually-hidden", role: "status", "aria-live": "polite", children: unread > 0 ? strings.bellLabelWithUnread(unread) : "" }),
231
234
  /* @__PURE__ */ jsxs(Popover.Root, { open, onOpenChange: setOpen, children: [
232
235
  /* @__PURE__ */ jsx2(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
@@ -245,7 +248,10 @@ function HermsInbox({ placement = "bottom-end", onItemClick, theme, colorScheme
245
248
  /* @__PURE__ */ jsx2(Popover.Portal, { children: /* @__PURE__ */ jsxs(
246
249
  Popover.Content,
247
250
  {
248
- className: "herms-inbox__panel",
251
+ className: panelClassName,
252
+ style: rootStyle,
253
+ "data-herms-color-scheme": colorSchemeAttribute,
254
+ "aria-labelledby": titleId,
249
255
  side,
250
256
  align,
251
257
  sideOffset: 8,
@@ -257,7 +263,7 @@ function HermsInbox({ placement = "bottom-end", onItemClick, theme, colorScheme
257
263
  },
258
264
  children: [
259
265
  /* @__PURE__ */ jsxs("div", { className: "herms-inbox__header", children: [
260
- /* @__PURE__ */ jsx2("p", { className: "herms-inbox__title", children: strings.panelTitle }),
266
+ /* @__PURE__ */ jsx2("p", { className: "herms-inbox__title", id: titleId, children: strings.panelTitle }),
261
267
  /* @__PURE__ */ jsxs("div", { className: "herms-inbox__header-actions", children: [
262
268
  /* @__PURE__ */ jsx2("button", { type: "button", className: "herms-inbox__text-button", onClick: () => void markAllRead(), disabled: unread === 0, children: strings.markAllRead }),
263
269
  /* @__PURE__ */ jsx2(Popover.Close, { asChild: true, children: /* @__PURE__ */ jsx2("button", { type: "button", className: "herms-inbox__icon-button", "aria-label": strings.close, children: /* @__PURE__ */ jsx2(CloseIcon, {}) }) })
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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":["useEffect","useMemo","useMemo","useEffect","useEffect","useMemo","useSyncExternalStore","useMemo","useEffect","useSyncExternalStore","useEffect","useMemo","useSyncExternalStore","useMemo","useEffect","useSyncExternalStore","useEffect","useMemo","jsx","useEffect","useMemo"]}
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, useId, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent } from 'react'\nimport * as Popover from '@radix-ui/react-popover'\nimport type { HermsInboxItem } from '@hermesihq/js'\nimport { useHermsContext } from './HermsProvider'\nimport { useInbox } from './useInbox'\nimport { useUnreadCount } from './useUnreadCount'\nimport { getHermsInboxStrings, type HermsLocale } from './locale'\nimport './HermsInbox.css'\n\nexport type HermsInboxPlacement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'\n\nexport interface HermsInboxTheme {\n accent?: string\n radius?: string\n}\n\nexport interface HermsInboxProps {\n /** Popover position relative to the bell trigger. Default `bottom-end`. */\n placement?: HermsInboxPlacement\n /** Called when an item is activated (click, or Enter on a focused row).\n * Lets a host app drive its own router (for example\n * `onItemClick={(item) => navigate(item.actionUrl)}`) instead of a hard\n * page navigation. When omitted, activating an item with a non-null\n * `actionUrl` does a plain `window.location.assign` to it. Either way, the\n * item is marked read first. */\n onItemClick?: (item: HermsInboxItem) => void\n /** A couple of the most commonly re-themed CSS custom properties, settable\n * without a host app writing any CSS of its own. Anything else is\n * reachable by overriding `--herms-color-*`/`--herms-radius` variables\n * directly (see `HermsInbox.css`). */\n theme?: HermsInboxTheme\n /** `'auto'` (default) follows the visitor's OS `prefers-color-scheme`;\n * `'light'`/`'dark'` force one regardless of it. */\n colorScheme?: 'auto' | 'light' | 'dark'\n locale?: HermsLocale\n className?: string\n}\n\nconst placementToSideAlign: Record<HermsInboxPlacement, { side: 'top' | 'bottom'; align: 'start' | 'end' }> = {\n 'bottom-start': { side: 'bottom', align: 'start' },\n 'bottom-end': { side: 'bottom', align: 'end' },\n 'top-start': { side: 'top', align: 'start' },\n 'top-end': { side: 'top', align: 'end' },\n}\n\nfunction BellIcon() {\n return (\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\">\n <path d=\"M6 8a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6\" />\n <path d=\"M10 21a2 2 0 0 0 4 0\" />\n </svg>\n )\n}\n\nfunction CloseIcon() {\n return (\n <svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden=\"true\">\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n )\n}\n\nfunction ArchiveIcon() {\n return (\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\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"4\" rx=\"1\" />\n <path d=\"M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8\" />\n <path d=\"M10 13h4\" />\n </svg>\n )\n}\n\nfunction AlertIcon() {\n return (\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\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M12 8v5M12 16h.01\" />\n </svg>\n )\n}\n\nfunction relativeTime(iso: string, locale: HermsLocale): string {\n const then = new Date(iso).getTime()\n if (Number.isNaN(then)) return ''\n const diffSeconds = Math.max(0, Math.round((Date.now() - then) / 1000))\n const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })\n if (diffSeconds < 60) return formatter.format(0, 'minute')\n const diffMinutes = Math.round(diffSeconds / 60)\n if (diffMinutes < 60) return formatter.format(-diffMinutes, 'minute')\n const diffHours = Math.round(diffMinutes / 60)\n if (diffHours < 24) return formatter.format(-diffHours, 'hour')\n const diffDays = Math.round(diffHours / 24)\n return formatter.format(-diffDays, 'day')\n}\n\n/**\n * `<HermsInbox />`: bell trigger + dropdown panel. Built\n * entirely on the headless `useInbox`/`useUnreadCount` hooks; every state\n * (loading, empty, error, populated) renders here, none of it invented by a\n * host app. Accessible: a real `<button>` trigger with `aria-label`/\n * `aria-expanded`, the panel as `role=\"menu\"` with roving-tabindex arrow-key\n * navigation, `Home`/`End`, `Escape`-to-close (native to Radix `Popover`,\n * which also returns focus to the trigger), and `Enter`/`Space` (native\n * `<button>` behavior) to activate the focused item.\n */\nexport function HermsInbox({ placement = 'bottom-end', onItemClick, theme, colorScheme = 'auto', locale, className }: HermsInboxProps) {\n const { client } = useHermsContext()\n const { unread, unseen } = useUnreadCount()\n const { items, isLoading, isLoadingMore, error, hasMore, loadMore, markRead, markAllRead, archive, refetch } = useInbox()\n const [open, setOpen] = useState(false)\n const [activeIndex, setActiveIndex] = useState(0)\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([])\n const seenIdsRef = useRef<Set<string>>(new Set())\n const strings = getHermsInboxStrings(locale)\n const titleId = useId()\n\n // \"Seen\" (the bell was opened) is distinct from \"read.\" Marks\n // every currently loaded, not-yet-seen id seen once the panel is open and\n // has items, including any additional ids that arrive later via\n // `loadMore`/live updates while it stays open.\n useEffect(() => {\n if (!open || items.length === 0) return\n const toMark = items.filter((item) => !seenIdsRef.current.has(item.id)).map((item) => item.id)\n if (toMark.length === 0) return\n for (const id of toMark) seenIdsRef.current.add(id)\n void client.markSeen(toMark)\n }, [open, items, client])\n\n useEffect(() => {\n if (open) setActiveIndex(0)\n }, [open])\n\n const focusItem = useCallback((index: number) => {\n setActiveIndex(index)\n itemRefs.current[index]?.focus()\n }, [])\n\n const handleListKeyDown = useCallback(\n (event: KeyboardEvent<HTMLUListElement>) => {\n if (items.length === 0) return\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n focusItem(Math.min(activeIndex + 1, items.length - 1))\n break\n case 'ArrowUp':\n event.preventDefault()\n focusItem(Math.max(activeIndex - 1, 0))\n break\n case 'Home':\n event.preventDefault()\n focusItem(0)\n break\n case 'End':\n event.preventDefault()\n focusItem(items.length - 1)\n break\n default:\n break\n }\n },\n [items.length, activeIndex, focusItem],\n )\n\n const handleActivate = useCallback(\n (item: HermsInboxItem) => {\n if (!item.readAt) void markRead(item.id)\n if (onItemClick) {\n onItemClick(item)\n } else if (item.actionUrl && typeof window !== 'undefined') {\n window.location.assign(item.actionUrl)\n }\n },\n [markRead, onItemClick],\n )\n\n const rootStyle = useMemo<CSSProperties>(() => {\n const style: CSSProperties & Record<string, string> = {}\n if (theme?.accent) style['--herms-color-accent'] = theme.accent\n if (theme?.radius) style['--herms-radius'] = theme.radius\n return style\n }, [theme?.accent, theme?.radius])\n\n const { side, align } = placementToSideAlign[placement]\n const rootClassName = ['herms-inbox', className].filter(Boolean).join(' ')\n // The panel is rendered by Radix in a portal under <body>, so it is not inside the root\n // above and inherits nothing from it. Whatever the root carries so that the stylesheet can\n // theme it, the panel has to carry as well: the theme's inline variables, the forced colour\n // scheme, and the host's class (so that one rule on `className` themes both).\n const panelClassName = ['herms-inbox__panel', className].filter(Boolean).join(' ')\n const colorSchemeAttribute = colorScheme === 'auto' ? undefined : colorScheme\n\n return (\n <div className={rootClassName} style={rootStyle} data-herms-color-scheme={colorSchemeAttribute}>\n <span className=\"herms-inbox__visually-hidden\" role=\"status\" aria-live=\"polite\">\n {unread > 0 ? strings.bellLabelWithUnread(unread) : ''}\n </span>\n <Popover.Root open={open} onOpenChange={setOpen}>\n <Popover.Trigger asChild>\n <button\n type=\"button\"\n className=\"herms-inbox__trigger\"\n aria-label={unread > 0 ? strings.bellLabelWithUnread(unread) : strings.bellLabel}\n aria-expanded={open}\n >\n <BellIcon />\n {unseen > 0 && (\n <span className=\"herms-inbox__badge\" aria-hidden=\"true\">\n {unseen > 99 ? '99+' : unseen}\n </span>\n )}\n </button>\n </Popover.Trigger>\n <Popover.Portal>\n <Popover.Content\n className={panelClassName}\n style={rootStyle}\n data-herms-color-scheme={colorSchemeAttribute}\n // The dialog needs a name of its own. Only the list, the bell and the loading\n // state were named; a dialog without one is announced as just \"dialog\".\n aria-labelledby={titleId}\n side={side}\n align={align}\n sideOffset={8}\n onOpenAutoFocus={(event) => {\n // Radix's default is to focus the content wrapper itself; we\n // want the first item (or the header when there are none)\n // focused instead, so keyboard users land somewhere useful.\n if (items.length > 0) {\n event.preventDefault()\n itemRefs.current[0]?.focus()\n }\n }}\n >\n <div className=\"herms-inbox__header\">\n <p className=\"herms-inbox__title\" id={titleId}>{strings.panelTitle}</p>\n <div className=\"herms-inbox__header-actions\">\n <button type=\"button\" className=\"herms-inbox__text-button\" onClick={() => void markAllRead()} disabled={unread === 0}>\n {strings.markAllRead}\n </button>\n <Popover.Close asChild>\n <button type=\"button\" className=\"herms-inbox__icon-button\" aria-label={strings.close}>\n <CloseIcon />\n </button>\n </Popover.Close>\n </div>\n </div>\n\n {isLoading ? (\n <div role=\"status\" aria-label={strings.loading}>\n {[0, 1, 2, 3].map((i) => (\n <div key={i} className=\"herms-inbox__skeleton-line\" style={{ width: i % 2 === 0 ? '85%' : '65%' }} />\n ))}\n </div>\n ) : error ? (\n <div className=\"herms-inbox__state\" role=\"alert\">\n <AlertIcon />\n <p className=\"herms-inbox__state-title\">{strings.errorTitle}</p>\n <button type=\"button\" className=\"herms-inbox__text-button\" onClick={() => refetch()}>\n {strings.retry}\n </button>\n </div>\n ) : items.length === 0 ? (\n <div className=\"herms-inbox__state\">{strings.empty}</div>\n ) : (\n <>\n <ul className=\"herms-inbox__list\" role=\"menu\" aria-label={strings.panelTitle} onKeyDown={handleListKeyDown}>\n {items.map((item, index) => (\n <li key={item.id} className=\"herms-inbox__list-row\" role=\"none\">\n <button\n type=\"button\"\n role=\"menuitem\"\n ref={(el) => {\n itemRefs.current[index] = el\n }}\n tabIndex={index === activeIndex ? 0 : -1}\n className=\"herms-inbox__item\"\n data-unread={!item.readAt}\n onFocus={() => setActiveIndex(index)}\n onClick={() => handleActivate(item)}\n >\n <span className=\"herms-inbox__item-title\">{item.title}</span>\n {item.body && <span className=\"herms-inbox__item-body\">{item.body}</span>}\n <span className=\"herms-inbox__item-meta\">{relativeTime(item.createdAt, locale ?? 'en')}</span>\n </button>\n <button\n type=\"button\"\n className=\"herms-inbox__archive-button\"\n aria-label={`${strings.archive}: ${item.title}`}\n onClick={(event) => {\n event.stopPropagation()\n void archive(item.id)\n }}\n >\n <ArchiveIcon />\n </button>\n </li>\n ))}\n </ul>\n {hasMore && (\n <button type=\"button\" className=\"herms-inbox__load-more\" onClick={loadMore} disabled={isLoadingMore}>\n {isLoadingMore ? strings.loadingMore : strings.loadMore}\n </button>\n )}\n </>\n )}\n </Popover.Content>\n </Popover.Portal>\n </Popover.Root>\n </div>\n )\n}\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":["useEffect","useMemo","useMemo","useEffect","useEffect","useMemo","useSyncExternalStore","useMemo","useEffect","useSyncExternalStore","useEffect","useMemo","useSyncExternalStore","useMemo","useEffect","useSyncExternalStore","useEffect","useMemo","jsx","useEffect","useMemo"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hermesihq/react",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Embeddable notification inbox for React: hooks over Hermesi's client API, plus a ready bell-and-panel component.",
5
5
  "type": "module",
6
6
  "license": "MIT",