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.
- package/CHANGELOG.md +64 -1
- package/COMPONENT-GOTCHAS.md +37 -13
- package/DESIGN-GUIDE.md +4 -1
- package/dist/Components/NotificationBell/NotificationBellProps.d.ts +3 -3
- package/dist/Components/NotificationBell/NotificationItem.d.ts +1 -1
- package/dist/Components/NotificationBell/NotificationItem.js +1 -1
- package/dist/Components/NotificationBell/NotificationList.d.ts +1 -1
- package/dist/Components/NotificationBell/NotificationList.js +1 -1
- package/dist/Components/NotificationBell/NotificationPanel.d.ts +1 -1
- package/dist/Components/NotificationBell/NotificationPanel.js +1 -1
- package/dist/Components/NotificationCenter/center/CenterPanel.d.ts +15 -0
- package/dist/Components/NotificationCenter/center/CenterPanel.js +87 -0
- package/dist/Components/NotificationCenter/center/NotificationCenter.d.ts +4 -0
- package/dist/Components/NotificationCenter/center/NotificationCenter.js +130 -0
- package/dist/Components/NotificationCenter/center/NotificationCenterProps.d.ts +71 -0
- package/dist/Components/NotificationCenter/center/NotificationCenterProps.js +2 -0
- package/dist/Components/NotificationCenter/center/PanelRows.d.ts +10 -0
- package/dist/Components/NotificationCenter/center/PanelRows.js +35 -0
- package/dist/Components/NotificationCenter/center/PanelStates.d.ts +17 -0
- package/dist/Components/NotificationCenter/center/PanelStates.js +21 -0
- package/dist/Components/NotificationCenter/center/centerLabels.d.ts +4 -0
- package/dist/Components/NotificationCenter/center/centerLabels.js +38 -0
- package/dist/Components/NotificationCenter/center/useArrivals.d.ts +14 -0
- package/dist/Components/NotificationCenter/center/useArrivals.js +71 -0
- package/dist/Components/NotificationCenter/center/useIsSheet.d.ts +2 -0
- package/dist/Components/NotificationCenter/center/useIsSheet.js +20 -0
- package/dist/Components/NotificationCenter/core/NotificationCenterProvider.d.ts +17 -0
- package/dist/Components/NotificationCenter/core/NotificationCenterProvider.js +46 -0
- package/dist/Components/NotificationCenter/core/NotificationEngineProps.d.ts +182 -0
- package/dist/Components/NotificationCenter/core/NotificationEngineProps.js +2 -0
- package/dist/Components/NotificationCenter/core/NotificationSourceProps.d.ts +284 -0
- package/dist/Components/NotificationCenter/core/NotificationSourceProps.js +2 -0
- package/dist/Components/NotificationCenter/core/actionError.d.ts +4 -0
- package/dist/Components/NotificationCenter/core/actionError.js +45 -0
- package/dist/Components/NotificationCenter/core/format.d.ts +4 -0
- package/dist/Components/NotificationCenter/core/format.js +91 -0
- package/dist/Components/NotificationCenter/core/grouping.d.ts +14 -0
- package/dist/Components/NotificationCenter/core/grouping.js +102 -0
- package/dist/Components/NotificationCenter/core/labels.d.ts +6 -0
- package/dist/Components/NotificationCenter/core/labels.js +94 -0
- package/dist/Components/NotificationCenter/core/overlay.d.ts +109 -0
- package/dist/Components/NotificationCenter/core/overlay.js +281 -0
- package/dist/Components/NotificationCenter/core/useNotificationEngine.d.ts +13 -0
- package/dist/Components/NotificationCenter/core/useNotificationEngine.js +415 -0
- package/dist/Components/NotificationCenter/inbox/InboxList.d.ts +17 -0
- package/dist/Components/NotificationCenter/inbox/InboxList.js +25 -0
- package/dist/Components/NotificationCenter/inbox/InboxStates.d.ts +17 -0
- package/dist/Components/NotificationCenter/inbox/InboxStates.js +27 -0
- package/dist/Components/NotificationCenter/inbox/NotificationInbox.d.ts +4 -0
- package/dist/Components/NotificationCenter/inbox/NotificationInbox.js +434 -0
- package/dist/Components/NotificationCenter/inbox/NotificationInboxProps.d.ts +106 -0
- package/dist/Components/NotificationCenter/inbox/NotificationInboxProps.js +2 -0
- package/dist/Components/NotificationCenter/inbox/ShortcutsDialog.d.ts +8 -0
- package/dist/Components/NotificationCenter/inbox/ShortcutsDialog.js +63 -0
- package/dist/Components/NotificationCenter/inbox/inboxLabels.d.ts +4 -0
- package/dist/Components/NotificationCenter/inbox/inboxLabels.js +56 -0
- package/dist/Components/NotificationCenter/inbox/inboxModel.d.ts +17 -0
- package/dist/Components/NotificationCenter/inbox/inboxModel.js +47 -0
- package/dist/Components/NotificationCenter/inbox/useControllable.d.ts +1 -0
- package/dist/Components/NotificationCenter/inbox/useControllable.js +15 -0
- package/dist/Components/NotificationCenter/index.d.ts +8 -0
- package/dist/Components/NotificationCenter/index.js +26 -0
- package/dist/Components/NotificationCenter/preferences/NotificationPreferences.d.ts +4 -0
- package/dist/Components/NotificationCenter/preferences/NotificationPreferences.js +72 -0
- package/dist/Components/NotificationCenter/preferences/NotificationPreferencesProps.d.ts +111 -0
- package/dist/Components/NotificationCenter/preferences/NotificationPreferencesProps.js +2 -0
- package/dist/Components/NotificationCenter/preferences/PreferenceCells.d.ts +16 -0
- package/dist/Components/NotificationCenter/preferences/PreferenceCells.js +35 -0
- package/dist/Components/NotificationCenter/preferences/PreferenceGroups.d.ts +15 -0
- package/dist/Components/NotificationCenter/preferences/PreferenceGroups.js +26 -0
- package/dist/Components/NotificationCenter/preferences/preferencesLabels.d.ts +2 -0
- package/dist/Components/NotificationCenter/preferences/preferencesLabels.js +28 -0
- package/dist/Components/NotificationCenter/preferences/useAutosave.d.ts +8 -0
- package/dist/Components/NotificationCenter/preferences/useAutosave.js +41 -0
- package/dist/Components/NotificationCenter/row/NotificationDetail.d.ts +4 -0
- package/dist/Components/NotificationCenter/row/NotificationDetail.js +65 -0
- package/dist/Components/NotificationCenter/row/NotificationDetailProps.d.ts +15 -0
- package/dist/Components/NotificationCenter/row/NotificationDetailProps.js +2 -0
- package/dist/Components/NotificationCenter/row/NotificationRow.d.ts +3 -0
- package/dist/Components/NotificationCenter/row/NotificationRow.js +124 -0
- package/dist/Components/NotificationCenter/row/NotificationRowProps.d.ts +33 -0
- package/dist/Components/NotificationCenter/row/NotificationRowProps.js +2 -0
- package/dist/Components/NotificationCenter/row/ReasonForm.d.ts +14 -0
- package/dist/Components/NotificationCenter/row/ReasonForm.js +59 -0
- package/dist/Components/NotificationCenter/row/RowActions.d.ts +16 -0
- package/dist/Components/NotificationCenter/row/RowActions.js +36 -0
- package/dist/Components/NotificationCenter/row/RowQuickActions.d.ts +17 -0
- package/dist/Components/NotificationCenter/row/RowQuickActions.js +57 -0
- package/dist/Components/NotificationCenter/row/RowStatus.d.ts +10 -0
- package/dist/Components/NotificationCenter/row/RowStatus.js +42 -0
- package/dist/Components/NotificationCenter/row/category.d.ts +8 -0
- package/dist/Components/NotificationCenter/row/category.js +13 -0
- package/dist/Components/NotificationCenter/row/rowMenu.d.ts +14 -0
- package/dist/Components/NotificationCenter/row/rowMenu.js +37 -0
- package/dist/Components/NotificationCenter/row/rowParts.d.ts +40 -0
- package/dist/Components/NotificationCenter/row/rowParts.js +72 -0
- package/dist/Components/NotificationCenter/row/rowStyles.d.ts +11 -0
- package/dist/Components/NotificationCenter/row/rowStyles.js +25 -0
- package/dist/Components/SegmentedControl/segmentedInk.d.ts +4 -0
- package/dist/Components/SegmentedControl/segmentedInk.js +16 -2
- package/dist/Components/SideBar/SecondaryBar.js +2 -2
- package/dist/Components/SideBar/SideBar.js +18 -8
- package/dist/Components/SideBar/SideBarProps.d.ts +4 -2
- package/dist/Components/SideBar/components/MenuItemBox.d.ts +0 -2
- package/dist/Components/SideBar/components/MenuItemBox.js +2 -3
- package/dist/Components/SideBar/components/MenuItems.js +2 -2
- package/dist/Components/SideBar/components/PinnedSection.js +3 -4
- package/dist/Components/SideBar/sidebarScale.d.ts +0 -2
- package/dist/Components/SideBar/sidebarScale.js +0 -2
- package/dist/Components/Switch/Switch.d.ts +1 -1
- package/dist/Components/Switch/Switch.js +2 -2
- package/dist/Components/Switch/SwitchProps.d.ts +3 -0
- package/dist/Components/Toggle/TableToggle.js +4 -5
- package/dist/Components/Toggle/tableToggleInk.d.ts +7 -0
- package/dist/Components/Toggle/tableToggleInk.js +28 -0
- package/dist/esm/Components/NotificationBell/NotificationItem.js +1 -1
- package/dist/esm/Components/NotificationBell/NotificationList.js +1 -1
- package/dist/esm/Components/NotificationBell/NotificationPanel.js +1 -1
- package/dist/esm/Components/NotificationCenter/center/CenterPanel.js +81 -0
- package/dist/esm/Components/NotificationCenter/center/NotificationCenter.js +92 -0
- package/dist/esm/Components/NotificationCenter/center/NotificationCenterProps.js +1 -0
- package/dist/esm/Components/NotificationCenter/center/PanelRows.js +28 -0
- package/dist/esm/Components/NotificationCenter/center/PanelStates.js +11 -0
- package/dist/esm/Components/NotificationCenter/center/centerLabels.js +34 -0
- package/dist/esm/Components/NotificationCenter/center/useArrivals.js +67 -0
- package/dist/esm/Components/NotificationCenter/center/useIsSheet.js +16 -0
- package/dist/esm/Components/NotificationCenter/core/NotificationCenterProvider.js +35 -0
- package/dist/esm/Components/NotificationCenter/core/NotificationEngineProps.js +1 -0
- package/dist/esm/Components/NotificationCenter/core/NotificationSourceProps.js +1 -0
- package/dist/esm/Components/NotificationCenter/core/actionError.js +39 -0
- package/dist/esm/Components/NotificationCenter/core/format.js +87 -0
- package/dist/esm/Components/NotificationCenter/core/grouping.js +88 -0
- package/dist/esm/Components/NotificationCenter/core/labels.js +88 -0
- package/dist/esm/Components/NotificationCenter/core/overlay.js +274 -0
- package/dist/esm/Components/NotificationCenter/core/useNotificationEngine.js +408 -0
- package/dist/esm/Components/NotificationCenter/inbox/InboxList.js +18 -0
- package/dist/esm/Components/NotificationCenter/inbox/InboxStates.js +18 -0
- package/dist/esm/Components/NotificationCenter/inbox/NotificationInbox.js +396 -0
- package/dist/esm/Components/NotificationCenter/inbox/NotificationInboxProps.js +1 -0
- package/dist/esm/Components/NotificationCenter/inbox/ShortcutsDialog.js +23 -0
- package/dist/esm/Components/NotificationCenter/inbox/inboxLabels.js +52 -0
- package/dist/esm/Components/NotificationCenter/inbox/inboxModel.js +37 -0
- package/dist/esm/Components/NotificationCenter/inbox/useControllable.js +11 -0
- package/dist/esm/Components/NotificationCenter/index.js +8 -0
- package/dist/esm/Components/NotificationCenter/preferences/NotificationPreferences.js +67 -0
- package/dist/esm/Components/NotificationCenter/preferences/NotificationPreferencesProps.js +1 -0
- package/dist/esm/Components/NotificationCenter/preferences/PreferenceCells.js +27 -0
- package/dist/esm/Components/NotificationCenter/preferences/PreferenceGroups.js +21 -0
- package/dist/esm/Components/NotificationCenter/preferences/preferencesLabels.js +24 -0
- package/dist/esm/Components/NotificationCenter/preferences/useAutosave.js +37 -0
- package/dist/esm/Components/NotificationCenter/row/NotificationDetail.js +60 -0
- package/dist/esm/Components/NotificationCenter/row/NotificationDetailProps.js +1 -0
- package/dist/esm/Components/NotificationCenter/row/NotificationRow.js +119 -0
- package/dist/esm/Components/NotificationCenter/row/NotificationRowProps.js +1 -0
- package/dist/esm/Components/NotificationCenter/row/ReasonForm.js +52 -0
- package/dist/esm/Components/NotificationCenter/row/RowActions.js +28 -0
- package/dist/esm/Components/NotificationCenter/row/RowQuickActions.js +49 -0
- package/dist/esm/Components/NotificationCenter/row/RowStatus.js +35 -0
- package/dist/esm/Components/NotificationCenter/row/category.js +9 -0
- package/dist/esm/Components/NotificationCenter/row/rowMenu.js +30 -0
- package/dist/esm/Components/NotificationCenter/row/rowParts.js +59 -0
- package/dist/esm/Components/NotificationCenter/row/rowStyles.js +19 -0
- package/dist/esm/Components/SegmentedControl/segmentedInk.js +15 -2
- package/dist/esm/Components/SideBar/SecondaryBar.js +3 -3
- package/dist/esm/Components/SideBar/SideBar.js +18 -8
- package/dist/esm/Components/SideBar/components/MenuItemBox.js +2 -3
- package/dist/esm/Components/SideBar/components/MenuItems.js +2 -2
- package/dist/esm/Components/SideBar/components/PinnedSection.js +3 -4
- package/dist/esm/Components/SideBar/sidebarScale.js +0 -2
- package/dist/esm/Components/Switch/Switch.js +2 -2
- package/dist/esm/Components/Toggle/TableToggle.js +4 -5
- package/dist/esm/Components/Toggle/tableToggleInk.js +24 -0
- package/dist/esm/index.js +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +16 -3
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|