@ttt-productions/notification-core 0.15.13 → 0.15.15

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.
@@ -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,QAAQ,EACR,SAAS,EACT,SAAS,EACT,WAAW,EACX,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+BAqG9B"}
@@ -12,7 +12,7 @@ import { formatRelativeTime } from './relative-time.js';
12
12
  * ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
13
13
  */
14
14
  export function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, renderError, title, renderRowAction, }) {
15
- const { data: notifications, isLoading, isFetching, isError, error, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useNotificationHistory({
15
+ const { data: notifications, isFetching, retryState, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useNotificationHistory({
16
16
  config,
17
17
  userId,
18
18
  category,
@@ -22,17 +22,17 @@ export function NotificationHistoryList({ config, userId, category, pageSize, st
22
22
  const hasRows = !!notifications && notifications.length > 0;
23
23
  // A later page that came back empty keeps its pager so the user can step back; the
24
24
  // empty state is the answer for page 1 only.
25
- const showEmptyState = !isError && notifications !== undefined && notifications.length === 0 && page === 1;
25
+ const showEmptyState = retryState.error === null && notifications !== undefined && notifications.length === 0 && page === 1;
26
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) => {
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: retryState.isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => {
28
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));
29
- }), isError && error
29
+ }), retryState.error !== null
30
30
  ? renderError({
31
- error,
31
+ error: retryState.error,
32
32
  retry: () => {
33
33
  void refetch();
34
34
  },
35
- retrying: isFetching,
35
+ retrying: retryState.isRetrying,
36
36
  hasRows,
37
37
  })
38
38
  : null, showEmptyState && _jsx(NotificationEmptyState, { text: emptyText }), _jsx(ListPagination, { pagination: {
@@ -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 { 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 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 && 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 {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\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 {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 +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;AAK7E;;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,+BAwQvB"}
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;AAK7E;;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,+BAsQvB"}
@@ -12,7 +12,7 @@ const FOCUSABLE = 'button:not([disabled]), a[href], input:not([disabled]), selec
12
12
  * Scrollable list of active notifications with click-to-archive and clear-all.
13
13
  */
14
14
  export function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, staleTime, emptyText, renderError, onRenderedRowsChange, renderRowAction, }) {
15
- const { data: notifications, isLoading, isFetching, isError, error, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useActiveNotifications({
15
+ const { data: notifications, isFetching, retryState, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useActiveNotifications({
16
16
  config,
17
17
  userId,
18
18
  category,
@@ -67,7 +67,7 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
67
67
  const focusedRowId = useRef(null);
68
68
  const renderedIds = useRef([]);
69
69
  useLayoutEffect(() => {
70
- const ids = isLoading ? [] : (notifications ?? []).map((notification) => notification.id);
70
+ const ids = retryState.isLoading ? [] : (notifications ?? []).map((notification) => notification.id);
71
71
  const previousIds = renderedIds.current;
72
72
  renderedIds.current = ids;
73
73
  const leftId = focusedRowId.current;
@@ -83,7 +83,7 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
83
83
  .map((id) => rowRefs.current.get(id)?.querySelector('.ntf-item-row-action')?.querySelector(FOCUSABLE))
84
84
  .find((control) => control != null);
85
85
  (nextControl ?? listRef.current)?.focus();
86
- }, [notifications, isLoading]);
86
+ }, [notifications, retryState.isLoading]);
87
87
  const archiveOne = useCallback(async (notification) => {
88
88
  const clearPending = () => setPendingArchiveIds((current) => {
89
89
  const next = new Set(current);
@@ -138,10 +138,10 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
138
138
  }, [config]);
139
139
  // A later page that came back empty keeps its pager so the user can step back; the
140
140
  // empty state is the answer for page 1 only.
141
- const showEmptyState = !isError && notifications !== undefined && notifications.length === 0 && page === 1;
141
+ const showEmptyState = retryState.error === null && notifications !== undefined && notifications.length === 0 && page === 1;
142
142
  const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;
143
143
  const titleId = useId();
144
- 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 ? '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", { ref: (row) => {
144
+ 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 ? '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: retryState.isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => (_jsxs("div", { ref: (row) => {
145
145
  if (row)
146
146
  rowRefs.current.set(notification.id, row);
147
147
  else
@@ -155,13 +155,13 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
155
155
  }, 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, {
156
156
  archive: () => archiveOne(notification),
157
157
  isArchivePending: pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),
158
- }) }))] }, notification.id))), isError && error
158
+ }) }))] }, notification.id))), retryState.error !== null
159
159
  ? renderError({
160
- error,
160
+ error: retryState.error,
161
161
  retry: () => {
162
162
  void refetch();
163
163
  },
164
- retrying: isFetching,
164
+ retrying: retryState.isRetrying,
165
165
  hasRows: hasNotifications,
166
166
  })
167
167
  : null, showEmptyState && _jsx(NotificationEmptyState, { text: emptyText }), _jsx(ListPagination, { pagination: {
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACzF,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AACnG,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,0BAA0B,EAAE,MAAM,wCAAwC,CAAC;AACpF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxD,MAAM,SAAS,GACb,2IAA2I,CAAC;AAE9I;;GAEG;AACH,MAAM,UAAU,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,GACO;IACtB,MAAM,EACJ,IAAI,EAAE,aAAa,EACnB,SAAS,EACT,UAAU,EACV,OAAO,EACP,KAAK,EACL,OAAO,EACP,IAAI,EACJ,WAAW,EACX,WAAW,EACX,QAAQ,EACR,QAAQ,GACT,GAAG,sBAAsB,CAAC;QACzB,MAAM;QACN,MAAM;QACN,QAAQ;QACR,eAAe;QACf,SAAS;KACV,CAAC,CAAC;IAEH,MAAM,eAAe,GAAG,sBAAsB,CAAC;QAC7C,MAAM;QACN,QAAQ;QACR,SAAS;KACV,CAAC,CAAC;IAEH,MAAM,kBAAkB,GAAG,0BAA0B,CAAC;QACpD,MAAM;QACN,QAAQ;QACR,mBAAmB;QACnB,qBAAqB;KACtB,CAAC,CAAC;IAEH,iGAAiG;IACjG,gGAAgG;IAChG,kGAAkG;IAClG,gGAAgG;IAChG,2BAA2B;IAC3B,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9D,MAAM,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,GAAG,QAAQ,CACxD,GAAG,EAAE,CAAC,IAAI,GAAG,EAAE,CAChB,CAAC;IACF,MAAM,CAAC,kBAAkB,EAAE,qBAAqB,CAAC,GAAG,QAAQ,CAC1D,GAAG,EAAE,CAAC,IAAI,GAAG,EAAE,CAChB,CAAC;IAEF,MAAM,eAAe,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;IACrD,eAAe,CAAC,OAAO,GAAG,oBAAoB,CAAC;IAC/C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,aAAa;YAAE,eAAe,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC,CAAC;IAC9D,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAEpB,gFAAgF;IAChF,kFAAkF;IAClF,gCAAgC;IAChC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,aAAa;YAAE,OAAO;QAC3B,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC;QAChF,MAAM,UAAU,GAAG,CAAC,OAA4B,EAAE,EAAE;YAClD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACrE,OAAO,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QACrD,CAAC,CAAC;QACF,oBAAoB,CAAC,UAAU,CAAC,CAAC;QACjC,qBAAqB,CAAC,UAAU,CAAC,CAAC;IACpC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAEpB,kGAAkG;IAClG,kGAAkG;IAClG,mGAAmG;IACnG,yBAAyB;IACzB,MAAM,OAAO,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,GAAG,EAA0B,CAAC,CAAC;IAC1D,MAAM,YAAY,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IACjD,MAAM,WAAW,GAAG,MAAM,CAAoB,EAAE,CAAC,CAAC;IAClD,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,GAAG,GAAG,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAC1F,MAAM,WAAW,GAAG,WAAW,CAAC,OAAO,CAAC;QACxC,WAAW,CAAC,OAAO,GAAG,GAAG,CAAC;QAC1B,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC;QACpC,IAAI,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO;QACpD,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;QAC5B,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC;QACtC,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,QAAQ,CAAC,IAAI,IAAI,MAAM,CAAC,WAAW;YAAE,OAAO;QAE9E,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACvG,MAAM,WAAW,GAAG,SAAS;aAC1B,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAAE,aAAa,CAAc,SAAS,CAAC,CAAC;aAClH,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC;QACtC,CAAC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC;IAC5C,CAAC,EAAE,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC;IAE/B,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,EAAE,YAA6B,EAAE,EAAE;QACrE,MAAM,YAAY,GAAG,GAAG,EAAE,CACxB,oBAAoB,CAAC,CAAC,OAAO,EAAE,EAAE;YAC/B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;YAC9B,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;QACL,oBAAoB,CAAC,CAAC,OAAO,EAAE,EAAE;YAC/B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;YAC9B,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;YAC1B,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;QACH,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;YAC/D,+EAA+E;YAC/E,yDAAyD;YACzD,IAAI,CAAC,MAAM,CAAC,QAAQ;gBAAE,YAAY,EAAE,CAAC;QACvC,CAAC;QAAC,OAAO,YAAY,EAAE,CAAC;YACtB,YAAY,EAAE,CAAC;YACf,MAAM,YAAY,CAAC;QACrB,CAAC;IACH,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAEtB,MAAM,gBAAgB,GAAG,CAAC,CAAC,aAAa,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC;IAErE,MAAM,cAAc,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAC5C,IAAI,CAAC,gBAAgB;YAAE,OAAO;QAC9B,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAC1B,qBAAqB,CAAC,IAAI,GAAG,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7F,IAAI,MAAM,CAAC;QACX,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,kBAAkB,CAAC,WAAW,EAAE,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,2FAA2F;YAC3F,qBAAqB,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;YACjC,kBAAkB,CAAC,IAAI,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YACrB,oFAAoF;YACpF,oCAAoC;YACpC,qBAAqB,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;YACjC,kBAAkB,CAAC,IAAI,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QACD,UAAU,EAAE,EAAE,CAAC;IACjB,CAAC,EAAE,CAAC,kBAAkB,EAAE,gBAAgB,EAAE,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC;IAEtE,MAAM,WAAW,GAAG,WAAW,CAC7B,CAAC,IAAY,EAAE,EAAE;QACf,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtC,OAAO,UAAU,EAAE,IAAI,IAAI,IAAI,CAAC;IAClC,CAAC,EACD,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,mFAAmF;IACnF,6CAA6C;IAC7C,MAAM,cAAc,GAClB,CAAC,OAAO,IAAI,aAAa,KAAK,SAAS,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,CAAC;IACtF,MAAM,iBAAiB,GAAG,kBAAkB,CAAC,SAAS,IAAI,kBAAkB,CAAC,IAAI,GAAG,CAAC,CAAC;IACtF,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;IAExB,OAAO,CACL,eACE,GAAG,EAAE,OAAO,EACZ,SAAS,EAAC,UAAU,EACpB,IAAI,EAAC,OAAO,qBACK,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,EACpD,QAAQ,EAAE,CAAC,CAAC,aAEZ,eAAK,SAAS,EAAC,iBAAiB,aAC7B,KAAK,IAAI,IAAI,IAAI,cAAK,EAAE,EAAE,OAAO,EAAE,SAAS,EAAC,gBAAgB,YAAE,KAAK,GAAO,EAG5E,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,oBAAoB,EAC9B,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,cAAc,mBACR,CAAC,gBAAgB,IAAI,SAAS,EAC7C,OAAO,EAAE,iBAAiB,YAEzB,iBAAiB,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,GACzC,EACR,eAAe,IAAI,CAAC,iBAAiB,IAAI,CACxC,cAAK,SAAS,EAAC,sBAAsB,EAAC,IAAI,EAAC,QAAQ,4DAE7C,CACP,IACG,EACN,KAAC,SAAS,KAAG,EAEb,cAAK,SAAS,EAAC,eAAe,YAC7B,SAAS,CAAC,CAAC,CAAC,CACX,cAAK,SAAS,EAAC,aAAa,YAC1B,KAAC,OAAO,IAAC,IAAI,EAAC,IAAI,EAAC,KAAK,EAAC,uBAAuB,GAAG,GAC/C,CACP,CAAC,CAAC,CAAC,CACF,8BACG,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,YAA6B,EAAE,EAAE,CAAC,CAC5D,eAEE,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE;gCACX,IAAI,GAAG;oCAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;;oCAC9C,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;4BAC/C,CAAC,EACD,SAAS,EAAC,UAAU,EACpB,OAAO,EAAE,GAAG,EAAE;gCACZ,YAAY,CAAC,OAAO,GAAG,YAAY,CAAC,EAAE,CAAC;4BACzC,CAAC,EACD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;gCAChB,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,aAA4B,CAAC,EAAE,CAAC;oCACtE,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;gCAC9B,CAAC;4BACH,CAAC,aAED,cAAK,SAAS,EAAC,eAAe,YAC3B,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,GAC3B,EACN,eAAK,SAAS,EAAC,kBAAkB,aAC/B,cAAK,SAAS,EAAC,gBAAgB,YAAE,YAAY,CAAC,KAAK,GAAO,EAC1D,cAAK,SAAS,EAAC,kBAAkB,YAAE,YAAY,CAAC,OAAO,GAAO,EAC9D,cAAK,SAAS,EAAC,oBAAoB,YAChC,kBAAkB,CAAC,YAAY,CAAC,SAAS,CAAC,GACvC,IACF,EACL,YAAY,CAAC,KAAK,GAAG,CAAC,IAAI,CACzB,cAAK,SAAS,EAAC,gBAAgB,YAC7B,MAAC,KAAK,IAAC,OAAO,EAAC,WAAW,uBAAG,YAAY,CAAC,KAAK,IAAS,GACpD,CACP,EACA,eAAe,IAAI,CAClB,cAAK,SAAS,EAAC,qBAAqB,YACjC,eAAe,CAAC,YAAY,EAAE;wCAC7B,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC;wCACvC,gBAAgB,EACd,kBAAkB,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,iBAAiB,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;qCACpF,CAAC,GACE,CACP,KAtCI,YAAY,CAAC,EAAE,CAuChB,CACP,CAAC,EACD,OAAO,IAAI,KAAK;4BACf,CAAC,CAAC,WAAW,CAAC;gCACV,KAAK;gCACL,KAAK,EAAE,GAAG,EAAE;oCACV,KAAK,OAAO,EAAE,CAAC;gCACjB,CAAC;gCACD,QAAQ,EAAE,UAAU;gCACpB,OAAO,EAAE,gBAAgB;6BAC1B,CAAC;4BACJ,CAAC,CAAC,IAAI,EACP,cAAc,IAAI,KAAC,sBAAsB,IAAC,IAAI,EAAE,SAAS,GAAI,EAC9D,KAAC,cAAc,IACb,UAAU,EAAE;gCACV,WAAW,EAAE,IAAI;gCACjB,eAAe,EAAE,WAAW;gCAC5B,WAAW,EAAE,WAAW;gCACxB,gBAAgB,EAAE,QAAQ;gCAC1B,YAAY,EAAE,QAAQ;6BACvB,EACD,IAAI,EAAE,UAAU,EAChB,SAAS,EAAC,sBAAsB,GAChC,IACD,CACJ,GACK,IACF,CACP,CAAC;AACJ,CAAC","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, NotificationListProps } from '../../types.js';\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 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 // 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) => {\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 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 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 !isError && notifications !== undefined && notifications.length === 0 && page === 1;\n const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;\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 && <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 ? '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\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\">\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 { formatRelativeTime } from './relative-time.js';\nimport type { NotificationDoc, NotificationListProps } from '../../types.js';\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 [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 = retryState.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, retryState.isLoading]);\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 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 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 && notifications !== undefined && notifications.length === 0 && page === 1;\n const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;\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 && <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 ? '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\">\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 {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"]}
@@ -3,5 +3,40 @@ 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, }: UseActiveNotificationsOptions): {
7
+ retryState: import("@ttt-productions/query-core/react").ReadRetryState<Error>;
8
+ promise: Promise<import("@ttt-productions/query-core").PaginatedPage<import("@ttt-productions/query-core").WithId<NotificationDoc>>>;
9
+ error: Error | null;
10
+ isError: boolean;
11
+ isPending: boolean;
12
+ isLoading: boolean;
13
+ isLoadingError: boolean;
14
+ isRefetchError: boolean;
15
+ isSuccess: boolean;
16
+ isPlaceholderData: boolean;
17
+ status: "success" | "error" | "pending";
18
+ dataUpdatedAt: number;
19
+ errorUpdatedAt: number;
20
+ failureCount: number;
21
+ failureReason: Error | null;
22
+ errorUpdateCount: number;
23
+ isFetched: boolean;
24
+ isFetchedAfterMount: boolean;
25
+ isFetching: boolean;
26
+ isInitialLoading: boolean;
27
+ isPaused: boolean;
28
+ isRefetching: boolean;
29
+ isStale: boolean;
30
+ isEnabled: boolean;
31
+ refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<import("@ttt-productions/query-core").PaginatedPage<import("@ttt-productions/query-core").WithId<NotificationDoc>>, Error>>;
32
+ fetchStatus: import("@tanstack/query-core").FetchStatus;
33
+ data: import("@ttt-productions/query-core").WithId<NotificationDoc>[] | undefined;
34
+ page: number;
35
+ pageSize: number;
36
+ hasNextPage: boolean;
37
+ hasPrevPage: boolean;
38
+ setPage: (page: number) => void;
39
+ nextPage: () => void;
40
+ prevPage: () => void;
41
+ };
7
42
  //# 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":"AAKA,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,5 +1,6 @@
1
1
  'use client';
2
- import { useFirestorePaginated } from '@ttt-productions/query-core/react';
2
+ import { paginatedPageKey } from '@ttt-productions/query-core';
3
+ import { useFirestorePaginated, useReadRetryState } from '@ttt-productions/query-core/react';
3
4
  import { orderBy, where } from 'firebase/firestore';
4
5
  const DEFAULT_PAGE_SIZE = 20;
5
6
  const DEFAULT_REFETCH_INTERVAL = 30_000;
@@ -19,14 +20,18 @@ export function useActiveNotifications({ config, userId, category, enabled = tru
19
20
  ...(isPersonal ? [where('targetUserId', '==', userId)] : []),
20
21
  orderBy('updatedAt', 'desc'),
21
22
  ];
22
- return useFirestorePaginated({
23
+ const queryKey = ['notifications', 'active', category, userId, { pageSize }];
24
+ const read = useFirestorePaginated({
23
25
  collectionPath,
24
- queryKey: ['notifications', 'active', category, userId, { pageSize }],
26
+ queryKey,
25
27
  constraints,
26
28
  pageSize,
27
29
  enabled: enabled && !!userId,
28
30
  refetchInterval,
29
31
  staleTime,
30
32
  });
33
+ // The displayed page's failure held through its retry: a refetch of a never-succeeded page clears
34
+ // `error` while it runs, so the list would drop its failure under the retry the person pressed.
35
+ return { ...read, retryState: useReadRetryState(read, paginatedPageKey(queryKey, read.page)) };
31
36
  }
32
37
  //# 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,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,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC7F,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 { paginatedPageKey } from '@ttt-productions/query-core';\nimport { useFirestorePaginated, useReadRetryState } 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"]}
@@ -13,5 +13,40 @@ import type { NotificationHistoryItem, UseNotificationHistoryOptions } from '../
13
13
  * list. Owner-only (user) / admin-only (admin) reads are enforced by Firestore
14
14
  * rules — this hook performs no writes.
15
15
  */
16
- export declare function useNotificationHistory({ config, userId, category, enabled, pageSize, staleTime, }: UseNotificationHistoryOptions): import("@ttt-productions/query-core/react").UseFirestorePaginatedResult<NotificationHistoryItem>;
16
+ export declare function useNotificationHistory({ config, userId, category, enabled, pageSize, staleTime, }: UseNotificationHistoryOptions): {
17
+ retryState: import("@ttt-productions/query-core/react").ReadRetryState<Error>;
18
+ promise: Promise<import("@ttt-productions/query-core").PaginatedPage<import("@ttt-productions/query-core").WithId<NotificationHistoryItem>>>;
19
+ error: Error | null;
20
+ isError: boolean;
21
+ isPending: boolean;
22
+ isLoading: boolean;
23
+ isLoadingError: boolean;
24
+ isRefetchError: boolean;
25
+ isSuccess: boolean;
26
+ isPlaceholderData: boolean;
27
+ status: "success" | "error" | "pending";
28
+ dataUpdatedAt: number;
29
+ errorUpdatedAt: number;
30
+ failureCount: number;
31
+ failureReason: Error | null;
32
+ errorUpdateCount: number;
33
+ isFetched: boolean;
34
+ isFetchedAfterMount: boolean;
35
+ isFetching: boolean;
36
+ isInitialLoading: boolean;
37
+ isPaused: boolean;
38
+ isRefetching: boolean;
39
+ isStale: boolean;
40
+ isEnabled: boolean;
41
+ refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<import("@ttt-productions/query-core").PaginatedPage<import("@ttt-productions/query-core").WithId<NotificationHistoryItem>>, Error>>;
42
+ fetchStatus: import("@tanstack/query-core").FetchStatus;
43
+ data: import("@ttt-productions/query-core").WithId<NotificationHistoryItem>[] | undefined;
44
+ page: number;
45
+ pageSize: number;
46
+ hasNextPage: boolean;
47
+ hasPrevPage: boolean;
48
+ setPage: (page: number) => void;
49
+ nextPage: () => void;
50
+ prevPage: () => void;
51
+ };
17
52
  //# sourceMappingURL=useNotificationHistory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useNotificationHistory.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useNotificationHistory.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,uBAAuB,EACvB,6BAA6B,EAC9B,MAAM,gBAAgB,CAAC;AAKxB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,QAA4B,EAC5B,SAA8B,GAC/B,EAAE,6BAA6B,oGA4B/B"}
1
+ {"version":3,"file":"useNotificationHistory.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useNotificationHistory.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,uBAAuB,EACvB,6BAA6B,EAC9B,MAAM,gBAAgB,CAAC;AAKxB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,QAA4B,EAC5B,SAA8B,GAC/B,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgC/B"}
@@ -1,5 +1,6 @@
1
1
  'use client';
2
- import { useFirestorePaginated } from '@ttt-productions/query-core/react';
2
+ import { paginatedPageKey } from '@ttt-productions/query-core';
3
+ import { useFirestorePaginated, useReadRetryState } from '@ttt-productions/query-core/react';
3
4
  import { orderBy } from 'firebase/firestore';
4
5
  const DEFAULT_PAGE_SIZE = 20;
5
6
  const DEFAULT_STALE_TIME = 60_000;
@@ -24,9 +25,10 @@ export function useNotificationHistory({ config, userId, category, enabled = tru
24
25
  }
25
26
  const collectionPath = categoryConfig.historyPath(userId);
26
27
  const constraints = [orderBy('archivedAt', 'desc')];
27
- return useFirestorePaginated({
28
+ const queryKey = ['notifications', 'history', category, userId, { pageSize }];
29
+ const read = useFirestorePaginated({
28
30
  collectionPath,
29
- queryKey: ['notifications', 'history', category, userId, { pageSize }],
31
+ queryKey,
30
32
  constraints,
31
33
  pageSize,
32
34
  enabled: enabled && !!userId,
@@ -42,5 +44,8 @@ export function useNotificationHistory({ config, userId, category, enabled = tru
42
44
  };
43
45
  },
44
46
  });
47
+ // The displayed page's failure held through its retry: a refetch of a never-succeeded page clears
48
+ // `error` while it runs, so the list would drop its failure under the retry the person pressed.
49
+ return { ...read, retryState: useReadRetryState(read, paginatedPageKey(queryKey, read.page)) };
45
50
  }
46
51
  //# sourceMappingURL=useNotificationHistory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useNotificationHistory.js","sourceRoot":"","sources":["../../../src/react/hooks/useNotificationHistory.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAwB,MAAM,oBAAoB,CAAC;AAOnE,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,GAAG,IAAI,EACd,QAAQ,GAAG,iBAAiB,EAC5B,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,WAAW,CAAC,MAAM,CAAC,CAAC;IAE1D,MAAM,WAAW,GAAsB,CAAC,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IAEvE,OAAO,qBAAqB,CAA0B;QACpD,cAAc;QACd,QAAQ,EAAE,CAAC,eAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC;QACtE,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,SAAS;QACT,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACf,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,gBAAgB,IAAI,EAAE,CAAoB,CAAC;YAClE,OAAO;gBACL,GAAG,QAAQ;gBACX,kEAAkE;gBAClE,mCAAmC;gBACnC,mBAAmB,EAAE,IAAI,CAAC,EAAE;gBAC5B,UAAU,EAAG,IAAI,CAAC,UAAiC,IAAI,QAAQ,CAAC,SAAS,IAAI,CAAC;aAC/E,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\nimport { useFirestorePaginated } from '@ttt-productions/query-core/react';\nimport { orderBy, type QueryConstraint } from 'firebase/firestore';\nimport type {\n NotificationDoc,\n NotificationHistoryItem,\n UseNotificationHistoryOptions,\n} from '../../types.js';\n\nconst DEFAULT_PAGE_SIZE = 20;\nconst DEFAULT_STALE_TIME = 60_000;\n\n/**\n * Paginated read of ARCHIVED notification history (the two-tier system's history\n * tier). Personal history lives under a per-user path\n * (`userProfiles/{uid}/notificationHistory`) and shared/admin history under a\n * top-level collection (`adminNotificationHistory`) — both resolved from the\n * category's `historyPath`, so no `where` clause is needed either way. Rows are\n * immutable and ordered newest-first by `archivedAt`.\n *\n * Each history doc is a wrapper (`archivedSnapshot` + archive metadata + a\n * native-TTL `expireAt`); the `select` mapper flattens it into a\n * {@link NotificationHistoryItem} so the read surface renders like the active\n * list. Owner-only (user) / admin-only (admin) reads are enforced by Firestore\n * rules — this hook performs no writes.\n */\nexport function useNotificationHistory({\n config,\n userId,\n category,\n enabled = true,\n pageSize = DEFAULT_PAGE_SIZE,\n staleTime = DEFAULT_STALE_TIME,\n}: UseNotificationHistoryOptions) {\n const categoryConfig = config.categories[category];\n if (!categoryConfig) {\n throw new Error(`[notification-core] Unknown category: ${category}`);\n }\n\n const collectionPath = categoryConfig.historyPath(userId);\n\n const constraints: QueryConstraint[] = [orderBy('archivedAt', 'desc')];\n\n return useFirestorePaginated<NotificationHistoryItem>({\n collectionPath,\n queryKey: ['notifications', 'history', category, userId, { pageSize }],\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n staleTime,\n select: (data) => {\n const snapshot = (data.archivedSnapshot ?? {}) as NotificationDoc;\n return {\n ...snapshot,\n // The archive occurrence id is the stable history-doc key; expose\n // archivedAt for ordering/display.\n archiveOccurrenceId: data.id,\n archivedAt: (data.archivedAt as number | undefined) ?? snapshot.updatedAt ?? 0,\n };\n },\n });\n}\n"]}
1
+ {"version":3,"file":"useNotificationHistory.js","sourceRoot":"","sources":["../../../src/react/hooks/useNotificationHistory.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC7F,OAAO,EAAE,OAAO,EAAwB,MAAM,oBAAoB,CAAC;AAOnE,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,GAAG,IAAI,EACd,QAAQ,GAAG,iBAAiB,EAC5B,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,WAAW,CAAC,MAAM,CAAC,CAAC;IAE1D,MAAM,WAAW,GAAsB,CAAC,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IAEvE,MAAM,QAAQ,GAAG,CAAC,eAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC9E,MAAM,IAAI,GAAG,qBAAqB,CAA0B;QAC1D,cAAc;QACd,QAAQ;QACR,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,SAAS;QACT,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACf,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,gBAAgB,IAAI,EAAE,CAAoB,CAAC;YAClE,OAAO;gBACL,GAAG,QAAQ;gBACX,kEAAkE;gBAClE,mCAAmC;gBACnC,mBAAmB,EAAE,IAAI,CAAC,EAAE;gBAC5B,UAAU,EAAG,IAAI,CAAC,UAAiC,IAAI,QAAQ,CAAC,SAAS,IAAI,CAAC;aAC/E,CAAC;QACJ,CAAC;KACF,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 { paginatedPageKey } from '@ttt-productions/query-core';\nimport { useFirestorePaginated, useReadRetryState } from '@ttt-productions/query-core/react';\nimport { orderBy, type QueryConstraint } from 'firebase/firestore';\nimport type {\n NotificationDoc,\n NotificationHistoryItem,\n UseNotificationHistoryOptions,\n} from '../../types.js';\n\nconst DEFAULT_PAGE_SIZE = 20;\nconst DEFAULT_STALE_TIME = 60_000;\n\n/**\n * Paginated read of ARCHIVED notification history (the two-tier system's history\n * tier). Personal history lives under a per-user path\n * (`userProfiles/{uid}/notificationHistory`) and shared/admin history under a\n * top-level collection (`adminNotificationHistory`) — both resolved from the\n * category's `historyPath`, so no `where` clause is needed either way. Rows are\n * immutable and ordered newest-first by `archivedAt`.\n *\n * Each history doc is a wrapper (`archivedSnapshot` + archive metadata + a\n * native-TTL `expireAt`); the `select` mapper flattens it into a\n * {@link NotificationHistoryItem} so the read surface renders like the active\n * list. Owner-only (user) / admin-only (admin) reads are enforced by Firestore\n * rules — this hook performs no writes.\n */\nexport function useNotificationHistory({\n config,\n userId,\n category,\n enabled = true,\n pageSize = DEFAULT_PAGE_SIZE,\n staleTime = DEFAULT_STALE_TIME,\n}: UseNotificationHistoryOptions) {\n const categoryConfig = config.categories[category];\n if (!categoryConfig) {\n throw new Error(`[notification-core] Unknown category: ${category}`);\n }\n\n const collectionPath = categoryConfig.historyPath(userId);\n\n const constraints: QueryConstraint[] = [orderBy('archivedAt', 'desc')];\n\n const queryKey = ['notifications', 'history', category, userId, { pageSize }];\n const read = useFirestorePaginated<NotificationHistoryItem>({\n collectionPath,\n queryKey,\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n staleTime,\n select: (data) => {\n const snapshot = (data.archivedSnapshot ?? {}) as NotificationDoc;\n return {\n ...snapshot,\n // The archive occurrence id is the stable history-doc key; expose\n // archivedAt for ordering/display.\n archiveOccurrenceId: data.id,\n archivedAt: (data.archivedAt as number | undefined) ?? snapshot.updatedAt ?? 0,\n };\n },\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"]}
package/dist/schemas.d.ts CHANGED
@@ -3,8 +3,7 @@
3
3
  *
4
4
  * Runtime validation schemas for the generic notification document shapes whose
5
5
  * TypeScript types live in `./types.ts`. notification-core OWNS these shapes, so
6
- * it owns their schemas too — the consuming app (e.g. `@ttt-productions/ttt-core`)
7
- * composes these rather than hand-mirroring them, so the schema can never drift
6
+ * it owns their schemas too — the consuming app's data package composes these rather than hand-mirroring them, so the schema can never drift
8
7
  * from the type.
9
8
  *
10
9
  * The interfaces in `./types.ts` remain the documented public type API. Each
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;iBAoBK,CAAC;AAExC;;GAEG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;iBAQK,CAAC"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;iBAoBK,CAAC;AAExC;;GAEG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;iBAQK,CAAC"}
package/dist/schemas.js CHANGED
@@ -3,8 +3,7 @@
3
3
  *
4
4
  * Runtime validation schemas for the generic notification document shapes whose
5
5
  * TypeScript types live in `./types.ts`. notification-core OWNS these shapes, so
6
- * it owns their schemas too — the consuming app (e.g. `@ttt-productions/ttt-core`)
7
- * composes these rather than hand-mirroring them, so the schema can never drift
6
+ * it owns their schemas too — the consuming app's data package composes these rather than hand-mirroring them, so the schema can never drift
8
7
  * from the type.
9
8
  *
10
9
  * The interfaces in `./types.ts` remain the documented public type API. Each
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,cAAc,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IACnC,sEAAsE;IACtE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;IAC3C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;IAClB,iFAAiF;IACjF,uEAAuE;IACvE,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACzC,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACvC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAsC,CAAC;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;IAC3C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAA0C,CAAC","sourcesContent":["/**\n * @ttt-productions/notification-core — Zod schemas\n *\n * Runtime validation schemas for the generic notification document shapes whose\n * TypeScript types live in `./types.ts`. notification-core OWNS these shapes, so\n * it owns their schemas too — the consuming app (e.g. `@ttt-productions/ttt-core`)\n * composes these rather than hand-mirroring them, so the schema can never drift\n * from the type.\n *\n * The interfaces in `./types.ts` remain the documented public type API. Each\n * schema is compile-time-checked against its interface via\n * `satisfies z.ZodType<Shape>` — a shape drift between the interface and the\n * schema fails the build here.\n *\n * zod-only, no React/firebase: safe for both server (Cloud Functions) and UI.\n */\n\nimport { z } from 'zod';\nimport type { NotificationDoc, PendingNotification } from './types.js';\n\n/**\n * Active notification document (`NotificationDoc`). Same shape for both the user\n * and admin active collections.\n */\nexport const NotificationDocSchema = z.object({\n id: z.string(),\n type: z.string(),\n dedupKey: z.string(),\n category: z.string(),\n targetUserId: z.string().nullable(),\n title: z.string(),\n message: z.string(),\n count: z.number(),\n latestActorIds: z.array(z.string()),\n // Absent on linkless types (no defaultTargetPath in the type config).\n targetPath: z.string().optional(),\n metadata: z.record(z.string(), z.unknown()),\n seenAt: z.number(),\n // Opaque per-generation token rotated on create / material re-light. Optional on\n // the legacy active doc shape; the ledger materializer always sets it.\n activityGeneration: z.string().optional(),\n seenAtGeneration: z.string().optional(),\n createdAt: z.number(),\n updatedAt: z.number(),\n}) satisfies z.ZodType<NotificationDoc>;\n\n/**\n * Pending notification (`PendingNotification`) — queue item for the batch processor.\n */\nexport const PendingNotificationSchema = z.object({\n id: z.string(),\n type: z.string(),\n category: z.string(),\n targetUserId: z.string().nullable(),\n actorId: z.string(),\n metadata: z.record(z.string(), z.unknown()),\n createdAt: z.number(),\n}) satisfies z.ZodType<PendingNotification>;\n"]}
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,cAAc,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IACnC,sEAAsE;IACtE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;IAC3C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;IAClB,iFAAiF;IACjF,uEAAuE;IACvE,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACzC,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACvC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAsC,CAAC;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;IAC3C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAA0C,CAAC","sourcesContent":["/**\n * @ttt-productions/notification-core — Zod schemas\n *\n * Runtime validation schemas for the generic notification document shapes whose\n * TypeScript types live in `./types.ts`. notification-core OWNS these shapes, so\n * it owns their schemas too — the consuming app's data package composes these rather than hand-mirroring them, so the schema can never drift\n * from the type.\n *\n * The interfaces in `./types.ts` remain the documented public type API. Each\n * schema is compile-time-checked against its interface via\n * `satisfies z.ZodType<Shape>` — a shape drift between the interface and the\n * schema fails the build here.\n *\n * zod-only, no React/firebase: safe for both server (Cloud Functions) and UI.\n */\n\nimport { z } from 'zod';\nimport type { NotificationDoc, PendingNotification } from './types.js';\n\n/**\n * Active notification document (`NotificationDoc`). Same shape for both the user\n * and admin active collections.\n */\nexport const NotificationDocSchema = z.object({\n id: z.string(),\n type: z.string(),\n dedupKey: z.string(),\n category: z.string(),\n targetUserId: z.string().nullable(),\n title: z.string(),\n message: z.string(),\n count: z.number(),\n latestActorIds: z.array(z.string()),\n // Absent on linkless types (no defaultTargetPath in the type config).\n targetPath: z.string().optional(),\n metadata: z.record(z.string(), z.unknown()),\n seenAt: z.number(),\n // Opaque per-generation token rotated on create / material re-light. Optional on\n // the legacy active doc shape; the ledger materializer always sets it.\n activityGeneration: z.string().optional(),\n seenAtGeneration: z.string().optional(),\n createdAt: z.number(),\n updatedAt: z.number(),\n}) satisfies z.ZodType<NotificationDoc>;\n\n/**\n * Pending notification (`PendingNotification`) — queue item for the batch processor.\n */\nexport const PendingNotificationSchema = z.object({\n id: z.string(),\n type: z.string(),\n category: z.string(),\n targetUserId: z.string().nullable(),\n actorId: z.string(),\n metadata: z.record(z.string(), z.unknown()),\n createdAt: z.number(),\n}) satisfies z.ZodType<PendingNotification>;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/notification-core",
3
- "version": "0.15.13",
3
+ "version": "0.15.15",
4
4
  "description": "Shared notification system for TTT Productions apps — active/history two-tier architecture with dedup, batch processing, and themed UI components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,8 +42,8 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@tanstack/react-query": ">=5.0.0",
45
- "@ttt-productions/query-core": "^0.14.13",
46
- "@ttt-productions/ui-core": "^0.11.28",
45
+ "@ttt-productions/query-core": "^0.14.15",
46
+ "@ttt-productions/ui-core": "^0.11.31",
47
47
  "firebase": ">=12.0.0",
48
48
  "react": ">=19.0.0",
49
49
  "react-dom": ">=19.0.0"
@@ -70,8 +70,8 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "@tanstack/react-query": "^5.0.0",
73
- "@ttt-productions/query-core": "^0.14.13",
74
- "@ttt-productions/ui-core": "^0.11.28",
73
+ "@ttt-productions/query-core": "^0.14.15",
74
+ "@ttt-productions/ui-core": "^0.11.31",
75
75
  "@types/react": "^19.0.0",
76
76
  "@types/react-dom": "^19.0.0",
77
77
  "firebase": "^12.14.0",