@12-apps/notifications 4.7.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADOPTING.md +32 -0
- package/dist/{chunk-TIGTBSAQ.js → chunk-7234ROOP.js} +11 -5
- package/dist/chunk-7234ROOP.js.map +1 -0
- package/dist/{chunk-Y34FX24X.js → chunk-BGI4BQ66.js} +2 -10
- package/dist/chunk-BGI4BQ66.js.map +1 -0
- package/dist/chunk-BW723CX2.js +214 -0
- package/dist/chunk-BW723CX2.js.map +1 -0
- package/dist/chunk-CQZMTFPY.js +76 -0
- package/dist/chunk-CQZMTFPY.js.map +1 -0
- package/dist/{chunk-HQU4R4SG.js → chunk-GGAZOLNN.js} +2 -2
- package/dist/{chunk-QRAXX3GR.js → chunk-KU4FVOFA.js} +2 -2
- package/dist/chunk-M2TVBVH2.js +15 -0
- package/dist/chunk-M2TVBVH2.js.map +1 -0
- package/dist/chunk-M5XTROYM.js +263 -0
- package/dist/chunk-M5XTROYM.js.map +1 -0
- package/dist/chunk-O5BVUXPO.js +22 -0
- package/dist/chunk-O5BVUXPO.js.map +1 -0
- package/dist/{create-api-notifications-B3u6Kx3x.d.ts → create-api-notifications-WaexrZnn.d.ts} +12 -4
- package/dist/{create-web-notifications-BpNR8qH3.d.ts → create-web-notifications-DrgqtU4Q.d.ts} +15 -4
- package/dist/{generators-B9xt3sRh.d.ts → generators-CH5m8Ucj.d.ts} +1 -1
- package/dist/hono/index.d.ts +5 -5
- package/dist/hono/index.js +4 -3
- package/dist/hono/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -3
- package/dist/{jobs-BDZ7aGHV.d.ts → jobs-CcsSA0YR.d.ts} +1 -1
- package/dist/manifest/server.d.ts +6 -6
- package/dist/manifest/server.js +5 -4
- package/dist/manifest/server.js.map +1 -1
- package/dist/manifest/web.d.ts +3 -3
- package/dist/manifest/web.js +3 -2
- package/dist/manifest/web.js.map +1 -1
- package/dist/panel-UFXNO4AF.js +243 -0
- package/dist/panel-UFXNO4AF.js.map +1 -0
- package/dist/preferences-screen-V3ZBWP7A.js +294 -0
- package/dist/preferences-screen-V3ZBWP7A.js.map +1 -0
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +17 -11
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +5 -4
- package/dist/{types-CXLAG3UU.d.ts → types-CYJN1xCM.d.ts} +39 -2
- package/dist/web-push/index.d.ts +2 -2
- package/dist/{web-push-Cs14Wp9u.d.ts → web-push-iER5StBm.d.ts} +1 -1
- package/dist/{wire-6dzyfDE7.d.ts → wire-CmRrB3_s.d.ts} +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/react/create-web-notifications.tsx +19 -10
- package/src/react/page-lazy.tsx +73 -0
- package/src/react/panel-lazy.tsx +74 -0
- package/src/server/db.ts +12 -1
- package/src/server/dispatch.ts +4 -0
- package/src/server/router.ts +17 -1
- package/src/types.ts +39 -1
- package/dist/chunk-6HLHQDKS.js +0 -1022
- package/dist/chunk-6HLHQDKS.js.map +0 -1
- package/dist/chunk-TIGTBSAQ.js.map +0 -1
- package/dist/chunk-Y34FX24X.js.map +0 -1
- /package/dist/{chunk-HQU4R4SG.js.map → chunk-GGAZOLNN.js.map} +0 -0
- /package/dist/{chunk-QRAXX3GR.js.map → chunk-KU4FVOFA.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/api.ts","../src/react/transport.ts","../src/react/create-web-notifications.tsx","../src/react/bell-button.tsx","../src/react/panel-lazy.tsx","../src/react/page-lazy.tsx"],"sourcesContent":["import type { ChannelRow } from '../preferences-core';\nimport type { NotificationChannel } from '../types';\nimport type { ListNotificationsResult } from '../wire';\n\nimport type { NotificationsResult, NotificationsTransport } from './transport';\n\n/**\n * The wire client, bound to one mount (12-15).\n *\n * Every path this package's screens can call, in one place — which is what\n * makes the api half's route table and the web half's URLs one contract instead\n * of two lists that drift.\n */\n\n/** `GET <mount>/notification-preferences` and the PUT's answer. */\nexport interface PreferencesPayload {\n preferences: Record<string, ChannelRow>;\n availability: Record<NotificationChannel, boolean>;\n /** The host's taxonomy, so the screen renders it without being told twice. */\n categories: string[];\n}\n\n/** `GET <mount>/push-subscriptions`. */\nexport interface PushRegistrationPayload {\n /** null = web push is not configured on this deployment. */\n vapidPublicKey: string | null;\n count: number;\n /**\n * Whether the endpoint asked about is still registered to the caller. Present\n * only when one was passed — see {@link NotificationsApiClient.getPushRegistration}.\n */\n registered?: boolean;\n}\n\nexport interface NotificationsApiClient {\n listNotifications(input: {\n cursor?: string | null;\n limit?: number;\n filter?: 'all' | 'unread';\n }): Promise<ListNotificationsResult>;\n unreadCount(): Promise<number>;\n markRead(ids: readonly string[]): Promise<NotificationsResult<{ updated: number }>>;\n markAllRead(): Promise<NotificationsResult<{ updated: number }>>;\n remove(ids: readonly string[]): Promise<NotificationsResult<{ deleted: number }>>;\n getPreferences(): Promise<PreferencesPayload>;\n savePreference(\n category: string,\n channel: NotificationChannel,\n enabled: boolean,\n ): Promise<NotificationsResult<PreferencesPayload>>;\n /**\n * The deployment's VAPID key and the caller's device count — and, when an\n * `endpoint` is passed, whether the SERVER still has that exact subscription\n * under the caller's id. The browser holding a subscription object is not\n * evidence of that: a re-own or a 404/410 prune drops the row and leaves the\n * browser's object in place.\n */\n getPushRegistration(input?: { endpoint?: string }): Promise<PushRegistrationPayload>;\n savePushSubscription(input: {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n }): Promise<NotificationsResult<{ count: number }>>;\n removePushSubscription(endpoint: string): Promise<NotificationsResult<{ count: number }>>;\n}\n\nexport function createNotificationsApiClient(\n apiBase: string,\n transport: NotificationsTransport,\n): NotificationsApiClient {\n const base = apiBase.replace(/\\/$/, '');\n const url = (path: string): string => `${base}${path}`;\n\n return {\n listNotifications({ cursor, limit, filter }) {\n const params = new URLSearchParams();\n if (limit !== undefined) params.set('limit', String(limit));\n if (cursor) params.set('cursor', cursor);\n if (filter) params.set('filter', filter);\n const query = params.toString();\n return transport.get<ListNotificationsResult>(\n url(`/notifications${query ? `?${query}` : ''}`),\n );\n },\n async unreadCount() {\n const { count } = await transport.get<{ count: number }>(\n url('/notifications/unread-count'),\n );\n return count;\n },\n markRead: (ids) =>\n transport.send(url('/notifications/mark-read'), 'POST', { ids: [...ids] }),\n markAllRead: () => transport.send(url('/notifications/mark-read'), 'POST', { all: true }),\n remove: (ids) => transport.send(url('/notifications/delete'), 'POST', { ids: [...ids] }),\n getPreferences: () => transport.get<PreferencesPayload>(url('/notification-preferences')),\n savePreference: (category, channel, enabled) =>\n transport.send(url('/notification-preferences'), 'PUT', {\n [category]: { [channel]: enabled },\n }),\n getPushRegistration: ({ endpoint } = {}) =>\n transport.get<PushRegistrationPayload>(\n url(\n endpoint\n ? `/push-subscriptions?endpoint=${encodeURIComponent(endpoint)}`\n : '/push-subscriptions',\n ),\n ),\n savePushSubscription: (input) => transport.send(url('/push-subscriptions'), 'POST', input),\n removePushSubscription: (endpoint) =>\n transport.send(url('/push-subscriptions'), 'DELETE', { endpoint }),\n };\n}\n","/**\n * How the notification screens reach their data (12-15) — the report-builder\n * transport doctrine: this is the ONLY way the surface performs I/O, so a\n * caller supplying one has substituted the entire backend without stubbing a\n * global. The default is same-origin `fetch` riding the browser's cookies.\n */\n\n/** A write outcome the screens branch on — never a thrown mutation. */\nexport type NotificationsResult<T> = { ok: true; data: T } | { ok: false; error: string };\n\n/** A failed read, carrying the status the screens branch on (401 = signed out). */\nexport class NotificationsHttpError extends Error {\n readonly status: number;\n constructor(status: number, message: string) {\n super(message);\n this.name = 'NotificationsHttpError';\n this.status = status;\n Object.setPrototypeOf(this, NotificationsHttpError.prototype);\n }\n}\n\nexport interface NotificationsTransport {\n /** A read. Returns the payload INSIDE the `{ data }` envelope. */\n get<T>(path: string): Promise<T>;\n /** A write. Returns a {@link NotificationsResult} rather than rejecting. */\n send<T>(path: string, method: string, body?: unknown): Promise<NotificationsResult<T>>;\n}\n\n/**\n * @param fallbackError What a failed write says when the server sent no\n * sentence of its own — REQUIRED, the host's words. `createWebNotifications`\n * already passes its (equally required) `messages.operationFailed`; only a\n * host constructing the transport directly writes it here. The old default\n * was one application's Portuguese, and the only string in this package the\n * required-messages port did not cover.\n */\nexport function httpNotificationsTransport(fallbackError: string): NotificationsTransport {\n return {\n async get<T>(path: string): Promise<T> {\n const response = await fetch(path, {\n credentials: 'same-origin',\n headers: { Accept: 'application/json' },\n });\n const payload = (await response.json().catch(() => null)) as\n | { data?: T; error?: string }\n | null;\n if (!response.ok) {\n throw new NotificationsHttpError(\n response.status,\n payload?.error ?? `HTTP ${response.status} for ${path}`,\n );\n }\n return (payload?.data ?? payload) as T;\n },\n\n async send<T>(path: string, method: string, body?: unknown): Promise<NotificationsResult<T>> {\n try {\n const response = await fetch(path, {\n method,\n credentials: 'same-origin',\n headers: {\n Accept: 'application/json',\n ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),\n },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (response.status === 204) return { ok: true, data: undefined as T };\n const payload = (await response.json().catch(() => null)) as\n | { data?: T; error?: string }\n | null;\n if (!response.ok) return { ok: false, error: payload?.error ?? fallbackError };\n return { ok: true, data: (payload?.data ?? payload) as T };\n } catch {\n return { ok: false, error: fallbackError };\n }\n },\n };\n}\n","import { useState, type ComponentType, type JSX } from 'react';\n\nimport { messagesOf, type NotificationMessages } from '../messages';\n\nimport { createNotificationsApiClient, type NotificationsApiClient } from './api';\nimport { BellButton, type BellButtonProps } from './bell-button';\nimport {\n useUnreadCount,\n type NotificationsSignalHook,\n type NotificationsSubscribe,\n} from './hooks';\nimport { createInboxStore, type InboxStore } from './inbox-state';\nimport { lazyNotificationsPanel } from './panel-lazy';\nimport type { NotificationsPanelProps } from './panel';\nimport { lazyPreferencesPage } from './page-lazy';\nimport type { PreferencesScreenProps } from './preferences-screen';\nimport { httpNotificationsTransport, type NotificationsTransport } from './transport';\nimport type { WebPushSetupConfig } from './web-push-setup';\n\n/**\n * The one thing this package exposes to a FRONTEND host (12-15).\n *\n * Everything the notification centre IS — the bell with its live badge, the\n * slide-over inbox with its optimistic mark-read / delete / mark-all and its\n * cursor pager, the preferences matrix with its availability hints and the\n * per-browser push enable step, and every wire call between them — lives inside\n * this package. The host names where the API is mounted, and that is the whole\n * wiring.\n *\n * `page` is the standalone surface (the preferences screen), which is the one\n * thing a host routes to. The bell and the panel are a PAIR a host drops into\n * its own chrome, and they share one store, so a read in the panel moves the\n * badge in the same tick.\n */\n\nexport interface NotificationsWebConfig {\n /** The account mount the routes live under, e.g. `/api/account`. */\n apiBase: string;\n /** How the surface reaches its data. Default: same-origin fetch. */\n transport?: NotificationsTransport;\n /** User-facing copy overrides (pt-BR product copy by default). */\n messages: NotificationMessages;\n /**\n * How the surface learns an inbox changed without asking — the host's message\n * bus. Without it the badge keeps its 60 s poll, which is the standing\n * contract rather than a fallback: a dropped event must cost latency, never\n * correctness.\n */\n subscribe?: NotificationsSubscribe;\n /**\n * The same wiring as a HOOK, for a host whose realtime connection lives in\n * React context — see `NotificationsSignalHook`. `subscribe` is read at\n * factory time, which such a host cannot reach.\n */\n useSignal?: NotificationsSignalHook;\n /** The browser push enable step's host seams (SW path, platform hint). */\n webPush?: WebPushSetupConfig;\n}\n\nexport interface WebNotifications {\n /**\n * The routed surface: the preferences screen.\n *\n * Loaded on demand — see `page-lazy.tsx`. A host that mounts only the bell and\n * the panel never downloads it, and a host that routes to it fetches it while\n * entering that route.\n */\n page: ComponentType<PreferencesScreenProps>;\n /** The bell, already bound to the shared store. */\n BellButton: ComponentType<BellButtonProps>;\n /**\n * The inbox slide-over, sharing that store.\n *\n * Loaded the first time it is opened — see `panel-lazy.tsx`. Until then a\n * host's chrome carries the bell and nothing else.\n */\n Panel: ComponentType<NotificationsPanelProps>;\n /**\n * Bell + panel as ONE element, for a host that just wants the feature in its\n * header and does not want to own the open/closed state.\n */\n BellWithPanel: ComponentType<{\n enabled?: boolean;\n onNavigate?: (link: string) => void;\n }>;\n /** The badge number, for a host with its own trigger chrome. */\n useUnreadCount: (options?: { enabled?: boolean }) => number;\n /** The shared client state, for host glue. */\n store: InboxStore;\n /** The bound wire client. */\n api: NotificationsApiClient;\n /** The copy in force, so a host's own chrome can reuse a sentence. */\n messages: NotificationMessages;\n}\n\nexport function createWebNotifications(config: NotificationsWebConfig): WebNotifications {\n const messages = messagesOf(config);\n const api = createNotificationsApiClient(\n config.apiBase,\n config.transport ?? httpNotificationsTransport(messages.operationFailed),\n );\n const store = createInboxStore(api);\n const webPush = config.webPush ?? {};\n const subscribe = config.subscribe;\n const subscribeOption = {\n ...(subscribe ? { subscribe } : {}),\n ...(config.useSignal ? { useSignal: config.useSignal } : {}),\n };\n\n const Bell: ComponentType<BellButtonProps> = (props) => (\n <BellButton {...props} store={store} messages={messages} {...subscribeOption} />\n );\n const Panel = lazyNotificationsPanel({ store, messages });\n\n function useBoundUnreadCount(options: { enabled?: boolean } = {}): number {\n return useUnreadCount(store, { ...options, ...subscribeOption });\n }\n\n function BellWithPanel({\n enabled = true,\n onNavigate,\n }: {\n enabled?: boolean;\n onNavigate?: (link: string) => void;\n }): JSX.Element {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Bell enabled={enabled} onClick={() => setOpen(true)} />\n <Panel\n open={open}\n onClose={() => setOpen(false)}\n {...(onNavigate ? { onNavigate } : {})}\n />\n </>\n );\n }\n\n return {\n page: lazyPreferencesPage({ api, messages, webPush }),\n BellButton: Bell,\n Panel,\n BellWithPanel,\n useUnreadCount: useBoundUnreadCount,\n store,\n api,\n messages,\n };\n}\n","/**\n * Bare bell trigger with the live unread badge — for hosts that do not already\n * have a styled icon-button slot. A host with its own trigger chrome uses\n * `useUnreadCount` + `Panel` directly.\n */\nimport type { JSX } from 'react';\n\nimport { Badge } from '@12-apps/ui/data-display/Badge';\nimport { Box } from '@12-apps/ui/mui/Box';\n\nimport type { NotificationMessages } from '../messages';\n\nimport { BellIcon } from './bell-icon';\nimport { useUnreadCount, type NotificationsSignalHook, type NotificationsSubscribe } from './hooks';\nimport type { InboxStore } from './inbox-state';\n\nconst triggerSx = {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n p: 0.5,\n border: 'none',\n background: 'none',\n cursor: 'pointer',\n color: 'text.primary',\n lineHeight: 0,\n '& *': { cursor: 'pointer' },\n '&:hover': { color: 'primary.main' },\n '&:focus-visible': {\n outline: '2px solid',\n outlineColor: 'primary.main',\n outlineOffset: '2px',\n borderRadius: '50%',\n },\n} as const;\n\nexport interface BellButtonProps {\n onClick: () => void;\n /** Signed-out hosts still mount the bell; `false` silences it. */\n enabled?: boolean;\n}\n\nexport function BellButton({\n onClick,\n enabled = true,\n store,\n messages,\n subscribe,\n useSignal,\n}: BellButtonProps & {\n store: InboxStore;\n messages: NotificationMessages;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n}): JSX.Element {\n const count = useUnreadCount(store, {\n enabled,\n ...(subscribe ? { subscribe } : {}),\n ...(useSignal ? { useSignal } : {}),\n });\n return (\n <Box\n component=\"button\"\n type=\"button\"\n onClick={onClick}\n aria-label={count > 0 ? messages.openBellWithUnread(count) : messages.openBell}\n data-testid=\"notifications-bell\"\n sx={triggerSx}\n >\n <Badge\n content={count > 0 ? count : undefined}\n color=\"primary\"\n variant=\"count\"\n max={99}\n data-testid=\"notifications-badge\"\n >\n <BellIcon size={28} />\n </Badge>\n </Box>\n );\n}\n","/**\n * The inbox slide-over, fetched the first time somebody opens it.\n *\n * The bell and the panel are a PAIR a host drops into its chrome, and that is\n * still true — but only the BELL is on screen when a page paints. The panel is\n * behind a tap, and a static import made every host pay for it up front: the\n * design-system `Drawer` and, through it, MUI's `SwipeableDrawer`, `Modal`,\n * `Slide` and the focus trap, plus the row, the empty state and the pager. On a\n * storefront that is a slide-over most visits never open, parsed before the\n * first screen can render.\n *\n * ## Why the gate is \"ever opened\" rather than `open`\n *\n * `lazy` fetches when a component first RENDERS, so a boundary that still\n * rendered the panel while closed would fetch immediately and buy nothing. This\n * renders `null` until the panel has been open once, which is what actually\n * defers the download to the tap.\n *\n * And once opened it STAYS mounted. Unmounting on close would throw away the\n * drawer's transition state, so the panel would vanish instead of sliding out,\n * and the entrance animation would re-run on every reopen — which someone\n * working through an inbox does repeatedly. The fetch happens once.\n *\n * The initial state reads `open` rather than starting at `false`, so a host that\n * mounts the panel already open renders it in the same commit instead of a frame\n * later.\n *\n * ## Why `null` for the fallback\n *\n * The only frame this can show anything is the one right after the tap, where a\n * spinner reads as a stall rather than as progress. The chunk is small and\n * same-origin.\n */\nimport { Suspense, lazy, useEffect, useState, type ComponentType, type JSX } from 'react';\n\nimport type { NotificationMessages } from '../messages';\n\nimport type { InboxStore } from './inbox-state';\nimport type { NotificationsPanelProps } from './panel';\n\n/** What the factory binds into the panel, and the host never passes. */\ninterface PanelParts {\n store: InboxStore;\n messages: NotificationMessages;\n}\n\nexport function lazyNotificationsPanel(\n parts: PanelParts,\n): ComponentType<NotificationsPanelProps> {\n const Bound = lazy(async () => {\n const { NotificationsPanel } = await import('./panel');\n return {\n default: (props: NotificationsPanelProps): JSX.Element => (\n <NotificationsPanel {...props} {...parts} />\n ),\n };\n });\n\n return function NotificationsPanelSlot(props: NotificationsPanelProps): JSX.Element | null {\n const [everOpened, setEverOpened] = useState(props.open);\n\n useEffect(() => {\n if (props.open) setEverOpened(true);\n }, [props.open]);\n\n if (!everOpened) return null;\n\n return (\n <Suspense fallback={null}>\n <Bound {...props} />\n </Suspense>\n );\n };\n}\n","/**\n * The routed preferences screen, fetched when a host actually routes to it.\n *\n * `createWebNotifications` returns two different KINDS of thing, and its own\n * docstring says so: `page` is \"the standalone surface … the one thing a host\n * routes to\", while the bell and the panel \"are a PAIR a host drops into its own\n * chrome\". Chrome is on screen from the first paint; a routed surface is not.\n *\n * A static import made that distinction invisible to a bundler. Every host that\n * put the bell in its header also shipped the preferences matrix — its channel\n * toggles, the per-browser push enable step, and the design-system `Switch`\n * behind them — in the same chunk as the header. A storefront paid for a\n * settings screen a shopper never opens, before its first screen could render;\n * a host that renders its OWN preferences page paid for this one twice.\n *\n * So `page` now loads on demand. Nothing else moves: the bell, the panel and\n * `BellWithPanel` stay exactly as eager as the chrome they belong to, because\n * that is what they are.\n *\n * NO PREFETCH, deliberately, and this is the opposite call from a surface a\n * host opens from chrome it already has. A routed surface is reached by\n * NAVIGATION, and every host here already code-splits its routes — so the\n * fetch happens while the route is being entered, which is the moment a\n * prefetch would have been trying to anticipate. Warming it at factory time\n * would put the screen back on the boot path of every app, which is the whole\n * cost this removes.\n */\nimport { Suspense, lazy, type ComponentType, type JSX } from 'react';\n\nimport type { NotificationMessages } from '../messages';\n\nimport type { NotificationsApiClient } from './api';\nimport type { PreferencesScreenProps } from './preferences-screen';\nimport type { WebPushSetupConfig } from './web-push-setup';\n\n/** What the factory binds into the screen, and the host never passes. */\ninterface PreferencesPageParts {\n api: NotificationsApiClient;\n messages: NotificationMessages;\n webPush: WebPushSetupConfig;\n}\n\n/**\n * The routed screen, bound and loaded on first render.\n *\n * `lazy` memoises its factory, so the binding below happens once however many\n * times a host mounts the page — the same guarantee the direct call gave.\n *\n * The fallback is `null` because a host routes to this: whatever it renders\n * around the route is already on screen, and a second spinner inside it would\n * be one more thing appearing and disappearing during a navigation the host is\n * already indicating.\n */\nexport function lazyPreferencesPage(\n parts: PreferencesPageParts,\n): ComponentType<PreferencesScreenProps> {\n const Bound = lazy(async () => {\n const { PreferencesScreen } = await import('./preferences-screen');\n return {\n default: (props: PreferencesScreenProps): JSX.Element => (\n <PreferencesScreen {...props} {...parts} />\n ),\n };\n });\n\n return function NotificationsPreferencesPage(props: PreferencesScreenProps): JSX.Element {\n return (\n <Suspense fallback={null}>\n <Bound {...props} />\n </Suspense>\n );\n };\n}\n"],"mappings":"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","names":["useState","jsx","Suspense","lazy","jsx","lazy","Suspense","jsx","useState"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-7QVYU63E.js";
|
|
4
|
+
|
|
5
|
+
// src/react/relative-time.ts
|
|
6
|
+
function relativeTime(iso, messages) {
|
|
7
|
+
const elapsedMs = Date.now() - new Date(iso).getTime();
|
|
8
|
+
const minutes = Math.round(elapsedMs / 6e4);
|
|
9
|
+
if (minutes < 1) return messages.justNow;
|
|
10
|
+
if (minutes < 60) return messages.minutesAgo(minutes);
|
|
11
|
+
const hours = Math.round(minutes / 60);
|
|
12
|
+
if (hours < 24) return messages.hoursAgo(hours);
|
|
13
|
+
const days = Math.round(hours / 24);
|
|
14
|
+
if (days < 7) return messages.daysAgo(days);
|
|
15
|
+
return new Date(iso).toLocaleDateString(messages.dateLocale);
|
|
16
|
+
}
|
|
17
|
+
__name(relativeTime, "relativeTime");
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
relativeTime
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=chunk-O5BVUXPO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/relative-time.ts"],"sourcesContent":["import type { NotificationMessages } from '../messages';\n\n/**\n * \"há 5 min\"-style relative timestamp, falling back to an absolute date for\n * anything older than a week. Every word comes from the messages table, so a\n * host in another locale changes the copy and the locale together.\n */\nexport function relativeTime(iso: string, messages: NotificationMessages): string {\n const elapsedMs = Date.now() - new Date(iso).getTime();\n const minutes = Math.round(elapsedMs / 60_000);\n if (minutes < 1) return messages.justNow;\n if (minutes < 60) return messages.minutesAgo(minutes);\n const hours = Math.round(minutes / 60);\n if (hours < 24) return messages.hoursAgo(hours);\n const days = Math.round(hours / 24);\n if (days < 7) return messages.daysAgo(days);\n return new Date(iso).toLocaleDateString(messages.dateLocale);\n}\n"],"mappings":";;;;;AAOO,SAAS,aAAa,KAAa,UAAwC;AAChF,QAAM,YAAY,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,EAAE,QAAQ;AACrD,QAAM,UAAU,KAAK,MAAM,YAAY,GAAM;AAC7C,MAAI,UAAU,EAAG,QAAO,SAAS;AACjC,MAAI,UAAU,GAAI,QAAO,SAAS,WAAW,OAAO;AACpD,QAAM,QAAQ,KAAK,MAAM,UAAU,EAAE;AACrC,MAAI,QAAQ,GAAI,QAAO,SAAS,SAAS,KAAK;AAC9C,QAAM,OAAO,KAAK,MAAM,QAAQ,EAAE;AAClC,MAAI,OAAO,EAAG,QAAO,SAAS,QAAQ,IAAI;AAC1C,SAAO,IAAI,KAAK,GAAG,EAAE,mBAAmB,SAAS,UAAU;AAC7D;AAVgB;","names":[]}
|
package/dist/{create-api-notifications-B3u6Kx3x.d.ts → create-api-notifications-WaexrZnn.d.ts}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { N as NotificationGeneratorRegistry } from './generators-
|
|
2
|
-
import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, L as ListNotificationsResult, h as NotificationsCopySource, c as NotificationWireMessages } from './wire-
|
|
3
|
-
import { b as NotificationChannel, D as DeliveryStatus,
|
|
4
|
-
import { e as WebPushSubscriptionSource, D as DriverDeclarationBase, a as WebPushDriverDeclaration, c as WebPushSender } from './web-push-
|
|
1
|
+
import { N as NotificationGeneratorRegistry } from './generators-CH5m8Ucj.js';
|
|
2
|
+
import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, L as ListNotificationsResult, h as NotificationsCopySource, c as NotificationWireMessages } from './wire-CmRrB3_s.js';
|
|
3
|
+
import { b as NotificationChannel, D as DeliveryStatus, g as NotificationLogger, j as NotificationTransport, c as NotificationContent, a as NotificationCategory, d as NotificationEvent, f as NotificationGenerator } from './types-CYJN1xCM.js';
|
|
4
|
+
import { e as WebPushSubscriptionSource, D as DriverDeclarationBase, a as WebPushDriverDeclaration, c as WebPushSender } from './web-push-iER5StBm.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The database seam (12-15) — the exact, narrow slice of a Prisma-shaped
|
|
@@ -306,11 +306,19 @@ type NotificationsDbProvider = () => Promise<NotificationsDb>;
|
|
|
306
306
|
* Returning `null` for a user id means "no such recipient", which `notify`
|
|
307
307
|
* treats as a caller bug and throws on — a notification addressed to nobody is
|
|
308
308
|
* never silently dropped.
|
|
309
|
+
*
|
|
310
|
+
* `locale` is the recipient's own language, and it is here rather than on the
|
|
311
|
+
* event because it is a fact about the PERSON, not about what happened
|
|
312
|
+
* The host owns it for the same reason it owns the address: this
|
|
313
|
+
* package has no user table to read it from. Absent — the field omitted, or
|
|
314
|
+
* `null` for a host that stores no language yet — means "nobody said", and
|
|
315
|
+
* every generator answers with its default exactly as before.
|
|
309
316
|
*/
|
|
310
317
|
interface NotificationContactDirectory {
|
|
311
318
|
getContact(userId: string): Promise<{
|
|
312
319
|
email: string | null;
|
|
313
320
|
phone: string | null;
|
|
321
|
+
locale?: string | null;
|
|
314
322
|
} | null>;
|
|
315
323
|
}
|
|
316
324
|
|
package/dist/{create-web-notifications-BpNR8qH3.d.ts → create-web-notifications-DrgqtU4Q.d.ts}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { JSX, ComponentType } from 'react';
|
|
2
|
-
import { L as ListNotificationsResult, a as ChannelRow, I as InboxNotification, N as NotificationMessages } from './wire-
|
|
3
|
-
import { b as NotificationChannel } from './types-
|
|
2
|
+
import { L as ListNotificationsResult, a as ChannelRow, I as InboxNotification, N as NotificationMessages } from './wire-CmRrB3_s.js';
|
|
3
|
+
import { b as NotificationChannel } from './types-CYJN1xCM.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* How the notification screens reach their data (12-15) — the report-builder
|
|
@@ -324,11 +324,22 @@ interface NotificationsWebConfig {
|
|
|
324
324
|
webPush?: WebPushSetupConfig;
|
|
325
325
|
}
|
|
326
326
|
interface WebNotifications {
|
|
327
|
-
/**
|
|
327
|
+
/**
|
|
328
|
+
* The routed surface: the preferences screen.
|
|
329
|
+
*
|
|
330
|
+
* Loaded on demand — see `page-lazy.tsx`. A host that mounts only the bell and
|
|
331
|
+
* the panel never downloads it, and a host that routes to it fetches it while
|
|
332
|
+
* entering that route.
|
|
333
|
+
*/
|
|
328
334
|
page: ComponentType<PreferencesScreenProps>;
|
|
329
335
|
/** The bell, already bound to the shared store. */
|
|
330
336
|
BellButton: ComponentType<BellButtonProps>;
|
|
331
|
-
/**
|
|
337
|
+
/**
|
|
338
|
+
* The inbox slide-over, sharing that store.
|
|
339
|
+
*
|
|
340
|
+
* Loaded the first time it is opened — see `panel-lazy.tsx`. Until then a
|
|
341
|
+
* host's chrome carries the bell and nothing else.
|
|
342
|
+
*/
|
|
332
343
|
Panel: ComponentType<NotificationsPanelProps>;
|
|
333
344
|
/**
|
|
334
345
|
* Bell + panel as ONE element, for a host that just wants the feature in its
|
package/dist/hono/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Hono, Context } from 'hono';
|
|
2
|
-
import { A as ApiNotifications, N as NotificationsServerConfig, a as NotificationsActor } from '../create-api-notifications-
|
|
3
|
-
import '../generators-
|
|
4
|
-
import '../types-
|
|
5
|
-
import '../wire-
|
|
6
|
-
import '../web-push-
|
|
2
|
+
import { A as ApiNotifications, N as NotificationsServerConfig, a as NotificationsActor } from '../create-api-notifications-WaexrZnn.js';
|
|
3
|
+
import '../generators-CH5m8Ucj.js';
|
|
4
|
+
import '../types-CYJN1xCM.js';
|
|
5
|
+
import '../wire-CmRrB3_s.js';
|
|
6
|
+
import '../web-push-iER5StBm.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* `@12-apps/notifications/hono` — the account notification endpoints as a
|
package/dist/hono/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createApiNotifications
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-7234ROOP.js";
|
|
4
|
+
import "../chunk-GGAZOLNN.js";
|
|
5
|
+
import "../chunk-BGI4BQ66.js";
|
|
5
6
|
import {
|
|
6
7
|
messagesOf
|
|
7
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-M2TVBVH2.js";
|
|
8
9
|
import {
|
|
9
10
|
__name
|
|
10
11
|
} from "../chunk-7QVYU63E.js";
|
package/dist/hono/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hono/index.ts"],"sourcesContent":["import { Hono } from 'hono';\nimport type { Context } from 'hono';\n\nimport { messagesOf } from '../messages';\n\nimport {\n createApiNotifications,\n type ApiNotifications,\n type NotificationsServerConfig,\n} from '../server/create-api-notifications';\nimport type { NotificationsActor } from '../server/context';\n\n/**\n * `@12-apps/notifications/hono` — the account notification endpoints as a\n * mountable router.\n *\n * The framework-neutral descriptors in `/server` are the contract; this is the\n * adapter for the framework we happen to use, behind its own subpath with\n * `hono` as an OPTIONAL peer (the report-builder precedent — a host on Express,\n * or one that only wants the React surface, never resolves Hono).\n *\n * A host writes:\n *\n * const notifications = notificationsRouter({ …config, resolveActor });\n * app.route('/api/account', notifications.router);\n *\n * and keeps what is genuinely its own: who the caller is. Everything after\n * that — parsing, status codes, the envelope, the pt-BR copy — is the\n * package's.\n */\n\n/**\n * Resolve the caller. Returning `null` means unauthenticated, which answers 401\n * before any handler runs.\n *\n * Note the 401 is self-guarded HERE rather than assumed from middleware: these\n * paths sit under an API prefix that a host's page middleware typically does not\n * match, and an unauthenticated inbox read that fell through would answer\n * somebody else's rows or none at all — both worse than a 401.\n */\nexport type ResolveNotificationsActor = (\n c: Context,\n) => Promise<NotificationsActor | null> | NotificationsActor | null;\n\nexport interface NotificationsHonoConfig extends NotificationsServerConfig {\n resolveActor: ResolveNotificationsActor;\n}\n\nexport interface NotificationsHono extends ApiNotifications {\n router: Hono;\n}\n\n/**\n * Reads the JSON body, tolerating an absent or malformed one — and only when\n * the caller SAID it was JSON.\n *\n * The content-type check is a CSRF speed bump, not a defence (see ADOPTING rule\n * 13, which names the actual one). `text/plain`, `multipart/form-data` and\n * `application/x-www-form-urlencoded` are the three types a cross-site `fetch`\n * or a plain `<form>` can send with NO preflight, so parsing a body regardless\n * of its type is what lets such a request reach these handlers at all. Refusing\n * them means a cross-site write has to earn a preflight first, which the browser\n * will then refuse on its own. The price is nil: every client of this surface,\n * the packaged one included, sends `application/json`.\n */\nfunction saysJson(c: Context): boolean {\n const type = c.req.header('content-type');\n if (!type) return false;\n const mime = (type.split(';')[0] ?? '').trim().toLowerCase();\n return mime === 'application/json' || mime.endsWith('+json');\n}\n\nasync function readBody(c: Context): Promise<unknown> {\n if (c.req.method === 'GET') return undefined;\n if (!saysJson(c)) return undefined;\n try {\n return await c.req.json();\n } catch {\n // A malformed body is the caller's error; the handler's own validation\n // reports it far better than a parse failure would.\n return undefined;\n }\n}\n\nexport function notificationsRouter(config: NotificationsHonoConfig): NotificationsHono {\n const api = createApiNotifications(config);\n const messages = messagesOf(config);\n const router = new Hono();\n\n // Mounted IN DESCRIPTOR ORDER, which any adapter must preserve. Hono resolves\n // by registration order, so a host route shaped `/notifications/:id` under the\n // same prefix must be registered AFTER this router or it captures\n // `/notifications/unread-count`.\n for (const route of api.routes) {\n const handler = async (c: Context): Promise<Response> => {\n const actor = await config.resolveActor(c);\n if (!actor) return c.json({ error: messages.unauthenticated }, 401);\n\n const response = await route.handle({\n actor,\n params: c.req.param() as Record<string, string | undefined>,\n query: c.req.query() as Record<string, string | undefined>,\n body: await readBody(c),\n headers: { 'user-agent': c.req.header('user-agent') },\n });\n\n // A handler that chose NO body means exactly that (204).\n if (response.body === undefined) return c.body(null, response.status as 204);\n // The status travels with the body the handler chose; the adapter never\n // reinterprets either.\n return c.json(response.body as Record<string, unknown>, response.status as 200);\n };\n\n if (route.method === 'GET') router.get(route.path, handler);\n else if (route.method === 'POST') router.post(route.path, handler);\n else if (route.method === 'PUT') router.put(route.path, handler);\n else router.delete(route.path, handler);\n }\n\n return { ...api, router };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/hono/index.ts"],"sourcesContent":["import { Hono } from 'hono';\nimport type { Context } from 'hono';\n\nimport { messagesOf } from '../messages';\n\nimport {\n createApiNotifications,\n type ApiNotifications,\n type NotificationsServerConfig,\n} from '../server/create-api-notifications';\nimport type { NotificationsActor } from '../server/context';\n\n/**\n * `@12-apps/notifications/hono` — the account notification endpoints as a\n * mountable router.\n *\n * The framework-neutral descriptors in `/server` are the contract; this is the\n * adapter for the framework we happen to use, behind its own subpath with\n * `hono` as an OPTIONAL peer (the report-builder precedent — a host on Express,\n * or one that only wants the React surface, never resolves Hono).\n *\n * A host writes:\n *\n * const notifications = notificationsRouter({ …config, resolveActor });\n * app.route('/api/account', notifications.router);\n *\n * and keeps what is genuinely its own: who the caller is. Everything after\n * that — parsing, status codes, the envelope, the pt-BR copy — is the\n * package's.\n */\n\n/**\n * Resolve the caller. Returning `null` means unauthenticated, which answers 401\n * before any handler runs.\n *\n * Note the 401 is self-guarded HERE rather than assumed from middleware: these\n * paths sit under an API prefix that a host's page middleware typically does not\n * match, and an unauthenticated inbox read that fell through would answer\n * somebody else's rows or none at all — both worse than a 401.\n */\nexport type ResolveNotificationsActor = (\n c: Context,\n) => Promise<NotificationsActor | null> | NotificationsActor | null;\n\nexport interface NotificationsHonoConfig extends NotificationsServerConfig {\n resolveActor: ResolveNotificationsActor;\n}\n\nexport interface NotificationsHono extends ApiNotifications {\n router: Hono;\n}\n\n/**\n * Reads the JSON body, tolerating an absent or malformed one — and only when\n * the caller SAID it was JSON.\n *\n * The content-type check is a CSRF speed bump, not a defence (see ADOPTING rule\n * 13, which names the actual one). `text/plain`, `multipart/form-data` and\n * `application/x-www-form-urlencoded` are the three types a cross-site `fetch`\n * or a plain `<form>` can send with NO preflight, so parsing a body regardless\n * of its type is what lets such a request reach these handlers at all. Refusing\n * them means a cross-site write has to earn a preflight first, which the browser\n * will then refuse on its own. The price is nil: every client of this surface,\n * the packaged one included, sends `application/json`.\n */\nfunction saysJson(c: Context): boolean {\n const type = c.req.header('content-type');\n if (!type) return false;\n const mime = (type.split(';')[0] ?? '').trim().toLowerCase();\n return mime === 'application/json' || mime.endsWith('+json');\n}\n\nasync function readBody(c: Context): Promise<unknown> {\n if (c.req.method === 'GET') return undefined;\n if (!saysJson(c)) return undefined;\n try {\n return await c.req.json();\n } catch {\n // A malformed body is the caller's error; the handler's own validation\n // reports it far better than a parse failure would.\n return undefined;\n }\n}\n\nexport function notificationsRouter(config: NotificationsHonoConfig): NotificationsHono {\n const api = createApiNotifications(config);\n const messages = messagesOf(config);\n const router = new Hono();\n\n // Mounted IN DESCRIPTOR ORDER, which any adapter must preserve. Hono resolves\n // by registration order, so a host route shaped `/notifications/:id` under the\n // same prefix must be registered AFTER this router or it captures\n // `/notifications/unread-count`.\n for (const route of api.routes) {\n const handler = async (c: Context): Promise<Response> => {\n const actor = await config.resolveActor(c);\n if (!actor) return c.json({ error: messages.unauthenticated }, 401);\n\n const response = await route.handle({\n actor,\n params: c.req.param() as Record<string, string | undefined>,\n query: c.req.query() as Record<string, string | undefined>,\n body: await readBody(c),\n headers: { 'user-agent': c.req.header('user-agent') },\n });\n\n // A handler that chose NO body means exactly that (204).\n if (response.body === undefined) return c.body(null, response.status as 204);\n // The status travels with the body the handler chose; the adapter never\n // reinterprets either.\n return c.json(response.body as Record<string, unknown>, response.status as 200);\n };\n\n if (route.method === 'GET') router.get(route.path, handler);\n else if (route.method === 'POST') router.post(route.path, handler);\n else if (route.method === 'PUT') router.put(route.path, handler);\n else router.delete(route.path, handler);\n }\n\n return { ...api, router };\n}\n"],"mappings":";;;;;;;;;;;;;AAAA,SAAS,YAAY;AAiErB,SAAS,SAAS,GAAqB;AACrC,QAAM,OAAO,EAAE,IAAI,OAAO,cAAc;AACxC,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,QAAQ,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,KAAK,EAAE,YAAY;AAC3D,SAAO,SAAS,sBAAsB,KAAK,SAAS,OAAO;AAC7D;AALS;AAOT,eAAe,SAAS,GAA8B;AACpD,MAAI,EAAE,IAAI,WAAW,MAAO,QAAO;AACnC,MAAI,CAAC,SAAS,CAAC,EAAG,QAAO;AACzB,MAAI;AACF,WAAO,MAAM,EAAE,IAAI,KAAK;AAAA,EAC1B,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;AAVe;AAYR,SAAS,oBAAoB,QAAoD;AACtF,QAAM,MAAM,uBAAuB,MAAM;AACzC,QAAM,WAAW,WAAW,MAAM;AAClC,QAAM,SAAS,IAAI,KAAK;AAMxB,aAAW,SAAS,IAAI,QAAQ;AAC9B,UAAM,UAAU,8BAAO,MAAkC;AACvD,YAAM,QAAQ,MAAM,OAAO,aAAa,CAAC;AACzC,UAAI,CAAC,MAAO,QAAO,EAAE,KAAK,EAAE,OAAO,SAAS,gBAAgB,GAAG,GAAG;AAElE,YAAM,WAAW,MAAM,MAAM,OAAO;AAAA,QAClC;AAAA,QACA,QAAQ,EAAE,IAAI,MAAM;AAAA,QACpB,OAAO,EAAE,IAAI,MAAM;AAAA,QACnB,MAAM,MAAM,SAAS,CAAC;AAAA,QACtB,SAAS,EAAE,cAAc,EAAE,IAAI,OAAO,YAAY,EAAE;AAAA,MACtD,CAAC;AAGD,UAAI,SAAS,SAAS,OAAW,QAAO,EAAE,KAAK,MAAM,SAAS,MAAa;AAG3E,aAAO,EAAE,KAAK,SAAS,MAAiC,SAAS,MAAa;AAAA,IAChF,GAjBgB;AAmBhB,QAAI,MAAM,WAAW,MAAO,QAAO,IAAI,MAAM,MAAM,OAAO;AAAA,aACjD,MAAM,WAAW,OAAQ,QAAO,KAAK,MAAM,MAAM,OAAO;AAAA,aACxD,MAAM,WAAW,MAAO,QAAO,IAAI,MAAM,MAAM,OAAO;AAAA,QAC1D,QAAO,OAAO,MAAM,MAAM,OAAO;AAAA,EACxC;AAEA,SAAO,EAAE,GAAG,KAAK,OAAO;AAC1B;AApCgB;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { D as DeliveryStatus, N as NOTIFICATION_CHANNELS, a as NotificationCategory, b as NotificationChannel, c as NotificationContent, d as NotificationEvent, e as
|
|
2
|
-
export { N as NotificationGeneratorRegistry, c as createGeneratorRegistry } from './generators-
|
|
3
|
-
export { C as ChannelMatrix, a as ChannelRow, D as DEFAULT_CHANNEL_ROW, I as InboxNotification, L as ListNotificationsResult, N as NotificationMessages, b as NotificationRow, c as NotificationWireMessages, d as defaultChannelMatrix, e as enabledChannelsOf, i as inboxWire, m as mergeChoices, f as mergeStoredRow, g as messagesOf } from './wire-
|
|
1
|
+
export { D as DeliveryStatus, N as NOTIFICATION_CHANNELS, a as NotificationCategory, b as NotificationChannel, c as NotificationContent, d as NotificationEvent, e as NotificationGenerateContext, f as NotificationGenerator, g as NotificationLogger, h as NotificationRecipient, i as NotificationTaxonomy, j as NotificationTransport, T as TransportRecipient, t as taxonomyOf } from './types-CYJN1xCM.js';
|
|
2
|
+
export { N as NotificationGeneratorRegistry, c as createGeneratorRegistry } from './generators-CH5m8Ucj.js';
|
|
3
|
+
export { C as ChannelMatrix, a as ChannelRow, D as DEFAULT_CHANNEL_ROW, I as InboxNotification, L as ListNotificationsResult, N as NotificationMessages, b as NotificationRow, c as NotificationWireMessages, d as defaultChannelMatrix, e as enabledChannelsOf, i as inboxWire, m as mergeChoices, f as mergeStoredRow, g as messagesOf } from './wire-CmRrB3_s.js';
|
|
4
4
|
|
|
5
5
|
/** Thrown by `notify` when no generator is registered for the event type. */
|
|
6
6
|
declare class UnknownNotificationTypeError extends Error {
|
package/dist/index.js
CHANGED
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
mergeChoices,
|
|
10
10
|
mergeStoredRow,
|
|
11
11
|
normalizePhoneE164
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-GGAZOLNN.js";
|
|
13
13
|
import {
|
|
14
14
|
NOTIFICATION_CHANNELS,
|
|
15
|
-
messagesOf,
|
|
16
15
|
taxonomyOf
|
|
17
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-BGI4BQ66.js";
|
|
17
|
+
import {
|
|
18
|
+
messagesOf
|
|
19
|
+
} from "./chunk-M2TVBVH2.js";
|
|
18
20
|
import "./chunk-7QVYU63E.js";
|
|
19
21
|
export {
|
|
20
22
|
DEFAULT_CHANNEL_ROW,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WireJobBlueprint } from '@12-apps/wiring';
|
|
2
|
-
import { A as ApiNotifications } from './create-api-notifications-
|
|
2
|
+
import { A as ApiNotifications } from './create-api-notifications-WaexrZnn.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The two background jobs getting a message out actually needs.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as _12_apps_wiring from '@12-apps/wiring';
|
|
2
2
|
import { WireRequest } from '@12-apps/wiring';
|
|
3
|
-
import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-
|
|
4
|
-
import { N as NotificationsJobDeps } from '../jobs-
|
|
5
|
-
import '../generators-
|
|
6
|
-
import '../types-
|
|
7
|
-
import '../wire-
|
|
8
|
-
import '../web-push-
|
|
3
|
+
import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-WaexrZnn.js';
|
|
4
|
+
import { N as NotificationsJobDeps } from '../jobs-CcsSA0YR.js';
|
|
5
|
+
import '../generators-CH5m8Ucj.js';
|
|
6
|
+
import '../types-CYJN1xCM.js';
|
|
7
|
+
import '../wire-CmRrB3_s.js';
|
|
8
|
+
import '../web-push-iER5StBm.js';
|
|
9
9
|
|
|
10
10
|
/** One `NotificationsRoute` as the wiring contract reads it. */
|
|
11
11
|
declare function asWireRoute(route: NotificationsRoute): {
|
package/dist/manifest/server.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NOTIFICATIONS_JOBS
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-KU4FVOFA.js";
|
|
4
4
|
import {
|
|
5
5
|
createApiNotifications
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-7234ROOP.js";
|
|
7
|
+
import "../chunk-GGAZOLNN.js";
|
|
8
|
+
import "../chunk-BGI4BQ66.js";
|
|
9
|
+
import "../chunk-M2TVBVH2.js";
|
|
9
10
|
import {
|
|
10
11
|
__name
|
|
11
12
|
} from "../chunk-7QVYU63E.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/manifest/server.ts"],"sourcesContent":["/**\n * `@12-apps/notifications/manifest/server` — the server capabilities.\n *\n * `http.create` wraps `createApiNotifications` in a WIRE VIEW, and the reason\n * is one field. `NotificationsRequest` carries `headers` — the contract's\n * `WireRequest` does not, because headers are the adapter's business\n * everywhere else — and exactly one descriptor reads it: push-subscribe takes\n * `user-agent` as the DEVICE HINT it labels a subscription with. Without the\n * view the field would simply be absent at runtime while still type-checking,\n * and every saved device would come back unnamed: a silent quality loss, the\n * failure mode the wiring contract exists to convert into a loud one.\n *\n * So the view derives `headers` from the raw request the contract already\n * carries for the handlers `params`/`query`/`body` cannot serve. A host whose\n * adapter leaves `request` unset still gets a working surface — every route\n * answers, the subscription saves — with an unnamed device, which is the\n * honest degradation for a hint. `@12-apps/notifications/hono` populates it.\n *\n * Everything else rides beside the mapped routes on the aggregate unchanged:\n * `notify`, `notifyByPermission`, `dispatchDeliveries`, `drainPending`, the\n * three stores, `registerGenerator` and the transports registry. A host still\n * calls those directly — being mounted does not make the emit front door stop\n * being a library.\n */\n\nimport type { AnyServerManifest, WireRequest } from '@12-apps/wiring';\n\nimport {\n createApiNotifications,\n NOTIFICATIONS_JOBS,\n type ApiNotifications,\n type NotificationsRoute,\n type NotificationsServerConfig,\n} from '../server';\n\n/** The header names this surface reads — the device hint, and nothing else. */\nconst READ_HEADERS = ['user-agent'] as const;\n\n/** The headers the package expects, taken off the raw request when there is one. */\nfunction headersOf(request: WireRequest<never>): Record<string, string | undefined> {\n const raw = request.request;\n if (!raw) return {};\n return Object.fromEntries(\n READ_HEADERS.map((name) => [name, raw.headers.get(name) ?? undefined]),\n );\n}\n\n/** One `NotificationsRoute` as the wiring contract reads it. */\nfunction asWireRoute(route: NotificationsRoute): {\n method: NotificationsRoute['method'];\n path: string;\n handle(request: WireRequest<never>): Promise<{ status: number; body: unknown }>;\n} {\n return {\n method: route.method,\n path: route.path,\n handle: (request) =>\n route.handle({\n actor: request.actor,\n params: request.params,\n query: request.query,\n body: request.body,\n headers: headersOf(request),\n }),\n };\n}\n\n/** `createApiNotifications`, its routes re-shaped for the aggregate. */\nexport function createWireApiNotifications(\n config: NotificationsServerConfig,\n): Omit<ApiNotifications, 'routes'> & { routes: ReturnType<typeof asWireRoute>[] } {\n const api = createApiNotifications(config);\n return { ...api, routes: api.routes.map(asWireRoute) };\n}\n\nexport const notificationsServerManifest = {\n name: '@12-apps/notifications',\n http: { create: createWireApiNotifications },\n /**\n * The dispatch fast path and the retry sweep, with their cadence. The host\n * binds `{ dispatchDeliveries, drainPending }` off its own mount — the two\n * methods the aggregate already hands it — and deletes the hand-rolled\n * copies. See `../server/jobs` for why the numbers are the package's.\n */\n jobs: NOTIFICATIONS_JOBS,\n} as const satisfies AnyServerManifest;\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/manifest/server.ts"],"sourcesContent":["/**\n * `@12-apps/notifications/manifest/server` — the server capabilities.\n *\n * `http.create` wraps `createApiNotifications` in a WIRE VIEW, and the reason\n * is one field. `NotificationsRequest` carries `headers` — the contract's\n * `WireRequest` does not, because headers are the adapter's business\n * everywhere else — and exactly one descriptor reads it: push-subscribe takes\n * `user-agent` as the DEVICE HINT it labels a subscription with. Without the\n * view the field would simply be absent at runtime while still type-checking,\n * and every saved device would come back unnamed: a silent quality loss, the\n * failure mode the wiring contract exists to convert into a loud one.\n *\n * So the view derives `headers` from the raw request the contract already\n * carries for the handlers `params`/`query`/`body` cannot serve. A host whose\n * adapter leaves `request` unset still gets a working surface — every route\n * answers, the subscription saves — with an unnamed device, which is the\n * honest degradation for a hint. `@12-apps/notifications/hono` populates it.\n *\n * Everything else rides beside the mapped routes on the aggregate unchanged:\n * `notify`, `notifyByPermission`, `dispatchDeliveries`, `drainPending`, the\n * three stores, `registerGenerator` and the transports registry. A host still\n * calls those directly — being mounted does not make the emit front door stop\n * being a library.\n */\n\nimport type { AnyServerManifest, WireRequest } from '@12-apps/wiring';\n\nimport {\n createApiNotifications,\n NOTIFICATIONS_JOBS,\n type ApiNotifications,\n type NotificationsRoute,\n type NotificationsServerConfig,\n} from '../server';\n\n/** The header names this surface reads — the device hint, and nothing else. */\nconst READ_HEADERS = ['user-agent'] as const;\n\n/** The headers the package expects, taken off the raw request when there is one. */\nfunction headersOf(request: WireRequest<never>): Record<string, string | undefined> {\n const raw = request.request;\n if (!raw) return {};\n return Object.fromEntries(\n READ_HEADERS.map((name) => [name, raw.headers.get(name) ?? undefined]),\n );\n}\n\n/** One `NotificationsRoute` as the wiring contract reads it. */\nfunction asWireRoute(route: NotificationsRoute): {\n method: NotificationsRoute['method'];\n path: string;\n handle(request: WireRequest<never>): Promise<{ status: number; body: unknown }>;\n} {\n return {\n method: route.method,\n path: route.path,\n handle: (request) =>\n route.handle({\n actor: request.actor,\n params: request.params,\n query: request.query,\n body: request.body,\n headers: headersOf(request),\n }),\n };\n}\n\n/** `createApiNotifications`, its routes re-shaped for the aggregate. */\nexport function createWireApiNotifications(\n config: NotificationsServerConfig,\n): Omit<ApiNotifications, 'routes'> & { routes: ReturnType<typeof asWireRoute>[] } {\n const api = createApiNotifications(config);\n return { ...api, routes: api.routes.map(asWireRoute) };\n}\n\nexport const notificationsServerManifest = {\n name: '@12-apps/notifications',\n http: { create: createWireApiNotifications },\n /**\n * The dispatch fast path and the retry sweep, with their cadence. The host\n * binds `{ dispatchDeliveries, drainPending }` off its own mount — the two\n * methods the aggregate already hands it — and deletes the hand-rolled\n * copies. See `../server/jobs` for why the numbers are the package's.\n */\n jobs: NOTIFICATIONS_JOBS,\n} as const satisfies AnyServerManifest;\n"],"mappings":";;;;;;;;;;;;;;AAoCA,IAAM,eAAe,CAAC,YAAY;AAGlC,SAAS,UAAU,SAAiE;AAClF,QAAM,MAAM,QAAQ;AACpB,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,SAAO,OAAO;AAAA,IACZ,aAAa,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,QAAQ,IAAI,IAAI,KAAK,MAAS,CAAC;AAAA,EACvE;AACF;AANS;AAST,SAAS,YAAY,OAInB;AACA,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,MAAM,MAAM;AAAA,IACZ,QAAQ,wBAAC,YACP,MAAM,OAAO;AAAA,MACX,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,MAChB,OAAO,QAAQ;AAAA,MACf,MAAM,QAAQ;AAAA,MACd,SAAS,UAAU,OAAO;AAAA,IAC5B,CAAC,GAPK;AAAA,EAQV;AACF;AAjBS;AAoBF,SAAS,2BACd,QACiF;AACjF,QAAM,MAAM,uBAAuB,MAAM;AACzC,SAAO,EAAE,GAAG,KAAK,QAAQ,IAAI,OAAO,IAAI,WAAW,EAAE;AACvD;AALgB;AAOT,IAAM,8BAA8B;AAAA,EACzC,MAAM;AAAA,EACN,MAAM,EAAE,QAAQ,2BAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO3C,MAAM;AACR;","names":[]}
|
package/dist/manifest/web.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { c as createWebNotifications } from '../create-web-notifications-
|
|
1
|
+
import { c as createWebNotifications } from '../create-web-notifications-DrgqtU4Q.js';
|
|
2
2
|
import 'react';
|
|
3
|
-
import '../wire-
|
|
4
|
-
import '../types-
|
|
3
|
+
import '../wire-CmRrB3_s.js';
|
|
4
|
+
import '../types-CYJN1xCM.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* `@12-apps/notifications/manifest/web` — the web capabilities.
|
package/dist/manifest/web.js
CHANGED
package/dist/manifest/web.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/manifest/web.ts"],"sourcesContent":["/**\n * `@12-apps/notifications/manifest/web` — the web capabilities.\n *\n * `surface.create` IS `createWebNotifications`, unchanged: the bell, the inbox\n * slide-over, the preferences screen and the bound store, built once per\n * adoption by the consumer's binder (the members are component TYPES, so\n * rebuilding per render unmounts the panel mid-interaction — the memoisation\n * rule every hand wiring carries as a comment today).\n *\n * ## Why this manifest exists now\n *\n * It was narrowed away with a reason that reads plausibly and is FALSE:\n * \"listing `web` would oblige every server host adopting this manifest to\n * answer for a React surface it never mounts.\" The consumer does not work\n * that way. A capability declared for the other runtime is reported\n * `out-of-scope` — \"a web host answers for this\" — and `assemble()` returns\n * fine; only a capability applicable to THIS runtime and unanswered is\n * `unbound`. `wiring`'s own fixture package declares both halves and its\n * server-host suite asserts exactly that.\n *\n * The narrowing therefore bought nothing and cost the thing the capability is\n * for. `./react` and `./web-push` ship Bell, Panel and Preferences — the exact\n * screens the adaptation report wanted to stop being hand-duplicated in hosts,\n * and the origin host duplicated the preferences screen and the push setup\n * anyway, because a manifest that never mentions them is a manifest nobody\n * discovers them from.\n *\n * ## Why there are no `areas`\n *\n * The bell is not a routed screen: it lives in a host's header chrome, beside\n * whatever else that host puts there, and no package can suggest a route for\n * it. Preferences IS routed, but where it belongs differs per host — under\n * account settings in one app, under a tenant's configuration in another — and\n * it is offered as `page` on the surface for the host to route at its own\n * call site. A suggested nav row would be wrong for every host but the first,\n * which is the `AreaContribution` doctrine's own test.\n */\n\nimport type { AnyWebManifest } from '@12-apps/wiring';\n\nimport { createWebNotifications } from '../react/create-web-notifications';\n\nexport const notificationsWebManifest = {\n name: '@12-apps/notifications',\n surface: { create: createWebNotifications },\n} as const satisfies AnyWebManifest;\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/manifest/web.ts"],"sourcesContent":["/**\n * `@12-apps/notifications/manifest/web` — the web capabilities.\n *\n * `surface.create` IS `createWebNotifications`, unchanged: the bell, the inbox\n * slide-over, the preferences screen and the bound store, built once per\n * adoption by the consumer's binder (the members are component TYPES, so\n * rebuilding per render unmounts the panel mid-interaction — the memoisation\n * rule every hand wiring carries as a comment today).\n *\n * ## Why this manifest exists now\n *\n * It was narrowed away with a reason that reads plausibly and is FALSE:\n * \"listing `web` would oblige every server host adopting this manifest to\n * answer for a React surface it never mounts.\" The consumer does not work\n * that way. A capability declared for the other runtime is reported\n * `out-of-scope` — \"a web host answers for this\" — and `assemble()` returns\n * fine; only a capability applicable to THIS runtime and unanswered is\n * `unbound`. `wiring`'s own fixture package declares both halves and its\n * server-host suite asserts exactly that.\n *\n * The narrowing therefore bought nothing and cost the thing the capability is\n * for. `./react` and `./web-push` ship Bell, Panel and Preferences — the exact\n * screens the adaptation report wanted to stop being hand-duplicated in hosts,\n * and the origin host duplicated the preferences screen and the push setup\n * anyway, because a manifest that never mentions them is a manifest nobody\n * discovers them from.\n *\n * ## Why there are no `areas`\n *\n * The bell is not a routed screen: it lives in a host's header chrome, beside\n * whatever else that host puts there, and no package can suggest a route for\n * it. Preferences IS routed, but where it belongs differs per host — under\n * account settings in one app, under a tenant's configuration in another — and\n * it is offered as `page` on the surface for the host to route at its own\n * call site. A suggested nav row would be wrong for every host but the first,\n * which is the `AreaContribution` doctrine's own test.\n */\n\nimport type { AnyWebManifest } from '@12-apps/wiring';\n\nimport { createWebNotifications } from '../react/create-web-notifications';\n\nexport const notificationsWebManifest = {\n name: '@12-apps/notifications',\n surface: { create: createWebNotifications },\n} as const satisfies AnyWebManifest;\n"],"mappings":";;;;;;;;AA0CO,IAAM,2BAA2B;AAAA,EACtC,MAAM;AAAA,EACN,SAAS,EAAE,QAAQ,uBAAuB;AAC5C;","names":[]}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
relativeTime
|
|
3
|
+
} from "./chunk-O5BVUXPO.js";
|
|
4
|
+
import {
|
|
5
|
+
BellIcon,
|
|
6
|
+
useInboxList
|
|
7
|
+
} from "./chunk-BW723CX2.js";
|
|
8
|
+
import {
|
|
9
|
+
__name
|
|
10
|
+
} from "./chunk-7QVYU63E.js";
|
|
11
|
+
|
|
12
|
+
// src/react/panel.tsx
|
|
13
|
+
import { useCallback } from "react";
|
|
14
|
+
import { EmptyState } from "@12-apps/ui/data-display/EmptyState";
|
|
15
|
+
import { LoadingState } from "@12-apps/ui/data-display/LoadingState";
|
|
16
|
+
import { Button as Button2 } from "@12-apps/ui/form/Button";
|
|
17
|
+
import { Drawer, DrawerContent, DrawerHeader } from "@12-apps/ui/layout/Drawer";
|
|
18
|
+
import { Box as Box2 } from "@12-apps/ui/mui/Box";
|
|
19
|
+
import { useMediaQuery } from "@12-apps/ui/mui/useMediaQuery";
|
|
20
|
+
import { useTheme } from "@12-apps/ui/mui/styles";
|
|
21
|
+
|
|
22
|
+
// src/react/row.tsx
|
|
23
|
+
import { Button } from "@12-apps/ui/form/Button";
|
|
24
|
+
import { Box } from "@12-apps/ui/mui/Box";
|
|
25
|
+
import { alpha } from "@12-apps/ui/mui/styles";
|
|
26
|
+
import { Text } from "@12-apps/ui/typography/Text";
|
|
27
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
28
|
+
var contentButtonSx = {
|
|
29
|
+
flex: 1,
|
|
30
|
+
minWidth: 0,
|
|
31
|
+
display: "flex",
|
|
32
|
+
flexDirection: "column",
|
|
33
|
+
gap: 0.25,
|
|
34
|
+
textAlign: "left",
|
|
35
|
+
border: "none",
|
|
36
|
+
background: "none",
|
|
37
|
+
p: 0,
|
|
38
|
+
cursor: "pointer",
|
|
39
|
+
color: "text.primary",
|
|
40
|
+
fontFamily: "inherit"
|
|
41
|
+
};
|
|
42
|
+
var unreadDotSx = {
|
|
43
|
+
width: 8,
|
|
44
|
+
height: 8,
|
|
45
|
+
borderRadius: "50%",
|
|
46
|
+
bgcolor: "primary.main",
|
|
47
|
+
flex: "0 0 auto"
|
|
48
|
+
};
|
|
49
|
+
function NotificationRow({
|
|
50
|
+
notification,
|
|
51
|
+
messages,
|
|
52
|
+
onOpen,
|
|
53
|
+
onDelete
|
|
54
|
+
}) {
|
|
55
|
+
const unread = notification.readAt === null;
|
|
56
|
+
return /* @__PURE__ */ jsxs(
|
|
57
|
+
Box,
|
|
58
|
+
{
|
|
59
|
+
"data-testid": `notification-${notification.id}`,
|
|
60
|
+
sx: {
|
|
61
|
+
display: "flex",
|
|
62
|
+
alignItems: "flex-start",
|
|
63
|
+
gap: 1,
|
|
64
|
+
py: 1.5,
|
|
65
|
+
px: 1,
|
|
66
|
+
borderBottom: "1px solid",
|
|
67
|
+
borderColor: "divider",
|
|
68
|
+
bgcolor: unread ? (t) => alpha(t.palette.primary.main, 0.06) : "transparent"
|
|
69
|
+
},
|
|
70
|
+
children: [
|
|
71
|
+
/* @__PURE__ */ jsxs(
|
|
72
|
+
Box,
|
|
73
|
+
{
|
|
74
|
+
component: "button",
|
|
75
|
+
type: "button",
|
|
76
|
+
onClick: () => onOpen(notification),
|
|
77
|
+
"aria-label": unread ? `${notification.title} (${messages.unreadSuffix})` : notification.title,
|
|
78
|
+
sx: contentButtonSx,
|
|
79
|
+
children: [
|
|
80
|
+
/* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.75 }, children: [
|
|
81
|
+
unread ? /* @__PURE__ */ jsx(Box, { "aria-hidden": true, sx: unreadDotSx }) : null,
|
|
82
|
+
/* @__PURE__ */ jsx(Text, { variant: "body", size: "sm", weight: unread ? "bold" : "medium", as: "span", children: notification.title })
|
|
83
|
+
] }),
|
|
84
|
+
/* @__PURE__ */ jsx(Text, { variant: "caption", size: "xs", color: "secondary", as: "span", children: notification.body }),
|
|
85
|
+
/* @__PURE__ */ jsx(Text, { variant: "caption", size: "xs", color: "secondary", as: "span", italic: true, children: relativeTime(notification.createdAt, messages) })
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
),
|
|
89
|
+
/* @__PURE__ */ jsx(
|
|
90
|
+
Button,
|
|
91
|
+
{
|
|
92
|
+
variant: "ghost",
|
|
93
|
+
color: "neutral",
|
|
94
|
+
size: "xs",
|
|
95
|
+
"aria-label": messages.deleteOne(notification.title),
|
|
96
|
+
onClick: () => onDelete(notification.id),
|
|
97
|
+
dataTestId: `notification-delete-${notification.id}`,
|
|
98
|
+
children: "\u2715"
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
__name(NotificationRow, "NotificationRow");
|
|
106
|
+
|
|
107
|
+
// src/react/panel.tsx
|
|
108
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
109
|
+
function PanelBody({
|
|
110
|
+
state,
|
|
111
|
+
messages,
|
|
112
|
+
onRetry,
|
|
113
|
+
onLoadMore,
|
|
114
|
+
onOpen,
|
|
115
|
+
onDelete
|
|
116
|
+
}) {
|
|
117
|
+
if (state.status === "pending" || state.status === "idle") {
|
|
118
|
+
return /* @__PURE__ */ jsx2(
|
|
119
|
+
LoadingState,
|
|
120
|
+
{
|
|
121
|
+
variant: "spinner",
|
|
122
|
+
message: messages.loading,
|
|
123
|
+
size: "md",
|
|
124
|
+
dataTestId: "notifications-loading"
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (state.status === "error") {
|
|
129
|
+
return /* @__PURE__ */ jsx2(
|
|
130
|
+
EmptyState,
|
|
131
|
+
{
|
|
132
|
+
variant: "minimal",
|
|
133
|
+
title: messages.loadFailedTitle,
|
|
134
|
+
description: messages.loadFailedBody,
|
|
135
|
+
onRefresh: onRetry,
|
|
136
|
+
refreshLabel: messages.retry,
|
|
137
|
+
dataTestId: "notifications-error"
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (state.items.length === 0) {
|
|
142
|
+
return /* @__PURE__ */ jsx2(
|
|
143
|
+
EmptyState,
|
|
144
|
+
{
|
|
145
|
+
variant: "illustrated",
|
|
146
|
+
illustration: /* @__PURE__ */ jsx2(BellIcon, { size: 44, dim: true }),
|
|
147
|
+
title: messages.emptyTitle,
|
|
148
|
+
description: messages.emptyBody,
|
|
149
|
+
dataTestId: "notifications-empty"
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
154
|
+
state.items.map((notification) => /* @__PURE__ */ jsx2(
|
|
155
|
+
NotificationRow,
|
|
156
|
+
{
|
|
157
|
+
notification,
|
|
158
|
+
messages,
|
|
159
|
+
onOpen,
|
|
160
|
+
onDelete
|
|
161
|
+
},
|
|
162
|
+
notification.id
|
|
163
|
+
)),
|
|
164
|
+
state.nextCursor ? /* @__PURE__ */ jsx2(Box2, { sx: { display: "flex", justifyContent: "center", py: 1.5 }, children: /* @__PURE__ */ jsx2(
|
|
165
|
+
Button2,
|
|
166
|
+
{
|
|
167
|
+
variant: "outline",
|
|
168
|
+
color: "neutral",
|
|
169
|
+
size: "sm",
|
|
170
|
+
disabled: state.loadingMore,
|
|
171
|
+
onClick: onLoadMore,
|
|
172
|
+
dataTestId: "notifications-load-more",
|
|
173
|
+
children: state.loadingMore ? messages.loadingMore : messages.loadMore
|
|
174
|
+
}
|
|
175
|
+
) }) : null
|
|
176
|
+
] });
|
|
177
|
+
}
|
|
178
|
+
__name(PanelBody, "PanelBody");
|
|
179
|
+
function NotificationsPanel({
|
|
180
|
+
open,
|
|
181
|
+
onClose,
|
|
182
|
+
onNavigate,
|
|
183
|
+
store,
|
|
184
|
+
messages
|
|
185
|
+
}) {
|
|
186
|
+
const theme = useTheme();
|
|
187
|
+
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
|
188
|
+
const state = useInboxList(store, open);
|
|
189
|
+
const openNotification = useCallback(
|
|
190
|
+
(notification) => {
|
|
191
|
+
if (notification.readAt === null) store.markRead([notification.id]);
|
|
192
|
+
if (notification.link && onNavigate) {
|
|
193
|
+
onClose();
|
|
194
|
+
onNavigate(notification.link);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
[store, onClose, onNavigate]
|
|
198
|
+
);
|
|
199
|
+
const hasUnread = state.items.some((item) => item.readAt === null);
|
|
200
|
+
return /* @__PURE__ */ jsxs2(
|
|
201
|
+
Drawer,
|
|
202
|
+
{
|
|
203
|
+
open,
|
|
204
|
+
onClose,
|
|
205
|
+
anchor: "right",
|
|
206
|
+
variant: "right",
|
|
207
|
+
width: isMobile ? "100vw" : 400,
|
|
208
|
+
dataTestId: "notifications-panel",
|
|
209
|
+
children: [
|
|
210
|
+
/* @__PURE__ */ jsx2(DrawerHeader, { onClose, children: messages.panelTitle }),
|
|
211
|
+
/* @__PURE__ */ jsxs2(DrawerContent, { children: [
|
|
212
|
+
hasUnread ? /* @__PURE__ */ jsx2(Box2, { sx: { display: "flex", justifyContent: "flex-end", pb: 1 }, children: /* @__PURE__ */ jsx2(
|
|
213
|
+
Button2,
|
|
214
|
+
{
|
|
215
|
+
variant: "ghost",
|
|
216
|
+
color: "primary",
|
|
217
|
+
size: "xs",
|
|
218
|
+
onClick: () => store.markAllRead(),
|
|
219
|
+
dataTestId: "notifications-mark-all-read",
|
|
220
|
+
children: messages.markAllRead
|
|
221
|
+
}
|
|
222
|
+
) }) : null,
|
|
223
|
+
/* @__PURE__ */ jsx2(
|
|
224
|
+
PanelBody,
|
|
225
|
+
{
|
|
226
|
+
state,
|
|
227
|
+
messages,
|
|
228
|
+
onRetry: () => store.invalidate(),
|
|
229
|
+
onLoadMore: () => store.loadMore(),
|
|
230
|
+
onOpen: openNotification,
|
|
231
|
+
onDelete: (id) => store.remove(id)
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
] })
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
__name(NotificationsPanel, "NotificationsPanel");
|
|
240
|
+
export {
|
|
241
|
+
NotificationsPanel
|
|
242
|
+
};
|
|
243
|
+
//# sourceMappingURL=panel-UFXNO4AF.js.map
|