pixelize-design-library 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/COMPONENT-GOTCHAS.md +37 -13
  3. package/DESIGN-GUIDE.md +4 -1
  4. package/dist/Components/NotificationBell/NotificationBellProps.d.ts +3 -3
  5. package/dist/Components/NotificationBell/NotificationItem.d.ts +1 -1
  6. package/dist/Components/NotificationBell/NotificationItem.js +1 -1
  7. package/dist/Components/NotificationBell/NotificationList.d.ts +1 -1
  8. package/dist/Components/NotificationBell/NotificationList.js +1 -1
  9. package/dist/Components/NotificationBell/NotificationPanel.d.ts +1 -1
  10. package/dist/Components/NotificationBell/NotificationPanel.js +1 -1
  11. package/dist/Components/NotificationCenter/center/CenterPanel.d.ts +15 -0
  12. package/dist/Components/NotificationCenter/center/CenterPanel.js +87 -0
  13. package/dist/Components/NotificationCenter/center/NotificationCenter.d.ts +4 -0
  14. package/dist/Components/NotificationCenter/center/NotificationCenter.js +130 -0
  15. package/dist/Components/NotificationCenter/center/NotificationCenterProps.d.ts +71 -0
  16. package/dist/Components/NotificationCenter/center/NotificationCenterProps.js +2 -0
  17. package/dist/Components/NotificationCenter/center/PanelRows.d.ts +10 -0
  18. package/dist/Components/NotificationCenter/center/PanelRows.js +35 -0
  19. package/dist/Components/NotificationCenter/center/PanelStates.d.ts +17 -0
  20. package/dist/Components/NotificationCenter/center/PanelStates.js +21 -0
  21. package/dist/Components/NotificationCenter/center/centerLabels.d.ts +4 -0
  22. package/dist/Components/NotificationCenter/center/centerLabels.js +38 -0
  23. package/dist/Components/NotificationCenter/center/useArrivals.d.ts +14 -0
  24. package/dist/Components/NotificationCenter/center/useArrivals.js +71 -0
  25. package/dist/Components/NotificationCenter/center/useIsSheet.d.ts +2 -0
  26. package/dist/Components/NotificationCenter/center/useIsSheet.js +20 -0
  27. package/dist/Components/NotificationCenter/core/NotificationCenterProvider.d.ts +17 -0
  28. package/dist/Components/NotificationCenter/core/NotificationCenterProvider.js +46 -0
  29. package/dist/Components/NotificationCenter/core/NotificationEngineProps.d.ts +182 -0
  30. package/dist/Components/NotificationCenter/core/NotificationEngineProps.js +2 -0
  31. package/dist/Components/NotificationCenter/core/NotificationSourceProps.d.ts +284 -0
  32. package/dist/Components/NotificationCenter/core/NotificationSourceProps.js +2 -0
  33. package/dist/Components/NotificationCenter/core/actionError.d.ts +4 -0
  34. package/dist/Components/NotificationCenter/core/actionError.js +45 -0
  35. package/dist/Components/NotificationCenter/core/format.d.ts +4 -0
  36. package/dist/Components/NotificationCenter/core/format.js +91 -0
  37. package/dist/Components/NotificationCenter/core/grouping.d.ts +14 -0
  38. package/dist/Components/NotificationCenter/core/grouping.js +102 -0
  39. package/dist/Components/NotificationCenter/core/labels.d.ts +6 -0
  40. package/dist/Components/NotificationCenter/core/labels.js +94 -0
  41. package/dist/Components/NotificationCenter/core/overlay.d.ts +109 -0
  42. package/dist/Components/NotificationCenter/core/overlay.js +281 -0
  43. package/dist/Components/NotificationCenter/core/useNotificationEngine.d.ts +13 -0
  44. package/dist/Components/NotificationCenter/core/useNotificationEngine.js +415 -0
  45. package/dist/Components/NotificationCenter/inbox/InboxList.d.ts +17 -0
  46. package/dist/Components/NotificationCenter/inbox/InboxList.js +25 -0
  47. package/dist/Components/NotificationCenter/inbox/InboxStates.d.ts +17 -0
  48. package/dist/Components/NotificationCenter/inbox/InboxStates.js +27 -0
  49. package/dist/Components/NotificationCenter/inbox/NotificationInbox.d.ts +4 -0
  50. package/dist/Components/NotificationCenter/inbox/NotificationInbox.js +434 -0
  51. package/dist/Components/NotificationCenter/inbox/NotificationInboxProps.d.ts +106 -0
  52. package/dist/Components/NotificationCenter/inbox/NotificationInboxProps.js +2 -0
  53. package/dist/Components/NotificationCenter/inbox/ShortcutsDialog.d.ts +8 -0
  54. package/dist/Components/NotificationCenter/inbox/ShortcutsDialog.js +63 -0
  55. package/dist/Components/NotificationCenter/inbox/inboxLabels.d.ts +4 -0
  56. package/dist/Components/NotificationCenter/inbox/inboxLabels.js +56 -0
  57. package/dist/Components/NotificationCenter/inbox/inboxModel.d.ts +17 -0
  58. package/dist/Components/NotificationCenter/inbox/inboxModel.js +47 -0
  59. package/dist/Components/NotificationCenter/inbox/useControllable.d.ts +1 -0
  60. package/dist/Components/NotificationCenter/inbox/useControllable.js +15 -0
  61. package/dist/Components/NotificationCenter/index.d.ts +8 -0
  62. package/dist/Components/NotificationCenter/index.js +26 -0
  63. package/dist/Components/NotificationCenter/preferences/NotificationPreferences.d.ts +4 -0
  64. package/dist/Components/NotificationCenter/preferences/NotificationPreferences.js +72 -0
  65. package/dist/Components/NotificationCenter/preferences/NotificationPreferencesProps.d.ts +111 -0
  66. package/dist/Components/NotificationCenter/preferences/NotificationPreferencesProps.js +2 -0
  67. package/dist/Components/NotificationCenter/preferences/PreferenceCells.d.ts +16 -0
  68. package/dist/Components/NotificationCenter/preferences/PreferenceCells.js +35 -0
  69. package/dist/Components/NotificationCenter/preferences/PreferenceGroups.d.ts +15 -0
  70. package/dist/Components/NotificationCenter/preferences/PreferenceGroups.js +26 -0
  71. package/dist/Components/NotificationCenter/preferences/preferencesLabels.d.ts +2 -0
  72. package/dist/Components/NotificationCenter/preferences/preferencesLabels.js +28 -0
  73. package/dist/Components/NotificationCenter/preferences/useAutosave.d.ts +8 -0
  74. package/dist/Components/NotificationCenter/preferences/useAutosave.js +41 -0
  75. package/dist/Components/NotificationCenter/row/NotificationDetail.d.ts +4 -0
  76. package/dist/Components/NotificationCenter/row/NotificationDetail.js +65 -0
  77. package/dist/Components/NotificationCenter/row/NotificationDetailProps.d.ts +15 -0
  78. package/dist/Components/NotificationCenter/row/NotificationDetailProps.js +2 -0
  79. package/dist/Components/NotificationCenter/row/NotificationRow.d.ts +3 -0
  80. package/dist/Components/NotificationCenter/row/NotificationRow.js +124 -0
  81. package/dist/Components/NotificationCenter/row/NotificationRowProps.d.ts +33 -0
  82. package/dist/Components/NotificationCenter/row/NotificationRowProps.js +2 -0
  83. package/dist/Components/NotificationCenter/row/ReasonForm.d.ts +14 -0
  84. package/dist/Components/NotificationCenter/row/ReasonForm.js +59 -0
  85. package/dist/Components/NotificationCenter/row/RowActions.d.ts +16 -0
  86. package/dist/Components/NotificationCenter/row/RowActions.js +36 -0
  87. package/dist/Components/NotificationCenter/row/RowQuickActions.d.ts +17 -0
  88. package/dist/Components/NotificationCenter/row/RowQuickActions.js +57 -0
  89. package/dist/Components/NotificationCenter/row/RowStatus.d.ts +10 -0
  90. package/dist/Components/NotificationCenter/row/RowStatus.js +42 -0
  91. package/dist/Components/NotificationCenter/row/category.d.ts +8 -0
  92. package/dist/Components/NotificationCenter/row/category.js +13 -0
  93. package/dist/Components/NotificationCenter/row/rowMenu.d.ts +14 -0
  94. package/dist/Components/NotificationCenter/row/rowMenu.js +37 -0
  95. package/dist/Components/NotificationCenter/row/rowParts.d.ts +40 -0
  96. package/dist/Components/NotificationCenter/row/rowParts.js +72 -0
  97. package/dist/Components/NotificationCenter/row/rowStyles.d.ts +11 -0
  98. package/dist/Components/NotificationCenter/row/rowStyles.js +25 -0
  99. package/dist/Components/SegmentedControl/segmentedInk.d.ts +4 -0
  100. package/dist/Components/SegmentedControl/segmentedInk.js +16 -2
  101. package/dist/Components/SideBar/SecondaryBar.js +2 -2
  102. package/dist/Components/SideBar/SideBar.js +18 -8
  103. package/dist/Components/SideBar/SideBarProps.d.ts +4 -2
  104. package/dist/Components/SideBar/components/MenuItemBox.d.ts +0 -2
  105. package/dist/Components/SideBar/components/MenuItemBox.js +2 -3
  106. package/dist/Components/SideBar/components/MenuItems.js +2 -2
  107. package/dist/Components/SideBar/components/PinnedSection.js +3 -4
  108. package/dist/Components/SideBar/sidebarScale.d.ts +0 -2
  109. package/dist/Components/SideBar/sidebarScale.js +0 -2
  110. package/dist/Components/Switch/Switch.d.ts +1 -1
  111. package/dist/Components/Switch/Switch.js +2 -2
  112. package/dist/Components/Switch/SwitchProps.d.ts +3 -0
  113. package/dist/Components/Toggle/TableToggle.js +4 -5
  114. package/dist/Components/Toggle/tableToggleInk.d.ts +7 -0
  115. package/dist/Components/Toggle/tableToggleInk.js +28 -0
  116. package/dist/esm/Components/NotificationBell/NotificationItem.js +1 -1
  117. package/dist/esm/Components/NotificationBell/NotificationList.js +1 -1
  118. package/dist/esm/Components/NotificationBell/NotificationPanel.js +1 -1
  119. package/dist/esm/Components/NotificationCenter/center/CenterPanel.js +81 -0
  120. package/dist/esm/Components/NotificationCenter/center/NotificationCenter.js +92 -0
  121. package/dist/esm/Components/NotificationCenter/center/NotificationCenterProps.js +1 -0
  122. package/dist/esm/Components/NotificationCenter/center/PanelRows.js +28 -0
  123. package/dist/esm/Components/NotificationCenter/center/PanelStates.js +11 -0
  124. package/dist/esm/Components/NotificationCenter/center/centerLabels.js +34 -0
  125. package/dist/esm/Components/NotificationCenter/center/useArrivals.js +67 -0
  126. package/dist/esm/Components/NotificationCenter/center/useIsSheet.js +16 -0
  127. package/dist/esm/Components/NotificationCenter/core/NotificationCenterProvider.js +35 -0
  128. package/dist/esm/Components/NotificationCenter/core/NotificationEngineProps.js +1 -0
  129. package/dist/esm/Components/NotificationCenter/core/NotificationSourceProps.js +1 -0
  130. package/dist/esm/Components/NotificationCenter/core/actionError.js +39 -0
  131. package/dist/esm/Components/NotificationCenter/core/format.js +87 -0
  132. package/dist/esm/Components/NotificationCenter/core/grouping.js +88 -0
  133. package/dist/esm/Components/NotificationCenter/core/labels.js +88 -0
  134. package/dist/esm/Components/NotificationCenter/core/overlay.js +274 -0
  135. package/dist/esm/Components/NotificationCenter/core/useNotificationEngine.js +408 -0
  136. package/dist/esm/Components/NotificationCenter/inbox/InboxList.js +18 -0
  137. package/dist/esm/Components/NotificationCenter/inbox/InboxStates.js +18 -0
  138. package/dist/esm/Components/NotificationCenter/inbox/NotificationInbox.js +396 -0
  139. package/dist/esm/Components/NotificationCenter/inbox/NotificationInboxProps.js +1 -0
  140. package/dist/esm/Components/NotificationCenter/inbox/ShortcutsDialog.js +23 -0
  141. package/dist/esm/Components/NotificationCenter/inbox/inboxLabels.js +52 -0
  142. package/dist/esm/Components/NotificationCenter/inbox/inboxModel.js +37 -0
  143. package/dist/esm/Components/NotificationCenter/inbox/useControllable.js +11 -0
  144. package/dist/esm/Components/NotificationCenter/index.js +8 -0
  145. package/dist/esm/Components/NotificationCenter/preferences/NotificationPreferences.js +67 -0
  146. package/dist/esm/Components/NotificationCenter/preferences/NotificationPreferencesProps.js +1 -0
  147. package/dist/esm/Components/NotificationCenter/preferences/PreferenceCells.js +27 -0
  148. package/dist/esm/Components/NotificationCenter/preferences/PreferenceGroups.js +21 -0
  149. package/dist/esm/Components/NotificationCenter/preferences/preferencesLabels.js +24 -0
  150. package/dist/esm/Components/NotificationCenter/preferences/useAutosave.js +37 -0
  151. package/dist/esm/Components/NotificationCenter/row/NotificationDetail.js +60 -0
  152. package/dist/esm/Components/NotificationCenter/row/NotificationDetailProps.js +1 -0
  153. package/dist/esm/Components/NotificationCenter/row/NotificationRow.js +119 -0
  154. package/dist/esm/Components/NotificationCenter/row/NotificationRowProps.js +1 -0
  155. package/dist/esm/Components/NotificationCenter/row/ReasonForm.js +52 -0
  156. package/dist/esm/Components/NotificationCenter/row/RowActions.js +28 -0
  157. package/dist/esm/Components/NotificationCenter/row/RowQuickActions.js +49 -0
  158. package/dist/esm/Components/NotificationCenter/row/RowStatus.js +35 -0
  159. package/dist/esm/Components/NotificationCenter/row/category.js +9 -0
  160. package/dist/esm/Components/NotificationCenter/row/rowMenu.js +30 -0
  161. package/dist/esm/Components/NotificationCenter/row/rowParts.js +59 -0
  162. package/dist/esm/Components/NotificationCenter/row/rowStyles.js +19 -0
  163. package/dist/esm/Components/SegmentedControl/segmentedInk.js +15 -2
  164. package/dist/esm/Components/SideBar/SecondaryBar.js +3 -3
  165. package/dist/esm/Components/SideBar/SideBar.js +18 -8
  166. package/dist/esm/Components/SideBar/components/MenuItemBox.js +2 -3
  167. package/dist/esm/Components/SideBar/components/MenuItems.js +2 -2
  168. package/dist/esm/Components/SideBar/components/PinnedSection.js +3 -4
  169. package/dist/esm/Components/SideBar/sidebarScale.js +0 -2
  170. package/dist/esm/Components/Switch/Switch.js +2 -2
  171. package/dist/esm/Components/Toggle/TableToggle.js +4 -5
  172. package/dist/esm/Components/Toggle/tableToggleInk.js +24 -0
  173. package/dist/esm/index.js +1 -0
  174. package/dist/index.d.ts +8 -0
  175. package/dist/index.js +16 -3
  176. package/package.json +1 -1
@@ -0,0 +1,408 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, } from "react";
3
+ import { RefreshCw } from "lucide-react";
4
+ import Button from "../../Button/Button.js";
5
+ import { useToaster } from "../../Toaster/Toaster.js";
6
+ import { BUTTON_SIZES } from "../../../Theme/tokens/primitives/controlSizes.js";
7
+ import { toNotificationActionError } from "./actionError.js";
8
+ import { createNotificationFormatter } from "./format.js";
9
+ import { matchesNotificationQuery, plainTitle } from "./grouping.js";
10
+ import { DEFAULT_NOTIFICATION_LOCALE, resolveNotificationLabels } from "./labels.js";
11
+ import { EMPTY_OVERLAY, buildLiveLists, optimisticCounts, overlayReducer, } from "./overlay.js";
12
+ export const UNDO_WINDOW_MS = 6000;
13
+ export const DECISION_FOLD_MS = 900;
14
+ export const ARCHIVE_FOLD_MS = 220;
15
+ export const CLOCK_TICK_MS = 60000;
16
+ const TOAST_POSITION = "bottom-right";
17
+ const ZERO_WIDTH_SPACE = String.fromCodePoint(0x200b);
18
+ const unique = (ids) => [...new Set(ids)];
19
+ const hasActionId = (entry, actionId) => Boolean(entry.actions && entry.actions.some((action) => action.id === actionId));
20
+ const isLive = (item) => item !== undefined;
21
+ const createEngineActions = (ctx) => {
22
+ const { latest, windows, undoOrder, mounted, batches, dispatch, announce } = ctx;
23
+ const nextBatch = () => {
24
+ batches.current += 1;
25
+ return `batch-${batches.current}`;
26
+ };
27
+ const toast = (props) => latest.current.toaster.showToast({ position: TOAST_POSITION, ...props });
28
+ const closeWindow = (win) => {
29
+ win.timers.forEach(clearTimeout);
30
+ if (win.toastId !== undefined)
31
+ latest.current.toaster.close(win.toastId);
32
+ windows.current.delete(win.batch);
33
+ undoOrder.current = undoOrder.current.filter((batch) => batch !== win.batch);
34
+ };
35
+ const sendWindow = (batch) => {
36
+ const win = windows.current.get(batch);
37
+ if (!win)
38
+ return;
39
+ closeWindow(win);
40
+ dispatch({ type: "fold", batch });
41
+ dispatch({ type: "send", batch });
42
+ win.send();
43
+ };
44
+ const undoWindow = (batch) => {
45
+ const win = windows.current.get(batch);
46
+ if (!win)
47
+ return false;
48
+ closeWindow(win);
49
+ dispatch({ type: "drop", batch });
50
+ const { labels } = latest.current;
51
+ announce(win.kind === "decision" ? labels.decisionUndone : labels.undone);
52
+ return true;
53
+ };
54
+ const openWindow = (win, foldAfter, props) => {
55
+ const timers = [];
56
+ if (foldAfter !== null)
57
+ timers.push(setTimeout(() => dispatch({ type: "fold", batch: win.batch }), foldAfter));
58
+ timers.push(setTimeout(() => sendWindow(win.batch), UNDO_WINDOW_MS));
59
+ const record = { ...win, timers };
60
+ windows.current.set(win.batch, record);
61
+ undoOrder.current = [...undoOrder.current, win.batch];
62
+ const { labels } = latest.current;
63
+ record.toastId = toast({
64
+ ...props,
65
+ duration: null,
66
+ actions: () => (_jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", label: labels.undo, onClick: () => undoWindow(win.batch) })),
67
+ });
68
+ };
69
+ const sendThen = (batch, call, failTitle) => {
70
+ Promise.resolve()
71
+ .then(call)
72
+ .then(() => {
73
+ if (mounted.current)
74
+ dispatch({ type: "settle", batch });
75
+ }, () => {
76
+ if (!mounted.current)
77
+ return;
78
+ dispatch({ type: "drop", batch });
79
+ toast({ status: "error", title: failTitle });
80
+ });
81
+ };
82
+ const retryButton = (ids) => {
83
+ const renderRetry = (onClose) => (_jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", leftIcon: _jsx(RefreshCw, { size: BUTTON_SIZES.xs.iconSize, "aria-hidden": "true" }), label: latest.current.labels.retry, onClick: () => {
84
+ onClose();
85
+ retryMany(ids);
86
+ } }));
87
+ return renderRetry;
88
+ };
89
+ const finishDecisions = (batch, decision, results) => {
90
+ var _a, _b;
91
+ if (!mounted.current)
92
+ return;
93
+ const { labels } = latest.current;
94
+ const outcomes = {};
95
+ results.forEach((result) => {
96
+ outcomes[result.id] = { batch, error: result.error };
97
+ });
98
+ dispatch({ type: "settleDecisions", outcomes });
99
+ const ofKind = (kind) => results.filter((result) => { var _a; return ((_a = result.error) === null || _a === void 0 ? void 0 : _a.kind) === kind; });
100
+ const soleMessage = (list) => { var _a; return (list.length === 1 ? (_a = list[0].error) === null || _a === void 0 ? void 0 : _a.message : undefined); };
101
+ const failed = ofKind("failed");
102
+ const conflicts = ofKind("conflict");
103
+ const forbidden = ofKind("forbidden");
104
+ const needsReason = ofKind("reasonRequired");
105
+ if (failed.length) {
106
+ toast({
107
+ status: "error",
108
+ title: labels.decisionFailed(decision.label, failed.length),
109
+ description: (_a = soleMessage(failed)) !== null && _a !== void 0 ? _a : labels.failedMessage,
110
+ actions: retryButton(failed.map((result) => result.id)),
111
+ });
112
+ }
113
+ if (conflicts.length) {
114
+ const title = labels.conflicted(conflicts.length);
115
+ toast({ status: "info", title, description: soleMessage(conflicts) });
116
+ announce(title);
117
+ }
118
+ if (forbidden.length) {
119
+ toast({ status: "error", title: labels.forbidden(forbidden.length), description: soleMessage(forbidden) });
120
+ }
121
+ if (needsReason.length) {
122
+ const title = labels.reasonNeeded(decision.label);
123
+ toast({ status: "warning", title, description: (_b = soleMessage(needsReason)) !== null && _b !== void 0 ? _b : labels.reasonRequiredMessage });
124
+ announce(title);
125
+ }
126
+ };
127
+ const sendDecisions = (batch, targets, decision) => {
128
+ const { onAction } = latest.current.source;
129
+ if (!onAction || !targets.length)
130
+ return;
131
+ const sends = targets.map((entry) => {
132
+ const action = entry.actions && entry.actions.find((candidate) => candidate.id === decision.actionId);
133
+ if (!action)
134
+ return Promise.resolve({ id: entry.id, error: { kind: "conflict" } });
135
+ return Promise.resolve()
136
+ .then(() => onAction(entry, action, decision.reason))
137
+ .then(() => ({ id: entry.id }), (error) => ({ id: entry.id, error: toNotificationActionError(error) }));
138
+ });
139
+ Promise.all(sends).then((results) => finishDecisions(batch, decision, results));
140
+ };
141
+ const retryMany = (ids) => {
142
+ const { source, live } = latest.current;
143
+ if (source.isOffline)
144
+ return;
145
+ const byDecision = new Map();
146
+ unique(ids)
147
+ .map((id) => live.byId.get(id))
148
+ .filter(isLive)
149
+ .forEach(({ entry, state }) => {
150
+ if (state.status !== "failed" || !state.decision)
151
+ return;
152
+ const group = byDecision.get(state.decision);
153
+ if (group)
154
+ group.push(entry);
155
+ else
156
+ byDecision.set(state.decision, [entry]);
157
+ });
158
+ byDecision.forEach((group, decision) => {
159
+ const batch = nextBatch();
160
+ dispatch({ type: "retry", batch, ids: group.map((entry) => entry.id) });
161
+ sendDecisions(batch, group, decision);
162
+ });
163
+ };
164
+ const setRead = (ids, read) => {
165
+ const { source, live, labels } = latest.current;
166
+ const { onMarkRead } = source;
167
+ if (!onMarkRead || source.isOffline)
168
+ return;
169
+ const targets = unique(ids).filter((id) => {
170
+ const item = live.byId.get(id);
171
+ return item !== undefined && item.entry.isRead !== undefined && item.entry.isRead !== read;
172
+ });
173
+ if (!targets.length)
174
+ return;
175
+ const batch = nextBatch();
176
+ dispatch({ type: "flag", key: "read", batch, ids: targets, value: read, phase: "sending" });
177
+ sendThen(batch, () => onMarkRead(targets, read), labels.readFailed(read));
178
+ };
179
+ const actions = {
180
+ open: (entry, event, action) => {
181
+ const { source, live } = latest.current;
182
+ const item = live.byId.get(entry.id);
183
+ if (item && item.entry.isRead === false)
184
+ setRead([entry.id], true);
185
+ source.onOpen(action && action.href ? { ...entry, href: action.href } : entry, event, action);
186
+ },
187
+ decide: (ids, action, reason) => {
188
+ var _a, _b;
189
+ const { source, live, labels } = latest.current;
190
+ if (!source.onAction || source.isOffline)
191
+ return;
192
+ const items = unique(ids)
193
+ .map((id) => live.byId.get(id))
194
+ .filter(isLive)
195
+ .filter((item) => item.state.status === "open" && hasActionId(item.entry, action.id));
196
+ if (!items.length)
197
+ return;
198
+ const trimmed = reason === undefined ? "" : reason.trim();
199
+ const decision = {
200
+ actionId: action.id,
201
+ label: action.label,
202
+ doneLabel: (_a = action.doneLabel) !== null && _a !== void 0 ? _a : action.label,
203
+ intent: (_b = action.intent) !== null && _b !== void 0 ? _b : "neutral",
204
+ reason: trimmed ? trimmed : undefined,
205
+ };
206
+ const targetIds = items.map((item) => item.entry.id);
207
+ const batch = nextBatch();
208
+ dispatch({ type: "decide", batch, ids: targetIds, decision });
209
+ const subject = items.length > 1 ? labels.requests(items.length) : items[0].entry.subject;
210
+ announce(labels.decidedAnnouncement(decision.doneLabel, subject));
211
+ const decided = items.map((item) => item.entry);
212
+ openWindow({ batch, kind: "decision", ids: targetIds, send: () => sendDecisions(batch, decided, decision) }, DECISION_FOLD_MS, {
213
+ status: decision.intent === "negative" ? "info" : "success",
214
+ title: labels.decided(decision.doneLabel, subject),
215
+ description: decision.reason ? labels.reasonQuote(decision.reason) : undefined,
216
+ });
217
+ },
218
+ undo: (id) => {
219
+ const hit = [...windows.current.values()].find((win) => win.ids.includes(id));
220
+ if (hit)
221
+ undoWindow(hit.batch);
222
+ },
223
+ undoLast: () => {
224
+ const batch = undoOrder.current[undoOrder.current.length - 1];
225
+ return batch === undefined ? false : undoWindow(batch);
226
+ },
227
+ retry: (id) => retryMany([id]),
228
+ dismiss: (id) => dispatch({ type: "dismiss", id }),
229
+ clearReasonRequest: (id) => dispatch({ type: "clearReasonRequest", id }),
230
+ setRead,
231
+ markAllRead: (scope) => {
232
+ const { source, live, labels, counts } = latest.current;
233
+ const { onMarkAllRead } = source;
234
+ if (!onMarkAllRead || source.isOffline)
235
+ return;
236
+ const query = scope.query ? scope.query.trim() : "";
237
+ const ids = live.lists[scope.zone]
238
+ .filter((item) => item.entry.isRead === false &&
239
+ !item.state.isHidden &&
240
+ (!scope.category || item.entry.category === scope.category) &&
241
+ (!query || matchesNotificationQuery(item.entry, query)))
242
+ .map((item) => item.entry.id);
243
+ const wholeActivity = scope.zone === "activity" && !scope.category && !query;
244
+ const count = wholeActivity ? Math.max(ids.length, counts.unread) : ids.length;
245
+ if (!count)
246
+ return;
247
+ const batch = nextBatch();
248
+ dispatch({ type: "markAll", batch, scope, ids });
249
+ const title = labels.markedRead(count);
250
+ announce(title);
251
+ openWindow({ batch, kind: "markAll", ids, send: () => sendThen(batch, () => onMarkAllRead(scope), labels.markAllFailed) }, null, { status: "success", title });
252
+ },
253
+ archive: (ids) => {
254
+ const { source, live, labels } = latest.current;
255
+ const { onArchive } = source;
256
+ if (!onArchive || source.isOffline)
257
+ return;
258
+ const items = unique(ids)
259
+ .map((id) => live.byId.get(id))
260
+ .filter(isLive)
261
+ .filter((item) => item.state.zone === "activity" && !item.state.isHidden);
262
+ if (!items.length)
263
+ return;
264
+ const targetIds = items.map((item) => item.entry.id);
265
+ const batch = nextBatch();
266
+ dispatch({ type: "flag", key: "archived", batch, ids: targetIds, value: true, phase: "undo" });
267
+ const title = labels.archived(targetIds.length);
268
+ announce(title);
269
+ openWindow({ batch, kind: "archive", ids: targetIds, send: () => sendThen(batch, () => onArchive(targetIds), labels.archiveFailed) }, ARCHIVE_FOLD_MS, { status: "success", title, description: targetIds.length === 1 ? plainTitle(items[0].entry.title) : undefined });
270
+ },
271
+ restore: (ids) => {
272
+ const { source, live, labels } = latest.current;
273
+ const { onRestore } = source;
274
+ if (!onRestore || source.isOffline)
275
+ return;
276
+ const targets = unique(ids).filter((id) => {
277
+ const item = live.byId.get(id);
278
+ return item !== undefined && item.state.zone === "archived";
279
+ });
280
+ if (!targets.length)
281
+ return;
282
+ const batch = nextBatch();
283
+ dispatch({ type: "flag", key: "archived", batch, ids: targets, value: false, phase: "sending" });
284
+ toast({ status: "success", title: labels.restored });
285
+ announce(labels.restored);
286
+ sendThen(batch, () => onRestore(targets), labels.restoreFailed);
287
+ },
288
+ extraActionsFor: (entry) => {
289
+ const { extraActions } = latest.current.source;
290
+ return extraActions ? extraActions(entry) : [];
291
+ },
292
+ runExtraAction: (entry, extra) => {
293
+ const { labels } = latest.current;
294
+ Promise.resolve()
295
+ .then(() => extra.onSelect(entry))
296
+ .then(() => {
297
+ if (mounted.current && extra.doneLabel)
298
+ toast({ status: "success", title: extra.doneLabel });
299
+ }, () => {
300
+ if (mounted.current)
301
+ toast({ status: "error", title: labels.extraFailed(extra.label) });
302
+ });
303
+ },
304
+ loadMore: () => {
305
+ const { onLoadMore } = latest.current.source;
306
+ if (onLoadMore)
307
+ onLoadMore();
308
+ },
309
+ retryLoad: () => {
310
+ const { onRetry } = latest.current.source;
311
+ if (onRetry)
312
+ onRetry();
313
+ },
314
+ announce,
315
+ flush: () => {
316
+ [...windows.current.keys()].forEach(sendWindow);
317
+ },
318
+ };
319
+ return actions;
320
+ };
321
+ export const useNotificationEngine = (source) => {
322
+ const { entries, counts, categories, locale = DEFAULT_NOTIFICATION_LOCALE, labels: labelOverrides, status = "ready", isOffline = false, hasMore = false, isLoadingMore = false, loadedAt, appName, } = source;
323
+ const toaster = useToaster();
324
+ const [overlay, dispatch] = useReducer(overlayReducer, EMPTY_OVERLAY);
325
+ const [announcement, setAnnouncement] = useState("");
326
+ const [now, setNow] = useState(() => Date.now());
327
+ const cache = useRef(new WeakMap());
328
+ const labels = useMemo(() => resolveNotificationLabels(locale, labelOverrides), [locale, labelOverrides]);
329
+ const format = useMemo(() => createNotificationFormatter(locale, labels), [locale, labels]);
330
+ const live = useMemo(() => buildLiveLists(entries, overlay, categories, cache.current), [entries, overlay, categories]);
331
+ const { needsYou: serverNeedsYou, unread: serverUnread } = counts;
332
+ const liveCounts = useMemo(() => optimisticCounts(live, overlay, categories, { needsYou: serverNeedsYou, unread: serverUnread }), [live, overlay, categories, serverNeedsYou, serverUnread]);
333
+ const latest = useRef({ source, labels, live, counts: liveCounts, toaster });
334
+ useLayoutEffect(() => {
335
+ latest.current = { source, labels, live, counts: liveCounts, toaster };
336
+ });
337
+ const windows = useRef(new Map());
338
+ const undoOrder = useRef([]);
339
+ const mounted = useRef(true);
340
+ const batches = useRef(0);
341
+ const flip = useRef(false);
342
+ const announce = useCallback((message) => {
343
+ flip.current = !flip.current;
344
+ setAnnouncement(flip.current ? `${message}${ZERO_WIDTH_SPACE}` : message);
345
+ }, []);
346
+ const actions = useMemo(() => createEngineActions({ latest, windows, undoOrder, mounted, batches, dispatch, announce }), [announce]);
347
+ const find = useCallback((id) => live.byId.get(id), [live]);
348
+ useEffect(() => {
349
+ mounted.current = true;
350
+ return () => {
351
+ mounted.current = false;
352
+ actions.flush();
353
+ };
354
+ }, [actions]);
355
+ useEffect(() => {
356
+ const onPageHide = () => actions.flush();
357
+ const onVisibility = () => {
358
+ if (document.visibilityState === "hidden")
359
+ actions.flush();
360
+ };
361
+ window.addEventListener("pagehide", onPageHide);
362
+ document.addEventListener("visibilitychange", onVisibility);
363
+ return () => {
364
+ window.removeEventListener("pagehide", onPageHide);
365
+ document.removeEventListener("visibilitychange", onVisibility);
366
+ };
367
+ }, [actions]);
368
+ useEffect(() => {
369
+ const id = setInterval(() => setNow(Date.now()), CLOCK_TICK_MS);
370
+ return () => clearInterval(id);
371
+ }, []);
372
+ const countsKey = `${serverNeedsYou}|${serverUnread}`;
373
+ const lastCounts = useRef(countsKey);
374
+ useEffect(() => {
375
+ if (lastCounts.current === countsKey)
376
+ return;
377
+ lastCounts.current = countsKey;
378
+ dispatch({ type: "countsChanged" });
379
+ }, [countsKey]);
380
+ useEffect(() => {
381
+ dispatch({ type: "reconcile", entries });
382
+ }, [entries]);
383
+ const seen = useRef(null);
384
+ useEffect(() => {
385
+ if (status !== "ready")
386
+ return;
387
+ const newest = entries.reduce((max, entry) => Math.max(max, entry.createdAt), 0);
388
+ const known = seen.current;
389
+ seen.current = { ids: new Set(entries.map((entry) => entry.id)), newest: Math.max(newest, known ? known.newest : 0) };
390
+ if (!known)
391
+ return;
392
+ const fresh = entries.filter((entry) => !known.ids.has(entry.id) && entry.createdAt > known.newest && !entry.archived);
393
+ if (fresh.length === 1)
394
+ announce(labels.arrivedOne(plainTitle(fresh[0].title)));
395
+ else if (fresh.length > 1)
396
+ announce(labels.arrivedMany(fresh.length));
397
+ }, [entries, status, labels, announce]);
398
+ const decide = Boolean(source.onAction);
399
+ const markRead = Boolean(source.onMarkRead);
400
+ const markAllRead = Boolean(source.onMarkAllRead);
401
+ const archive = Boolean(source.onArchive);
402
+ const restore = Boolean(source.onRestore);
403
+ const loadMore = Boolean(source.onLoadMore);
404
+ const features = useMemo(() => ({ decide, markRead, markAllRead, archive, restore, loadMore }), [decide, markRead, markAllRead, archive, restore, loadMore]);
405
+ const shared = useMemo(() => ({ labels, format, now, categories, features, isOffline, appName, actions }), [labels, format, now, categories, features, isOffline, appName, actions]);
406
+ const value = useMemo(() => ({ ...shared, lists: live.lists, counts: liveCounts, status, loadedAt, hasMore, isLoadingMore, find }), [shared, live, liveCounts, status, loadedAt, hasMore, isLoadingMore, find]);
407
+ return { value, shared, announcement };
408
+ };
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { chakra } from "@chakra-ui/react";
3
+ import { Box } from "../../Primitives/Primitives.js";
4
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
5
+ import NotificationRow from "../row/NotificationRow.js";
6
+ export const InboxList = ({ groups, isPhone, selectedId, checked, openThreads, idPrefix, onOpenRow, onCheckedChange, onThreadToggle, linkRefFor, }) => {
7
+ const { colors } = useCustomTheme();
8
+ const variant = isPhone ? "phone" : "inbox";
9
+ const rowFor = (item, extra) => (_jsx(NotificationRow, { item: item, variant: variant, isSelected: !isPhone && selectedId === item.entry.id, isChecked: checked.has(item.entry.id), onCheckedChange: onCheckedChange, thread: extra.thread, isThreadOpen: extra.isOpen, onThreadToggle: extra.thread ? onThreadToggle : undefined, isThreadChild: extra.isChild, onOpen: onOpenRow, linkRef: linkRefFor(item.entry.id) }, item.entry.id));
10
+ return (_jsx(Box, { pb: 2, children: groups.map((group) => {
11
+ const headingId = `${idPrefix}-group-${group.key}`;
12
+ return (_jsxs(chakra.section, { "aria-labelledby": group.label === null ? undefined : headingId, children: [group.label !== null && (_jsxs(chakra.h2, { id: headingId, position: "sticky", top: 0, zIndex: "docked", display: "flex", alignItems: "center", gap: 1.5, m: 0, px: 4, pt: 3, pb: 1.5, bg: colors.surfaceRaised, color: colors.textMuted, fontSize: "xs-minus", fontWeight: 600, letterSpacing: "0.06em", textTransform: "uppercase", children: [_jsx("span", { children: group.label }), group.count > 0 && _jsx(chakra.span, { sx: { fontVariantNumeric: "tabular-nums" }, children: group.count })] })), _jsx(chakra.ul, { listStyleType: "none", display: "grid", gap: "0.125rem", m: 0, px: 1.5, py: 0, children: group.threads.flatMap((thread) => {
13
+ const isOpen = openThreads.has(thread.key);
14
+ const lead = rowFor(thread.lead, { thread: thread.rest.length > 0 ? thread : undefined, isOpen });
15
+ return isOpen ? [lead, ...thread.rest.map((item) => rowFor(item, { isChild: true }))] : [lead];
16
+ }) })] }, group.key));
17
+ }) }));
18
+ };
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Archive, Bell, CircleAlert, CircleCheck, Lock } from "lucide-react";
3
+ import { Box, Flex } from "../../Primitives/Primitives.js";
4
+ import Button from "../../Button/Button.js";
5
+ import EmptyState from "../../EmptyState/EmptyState.js";
6
+ import Skeletons from "../../Skeletons/Skeletons.js";
7
+ const SKELETON_ROWS = 6;
8
+ export const InboxSkeleton = ({ isPhone, label }) => (_jsx(Flex, { direction: "column", gap: 1, px: 2, py: 2, role: "status", "aria-busy": "true", "aria-label": label, children: Array.from({ length: SKELETON_ROWS }, (_, index) => (_jsxs(Flex, { gap: 3, px: 3, py: 2.5, "aria-hidden": "true", children: [_jsx(Skeletons, { type: "circle", size: isPhone ? "10" : "8" }), _jsx(Box, { flex: 1, children: _jsx(Skeletons, { type: "text", noOfLines: index % 2 === 0 ? 3 : 2, spacing: 2, skeletonHeight: 2 }) })] }, index))) }));
9
+ export const InboxLoadFailure = ({ kind, labels, appName, onRetry }) => kind === "error" ? (_jsx(EmptyState, { icon: CircleAlert, colorScheme: "error", title: labels.loadErrorTitle, description: labels.loadErrorBody, action: onRetry && _jsx(Button, { size: "sm", variant: "outline", colorScheme: "gray", label: labels.retry, onClick: onRetry }) })) : (_jsx(EmptyState, { icon: Lock, colorScheme: "gray", title: labels.noAccessTitle, description: labels.noAccessBody(appName) }));
10
+ export const InboxEmpty = ({ tab, labels }) => {
11
+ if (tab === "needsYou") {
12
+ return _jsx(EmptyState, { icon: CircleCheck, colorScheme: "success", title: labels.caughtUpTitle, description: labels.caughtUpBody });
13
+ }
14
+ if (tab === "archived") {
15
+ return _jsx(EmptyState, { icon: Archive, colorScheme: "gray", title: labels.archivedEmptyTitle, description: labels.archivedEmptyBody });
16
+ }
17
+ return _jsx(EmptyState, { icon: Bell, colorScheme: "gray", title: labels.activityEmptyTitle, description: labels.activityEmptyBody });
18
+ };