@ttt-productions/notification-core 0.15.17 → 0.15.18

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 (87) hide show
  1. package/README.md +9 -10
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +0 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/react/components/NotificationEmptyState.d.ts +0 -3
  6. package/dist/react/components/NotificationEmptyState.d.ts.map +1 -1
  7. package/dist/react/components/NotificationEmptyState.js +0 -3
  8. package/dist/react/components/NotificationEmptyState.js.map +1 -1
  9. package/dist/react/components/NotificationHistoryList.d.ts +0 -6
  10. package/dist/react/components/NotificationHistoryList.d.ts.map +1 -1
  11. package/dist/react/components/NotificationHistoryList.js +0 -8
  12. package/dist/react/components/NotificationHistoryList.js.map +1 -1
  13. package/dist/react/components/NotificationList.d.ts +0 -3
  14. package/dist/react/components/NotificationList.d.ts.map +1 -1
  15. package/dist/react/components/NotificationList.js +2 -29
  16. package/dist/react/components/NotificationList.js.map +1 -1
  17. package/dist/react/components/NotificationUnreadBadge.d.ts +0 -5
  18. package/dist/react/components/NotificationUnreadBadge.d.ts.map +1 -1
  19. package/dist/react/components/NotificationUnreadBadge.js +0 -5
  20. package/dist/react/components/NotificationUnreadBadge.js.map +1 -1
  21. package/dist/react/components/relative-time.d.ts +0 -3
  22. package/dist/react/components/relative-time.d.ts.map +1 -1
  23. package/dist/react/components/relative-time.js +0 -3
  24. package/dist/react/components/relative-time.js.map +1 -1
  25. package/dist/react/hooks/useActiveNotifications.d.ts +0 -4
  26. package/dist/react/hooks/useActiveNotifications.d.ts.map +1 -1
  27. package/dist/react/hooks/useActiveNotifications.js +2 -6
  28. package/dist/react/hooks/useActiveNotifications.js.map +1 -1
  29. package/dist/react/hooks/useArchiveAllNotifications.d.ts +0 -30
  30. package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
  31. package/dist/react/hooks/useArchiveAllNotifications.js +1 -43
  32. package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
  33. package/dist/react/hooks/useArchiveNotification.d.ts +0 -8
  34. package/dist/react/hooks/useArchiveNotification.d.ts.map +1 -1
  35. package/dist/react/hooks/useArchiveNotification.js +0 -8
  36. package/dist/react/hooks/useArchiveNotification.js.map +1 -1
  37. package/dist/react/hooks/useNotificationHistory.d.ts +0 -14
  38. package/dist/react/hooks/useNotificationHistory.d.ts.map +1 -1
  39. package/dist/react/hooks/useNotificationHistory.js +2 -18
  40. package/dist/react/hooks/useNotificationHistory.js.map +1 -1
  41. package/dist/react/hooks/useUnreadCount.d.ts +0 -22
  42. package/dist/react/hooks/useUnreadCount.d.ts.map +1 -1
  43. package/dist/react/hooks/useUnreadCount.js +1 -22
  44. package/dist/react/hooks/useUnreadCount.js.map +1 -1
  45. package/dist/react/index.d.ts.map +1 -1
  46. package/dist/react/index.js +0 -2
  47. package/dist/react/index.js.map +1 -1
  48. package/dist/react/types.d.ts +0 -42
  49. package/dist/react/types.d.ts.map +1 -1
  50. package/dist/react/types.js.map +1 -1
  51. package/dist/schemas.d.ts +0 -22
  52. package/dist/schemas.d.ts.map +1 -1
  53. package/dist/schemas.js +0 -25
  54. package/dist/schemas.js.map +1 -1
  55. package/dist/server/activeNotificationId.d.ts +1 -22
  56. package/dist/server/activeNotificationId.d.ts.map +1 -1
  57. package/dist/server/activeNotificationId.js +0 -16
  58. package/dist/server/activeNotificationId.js.map +1 -1
  59. package/dist/server/delivery-ledger.d.ts +1 -73
  60. package/dist/server/delivery-ledger.d.ts.map +1 -1
  61. package/dist/server/delivery-ledger.js +7 -59
  62. package/dist/server/delivery-ledger.js.map +1 -1
  63. package/dist/server/errors.d.ts +0 -9
  64. package/dist/server/errors.d.ts.map +1 -1
  65. package/dist/server/errors.js +1 -11
  66. package/dist/server/errors.js.map +1 -1
  67. package/dist/server/index.d.ts.map +1 -1
  68. package/dist/server/index.js +0 -3
  69. package/dist/server/index.js.map +1 -1
  70. package/dist/server/observed-generation.d.ts +1 -41
  71. package/dist/server/observed-generation.d.ts.map +1 -1
  72. package/dist/server/observed-generation.js +1 -31
  73. package/dist/server/observed-generation.js.map +1 -1
  74. package/dist/server/processBatchHelper.d.ts +0 -19
  75. package/dist/server/processBatchHelper.d.ts.map +1 -1
  76. package/dist/server/processBatchHelper.js +2 -46
  77. package/dist/server/processBatchHelper.js.map +1 -1
  78. package/dist/server/types.d.ts +0 -9
  79. package/dist/server/types.d.ts.map +1 -1
  80. package/dist/server/types.js +0 -4
  81. package/dist/server/types.js.map +1 -1
  82. package/dist/types.d.ts +2 -196
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js +0 -6
  85. package/dist/types.js.map +1 -1
  86. package/package.json +5 -5
  87. package/src/styles/notifications.css +0 -14
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ttt-productions/notification-core
2
2
 
3
- Generic notification system for TTT Productions apps — a two-tier
3
+ Generic notification system for consuming apps — a two-tier
4
4
  **active → history** model with dedup, batch processing, and themed React UI.
5
5
 
6
6
  ## Entrypoints
@@ -26,14 +26,13 @@ Duplicate triggers for the same `dedupKey` increment a single active doc's
26
26
  `count`, append the actor, and reset `seenAt` to `0` so new activity re-lights
27
27
  the badge.
28
28
 
29
- **Notifications are Cloud-Functions-only — clients never write notification
30
- docs.** The archive React hooks (`useArchiveNotification` /
29
+ **The React hooks write no notification docs.** The archive React hooks (`useArchiveNotification` /
31
30
  `useArchiveAllNotifications`) perform **no Firestore writes**; they take an
32
- app-supplied `archiveFn` / `archiveAllFn` adapter wired to the app's callable
31
+ app-supplied adapter (`archiveFn`, or `enqueueArchiveAllFn` + `getArchiveAllStatusFn`) wired to the app's callables
33
32
  (e.g. `httpsCallable(functions, 'archiveNotification')`) and invalidate the
34
- read keys on success. On the server, the app's callable enforces ownership
35
- (personal: `targetUserId === callerUid`; shared: caller must be admin) and then
36
- runs the active → history move through `archiveNotificationWithGeneration`,
33
+ read keys on success. On the server, the app's callable decides who may archive
34
+ a card and then runs the active → history move through
35
+ `archiveNotificationWithGeneration`,
37
36
  which keys the history doc on a retry-stable, app-built deterministic id and
38
37
  only archives when the card's observed `activityGeneration` still matches.
39
38
 
@@ -42,7 +41,7 @@ only archives when the card's observed `activityGeneration` still matches.
42
41
  Shared notification docs persist actor **ids only** — `NotificationDoc`
43
42
  exposes `latestActorIds`, and `PendingNotification` exposes `actorId`. There are
44
43
  **no persisted display names** (no `latestActorNames` / `actorName`); names are
45
- resolved at read time by the consuming app (e.g. from `publicUsers`) so they
44
+ resolved at read time by the consuming app from its own public profile source, so they
46
45
  never drift in the stored doc.
47
46
 
48
47
  ## Usage
@@ -51,7 +50,7 @@ never drift in the stored doc.
51
50
  import { useActiveNotifications } from '@ttt-productions/notification-core/react';
52
51
 
53
52
  const { data: notifications, isLoading } = useActiveNotifications({
54
- config: TTT_NOTIFICATION_CONFIG,
53
+ config: NOTIFICATION_CONFIG,
55
54
  userId: currentUser.uid,
56
55
  category: 'user',
57
56
  });
@@ -67,7 +66,7 @@ through the observed-generation helpers (`markNotificationSeenWithGeneration` /
67
66
  ```ts
68
67
  import { createDeliveryLedger } from '@ttt-productions/notification-core/server';
69
68
 
70
- const ledger = createDeliveryLedger(db, TTT_NOTIFICATION_CONFIG);
69
+ const ledger = createDeliveryLedger(db, NOTIFICATION_CONFIG, options);
71
70
  await ledger.enqueue([
72
71
  { deliveryId, notificationType, eventId, recipientUid, aggregationKey, strategy, payload, payloadVersion, materializationClass },
73
72
  ]);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,0BAA0B,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,iCAAiC,EACjC,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,0BAA0B,EAC1B,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,0BAA0B,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,iCAAiC,EACjC,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,0BAA0B,EAC1B,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,2 @@
1
- // Runtime schemas for the generic notification doc shapes (notification-core owns
2
- // these; consuming apps compose them instead of hand-mirroring). Server- and
3
- // UI-safe (zod only, no React/firebase).
4
1
  export { NotificationDocSchema, PendingNotificationSchema } from './schemas.js';
5
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwBA,kFAAkF;AAClF,6EAA6E;AAC7E,yCAAyC;AACzC,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC","sourcesContent":["export type {\n NotificationDoc,\n PendingNotification,\n NotificationCategoryConfig,\n NotificationTypeConfig,\n NotificationSystemConfig,\n UseActiveNotificationsOptions,\n UseNotificationHistoryOptions,\n NotificationHistoryItem,\n UseUnreadCountOptions,\n UseArchiveNotificationOptions,\n UseArchiveAllNotificationsOptions,\n ArchiveAllJobStatus,\n EnqueueArchiveAllResult,\n ArchiveAllJobSnapshot,\n ArchiveAllPollResult,\n NotificationRowActions,\n NotificationArchiveResult,\n NotificationArchiveFn,\n NotificationListErrorState,\n NotificationEmptyStateProps,\n NotificationUnreadBadgeProps,\n} from './types.js';\n\n// Runtime schemas for the generic notification doc shapes (notification-core owns\n// these; consuming apps compose them instead of hand-mirroring). Server- and\n// UI-safe (zod only, no React/firebase).\nexport { NotificationDocSchema, PendingNotificationSchema } from './schemas.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC","sourcesContent":["export type {\n NotificationDoc,\n PendingNotification,\n NotificationCategoryConfig,\n NotificationTypeConfig,\n NotificationSystemConfig,\n UseActiveNotificationsOptions,\n UseNotificationHistoryOptions,\n NotificationHistoryItem,\n UseUnreadCountOptions,\n UseArchiveNotificationOptions,\n UseArchiveAllNotificationsOptions,\n ArchiveAllJobStatus,\n EnqueueArchiveAllResult,\n ArchiveAllJobSnapshot,\n ArchiveAllPollResult,\n NotificationRowActions,\n NotificationArchiveResult,\n NotificationArchiveFn,\n NotificationListErrorState,\n NotificationEmptyStateProps,\n NotificationUnreadBadgeProps,\n} from './types.js';\n\nexport { NotificationDocSchema, PendingNotificationSchema } from './schemas.js';\n"]}
@@ -1,6 +1,3 @@
1
1
  import type { NotificationEmptyStateProps } from '../../types.js';
2
- /**
3
- * Empty state shown when there are no active notifications.
4
- */
5
2
  export declare function NotificationEmptyState({ text, }: NotificationEmptyStateProps): import("react").JSX.Element;
6
3
  //# sourceMappingURL=NotificationEmptyState.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationEmptyState.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationEmptyState.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAElE;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,IAA8B,GAC/B,EAAE,2BAA2B,+BAqB7B"}
1
+ {"version":3,"file":"NotificationEmptyState.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationEmptyState.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAElE,wBAAgB,sBAAsB,CAAC,EACrC,IAA8B,GAC/B,EAAE,2BAA2B,+BAqB7B"}
@@ -1,8 +1,5 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- /**
4
- * Empty state shown when there are no active notifications.
5
- */
6
3
  export function NotificationEmptyState({ text = "You're all caught up!", }) {
7
4
  return (_jsxs("div", { className: "ntf-empty", children: [_jsxs("svg", { className: "ntf-empty-icon", xmlns: "http://www.w3.org/2000/svg", width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }), _jsx("polyline", { points: "22 4 12 14.01 9 11.01" })] }), _jsx("p", { className: "ntf-empty-text", children: text })] }));
8
5
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationEmptyState.js","sourceRoot":"","sources":["../../../src/react/components/NotificationEmptyState.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAIb;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,IAAI,GAAG,uBAAuB,GACF;IAC5B,OAAO,CACL,eAAK,SAAS,EAAC,WAAW,aACxB,eACE,SAAS,EAAC,gBAAgB,EAC1B,KAAK,EAAC,4BAA4B,EAClC,KAAK,EAAC,IAAI,EACV,MAAM,EAAC,IAAI,EACX,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EACf,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,aAEtB,eAAM,CAAC,EAAC,oCAAoC,GAAG,EAC/C,mBAAU,MAAM,EAAC,uBAAuB,GAAG,IACvC,EACN,YAAG,SAAS,EAAC,gBAAgB,YAAE,IAAI,GAAK,IACpC,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport type { NotificationEmptyStateProps } from '../../types.js';\n\n/**\n * Empty state shown when there are no active notifications.\n */\nexport function NotificationEmptyState({\n text = \"You're all caught up!\",\n}: NotificationEmptyStateProps) {\n return (\n <div className=\"ntf-empty\">\n <svg\n className=\"ntf-empty-icon\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"32\"\n height=\"32\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\" />\n <polyline points=\"22 4 12 14.01 9 11.01\" />\n </svg>\n <p className=\"ntf-empty-text\">{text}</p>\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"NotificationEmptyState.js","sourceRoot":"","sources":["../../../src/react/components/NotificationEmptyState.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAIb,MAAM,UAAU,sBAAsB,CAAC,EACrC,IAAI,GAAG,uBAAuB,GACF;IAC5B,OAAO,CACL,eAAK,SAAS,EAAC,WAAW,aACxB,eACE,SAAS,EAAC,gBAAgB,EAC1B,KAAK,EAAC,4BAA4B,EAClC,KAAK,EAAC,IAAI,EACV,MAAM,EAAC,IAAI,EACX,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EACf,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,aAEtB,eAAM,CAAC,EAAC,oCAAoC,GAAG,EAC/C,mBAAU,MAAM,EAAC,uBAAuB,GAAG,IACvC,EACN,YAAG,SAAS,EAAC,gBAAgB,YAAE,IAAI,GAAK,IACpC,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport type { NotificationEmptyStateProps } from '../../types.js';\n\nexport function NotificationEmptyState({\n text = \"You're all caught up!\",\n}: NotificationEmptyStateProps) {\n return (\n <div className=\"ntf-empty\">\n <svg\n className=\"ntf-empty-icon\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"32\"\n height=\"32\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\" />\n <polyline points=\"22 4 12 14.01 9 11.01\" />\n </svg>\n <p className=\"ntf-empty-text\">{text}</p>\n </div>\n );\n}\n"]}
@@ -1,9 +1,3 @@
1
1
  import type { NotificationHistoryListProps } from '../types.js';
2
- /**
3
- * Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are
4
- * inert and read-only — archive is one-way, there is no re-archive, so the row
5
- * exposes no `archive` action. Any per-row control (e.g. an
6
- * ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
7
- */
8
2
  export declare function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, renderError, title, renderRowAction, }: NotificationHistoryListProps): import("react").JSX.Element;
9
3
  //# sourceMappingURL=NotificationHistoryList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationHistoryList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAEhE;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,WAAW,EACX,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+BAgG9B"}
1
+ {"version":3,"file":"NotificationHistoryList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAEhE,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,WAAW,EACX,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+BA8F9B"}
@@ -5,12 +5,6 @@ import { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-c
5
5
  import { useNotificationHistory } from '../hooks/useNotificationHistory.js';
6
6
  import { NotificationEmptyState } from './NotificationEmptyState.js';
7
7
  import { formatRelativeTime } from './relative-time.js';
8
- /**
9
- * Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are
10
- * inert and read-only — archive is one-way, there is no re-archive, so the row
11
- * exposes no `archive` action. Any per-row control (e.g. an
12
- * ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
13
- */
14
8
  export function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, renderError, title, renderRowAction, }) {
15
9
  const { data: notifications, isFetching, retryState, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useNotificationHistory({
16
10
  config,
@@ -20,8 +14,6 @@ export function NotificationHistoryList({ config, userId, category, pageSize, st
20
14
  staleTime,
21
15
  });
22
16
  const hasRows = !!notifications && notifications.length > 0;
23
- // A later page that came back empty keeps its pager so the user can step back; the
24
- // empty state is the answer for page 1 only.
25
17
  const showEmptyState = retryState.error === null &&
26
18
  notifications !== undefined &&
27
19
  notifications.length === 0 &&
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationHistoryList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useNotificationHistory } from '../hooks/useNotificationHistory.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationHistoryItem } from '../../types.js';\nimport type { NotificationHistoryListProps } from '../types.js';\n\n/**\n * Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are\n * inert and read-only — archive is one-way, there is no re-archive, so the row\n * exposes no `archive` action. Any per-row control (e.g. an\n * ArrowRight \"go to\") is rendered by the consumer via `renderRowAction`.\n */\nexport function NotificationHistoryList({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n emptyText,\n renderError,\n title,\n renderRowAction,\n}: NotificationHistoryListProps) {\n const {\n data: notifications,\n isFetching,\n retryState,\n refetch,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useNotificationHistory({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n });\n\n const hasRows = !!notifications && notifications.length > 0;\n // A later page that came back empty keeps its pager so the user can step back; the\n // empty state is the answer for page 1 only.\n const showEmptyState =\n retryState.error === null &&\n notifications !== undefined &&\n notifications.length === 0 &&\n page === 1;\n\n const getTypeIcon = useCallback((type: string) => config.types[type]?.icon ?? '🔔', [config]);\n\n return (\n <div className=\"ntf-list ntf-list-history\">\n {title != null && (\n <>\n <div className=\"ntf-list-header ntf-list-header-read-only\">\n <div className=\"ntf-list-title\">{title}</div>\n </div>\n <Separator />\n </>\n )}\n <div className=\"ntf-list-body\">\n {retryState.isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : (\n <>\n {(notifications ?? []).map((notification: NotificationHistoryItem) => {\n return (\n <div key={notification.archiveOccurrenceId} className=\"ntf-item ntf-item-archived\">\n <div className=\"ntf-item-icon\">{getTypeIcon(notification.type)}</div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.archivedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">{renderRowAction(notification, {})}</div>\n )}\n </div>\n );\n })}\n {retryState.error !== null\n ? renderError({\n error: retryState.error,\n retry: () => {\n void refetch();\n },\n retrying: retryState.isRetrying,\n hasRows,\n })\n : null}\n {showEmptyState && <NotificationEmptyState text={emptyText} />}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"NotificationHistoryList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useNotificationHistory } from '../hooks/useNotificationHistory.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationHistoryItem } from '../../types.js';\nimport type { NotificationHistoryListProps } from '../types.js';\n\nexport function NotificationHistoryList({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n emptyText,\n renderError,\n title,\n renderRowAction,\n}: NotificationHistoryListProps) {\n const {\n data: notifications,\n isFetching,\n retryState,\n refetch,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useNotificationHistory({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n });\n\n const hasRows = !!notifications && notifications.length > 0;\n const showEmptyState =\n retryState.error === null &&\n notifications !== undefined &&\n notifications.length === 0 &&\n page === 1;\n\n const getTypeIcon = useCallback((type: string) => config.types[type]?.icon ?? '🔔', [config]);\n\n return (\n <div className=\"ntf-list ntf-list-history\">\n {title != null && (\n <>\n <div className=\"ntf-list-header ntf-list-header-read-only\">\n <div className=\"ntf-list-title\">{title}</div>\n </div>\n <Separator />\n </>\n )}\n <div className=\"ntf-list-body\">\n {retryState.isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : (\n <>\n {(notifications ?? []).map((notification: NotificationHistoryItem) => {\n return (\n <div key={notification.archiveOccurrenceId} className=\"ntf-item ntf-item-archived\">\n <div className=\"ntf-item-icon\">{getTypeIcon(notification.type)}</div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.archivedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">{renderRowAction(notification, {})}</div>\n )}\n </div>\n );\n })}\n {retryState.error !== null\n ? renderError({\n error: retryState.error,\n retry: () => {\n void refetch();\n },\n retrying: retryState.isRetrying,\n hasRows,\n })\n : null}\n {showEmptyState && <NotificationEmptyState text={emptyText} />}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
@@ -1,6 +1,3 @@
1
1
  import type { NotificationListProps } from '../types.js';
2
- /**
3
- * Scrollable list of active notifications with click-to-archive and clear-all.
4
- */
5
2
  export declare function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, staleTime, emptyText, renderError, onRenderedRowsChange, renderRowAction, }: NotificationListProps): import("react").JSX.Element;
6
3
  //# sourceMappingURL=NotificationList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAoBzD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,EACL,UAAU,EACV,eAAe,EACf,SAAS,EACT,SAAS,EACT,WAAW,EACX,oBAAoB,EACpB,eAAe,GAChB,EAAE,qBAAqB,+BAuSvB"}
1
+ {"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAezD,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,EACL,UAAU,EACV,eAAe,EACf,SAAS,EACT,SAAS,EACT,WAAW,EACX,oBAAoB,EACpB,eAAe,GAChB,EAAE,qBAAqB,+BAkRvB"}
@@ -8,17 +8,10 @@ import { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.
8
8
  import { NotificationEmptyState } from './NotificationEmptyState.js';
9
9
  import { formatRelativeTime } from './relative-time.js';
10
10
  const NO_PENDING = new Map();
11
- /**
12
- * An archive belongs to the card generation it was sent for. A card relit on the same id gets a new
13
- * generation, which the archive never covered, so it is not pending for that card.
14
- */
15
11
  function isPendingFor(pending, notification) {
16
12
  return (pending.has(notification.id) && pending.get(notification.id) === notification.activityGeneration);
17
13
  }
18
14
  const FOCUSABLE = 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
19
- /**
20
- * Scrollable list of active notifications with click-to-archive and clear-all.
21
- */
22
15
  export function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, staleTime, emptyText, renderError, onRenderedRowsChange, renderRowAction, }) {
23
16
  const { data: notifications, isFetching, retryState, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useActiveNotifications({
24
17
  config,
@@ -38,11 +31,6 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
38
31
  enqueueArchiveAllFn,
39
32
  getArchiveAllStatusFn,
40
33
  });
41
- // Surface a failed/incomplete clear instead of swallowing it. `onClearAll` (and the tray-closing
42
- // side effects the app wires to it) fires ONLY when the server job fully drained the category —
43
- // otherwise the user keeps the "notifications remain — try again" affordance. The poller resolves
44
- // with an explicit terminal result (`complete` | `incomplete` | `failed`) and only rejects on a
45
- // hard enqueue/poll throw.
46
34
  const [clearIncomplete, setClearIncomplete] = useState(false);
47
35
  const [pendingArchives, setPendingArchives] = useState(NO_PENDING);
48
36
  const [pendingClearAll, setPendingClearAll] = useState(NO_PENDING);
@@ -52,10 +40,6 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
52
40
  if (notifications)
53
41
  renderedRowsRef.current?.(notifications);
54
42
  }, [notifications]);
55
- // An archive the server confirmed stays visibly pending until the authoritative
56
- // active-notification query removes the row: the callable's answer alone does not
57
- // prove the read has caught up. A row the read lists at a newer generation was relit
58
- // after the archive, so the archive no longer holds it.
59
43
  useEffect(() => {
60
44
  if (!notifications)
61
45
  return;
@@ -67,10 +51,8 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
67
51
  setPendingArchives(keepActive);
68
52
  setPendingClearAll(keepActive);
69
53
  }, [notifications]);
70
- // A row that leaves the list while focus is inside it takes the focused control with it, so focus
71
- // moves to the control of the row that followed it, or to the list itself (FRONTEND-203). A row's
72
- // removal fires no blur, so the row still recorded as focused when the rows change is the row that
73
- // held focus as it left.
54
+ // A removed row takes the focused control with it and fires no blur, so focus moves to the next
55
+ // row's control or to the list (RULE-STD-FRONTEND-203).
74
56
  const listRef = useRef(null);
75
57
  const rowRefs = useRef(new Map());
76
58
  const focusedRowId = useRef(null);
@@ -112,8 +94,6 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
112
94
  setPendingArchives((current) => new Map(current).set(notification.id, generation));
113
95
  try {
114
96
  const result = await archiveMutation.mutateAsync(notification);
115
- // Nothing was archived (the card changed after it was rendered): the row stays
116
- // active, so it is not left looking like it is clearing.
117
97
  if (!result.archived)
118
98
  clearPending();
119
99
  }
@@ -136,20 +116,15 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
136
116
  result = await archiveAllMutation.mutateAsync();
137
117
  }
138
118
  catch {
139
- // Hard failure (enqueue/poll threw): do not fire onClearAll; surface the retry affordance.
140
119
  setPendingClearAll(NO_PENDING);
141
120
  setClearIncomplete(true);
142
121
  return;
143
122
  }
144
123
  if (!result.complete) {
145
- // Job terminated without fully draining the category (incomplete/failed) — keep the
146
- // notifications and prompt a retry.
147
124
  setPendingClearAll(NO_PENDING);
148
125
  setClearIncomplete(true);
149
126
  return;
150
127
  }
151
- // The cards a complete drain left behind stay in the list and never leave the read, so their
152
- // pending is released here rather than waiting on a read that will keep listing them.
153
128
  if (result.notArchived > 0)
154
129
  setPendingClearAll(NO_PENDING);
155
130
  onClearAll?.(result);
@@ -158,8 +133,6 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
158
133
  const typeConfig = config.types[type];
159
134
  return typeConfig?.icon ?? '🔔';
160
135
  }, [config]);
161
- // A later page that came back empty keeps its pager so the user can step back; the
162
- // empty state is the answer for page 1 only.
163
136
  const showEmptyState = retryState.error === null &&
164
137
  notifications !== undefined &&
165
138
  notifications.length === 0 &&
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';\nimport { Badge, Button, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useActiveNotifications } from '../hooks/useActiveNotifications.js';\nimport { useArchiveNotification } from '../hooks/useArchiveNotification.js';\nimport { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationDoc } from '../../types.js';\nimport type { NotificationListProps } from '../types.js';\n\n/** Pending archives keyed by card id, holding the activity generation the archive was sent for. */\ntype PendingArchives = ReadonlyMap<string, string | undefined>;\n\nconst NO_PENDING: PendingArchives = new Map();\n\n/**\n * An archive belongs to the card generation it was sent for. A card relit on the same id gets a new\n * generation, which the archive never covered, so it is not pending for that card.\n */\nfunction isPendingFor(pending: PendingArchives, notification: NotificationDoc): boolean {\n return (\n pending.has(notification.id) && pending.get(notification.id) === notification.activityGeneration\n );\n}\n\nconst FOCUSABLE =\n 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Scrollable list of active notifications with click-to-archive and clear-all.\n */\nexport function NotificationList({\n config,\n userId,\n category,\n archiveFn,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n title,\n onClearAll,\n refetchInterval,\n staleTime,\n emptyText,\n renderError,\n onRenderedRowsChange,\n renderRowAction,\n}: NotificationListProps) {\n const {\n data: notifications,\n isFetching,\n retryState,\n refetch,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useActiveNotifications({\n config,\n userId,\n category,\n refetchInterval,\n staleTime,\n });\n\n const archiveMutation = useArchiveNotification({\n userId,\n category,\n archiveFn,\n });\n\n const archiveAllMutation = useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n });\n\n // Surface a failed/incomplete clear instead of swallowing it. `onClearAll` (and the tray-closing\n // side effects the app wires to it) fires ONLY when the server job fully drained the category —\n // otherwise the user keeps the \"notifications remain — try again\" affordance. The poller resolves\n // with an explicit terminal result (`complete` | `incomplete` | `failed`) and only rejects on a\n // hard enqueue/poll throw.\n const [clearIncomplete, setClearIncomplete] = useState(false);\n const [pendingArchives, setPendingArchives] = useState<PendingArchives>(NO_PENDING);\n const [pendingClearAll, setPendingClearAll] = useState<PendingArchives>(NO_PENDING);\n\n const renderedRowsRef = useRef(onRenderedRowsChange);\n renderedRowsRef.current = onRenderedRowsChange;\n useEffect(() => {\n if (notifications) renderedRowsRef.current?.(notifications);\n }, [notifications]);\n\n // An archive the server confirmed stays visibly pending until the authoritative\n // active-notification query removes the row: the callable's answer alone does not\n // prove the read has caught up. A row the read lists at a newer generation was relit\n // after the archive, so the archive no longer holds it.\n useEffect(() => {\n if (!notifications) return;\n const activeGenerations = new Map(\n notifications.map((notification) => [notification.id, notification.activityGeneration]),\n );\n const keepActive = (current: PendingArchives): PendingArchives => {\n const next = new Map(\n [...current].filter(\n ([id, generation]) =>\n activeGenerations.has(id) && activeGenerations.get(id) === generation,\n ),\n );\n return next.size === current.size ? current : next;\n };\n setPendingArchives(keepActive);\n setPendingClearAll(keepActive);\n }, [notifications]);\n\n // A row that leaves the list while focus is inside it takes the focused control with it, so focus\n // moves to the control of the row that followed it, or to the list itself (FRONTEND-203). A row's\n // removal fires no blur, so the row still recorded as focused when the rows change is the row that\n // held focus as it left.\n const listRef = useRef<HTMLDivElement>(null);\n const rowRefs = useRef(new Map<string, HTMLDivElement>());\n const focusedRowId = useRef<string | null>(null);\n const renderedIds = useRef<readonly string[]>([]);\n useLayoutEffect(() => {\n const ids = retryState.isLoading\n ? []\n : (notifications ?? []).map((notification) => notification.id);\n const previousIds = renderedIds.current;\n renderedIds.current = ids;\n const leftId = focusedRowId.current;\n if (leftId === null || ids.includes(leftId)) return;\n focusedRowId.current = null;\n const active = document.activeElement;\n if (active !== null && active !== document.body && active.isConnected) return;\n\n const remaining = new Set(ids);\n const followers = previousIds\n .slice(previousIds.indexOf(leftId) + 1)\n .filter((id) => remaining.has(id));\n const nextControl = followers\n .map((id) =>\n rowRefs.current\n .get(id)\n ?.querySelector('.ntf-item-row-action')\n ?.querySelector<HTMLElement>(FOCUSABLE),\n )\n .find((control) => control != null);\n (nextControl ?? listRef.current)?.focus();\n }, [notifications, retryState.isLoading]);\n\n const archiveOne = useCallback(\n async (notification: NotificationDoc) => {\n const generation = notification.activityGeneration;\n const clearPending = () =>\n setPendingArchives((current) => {\n if (!current.has(notification.id) || current.get(notification.id) !== generation)\n return current;\n const next = new Map(current);\n next.delete(notification.id);\n return next;\n });\n setPendingArchives((current) => new Map(current).set(notification.id, generation));\n try {\n const result = await archiveMutation.mutateAsync(notification);\n // Nothing was archived (the card changed after it was rendered): the row stays\n // active, so it is not left looking like it is clearing.\n if (!result.archived) clearPending();\n } catch (archiveError) {\n clearPending();\n throw archiveError;\n }\n },\n [archiveMutation],\n );\n\n const hasNotifications = !!notifications && notifications.length > 0;\n\n const handleClearAll = useCallback(async () => {\n if (!hasNotifications) return;\n setClearIncomplete(false);\n setPendingClearAll(\n new Map(\n (notifications ?? []).map((notification) => [\n notification.id,\n notification.activityGeneration,\n ]),\n ),\n );\n let result;\n try {\n result = await archiveAllMutation.mutateAsync();\n } catch {\n // Hard failure (enqueue/poll threw): do not fire onClearAll; surface the retry affordance.\n setPendingClearAll(NO_PENDING);\n setClearIncomplete(true);\n return;\n }\n if (!result.complete) {\n // Job terminated without fully draining the category (incomplete/failed) — keep the\n // notifications and prompt a retry.\n setPendingClearAll(NO_PENDING);\n setClearIncomplete(true);\n return;\n }\n // The cards a complete drain left behind stay in the list and never leave the read, so their\n // pending is released here rather than waiting on a read that will keep listing them.\n if (result.notArchived > 0) setPendingClearAll(NO_PENDING);\n onClearAll?.(result);\n }, [archiveAllMutation, hasNotifications, notifications, onClearAll]);\n\n const getTypeIcon = useCallback(\n (type: string) => {\n const typeConfig = config.types[type];\n return typeConfig?.icon ?? '🔔';\n },\n [config],\n );\n\n // A later page that came back empty keeps its pager so the user can step back; the\n // empty state is the answer for page 1 only.\n const showEmptyState =\n retryState.error === null &&\n notifications !== undefined &&\n notifications.length === 0 &&\n page === 1;\n const isClearAllPending =\n archiveAllMutation.isPending ||\n (notifications ?? []).some((notification) => isPendingFor(pendingClearAll, notification));\n const titleId = useId();\n\n return (\n <div\n ref={listRef}\n className=\"ntf-list\"\n role=\"group\"\n aria-labelledby={title != null ? titleId : undefined}\n tabIndex={-1}\n >\n <div className=\"ntf-list-header\">\n {title != null && (\n <div id={titleId} className=\"ntf-list-title\">\n {title}\n </div>\n )}\n {/* Unavailable with nothing to clear, but never natively disabled: the list empties under\n a Clear All that still holds focus, and focus must keep a destination (FRONTEND-203). */}\n <Button\n type=\"button\"\n className=\"ntf-list-clear-all\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClearAll}\n aria-disabled={!hasNotifications || undefined}\n pending={isClearAllPending}\n >\n {isClearAllPending ? 'Clearing...' : 'Clear All'}\n </Button>\n {clearIncomplete && !isClearAllPending && (\n <div className=\"ntf-list-clear-error\" role=\"status\">\n Some notifications remain — try again.\n </div>\n )}\n </div>\n <Separator />\n\n <div className=\"ntf-list-body\">\n {retryState.isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : (\n <>\n {(notifications ?? []).map((notification: NotificationDoc) => (\n <div\n key={notification.id}\n ref={(row) => {\n if (row) rowRefs.current.set(notification.id, row);\n else rowRefs.current.delete(notification.id);\n }}\n className=\"ntf-item\"\n onFocus={() => {\n focusedRowId.current = notification.id;\n }}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n focusedRowId.current = null;\n }\n }}\n >\n <div className=\"ntf-item-icon\">{getTypeIcon(notification.type)}</div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.updatedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {\n archive: () => archiveOne(notification),\n isArchivePending:\n isPendingFor(pendingClearAll, notification) ||\n isPendingFor(pendingArchives, notification),\n })}\n </div>\n )}\n </div>\n ))}\n {retryState.error !== null\n ? renderError({\n error: retryState.error,\n retry: () => {\n void refetch();\n },\n retrying: retryState.isRetrying,\n hasRows: hasNotifications,\n })\n : null}\n {showEmptyState && <NotificationEmptyState text={emptyText} />}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';\nimport { Badge, Button, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useActiveNotifications } from '../hooks/useActiveNotifications.js';\nimport { useArchiveNotification } from '../hooks/useArchiveNotification.js';\nimport { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationDoc } from '../../types.js';\nimport type { NotificationListProps } from '../types.js';\n\ntype PendingArchives = ReadonlyMap<string, string | undefined>;\n\nconst NO_PENDING: PendingArchives = new Map();\n\nfunction isPendingFor(pending: PendingArchives, notification: NotificationDoc): boolean {\n return (\n pending.has(notification.id) && pending.get(notification.id) === notification.activityGeneration\n );\n}\n\nconst FOCUSABLE =\n 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\nexport function NotificationList({\n config,\n userId,\n category,\n archiveFn,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n title,\n onClearAll,\n refetchInterval,\n staleTime,\n emptyText,\n renderError,\n onRenderedRowsChange,\n renderRowAction,\n}: NotificationListProps) {\n const {\n data: notifications,\n isFetching,\n retryState,\n refetch,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useActiveNotifications({\n config,\n userId,\n category,\n refetchInterval,\n staleTime,\n });\n\n const archiveMutation = useArchiveNotification({\n userId,\n category,\n archiveFn,\n });\n\n const archiveAllMutation = useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n });\n\n const [clearIncomplete, setClearIncomplete] = useState(false);\n const [pendingArchives, setPendingArchives] = useState<PendingArchives>(NO_PENDING);\n const [pendingClearAll, setPendingClearAll] = useState<PendingArchives>(NO_PENDING);\n\n const renderedRowsRef = useRef(onRenderedRowsChange);\n renderedRowsRef.current = onRenderedRowsChange;\n useEffect(() => {\n if (notifications) renderedRowsRef.current?.(notifications);\n }, [notifications]);\n\n useEffect(() => {\n if (!notifications) return;\n const activeGenerations = new Map(\n notifications.map((notification) => [notification.id, notification.activityGeneration]),\n );\n const keepActive = (current: PendingArchives): PendingArchives => {\n const next = new Map(\n [...current].filter(\n ([id, generation]) =>\n activeGenerations.has(id) && activeGenerations.get(id) === generation,\n ),\n );\n return next.size === current.size ? current : next;\n };\n setPendingArchives(keepActive);\n setPendingClearAll(keepActive);\n }, [notifications]);\n\n // A removed row takes the focused control with it and fires no blur, so focus moves to the next\n // row's control or to the list (RULE-STD-FRONTEND-203).\n const listRef = useRef<HTMLDivElement>(null);\n const rowRefs = useRef(new Map<string, HTMLDivElement>());\n const focusedRowId = useRef<string | null>(null);\n const renderedIds = useRef<readonly string[]>([]);\n useLayoutEffect(() => {\n const ids = retryState.isLoading\n ? []\n : (notifications ?? []).map((notification) => notification.id);\n const previousIds = renderedIds.current;\n renderedIds.current = ids;\n const leftId = focusedRowId.current;\n if (leftId === null || ids.includes(leftId)) return;\n focusedRowId.current = null;\n const active = document.activeElement;\n if (active !== null && active !== document.body && active.isConnected) return;\n\n const remaining = new Set(ids);\n const followers = previousIds\n .slice(previousIds.indexOf(leftId) + 1)\n .filter((id) => remaining.has(id));\n const nextControl = followers\n .map((id) =>\n rowRefs.current\n .get(id)\n ?.querySelector('.ntf-item-row-action')\n ?.querySelector<HTMLElement>(FOCUSABLE),\n )\n .find((control) => control != null);\n (nextControl ?? listRef.current)?.focus();\n }, [notifications, retryState.isLoading]);\n\n const archiveOne = useCallback(\n async (notification: NotificationDoc) => {\n const generation = notification.activityGeneration;\n const clearPending = () =>\n setPendingArchives((current) => {\n if (!current.has(notification.id) || current.get(notification.id) !== generation)\n return current;\n const next = new Map(current);\n next.delete(notification.id);\n return next;\n });\n setPendingArchives((current) => new Map(current).set(notification.id, generation));\n try {\n const result = await archiveMutation.mutateAsync(notification);\n if (!result.archived) clearPending();\n } catch (archiveError) {\n clearPending();\n throw archiveError;\n }\n },\n [archiveMutation],\n );\n\n const hasNotifications = !!notifications && notifications.length > 0;\n\n const handleClearAll = useCallback(async () => {\n if (!hasNotifications) return;\n setClearIncomplete(false);\n setPendingClearAll(\n new Map(\n (notifications ?? []).map((notification) => [\n notification.id,\n notification.activityGeneration,\n ]),\n ),\n );\n let result;\n try {\n result = await archiveAllMutation.mutateAsync();\n } catch {\n setPendingClearAll(NO_PENDING);\n setClearIncomplete(true);\n return;\n }\n if (!result.complete) {\n setPendingClearAll(NO_PENDING);\n setClearIncomplete(true);\n return;\n }\n if (result.notArchived > 0) setPendingClearAll(NO_PENDING);\n onClearAll?.(result);\n }, [archiveAllMutation, hasNotifications, notifications, onClearAll]);\n\n const getTypeIcon = useCallback(\n (type: string) => {\n const typeConfig = config.types[type];\n return typeConfig?.icon ?? '🔔';\n },\n [config],\n );\n\n const showEmptyState =\n retryState.error === null &&\n notifications !== undefined &&\n notifications.length === 0 &&\n page === 1;\n const isClearAllPending =\n archiveAllMutation.isPending ||\n (notifications ?? []).some((notification) => isPendingFor(pendingClearAll, notification));\n const titleId = useId();\n\n return (\n <div\n ref={listRef}\n className=\"ntf-list\"\n role=\"group\"\n aria-labelledby={title != null ? titleId : undefined}\n tabIndex={-1}\n >\n <div className=\"ntf-list-header\">\n {title != null && (\n <div id={titleId} className=\"ntf-list-title\">\n {title}\n </div>\n )}\n {/* Never natively disabled: focus must keep a destination as the list empties (RULE-STD-FRONTEND-203). */}\n <Button\n type=\"button\"\n className=\"ntf-list-clear-all\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClearAll}\n aria-disabled={!hasNotifications || undefined}\n pending={isClearAllPending}\n >\n {isClearAllPending ? 'Clearing...' : 'Clear All'}\n </Button>\n {clearIncomplete && !isClearAllPending && (\n <div className=\"ntf-list-clear-error\" role=\"status\">\n Some notifications remain — try again.\n </div>\n )}\n </div>\n <Separator />\n\n <div className=\"ntf-list-body\">\n {retryState.isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : (\n <>\n {(notifications ?? []).map((notification: NotificationDoc) => (\n <div\n key={notification.id}\n ref={(row) => {\n if (row) rowRefs.current.set(notification.id, row);\n else rowRefs.current.delete(notification.id);\n }}\n className=\"ntf-item\"\n onFocus={() => {\n focusedRowId.current = notification.id;\n }}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n focusedRowId.current = null;\n }\n }}\n >\n <div className=\"ntf-item-icon\">{getTypeIcon(notification.type)}</div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.updatedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {\n archive: () => archiveOne(notification),\n isArchivePending:\n isPendingFor(pendingClearAll, notification) ||\n isPendingFor(pendingArchives, notification),\n })}\n </div>\n )}\n </div>\n ))}\n {retryState.error !== null\n ? renderError({\n error: retryState.error,\n retry: () => {\n void refetch();\n },\n retrying: retryState.isRetrying,\n hasRows: hasNotifications,\n })\n : null}\n {showEmptyState && <NotificationEmptyState text={emptyText} />}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
@@ -1,8 +1,3 @@
1
1
  import type { NotificationUnreadBadgeProps } from '../../types.js';
2
- /**
3
- * Unread count badge (red circle with number).
4
- * Renders nothing when count is 0.
5
- * Apps wrap this around whatever trigger element they want.
6
- */
7
2
  export declare function NotificationUnreadBadge({ config, userId, category, refetchInterval, }: NotificationUnreadBadgeProps): import("react").JSX.Element | null;
8
3
  //# sourceMappingURL=NotificationUnreadBadge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationUnreadBadge.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationUnreadBadge.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAEnE;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,eAAe,GAChB,EAAE,4BAA4B,sCAa9B"}
1
+ {"version":3,"file":"NotificationUnreadBadge.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationUnreadBadge.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAEnE,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,eAAe,GAChB,EAAE,4BAA4B,sCAa9B"}
@@ -1,11 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useUnreadCount } from '../hooks/useUnreadCount.js';
4
- /**
5
- * Unread count badge (red circle with number).
6
- * Renders nothing when count is 0.
7
- * Apps wrap this around whatever trigger element they want.
8
- */
9
4
  export function NotificationUnreadBadge({ config, userId, category, refetchInterval, }) {
10
5
  const { count, hasMore } = useUnreadCount({
11
6
  config,
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationUnreadBadge.js","sourceRoot":"","sources":["../../../src/react/components/NotificationUnreadBadge.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG5D;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,eAAe,GACc;IAC7B,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC;QACxC,MAAM;QACN,MAAM;QACN,QAAQ;QACR,eAAe;KAChB,CAAC,CAAC;IAEH,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE7B,MAAM,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAExE,OAAO,eAAM,SAAS,EAAC,WAAW,YAAE,YAAY,GAAQ,CAAC;AAC3D,CAAC","sourcesContent":["'use client';\n\nimport { useUnreadCount } from '../hooks/useUnreadCount.js';\nimport type { NotificationUnreadBadgeProps } from '../../types.js';\n\n/**\n * Unread count badge (red circle with number).\n * Renders nothing when count is 0.\n * Apps wrap this around whatever trigger element they want.\n */\nexport function NotificationUnreadBadge({\n config,\n userId,\n category,\n refetchInterval,\n}: NotificationUnreadBadgeProps) {\n const { count, hasMore } = useUnreadCount({\n config,\n userId,\n category,\n refetchInterval,\n });\n\n if (count === 0) return null;\n\n const displayCount = hasMore ? '99+' : count > 9 ? '9+' : String(count);\n\n return <span className=\"ntf-badge\">{displayCount}</span>;\n}\n"]}
1
+ {"version":3,"file":"NotificationUnreadBadge.js","sourceRoot":"","sources":["../../../src/react/components/NotificationUnreadBadge.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG5D,MAAM,UAAU,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,eAAe,GACc;IAC7B,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC;QACxC,MAAM;QACN,MAAM;QACN,QAAQ;QACR,eAAe;KAChB,CAAC,CAAC;IAEH,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE7B,MAAM,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAExE,OAAO,eAAM,SAAS,EAAC,WAAW,YAAE,YAAY,GAAQ,CAAC;AAC3D,CAAC","sourcesContent":["'use client';\n\nimport { useUnreadCount } from '../hooks/useUnreadCount.js';\nimport type { NotificationUnreadBadgeProps } from '../../types.js';\n\nexport function NotificationUnreadBadge({\n config,\n userId,\n category,\n refetchInterval,\n}: NotificationUnreadBadgeProps) {\n const { count, hasMore } = useUnreadCount({\n config,\n userId,\n category,\n refetchInterval,\n });\n\n if (count === 0) return null;\n\n const displayCount = hasMore ? '99+' : count > 9 ? '9+' : String(count);\n\n return <span className=\"ntf-badge\">{displayCount}</span>;\n}\n"]}
@@ -1,5 +1,2 @@
1
- /**
2
- * Format epoch ms to a relative time string (e.g. "2m ago", "3h ago", "1d ago").
3
- */
4
1
  export declare function formatRelativeTime(epochMs: number): string;
5
2
  //# sourceMappingURL=relative-time.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"relative-time.d.ts","sourceRoot":"","sources":["../../../src/react/components/relative-time.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAc1D"}
1
+ {"version":3,"file":"relative-time.d.ts","sourceRoot":"","sources":["../../../src/react/components/relative-time.ts"],"names":[],"mappings":"AAAA,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAc1D"}
@@ -1,6 +1,3 @@
1
- /**
2
- * Format epoch ms to a relative time string (e.g. "2m ago", "3h ago", "1d ago").
3
- */
4
1
  export function formatRelativeTime(epochMs) {
5
2
  const now = Date.now();
6
3
  const diffMs = now - epochMs;
@@ -1 +1 @@
1
- {"version":3,"file":"relative-time.js","sourceRoot":"","sources":["../../../src/react/components/relative-time.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;IAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IAExC,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,UAAU,CAAC;IACpC,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,GAAG,OAAO,OAAO,CAAC;IAC3C,IAAI,MAAM,GAAG,EAAE;QAAE,OAAO,GAAG,MAAM,OAAO,CAAC;IACzC,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,GAAG,OAAO,OAAO,CAAC;IAC1C,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;IAC3D,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC;AAC7C,CAAC","sourcesContent":["/**\n * Format epoch ms to a relative time string (e.g. \"2m ago\", \"3h ago\", \"1d ago\").\n */\nexport function formatRelativeTime(epochMs: number): string {\n const now = Date.now();\n const diffMs = now - epochMs;\n const diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffSec / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffSec < 60) return 'just now';\n if (diffMin < 60) return `${diffMin}m ago`;\n if (diffHr < 24) return `${diffHr}h ago`;\n if (diffDay < 7) return `${diffDay}d ago`;\n if (diffDay < 30) return `${Math.floor(diffDay / 7)}w ago`;\n return `${Math.floor(diffDay / 30)}mo ago`;\n}\n"]}
1
+ {"version":3,"file":"relative-time.js","sourceRoot":"","sources":["../../../src/react/components/relative-time.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;IAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IAExC,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,UAAU,CAAC;IACpC,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,GAAG,OAAO,OAAO,CAAC;IAC3C,IAAI,MAAM,GAAG,EAAE;QAAE,OAAO,GAAG,MAAM,OAAO,CAAC;IACzC,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,GAAG,OAAO,OAAO,CAAC;IAC1C,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;IAC3D,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC;AAC7C,CAAC","sourcesContent":["export function formatRelativeTime(epochMs: number): string {\n const now = Date.now();\n const diffMs = now - epochMs;\n const diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffSec / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffSec < 60) return 'just now';\n if (diffMin < 60) return `${diffMin}m ago`;\n if (diffHr < 24) return `${diffHr}h ago`;\n if (diffDay < 7) return `${diffDay}d ago`;\n if (diffDay < 30) return `${Math.floor(diffDay / 7)}w ago`;\n return `${Math.floor(diffDay / 30)}mo ago`;\n}\n"]}
@@ -1,8 +1,4 @@
1
1
  import type { NotificationDoc, UseActiveNotificationsOptions } from '../../types.js';
2
- /**
3
- * Paginated read of active notifications. The displayed page is re-read every
4
- * `refetchInterval` ms while mounted — polling, not a realtime listener, for cost.
5
- */
6
2
  export declare function useActiveNotifications({ config, userId, category, enabled, pageSize, refetchInterval, staleTime, }: UseActiveNotificationsOptions): {
7
3
  retryState: import("@ttt-productions/query-core/react").ReadRetryState<Error>;
8
4
  promise: Promise<import("@ttt-productions/query-core/react").PaginatedPage<import("@ttt-productions/query-core/types").WithId<NotificationDoc>>>;
@@ -1 +1 @@
1
- {"version":3,"file":"useActiveNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAMrF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,QAA4B,EAC5B,eAA0C,EAC1C,SAA8B,GAC/B,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2B/B"}
1
+ {"version":3,"file":"useActiveNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAMrF,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,QAA4B,EAC5B,eAA0C,EAC1C,SAA8B,GAC/B,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2B/B"}
@@ -4,10 +4,6 @@ import { orderBy, where } from 'firebase/firestore';
4
4
  const DEFAULT_PAGE_SIZE = 20;
5
5
  const DEFAULT_REFETCH_INTERVAL = 30_000;
6
6
  const DEFAULT_STALE_TIME = 30_000;
7
- /**
8
- * Paginated read of active notifications. The displayed page is re-read every
9
- * `refetchInterval` ms while mounted — polling, not a realtime listener, for cost.
10
- */
11
7
  export function useActiveNotifications({ config, userId, category, enabled = true, pageSize = DEFAULT_PAGE_SIZE, refetchInterval = DEFAULT_REFETCH_INTERVAL, staleTime = DEFAULT_STALE_TIME, }) {
12
8
  const categoryConfig = config.categories[category];
13
9
  if (!categoryConfig) {
@@ -29,8 +25,8 @@ export function useActiveNotifications({ config, userId, category, enabled = tru
29
25
  refetchInterval,
30
26
  staleTime,
31
27
  });
32
- // The displayed page's failure held through its retry: a refetch of a never-succeeded page clears
33
- // `error` while it runs, so the list would drop its failure under the retry the person pressed.
28
+ // A refetch of a never-succeeded page clears `error` while it runs, so the failure is held
29
+ // through the retry.
34
30
  return { ...read, retryState: useReadRetryState(read, paginatedPageKey(queryKey, read.page)) };
35
31
  }
36
32
  //# sourceMappingURL=useActiveNotifications.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useActiveNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAwB,MAAM,oBAAoB,CAAC;AAG1E,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,GAAG,IAAI,EACd,QAAQ,GAAG,iBAAiB,EAC5B,eAAe,GAAG,wBAAwB,EAC1C,SAAS,GAAG,kBAAkB,GACA;IAC9B,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACnD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,yCAAyC,QAAQ,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,cAAc,GAAG,cAAc,CAAC,UAAU,CAAC;IACjD,MAAM,UAAU,GAAG,cAAc,CAAC,YAAY,KAAK,UAAU,CAAC;IAE9D,MAAM,WAAW,GAAsB;QACrC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC;KAC7B,CAAC;IAEF,MAAM,QAAQ,GAAG,CAAC,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,qBAAqB,CAAkB;QAClD,cAAc;QACd,QAAQ;QACR,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,eAAe;QACf,SAAS;KACV,CAAC,CAAC;IACH,kGAAkG;IAClG,gGAAgG;IAChG,OAAO,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AACjG,CAAC","sourcesContent":["'use client';\n\nimport {\n paginatedPageKey,\n useFirestorePaginated,\n useReadRetryState,\n} from '@ttt-productions/query-core/react';\nimport { orderBy, where, type QueryConstraint } from 'firebase/firestore';\nimport type { NotificationDoc, UseActiveNotificationsOptions } from '../../types.js';\n\nconst DEFAULT_PAGE_SIZE = 20;\nconst DEFAULT_REFETCH_INTERVAL = 30_000;\nconst DEFAULT_STALE_TIME = 30_000;\n\n/**\n * Paginated read of active notifications. The displayed page is re-read every\n * `refetchInterval` ms while mounted — polling, not a realtime listener, for cost.\n */\nexport function useActiveNotifications({\n config,\n userId,\n category,\n enabled = true,\n pageSize = DEFAULT_PAGE_SIZE,\n refetchInterval = DEFAULT_REFETCH_INTERVAL,\n staleTime = DEFAULT_STALE_TIME,\n}: UseActiveNotificationsOptions) {\n const categoryConfig = config.categories[category];\n if (!categoryConfig) {\n throw new Error(`[notification-core] Unknown category: ${category}`);\n }\n\n const collectionPath = categoryConfig.activePath;\n const isPersonal = categoryConfig.audienceType === 'personal';\n\n const constraints: QueryConstraint[] = [\n ...(isPersonal ? [where('targetUserId', '==', userId)] : []),\n orderBy('updatedAt', 'desc'),\n ];\n\n const queryKey = ['notifications', 'active', category, userId, { pageSize }];\n const read = useFirestorePaginated<NotificationDoc>({\n collectionPath,\n queryKey,\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n refetchInterval,\n staleTime,\n });\n // The displayed page's failure held through its retry: a refetch of a never-succeeded page clears\n // `error` while it runs, so the list would drop its failure under the retry the person pressed.\n return { ...read, retryState: useReadRetryState(read, paginatedPageKey(queryKey, read.page)) };\n}\n"]}
1
+ {"version":3,"file":"useActiveNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAwB,MAAM,oBAAoB,CAAC;AAG1E,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,GAAG,IAAI,EACd,QAAQ,GAAG,iBAAiB,EAC5B,eAAe,GAAG,wBAAwB,EAC1C,SAAS,GAAG,kBAAkB,GACA;IAC9B,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACnD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,yCAAyC,QAAQ,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,cAAc,GAAG,cAAc,CAAC,UAAU,CAAC;IACjD,MAAM,UAAU,GAAG,cAAc,CAAC,YAAY,KAAK,UAAU,CAAC;IAE9D,MAAM,WAAW,GAAsB;QACrC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC;KAC7B,CAAC;IAEF,MAAM,QAAQ,GAAG,CAAC,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,qBAAqB,CAAkB;QAClD,cAAc;QACd,QAAQ;QACR,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,eAAe;QACf,SAAS;KACV,CAAC,CAAC;IACH,2FAA2F;IAC3F,qBAAqB;IACrB,OAAO,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AACjG,CAAC","sourcesContent":["'use client';\n\nimport {\n paginatedPageKey,\n useFirestorePaginated,\n useReadRetryState,\n} from '@ttt-productions/query-core/react';\nimport { orderBy, where, type QueryConstraint } from 'firebase/firestore';\nimport type { NotificationDoc, UseActiveNotificationsOptions } from '../../types.js';\n\nconst DEFAULT_PAGE_SIZE = 20;\nconst DEFAULT_REFETCH_INTERVAL = 30_000;\nconst DEFAULT_STALE_TIME = 30_000;\n\nexport function useActiveNotifications({\n config,\n userId,\n category,\n enabled = true,\n pageSize = DEFAULT_PAGE_SIZE,\n refetchInterval = DEFAULT_REFETCH_INTERVAL,\n staleTime = DEFAULT_STALE_TIME,\n}: UseActiveNotificationsOptions) {\n const categoryConfig = config.categories[category];\n if (!categoryConfig) {\n throw new Error(`[notification-core] Unknown category: ${category}`);\n }\n\n const collectionPath = categoryConfig.activePath;\n const isPersonal = categoryConfig.audienceType === 'personal';\n\n const constraints: QueryConstraint[] = [\n ...(isPersonal ? [where('targetUserId', '==', userId)] : []),\n orderBy('updatedAt', 'desc'),\n ];\n\n const queryKey = ['notifications', 'active', category, userId, { pageSize }];\n const read = useFirestorePaginated<NotificationDoc>({\n collectionPath,\n queryKey,\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n refetchInterval,\n staleTime,\n });\n // A refetch of a never-succeeded page clears `error` while it runs, so the failure is held\n // through the retry.\n return { ...read, retryState: useReadRetryState(read, paginatedPageKey(queryKey, read.page)) };\n}\n"]}
@@ -1,33 +1,3 @@
1
1
  import type { ArchiveAllPollResult, UseArchiveAllNotificationsOptions } from '../../types.js';
2
- /**
3
- * Trigger + poll a SERVER-OWNED archive-all job for one notification category.
4
- *
5
- * This is a THIN STATUS POLLER, not a client loop. It:
6
- * 1. enqueues ONE server job via `enqueueArchiveAllFn(category)` (the category — the tab the tray
7
- * was invoked on — flows into the job, preserving per-tab scoping: the worker archives ONLY
8
- * that category's active cards, never all tabs);
9
- * 2. polls `getArchiveAllStatusFn(jobId)` until the job reaches a terminal state
10
- * (`complete` | `incomplete` | `failed`);
11
- * 3. resolves an {@link ArchiveAllPollResult} the caller gates side effects on, and invalidates
12
- * the read keys on a completed drain.
13
- *
14
- * All paging + ownership-checked per-card deletion happen server-side. The hook performs no client
15
- * Firestore writes and never chains archive calls. The generic package hardcodes no callable name —
16
- * the app injects both adapters.
17
- *
18
- * @example
19
- * ```tsx
20
- * const enqueue = httpsCallable(functions, 'enqueueArchiveAll');
21
- * const getStatus = httpsCallable(functions, 'getArchiveAllStatus');
22
- * const archiveAll = useArchiveAllNotifications({
23
- * userId: user.uid,
24
- * category: 'user',
25
- * enqueueArchiveAllFn: (category) => enqueue({ category }).then((r) => r.data),
26
- * getArchiveAllStatusFn: (jobId) => getStatus({ jobId }).then((r) => r.data),
27
- * });
28
- *
29
- * archiveAll.mutate();
30
- * ```
31
- */
32
2
  export declare function useArchiveAllNotifications({ userId, category, enqueueArchiveAllFn, getArchiveAllStatusFn, pollIntervalMs, maxPolls, invalidateKeys, }: UseArchiveAllNotificationsOptions): import("@tanstack/react-query").UseMutationResult<ArchiveAllPollResult, Error, void, unknown>;
33
3
  //# sourceMappingURL=useArchiveAllNotifications.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useArchiveAllNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,iCAAiC,EAAE,MAAM,gBAAgB,CAAC;AAS9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,qBAAqB,EACrB,cAAyC,EACzC,QAA4B,EAC5B,cAAc,GACf,EAAE,iCAAiC,iGA8EnC"}
1
+ {"version":3,"file":"useArchiveAllNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,iCAAiC,EAAE,MAAM,gBAAgB,CAAC;AAS9F,wBAAgB,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,qBAAqB,EACrB,cAAyC,EACzC,QAA4B,EAC5B,cAAc,GACf,EAAE,iCAAiC,iGAkEnC"}