@12-apps/notifications 4.9.0 → 4.10.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.
Files changed (64) hide show
  1. package/ADOPTING.md +60 -1
  2. package/README.md +73 -0
  3. package/dist/{chunk-BGI4BQ66.js → chunk-6W7INOYQ.js} +1 -1
  4. package/dist/chunk-6W7INOYQ.js.map +1 -0
  5. package/dist/{chunk-M5XTROYM.js → chunk-I5QUMTCN.js} +141 -17
  6. package/dist/chunk-I5QUMTCN.js.map +1 -0
  7. package/dist/chunk-RTURLH5U.js +33 -0
  8. package/dist/chunk-RTURLH5U.js.map +1 -0
  9. package/dist/{chunk-GGAZOLNN.js → chunk-SWOWHIFE.js} +2 -2
  10. package/dist/{chunk-KZG7QGFA.js → chunk-WVRODNXQ.js} +8 -4
  11. package/dist/chunk-WVRODNXQ.js.map +1 -0
  12. package/dist/{chunk-KU4FVOFA.js → chunk-ZIR3ILFH.js} +2 -2
  13. package/dist/chunk-ZY32PC34.js +231 -0
  14. package/dist/chunk-ZY32PC34.js.map +1 -0
  15. package/dist/{create-api-notifications-B-VMezVB.d.ts → create-api-notifications-CcPYrM3p.d.ts} +4 -4
  16. package/dist/{create-web-notifications-B-xQ6R0B.d.ts → create-web-notifications-DV3Y8k7e.d.ts} +89 -4
  17. package/dist/{generators-BsuXe8Mp.d.ts → generators-qAD4fNPq.d.ts} +1 -1
  18. package/dist/hono/index.d.ts +5 -5
  19. package/dist/hono/index.js +4 -3
  20. package/dist/hono/index.js.map +1 -1
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js +12 -2
  23. package/dist/{jobs-D64B_bg7.d.ts → jobs--fex87-q.d.ts} +1 -1
  24. package/dist/live-DYxEFO49.d.ts +158 -0
  25. package/dist/manifest/server.d.ts +6 -6
  26. package/dist/manifest/server.js +5 -4
  27. package/dist/manifest/server.js.map +1 -1
  28. package/dist/manifest/web.d.ts +4 -3
  29. package/dist/manifest/web.js +1 -1
  30. package/dist/{panel-UFXNO4AF.js → panel-OPB3DBLJ.js} +116 -61
  31. package/dist/panel-OPB3DBLJ.js.map +1 -0
  32. package/dist/{preferences-screen-V3ZBWP7A.js → preferences-screen-SXUIGECY.js} +2 -2
  33. package/dist/react/index.d.ts +119 -7
  34. package/dist/react/index.js +8 -2
  35. package/dist/server/index.d.ts +7 -7
  36. package/dist/server/index.js +5 -4
  37. package/dist/{types-BFmAY0P5.d.ts → types-BlqZkCWZ.d.ts} +10 -1
  38. package/dist/web-push/index.d.ts +2 -2
  39. package/dist/{web-push-D-ysQd5h.d.ts → web-push-Dnyaha2z.d.ts} +21 -1
  40. package/dist/{wire-_coV3tJk.d.ts → wire-BG1kuoXX.d.ts} +1 -1
  41. package/package.json +2 -2
  42. package/src/index.ts +10 -0
  43. package/src/live.ts +179 -0
  44. package/src/react/bell-button.tsx +112 -16
  45. package/src/react/create-web-notifications.tsx +40 -5
  46. package/src/react/index.ts +29 -0
  47. package/src/react/live-card.tsx +279 -0
  48. package/src/react/live-config.ts +79 -0
  49. package/src/react/live-section.tsx +179 -0
  50. package/src/react/live-seen.ts +138 -0
  51. package/src/react/panel-lazy.tsx +6 -0
  52. package/src/react/panel.tsx +144 -34
  53. package/src/react/relative-time.ts +14 -2
  54. package/src/server/transports/web-push.ts +22 -0
  55. package/src/types.ts +10 -1
  56. package/dist/chunk-BGI4BQ66.js.map +0 -1
  57. package/dist/chunk-KZG7QGFA.js.map +0 -1
  58. package/dist/chunk-M5XTROYM.js.map +0 -1
  59. package/dist/chunk-O5BVUXPO.js +0 -22
  60. package/dist/chunk-O5BVUXPO.js.map +0 -1
  61. package/dist/panel-UFXNO4AF.js.map +0 -1
  62. /package/dist/{chunk-GGAZOLNN.js.map → chunk-SWOWHIFE.js.map} +0 -0
  63. /package/dist/{chunk-KU4FVOFA.js.map → chunk-ZIR3ILFH.js.map} +0 -0
  64. /package/dist/{preferences-screen-V3ZBWP7A.js.map → preferences-screen-SXUIGECY.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/relative-time.ts","../src/react/live-section.tsx","../src/react/live-card.tsx"],"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 *\n * `now` is a parameter rather than a read, and the live section is why. A\n * relative phrase is only true for the instant it was computed, so something\n * has to CAUSE the render that recomputes it — and the live entries' own data\n * cannot: a host backed by react-query gets the previous object back whenever a\n * poll is deep-equal (`structuralSharing`, on by default), which within one\n * stage it always is. The section therefore ticks a clock and hands it down.\n * Defaulted, so every existing caller reads the wall clock exactly as before.\n */\nexport function relativeTime(\n iso: string,\n messages: NotificationMessages,\n now: number = Date.now(),\n): string {\n const elapsedMs = 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","/**\n * The pinned block at the top of the panel: everything that is happening NOW,\n * above everything that has already happened.\n *\n * ## Why it is here and not a second surface\n *\n * The notification centre is where a person goes to find out what they missed.\n * Splitting \"happening\" into its own bell would make them check two places to\n * answer one question, and the half they would stop checking is the one that\n * only has something in it occasionally — which is this one. Above the list,\n * inside the same drawer, it is on the path they already walk.\n *\n * ## What it deliberately does NOT do\n *\n * - It does not touch `unread`. A live entry is not news; counting it would put\n * a number on the bell that no amount of reading can clear.\n * - It renders nothing at all when there is nothing live — no heading, no empty\n * state, no reserved space. A panel with one permanent empty section in it is\n * a panel that has taught its reader to skip the top.\n * - It does not fetch. `useActivities` is the host's, and `active` tells it\n * whether anyone is looking.\n */\nimport { useEffect, useId, useState, type JSX, type ReactNode } from 'react';\n\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport type { LiveActivity } from '../live';\nimport type { NotificationMessages } from '../messages';\n\nimport { LiveActivityCard } from './live-card';\nimport type { LiveActivitiesConfig } from './live-config';\nimport type { LiveSeenStore } from './live-seen';\n\n/**\n * How often the section re-reads the clock.\n *\n * Every minute, because the timestamps under the cards are in minutes and a\n * tick that cannot change what is on screen is a wasted render — which is why\n * it is gated on there being something to tick as well as on the panel being\n * open. An open panel with nothing live schedules nothing at all; the earlier\n * gate was `active` alone, and it re-rendered a section that renders `null`\n * once a minute for as long as somebody left the inbox open.\n */\nconst TICK_MS = 60_000;\n\n/**\n * The current minute, re-read on a timer while there is something to tick.\n *\n * The caller passes `active && there are activities` — see {@link TICK_MS} for\n * why both halves are in it.\n */\nfunction useMinuteTick(active: boolean): number {\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n if (!active) return;\n // Re-read once on becoming active too: a panel reopened after ten minutes\n // would otherwise show the minute it was closed at until the first tick.\n setNow(Date.now());\n const timer = setInterval(() => setNow(Date.now()), TICK_MS);\n return () => clearInterval(timer);\n }, [active]);\n return now;\n}\n\nexport interface LiveSectionProps {\n config: LiveActivitiesConfig;\n messages: NotificationMessages;\n /** Whether the panel is open — passed straight through to the host's hook. */\n active: boolean;\n /**\n * Follow a card's link.\n *\n * Optional, and the panel omits it for a host with no router: a card that\n * cannot go anywhere renders as text rather than as a named control that\n * does nothing.\n */\n onOpen?: (activity: LiveActivity) => void;\n /**\n * The rest of the panel, given how many entries are live.\n *\n * A render prop rather than a sibling, because the count is knowable only\n * where the host's hook is CALLED, and it cannot be called anywhere else:\n * `live` is optional on the panel, so reading it there would mean calling a\n * hook conditionally — the failure React reports as a crash in some unrelated\n * component.\n *\n * The inbox needs the number for exactly one decision, and it is the decision\n * this section exists to inform: whether \"no notifications\" is true. A live\n * entry IS a notification, so a panel showing one under that sentence is\n * contradicting itself.\n */\n children?: (liveCount: number) => ReactNode;\n /**\n * Where \"the reader has seen these\" is recorded, for the bell to read.\n *\n * Written HERE because this is the component that puts them on screen, and\n * being on screen is what seen means. Optional so the section stays usable by\n * a host that mounts it outside the panel.\n */\n seen?: LiveSeenStore;\n}\n\n\n\nexport function LiveSection({\n config,\n messages,\n active,\n onOpen,\n children,\n seen,\n}: LiveSectionProps): JSX.Element {\n // Unconditional, because it is a hook. `active` is how it is told nobody is\n // looking — the same arrangement `useSignal` has one seam over.\n const activities = config.useActivities({ active });\n const now = useMinuteTick(active && activities.length > 0);\n // Per MOUNT, not per module: `LiveSection` is exported, and a host with a\n // desktop and a mobile panel would otherwise emit one id twice and have both\n // regions resolve their label to whichever came first.\n const headingId = useId();\n\n const liveCount = activities.length;\n\n // Only while somebody is looking. The panel keeps this mounted through the\n // closing transition, and marking there would swallow an update that arrived\n // in the frames after the reader turned away.\n useEffect(() => {\n if (active && liveCount > 0) seen?.mark(activities);\n }, [active, liveCount, activities, seen]);\n\n if (liveCount === 0) return <>{children?.(0)}</>;\n\n return (\n <>\n // A NAMED region. Without the label a screen-reader user meets a loose run\n // of controls ahead of the inbox with nothing saying what they are; the\n // panel's own title is the drawer's heading and cannot describe this block.\n <Box\n component=\"section\"\n aria-labelledby={headingId}\n data-testid=\"live-activities\"\n sx={{ pb: 1.5 }}\n >\n {/*\n A SPAN, not a heading. `aria-labelledby` names the region perfectly well\n from one, and an `<h2>` here would sit under the drawer's own `<h6>`\n title and ABOVE the inbox's `<h3>` empty state — an outline in which the\n inbox's states read as part of the live block, which is the opposite of\n what the two blocks are.\n */}\n <Text\n id={headingId}\n variant=\"caption\"\n size=\"xs\"\n color=\"secondary\"\n weight=\"semibold\"\n as=\"span\"\n >\n {config.messages.sectionTitle}\n </Text>\n <Box sx={{ pt: 0.75 }}>\n {activities.map((activity) => (\n <LiveActivityCard\n key={activity.id}\n activity={activity}\n messages={messages}\n live={config.messages}\n now={now}\n {...(onOpen ? { onOpen } : {})}\n {...(config.renderIcon ? { renderIcon: config.renderIcon } : {})}\n />\n ))}\n </Box>\n </Box>\n {children?.(liveCount)}\n </>\n );\n}\n","/**\n * ONE pinned live entry: a mark, what is happening, its lane, and when it last\n * moved.\n *\n * Visually a WASH rather than a fill — a tinted card with a brand-tinted border\n * — for the reason the inbox's unread row uses the same treatment: this sits at\n * the top of a list of other people's news, and a saturated block there\n * out-shouts everything it is supposed to be introducing.\n *\n * ## The card is a DIV, and the button is inside it\n *\n * The obvious shape — one `<button>` wrapping the whole card — is not\n * available, because `Stepper` draws every stop as a real `<button>`\n * (`@12-apps/ui`'s `StepButton` is `styled(Button)`), and `clickable={false}`\n * only sets `pointer-events: none`. A button inside a button is invalid HTML:\n * the parser auto-closes the outer one at the first nested one, so any host\n * that server-renders the panel open hydrates against a tree the browser\n * rewrote, and every adopter's dev console carries a React error besides.\n *\n * `aria-hidden` and `inert` on the lane fix the tab stops and the accessible\n * name — they do NOT fix the nesting, and an earlier draft of this file claimed\n * they did. So the tap target is the TEXT block, and the lane and the timestamp\n * are its siblings: valid markup, and a target that still covers everything a\n * reader would aim at.\n */\nimport { useId, type JSX, type ReactNode } from 'react';\n\nimport { Stepper } from '@12-apps/ui/data-display/Stepper';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { alpha, type Theme } from '@12-apps/ui/mui/styles';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport { liveActivityLane, type LiveActivity } from '../live';\nimport type { NotificationMessages } from '../messages';\n\nimport type { LiveActivitiesConfig, LiveActivityMessages } from './live-config';\nimport { relativeTime } from './relative-time';\n\nconst cardSx = {\n // `relative`, so the button below can stretch a hit area over the whole card\n // — see `targetSx`.\n position: 'relative',\n border: '1px solid',\n borderColor: (t: Theme) => alpha(t.palette.primary.main, 0.35),\n bgcolor: (t: Theme) => alpha(t.palette.primary.main, 0.06),\n borderRadius: 1.5,\n p: 1.25,\n mb: 1,\n} as const;\n\n/** The text block: the mark, the heading and the sentence under it. */\nconst targetSx = {\n display: 'flex',\n alignItems: 'center',\n gap: 1,\n width: '100%',\n textAlign: 'left',\n font: 'inherit',\n color: 'inherit',\n border: 'none',\n background: 'none',\n p: 0,\n} as const;\n\n/**\n * The button, stretched over the WHOLE card.\n *\n * Taking the lane out of the link fixed the markup and left the card looking\n * like one target while only its top half was one — the lane is the most\n * visually distinctive part of it, and aiming at the obvious thing did nothing.\n *\n * A stretched pseudo-element is the remedy that keeps the structure: the\n * `<button>` stays a sibling of the lane in the tree, so nothing nests, and its\n * `::after` covers the card. All three declarations are load-bearing — a\n * pseudo-element with no `content` generates no box at all, and an absolutely\n * positioned box with auto offsets is 0×0.\n *\n * ## What actually lets a click on the LANE reach it\n *\n * Not paint order. `@12-apps/ui` gives each `StepItem` `position: relative`, and\n * this overlay is positioned too — so the two sit in the SAME painting layer\n * (positioned, `z-index: auto`), where tree order decides, and the lane comes\n * after the button. Every stop, and its label, therefore sits over this\n * overlay. `clickable={false}` does not save it either: the package puts\n * `pointer-events: none` on the step CIRCLE and not on the label beside it.\n *\n * It is `inert` on {@link ActivityLane} that does it: an inert subtree is\n * skipped by hit-testing, so a click on a stop falls through to the overlay\n * underneath. That makes the attribute load-bearing for the TARGET as well as\n * for the tab order it was added for — remove it and the lane silently swallows\n * clicks again, which is why the two are pinned by one test.\n */\nconst stretchedSx = {\n ...targetSx,\n cursor: 'pointer',\n '&::after': { content: '\"\"', position: 'absolute', inset: 0 },\n} as const;\n\n/**\n * Make a four-stop lane fit the panel.\n *\n * The drawer is 400px on a desktop and the full viewport on a phone, so the\n * narrow case is ~320px of card minus its padding. `Stepper` renders its labels\n * at `body2` for every size but `sm` and reserves 24px of connector plus 8px of\n * margin on each side, which is more row than four short words have — measured\n * on a 320px viewport, the last stop hung off the edge and the DRAWER scrolled\n * sideways.\n *\n * Three overrides, each buying back a specific number of pixels: 11px labels, a\n * step column allowed to shrink below the package's 44px floor (so the row's\n * min-content width is the longest WORD rather than the longest phrase), and\n * thinner connectors. `overflow: hidden` is the backstop and not the mechanism\n * — a locale with longer words than any of this anticipates clips its own card\n * instead of making the panel scroll.\n */\nconst laneSx = {\n pt: 1.25,\n px: 0.5,\n overflow: 'hidden',\n '& .MuiTypography-root': { fontSize: 11, lineHeight: 1.25 },\n '& [data-testid^=\"stepper-step-content-\"]': { minWidth: 0 },\n '& [data-testid^=\"stepper-connector-\"]': { minWidth: 6, mx: 0.75 },\n} as const;\n\n/**\n * The lane, or nothing.\n *\n * `aria-hidden` AND `inert`, and each earns its place twice over. The stops are\n * real buttons, so leaving four focusable, named controls per entry in front of\n * an inbox would cost a keyboard user the list they opened the panel for —\n * that is what the pair was added for. `inert` then turns out to be what makes\n * the card's own hit area work as well, because an inert subtree is skipped by\n * hit-testing: see {@link stretchedSx}.\n *\n * Nothing is lost by hiding it — the stop the subject is at is already the\n * card's heading, and the row of dots restates it visually.\n */\nfunction ActivityLane({ activity }: { activity: LiveActivity }): JSX.Element | null {\n const lane = liveActivityLane(activity);\n if (lane === null) return null;\n return (\n <Box sx={laneSx} aria-hidden inert>\n <Stepper\n steps={lane.steps.map((step) => ({ id: step.id, label: step.label }))}\n activeId={lane.activeStepId}\n completed={new Set(lane.completed)}\n orientation=\"horizontal\"\n size=\"xs\"\n clickable={false}\n data-testid={`live-activity-steps-${activity.id}`}\n />\n </Box>\n );\n}\n\n/** The card's props. Not part of the package's surface — see `./index`. */\ninterface LiveActivityCardProps {\n activity: LiveActivity;\n messages: NotificationMessages;\n live: LiveActivityMessages;\n renderIcon?: LiveActivitiesConfig['renderIcon'];\n /** The clock this render reads, so the \"last moved\" line can be ticked. */\n now: number;\n /**\n * Follow the card's link.\n *\n * Absent — as it is for a host with no router — renders the text as text. A\n * named, focusable control that does nothing is worse than no control.\n */\n onOpen?: (activity: LiveActivity) => void;\n}\n\n/**\n * The mark on the left, when the host draws one.\n *\n * PRESENTATIONAL ONLY. It is rendered inside the card's `<button>` and inside\n * an `aria-hidden` wrapper, so a host returning anything focusable — an\n * icon-button, a link — puts a button inside a button (invalid HTML, and the\n * defect this card was restructured to remove) and hides a focusable node from\n * the accessibility tree. An icon, an emoji, an `<svg>`: yes. A control: no.\n */\nfunction ActivityIcon({ icon }: { icon: ReactNode }): JSX.Element | null {\n if (icon === undefined || icon === null) return null;\n return (\n <Box aria-hidden sx={{ display: 'flex', flex: '0 0 auto', color: 'primary.main' }}>\n {icon}\n </Box>\n );\n}\n\n/** The mark, the heading and the line under it. */\nfunction ActivityTarget({\n activity,\n renderIcon,\n bodyId,\n}: Pick<LiveActivityCardProps, 'activity' | 'renderIcon'> & {\n /** Ties the sentence to the button, so a label does not swallow it. */\n bodyId: string;\n}): JSX.Element {\n return (\n <>\n <ActivityIcon icon={renderIcon?.(activity)} />\n {/* A COLUMN, not a bare block: `Text` sets no `display`, so two adjacent\n spans in an ordinary div run together on one line with not even a\n space between them — which is how the heading and the sentence under\n it ended up as one word in an earlier draft. `row.tsx` gets this right\n the same way. */}\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25, minWidth: 0 }}>\n {/*\n The live region is THIS LINE and nothing else. The card also carries a\n relative timestamp that moves every minute for as long as the subject\n lasts, and announcing that is a polite interruption per minute for\n news the reader did not ask to be read. What is worth interrupting for\n is the subject MOVING, which is what the heading says.\n */}\n <Text\n variant=\"body\"\n size=\"sm\"\n weight=\"semibold\"\n as=\"span\"\n aria-live=\"polite\"\n data-testid={`live-activity-title-${activity.id}`}\n >\n {activity.title}\n </Text>\n {activity.body === null ? null : (\n <Text id={bodyId} variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"span\">\n {activity.body}\n </Text>\n )}\n </Box>\n </>\n );\n}\n\nexport function LiveActivityCard({\n activity,\n messages,\n live,\n renderIcon,\n now,\n onOpen,\n}: LiveActivityCardProps): JSX.Element {\n const followable = activity.link !== null && onOpen !== undefined;\n const bodyId = useId();\n const target = (\n <ActivityTarget\n activity={activity}\n bodyId={bodyId}\n {...(renderIcon ? { renderIcon } : {})}\n />\n );\n return (\n <Box data-testid={`live-activity-${activity.id}`} sx={cardSx}>\n {followable ? (\n <Box\n component=\"button\"\n type=\"button\"\n onClick={() => onOpen(activity)}\n // `aria-label` REPLACES the contents, so the sentence under the\n // heading — the detail that makes the heading actionable — would be\n // announced to nobody. `aria-describedby` puts it back.\n aria-label={live.openActivity(activity.title)}\n {...(activity.body === null ? {} : { 'aria-describedby': bodyId })}\n data-testid={`live-activity-open-${activity.id}`}\n sx={stretchedSx}\n >\n {target}\n </Box>\n ) : (\n <Box sx={targetSx}>{target}</Box>\n )}\n <ActivityLane activity={activity} />\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"span\" italic>\n {live.updated(relativeTime(activity.updatedAt, messages, now))}\n </Text>\n </Box>\n );\n}\n"],"mappings":"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","names":["useId","Box","Text","Fragment","jsx","jsxs","useId","Box","Text"]}
@@ -1,7 +1,7 @@
1
- import { N as NotificationGeneratorRegistry } from './generators-BsuXe8Mp.js';
2
- import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, L as ListNotificationsResult, h as NotificationsCopySource, c as NotificationWireMessages } from './wire-_coV3tJk.js';
3
- import { c as NotificationChannel, D as DeliveryStatus, g as NotificationLogger, j as NotificationTransport, d as NotificationContent, b as NotificationCategory, e as NotificationEvent, N as NotificationGenerator } from './types-BFmAY0P5.js';
4
- import { e as WebPushSubscriptionSource, D as DriverDeclarationBase, a as WebPushDriverDeclaration, c as WebPushSender } from './web-push-D-ysQd5h.js';
1
+ import { N as NotificationGeneratorRegistry } from './generators-qAD4fNPq.js';
2
+ import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, L as ListNotificationsResult, h as NotificationsCopySource, c as NotificationWireMessages } from './wire-BG1kuoXX.js';
3
+ import { c as NotificationChannel, D as DeliveryStatus, g as NotificationLogger, j as NotificationTransport, d as NotificationContent, b as NotificationCategory, e as NotificationEvent, N as NotificationGenerator } from './types-BlqZkCWZ.js';
4
+ import { e as WebPushSubscriptionSource, D as DriverDeclarationBase, a as WebPushDriverDeclaration, c as WebPushSender } from './web-push-Dnyaha2z.js';
5
5
  import { d as EmailChromeCopy, f as EmailTheme } from './template-Dlt5S54z.js';
6
6
 
7
7
  /**
@@ -1,6 +1,7 @@
1
- import { JSX, ComponentType } from 'react';
2
- import { L as ListNotificationsResult, a as ChannelRow, I as InboxNotification, N as NotificationMessages } from './wire-_coV3tJk.js';
3
- import { c as NotificationChannel } from './types-BFmAY0P5.js';
1
+ import { ReactNode, JSX, ComponentType } from 'react';
2
+ import { L as ListNotificationsResult, a as ChannelRow, I as InboxNotification, N as NotificationMessages } from './wire-BG1kuoXX.js';
3
+ import { c as NotificationChannel } from './types-BlqZkCWZ.js';
4
+ import { b as LiveActivity } from './live-DYxEFO49.js';
4
5
 
5
6
  /**
6
7
  * How the notification screens reach their data (12-15) — the report-builder
@@ -209,6 +210,81 @@ declare function useUnreadCount(store: InboxStore, options?: {
209
210
  /** The panel's list — only fetches while the panel is open. */
210
211
  declare function useInboxList(store: InboxStore, open: boolean): InboxState;
211
212
 
213
+ /**
214
+ * The host's half of live activities: where they come from, and what they say.
215
+ *
216
+ * Both are the host's because neither can be this package's. It has no idea
217
+ * what is happening — only the application does — and it has no words, for the
218
+ * same reason `NotificationMessages` is required rather than defaulted: a
219
+ * package that ships one product's sentences ships them to every other product
220
+ * too, and the adopter reached by saying nothing is the one who never notices.
221
+ */
222
+
223
+ /**
224
+ * Where the surface gets the activities that are live RIGHT NOW.
225
+ *
226
+ * A HOOK rather than a fetcher, and rather than a factory-time `subscribe`,
227
+ * because the answer almost always lives in React context: the tenant, the
228
+ * session, the host's query client. `NotificationsSignalHook` exists for
229
+ * exactly this reason one seam over, and a host in that shape had no way to
230
+ * pass anything at all.
231
+ *
232
+ * `active` is whether the surface currently needs the answer. It is a HINT
233
+ * about need, never about correctness: a host that ignores it and always
234
+ * answers is behaving correctly and merely paying for it.
235
+ *
236
+ * What it is NOT is the only thing standing between a shut panel and a query.
237
+ * The panel is fetched lazily and renders nothing until somebody first opens
238
+ * the bell, and the drawer unmounts its content on close — so a host that
239
+ * simply reads `active` and ignores it still issues nothing while the panel is
240
+ * away. `active` is `false` for the frames of the closing transition, which is
241
+ * where it earns its keep: a query told to stand down there does not fire one
242
+ * last time on the way out.
243
+ *
244
+ * Return whatever is live, newest activity first or in whatever order the host
245
+ * means; the surface renders them in the order given. An empty array is the
246
+ * normal answer and renders nothing — no heading, no empty state, no gap.
247
+ */
248
+ type LiveActivitiesHook = (options: {
249
+ active: boolean;
250
+ }) => readonly LiveActivity[];
251
+ /** The three sentences the live section says. */
252
+ interface LiveActivityMessages {
253
+ /** The heading over the pinned entries, e.g. "Em andamento". */
254
+ sectionTitle: string;
255
+ /** The link's accessible name, e.g. `(title) => `Abrir ${title}``. */
256
+ openActivity: (title: string) => string;
257
+ /**
258
+ * The "last moved" line, given an already-relative time.
259
+ *
260
+ * Takes the phrase rather than the instant so the relative wording stays in
261
+ * ONE place — `relativeTime` and the inbox rows' `há 5 min` — and a host
262
+ * cannot end up with two vocabularies for the same duration in one panel.
263
+ */
264
+ updated: (relative: string) => string;
265
+ }
266
+ /** Live activities, as a host turns them on. */
267
+ interface LiveActivitiesConfig {
268
+ useActivities: LiveActivitiesHook;
269
+ messages: LiveActivityMessages;
270
+ /**
271
+ * The mark on the left of a card — the fastest read, before any words.
272
+ *
273
+ * A node rather than a field on {@link LiveActivity} so the contract stays
274
+ * framework-free: the root entry is shared with the server half, and a
275
+ * `ReactNode` in it would put React on that import path for a backend that
276
+ * only ever writes rows. The host switches on `kind`, which is what `kind` is
277
+ * for. No renderer, no mark, and the card is text — never a placeholder box.
278
+ *
279
+ * **Return something PRESENTATIONAL.** The mark is drawn inside the card's
280
+ * own `<button>` and inside an `aria-hidden` wrapper, so a focusable node
281
+ * here is a button inside a button — invalid HTML, and the exact defect the
282
+ * card's structure exists to prevent — as well as a control hidden from the
283
+ * accessibility tree. An icon or an `<svg>`; not a control.
284
+ */
285
+ renderIcon?: (activity: LiveActivity) => ReactNode;
286
+ }
287
+
212
288
  /**
213
289
  * Bare bell trigger with the live unread badge — for hosts that do not already
214
290
  * have a styled icon-button slot. A host with its own trigger chrome uses
@@ -322,6 +398,15 @@ interface NotificationsWebConfig {
322
398
  useSignal?: NotificationsSignalHook;
323
399
  /** The browser push enable step's host seams (SW path, platform hint). */
324
400
  webPush?: WebPushSetupConfig;
401
+ /**
402
+ * LIVE ACTIVITIES — the ongoing-state entries pinned above the inbox list.
403
+ *
404
+ * Opt-in, and absent means absent: a host that passes nothing gets the panel
405
+ * it had, with no section, no heading and no reserved space. See
406
+ * `./live-config` for the two things a host has to supply (where they come
407
+ * from, and what the section says) and `../live` for what one IS.
408
+ */
409
+ liveActivities?: LiveActivitiesConfig;
325
410
  }
326
411
  interface WebNotifications {
327
412
  /**
@@ -362,4 +447,4 @@ interface WebNotifications {
362
447
  }
363
448
  declare function createWebNotifications(config: NotificationsWebConfig): WebNotifications;
364
449
 
365
- export { BADGE_POLL_MS as B, type InboxListStatus as I, type NotificationsApiClient as N, PAGE_SIZE as P, type WebNotifications as W, BADGE_RECONCILE_MS as a, type BellButtonProps as b, createWebNotifications as c, type InboxState as d, type InboxStore as e, NotificationsHttpError as f, type NotificationsPanelProps as g, type NotificationsResult as h, type NotificationsSignalHook as i, type NotificationsSubscribe as j, type NotificationsTransport as k, type NotificationsWebConfig as l, type PreferencesPayload as m, type PreferencesScreenProps as n, type PushRegistrationPayload as o, type WebPushPlatformHint as p, type WebPushSetupConfig as q, createInboxStore as r, createNotificationsApiClient as s, httpNotificationsTransport as t, useInboxList as u, useInboxState as v, useUnreadCount as w };
450
+ export { BADGE_POLL_MS as B, type InboxListStatus as I, type LiveActivitiesConfig as L, type NotificationsApiClient as N, PAGE_SIZE as P, type WebNotifications as W, BADGE_RECONCILE_MS as a, type BellButtonProps as b, createWebNotifications as c, type InboxState as d, type InboxStore as e, type LiveActivitiesHook as f, type LiveActivityMessages as g, NotificationsHttpError as h, type NotificationsPanelProps as i, type NotificationsResult as j, type NotificationsSignalHook as k, type NotificationsSubscribe as l, type NotificationsTransport as m, type NotificationsWebConfig as n, type PreferencesPayload as o, type PreferencesScreenProps as p, type PushRegistrationPayload as q, type WebPushPlatformHint as r, type WebPushSetupConfig as s, createInboxStore as t, createNotificationsApiClient as u, httpNotificationsTransport as v, useInboxList as w, useInboxState as x, useUnreadCount as y };
@@ -1,4 +1,4 @@
1
- import { N as NotificationGenerator } from './types-BFmAY0P5.js';
1
+ import { N as NotificationGenerator } from './types-BlqZkCWZ.js';
2
2
 
3
3
  /**
4
4
  * Generator registry: one {@link NotificationGenerator} per event `type`.
@@ -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-B-VMezVB.js';
3
- import '../generators-BsuXe8Mp.js';
4
- import '../types-BFmAY0P5.js';
5
- import '../wire-_coV3tJk.js';
6
- import '../web-push-D-ysQd5h.js';
2
+ import { A as ApiNotifications, N as NotificationsServerConfig, a as NotificationsActor } from '../create-api-notifications-CcPYrM3p.js';
3
+ import '../generators-qAD4fNPq.js';
4
+ import '../types-BlqZkCWZ.js';
5
+ import '../wire-BG1kuoXX.js';
6
+ import '../web-push-Dnyaha2z.js';
7
7
  import '../template-Dlt5S54z.js';
8
8
 
9
9
  /**
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  createApiNotifications
3
- } from "../chunk-KZG7QGFA.js";
4
- import "../chunk-GGAZOLNN.js";
3
+ } from "../chunk-WVRODNXQ.js";
4
+ import "../chunk-SWOWHIFE.js";
5
5
  import {
6
6
  messagesOf
7
7
  } from "../chunk-M2TVBVH2.js";
8
- import "../chunk-BGI4BQ66.js";
8
+ import "../chunk-RTURLH5U.js";
9
+ import "../chunk-6W7INOYQ.js";
9
10
  import "../chunk-EKUSNUBT.js";
10
11
  import {
11
12
  __name
@@ -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":";;;;;;;;;;;;;;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":[]}
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,7 @@
1
- export { D as DeliveryStatus, a as NOTIFICATION_CHANNELS, b as NotificationCategory, c as NotificationChannel, d as NotificationContent, e as NotificationEvent, f as NotificationGenerateContext, N as NotificationGenerator, g as NotificationLogger, h as NotificationRecipient, i as NotificationTaxonomy, j as NotificationTransport, T as TransportRecipient, t as taxonomyOf } from './types-BFmAY0P5.js';
2
- export { N as NotificationGeneratorRegistry, c as createGeneratorRegistry } from './generators-BsuXe8Mp.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-_coV3tJk.js';
1
+ export { D as DeliveryStatus, a as NOTIFICATION_CHANNELS, b as NotificationCategory, c as NotificationChannel, d as NotificationContent, e as NotificationEvent, f as NotificationGenerateContext, N as NotificationGenerator, g as NotificationLogger, h as NotificationRecipient, i as NotificationTaxonomy, j as NotificationTransport, T as TransportRecipient, t as taxonomyOf } from './types-BlqZkCWZ.js';
2
+ export { N as NotificationGeneratorRegistry, c as createGeneratorRegistry } from './generators-qAD4fNPq.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-BG1kuoXX.js';
4
+ export { L as LIVE_PUSH_TAG_PREFIX, a as LIVE_SUBJECT_KEY, b as LiveActivity, c as LiveActivityLane, d as LiveActivityStep, l as liveActivityLane, e as livePushTag } from './live-DYxEFO49.js';
4
5
 
5
6
  /** Thrown by `notify` when no generator is registered for the event type. */
6
7
  declare class UnknownNotificationTypeError extends Error {
package/dist/index.js CHANGED
@@ -9,17 +9,25 @@ import {
9
9
  mergeChoices,
10
10
  mergeStoredRow,
11
11
  normalizePhoneE164
12
- } from "./chunk-GGAZOLNN.js";
12
+ } from "./chunk-SWOWHIFE.js";
13
13
  import {
14
14
  messagesOf
15
15
  } from "./chunk-M2TVBVH2.js";
16
+ import {
17
+ LIVE_PUSH_TAG_PREFIX,
18
+ LIVE_SUBJECT_KEY,
19
+ liveActivityLane,
20
+ livePushTag
21
+ } from "./chunk-RTURLH5U.js";
16
22
  import {
17
23
  NOTIFICATION_CHANNELS,
18
24
  taxonomyOf
19
- } from "./chunk-BGI4BQ66.js";
25
+ } from "./chunk-6W7INOYQ.js";
20
26
  import "./chunk-7QVYU63E.js";
21
27
  export {
22
28
  DEFAULT_CHANNEL_ROW,
29
+ LIVE_PUSH_TAG_PREFIX,
30
+ LIVE_SUBJECT_KEY,
23
31
  NOTIFICATION_CHANNELS,
24
32
  UnknownNotificationRecipientError,
25
33
  UnknownNotificationTypeError,
@@ -27,6 +35,8 @@ export {
27
35
  defaultChannelMatrix,
28
36
  enabledChannelsOf,
29
37
  inboxWire,
38
+ liveActivityLane,
39
+ livePushTag,
30
40
  mergeChoices,
31
41
  mergeStoredRow,
32
42
  messagesOf,
@@ -1,5 +1,5 @@
1
1
  import { WireJobBlueprint } from '@12-apps/wiring';
2
- import { A as ApiNotifications } from './create-api-notifications-B-VMezVB.js';
2
+ import { A as ApiNotifications } from './create-api-notifications-CcPYrM3p.js';
3
3
 
4
4
  /**
5
5
  * The two background jobs getting a message out actually needs.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * LIVE ACTIVITIES — the notification centre's second kind of entry.
3
+ *
4
+ * An inbox notification is an EVENT: it happened, it is stamped, it is read or
5
+ * unread, and it is still there tomorrow saying the same thing. A live activity
6
+ * is ONGOING STATE: it is pinned above the list, it has no read/unread, nobody
7
+ * deletes it, it updates itself, and it is GONE the moment the thing it tracks
8
+ * finishes.
9
+ *
10
+ * The distinction is not cosmetic. A row that says "your order is on its way",
11
+ * read an hour later, is a claim about the past presented as news — and the
12
+ * more reliable the inbox is, the more of those a person accumulates. Somewhere
13
+ * in that list is the one question they actually have, which is *where is it
14
+ * now*. A live activity answers that question and then removes itself, which is
15
+ * the property no amount of better event copy can buy.
16
+ *
17
+ * ## This file is domain-free, and that is the whole design
18
+ *
19
+ * Nothing here knows what is being tracked. A host publishes activities through
20
+ * `LiveActivitiesConfig.useActivities` (see `./react/live-config`) and this
21
+ * package owns the contract, the placement, the rendering and the OS-level
22
+ * collapse. The package that raises an alert has never been this package's
23
+ * business — see the `generators` seam — and the thing an alert is ABOUT is not
24
+ * either.
25
+ *
26
+ * ## The lane
27
+ *
28
+ * `steps` + `activeStepId` describe a progress lane, because "how far along is
29
+ * this" is the shape almost every ongoing subject has. Both are optional in
30
+ * effect: an activity with no lane (`steps: []`, `activeStepId: null`) is a
31
+ * perfectly good live entry — a heading, a sentence and a timestamp that keeps
32
+ * moving.
33
+ */
34
+ /** One stop on a live activity's lane. */
35
+ interface LiveActivityStep {
36
+ /** Stable across updates — the id is what `activeStepId` names. */
37
+ id: string;
38
+ /** What the reader sees under the dot. The HOST's word, in the reader's language. */
39
+ label: string;
40
+ }
41
+ /** One thing that is happening right now, as the notification centre shows it. */
42
+ interface LiveActivity {
43
+ /**
44
+ * Stable for the whole life of the subject.
45
+ *
46
+ * The SAME id on every update, because it is the identity of the thing being
47
+ * tracked and not of the message: the panel keys on it so a stage change
48
+ * re-renders one card rather than swapping two, and it is what
49
+ * {@link livePushTag} collapses an OS notification onto.
50
+ *
51
+ * **Unique among the activities live at one moment**, for the same reason:
52
+ * it is the React key and the card's test id. Two activities sharing one
53
+ * gives a duplicate-key warning and a card that silently shows the wrong
54
+ * subject.
55
+ */
56
+ id: string;
57
+ /**
58
+ * Which host concern this belongs to, e.g. `order`, `delivery`, `import`.
59
+ *
60
+ * Free-form and never rendered — it exists so a host publishing from two
61
+ * sources can tell its own activities apart in a test or a log without
62
+ * parsing `id`.
63
+ */
64
+ kind: string;
65
+ /** The heading — what is happening. */
66
+ title: string;
67
+ /** The line under it: the detail that makes the heading actionable, or none. */
68
+ body: string | null;
69
+ /** Where tapping the card goes, as a same-origin path. `null` renders no link. */
70
+ link: string | null;
71
+ /** The lane, in the order it is walked. Empty when this subject has no lane. */
72
+ steps: readonly LiveActivityStep[];
73
+ /**
74
+ * The stop the subject is AT — one of `steps`, or `null` for a laneless entry.
75
+ *
76
+ * An id that names no step is neither, and it is handled rather than trusted:
77
+ * see {@link liveActivityLane}.
78
+ */
79
+ activeStepId: string | null;
80
+ /**
81
+ * ISO-8601 — when the subject last MOVED, not when it was last polled.
82
+ *
83
+ * A timestamp that advances on every read would render "just now" forever,
84
+ * which is the one thing a live entry must not say when nothing is happening.
85
+ */
86
+ updatedAt: string;
87
+ }
88
+ /** A live activity's lane, resolved for rendering. */
89
+ interface LiveActivityLane {
90
+ steps: readonly LiveActivityStep[];
91
+ activeStepId: string;
92
+ /** Every stop BEFORE the active one — never the active one itself. */
93
+ completed: ReadonlySet<string>;
94
+ }
95
+ /**
96
+ * The lane to draw, or `null` when this activity has none to draw.
97
+ *
98
+ * Three cases collapse to `null`, and the third is the one worth writing down:
99
+ * no steps, no active step, and **an active step the lane does not contain**.
100
+ * That last one is not a theoretical defect — it is what a host produces the
101
+ * first time a subject reaches a stage the lane was filtered to exclude, and
102
+ * the symptom is a row of dots with NONE of them lit, which reads as a process
103
+ * that has stopped. A card with no lane still says what is happening and when
104
+ * it last moved; a dead lane says the tracking is broken.
105
+ *
106
+ * `completed` deliberately stops short of the active stop: a completed step
107
+ * draws a tick, and ticking the stop the subject is sitting in claims it has
108
+ * already left.
109
+ */
110
+ declare function liveActivityLane(activity: LiveActivity): LiveActivityLane | null;
111
+ /**
112
+ * The reserved `data` key that ties a NOTIFICATION to a live subject.
113
+ *
114
+ * A notification and a live activity are different objects with different
115
+ * lifetimes, and this is the only thing that joins them: a generator whose
116
+ * event is about something also tracked live puts the activity's id here, and
117
+ * every channel that can collapse gets to.
118
+ *
119
+ * data: { [LIVE_SUBJECT_KEY]: `order:${orderId}` }
120
+ *
121
+ * A plain string on purpose. An object here would grow a second wire contract
122
+ * inside a column this package stores verbatim.
123
+ */
124
+ declare const LIVE_SUBJECT_KEY = "liveSubject";
125
+ /**
126
+ * Namespaces the tray tag, so a live tag does not collide with the ones a host
127
+ * already uses.
128
+ *
129
+ * A convention rather than an enforcement — nothing stops a host emitting its
130
+ * own `live:`-prefixed tags — but it means the two id spaces have to be made to
131
+ * meet rather than meeting by accident.
132
+ */
133
+ declare const LIVE_PUSH_TAG_PREFIX = "live:";
134
+ /**
135
+ * The OS notification tag for a push about a live subject, or `null` for an
136
+ * ordinary event.
137
+ *
138
+ * ## What the tag buys, and why it is the PWA half of this feature
139
+ *
140
+ * A `tag` makes a new notification REPLACE the one already in the tray instead
141
+ * of stacking under it, and — unless the sender asks otherwise — replace it
142
+ * *silently*. So a subject that moves through four stages costs one tray entry
143
+ * and one buzz, and the entry that remains is the CURRENT one. Without it a
144
+ * phone accumulates one alert per stage, all of them still asserting a stage
145
+ * the subject has since left; the freshest is at the top and the reader has to
146
+ * work out that the three below it are history.
147
+ *
148
+ * That is as close as the web platform gets to an ongoing/live notification,
149
+ * and it is the half that reaches a person who does not have the app open —
150
+ * which is most of the time a live activity is live.
151
+ *
152
+ * The id is passed through unchanged: it is the host's, it is already unique
153
+ * per subject, and a hash would make the tray impossible to reason about from a
154
+ * log line.
155
+ */
156
+ declare function livePushTag(data: Readonly<Record<string, unknown>> | null | undefined): string | null;
157
+
158
+ export { LIVE_PUSH_TAG_PREFIX as L, LIVE_SUBJECT_KEY as a, type LiveActivity as b, type LiveActivityLane as c, type LiveActivityStep as d, livePushTag as e, liveActivityLane as l };
@@ -1,13 +1,13 @@
1
1
  import { EmailPreviewRoute } from '../email/previews/index.js';
2
2
  import * as _12_apps_wiring from '@12-apps/wiring';
3
3
  import { WireRequest } from '@12-apps/wiring';
4
- import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-B-VMezVB.js';
5
- import { N as NotificationsJobDeps } from '../jobs-D64B_bg7.js';
4
+ import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-CcPYrM3p.js';
5
+ import { N as NotificationsJobDeps } from '../jobs--fex87-q.js';
6
6
  import { E as EmailPreviewsConfig } from '../catalog-BOVZ2b-b.js';
7
- import '../generators-BsuXe8Mp.js';
8
- import '../types-BFmAY0P5.js';
9
- import '../wire-_coV3tJk.js';
10
- import '../web-push-D-ysQd5h.js';
7
+ import '../generators-qAD4fNPq.js';
8
+ import '../types-BlqZkCWZ.js';
9
+ import '../wire-BG1kuoXX.js';
10
+ import '../web-push-Dnyaha2z.js';
11
11
  import '../template-Dlt5S54z.js';
12
12
 
13
13
  /** One `NotificationsRoute` as the wiring contract reads it. */
@@ -3,13 +3,14 @@ import {
3
3
  } from "../chunk-FTFLU6KH.js";
4
4
  import {
5
5
  NOTIFICATIONS_JOBS
6
- } from "../chunk-KU4FVOFA.js";
6
+ } from "../chunk-ZIR3ILFH.js";
7
7
  import {
8
8
  createApiNotifications
9
- } from "../chunk-KZG7QGFA.js";
10
- import "../chunk-GGAZOLNN.js";
9
+ } from "../chunk-WVRODNXQ.js";
10
+ import "../chunk-SWOWHIFE.js";
11
11
  import "../chunk-M2TVBVH2.js";
12
- import "../chunk-BGI4BQ66.js";
12
+ import "../chunk-RTURLH5U.js";
13
+ import "../chunk-6W7INOYQ.js";
13
14
  import "../chunk-EKUSNUBT.js";
14
15
  import {
15
16
  __name
@@ -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 { emailPreviewRoutes } from '../email/previews/routes';\nimport type { EmailPreviewsConfig } from '../email/previews/catalog';\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\n/**\n * The preview console's server half — the two endpoints over the catalogue.\n *\n * A CONSTANT, not a factory. `http.create(config)` receives whatever the host\n * bound at adoption, and \"which messages exist\" is precisely a host's binding:\n * a package cannot know that a product sends a \"your quota is exhausted\"\n * notice, let alone what data it renders from. Writing `EmailPreviewsConfig`\n * as a factory argument instead would move that decision out of\n * `bindings.http`, where `assemble()` can report on it, into a call the report\n * never sees.\n *\n * The routes carry `kind: 'authenticated'` — see `../email/previews/routes`\n * for why the descriptor states a posture it cannot itself enforce.\n */\nexport const notificationEmailPreviewsServerManifest = {\n name: '@12-apps/notifications-email-previews',\n http: {\n create: (config: EmailPreviewsConfig) => ({ routes: emailPreviewRoutes(config) }),\n },\n} as const satisfies AnyServerManifest;\n"],"mappings":";;;;;;;;;;;;;;;;;;AAsCA,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;AAgBO,IAAM,0CAA0C;AAAA,EACrD,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,QAAQ,wBAAC,YAAiC,EAAE,QAAQ,mBAAmB,MAAM,EAAE,IAAvE;AAAA,EACV;AACF;","names":[]}
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 { emailPreviewRoutes } from '../email/previews/routes';\nimport type { EmailPreviewsConfig } from '../email/previews/catalog';\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\n/**\n * The preview console's server half — the two endpoints over the catalogue.\n *\n * A CONSTANT, not a factory. `http.create(config)` receives whatever the host\n * bound at adoption, and \"which messages exist\" is precisely a host's binding:\n * a package cannot know that a product sends a \"your quota is exhausted\"\n * notice, let alone what data it renders from. Writing `EmailPreviewsConfig`\n * as a factory argument instead would move that decision out of\n * `bindings.http`, where `assemble()` can report on it, into a call the report\n * never sees.\n *\n * The routes carry `kind: 'authenticated'` — see `../email/previews/routes`\n * for why the descriptor states a posture it cannot itself enforce.\n */\nexport const notificationEmailPreviewsServerManifest = {\n name: '@12-apps/notifications-email-previews',\n http: {\n create: (config: EmailPreviewsConfig) => ({ routes: emailPreviewRoutes(config) }),\n },\n} as const satisfies AnyServerManifest;\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAsCA,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;AAgBO,IAAM,0CAA0C;AAAA,EACrD,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,QAAQ,wBAAC,YAAiC,EAAE,QAAQ,mBAAmB,MAAM,EAAE,IAAvE;AAAA,EACV;AACF;","names":[]}
@@ -1,8 +1,9 @@
1
1
  import { c as createEmailPreviewScreen } from '../preview-screen-DYJRAnAY.js';
2
- import { c as createWebNotifications } from '../create-web-notifications-B-xQ6R0B.js';
2
+ import { c as createWebNotifications } from '../create-web-notifications-DV3Y8k7e.js';
3
3
  import 'react';
4
- import '../wire-_coV3tJk.js';
5
- import '../types-BFmAY0P5.js';
4
+ import '../wire-BG1kuoXX.js';
5
+ import '../types-BlqZkCWZ.js';
6
+ import '../live-DYxEFO49.js';
6
7
 
7
8
  /**
8
9
  * `@12-apps/notifications/manifest/web` — the web capabilities.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createWebNotifications
3
- } from "../chunk-M5XTROYM.js";
3
+ } from "../chunk-I5QUMTCN.js";
4
4
  import "../chunk-BW723CX2.js";
5
5
  import {
6
6
  createEmailPreviewScreen