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,396 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { Archive, Check, CheckCheck, Inbox, Keyboard, Mail, MailOpen, Settings, SlidersHorizontal, WifiOff } from "lucide-react";
4
+ import Button from "../../Button/Button.js";
5
+ import BulkSelectionBar from "../../BulkSelectionBar/BulkSelectionBar.js";
6
+ import Drawer, { DrawerBody, DrawerHeader } from "../../Drawer/Drawer.js";
7
+ import EmptyState from "../../EmptyState/EmptyState.js";
8
+ import FilteredEmptyState from "../../EmptyState/FilteredEmptyState.js";
9
+ import FilterBar from "../../FilterBar/FilterBar.js";
10
+ import IconButton from "../../IconButton/IconButton.js";
11
+ import PageHeader from "../../PageHeader/PageHeader.js";
12
+ import { Box, Flex, Icon, Text } from "../../Primitives/Primitives.js";
13
+ import Search from "../../Search/Search.js";
14
+ import Select from "../../Select/Select.js";
15
+ import Tabs from "../../Tabs/Tabs.js";
16
+ import ToggleSwitch from "../../ToggleSwitch/ToggleSwitch.js";
17
+ import ToolTip from "../../ToolTip/ToolTip.js";
18
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
19
+ import { ruleOn } from "../../../Theme/tokens/builders/planeStep.js";
20
+ import { scrollbarStyles } from "../../../Theme/tokens/builders/scrollbar.js";
21
+ import { BORDER_WIDTHS } from "../../../Theme/tokens/primitives/borderWidths.js";
22
+ import { RADIUS_ROLE } from "../../../Theme/tokens/primitives/radii.js";
23
+ import { NotificationScope, useNotificationCenter } from "../core/NotificationCenterProvider.js";
24
+ import { groupByCategory, groupByDay, threadEntries } from "../core/grouping.js";
25
+ import NotificationDetail from "../row/NotificationDetail.js";
26
+ import { InboxEmpty, InboxLoadFailure, InboxSkeleton } from "./InboxStates.js";
27
+ import { InboxList } from "./InboxList.js";
28
+ import { ShortcutsDialog } from "./ShortcutsDialog.js";
29
+ import { ALL_TYPES, bulkDecision, filterItems, isShown, isTypingTarget, threadsByLead, visibleRowIds, } from "./inboxModel.js";
30
+ import { withInboxLabels } from "./inboxLabels.js";
31
+ import { useControllable } from "./useControllable.js";
32
+ const TAB_BY_ID = { needsYou: "needsYou", activity: "activity", archived: "archived" };
33
+ const NotificationInboxBody = ({ variant = "desktop", tab: tabProp, onTabChange, selectedId: selectedProp, onSelectedChange, onOpenSettings, labels: labelOverrides, "data-testid": testId, }) => {
34
+ const { colors } = useCustomTheme();
35
+ const center = useNotificationCenter();
36
+ const { lists, counts, categories, features, format, now, status, isOffline, hasMore, isLoadingMore, appName, actions, find } = center;
37
+ const labels = useMemo(() => withInboxLabels(center.labels, format, labelOverrides), [center.labels, format, labelOverrides]);
38
+ const baseId = useId();
39
+ const isPhone = variant === "phone";
40
+ const plane = colors.surfaceRaised;
41
+ const rule = `${BORDER_WIDTHS.hairline} solid ${ruleOn(colors, plane)}`;
42
+ const outline = `${BORDER_WIDTHS.hairline} solid ${colors.boxborder[200]}`;
43
+ const [tabValue, setTab] = useControllable(tabProp, "needsYou", onTabChange);
44
+ const [selectedId, setSelectedRaw, isSelectionControlled] = useControllable(selectedProp, null, onSelectedChange);
45
+ const [query, setQuery] = useState("");
46
+ const [type, setType] = useState(ALL_TYPES);
47
+ const [unreadOnly, setUnreadOnly] = useState(false);
48
+ const [checked, setChecked] = useState(() => new Set());
49
+ const [openThreads, setOpenThreads] = useState(() => new Set());
50
+ const [keysOpen, setKeysOpen] = useState(false);
51
+ const [filtersOpen, setFiltersOpen] = useState(false);
52
+ const rootRef = useRef(null);
53
+ const searchRef = useRef(null);
54
+ const titleRef = useRef(null);
55
+ const links = useRef(new Map());
56
+ const linkRefs = useRef(new Map());
57
+ const pendingFocus = useRef(null);
58
+ const orderRef = useRef([]);
59
+ const activeTab = tabValue === "archived" && !features.archive ? "needsYou" : tabValue;
60
+ const showUnreadOnly = unreadOnly && activeTab !== "needsYou";
61
+ const selectedItem = selectedId === null ? undefined : find(selectedId);
62
+ const isDetailOpen = isPhone && selectedItem !== undefined;
63
+ const isReady = status === "ready";
64
+ const canSelect = features.decide || features.markRead || features.archive || features.restore;
65
+ const base = lists[activeTab];
66
+ const filtered = useMemo(() => filterItems(base, { type, unreadOnly: showUnreadOnly, query, keepId: selectedId }), [base, type, showUnreadOnly, query, selectedId]);
67
+ const groups = useMemo(() => activeTab === "needsYou"
68
+ ? groupByCategory(filtered, categories, labels.otherCategory)
69
+ : groupByDay(filtered, format, now), [activeTab, filtered, categories, labels.otherCategory, format, now]);
70
+ const visibleItems = useMemo(() => filtered.filter(isShown), [filtered]);
71
+ const rowIds = useMemo(() => visibleRowIds(groups, openThreads), [groups, openThreads]);
72
+ const filterCount = (query.trim() === "" ? 0 : 1) + (type === ALL_TYPES ? 0 : 1) + (showUnreadOnly ? 1 : 0);
73
+ const typeOptions = useMemo(() => {
74
+ const tally = new Map();
75
+ base.filter(isShown).forEach(({ entry }) => { var _a; return tally.set(entry.category, ((_a = tally.get(entry.category)) !== null && _a !== void 0 ? _a : 0) + 1); });
76
+ const present = Object.keys(categories).filter((key) => tally.has(key) || key === type);
77
+ return [
78
+ { id: ALL_TYPES, label: labels.allTypes },
79
+ ...present.map((key) => { var _a; return ({ id: key, label: `${categories[key].label} (${format.number((_a = tally.get(key)) !== null && _a !== void 0 ? _a : 0)})` }); }),
80
+ ];
81
+ }, [base, categories, type, labels.allTypes, format]);
82
+ const visibleIds = useMemo(() => new Set(visibleItems.map((item) => item.entry.id)), [visibleItems]);
83
+ const leadThreads = useMemo(() => threadsByLead(groups), [groups]);
84
+ const checkedIds = useMemo(() => [...checked].filter((id) => visibleIds.has(id)), [checked, visibleIds]);
85
+ const checkedItems = useMemo(() => checkedIds.flatMap((id) => {
86
+ const item = find(id);
87
+ if (!item)
88
+ return [];
89
+ const thread = leadThreads.get(id);
90
+ return thread && !openThreads.has(id) ? threadEntries(thread).filter(isShown) : [item];
91
+ }), [checkedIds, find, leadThreads, openThreads]);
92
+ const select = (id, isExplicit) => {
93
+ setSelectedRaw(id);
94
+ if (id === null || !isExplicit)
95
+ return;
96
+ const item = find(id);
97
+ if (item && item.state.zone === "activity" && item.entry.isRead === false)
98
+ actions.setRead([id], true);
99
+ if (isPhone)
100
+ pendingFocus.current = { kind: "title" };
101
+ };
102
+ const clearChecked = () => setChecked(new Set());
103
+ const clearFilters = () => {
104
+ setQuery("");
105
+ setType(ALL_TYPES);
106
+ setUnreadOnly(false);
107
+ };
108
+ const latest = useRef({ select });
109
+ latest.current = { select };
110
+ const onOpenRow = useCallback((item) => latest.current.select(item.entry.id, true), []);
111
+ const onCheckedChange = useCallback((item, isChecked) => {
112
+ setChecked((current) => {
113
+ const next = new Set(current);
114
+ if (isChecked)
115
+ next.add(item.entry.id);
116
+ else
117
+ next.delete(item.entry.id);
118
+ return next;
119
+ });
120
+ }, []);
121
+ const onThreadToggle = useCallback((thread) => {
122
+ setOpenThreads((current) => {
123
+ const next = new Set(current);
124
+ if (!next.delete(thread.key))
125
+ next.add(thread.key);
126
+ return next;
127
+ });
128
+ }, []);
129
+ const linkRefFor = useCallback((id) => {
130
+ const cached = linkRefs.current.get(id);
131
+ if (cached)
132
+ return cached;
133
+ const ref = (element) => {
134
+ if (element)
135
+ links.current.set(id, element);
136
+ else
137
+ links.current.delete(id);
138
+ };
139
+ linkRefs.current.set(id, ref);
140
+ return ref;
141
+ }, []);
142
+ const goBack = () => {
143
+ if (selectedId !== null)
144
+ pendingFocus.current = { kind: "row", id: selectedId };
145
+ setSelectedRaw(null);
146
+ };
147
+ useEffect(() => {
148
+ clearChecked();
149
+ setType(ALL_TYPES);
150
+ }, [activeTab]);
151
+ useLayoutEffect(() => {
152
+ var _a, _b;
153
+ const previous = orderRef.current;
154
+ orderRef.current = rowIds;
155
+ if (!isReady)
156
+ return;
157
+ if (selectedId === null) {
158
+ if (!isPhone && !isSelectionControlled && rowIds.length > 0)
159
+ setSelectedRaw(rowIds[0]);
160
+ return;
161
+ }
162
+ if (!previous.includes(selectedId) || rowIds.includes(selectedId))
163
+ return;
164
+ if (isPhone) {
165
+ if (selectedItem === undefined || selectedItem.state.isHidden || selectedItem.state.zone === "archived") {
166
+ setSelectedRaw(null);
167
+ }
168
+ return;
169
+ }
170
+ const index = previous.indexOf(selectedId);
171
+ const after = previous.slice(index + 1).find((id) => rowIds.includes(id));
172
+ const before = previous.slice(0, index).reverse().find((id) => rowIds.includes(id));
173
+ const next = (_a = after !== null && after !== void 0 ? after : before) !== null && _a !== void 0 ? _a : null;
174
+ const active = document.activeElement;
175
+ const hadFocus = active === document.body || (((_b = rootRef.current) === null || _b === void 0 ? void 0 : _b.contains(active)) === true && !isTypingTarget(active));
176
+ if (next !== null && hadFocus)
177
+ pendingFocus.current = { kind: "row", id: next };
178
+ setSelectedRaw(next);
179
+ });
180
+ useEffect(() => {
181
+ const pending = pendingFocus.current;
182
+ if (!pending)
183
+ return;
184
+ if (pending.kind === "title" && isDetailOpen && titleRef.current) {
185
+ pendingFocus.current = null;
186
+ titleRef.current.focus();
187
+ }
188
+ else if (pending.kind === "row" && !isDetailOpen) {
189
+ const link = links.current.get(pending.id);
190
+ if (link) {
191
+ pendingFocus.current = null;
192
+ link.focus();
193
+ }
194
+ }
195
+ });
196
+ const filtersKey = `${activeTab}|${type}|${query}|${showUnreadOnly}`;
197
+ const lastFiltersKey = useRef(filtersKey);
198
+ useEffect(() => {
199
+ if (lastFiltersKey.current === filtersKey)
200
+ return undefined;
201
+ lastFiltersKey.current = filtersKey;
202
+ const timer = setTimeout(() => actions.announce(labels.results(visibleItems.length)), 500);
203
+ return () => clearTimeout(timer);
204
+ }, [filtersKey, actions, labels, visibleItems.length]);
205
+ const focusSearch = () => { var _a, _b; return (_b = (_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.querySelector("input")) === null || _b === void 0 ? void 0 : _b.focus(); };
206
+ const move = (direction) => {
207
+ var _a;
208
+ if (rowIds.length === 0)
209
+ return;
210
+ const index = selectedId === null ? -1 : rowIds.indexOf(selectedId);
211
+ const next = rowIds[Math.max(0, Math.min(rowIds.length - 1, index < 0 ? 0 : index + direction))];
212
+ if (!isPhone)
213
+ select(next, true);
214
+ (_a = links.current.get(next)) === null || _a === void 0 ? void 0 : _a.focus();
215
+ };
216
+ const onKey = (event) => {
217
+ var _a, _b, _c;
218
+ const target = event.target;
219
+ const inside = target instanceof Node && rootRef.current !== null && rootRef.current.contains(target);
220
+ if (event.defaultPrevented || keysOpen || filtersOpen || (!inside && target !== document.body))
221
+ return;
222
+ const typing = isTypingTarget(target);
223
+ const isUndo = (event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "z";
224
+ if (typing) {
225
+ if (event.key === "Escape" && target instanceof HTMLInputElement && ((_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.contains(target))) {
226
+ setQuery("");
227
+ target.blur();
228
+ }
229
+ return;
230
+ }
231
+ if (isUndo) {
232
+ if (!actions.undoLast())
233
+ actions.announce(labels.nothingToUndo);
234
+ event.preventDefault();
235
+ return;
236
+ }
237
+ if (event.metaKey || event.ctrlKey || event.altKey)
238
+ return;
239
+ const inList = target instanceof Node && ((_c = (_b = rootRef.current) === null || _b === void 0 ? void 0 : _b.querySelector('[role="tabpanel"]')) === null || _c === void 0 ? void 0 : _c.contains(target)) === true;
240
+ const handled = (() => {
241
+ switch (event.key) {
242
+ case "/":
243
+ focusSearch();
244
+ return true;
245
+ case "j":
246
+ move(1);
247
+ return true;
248
+ case "k":
249
+ move(-1);
250
+ return true;
251
+ case "ArrowDown":
252
+ if (inList)
253
+ move(1);
254
+ return inList;
255
+ case "ArrowUp":
256
+ if (inList)
257
+ move(-1);
258
+ return inList;
259
+ case "?":
260
+ setKeysOpen(true);
261
+ return true;
262
+ case "z":
263
+ if (!actions.undoLast())
264
+ actions.announce(labels.nothingToUndo);
265
+ return true;
266
+ case "Escape":
267
+ if (checked.size === 0)
268
+ return false;
269
+ clearChecked();
270
+ return true;
271
+ default:
272
+ break;
273
+ }
274
+ if (!selectedItem || isOffline)
275
+ return false;
276
+ const { entry, state } = selectedItem;
277
+ switch (event.key) {
278
+ case "o":
279
+ actions.open(entry);
280
+ return true;
281
+ case "u":
282
+ if (!features.markRead || entry.isRead === undefined)
283
+ return false;
284
+ actions.setRead([entry.id], !entry.isRead);
285
+ return true;
286
+ case "e":
287
+ if (state.zone === "activity" && features.archive)
288
+ actions.archive([entry.id]);
289
+ else if (state.zone === "archived" && features.restore)
290
+ actions.restore([entry.id]);
291
+ else
292
+ return false;
293
+ return true;
294
+ case "x":
295
+ if (!canSelect || !visibleIds.has(entry.id))
296
+ return false;
297
+ onCheckedChange(selectedItem, !checked.has(entry.id));
298
+ return true;
299
+ default:
300
+ return false;
301
+ }
302
+ })();
303
+ if (handled)
304
+ event.preventDefault();
305
+ };
306
+ const keyRef = useRef(onKey);
307
+ keyRef.current = onKey;
308
+ useEffect(() => {
309
+ const listener = (event) => keyRef.current(event);
310
+ document.addEventListener("keydown", listener);
311
+ return () => document.removeEventListener("keydown", listener);
312
+ }, []);
313
+ const unreadHere = visibleItems.some((item) => item.entry.isRead === false);
314
+ const markAllRead = () => {
315
+ if (activeTab === "archived")
316
+ return;
317
+ actions.markAllRead({
318
+ zone: activeTab,
319
+ category: type === ALL_TYPES ? undefined : type,
320
+ query: query.trim() === "" ? undefined : query.trim(),
321
+ });
322
+ };
323
+ const bulkActions = useMemo(() => {
324
+ const ids = checkedItems.map((item) => item.entry.id);
325
+ const afterwards = (run) => () => {
326
+ run();
327
+ clearChecked();
328
+ };
329
+ const result = [];
330
+ if (checkedIds.length < visibleItems.length) {
331
+ result.push({ id: "select-all", label: labels.selectAll, onClick: () => setChecked(new Set(visibleIds)) });
332
+ }
333
+ const decision = features.decide ? bulkDecision(checkedItems) : undefined;
334
+ if (decision) {
335
+ result.push({
336
+ id: "decide",
337
+ label: labels.bulkDecide(decision.label, checkedIds.length),
338
+ tone: "primary",
339
+ icon: Check,
340
+ isDisabled: isOffline,
341
+ onClick: afterwards(() => actions.decide(ids, decision)),
342
+ });
343
+ }
344
+ const unread = checkedItems.filter((item) => item.entry.isRead === false).map((item) => item.entry.id);
345
+ const read = checkedItems.filter((item) => item.entry.isRead === true).map((item) => item.entry.id);
346
+ if (features.markRead && unread.length > 0) {
347
+ result.push({ id: "read", label: labels.markRead, icon: MailOpen, isDisabled: isOffline, onClick: afterwards(() => actions.setRead(unread, true)) });
348
+ }
349
+ else if (features.markRead && read.length > 0) {
350
+ result.push({ id: "unread", label: labels.markUnread, icon: Mail, isDisabled: isOffline, onClick: afterwards(() => actions.setRead(read, false)) });
351
+ }
352
+ if (activeTab === "activity" && features.archive) {
353
+ result.push({ id: "archive", label: labels.archive, icon: Archive, isDisabled: isOffline, onClick: afterwards(() => actions.archive(ids)) });
354
+ }
355
+ if (activeTab === "archived" && features.restore) {
356
+ result.push({ id: "restore", label: labels.restore, icon: Archive, isDisabled: isOffline, onClick: afterwards(() => actions.restore(ids)) });
357
+ }
358
+ return result;
359
+ }, [checkedItems, checkedIds.length, visibleItems.length, visibleIds, features, labels, isOffline, activeTab, actions]);
360
+ const tabItems = [
361
+ { id: "needsYou", label: labels.needsYouTab, count: counts.needsYou > 0 ? counts.needsYou : undefined },
362
+ { id: "activity", label: labels.activityTab, count: counts.unread > 0 ? counts.unread : undefined },
363
+ ...(features.archive ? [{ id: "archived", label: labels.archivedTab }] : []),
364
+ ];
365
+ const typeSelect = (_jsx(Select, { native: false, isSearchable: false, size: isPhone ? "md" : "sm", "aria-label": labels.typeFilter, options: typeOptions, value: type, isDisabled: !isReady, width: isPhone ? "100%" : "11rem", onChange: (option) => setType(option === undefined ? ALL_TYPES : String(option.id)) }));
366
+ const unreadSwitch = activeTab !== "needsYou" && (_jsx(ToggleSwitch, { size: isPhone ? "md" : "sm", label: labels.unreadOnly, isChecked: showUnreadOnly, isDisabled: !isReady, onChange: setUnreadOnly }));
367
+ const search = (_jsx(Box, { ref: searchRef, minW: isPhone ? 0 : "15rem", flex: isPhone ? 1 : undefined, children: _jsx(Search, { size: isPhone ? "md" : "sm", placeholder: labels.search, "aria-label": labels.search, shortcut: false, query: query, isDisabled: !isReady, onSearch: (event) => setQuery(event.target.value), onClear: () => {
368
+ setQuery("");
369
+ focusSearch();
370
+ } }) }));
371
+ const bulkBar = (_jsx(BulkSelectionBar, { placement: "inline", selectedCount: checkedIds.length, countLabel: labels.selected(checkedIds.length), actions: bulkActions, clearLabel: labels.clearSelection, onClearSelection: clearChecked, "data-testid": testId ? `${testId}-bulk` : undefined }));
372
+ const toolbar = checkedIds.length > 0 ? (_jsx(Box, { flex: 1, minW: 0, py: 2, children: bulkBar })) : isPhone ? (_jsxs(Flex, { gap: 2, align: "center", w: "100%", py: 2, children: [search, _jsxs(Box, { position: "relative", flexShrink: 0, children: [_jsx(IconButton, { size: "md", variant: "ghost", colorScheme: "gray", "aria-label": labels.filters, "aria-haspopup": "dialog", icon: _jsx(Icon, { as: SlidersHorizontal, boxSize: 5, "aria-hidden": "true" }), onClick: () => setFiltersOpen(true) }), (type !== ALL_TYPES || showUnreadOnly) && (_jsx(Box, { position: "absolute", top: 2, right: 2, boxSize: 2, borderRadius: "full", bg: colors.primary[500], "aria-hidden": "true" }))] })] })) : (_jsx(Box, { ml: "auto", py: 2, children: _jsxs(FilterBar, { search: search, searchLabel: labels.search, children: [typeSelect, unreadSwitch] }) }));
373
+ const showMarkAll = features.markAllRead && isReady && activeTab !== "archived";
374
+ const headerActions = (_jsxs(_Fragment, { children: [showMarkAll &&
375
+ (isPhone ? (_jsx(ToolTip, { label: labels.markAllRead, children: _jsx(IconButton, { size: "md", variant: "ghost", colorScheme: "gray", "aria-label": labels.markAllRead, icon: _jsx(Icon, { as: CheckCheck, boxSize: 5, "aria-hidden": "true" }), isDisabled: !unreadHere || isOffline, onClick: markAllRead }) })) : (_jsx(Button, { size: "sm", variant: "outline", colorScheme: "gray", leftIcon: _jsx(CheckCheck, { size: 16, "aria-hidden": "true" }), label: labels.markAllRead, isDisabled: !unreadHere || isOffline, onClick: markAllRead }))), !isPhone && (_jsx(ToolTip, { label: labels.keyboardShortcuts, children: _jsx(IconButton, { size: "sm", variant: "ghost", colorScheme: "gray", "aria-label": labels.keyboardShortcuts, icon: _jsx(Icon, { as: Keyboard, boxSize: 4, "aria-hidden": "true" }), onClick: () => setKeysOpen(true) }) })), onOpenSettings && (_jsx(ToolTip, { label: labels.settings, children: _jsx(IconButton, { size: isPhone ? "md" : "sm", variant: "ghost", colorScheme: "gray", "aria-label": labels.settings, icon: _jsx(Icon, { as: Settings, boxSize: isPhone ? 5 : 4, "aria-hidden": "true" }), onClick: onOpenSettings }) }))] }));
376
+ const loadMore = hasMore && features.loadMore ? (_jsx(Flex, { justify: "center", p: 3, children: _jsx(Button, { size: isPhone ? "md" : "sm", variant: "outline", colorScheme: "gray", label: labels.loadMore, isLoading: isLoadingMore, onClick: actions.loadMore }) })) : null;
377
+ let listBody;
378
+ if (status === "loading")
379
+ listBody = _jsx(InboxSkeleton, { isPhone: isPhone, label: labels.loading });
380
+ else if (status === "error" || status === "noAccess") {
381
+ listBody = _jsx(InboxLoadFailure, { kind: status, labels: labels, appName: appName, onRetry: actions.retryLoad });
382
+ }
383
+ else if (visibleItems.length === 0) {
384
+ listBody =
385
+ filterCount > 0 && base.some(isShown) ? (_jsxs(_Fragment, { children: [_jsx(FilteredEmptyState, { scale: "card", filterCount: filterCount, onClearFilters: clearFilters }), loadMore] })) : (_jsxs(_Fragment, { children: [_jsx(InboxEmpty, { tab: activeTab, labels: labels }), loadMore] }));
386
+ }
387
+ else {
388
+ listBody = (_jsxs(_Fragment, { children: [_jsx(InboxList, { groups: groups, isPhone: isPhone, selectedId: selectedId, checked: checked, openThreads: openThreads, idPrefix: baseId, onOpenRow: onOpenRow, onCheckedChange: canSelect ? onCheckedChange : undefined, onThreadToggle: onThreadToggle, linkRefFor: linkRefFor }), loadMore] }));
389
+ }
390
+ const detail = selectedItem ? (_jsx(NotificationDetail, { item: selectedItem, variant: isPhone ? "phone" : "desktop", onBack: goBack, titleRef: titleRef }, selectedItem.entry.id)) : (_jsx(EmptyState, { size: "md", icon: Inbox, colorScheme: "gray", title: labels.nothingSelected, description: labels.nothingSelectedHint, minH: "100%" }));
391
+ const scroll = scrollbarStyles(colors, "hover");
392
+ return (_jsxs(Flex, { ref: rootRef, direction: "column", h: "100%", minH: isPhone ? undefined : "32rem", "data-testid": testId, children: [_jsxs(Box, { display: isDetailOpen ? "none" : "block", flexShrink: 0, px: isPhone ? 4 : 0, pt: isPhone ? 4 : 0, pb: isPhone ? 3 : 0, children: [_jsx(PageHeader, { title: labels.title, subtitle: isReady ? labels.subtitle(counts) : appName, actions: headerActions }), isOffline && (_jsxs(Flex, { align: "flex-start", gap: 2, mt: 3, px: 4, py: 2, fontSize: "xs", bg: colors.backgroundColor.subtle, color: colors.text[800], borderRadius: isPhone ? undefined : RADIUS_ROLE.control, children: [_jsx(Icon, { as: WifiOff, boxSize: 3.5, mt: 0.5, "aria-hidden": "true" }), _jsx(Text, { children: labels.offlineMessage })] }))] }), _jsxs(Flex, { direction: "column", flex: 1, minH: 0, mt: isPhone ? 0 : 4, bg: plane, border: isPhone ? undefined : outline, borderRadius: isPhone ? undefined : RADIUS_ROLE.card, overflow: "hidden", children: [_jsxs(Flex, { display: isDetailOpen ? "none" : "flex", flexShrink: 0, direction: isPhone ? "column" : "row", wrap: "wrap", align: isPhone ? "stretch" : "flex-end", justify: "space-between", columnGap: 4, px: isPhone ? 2 : 3, pt: isPhone ? 0 : 2, pb: 0, borderBottom: rule, children: [_jsx(Tabs, { id: baseId, "aria-label": labels.listsLabel, size: isPhone ? "md" : "sm", items: tabItems, value: activeTab, onChange: (value) => setTab(TAB_BY_ID[value]) }), toolbar] }), _jsxs(Flex, { flex: 1, minH: 0, display: isDetailOpen ? "none" : "flex", children: [_jsx(Box, { role: "tabpanel", id: `${baseId}-panel`, "aria-labelledby": `${baseId}-tab-${activeTab}`, "aria-busy": status === "loading" ? true : undefined, flex: isPhone ? 1 : undefined, w: isPhone ? undefined : "clamp(17.5rem, 32%, 22.5rem)", flexShrink: 0, minW: 0, overflowY: "auto", borderRight: isPhone ? undefined : rule, pt: 1, sx: scroll, children: listBody }), !isPhone && (_jsx(Box, { flex: 1, minW: 0, overflowY: "auto", sx: scroll, children: isReady ? detail : null }))] }), isDetailOpen && (_jsx(Box, { flex: 1, overflowY: "auto", sx: scroll, children: detail }))] }), _jsx(ShortcutsDialog, { isOpen: keysOpen, onClose: () => setKeysOpen(false), labels: labels }), isPhone && (_jsxs(Drawer, { isOpen: filtersOpen, onClose: () => setFiltersOpen(false), placement: "bottom", size: "md", children: [_jsx(DrawerHeader, { children: labels.filters }), _jsx(DrawerBody, { children: _jsxs(Flex, { direction: "column", gap: 4, children: [typeSelect, unreadSwitch] }) })] }))] }));
393
+ };
394
+ /** Full-page inbox: Needs you, Activity and Archived tabs, search, a type filter, bulk actions and a reading pane. */
395
+ const NotificationInbox = (props) => (_jsx(NotificationScope, { source: props, children: _jsx(NotificationInboxBody, { ...props }) }));
396
+ export default NotificationInbox;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment } from "react";
3
+ import { Flex, Text } from "../../Primitives/Primitives.js";
4
+ import Kbd from "../../Kbd/Kbd.js";
5
+ import Modal, { ModalBody } from "../../Modal/Modal.js";
6
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
7
+ export const ShortcutsDialog = ({ isOpen, onClose, labels }) => {
8
+ const { colors } = useCustomTheme();
9
+ const rows = [
10
+ [["j", "↓"], labels.shortcuts.next],
11
+ [["k", "↑"], labels.shortcuts.previous],
12
+ [[labels.keyEnter], labels.shortcuts.show],
13
+ [["o"], labels.shortcuts.openInApp],
14
+ [["u"], labels.shortcuts.toggleRead],
15
+ [["e"], labels.shortcuts.archive],
16
+ [["x"], labels.shortcuts.select],
17
+ [["z"], labels.shortcuts.undo],
18
+ [["/"], labels.shortcuts.search],
19
+ [[labels.keyEsc], labels.shortcuts.clear],
20
+ [["?"], labels.shortcuts.help],
21
+ ];
22
+ return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, size: "sm", isCentered: true, title: labels.keyboardShortcuts, subtitle: labels.shortcutsHint, children: _jsx(ModalBody, { children: _jsx(Flex, { as: "dl", direction: "column", gap: 2, m: 0, children: rows.map(([keys, name]) => (_jsxs(Flex, { justify: "space-between", align: "center", gap: 4, children: [_jsx(Text, { as: "dt", fontSize: "sm", color: colors.text[800], children: name }), _jsx(Flex, { as: "dd", gap: 1, align: "center", m: 0, children: keys.map((key, index) => (_jsxs(Fragment, { children: [index > 0 && _jsx(Text, { color: colors.textMuted, children: "/" }), _jsx(Kbd, { children: key })] }, key))) })] }, name))) }) }) }));
23
+ };
@@ -0,0 +1,52 @@
1
+ import { mergeDefined } from "../core/labels.js";
2
+ export const withInboxLabels = (shared, format, overrides = {}) => mergeDefined({
3
+ ...shared,
4
+ title: "Inbox",
5
+ subtitle: ({ needsYou, unread }) => `${format.number(needsYou)} need you · ${format.number(unread)} unread`,
6
+ listsLabel: "Inbox lists",
7
+ needsYouTab: "Needs you",
8
+ activityTab: "Activity",
9
+ archivedTab: "Archived",
10
+ search: "Search notifications",
11
+ typeFilter: "Type",
12
+ allTypes: "All types",
13
+ unreadOnly: "Unread only",
14
+ filters: "Filters",
15
+ markAllRead: "Mark all read",
16
+ settings: "Notification settings",
17
+ keyboardShortcuts: "Keyboard shortcuts",
18
+ shortcutsHint: "They work when focus is in the inbox, not while typing.",
19
+ shortcuts: {
20
+ next: "Next notification",
21
+ previous: "Previous notification",
22
+ show: "Show the details",
23
+ openInApp: "Open in the app",
24
+ toggleRead: "Mark as read or unread",
25
+ archive: "Archive",
26
+ select: "Select",
27
+ undo: "Undo the last action",
28
+ search: "Search",
29
+ clear: "Clear selection",
30
+ help: "Show these shortcuts",
31
+ },
32
+ selected: (count) => `${format.number(count)} selected`,
33
+ selectAll: "Select all",
34
+ clearSelection: "Clear selection",
35
+ bulkDecide: (actionLabel, count) => `${actionLabel} ${format.number(count)}`,
36
+ nothingSelected: "Nothing selected",
37
+ nothingSelectedHint: "Pick a notification to read it here.",
38
+ nothingToUndo: "Nothing to undo",
39
+ results: (count) => (count === 1 ? "1 result" : `${format.number(count)} results`),
40
+ caughtUpTitle: "You're all caught up",
41
+ caughtUpBody: "Nothing is waiting for your decision.",
42
+ activityEmptyTitle: "No activity yet",
43
+ activityEmptyBody: "Updates about your work and requests show up here.",
44
+ archivedEmptyTitle: "Nothing archived",
45
+ archivedEmptyBody: "Archived notifications stay here until you move them back.",
46
+ loadErrorTitle: "Couldn't load notifications",
47
+ loadErrorBody: "Check your connection, then try again. Nothing you did was lost.",
48
+ noAccessTitle: "Notifications aren't turned on for you",
49
+ noAccessBody: (appName) => `Your ${appName !== null && appName !== void 0 ? appName : "app"} admin hasn't given your role access to notifications in this workspace.`,
50
+ loading: "Loading notifications",
51
+ loadMore: "Load more",
52
+ }, overrides);
@@ -0,0 +1,37 @@
1
+ import { matchesNotificationQuery, threadEntries } from "../core/grouping.js";
2
+ export const ALL_TYPES = "all";
3
+ export const filterItems = (items, filters) => items.filter(({ entry }) => (filters.type === ALL_TYPES || entry.category === filters.type) &&
4
+ (!filters.unreadOnly || entry.isRead === false || entry.id === filters.keepId) &&
5
+ matchesNotificationQuery(entry, filters.query));
6
+ export const isShown = (item) => !item.state.isHidden;
7
+ export const visibleRowIds = (groups, openThreads) => groups.flatMap((group) => group.threads.flatMap((thread) => {
8
+ const rows = openThreads.has(thread.key) ? threadEntries(thread) : [thread.lead];
9
+ return rows.filter(isShown).map((item) => item.entry.id);
10
+ }));
11
+ export const threadsByLead = (groups) => new Map(groups.flatMap((group) => group.threads.map((thread) => [thread.key, thread])));
12
+ export const bulkDecision = (items) => {
13
+ var _a, _b;
14
+ const quick = (action) => action.href === undefined && action.reason === undefined;
15
+ const candidate = (_b = (_a = items[0]) === null || _a === void 0 ? void 0 : _a.entry.actions) === null || _b === void 0 ? void 0 : _b.find((action) => quick(action) && action.intent === "positive");
16
+ if (!candidate)
17
+ return undefined;
18
+ const everyone = items.every(({ entry, state }) => {
19
+ var _a;
20
+ return state.zone === "needsYou" &&
21
+ state.status === "open" &&
22
+ ((_a = entry.actions) === null || _a === void 0 ? void 0 : _a.some((action) => action.id === candidate.id && quick(action)));
23
+ });
24
+ return everyone ? candidate : undefined;
25
+ };
26
+ export const tabZoneMatches = (tab, item) => (item.state.zone === "archived") === (tab === "archived");
27
+ const TYPING_ROLES = '[role="combobox"],[role="listbox"],[role="menu"],[role="textbox"],[role="searchbox"]';
28
+ const NON_TEXT_INPUTS = ["checkbox", "radio", "button", "submit"];
29
+ export const isTypingTarget = (target) => {
30
+ if (!(target instanceof HTMLElement))
31
+ return false;
32
+ if (target.isContentEditable || target.closest(TYPING_ROLES))
33
+ return true;
34
+ if (target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement)
35
+ return true;
36
+ return target instanceof HTMLInputElement && !NON_TEXT_INPUTS.includes(target.type);
37
+ };
@@ -0,0 +1,11 @@
1
+ import { useCallback, useState } from "react";
2
+ export const useControllable = (controlled, initial, onChange) => {
3
+ const [inner, setInner] = useState(initial);
4
+ const isControlled = controlled !== undefined;
5
+ const set = useCallback((next) => {
6
+ if (!isControlled)
7
+ setInner(next);
8
+ onChange === null || onChange === void 0 ? void 0 : onChange(next);
9
+ }, [isControlled, onChange]);
10
+ return [isControlled ? controlled : inner, set, isControlled];
11
+ };
@@ -0,0 +1,8 @@
1
+ export { NotificationCenterProvider, useNotificationCenter } from "./core/NotificationCenterProvider.js";
2
+ export { isNotificationActionError, notificationActionError, toNotificationActionError } from "./core/actionError.js";
3
+ export { DEFAULT_NOTIFICATION_LOCALE, defaultNotificationLabels } from "./core/labels.js";
4
+ export { default as NotificationRow } from "./row/NotificationRow.js";
5
+ export { default as NotificationDetail } from "./row/NotificationDetail.js";
6
+ export { default as NotificationCenter } from "./center/NotificationCenter.js";
7
+ export { default as NotificationInbox } from "./inbox/NotificationInbox.js";
8
+ export { default as NotificationPreferences } from "./preferences/NotificationPreferences.js";
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useMemo } from "react";
3
+ import { AlertTriangle, Check, Lock, RotateCw, WifiOff } from "lucide-react";
4
+ import { Alert, AlertDescription, Box, Flex, Text } from "../../Primitives/Primitives.js";
5
+ import Card from "../../Card/Card.js";
6
+ import PageHeader from "../../PageHeader/PageHeader.js";
7
+ import SettingRow from "../../SettingRow/SettingRow.js";
8
+ import SettingsSection from "../../SettingRow/SettingsSection.js";
9
+ import SegmentedControl from "../../SegmentedControl/SegmentedControl.js";
10
+ import Skeletons from "../../Skeletons/Skeletons.js";
11
+ import Spinner from "../../Spinner/Spinner.js";
12
+ import VisuallyHidden from "../../VisuallyHidden/VisuallyHidden.js";
13
+ import Button from "../../Button/Button.js";
14
+ import EmptyState from "../../EmptyState/EmptyState.js";
15
+ import { useToaster } from "../../Toaster/Toaster.js";
16
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
17
+ import PreferenceGroups from "./PreferenceGroups.js";
18
+ import { resolvePreferencesLabels } from "./preferencesLabels.js";
19
+ import { useAutosave } from "./useAutosave.js";
20
+ const EMAIL_KEY = "email-summary";
21
+ const SaveStatus = ({ isSaving, hasSaved, labels }) => {
22
+ const { colors } = useCustomTheme();
23
+ return (_jsxs(Flex, { role: "status", align: "center", gap: 1.5, fontSize: "xs", color: hasSaved && !isSaving ? colors.semanticText.success : colors.textMuted, fontWeight: hasSaved && !isSaving ? 500 : 400, children: [isSaving && _jsx(Spinner, { size: "xs", label: "" }), !isSaving && hasSaved && _jsx(Check, { size: 14, "aria-hidden": "true" }), _jsx("span", { children: isSaving ? labels.saving : hasSaved ? labels.saved : labels.autosave })] }));
24
+ };
25
+ const Banner = ({ icon, children }) => (_jsxs(Alert, { status: "info", borderRadius: "lg", alignItems: "flex-start", children: [_jsx(Box, { mt: 0.5, mr: 2, flexShrink: 0, children: icon }), _jsx(AlertDescription, { fontSize: "sm", children: children })] }));
26
+ const SkeletonCard = ({ rows }) => (_jsx(Card, { header: _jsx(Skeletons, { type: "skeleton", height: "1rem", width: "8rem" }), children: _jsx(Flex, { direction: "column", gap: 5, children: Array.from({ length: rows }, (_, index) => (_jsxs(Flex, { justify: "space-between", align: "center", gap: 4, children: [_jsxs(Flex, { direction: "column", gap: 2, children: [_jsx(Skeletons, { type: "skeleton", height: "0.875rem", width: "9rem" }), _jsx(Skeletons, { type: "skeleton", height: "0.75rem", width: "14rem" })] }), _jsx(Skeletons, { type: "skeleton", height: "1rem", width: "6rem" })] }, index))) }) }));
27
+ /** Notification settings: a type-by-channel grid with locked cells and an email summary choice; every change autosaves and rolls back on failure. */
28
+ const NotificationPreferences = ({ channels, groups, values, onChange, emailSummary, onEmailSummaryChange, status = "ready", onRetry, isOffline = false, note, appName, variant = "desktop", labels: labelOverrides, "data-testid": testId, }) => {
29
+ var _a;
30
+ const { colors } = useCustomTheme();
31
+ const { showToast } = useToaster();
32
+ const labels = useMemo(() => resolvePreferencesLabels(labelOverrides), [labelOverrides]);
33
+ const saved = useMemo(() => ({ values, emailSummary }), [values, emailSummary]);
34
+ const notifyFailed = useCallback((what) => showToast({ status: "error", title: labels.saveFailed(what) }), [showToast, labels]);
35
+ const { draft, pendingKeys, hasSaved, commit } = useAutosave(saved, notifyFailed);
36
+ const isFrozen = status !== "ready" || isOffline;
37
+ const isReadOnly = status === "readOnly";
38
+ const pagePadding = variant === "phone" ? 4 : 6;
39
+ const cellKey = (rowId, channelId) => `cell:${rowId}:${channelId}`;
40
+ const valueOf = (rowId, channelId) => {
41
+ const pending = draft[cellKey(rowId, channelId)];
42
+ if (typeof pending === "boolean")
43
+ return pending;
44
+ const row = values[rowId];
45
+ return Boolean(row && row[channelId]);
46
+ };
47
+ const isPending = (rowId, channelId) => pendingKeys.includes(cellKey(rowId, channelId));
48
+ const toggle = (row, channel, next) => commit(cellKey(row.id, channel.id), next, () => onChange(row.id, channel.id, next), `${row.label} by ${channel.label.toLowerCase()}`);
49
+ const shownEmail = (_a = draft[EMAIL_KEY]) !== null && _a !== void 0 ? _a : emailSummary;
50
+ const changeEmail = (next) => {
51
+ if (!onEmailSummaryChange || next === shownEmail)
52
+ return;
53
+ commit(EMAIL_KEY, next, () => onEmailSummaryChange(next), labels.email);
54
+ };
55
+ const header = (_jsx(PageHeader, { title: labels.title, subtitle: labels.subtitle(appName), actions: status === "ready" && !isOffline ? _jsx(SaveStatus, { isSaving: pendingKeys.length > 0, hasSaved: hasSaved, labels: labels }) : undefined, actionsLayout: "inline" }));
56
+ if (status === "loading") {
57
+ return (_jsxs(Flex, { direction: "column", gap: 5, px: pagePadding, py: 5, maxW: "57.5rem", "aria-busy": "true", "data-testid": testId, children: [header, _jsx(VisuallyHidden, { role: "status", children: labels.loading }), _jsx(SkeletonCard, { rows: 1 }), _jsx(SkeletonCard, { rows: 4 })] }));
58
+ }
59
+ if (status === "error") {
60
+ return (_jsxs(Flex, { direction: "column", gap: 5, px: pagePadding, py: 5, maxW: "57.5rem", "data-testid": testId, children: [header, _jsx(Box, { role: "alert", w: "100%", children: _jsx(EmptyState, { colorScheme: "error", icon: AlertTriangle, titleAs: "h2", title: labels.errorTitle, description: labels.errorDescription, size: "md", action: _jsx(Button, { size: "sm", variant: "outline", colorScheme: "gray", label: labels.retry, leftIcon: _jsx(RotateCw, { size: 16 }), onClick: onRetry }) }) })] }));
61
+ }
62
+ return (_jsxs(Flex, { direction: "column", gap: 5, px: pagePadding, py: 5, maxW: "57.5rem", "data-testid": testId, children: [header, isReadOnly && _jsx(Banner, { icon: _jsx(Lock, { size: 16, "aria-hidden": "true" }), children: labels.readOnlyBanner }), isOffline && _jsx(Banner, { icon: _jsx(WifiOff, { size: 16, "aria-hidden": "true" }), children: labels.offlineBanner }), shownEmail && (_jsx(SettingsSection, { title: labels.email, children: _jsx(SettingRow, { title: labels.emailTiming, description: labels.emailHint(appName), control: _jsx(SegmentedControl, { "aria-label": labels.emailTiming, options: [
63
+ { value: "asItHappens", label: labels.asItHappens, isDisabled: isFrozen },
64
+ { value: "daily", label: labels.daily, isDisabled: isFrozen },
65
+ ], value: shownEmail, onChange: changeEmail }) }) })), _jsx(PreferenceGroups, { channels: channels, groups: groups, labels: labels, variant: variant, isFrozen: isFrozen, valueOf: valueOf, isPending: isPending, onToggle: toggle, testId: testId }), note && (_jsxs(Flex, { align: "flex-start", gap: 1.5, maxW: "72ch", fontSize: "xs", color: colors.textMuted, children: [_jsx(Box, { mt: 0.5, flexShrink: 0, children: _jsx(Lock, { size: 12, "aria-hidden": "true" }) }), _jsx(Text, { children: note })] }))] }));
66
+ };
67
+ export default NotificationPreferences;