@ttt-productions/notification-core 0.15.9 → 0.15.10

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 (55) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/react/components/NotificationHistoryList.d.ts +1 -1
  5. package/dist/react/components/NotificationHistoryList.d.ts.map +1 -1
  6. package/dist/react/components/NotificationHistoryList.js +6 -6
  7. package/dist/react/components/NotificationHistoryList.js.map +1 -1
  8. package/dist/react/components/NotificationList.d.ts +1 -1
  9. package/dist/react/components/NotificationList.d.ts.map +1 -1
  10. package/dist/react/components/NotificationList.js +65 -10
  11. package/dist/react/components/NotificationList.js.map +1 -1
  12. package/dist/react/components/NotificationUnreadBadge.d.ts +1 -1
  13. package/dist/react/components/NotificationUnreadBadge.d.ts.map +1 -1
  14. package/dist/react/components/NotificationUnreadBadge.js +2 -1
  15. package/dist/react/components/NotificationUnreadBadge.js.map +1 -1
  16. package/dist/react/components/notification-type-icon.d.ts +11 -0
  17. package/dist/react/components/notification-type-icon.d.ts.map +1 -0
  18. package/dist/react/components/notification-type-icon.js +12 -0
  19. package/dist/react/components/notification-type-icon.js.map +1 -0
  20. package/dist/react/hooks/useActiveNotifications.d.ts +1 -1
  21. package/dist/react/hooks/useActiveNotifications.d.ts.map +1 -1
  22. package/dist/react/hooks/useActiveNotifications.js +3 -2
  23. package/dist/react/hooks/useActiveNotifications.js.map +1 -1
  24. package/dist/react/hooks/useArchiveAllNotifications.d.ts +1 -1
  25. package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
  26. package/dist/react/hooks/useArchiveAllNotifications.js +3 -7
  27. package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
  28. package/dist/react/hooks/useArchiveNotification.d.ts +1 -1
  29. package/dist/react/hooks/useArchiveNotification.d.ts.map +1 -1
  30. package/dist/react/hooks/useArchiveNotification.js +3 -7
  31. package/dist/react/hooks/useArchiveNotification.js.map +1 -1
  32. package/dist/react/hooks/useNotificationHistory.d.ts +1 -1
  33. package/dist/react/hooks/useNotificationHistory.d.ts.map +1 -1
  34. package/dist/react/hooks/useNotificationHistory.js +3 -2
  35. package/dist/react/hooks/useNotificationHistory.js.map +1 -1
  36. package/dist/react/hooks/useUnreadCount.d.ts +10 -2
  37. package/dist/react/hooks/useUnreadCount.d.ts.map +1 -1
  38. package/dist/react/hooks/useUnreadCount.js +16 -6
  39. package/dist/react/hooks/useUnreadCount.js.map +1 -1
  40. package/dist/react/query-keys.d.ts +5 -0
  41. package/dist/react/query-keys.d.ts.map +1 -0
  42. package/dist/react/query-keys.js +15 -0
  43. package/dist/react/query-keys.js.map +1 -0
  44. package/dist/server/delivery-ledger.d.ts +34 -5
  45. package/dist/server/delivery-ledger.d.ts.map +1 -1
  46. package/dist/server/delivery-ledger.js +24 -5
  47. package/dist/server/delivery-ledger.js.map +1 -1
  48. package/dist/server/index.d.ts +1 -1
  49. package/dist/server/index.d.ts.map +1 -1
  50. package/dist/server/index.js.map +1 -1
  51. package/dist/types.d.ts +65 -5
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/types.js.map +1 -1
  54. package/package.json +5 -5
  55. package/src/styles/notifications.css +10 -33
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export type { NotificationDoc, PendingNotification, NotificationCategoryConfig, NotificationTypeConfig, NotificationSystemConfig, UseActiveNotificationsOptions, UseNotificationHistoryOptions, NotificationHistoryItem, UseUnreadCountOptions, UseArchiveNotificationOptions, UseArchiveAllNotificationsOptions, ArchiveAllJobStatus, EnqueueArchiveAllResult, ArchiveAllJobSnapshot, ArchiveAllPollResult, NotificationRowActions, NotificationArchiveResult, NotificationArchiveFn, NotificationListErrorState, NotificationListProps, NotificationHistoryListProps, NotificationEmptyStateProps, NotificationUnreadBadgeProps, } from './types.js';
1
+ export type { NotificationDoc, PendingNotification, NotificationCategoryConfig, NotificationTypeConfig, NotificationSystemConfig, UseActiveNotificationsOptions, UseNotificationHistoryOptions, NotificationHistoryItem, UseUnreadCountOptions, UseArchiveNotificationOptions, UseArchiveAllNotificationsOptions, ArchiveAllJobStatus, EnqueueArchiveAllResult, ArchiveAllJobSnapshot, ArchiveAllPollResult, NotificationRowActions, NotificationArchiveResult, NotificationArchiveFn, NotificationQueryKeys, NotificationListErrorState, NotificationListLabels, NotificationListProps, NotificationHistoryListProps, NotificationEmptyStateProps, NotificationUnreadBadgeProps, } from './types.js';
2
2
  export { NotificationDocSchema, PendingNotificationSchema } from './schemas.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -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,qBAAqB,EACrB,4BAA4B,EAC5B,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,qBAAqB,EACrB,0BAA0B,EAC1B,sBAAsB,EACtB,qBAAqB,EACrB,4BAA4B,EAC5B,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA0BA,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 NotificationListProps,\n NotificationHistoryListProps,\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":"AA4BA,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 NotificationQueryKeys,\n NotificationListErrorState,\n NotificationListLabels,\n NotificationListProps,\n NotificationHistoryListProps,\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"]}
@@ -5,5 +5,5 @@ import type { NotificationHistoryListProps } from '../../types.js';
5
5
  * exposes no `archive` action. Any per-row control (e.g. an
6
6
  * ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
7
7
  */
8
- export declare function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, renderError, title, renderRowAction, }: NotificationHistoryListProps): import("react").JSX.Element;
8
+ export declare function NotificationHistoryList({ config, userId, category, queryKeys, pageSize, staleTime, emptyText, labels, renderError, title, renderRowAction, }: NotificationHistoryListProps): import("react").JSX.Element;
9
9
  //# sourceMappingURL=NotificationHistoryList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationHistoryList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA2B,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAE5F;;;;;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,+BAuG9B"}
1
+ {"version":3,"file":"NotificationHistoryList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA2B,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAE5F;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,SAAS,EACT,SAAS,EACT,MAAM,EACN,WAAW,EACX,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+BAmG9B"}
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback } from 'react';
4
3
  import { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';
5
4
  import { useNotificationHistory } from '../hooks/useNotificationHistory.js';
6
5
  import { NotificationEmptyState } from './NotificationEmptyState.js';
6
+ import { NotificationTypeIcon } from './notification-type-icon.js';
7
7
  import { formatRelativeTime } from './relative-time.js';
8
8
  /**
9
9
  * Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are
@@ -11,21 +11,21 @@ import { formatRelativeTime } from './relative-time.js';
11
11
  * exposes no `archive` action. Any per-row control (e.g. an
12
12
  * ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
13
13
  */
14
- export function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, renderError, title, renderRowAction, }) {
14
+ export function NotificationHistoryList({ config, userId, category, queryKeys, pageSize, staleTime, emptyText, labels, renderError, title, renderRowAction, }) {
15
15
  const { data: notifications, isLoading, isFetching, isError, error, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useNotificationHistory({
16
16
  config,
17
17
  userId,
18
18
  category,
19
19
  pageSize,
20
20
  staleTime,
21
+ queryKeys,
21
22
  });
22
23
  const hasRows = !!notifications && notifications.length > 0;
23
24
  // A later page that came back empty keeps its pager so the user can step back; the
24
25
  // empty state is the answer for page 1 only.
25
26
  const showEmptyState = !isError && notifications !== undefined && notifications.length === 0 && page === 1;
26
- const getTypeIcon = useCallback((type) => config.types[type]?.icon ?? '🔔', [config]);
27
- return (_jsxs("div", { className: "ntf-list ntf-list-history", children: [title != null && (_jsxs(_Fragment, { children: [_jsx("div", { className: "ntf-list-header ntf-list-header-read-only", children: _jsx("div", { className: "ntf-list-title", children: title }) }), _jsx(Separator, {})] })), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => {
28
- return (_jsxs("div", { className: "ntf-item ntf-item-archived", children: [_jsx("div", { className: "ntf-item-icon", children: getTypeIcon(notification.type) }), _jsxs("div", { className: "ntf-item-content", children: [_jsx("div", { className: "ntf-item-title", children: notification.title }), _jsx("div", { className: "ntf-item-message", children: notification.message }), _jsx("div", { className: "ntf-item-timestamp", children: formatRelativeTime(notification.archivedAt) })] }), notification.count > 1 && (_jsx("div", { className: "ntf-item-count", children: _jsxs(Badge, { variant: "secondary", children: ["\u00D7", notification.count] }) })), renderRowAction && (_jsx("div", { className: "ntf-item-row-action", children: renderRowAction(notification, {}) }))] }, notification.archiveOccurrenceId));
27
+ return (_jsxs("div", { className: "ntf-list ntf-list-history", children: [title != null && (_jsxs(_Fragment, { children: [_jsx("div", { className: "ntf-list-header ntf-list-header-read-only", children: _jsx("div", { className: "ntf-list-title", children: title }) }), _jsx(Separator, {})] })), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: labels?.loading ?? 'Loading notifications' }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => {
28
+ return (_jsxs("div", { className: "ntf-item ntf-item-archived", children: [_jsx(NotificationTypeIcon, { config: config, type: notification.type }), _jsxs("div", { className: "ntf-item-content", children: [_jsx("div", { className: "ntf-item-title", children: notification.title }), _jsx("div", { className: "ntf-item-message", children: notification.message }), _jsx("div", { className: "ntf-item-timestamp", children: formatRelativeTime(notification.archivedAt) })] }), notification.count > 1 && (_jsx("div", { className: "ntf-item-count", children: _jsxs(Badge, { variant: "secondary", children: ["\u00D7", notification.count] }) })), renderRowAction && (_jsx("div", { className: "ntf-item-row-action", children: renderRowAction(notification, {}) }))] }, notification.archiveOccurrenceId));
29
29
  }), isError && error
30
30
  ? renderError({
31
31
  error,
@@ -41,6 +41,6 @@ export function NotificationHistoryList({ config, userId, category, pageSize, st
41
41
  canNextPage: hasNextPage,
42
42
  goToPreviousPage: prevPage,
43
43
  goToNextPage: nextPage,
44
- }, busy: isFetching, className: "ntf-list-footer mt-0" })] })) })] }));
44
+ }, busy: isFetching, className: "ntf-list-footer" })] })) })] }));
45
45
  }
46
46
  //# sourceMappingURL=NotificationHistoryList.js.map
@@ -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, 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 isLoading,\n isFetching,\n isError,\n error,\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 !isError && notifications !== undefined && notifications.length === 0 && page === 1;\n\n const getTypeIcon = useCallback(\n (type: string) => config.types[type]?.icon ?? '🔔',\n [config],\n );\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 {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\n key={notification.archiveOccurrenceId}\n className=\"ntf-item ntf-item-archived\"\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.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\">\n {renderRowAction(notification, {})}\n </div>\n )}\n </div>\n );\n })}\n {isError && error\n ? renderError({\n error,\n retry: () => {\n void refetch();\n },\n retrying: isFetching,\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 { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useNotificationHistory } from '../hooks/useNotificationHistory.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { NotificationTypeIcon } from './notification-type-icon.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationHistoryItem, 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 queryKeys,\n pageSize,\n staleTime,\n emptyText,\n labels,\n renderError,\n title,\n renderRowAction,\n}: NotificationHistoryListProps) {\n const {\n data: notifications,\n isLoading,\n isFetching,\n isError,\n error,\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 queryKeys,\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 !isError && notifications !== undefined && notifications.length === 0 && page === 1;\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 {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label={labels?.loading ?? 'Loading notifications'} />\n </div>\n ) : (\n <>\n {(notifications ?? []).map((notification: NotificationHistoryItem) => {\n return (\n <div\n key={notification.archiveOccurrenceId}\n className=\"ntf-item ntf-item-archived\"\n >\n <NotificationTypeIcon config={config} type={notification.type} />\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\">\n {renderRowAction(notification, {})}\n </div>\n )}\n </div>\n );\n })}\n {isError && error\n ? renderError({\n error,\n retry: () => {\n void refetch();\n },\n retrying: isFetching,\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\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
@@ -2,5 +2,5 @@ import type { NotificationListProps } from '../../types.js';
2
2
  /**
3
3
  * Scrollable list of active notifications with click-to-archive and clear-all.
4
4
  */
5
- export declare function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, staleTime, emptyText, renderError, onRenderedRowsChange, renderRowAction, }: NotificationListProps): import("react").JSX.Element;
5
+ export declare function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, queryKeys, title, onClearAll, refetchInterval, staleTime, emptyText, labels, renderError, onRenderedRowsChange, renderRowAction, }: NotificationListProps): import("react").JSX.Element;
6
6
  //# sourceMappingURL=NotificationList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAE7E;;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,+BAmNvB"}
1
+ {"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAIV,qBAAqB,EACtB,MAAM,gBAAgB,CAAC;AAYxB;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,SAAS,EACT,KAAK,EACL,UAAU,EACV,eAAe,EACf,SAAS,EACT,SAAS,EACT,MAAM,EACN,WAAW,EACX,oBAAoB,EACpB,eAAe,GAChB,EAAE,qBAAqB,+BAyQvB"}
@@ -1,33 +1,44 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
4
4
  import { Badge, Button, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';
5
5
  import { useActiveNotifications } from '../hooks/useActiveNotifications.js';
6
6
  import { useArchiveNotification } from '../hooks/useArchiveNotification.js';
7
7
  import { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';
8
8
  import { NotificationEmptyState } from './NotificationEmptyState.js';
9
+ import { NotificationTypeIcon } from './notification-type-icon.js';
9
10
  import { formatRelativeTime } from './relative-time.js';
11
+ const DEFAULT_LABELS = {
12
+ clearAll: 'Clear All',
13
+ clearing: 'Clearing...',
14
+ clearIncomplete: 'Some notifications remain — try again.',
15
+ loading: 'Loading notifications',
16
+ };
17
+ const FOCUSABLE = 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
10
18
  /**
11
19
  * Scrollable list of active notifications with click-to-archive and clear-all.
12
20
  */
13
- export function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, staleTime, emptyText, renderError, onRenderedRowsChange, renderRowAction, }) {
21
+ export function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, queryKeys, title, onClearAll, refetchInterval, staleTime, emptyText, labels, renderError, onRenderedRowsChange, renderRowAction, }) {
14
22
  const { data: notifications, isLoading, isFetching, isError, error, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useActiveNotifications({
15
23
  config,
16
24
  userId,
17
25
  category,
18
26
  refetchInterval,
19
27
  staleTime,
28
+ queryKeys,
20
29
  });
21
30
  const archiveMutation = useArchiveNotification({
22
31
  userId,
23
32
  category,
24
33
  archiveFn,
34
+ queryKeys,
25
35
  });
26
36
  const archiveAllMutation = useArchiveAllNotifications({
27
37
  userId,
28
38
  category,
29
39
  enqueueArchiveAllFn,
30
40
  getArchiveAllStatusFn,
41
+ queryKeys,
31
42
  });
32
43
  // Surface a failed/incomplete clear instead of swallowing it. `onClearAll` (and the tray-closing
33
44
  // side effects the app wires to it) fires ONLY when the server job fully drained the category —
@@ -57,6 +68,32 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
57
68
  setPendingArchiveIds(keepActive);
58
69
  setPendingClearAllIds(keepActive);
59
70
  }, [notifications]);
71
+ // A row that leaves the list while focus is inside it takes the focused control with it, so focus
72
+ // moves to the control of the row that followed it, or to the list itself (FRONTEND-203). A row's
73
+ // removal fires no blur, so the row still recorded as focused when the rows change is the row that
74
+ // held focus as it left.
75
+ const listRef = useRef(null);
76
+ const rowRefs = useRef(new Map());
77
+ const focusedRowId = useRef(null);
78
+ const renderedIds = useRef([]);
79
+ useLayoutEffect(() => {
80
+ const ids = isLoading ? [] : (notifications ?? []).map((notification) => notification.id);
81
+ const previousIds = renderedIds.current;
82
+ renderedIds.current = ids;
83
+ const leftId = focusedRowId.current;
84
+ if (leftId === null || ids.includes(leftId))
85
+ return;
86
+ focusedRowId.current = null;
87
+ const active = document.activeElement;
88
+ if (active !== null && active !== document.body && active.isConnected)
89
+ return;
90
+ const remaining = new Set(ids);
91
+ const followers = previousIds.slice(previousIds.indexOf(leftId) + 1).filter((id) => remaining.has(id));
92
+ const nextControl = followers
93
+ .map((id) => rowRefs.current.get(id)?.querySelector('.ntf-item-row-action')?.querySelector(FOCUSABLE))
94
+ .find((control) => control != null);
95
+ (nextControl ?? listRef.current)?.focus();
96
+ }, [notifications, isLoading]);
60
97
  const archiveOne = useCallback(async (notification) => {
61
98
  const clearPending = () => setPendingArchiveIds((current) => {
62
99
  const next = new Set(current);
@@ -74,13 +111,17 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
74
111
  // active, so it is not left looking like it is clearing.
75
112
  if (!result.archived)
76
113
  clearPending();
114
+ return result;
77
115
  }
78
116
  catch (archiveError) {
79
117
  clearPending();
80
118
  throw archiveError;
81
119
  }
82
120
  }, [archiveMutation]);
121
+ const hasNotifications = !!notifications && notifications.length > 0;
83
122
  const handleClearAll = useCallback(async () => {
123
+ if (!hasNotifications)
124
+ return;
84
125
  setClearIncomplete(false);
85
126
  setPendingClearAllIds(new Set((notifications ?? []).map((notification) => notification.id)));
86
127
  let result;
@@ -101,17 +142,31 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
101
142
  return;
102
143
  }
103
144
  onClearAll?.();
104
- }, [archiveAllMutation, notifications, onClearAll]);
105
- const getTypeIcon = useCallback((type) => {
106
- const typeConfig = config.types[type];
107
- return typeConfig?.icon ?? '🔔';
108
- }, [config]);
109
- const hasNotifications = !!notifications && notifications.length > 0;
145
+ }, [archiveAllMutation, hasNotifications, notifications, onClearAll]);
110
146
  // A later page that came back empty keeps its pager so the user can step back; the
111
147
  // empty state is the answer for page 1 only.
112
148
  const showEmptyState = !isError && notifications !== undefined && notifications.length === 0 && page === 1;
113
149
  const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;
114
- return (_jsxs("div", { className: "ntf-list", children: [_jsxs("div", { className: "ntf-list-header", children: [title != null && _jsx("div", { className: "ntf-list-title", children: title }), _jsx(Button, { className: "ntf-list-clear-all", variant: "ghost", size: "sm", onClick: handleClearAll, disabled: !hasNotifications, pending: isClearAllPending, children: isClearAllPending ? 'Clearing...' : 'Clear All' }), clearIncomplete && !isClearAllPending && (_jsx("div", { className: "ntf-list-clear-error", role: "status", children: "Some notifications remain \u2014 try again." }))] }), _jsx(Separator, {}), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => (_jsxs("div", { className: "ntf-item", children: [_jsx("div", { className: "ntf-item-icon", children: getTypeIcon(notification.type) }), _jsxs("div", { className: "ntf-item-content", children: [_jsx("div", { className: "ntf-item-title", children: notification.title }), _jsx("div", { className: "ntf-item-message", children: notification.message }), _jsx("div", { className: "ntf-item-timestamp", children: formatRelativeTime(notification.updatedAt) })] }), notification.count > 1 && (_jsx("div", { className: "ntf-item-count", children: _jsxs(Badge, { variant: "secondary", children: ["\u00D7", notification.count] }) })), renderRowAction && (_jsx("div", { className: "ntf-item-row-action", children: renderRowAction(notification, {
150
+ const titleId = useId();
151
+ // Per key, so a label passed as `undefined` keeps its default rather than erasing it.
152
+ const words = {
153
+ clearAll: labels?.clearAll ?? DEFAULT_LABELS.clearAll,
154
+ clearing: labels?.clearing ?? DEFAULT_LABELS.clearing,
155
+ clearIncomplete: labels?.clearIncomplete ?? DEFAULT_LABELS.clearIncomplete,
156
+ loading: labels?.loading ?? DEFAULT_LABELS.loading,
157
+ };
158
+ return (_jsxs("div", { ref: listRef, className: "ntf-list", role: "group", "aria-labelledby": title != null ? titleId : undefined, tabIndex: -1, children: [_jsxs("div", { className: "ntf-list-header", children: [title != null && _jsx("div", { id: titleId, className: "ntf-list-title", children: title }), _jsx(Button, { type: "button", className: "ntf-list-clear-all", variant: "ghost", size: "sm", onClick: handleClearAll, "aria-disabled": !hasNotifications || undefined, pending: isClearAllPending, children: isClearAllPending ? words.clearing : words.clearAll }), clearIncomplete && !isClearAllPending && (_jsx("div", { className: "ntf-list-clear-error", role: "status", children: words.clearIncomplete }))] }), _jsx(Separator, {}), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: words.loading }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => (_jsxs("div", { ref: (row) => {
159
+ if (row)
160
+ rowRefs.current.set(notification.id, row);
161
+ else
162
+ rowRefs.current.delete(notification.id);
163
+ }, className: "ntf-item", onFocus: () => {
164
+ focusedRowId.current = notification.id;
165
+ }, onBlur: (event) => {
166
+ if (!event.currentTarget.contains(event.relatedTarget)) {
167
+ focusedRowId.current = null;
168
+ }
169
+ }, children: [_jsx(NotificationTypeIcon, { config: config, type: notification.type }), _jsxs("div", { className: "ntf-item-content", children: [_jsx("div", { className: "ntf-item-title", children: notification.title }), _jsx("div", { className: "ntf-item-message", children: notification.message }), _jsx("div", { className: "ntf-item-timestamp", children: formatRelativeTime(notification.updatedAt) })] }), notification.count > 1 && (_jsx("div", { className: "ntf-item-count", children: _jsxs(Badge, { variant: "secondary", children: ["\u00D7", notification.count] }) })), renderRowAction && (_jsx("div", { className: "ntf-item-row-action", children: renderRowAction(notification, {
115
170
  archive: () => archiveOne(notification),
116
171
  isArchivePending: pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),
117
172
  }) }))] }, notification.id))), isError && error
@@ -129,6 +184,6 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
129
184
  canNextPage: hasNextPage,
130
185
  goToPreviousPage: prevPage,
131
186
  goToNextPage: nextPage,
132
- }, busy: isFetching, className: "ntf-list-footer mt-0" })] })) })] }));
187
+ }, busy: isFetching, className: "ntf-list-footer" })] })) })] }));
133
188
  }
134
189
  //# sourceMappingURL=NotificationList.js.map
@@ -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, 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, NotificationListProps } from '../../types.js';\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 isLoading,\n isFetching,\n isError,\n error,\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 [pendingArchiveIds, setPendingArchiveIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const [pendingClearAllIds, setPendingClearAllIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\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.\n useEffect(() => {\n if (!notifications) return;\n const activeIds = new Set(notifications.map((notification) => notification.id));\n const keepActive = (current: ReadonlySet<string>) => {\n const next = new Set([...current].filter((id) => activeIds.has(id)));\n return next.size === current.size ? current : next;\n };\n setPendingArchiveIds(keepActive);\n setPendingClearAllIds(keepActive);\n }, [notifications]);\n\n const archiveOne = useCallback(async (notification: NotificationDoc) => {\n const clearPending = () =>\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.delete(notification.id);\n return next;\n });\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.add(notification.id);\n return next;\n });\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 }, [archiveMutation]);\n\n const handleClearAll = useCallback(async () => {\n setClearIncomplete(false);\n setPendingClearAllIds(new Set((notifications ?? []).map((notification) => notification.id)));\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 setPendingClearAllIds(new Set());\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 setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n onClearAll?.();\n }, [archiveAllMutation, 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 hasNotifications = !!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 !isError && notifications !== undefined && notifications.length === 0 && page === 1;\n const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;\n\n return (\n <div className=\"ntf-list\">\n <div className=\"ntf-list-header\">\n {title != null && <div className=\"ntf-list-title\">{title}</div>}\n <Button\n className=\"ntf-list-clear-all\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClearAll}\n disabled={!hasNotifications}\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 {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : (\n <>\n {(notifications ?? []).map((notification: NotificationDoc) => (\n <div key={notification.id} className=\"ntf-item\">\n <div className=\"ntf-item-icon\">\n {getTypeIcon(notification.type)}\n </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 pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),\n })}\n </div>\n )}\n </div>\n ))}\n {isError && error\n ? renderError({\n error,\n retry: () => {\n void refetch();\n },\n retrying: isFetching,\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 { NotificationTypeIcon } from './notification-type-icon.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type {\n NotificationArchiveResult,\n NotificationDoc,\n NotificationListLabels,\n NotificationListProps,\n} from '../../types.js';\n\nconst DEFAULT_LABELS: NotificationListLabels = {\n clearAll: 'Clear All',\n clearing: 'Clearing...',\n clearIncomplete: 'Some notifications remain — try again.',\n loading: 'Loading notifications',\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 queryKeys,\n title,\n onClearAll,\n refetchInterval,\n staleTime,\n emptyText,\n labels,\n renderError,\n onRenderedRowsChange,\n renderRowAction,\n}: NotificationListProps) {\n const {\n data: notifications,\n isLoading,\n isFetching,\n isError,\n error,\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 queryKeys,\n });\n\n const archiveMutation = useArchiveNotification({\n userId,\n category,\n archiveFn,\n queryKeys,\n });\n\n const archiveAllMutation = useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n queryKeys,\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 [pendingArchiveIds, setPendingArchiveIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const [pendingClearAllIds, setPendingClearAllIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\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.\n useEffect(() => {\n if (!notifications) return;\n const activeIds = new Set(notifications.map((notification) => notification.id));\n const keepActive = (current: ReadonlySet<string>) => {\n const next = new Set([...current].filter((id) => activeIds.has(id)));\n return next.size === current.size ? current : next;\n };\n setPendingArchiveIds(keepActive);\n setPendingClearAllIds(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 = isLoading ? [] : (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.slice(previousIds.indexOf(leftId) + 1).filter((id) => remaining.has(id));\n const nextControl = followers\n .map((id) => rowRefs.current.get(id)?.querySelector('.ntf-item-row-action')?.querySelector<HTMLElement>(FOCUSABLE))\n .find((control) => control != null);\n (nextControl ?? listRef.current)?.focus();\n }, [notifications, isLoading]);\n\n const archiveOne = useCallback(async (notification: NotificationDoc): Promise<NotificationArchiveResult> => {\n const clearPending = () =>\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.delete(notification.id);\n return next;\n });\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.add(notification.id);\n return next;\n });\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 return result;\n } catch (archiveError) {\n clearPending();\n throw archiveError;\n }\n }, [archiveMutation]);\n\n const hasNotifications = !!notifications && notifications.length > 0;\n\n const handleClearAll = useCallback(async () => {\n if (!hasNotifications) return;\n setClearIncomplete(false);\n setPendingClearAllIds(new Set((notifications ?? []).map((notification) => notification.id)));\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 setPendingClearAllIds(new Set());\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 setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n onClearAll?.();\n }, [archiveAllMutation, hasNotifications, notifications, onClearAll]);\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 !isError && notifications !== undefined && notifications.length === 0 && page === 1;\n const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;\n const titleId = useId();\n // Per key, so a label passed as `undefined` keeps its default rather than erasing it.\n const words: NotificationListLabels = {\n clearAll: labels?.clearAll ?? DEFAULT_LABELS.clearAll,\n clearing: labels?.clearing ?? DEFAULT_LABELS.clearing,\n clearIncomplete: labels?.clearIncomplete ?? DEFAULT_LABELS.clearIncomplete,\n loading: labels?.loading ?? DEFAULT_LABELS.loading,\n };\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 && <div id={titleId} className=\"ntf-list-title\">{title}</div>}\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 ? words.clearing : words.clearAll}\n </Button>\n {clearIncomplete && !isClearAllPending && (\n <div className=\"ntf-list-clear-error\" role=\"status\">\n {words.clearIncomplete}\n </div>\n )}\n </div>\n <Separator />\n\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label={words.loading} />\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 <NotificationTypeIcon config={config} type={notification.type} />\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 pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),\n })}\n </div>\n )}\n </div>\n ))}\n {isError && error\n ? renderError({\n error,\n retry: () => {\n void refetch();\n },\n retrying: isFetching,\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\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
@@ -4,5 +4,5 @@ import type { NotificationUnreadBadgeProps } from '../../types.js';
4
4
  * Renders nothing when count is 0.
5
5
  * Apps wrap this around whatever trigger element they want.
6
6
  */
7
- export declare function NotificationUnreadBadge({ config, userId, category, refetchInterval, }: NotificationUnreadBadgeProps): import("react").JSX.Element | null;
7
+ export declare function NotificationUnreadBadge({ config, userId, category, queryKeys, refetchInterval, }: NotificationUnreadBadgeProps): import("react").JSX.Element | null;
8
8
  //# 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,sCAe9B"}
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,SAAS,EACT,eAAe,GAChB,EAAE,4BAA4B,sCAgB9B"}
@@ -6,12 +6,13 @@ import { useUnreadCount } from '../hooks/useUnreadCount.js';
6
6
  * Renders nothing when count is 0.
7
7
  * Apps wrap this around whatever trigger element they want.
8
8
  */
9
- export function NotificationUnreadBadge({ config, userId, category, refetchInterval, }) {
9
+ export function NotificationUnreadBadge({ config, userId, category, queryKeys, refetchInterval, }) {
10
10
  const { count, hasMore } = useUnreadCount({
11
11
  config,
12
12
  userId,
13
13
  category,
14
14
  refetchInterval,
15
+ queryKeys,
15
16
  });
16
17
  if (count === 0)
17
18
  return null;
@@ -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,CACL,eAAM,SAAS,EAAC,WAAW,YAAE,YAAY,GAAQ,CAClD,CAAC;AACJ,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 (\n <span className=\"ntf-badge\">{displayCount}</span>\n );\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;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,eAAe,GACc;IAC7B,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC;QACxC,MAAM;QACN,MAAM;QACN,QAAQ;QACR,eAAe;QACf,SAAS;KACV,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,CACL,eAAM,SAAS,EAAC,WAAW,YAAE,YAAY,GAAQ,CAClD,CAAC;AACJ,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 queryKeys,\n refetchInterval,\n}: NotificationUnreadBadgeProps) {\n const { count, hasMore } = useUnreadCount({\n config,\n userId,\n category,\n refetchInterval,\n queryKeys,\n });\n\n if (count === 0) return null;\n\n const displayCount = hasMore ? '99+' : count > 9 ? '9+' : String(count);\n\n return (\n <span className=\"ntf-badge\">{displayCount}</span>\n );\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import type { NotificationSystemConfig } from '../../types.js';
2
+ /**
3
+ * A row's type icon: the type's own `icon` text, or a bell drawn in `currentColor`, so the
4
+ * row's icon class colors and sizes it like any other glyph. Decorative — the row's title says
5
+ * what it is.
6
+ */
7
+ export declare function NotificationTypeIcon({ config, type }: {
8
+ config: NotificationSystemConfig;
9
+ type: string;
10
+ }): import("react").JSX.Element;
11
+ //# sourceMappingURL=notification-type-icon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-type-icon.d.ts","sourceRoot":"","sources":["../../../src/react/components/notification-type-icon.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAE/D;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;IAAE,MAAM,EAAE,wBAAwB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,+BAuBxG"}
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * A row's type icon: the type's own `icon` text, or a bell drawn in `currentColor`, so the
5
+ * row's icon class colors and sizes it like any other glyph. Decorative — the row's title says
6
+ * what it is.
7
+ */
8
+ export function NotificationTypeIcon({ config, type }) {
9
+ const icon = config.types[type]?.icon;
10
+ return (_jsx("div", { className: "ntf-item-icon", "aria-hidden": "true", children: icon ?? (_jsxs("svg", { className: "ntf-item-icon-glyph", xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }), _jsx("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" })] })) }));
11
+ }
12
+ //# sourceMappingURL=notification-type-icon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-type-icon.js","sourceRoot":"","sources":["../../../src/react/components/notification-type-icon.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAIb;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAE,MAAM,EAAE,IAAI,EAAsD;IACvG,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACtC,OAAO,CACL,cAAK,SAAS,EAAC,eAAe,iBAAa,MAAM,YAC9C,IAAI,IAAI,CACP,eACE,SAAS,EAAC,qBAAqB,EAC/B,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,2CAA2C,GAAG,EACtD,eAAM,CAAC,EAAC,gCAAgC,GAAG,IACvC,CACP,GACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport type { NotificationSystemConfig } from '../../types.js';\n\n/**\n * A row's type icon: the type's own `icon` text, or a bell drawn in `currentColor`, so the\n * row's icon class colors and sizes it like any other glyph. Decorative — the row's title says\n * what it is.\n */\nexport function NotificationTypeIcon({ config, type }: { config: NotificationSystemConfig; type: string }) {\n const icon = config.types[type]?.icon;\n return (\n <div className=\"ntf-item-icon\" aria-hidden=\"true\">\n {icon ?? (\n <svg\n className=\"ntf-item-icon-glyph\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\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=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\" />\n <path d=\"M10.3 21a1.94 1.94 0 0 0 3.4 0\" />\n </svg>\n )}\n </div>\n );\n}\n"]}
@@ -3,5 +3,5 @@ import type { NotificationDoc, UseActiveNotificationsOptions } from '../../types
3
3
  * Paginated read of active notifications. The displayed page is re-read every
4
4
  * `refetchInterval` ms while mounted — polling, not a realtime listener, for cost.
5
5
  */
6
- export declare function useActiveNotifications({ config, userId, category, enabled, pageSize, refetchInterval, staleTime, }: UseActiveNotificationsOptions): import("@ttt-productions/query-core/react").UseFirestorePaginatedResult<NotificationDoc>;
6
+ export declare function useActiveNotifications({ config, userId, category, enabled, pageSize, refetchInterval, staleTime, queryKeys, }: UseActiveNotificationsOptions): import("@ttt-productions/query-core/react").UseFirestorePaginatedResult<NotificationDoc>;
7
7
  //# sourceMappingURL=useActiveNotifications.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useActiveNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAIA,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,4FAuB/B"}
1
+ {"version":3,"file":"useActiveNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAOrF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,QAA4B,EAC5B,eAA0C,EAC1C,SAA8B,EAC9B,SAAS,GACV,EAAE,6BAA6B,4FAuB/B"}
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { useFirestorePaginated } from '@ttt-productions/query-core/react';
3
3
  import { orderBy, where } from 'firebase/firestore';
4
+ import { resolveNotificationQueryKeys } from '../query-keys.js';
4
5
  const DEFAULT_PAGE_SIZE = 20;
5
6
  const DEFAULT_REFETCH_INTERVAL = 30_000;
6
7
  const DEFAULT_STALE_TIME = 30_000;
@@ -8,7 +9,7 @@ const DEFAULT_STALE_TIME = 30_000;
8
9
  * Paginated read of active notifications. The displayed page is re-read every
9
10
  * `refetchInterval` ms while mounted — polling, not a realtime listener, for cost.
10
11
  */
11
- export function useActiveNotifications({ config, userId, category, enabled = true, pageSize = DEFAULT_PAGE_SIZE, refetchInterval = DEFAULT_REFETCH_INTERVAL, staleTime = DEFAULT_STALE_TIME, }) {
12
+ export function useActiveNotifications({ config, userId, category, enabled = true, pageSize = DEFAULT_PAGE_SIZE, refetchInterval = DEFAULT_REFETCH_INTERVAL, staleTime = DEFAULT_STALE_TIME, queryKeys, }) {
12
13
  const categoryConfig = config.categories[category];
13
14
  if (!categoryConfig) {
14
15
  throw new Error(`[notification-core] Unknown category: ${category}`);
@@ -21,7 +22,7 @@ export function useActiveNotifications({ config, userId, category, enabled = tru
21
22
  ];
22
23
  return useFirestorePaginated({
23
24
  collectionPath,
24
- queryKey: ['notifications', 'active', category, userId, { pageSize }],
25
+ queryKey: [...resolveNotificationQueryKeys(queryKeys).active(category, userId), { pageSize }],
25
26
  constraints,
26
27
  pageSize,
27
28
  enabled: enabled && !!userId,
@@ -1 +1 @@
1
- {"version":3,"file":"useActiveNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,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,OAAO,qBAAqB,CAAkB;QAC5C,cAAc;QACd,QAAQ,EAAE,CAAC,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC;QACrE,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,eAAe;QACf,SAAS;KACV,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\nimport { useFirestorePaginated } 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 return useFirestorePaginated<NotificationDoc>({\n collectionPath,\n queryKey: ['notifications', 'active', category, userId, { pageSize }],\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n refetchInterval,\n staleTime,\n });\n}\n"]}
1
+ {"version":3,"file":"useActiveNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAwB,MAAM,oBAAoB,CAAC;AAE1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAEhE,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,EAC9B,SAAS,GACqB;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,OAAO,qBAAqB,CAAkB;QAC5C,cAAc;QACd,QAAQ,EAAE,CAAC,GAAG,4BAA4B,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC;QAC7F,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,eAAe;QACf,SAAS;KACV,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\nimport { useFirestorePaginated } from '@ttt-productions/query-core/react';\nimport { orderBy, where, type QueryConstraint } from 'firebase/firestore';\nimport type { NotificationDoc, UseActiveNotificationsOptions } from '../../types.js';\nimport { resolveNotificationQueryKeys } from '../query-keys.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 queryKeys,\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 return useFirestorePaginated<NotificationDoc>({\n collectionPath,\n queryKey: [...resolveNotificationQueryKeys(queryKeys).active(category, userId), { pageSize }],\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n refetchInterval,\n staleTime,\n });\n}\n"]}
@@ -29,5 +29,5 @@ import type { ArchiveAllPollResult, UseArchiveAllNotificationsOptions } from '..
29
29
  * archiveAll.mutate();
30
30
  * ```
31
31
  */
32
- export declare function useArchiveAllNotifications({ userId, category, enqueueArchiveAllFn, getArchiveAllStatusFn, pollIntervalMs, maxPolls, invalidateKeys, }: UseArchiveAllNotificationsOptions): import("@tanstack/react-query").UseMutationResult<ArchiveAllPollResult, Error, void, unknown>;
32
+ export declare function useArchiveAllNotifications({ userId, category, enqueueArchiveAllFn, getArchiveAllStatusFn, pollIntervalMs, maxPolls, queryKeys, invalidateKeys, }: UseArchiveAllNotificationsOptions): import("@tanstack/react-query").UseMutationResult<ArchiveAllPollResult, Error, void, unknown>;
33
33
  //# 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,EACV,oBAAoB,EACpB,iCAAiC,EAClC,MAAM,gBAAgB,CAAC;AASxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,qBAAqB,EACrB,cAAyC,EACzC,QAA4B,EAC5B,cAAc,GACf,EAAE,iCAAiC,iGAqDnC"}
1
+ {"version":3,"file":"useArchiveAllNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,oBAAoB,EACpB,iCAAiC,EAClC,MAAM,gBAAgB,CAAC;AAUxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,qBAAqB,EACrB,cAAyC,EACzC,QAA4B,EAC5B,SAAS,EACT,cAAc,GACf,EAAE,iCAAiC,iGA+CnC"}
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { useMutation, useQueryClient } from '@tanstack/react-query';
3
+ import { archiveRefreshKeys } from '../query-keys.js';
3
4
  const DEFAULT_POLL_INTERVAL_MS = 1500;
4
5
  const DEFAULT_MAX_POLLS = 120; // ~3 min at the default interval
5
6
  function sleep(ms) {
@@ -35,13 +36,8 @@ function sleep(ms) {
35
36
  * archiveAll.mutate();
36
37
  * ```
37
38
  */
38
- export function useArchiveAllNotifications({ userId, category, enqueueArchiveAllFn, getArchiveAllStatusFn, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, maxPolls = DEFAULT_MAX_POLLS, invalidateKeys, }) {
39
+ export function useArchiveAllNotifications({ userId, category, enqueueArchiveAllFn, getArchiveAllStatusFn, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, maxPolls = DEFAULT_MAX_POLLS, queryKeys, invalidateKeys, }) {
39
40
  const queryClient = useQueryClient();
40
- const defaultInvalidateKeys = [
41
- ['notifications', 'active', category, userId],
42
- ['notifications', 'unread-count', category, userId],
43
- ['notifications', 'history', category, userId],
44
- ];
45
41
  return useMutation({
46
42
  // Server-owned archive-all: enqueue ONE job scoped to `category`, then poll its status until it
47
43
  // reaches a terminal state. No browser loop, no chain of archive transactions. The mutation
@@ -76,7 +72,7 @@ export function useArchiveAllNotifications({ userId, category, enqueueArchiveAll
76
72
  // Only a completed drain changed the active set; still invalidate on an incomplete drain since
77
73
  // SOME cards were archived. A hard enqueue/poll failure rejects the mutation (onError), so this
78
74
  // never runs for `failed`.
79
- const keysToInvalidate = invalidateKeys ?? defaultInvalidateKeys;
75
+ const keysToInvalidate = invalidateKeys ?? archiveRefreshKeys(queryKeys, category, userId);
80
76
  keysToInvalidate.forEach((key) => {
81
77
  queryClient.invalidateQueries({ queryKey: [...key], exact: false });
82
78
  });