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
@@ -7,13 +7,13 @@ const jsx_runtime_1 = require("react/jsx-runtime");
7
7
  const react_1 = require("@chakra-ui/react");
8
8
  const lucide_react_1 = require("lucide-react");
9
9
  const useCustomTheme_1 = require("../../Theme/useCustomTheme");
10
- const outlineRung_1 = require("../../Theme/tokens/builders/outlineRung");
11
10
  const filledSurface_1 = require("../../Theme/tokens/builders/filledSurface");
12
11
  const ToolTip_1 = __importDefault(require("../ToolTip/ToolTip"));
13
12
  const useSlidingPill_1 = require("../SegmentedControl/useSlidingPill");
14
13
  const controlSizes_1 = require("../../Theme/tokens/primitives/controlSizes");
15
14
  const borderWidths_1 = require("../../Theme/tokens/primitives/borderWidths");
16
15
  const motion_1 = require("../../Theme/tokens/primitives/motion");
16
+ const tableToggleInk_1 = require("./tableToggleInk");
17
17
  const MODES = ["grid", "list"];
18
18
  const TRACK_HEIGHT = controlSizes_1.BUTTON_SIZES.sm.height;
19
19
  const SEGMENT_HEIGHT = `calc(${TRACK_HEIGHT} - 0.25rem)`;
@@ -27,15 +27,14 @@ const TableToggle = ({ value, onChange, variant = "default" }) => {
27
27
  const isUnderlineSlide = variant === "underlineSlide";
28
28
  const hasIndicator = isBrandSlide || isUnderlineSlide;
29
29
  const brandFill = (0, filledSurface_1.resolveFilledSurface)(colors.primary, colors);
30
- const restInk = (0, outlineRung_1.liftToContrast)(colors.background[800], colors.background[600], outlineRung_1.AA_NON_TEXT);
31
- const hoverInk = (0, outlineRung_1.liftToContrast)(colors.background[900], colors.background[400], outlineRung_1.AA_NON_TEXT);
30
+ const { track, hover, restInk, hoverInk } = (0, tableToggleInk_1.tableToggleInk)(colors);
32
31
  const selectedIndex = MODES.indexOf(value);
33
32
  const { trackRef, itemRefs: refs, pill } = (0, useSlidingPill_1.useSlidingPill)(hasIndicator, selectedIndex, []);
34
33
  const styles = {
35
34
  container: {
36
35
  display: "flex",
37
36
  alignItems: "center",
38
- bg: isBrandSlide ? colors.primary.opacity[8] : isUnderlineSlide ? "transparent" : colors.background[600],
37
+ bg: isBrandSlide ? colors.primary.opacity[8] : isUnderlineSlide ? "transparent" : track,
39
38
  p: isUnderlineSlide ? "0" : "0.125rem",
40
39
  borderRadius: isUnderlineSlide ? "none" : "lg",
41
40
  borderBottom: isUnderlineSlide ? `${borderWidths_1.BORDER_WIDTHS.hairline} solid ${colors.boxborder[200]}` : undefined,
@@ -70,7 +69,7 @@ const TableToggle = ({ value, onChange, variant = "default" }) => {
70
69
  ? "transparent"
71
70
  : value === mode
72
71
  ? colors.backgroundColor.light
73
- : colors.background[400],
72
+ : hover,
74
73
  color: isBrandSlide
75
74
  ? value === mode
76
75
  ? brandFill.color
@@ -0,0 +1,7 @@
1
+ import type { PaletteProps } from "../../Theme/tokens/types";
2
+ export declare const tableToggleInk: (colors: PaletteProps) => {
3
+ track: string;
4
+ hover: string;
5
+ restInk: string;
6
+ hoverInk: string;
7
+ };
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tableToggleInk = void 0;
4
+ const outlineRung_1 = require("../../Theme/tokens/builders/outlineRung");
5
+ const segmentedInk_1 = require("../SegmentedControl/segmentedInk");
6
+ // NOTE: each rung keeps its offset from the track, so Zinc's [400] track matches the others' [600].
7
+ const RUNGS = {
8
+ 600: { hover: 400, rest: 800, hoverInk: 900 },
9
+ 500: { hover: 300, rest: 700, hoverInk: 800 },
10
+ 400: { hover: 200, rest: 600, hoverInk: 700 },
11
+ 300: { hover: 100, rest: 500, hoverInk: 600 },
12
+ 200: { hover: 50, rest: 400, hoverInk: 500 },
13
+ 100: { hover: 50, rest: 300, hoverInk: 400 },
14
+ 50: { hover: 50, rest: 200, hoverInk: 300 },
15
+ };
16
+ const tableToggleInk = (colors) => {
17
+ const step = (0, segmentedInk_1.segmentedTrackStep)(colors);
18
+ const rungs = RUNGS[step];
19
+ const track = colors.background[step];
20
+ const hover = colors.background[rungs.hover];
21
+ return {
22
+ track,
23
+ hover,
24
+ restInk: (0, outlineRung_1.liftToContrast)(colors.background[rungs.rest], track, outlineRung_1.AA_NON_TEXT),
25
+ hoverInk: (0, outlineRung_1.liftToContrast)(colors.background[rungs.hoverInk], hover, outlineRung_1.AA_NON_TEXT),
26
+ };
27
+ };
28
+ exports.tableToggleInk = tableToggleInk;
@@ -9,7 +9,7 @@ import { resolveSoftTone, toneInk, toneScale } from "../../Theme/tokens/builders
9
9
  import { focusVisibleStyles } from "../../Theme/chakra/focusRing.styles.js";
10
10
  import { formatRelativeTime } from "./notificationHelpers.js";
11
11
  const TILE_SIZE = "1.75rem";
12
- /** One notification row; used by NotificationList and reusable standalone under a `renderItem` override. */
12
+ /** @deprecated Use NotificationRow inside a NotificationCenterProvider. */
13
13
  const NotificationItem = ({ item, onClick, actions, actionsAlwaysVisible = false, preview, trailing, unreadIndicator = "bar", groupCount, onExpandGroup, }) => {
14
14
  var _a, _b, _c;
15
15
  const theme = useCustomTheme();
@@ -5,7 +5,7 @@ import EmptyState from "../EmptyState/EmptyState.js";
5
5
  import NotificationItem from "./NotificationItem.js";
6
6
  import { useCustomTheme } from "../../Theme/useCustomTheme.js";
7
7
  import { DEFAULT_DAY_LABELS, groupConsecutive, sectionizeByDay } from "./notificationHelpers.js";
8
- /** Day sections + consecutive groupKey runs over NotificationItem rows. Generic over the caller's row type. */
8
+ /** @deprecated Use NotificationInbox, or NotificationCenter for the bell panel. */
9
9
  function NotificationList({ items, toItem, groupBy = "day", collapseGroups = false, dayLabels = DEFAULT_DAY_LABELS, onItemClick, itemActions, renderItem, renderSectionHeader, unreadIndicator = "bar", actionsAlwaysVisible = false, "aria-label": ariaLabel, }) {
10
10
  const { colors } = useCustomTheme();
11
11
  const [expandedGroups, setExpandedGroups] = useState(new Set());
@@ -10,7 +10,7 @@ import { useCustomTheme } from "../../Theme/useCustomTheme.js";
10
10
  import { raisedRule } from "../../Theme/tokens/builders/surfaceRaised.js";
11
11
  import { formatCount, useAnchoredPanelHeight } from "./notificationHelpers.js";
12
12
  const DEFAULT_EMPTY = { title: "No notifications yet", description: "You're all caught up.", size: "sm" };
13
- /** The panel shell: header, zone/filter row, scroll body with loading/empty/error, footer. */
13
+ /** @deprecated Use NotificationCenter, which wraps NotificationBell with a two-tab panel. */
14
14
  function NotificationPanel({ items, toItem, zones, activeZone, onZoneChange, filters, activeFilter, onFilterChange, isLoading = false, error, onRetry, empty, header, headerAction, footer, variant = "popover", maxH, onLoadMore, hasMore = false, isLoadingMore = false, panelRef, groupBy, collapseGroups, dayLabels, onItemClick, itemActions, renderItem, renderSectionHeader, unreadIndicator, actionsAlwaysVisible, "aria-label": ariaLabel, }) {
15
15
  const { colors } = useCustomTheme();
16
16
  const internalRef = useRef(null);
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useMemo, useRef } from "react";
3
+ import { CheckCheck, ChevronRight, ChevronUp, Settings, WifiOff, X } from "lucide-react";
4
+ import { Box, Flex, Heading, Icon } from "../../Primitives/Primitives.js";
5
+ import Button from "../../Button/Button.js";
6
+ import IconButton from "../../IconButton/IconButton.js";
7
+ import ToolTip from "../../ToolTip/ToolTip.js";
8
+ import Tabs from "../../Tabs/Tabs.js";
9
+ import { tabId } from "../../Tabs/TabsContext.js";
10
+ import VisuallyHidden from "../../VisuallyHidden/VisuallyHidden.js";
11
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
12
+ import { raisedRule } from "../../../Theme/tokens/builders/surfaceRaised.js";
13
+ import { scrollbarStyles } from "../../../Theme/tokens/builders/scrollbar.js";
14
+ import { resolveFilledSurface } from "../../../Theme/tokens/builders/filledSurface.js";
15
+ import { resolveSoftTone, toneScale } from "../../../Theme/tokens/builders/tone.js";
16
+ import { insetFocusRingShadow } from "../../../Theme/chakra/focusRing.styles.js";
17
+ import { useNotificationCenter } from "../core/NotificationCenterProvider.js";
18
+ import { groupByCategory, groupByDay } from "../core/grouping.js";
19
+ import { PanelEmpty, PanelError, PanelNoAccess, PanelSkeleton } from "./PanelStates.js";
20
+ import { PanelRows } from "./PanelRows.js";
21
+ import { useArrivals } from "./useArrivals.js";
22
+ export const PANEL_ROW_LIMIT = 20;
23
+ const TAB_COUNT_CAP = 99;
24
+ const CenterPanel = forwardRef(function CenterPanel({ isSheet, tab, onTabChange, labels, titleId, onClose, onOpenInbox, onOpenSettings, "data-testid": testId }, ref) {
25
+ const { colors } = useCustomTheme();
26
+ const center = useNotificationCenter();
27
+ const { lists, counts, categories, features, format, now, status, isOffline, loadedAt, appName, actions } = center;
28
+ const bodyRef = useRef(null);
29
+ const all = tab === "needsYou" ? lists.needsYou : lists.activity;
30
+ const ids = useMemo(() => all.map((item) => item.entry.id), [all]);
31
+ const { heldIds, reveal, track } = useArrivals(ids, tab, bodyRef);
32
+ const visible = useMemo(() => {
33
+ const held = new Set(heldIds);
34
+ return all.slice(0, PANEL_ROW_LIMIT).filter((item) => !held.has(item.entry.id));
35
+ }, [all, heldIds]);
36
+ const groups = useMemo(() => tab === "needsYou"
37
+ ? groupByCategory(visible, categories, center.labels.otherCategory)
38
+ : groupByDay(visible, format, now), [tab, visible, categories, center.labels.otherCategory, format, now]);
39
+ const isReady = status === "ready";
40
+ const hasUnreadHere = all.some((item) => item.entry.isRead === false && !item.state.isHidden);
41
+ const canMarkAll = tab === "activity" ? counts.unread > 0 || hasUnreadHere : hasUnreadHere;
42
+ const cap = (count) => (count > TAB_COUNT_CAP ? labels.overflowCount(TAB_COUNT_CAP) : format.number(count));
43
+ const tabItems = [
44
+ { id: "needsYou", label: labels.needsYouTab, count: isReady && counts.needsYou > 0 ? cap(counts.needsYou) : undefined },
45
+ { id: "activity", label: labels.activityTab, count: isReady && counts.unread > 0 ? cap(counts.unread) : undefined },
46
+ ];
47
+ const openItem = useCallback((item, event) => {
48
+ actions.open(item.entry, event);
49
+ onClose();
50
+ }, [actions, onClose]);
51
+ const pill = resolveFilledSurface(colors.primary, colors);
52
+ const banner = resolveSoftTone(colors, toneScale(colors, "neutral"), colors.surfaceRaised);
53
+ const rule = raisedRule(colors);
54
+ const toolSize = isSheet ? "md" : "sm";
55
+ const iconPx = isSheet ? 18 : 16;
56
+ const markAll = () => actions.markAllRead({ zone: tab });
57
+ let content;
58
+ if (status === "loading")
59
+ content = _jsx(PanelSkeleton, { isSheet: isSheet });
60
+ else if (status === "error")
61
+ content = _jsx(PanelError, { labels: labels, onRetry: actions.retryLoad });
62
+ else if (status === "noAccess")
63
+ content = _jsx(PanelNoAccess, { labels: labels, appName: appName });
64
+ else if (groups.length === 0)
65
+ content = _jsx(PanelEmpty, { labels: labels, tab: tab });
66
+ else
67
+ content = _jsx(PanelRows, { groups: groups, variant: isSheet ? "phone" : "panel", format: format, onOpenItem: openItem });
68
+ return (_jsxs(Flex, { ref: ref, direction: "column", flex: 1, minH: 0, w: "100%", bg: colors.surfaceRaised, color: colors.text[800], "data-testid": testId, children: [_jsxs(Flex, { align: "center", justify: "space-between", gap: 2, flexShrink: 0, minH: isSheet ? "3.5rem" : "3rem", pl: 4, pr: 2, pt: 2, pb: 1, children: [_jsx(Heading, { as: "h2", id: titleId, fontSize: "1rem", fontWeight: 600, lineHeight: "1.5", color: colors.header[900], children: labels.title }), _jsxs(Flex, { align: "center", gap: 1, children: [isReady && features.markAllRead && (isSheet ? (_jsx(ToolTip, { label: labels.markAllRead, children: _jsx(IconButton, { size: "md", variant: "ghost", colorScheme: "gray", "aria-label": labels.markAllRead, icon: _jsx(Icon, { as: CheckCheck, boxSize: "1.125rem", "aria-hidden": "true" }), isDisabled: isOffline || !canMarkAll, onClick: markAll }) })) : (_jsx(Button, { size: "sm", variant: "ghost", colorScheme: "gray", label: labels.markAllRead, leftIcon: _jsx(CheckCheck, { size: iconPx, "aria-hidden": "true" }), isDisabled: isOffline || !canMarkAll, onClick: markAll }))), onOpenSettings && (_jsx(ToolTip, { label: labels.settings, children: _jsx(IconButton, { size: toolSize, variant: "ghost", colorScheme: "gray", "aria-label": labels.settings, icon: _jsx(Icon, { as: Settings, boxSize: `${iconPx / 16}rem`, "aria-hidden": "true" }), onClick: onOpenSettings }) })), isSheet && (_jsx(IconButton, { size: "md", variant: "ghost", colorScheme: "gray", "aria-label": labels.close, icon: _jsx(Icon, { as: X, boxSize: "1.125rem", "aria-hidden": "true" }), onClick: onClose }))] })] }), isOffline && (_jsxs(Flex, { role: "status", align: "flex-start", gap: 2, flexShrink: 0, px: 4, py: 2, fontSize: "xs", lineHeight: "1.5", bg: banner.bg, color: banner.fg, borderBottom: `1px solid ${rule}`, children: [_jsx(Icon, { as: WifiOff, boxSize: "0.875rem", mt: "0.125rem", "aria-hidden": "true" }), _jsx("span", { children: labels.offlineBanner(loadedAt === undefined ? undefined : format.absolute(loadedAt, now)) })] })), _jsx(Box, { px: 2, flexShrink: 0, children: _jsx(Tabs, { id: `${titleId}-tabs`, items: tabItems, value: tab, onChange: (value) => onTabChange(value), variant: "line", size: isSheet ? "md" : "sm", isFitted: true, "aria-label": labels.tabsLabel }) }), _jsxs(Box, { ref: bodyRef, role: "tabpanel", "aria-labelledby": tabId(`${titleId}-tabs`, tab), "aria-busy": status === "loading" ? true : undefined, tabIndex: -1, flex: 1, minH: 0, overflowY: "auto", position: "relative", sx: {
69
+ overscrollBehavior: "contain",
70
+ ...scrollbarStyles(colors, "hover"),
71
+ "&:focus-visible": { outline: "none", boxShadow: insetFocusRingShadow(colors) },
72
+ }, ...track, children: [heldIds.length > 0 && (_jsx(Box, { position: "sticky", top: 2, zIndex: 5, h: 0, display: "flex", justifyContent: "center", pointerEvents: "none", children: _jsx(Button, { size: isSheet ? "md" : "xs", mt: 2, h: isSheet ? undefined : "1.75rem", borderRadius: "full", "aria-label": labels.newCountName(heldIds.length), leftIcon: _jsx(ChevronUp, { size: 14, "aria-hidden": "true" }), label: labels.newCount(heldIds.length), onClick: reveal, sx: {
73
+ bg: pill.fill,
74
+ color: pill.color,
75
+ boxShadow: "md",
76
+ pointerEvents: "auto",
77
+ "&:hover:not(:disabled)": { bg: pill.hoverFill, color: pill.hoverColor },
78
+ "&:active:not(:disabled)": { bg: pill.activeFill, color: pill.activeColor },
79
+ } }) })), status === "loading" && _jsx(VisuallyHidden, { children: labels.loading }), content] }), onOpenInbox && (_jsx(Flex, { flexShrink: 0, p: 2, borderTop: `1px solid ${rule}`, children: _jsx(Button, { size: isSheet ? "md" : "sm", variant: "ghost", colorScheme: "gray", width: "100%", label: labels.openInbox, rightIcon: _jsx(ChevronRight, { size: iconPx, "aria-hidden": "true" }), onClick: onOpenInbox }) }))] }));
80
+ });
81
+ export default CenterPanel;
@@ -0,0 +1,92 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
3
+ import { Box, Popover, PopoverContent, PopoverTrigger, Portal } from "../../Primitives/Primitives.js";
4
+ import Drawer, { DrawerBody } from "../../Drawer/Drawer.js";
5
+ import NotificationBell from "../../NotificationBell/NotificationBell.js";
6
+ import ToolTip from "../../ToolTip/ToolTip.js";
7
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
8
+ import { RADIUS_ROLE } from "../../../Theme/tokens/primitives/radii.js";
9
+ import { NotificationScope, useNotificationCenter } from "../core/NotificationCenterProvider.js";
10
+ import { withCenterLabels } from "./centerLabels.js";
11
+ import { useIsSheet } from "./useIsSheet.js";
12
+ import CenterPanel from "./CenterPanel.js";
13
+ const TOAST_SELECTOR = '[id^="chakra-toast-manager"]';
14
+ const TAB_SELECTOR = '[role="tab"][aria-selected="true"]';
15
+ const CenterBody = ({ variant, isOpen, onOpenChange, defaultTab, labels: overrides, onOpenInbox, onOpenSettings, "data-testid": testId, }) => {
16
+ const { colors } = useCustomTheme();
17
+ const { counts, labels, format, lists, status, actions } = useNotificationCenter();
18
+ const text = useMemo(() => withCenterLabels(labels, format, overrides), [labels, format, overrides]);
19
+ const isSheet = useIsSheet(variant);
20
+ const titleId = useId();
21
+ const bellRef = useRef(null);
22
+ const panelRef = useRef(null);
23
+ const [ownOpen, setOwnOpen] = useState(false);
24
+ const open = isOpen === undefined ? ownOpen : isOpen;
25
+ const setOpen = useCallback((next) => {
26
+ setOwnOpen(next);
27
+ onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(next);
28
+ }, [onOpenChange]);
29
+ const close = useCallback(() => setOpen(false), [setOpen]);
30
+ const startTab = () => {
31
+ if (defaultTab !== undefined)
32
+ return defaultTab;
33
+ const holdsAny = lists.needsYou.some((item) => !item.state.isHidden);
34
+ return status !== "ready" || holdsAny ? "needsYou" : "activity";
35
+ };
36
+ const [tab, setTab] = useState(startTab);
37
+ const [wasOpen, setWasOpen] = useState(open);
38
+ if (open !== wasOpen) {
39
+ setWasOpen(open);
40
+ if (open)
41
+ setTab(startTab());
42
+ }
43
+ useEffect(() => {
44
+ if (!open || isSheet)
45
+ return undefined;
46
+ const onPointerDown = (event) => {
47
+ var _a, _b;
48
+ const target = event.target;
49
+ const inside = ((_a = panelRef.current) === null || _a === void 0 ? void 0 : _a.contains(target)) || ((_b = bellRef.current) === null || _b === void 0 ? void 0 : _b.contains(target));
50
+ if (!inside && !target.closest(TOAST_SELECTOR))
51
+ close();
52
+ };
53
+ document.addEventListener("pointerdown", onPointerDown);
54
+ return () => document.removeEventListener("pointerdown", onPointerDown);
55
+ }, [open, isSheet, close]);
56
+ useEffect(() => {
57
+ if (!open)
58
+ return undefined;
59
+ const onKeyDown = (event) => {
60
+ const isUndo = (event.ctrlKey || event.metaKey) && !event.shiftKey && event.key.toLowerCase() === "z";
61
+ const isTyping = event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLInputElement;
62
+ if (isUndo && !isTyping && actions.undoLast())
63
+ event.preventDefault();
64
+ };
65
+ document.addEventListener("keydown", onKeyDown);
66
+ return () => document.removeEventListener("keydown", onKeyDown);
67
+ }, [open, actions]);
68
+ const focusTarget = useMemo(() => ({ get current() { var _a, _b; return (_b = (_a = panelRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(TAB_SELECTOR)) !== null && _b !== void 0 ? _b : null; } }), []);
69
+ const panel = (_jsx(CenterPanel, { ref: panelRef, isSheet: isSheet, tab: tab, onTabChange: setTab, labels: text, titleId: titleId, onClose: close, onOpenInbox: onOpenInbox, onOpenSettings: onOpenSettings, "data-testid": testId ? `${testId}-panel` : undefined }));
70
+ const bellProps = {
71
+ ref: bellRef,
72
+ count: counts.needsYou,
73
+ showDot: counts.unread > 0,
74
+ "aria-label": text.bell(counts),
75
+ isOpen: open,
76
+ };
77
+ if (isSheet) {
78
+ return (_jsxs(Box, { display: "inline-flex", "data-testid": testId ? `${testId}-bell` : undefined, children: [_jsx(ToolTip, { label: text.title, isDisabled: open, children: _jsx(Box, { display: "inline-flex", children: _jsx(NotificationBell, { ...bellProps, onClick: () => setOpen(!open) }) }) }), _jsx(Drawer, { isOpen: open, onClose: close, placement: "bottom", size: "full", hideCloseButton: true, "aria-labelledby": titleId, initialFocusRef: focusTarget, finalFocusRef: bellRef, contentProps: {
79
+ bg: colors.surfaceRaised,
80
+ borderRadius: 0,
81
+ border: "none",
82
+ sx: { paddingTop: "env(safe-area-inset-top)", paddingBottom: "env(safe-area-inset-bottom)" },
83
+ }, children: _jsx(DrawerBody, { p: 0, display: "flex", flexDirection: "column", overflow: "hidden", children: panel }) })] }));
84
+ }
85
+ return (_jsxs(Popover, { isOpen: open, onOpen: () => setOpen(true), onClose: close, placement: "bottom-end", gutter: 8, isLazy: true, lazyBehavior: "keepMounted", closeOnBlur: false, initialFocusRef: focusTarget, children: [_jsx(Box, { display: "inline-flex", "data-testid": testId ? `${testId}-bell` : undefined, children: _jsx(ToolTip, { label: text.title, isDisabled: open, children: _jsx(Box, { display: "inline-flex", children: _jsx(PopoverTrigger, { children: _jsx(NotificationBell, { ...bellProps }) }) }) }) }), _jsx(Portal, { children: _jsx(PopoverContent, { w: "25rem", maxW: "calc(100vw - 3rem)", p: 0, display: "flex", flexDirection: "column", overflow: "hidden", borderRadius: RADIUS_ROLE.float, bg: colors.surfaceRaised, minH: "18.75rem", maxH: "min(40rem, calc(100dvh - 15rem))", _focus: { outline: "none", boxShadow: undefined }, _focusVisible: { outline: "none" }, "aria-label": text.title, children: panel }) })] }));
86
+ };
87
+ /** Bell with the server's Needs-you count (99+), else a dot for unread Activity; opens a two-tab panel or, on phones, a sheet. */
88
+ const NotificationCenter = (props) => {
89
+ const { variant, onOpenInbox, onOpenSettings, isOpen, onOpenChange, defaultTab, labels, "data-testid": testId } = props;
90
+ return (_jsx(NotificationScope, { source: props, children: _jsx(CenterBody, { variant: variant, onOpenInbox: onOpenInbox, onOpenSettings: onOpenSettings, isOpen: isOpen, onOpenChange: onOpenChange, defaultTab: defaultTab, labels: labels, "data-testid": testId }) }));
91
+ };
92
+ export default NotificationCenter;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment, useCallback, useId, useState } from "react";
3
+ import { Box, Heading } from "../../Primitives/Primitives.js";
4
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
5
+ import NotificationRow from "../row/NotificationRow.js";
6
+ export const PanelRows = ({ groups, variant, format, onOpenItem }) => {
7
+ const { colors } = useCustomTheme();
8
+ const idPrefix = useId();
9
+ const [openThreads, setOpenThreads] = useState(() => new Set());
10
+ const toggleThread = useCallback((thread) => {
11
+ setOpenThreads((current) => {
12
+ const next = new Set(current);
13
+ if (next.has(thread.key))
14
+ next.delete(thread.key);
15
+ else
16
+ next.add(thread.key);
17
+ return next;
18
+ });
19
+ }, []);
20
+ return (_jsx(Box, { pb: 2, children: groups.map((group) => {
21
+ const headingId = `${idPrefix}-${group.key}`;
22
+ return (_jsxs(Box, { as: "section", "aria-labelledby": group.label === null ? undefined : headingId, children: [group.label !== null && (_jsxs(Heading, { as: "h3", id: headingId, position: "sticky", top: 0, zIndex: 3, display: "flex", alignItems: "center", gap: 1.5, px: 4, pt: 3, pb: 1.5, bg: colors.surfaceRaised, color: colors.textMuted, fontSize: "0.6875rem", fontWeight: 600, letterSpacing: "0.06em", textTransform: "uppercase", children: [_jsx("span", { children: group.label }), group.count > 0 && _jsx("span", { children: format.number(group.count) })] })), _jsx(Box, { as: "ul", role: "list", listStyleType: "none", m: 0, px: "6px", display: "grid", gap: "2px", children: group.threads.map((thread) => {
23
+ const isOpen = openThreads.has(thread.key);
24
+ return (_jsxs(Fragment, { children: [_jsx(NotificationRow, { item: thread.lead, variant: variant, thread: thread, isThreadOpen: isOpen, onThreadToggle: toggleThread, onOpen: onOpenItem }), isOpen &&
25
+ thread.rest.map((item) => (_jsx(NotificationRow, { item: item, variant: variant, isThreadChild: true, onOpen: onOpenItem }, item.entry.id)))] }, thread.key));
26
+ }) })] }, group.key));
27
+ }) }));
28
+ };
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CheckCircle2, Bell, Lock } from "lucide-react";
3
+ import { Box, Flex } from "../../Primitives/Primitives.js";
4
+ import EmptyState from "../../EmptyState/EmptyState.js";
5
+ import ErrorState from "../../QueryState/ErrorState.js";
6
+ import Skeletons from "../../Skeletons/Skeletons.js";
7
+ const SKELETON_ROWS = 4;
8
+ export const PanelSkeleton = ({ isSheet }) => (_jsx(Box, { "aria-hidden": "true", py: 2, px: "6px", children: Array.from({ length: SKELETON_ROWS }, (_, index) => (_jsxs(Flex, { gap: 3, py: 2.5, pl: 5, pr: 3, children: [_jsx(Skeletons, { type: "circle", size: isSheet ? "10" : "8" }), _jsxs(Flex, { flex: 1, direction: "column", gap: 2, pt: 1, children: [_jsx(Skeletons, { type: "skeleton", height: "0.75rem", width: `${88 - index * 9}%` }), _jsx(Skeletons, { type: "skeleton", height: "0.625rem", width: `${54 + index * 7}%` }), index % 2 === 0 && _jsx(Skeletons, { type: "skeleton", height: index === 0 ? "1.5rem" : "0.625rem", width: "6rem" })] })] }, index))) }));
9
+ export const PanelError = ({ labels, onRetry }) => (_jsx(ErrorState, { size: "sm", title: labels.loadErrorTitle, description: labels.loadErrorDescription, retryLabel: labels.retry, onRetry: onRetry }));
10
+ export const PanelNoAccess = ({ labels, appName }) => (_jsx(Box, { role: "status", children: _jsx(EmptyState, { size: "sm", icon: Lock, colorScheme: "gray", title: labels.noAccessTitle, description: labels.noAccessDescription(appName) }) }));
11
+ export const PanelEmpty = ({ labels, tab }) => tab === "needsYou" ? (_jsx(EmptyState, { size: "sm", icon: CheckCircle2, colorScheme: "success", title: labels.emptyNeedsYouTitle, description: labels.emptyNeedsYouDescription })) : (_jsx(EmptyState, { size: "sm", icon: Bell, title: labels.emptyActivityTitle, description: labels.emptyActivityDescription }));
@@ -0,0 +1,34 @@
1
+ import { mergeDefined } from "../core/labels.js";
2
+ export const withCenterLabels = (shared, format, overrides = {}) => mergeDefined({
3
+ ...shared,
4
+ bell: ({ needsYou, unread }) => {
5
+ if (needsYou > 0)
6
+ return `Notifications, ${format.number(needsYou)} need you`;
7
+ if (unread > 0)
8
+ return `Notifications, ${format.number(unread)} unread`;
9
+ return "Notifications";
10
+ },
11
+ title: "Notifications",
12
+ needsYouTab: "Needs you",
13
+ activityTab: "Activity",
14
+ tabsLabel: "Notification lists",
15
+ markAllRead: "Mark all read",
16
+ openInbox: "Open inbox",
17
+ settings: "Notification settings",
18
+ close: "Close notifications",
19
+ loading: "Loading notifications",
20
+ loadErrorTitle: "Couldn't load notifications",
21
+ loadErrorDescription: "Check your connection, then try again. Nothing you did was lost.",
22
+ noAccessTitle: "Notifications aren't turned on for you",
23
+ noAccessDescription: (appName) => `Your ${appName !== null && appName !== void 0 ? appName : "workspace"} admin hasn't given your role access to notifications in this workspace.`,
24
+ emptyNeedsYouTitle: "You're all caught up",
25
+ emptyNeedsYouDescription: "Nothing is waiting for your decision.",
26
+ emptyActivityTitle: "No activity yet",
27
+ emptyActivityDescription: "Updates about your work and requests show up here.",
28
+ offlineBanner: (loadedAt) => loadedAt === undefined
29
+ ? "You're offline. Changes are paused."
30
+ : `You're offline. These are from ${loadedAt}, and changes are paused.`,
31
+ newCount: (count) => `${format.number(count)} new`,
32
+ newCountName: (count) => `${format.number(count)} new notifications`,
33
+ overflowCount: (count) => `${format.number(count)}+`,
34
+ }, overrides);
@@ -0,0 +1,67 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ const ROW_SELECTOR = "[data-notification-id]";
3
+ const FOCUS_SELECTOR = "a[href], button";
4
+ export const useArrivals = (ids, scope, bodyRef) => {
5
+ const latest = useRef(ids);
6
+ latest.current = ids;
7
+ const known = useRef(new Set(ids));
8
+ const seenScope = useRef(scope);
9
+ const busy = useRef(false);
10
+ const [held, setHeld] = useState({ scope, ids: [] });
11
+ const key = ids.join();
12
+ useEffect(() => {
13
+ var _a, _b;
14
+ if (seenScope.current !== scope) {
15
+ seenScope.current = scope;
16
+ known.current = new Set(latest.current);
17
+ setHeld({ scope, ids: [] });
18
+ if (bodyRef.current)
19
+ bodyRef.current.scrollTop = 0;
20
+ return;
21
+ }
22
+ const fresh = latest.current.filter((id) => !known.current.has(id));
23
+ if (!fresh.length)
24
+ return;
25
+ fresh.forEach((id) => known.current.add(id));
26
+ const scrolled = ((_b = (_a = bodyRef.current) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0) > 4;
27
+ if (busy.current || scrolled)
28
+ setHeld((current) => ({ scope, ids: [...current.ids, ...fresh] }));
29
+ }, [key, scope, bodyRef]);
30
+ const heldIds = useMemo(() => {
31
+ if (held.scope !== scope)
32
+ return [];
33
+ const present = new Set(ids);
34
+ return held.ids.filter((id) => present.has(id));
35
+ }, [held, scope, ids]);
36
+ const reveal = useCallback(() => {
37
+ setHeld({ scope, ids: [] });
38
+ requestAnimationFrame(() => {
39
+ var _a;
40
+ const body = bodyRef.current;
41
+ if (!body)
42
+ return;
43
+ const row = [...body.querySelectorAll(ROW_SELECTOR)].find((node) => { var _a; return heldIds.includes((_a = node.dataset.notificationId) !== null && _a !== void 0 ? _a : ""); });
44
+ if (!row)
45
+ return;
46
+ const top = row.getBoundingClientRect().top - body.getBoundingClientRect().top;
47
+ body.scrollTop += top - 8;
48
+ (_a = row.querySelector(FOCUS_SELECTOR)) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true });
49
+ });
50
+ }, [heldIds, scope, bodyRef]);
51
+ const track = useMemo(() => ({
52
+ onPointerEnter: () => {
53
+ busy.current = true;
54
+ },
55
+ onPointerLeave: () => {
56
+ busy.current = false;
57
+ },
58
+ onFocus: () => {
59
+ busy.current = true;
60
+ },
61
+ onBlur: (event) => {
62
+ if (!event.currentTarget.contains(event.relatedTarget))
63
+ busy.current = false;
64
+ },
65
+ }), []);
66
+ return { heldIds, reveal, track };
67
+ };
@@ -0,0 +1,16 @@
1
+ import { useEffect, useState } from "react";
2
+ const DESKTOP = "(min-width: 48em)";
3
+ const isNarrow = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && !window.matchMedia(DESKTOP).matches;
4
+ export const useIsSheet = (variant) => {
5
+ const [narrow, setNarrow] = useState(isNarrow);
6
+ useEffect(() => {
7
+ if (variant !== undefined || typeof window.matchMedia !== "function")
8
+ return undefined;
9
+ const query = window.matchMedia(DESKTOP);
10
+ const sync = () => setNarrow(!query.matches);
11
+ sync();
12
+ query.addEventListener("change", sync);
13
+ return () => query.removeEventListener("change", sync);
14
+ }, [variant]);
15
+ return variant === undefined ? narrow : variant === "sheet";
16
+ };
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ import VisuallyHidden from "../../VisuallyHidden/VisuallyHidden.js";
4
+ import { useNotificationEngine } from "./useNotificationEngine.js";
5
+ const NotificationCenterContext = createContext(null);
6
+ const NotificationSharedContext = createContext(null);
7
+ const MISSING_PROVIDER = "This notification component has no data: pass entries, counts, categories and onOpen, or render it inside a NotificationCenterProvider.";
8
+ /** One overlay, one undo queue and one polite live region for every notification component inside it. */
9
+ export const NotificationCenterProvider = ({ children, ...source }) => {
10
+ const { value, shared, announcement } = useNotificationEngine(source);
11
+ return (_jsx(NotificationCenterContext.Provider, { value: value, children: _jsxs(NotificationSharedContext.Provider, { value: shared, children: [children, _jsx(VisuallyHidden, { role: "status", "aria-live": "polite", "aria-atomic": "true", "data-testid": "notification-center-announcer", children: announcement })] }) }));
12
+ };
13
+ /** The live lists, counts and actions of the nearest `NotificationCenterProvider`; throws outside one. */
14
+ export const useNotificationCenter = () => {
15
+ const value = useContext(NotificationCenterContext);
16
+ if (!value)
17
+ throw new Error(MISSING_PROVIDER);
18
+ return value;
19
+ };
20
+ export const useNotificationShared = () => {
21
+ const value = useContext(NotificationSharedContext);
22
+ if (!value)
23
+ throw new Error(MISSING_PROVIDER);
24
+ return value;
25
+ };
26
+ export const hasNotificationSource = (binding) => binding.entries !== undefined;
27
+ /** A component's own source props win with a private provider; without them it joins the provider above. */
28
+ export const NotificationScope = ({ source, children }) => {
29
+ const parent = useContext(NotificationCenterContext);
30
+ if (hasNotificationSource(source))
31
+ return _jsx(NotificationCenterProvider, { ...source, children: children });
32
+ if (!parent)
33
+ throw new Error(MISSING_PROVIDER);
34
+ return _jsx(_Fragment, { children: children });
35
+ };
@@ -0,0 +1,39 @@
1
+ import { getApiErrorStatus } from "../../../Utils/apiError.js";
2
+ const KINDS = ["conflict", "forbidden", "reasonRequired", "failed"];
3
+ const asRecord = (value) => typeof value === "object" && value !== null ? value : {};
4
+ const text = (value) => typeof value === "string" && value.trim() ? value : undefined;
5
+ export const isNotificationActionError = (value) => KINDS.includes(asRecord(value).kind);
6
+ export const notificationActionError = (kind, message) => {
7
+ const error = new Error(message !== null && message !== void 0 ? message : "");
8
+ error.name = "NotificationActionError";
9
+ error.kind = kind;
10
+ return error;
11
+ };
12
+ const statusOf = (error) => {
13
+ const direct = getApiErrorStatus(error);
14
+ if (direct !== null)
15
+ return direct;
16
+ const status = asRecord(asRecord(error).response).status;
17
+ return typeof status === "number" ? status : null;
18
+ };
19
+ const serverMessage = (error) => {
20
+ var _a;
21
+ const record = asRecord(error);
22
+ return (_a = text(asRecord(record.data).message)) !== null && _a !== void 0 ? _a : text(asRecord(asRecord(record.response).data).message);
23
+ };
24
+ const kindFor = (status) => {
25
+ if (status === 409)
26
+ return "conflict";
27
+ if (status === 403)
28
+ return "forbidden";
29
+ return "failed";
30
+ };
31
+ export const toNotificationActionError = (error) => {
32
+ if (isNotificationActionError(error)) {
33
+ const message = text(error.message);
34
+ return message ? { kind: error.kind, message } : { kind: error.kind };
35
+ }
36
+ const message = serverMessage(error);
37
+ const kind = kindFor(statusOf(error));
38
+ return message ? { kind, message } : { kind };
39
+ };
@@ -0,0 +1,87 @@
1
+ const MINUTE_MS = 60000;
2
+ const HOUR_MS = 60 * MINUTE_MS;
3
+ export const DAY_MS = 24 * HOUR_MS;
4
+ const WEEK_DAYS = 7;
5
+ const startOfDay = (at) => {
6
+ const date = new Date(at);
7
+ date.setHours(0, 0, 0, 0);
8
+ return date.getTime();
9
+ };
10
+ const daysBefore = (dayStart, days) => {
11
+ const date = new Date(dayStart);
12
+ date.setDate(date.getDate() - days);
13
+ return date.getTime();
14
+ };
15
+ const sameYear = (a, b) => new Date(a).getFullYear() === new Date(b).getFullYear();
16
+ export const createNotificationFormatter = (locale, labels) => {
17
+ const time = new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit" });
18
+ const shortDate = new Intl.DateTimeFormat(locale, { day: "numeric", month: "short" });
19
+ const shortDateWithYear = new Intl.DateTimeFormat(locale, { day: "numeric", month: "short", year: "numeric" });
20
+ const full = new Intl.DateTimeFormat(locale, {
21
+ weekday: "short",
22
+ day: "numeric",
23
+ month: "short",
24
+ hour: "numeric",
25
+ minute: "2-digit",
26
+ });
27
+ const relative = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
28
+ const narrow = (unit) => new Intl.NumberFormat(locale, { style: "unit", unit, unitDisplay: "narrow" });
29
+ const minutes = narrow("minute");
30
+ const hours = narrow("hour");
31
+ const days = narrow("day");
32
+ const digits = new Intl.NumberFormat(locale);
33
+ const elapsed = (at, now) => Math.max(0, now - at);
34
+ const dayBucket = (at, now) => {
35
+ const today = startOfDay(now);
36
+ if (at >= today)
37
+ return "today";
38
+ if (at >= daysBefore(today, 1))
39
+ return "yesterday";
40
+ if (at >= daysBefore(today, WEEK_DAYS - 1))
41
+ return "thisWeek";
42
+ return "older";
43
+ };
44
+ const dayLabels = {
45
+ today: labels.today,
46
+ yesterday: labels.yesterday,
47
+ thisWeek: labels.thisWeek,
48
+ older: labels.older,
49
+ };
50
+ return {
51
+ shortAge: (at, now) => {
52
+ const gap = elapsed(at, now);
53
+ if (gap < MINUTE_MS)
54
+ return labels.now;
55
+ if (gap < HOUR_MS)
56
+ return minutes.format(Math.floor(gap / MINUTE_MS));
57
+ if (gap < DAY_MS)
58
+ return hours.format(Math.floor(gap / HOUR_MS));
59
+ if (gap < WEEK_DAYS * DAY_MS)
60
+ return days.format(Math.floor(gap / DAY_MS));
61
+ return (sameYear(at, now) ? shortDate : shortDateWithYear).format(at);
62
+ },
63
+ longAge: (at, now) => {
64
+ const gap = elapsed(at, now);
65
+ if (gap < MINUTE_MS)
66
+ return labels.justNow;
67
+ if (gap < HOUR_MS)
68
+ return relative.format(-Math.floor(gap / MINUTE_MS), "minute");
69
+ if (gap < DAY_MS)
70
+ return relative.format(-Math.floor(gap / HOUR_MS), "hour");
71
+ return full.format(at);
72
+ },
73
+ absolute: (at, now) => {
74
+ const bucket = dayBucket(at, now);
75
+ if (bucket === "today")
76
+ return labels.todayAt(time.format(at));
77
+ if (bucket === "yesterday")
78
+ return labels.yesterdayAt(time.format(at));
79
+ return full.format(at);
80
+ },
81
+ dayBucket,
82
+ dayLabel: (bucket) => dayLabels[bucket],
83
+ daysSince: (at, now) => Math.floor(elapsed(at, now) / DAY_MS),
84
+ number: (value) => digits.format(value),
85
+ iso: (at) => new Date(at).toISOString(),
86
+ };
87
+ };