pixelize-design-library 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/COMPONENT-GOTCHAS.md +37 -13
  3. package/DESIGN-GUIDE.md +4 -1
  4. package/dist/Components/NotificationBell/NotificationBellProps.d.ts +3 -3
  5. package/dist/Components/NotificationBell/NotificationItem.d.ts +1 -1
  6. package/dist/Components/NotificationBell/NotificationItem.js +1 -1
  7. package/dist/Components/NotificationBell/NotificationList.d.ts +1 -1
  8. package/dist/Components/NotificationBell/NotificationList.js +1 -1
  9. package/dist/Components/NotificationBell/NotificationPanel.d.ts +1 -1
  10. package/dist/Components/NotificationBell/NotificationPanel.js +1 -1
  11. package/dist/Components/NotificationCenter/center/CenterPanel.d.ts +15 -0
  12. package/dist/Components/NotificationCenter/center/CenterPanel.js +87 -0
  13. package/dist/Components/NotificationCenter/center/NotificationCenter.d.ts +4 -0
  14. package/dist/Components/NotificationCenter/center/NotificationCenter.js +130 -0
  15. package/dist/Components/NotificationCenter/center/NotificationCenterProps.d.ts +71 -0
  16. package/dist/Components/NotificationCenter/center/NotificationCenterProps.js +2 -0
  17. package/dist/Components/NotificationCenter/center/PanelRows.d.ts +10 -0
  18. package/dist/Components/NotificationCenter/center/PanelRows.js +35 -0
  19. package/dist/Components/NotificationCenter/center/PanelStates.d.ts +17 -0
  20. package/dist/Components/NotificationCenter/center/PanelStates.js +21 -0
  21. package/dist/Components/NotificationCenter/center/centerLabels.d.ts +4 -0
  22. package/dist/Components/NotificationCenter/center/centerLabels.js +38 -0
  23. package/dist/Components/NotificationCenter/center/useArrivals.d.ts +14 -0
  24. package/dist/Components/NotificationCenter/center/useArrivals.js +71 -0
  25. package/dist/Components/NotificationCenter/center/useIsSheet.d.ts +2 -0
  26. package/dist/Components/NotificationCenter/center/useIsSheet.js +20 -0
  27. package/dist/Components/NotificationCenter/core/NotificationCenterProvider.d.ts +17 -0
  28. package/dist/Components/NotificationCenter/core/NotificationCenterProvider.js +46 -0
  29. package/dist/Components/NotificationCenter/core/NotificationEngineProps.d.ts +182 -0
  30. package/dist/Components/NotificationCenter/core/NotificationEngineProps.js +2 -0
  31. package/dist/Components/NotificationCenter/core/NotificationSourceProps.d.ts +284 -0
  32. package/dist/Components/NotificationCenter/core/NotificationSourceProps.js +2 -0
  33. package/dist/Components/NotificationCenter/core/actionError.d.ts +4 -0
  34. package/dist/Components/NotificationCenter/core/actionError.js +45 -0
  35. package/dist/Components/NotificationCenter/core/format.d.ts +4 -0
  36. package/dist/Components/NotificationCenter/core/format.js +91 -0
  37. package/dist/Components/NotificationCenter/core/grouping.d.ts +14 -0
  38. package/dist/Components/NotificationCenter/core/grouping.js +102 -0
  39. package/dist/Components/NotificationCenter/core/labels.d.ts +6 -0
  40. package/dist/Components/NotificationCenter/core/labels.js +94 -0
  41. package/dist/Components/NotificationCenter/core/overlay.d.ts +109 -0
  42. package/dist/Components/NotificationCenter/core/overlay.js +281 -0
  43. package/dist/Components/NotificationCenter/core/useNotificationEngine.d.ts +13 -0
  44. package/dist/Components/NotificationCenter/core/useNotificationEngine.js +415 -0
  45. package/dist/Components/NotificationCenter/inbox/InboxList.d.ts +17 -0
  46. package/dist/Components/NotificationCenter/inbox/InboxList.js +25 -0
  47. package/dist/Components/NotificationCenter/inbox/InboxStates.d.ts +17 -0
  48. package/dist/Components/NotificationCenter/inbox/InboxStates.js +27 -0
  49. package/dist/Components/NotificationCenter/inbox/NotificationInbox.d.ts +4 -0
  50. package/dist/Components/NotificationCenter/inbox/NotificationInbox.js +434 -0
  51. package/dist/Components/NotificationCenter/inbox/NotificationInboxProps.d.ts +106 -0
  52. package/dist/Components/NotificationCenter/inbox/NotificationInboxProps.js +2 -0
  53. package/dist/Components/NotificationCenter/inbox/ShortcutsDialog.d.ts +8 -0
  54. package/dist/Components/NotificationCenter/inbox/ShortcutsDialog.js +63 -0
  55. package/dist/Components/NotificationCenter/inbox/inboxLabels.d.ts +4 -0
  56. package/dist/Components/NotificationCenter/inbox/inboxLabels.js +56 -0
  57. package/dist/Components/NotificationCenter/inbox/inboxModel.d.ts +17 -0
  58. package/dist/Components/NotificationCenter/inbox/inboxModel.js +47 -0
  59. package/dist/Components/NotificationCenter/inbox/useControllable.d.ts +1 -0
  60. package/dist/Components/NotificationCenter/inbox/useControllable.js +15 -0
  61. package/dist/Components/NotificationCenter/index.d.ts +8 -0
  62. package/dist/Components/NotificationCenter/index.js +26 -0
  63. package/dist/Components/NotificationCenter/preferences/NotificationPreferences.d.ts +4 -0
  64. package/dist/Components/NotificationCenter/preferences/NotificationPreferences.js +72 -0
  65. package/dist/Components/NotificationCenter/preferences/NotificationPreferencesProps.d.ts +111 -0
  66. package/dist/Components/NotificationCenter/preferences/NotificationPreferencesProps.js +2 -0
  67. package/dist/Components/NotificationCenter/preferences/PreferenceCells.d.ts +16 -0
  68. package/dist/Components/NotificationCenter/preferences/PreferenceCells.js +35 -0
  69. package/dist/Components/NotificationCenter/preferences/PreferenceGroups.d.ts +15 -0
  70. package/dist/Components/NotificationCenter/preferences/PreferenceGroups.js +26 -0
  71. package/dist/Components/NotificationCenter/preferences/preferencesLabels.d.ts +2 -0
  72. package/dist/Components/NotificationCenter/preferences/preferencesLabels.js +28 -0
  73. package/dist/Components/NotificationCenter/preferences/useAutosave.d.ts +8 -0
  74. package/dist/Components/NotificationCenter/preferences/useAutosave.js +41 -0
  75. package/dist/Components/NotificationCenter/row/NotificationDetail.d.ts +4 -0
  76. package/dist/Components/NotificationCenter/row/NotificationDetail.js +65 -0
  77. package/dist/Components/NotificationCenter/row/NotificationDetailProps.d.ts +15 -0
  78. package/dist/Components/NotificationCenter/row/NotificationDetailProps.js +2 -0
  79. package/dist/Components/NotificationCenter/row/NotificationRow.d.ts +3 -0
  80. package/dist/Components/NotificationCenter/row/NotificationRow.js +124 -0
  81. package/dist/Components/NotificationCenter/row/NotificationRowProps.d.ts +33 -0
  82. package/dist/Components/NotificationCenter/row/NotificationRowProps.js +2 -0
  83. package/dist/Components/NotificationCenter/row/ReasonForm.d.ts +14 -0
  84. package/dist/Components/NotificationCenter/row/ReasonForm.js +59 -0
  85. package/dist/Components/NotificationCenter/row/RowActions.d.ts +16 -0
  86. package/dist/Components/NotificationCenter/row/RowActions.js +36 -0
  87. package/dist/Components/NotificationCenter/row/RowQuickActions.d.ts +17 -0
  88. package/dist/Components/NotificationCenter/row/RowQuickActions.js +57 -0
  89. package/dist/Components/NotificationCenter/row/RowStatus.d.ts +10 -0
  90. package/dist/Components/NotificationCenter/row/RowStatus.js +42 -0
  91. package/dist/Components/NotificationCenter/row/category.d.ts +8 -0
  92. package/dist/Components/NotificationCenter/row/category.js +13 -0
  93. package/dist/Components/NotificationCenter/row/rowMenu.d.ts +14 -0
  94. package/dist/Components/NotificationCenter/row/rowMenu.js +37 -0
  95. package/dist/Components/NotificationCenter/row/rowParts.d.ts +40 -0
  96. package/dist/Components/NotificationCenter/row/rowParts.js +72 -0
  97. package/dist/Components/NotificationCenter/row/rowStyles.d.ts +11 -0
  98. package/dist/Components/NotificationCenter/row/rowStyles.js +25 -0
  99. package/dist/Components/SegmentedControl/segmentedInk.d.ts +4 -0
  100. package/dist/Components/SegmentedControl/segmentedInk.js +16 -2
  101. package/dist/Components/SideBar/SecondaryBar.js +2 -2
  102. package/dist/Components/SideBar/SideBar.js +18 -8
  103. package/dist/Components/SideBar/SideBarProps.d.ts +4 -2
  104. package/dist/Components/SideBar/components/MenuItemBox.d.ts +0 -2
  105. package/dist/Components/SideBar/components/MenuItemBox.js +2 -3
  106. package/dist/Components/SideBar/components/MenuItems.js +2 -2
  107. package/dist/Components/SideBar/components/PinnedSection.js +3 -4
  108. package/dist/Components/SideBar/sidebarScale.d.ts +0 -2
  109. package/dist/Components/SideBar/sidebarScale.js +0 -2
  110. package/dist/Components/Switch/Switch.d.ts +1 -1
  111. package/dist/Components/Switch/Switch.js +2 -2
  112. package/dist/Components/Switch/SwitchProps.d.ts +3 -0
  113. package/dist/Components/Toggle/TableToggle.js +4 -5
  114. package/dist/Components/Toggle/tableToggleInk.d.ts +7 -0
  115. package/dist/Components/Toggle/tableToggleInk.js +28 -0
  116. package/dist/esm/Components/NotificationBell/NotificationItem.js +1 -1
  117. package/dist/esm/Components/NotificationBell/NotificationList.js +1 -1
  118. package/dist/esm/Components/NotificationBell/NotificationPanel.js +1 -1
  119. package/dist/esm/Components/NotificationCenter/center/CenterPanel.js +81 -0
  120. package/dist/esm/Components/NotificationCenter/center/NotificationCenter.js +92 -0
  121. package/dist/esm/Components/NotificationCenter/center/NotificationCenterProps.js +1 -0
  122. package/dist/esm/Components/NotificationCenter/center/PanelRows.js +28 -0
  123. package/dist/esm/Components/NotificationCenter/center/PanelStates.js +11 -0
  124. package/dist/esm/Components/NotificationCenter/center/centerLabels.js +34 -0
  125. package/dist/esm/Components/NotificationCenter/center/useArrivals.js +67 -0
  126. package/dist/esm/Components/NotificationCenter/center/useIsSheet.js +16 -0
  127. package/dist/esm/Components/NotificationCenter/core/NotificationCenterProvider.js +35 -0
  128. package/dist/esm/Components/NotificationCenter/core/NotificationEngineProps.js +1 -0
  129. package/dist/esm/Components/NotificationCenter/core/NotificationSourceProps.js +1 -0
  130. package/dist/esm/Components/NotificationCenter/core/actionError.js +39 -0
  131. package/dist/esm/Components/NotificationCenter/core/format.js +87 -0
  132. package/dist/esm/Components/NotificationCenter/core/grouping.js +88 -0
  133. package/dist/esm/Components/NotificationCenter/core/labels.js +88 -0
  134. package/dist/esm/Components/NotificationCenter/core/overlay.js +274 -0
  135. package/dist/esm/Components/NotificationCenter/core/useNotificationEngine.js +408 -0
  136. package/dist/esm/Components/NotificationCenter/inbox/InboxList.js +18 -0
  137. package/dist/esm/Components/NotificationCenter/inbox/InboxStates.js +18 -0
  138. package/dist/esm/Components/NotificationCenter/inbox/NotificationInbox.js +396 -0
  139. package/dist/esm/Components/NotificationCenter/inbox/NotificationInboxProps.js +1 -0
  140. package/dist/esm/Components/NotificationCenter/inbox/ShortcutsDialog.js +23 -0
  141. package/dist/esm/Components/NotificationCenter/inbox/inboxLabels.js +52 -0
  142. package/dist/esm/Components/NotificationCenter/inbox/inboxModel.js +37 -0
  143. package/dist/esm/Components/NotificationCenter/inbox/useControllable.js +11 -0
  144. package/dist/esm/Components/NotificationCenter/index.js +8 -0
  145. package/dist/esm/Components/NotificationCenter/preferences/NotificationPreferences.js +67 -0
  146. package/dist/esm/Components/NotificationCenter/preferences/NotificationPreferencesProps.js +1 -0
  147. package/dist/esm/Components/NotificationCenter/preferences/PreferenceCells.js +27 -0
  148. package/dist/esm/Components/NotificationCenter/preferences/PreferenceGroups.js +21 -0
  149. package/dist/esm/Components/NotificationCenter/preferences/preferencesLabels.js +24 -0
  150. package/dist/esm/Components/NotificationCenter/preferences/useAutosave.js +37 -0
  151. package/dist/esm/Components/NotificationCenter/row/NotificationDetail.js +60 -0
  152. package/dist/esm/Components/NotificationCenter/row/NotificationDetailProps.js +1 -0
  153. package/dist/esm/Components/NotificationCenter/row/NotificationRow.js +119 -0
  154. package/dist/esm/Components/NotificationCenter/row/NotificationRowProps.js +1 -0
  155. package/dist/esm/Components/NotificationCenter/row/ReasonForm.js +52 -0
  156. package/dist/esm/Components/NotificationCenter/row/RowActions.js +28 -0
  157. package/dist/esm/Components/NotificationCenter/row/RowQuickActions.js +49 -0
  158. package/dist/esm/Components/NotificationCenter/row/RowStatus.js +35 -0
  159. package/dist/esm/Components/NotificationCenter/row/category.js +9 -0
  160. package/dist/esm/Components/NotificationCenter/row/rowMenu.js +30 -0
  161. package/dist/esm/Components/NotificationCenter/row/rowParts.js +59 -0
  162. package/dist/esm/Components/NotificationCenter/row/rowStyles.js +19 -0
  163. package/dist/esm/Components/SegmentedControl/segmentedInk.js +15 -2
  164. package/dist/esm/Components/SideBar/SecondaryBar.js +3 -3
  165. package/dist/esm/Components/SideBar/SideBar.js +18 -8
  166. package/dist/esm/Components/SideBar/components/MenuItemBox.js +2 -3
  167. package/dist/esm/Components/SideBar/components/MenuItems.js +2 -2
  168. package/dist/esm/Components/SideBar/components/PinnedSection.js +3 -4
  169. package/dist/esm/Components/SideBar/sidebarScale.js +0 -2
  170. package/dist/esm/Components/Switch/Switch.js +2 -2
  171. package/dist/esm/Components/Toggle/TableToggle.js +4 -5
  172. package/dist/esm/Components/Toggle/tableToggleInk.js +24 -0
  173. package/dist/esm/index.js +1 -0
  174. package/dist/index.d.ts +8 -0
  175. package/dist/index.js +16 -3
  176. package/package.json +1 -1
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Lock } from "lucide-react";
3
+ import { Box, Flex } from "../../Primitives/Primitives.js";
4
+ import Checkbox from "../../Checkbox/Checkbox.js";
5
+ import ToggleSwitch from "../../ToggleSwitch/ToggleSwitch.js";
6
+ import ToolTip from "../../ToolTip/ToolTip.js";
7
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
8
+ const LockMark = ({ label }) => {
9
+ const { colors } = useCustomTheme();
10
+ return (_jsx(Box, { as: "span", role: "img", "aria-label": label, tabIndex: 0, display: "inline-flex", color: colors.textMuted, borderRadius: "sm", children: _jsx(Lock, { size: 12, "aria-hidden": "true" }) }));
11
+ };
12
+ export const GridCell = ({ row, channel, labels, isOn, isLocked, isFrozen, isPending, onToggle, "data-testid": testId }) => {
13
+ const name = `${row.label}: ${channel.label}`;
14
+ const checkbox = (_jsx(Checkbox, { "aria-label": isLocked ? `${name}, ${labels.alwaysOn}` : name, isChecked: isOn, isDisabled: isLocked || isFrozen, onChange: (event) => onToggle(event.target.checked) }));
15
+ if (!isLocked) {
16
+ return (_jsx(Flex, { "data-testid": testId, justify: "center", opacity: isPending ? 0.6 : 1, children: checkbox }));
17
+ }
18
+ return (_jsx(ToolTip, { label: labels.lockedTip, placement: "top", children: _jsxs(Flex, { "data-testid": testId, justify: "center", position: "relative", w: "fit-content", mx: "auto", children: [checkbox, _jsx(Box, { position: "absolute", left: "calc(100% + 0.375rem)", top: "50%", transform: "translateY(-50%)", children: _jsx(LockMark, { label: labels.lockedTip }) })] }) }));
19
+ };
20
+ const touchTarget = {
21
+ "& label": { display: "inline-flex", alignItems: "center", minHeight: "2.75rem" },
22
+ "& input[role='switch']": { top: "-0.6875rem", height: "calc(100% + 1.375rem)", left: "-0.25rem", width: "calc(100% + 0.5rem)" },
23
+ };
24
+ export const SwitchCell = ({ row, channel, labels, isOn, isLocked, isFrozen, isPending, onToggle, "data-testid": testId }) => {
25
+ const name = `${row.label}: ${channel.label}`;
26
+ return (_jsxs(Flex, { "data-testid": testId, align: "center", gap: 1.5, minH: "2.75rem", opacity: isPending ? 0.6 : 1, sx: touchTarget, children: [_jsx(ToggleSwitch, { size: "md", label: isLocked ? `${channel.label} (${labels.alwaysOn})` : channel.label, "aria-label": isLocked ? `${name}, ${labels.alwaysOn}` : name, isChecked: isOn, isDisabled: isLocked || isFrozen, onChange: onToggle }), isLocked && _jsx(LockMark, { label: labels.lockedTip })] }));
27
+ };
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Flex, Text } from "../../Primitives/Primitives.js";
3
+ import SettingRow from "../../SettingRow/SettingRow.js";
4
+ import SettingsSection from "../../SettingRow/SettingsSection.js";
5
+ import VisuallyHidden from "../../VisuallyHidden/VisuallyHidden.js";
6
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
7
+ import { cardRule } from "../../../Theme/tokens/builders/planeStep.js";
8
+ import { GridCell, SwitchCell } from "./PreferenceCells.js";
9
+ const isLockedCell = (row, channelId) => Boolean(row.locked && row.locked.includes(channelId));
10
+ const cellId = (testId, rowId, channelId) => (testId ? `${testId}-${rowId}-${channelId}` : undefined);
11
+ const GroupTable = ({ group, channels, labels, isFrozen, valueOf, isPending, onToggle, testId }) => {
12
+ const { colors } = useCustomTheme();
13
+ const rule = cardRule(colors);
14
+ return (_jsxs(Box, { as: "table", w: "100%", sx: { borderCollapse: "collapse" }, children: [_jsx(Box, { as: "thead", children: _jsxs(Box, { as: "tr", children: [_jsx(Box, { as: "th", scope: "col", textAlign: "left", pb: 2, children: _jsx(VisuallyHidden, { children: labels.typeColumn }) }), channels.map((channel) => (_jsx(Box, { as: "th", scope: "col", w: "7rem", pb: 2, textAlign: "center", fontSize: "xs", fontWeight: 500, color: colors.textMuted, children: channel.label }, channel.id)))] }) }), _jsx(Box, { as: "tbody", children: group.rows.map((row) => (_jsxs(Box, { as: "tr", borderTop: "1px solid", borderColor: rule, children: [_jsxs(Box, { as: "th", scope: "row", textAlign: "left", fontWeight: 400, py: 3, pr: 4, children: [_jsx(Text, { fontSize: "sm", fontWeight: 500, color: colors.text[800], children: row.label }), row.hint && (_jsx(Text, { fontSize: "xs", color: colors.textMuted, children: row.hint }))] }), channels.map((channel) => (_jsx(Box, { as: "td", textAlign: "center", verticalAlign: "middle", py: 3, children: _jsx(GridCell, { row: row, channel: channel, labels: labels, isOn: isLockedCell(row, channel.id) || valueOf(row.id, channel.id), isLocked: isLockedCell(row, channel.id), isFrozen: isFrozen, isPending: isPending(row.id, channel.id), onToggle: (next) => onToggle(row, channel, next), "data-testid": cellId(testId, row.id, channel.id) }) }, channel.id)))] }, row.id))) })] }));
15
+ };
16
+ const GroupRows = ({ group, channels, labels, isFrozen, valueOf, isPending, onToggle, testId }) => (_jsx(_Fragment, { children: group.rows.map((row) => {
17
+ var _a;
18
+ return (_jsx(SettingRow, { title: row.label, description: (_a = row.hint) !== null && _a !== void 0 ? _a : null, control: _jsx(Flex, { role: "group", "aria-label": row.label, wrap: "wrap", columnGap: 5, rowGap: 0, children: channels.map((channel) => (_jsx(SwitchCell, { row: row, channel: channel, labels: labels, isOn: isLockedCell(row, channel.id) || valueOf(row.id, channel.id), isLocked: isLockedCell(row, channel.id), isFrozen: isFrozen, isPending: isPending(row.id, channel.id), onToggle: (next) => onToggle(row, channel, next), "data-testid": cellId(testId, row.id, channel.id) }, channel.id))) }) }, row.id));
19
+ }) }));
20
+ const PreferenceGroups = (props) => (_jsx(_Fragment, { children: props.groups.map((group) => (_jsx(SettingsSection, { title: group.label, children: props.variant === "phone" ? _jsx(GroupRows, { ...props, group: group }) : _jsx(GroupTable, { ...props, group: group }) }, group.id))) }));
21
+ export default PreferenceGroups;
@@ -0,0 +1,24 @@
1
+ import { mergeDefined } from "../core/labels.js";
2
+ const DEFAULT_PREFERENCES_LABELS = {
3
+ title: "Notification settings",
4
+ subtitle: (appName) => (appName ? `Choose where ${appName} tells you about things.` : "Choose where you hear about things."),
5
+ email: "Email",
6
+ emailTiming: "Email timing",
7
+ emailHint: (appName) => appName ? `A summary skips anything you've already read in ${appName}.` : "A summary skips anything you've already read.",
8
+ typeColumn: "Notification type",
9
+ lockedTip: "Always on for this type",
10
+ asItHappens: "As it happens",
11
+ daily: "Daily at 9:00 am",
12
+ alwaysOn: "always on",
13
+ saving: "Saving…",
14
+ saved: "Saved",
15
+ autosave: "Changes save automatically",
16
+ saveFailed: (what) => `Couldn't save that change. ${what} was put back.`,
17
+ readOnlyBanner: "Your admin manages notification settings for your role. You can see them, but not change them.",
18
+ offlineBanner: "You're offline. Settings can't be changed until you reconnect.",
19
+ loading: "Loading settings",
20
+ errorTitle: "Couldn't load your settings",
21
+ errorDescription: "Nothing was changed. Try again in a moment.",
22
+ retry: "Try again",
23
+ };
24
+ export const resolvePreferencesLabels = (overrides) => mergeDefined(DEFAULT_PREFERENCES_LABELS, overrides);
@@ -0,0 +1,37 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ export const useAutosave = (saved, onFailed) => {
3
+ const [draft, setDraft] = useState({});
4
+ const [pendingKeys, setPendingKeys] = useState([]);
5
+ const [hasSaved, setHasSaved] = useState(false);
6
+ const inFlight = useRef(new Set());
7
+ useEffect(() => {
8
+ setDraft((current) => {
9
+ const kept = Object.keys(current).filter((key) => inFlight.current.has(key));
10
+ return kept.length === Object.keys(current).length ? current : Object.fromEntries(kept.map((key) => [key, current[key]]));
11
+ });
12
+ }, [saved]);
13
+ const commit = useCallback((key, next, save, what) => {
14
+ if (inFlight.current.has(key))
15
+ return;
16
+ inFlight.current.add(key);
17
+ setDraft((current) => ({ ...current, [key]: next }));
18
+ setPendingKeys(Array.from(inFlight.current));
19
+ const settle = () => {
20
+ inFlight.current.delete(key);
21
+ setPendingKeys(Array.from(inFlight.current));
22
+ };
23
+ save().then(() => {
24
+ setHasSaved(true);
25
+ settle();
26
+ }, () => {
27
+ setDraft((current) => {
28
+ const rest = { ...current };
29
+ delete rest[key];
30
+ return rest;
31
+ });
32
+ settle();
33
+ onFailed(what);
34
+ });
35
+ }, [onFailed]);
36
+ return { draft, pendingKeys, hasSaved, commit };
37
+ };
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useState } from "react";
3
+ import { chakra } from "@chakra-ui/react";
4
+ import { Archive, ArchiveRestore, ArrowLeft, ExternalLink, Hourglass, Lock, Mail, MailOpen } from "lucide-react";
5
+ import { Box, Flex, Heading, Icon, Text } from "../../Primitives/Primitives.js";
6
+ import Avatar from "../../Avatar/Avatar.js";
7
+ import Button from "../../Button/Button.js";
8
+ import DescriptionList from "../../DescriptionList/DescriptionList.js";
9
+ import EmptyState from "../../EmptyState/EmptyState.js";
10
+ import Tag from "../../Tag/Tag.js";
11
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
12
+ import { focusVisibleRing } from "../../../Theme/chakra/focusRing.styles.js";
13
+ import { ruleOn } from "../../../Theme/tokens/builders/planeStep.js";
14
+ import { BORDER_WIDTHS } from "../../../Theme/tokens/primitives/borderWidths.js";
15
+ import { RADIUS_ROLE } from "../../../Theme/tokens/primitives/radii.js";
16
+ import { useNotificationShared } from "../core/NotificationCenterProvider.js";
17
+ import { categoryFor } from "./category.js";
18
+ import { ReasonForm } from "./ReasonForm.js";
19
+ import { OfflineNote, RowActions } from "./RowActions.js";
20
+ import { RowStatus } from "./RowStatus.js";
21
+ import { ChangeChips, TitleText, useToneTile } from "./rowParts.js";
22
+ import { isDecision, visibleActionsFor } from "./rowMenu.js";
23
+ import { WAITING_AFTER_DAYS, iconSizeFor, testIdFor, touchTargetFor } from "./rowStyles.js";
24
+ /** One notification in full for a reading pane or a phone screen; text only, never markup. */
25
+ const NotificationDetail = ({ item, variant = "desktop", onBack, titleRef }) => {
26
+ var _a, _b, _c;
27
+ const { colors } = useCustomTheme();
28
+ const shared = useNotificationShared();
29
+ const { labels, format, now, categories, features, isOffline, appName, actions } = shared;
30
+ const { entry, state } = item;
31
+ const titleId = useId();
32
+ const [asking, setAsking] = useState(null);
33
+ const isPhone = variant === "phone";
34
+ const size = isPhone ? "md" : "sm";
35
+ const iconPx = iconSizeFor(size);
36
+ const category = categoryFor(categories, entry.category, labels.otherCategory);
37
+ const tile = useToneTile(category.tone);
38
+ const isUnread = entry.isRead === false;
39
+ const actionList = visibleActionsFor(entry, features);
40
+ const decides = actionList.some(isDecision);
41
+ const days = format.daysSince(entry.createdAt, now);
42
+ const isWaiting = state.zone === "needsYou" && decides && days >= WAITING_AFTER_DAYS;
43
+ const request = state.reasonRequest;
44
+ const askingAction = asking && asking.id === entry.id ? asking.action : undefined;
45
+ const requested = request ? actionList.find((action) => action.id === request.actionId) : undefined;
46
+ const formAction = state.status === "open" ? (askingAction !== null && askingAction !== void 0 ? askingAction : requested) : undefined;
47
+ const isQuiet = state.status === "open";
48
+ const plane = colors.surfaceRaised;
49
+ const closeForm = () => {
50
+ setAsking(null);
51
+ if (request)
52
+ actions.clearReasonRequest(entry.id);
53
+ };
54
+ const archiveButton = features.archive && state.zone === "activity" ? (_jsx(Button, { size: size, variant: entry.blocked ? "outline" : "ghost", colorScheme: "gray", leftIcon: _jsx(Archive, { size: iconPx, "aria-hidden": "true" }), label: labels.archive, isDisabled: isOffline, sx: touchTargetFor(size), "data-testid": testIdFor(entry, "archive"), onClick: () => actions.archive([entry.id]) })) : null;
55
+ return (_jsxs(chakra.article, { "aria-labelledby": titleId, display: "flex", flexDirection: "column", gap: 4, px: isPhone ? 4 : 8, pt: isPhone ? 2 : 6, pb: 8, maxW: "47.5rem", "data-testid": testIdFor(entry, "detail"), children: [isPhone && onBack && (_jsx(Box, { mx: -2, children: _jsx(Button, { size: "md", variant: "ghost", colorScheme: "gray", leftIcon: _jsx(ArrowLeft, { size: iconSizeFor("md"), "aria-hidden": "true" }), label: labels.back, sx: touchTargetFor("md"), onClick: onBack }) })), _jsxs(Box, { as: "header", display: "grid", gap: 1.5, children: [_jsxs(Flex, { wrap: "wrap", align: "center", gap: 1.5, fontSize: "xs", fontWeight: 500, color: colors.textMuted, children: [_jsx(Flex, { boxSize: "1.25rem", align: "center", justify: "center", borderRadius: RADIUS_ROLE.option, bg: tile.bg, color: tile.fg, "aria-hidden": "true", children: _jsx(Icon, { as: category.icon, boxSize: "0.75rem" }) }), _jsx("span", { children: category.label }), entry.eyebrow && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", children: "\u00B7" }), _jsx(chakra.span, { fontFamily: "mono", children: entry.eyebrow })] })), isUnread && _jsx(Tag, { size: "xs", colorScheme: "primary", label: labels.unread })] }), _jsx(Heading, { as: "h2", id: titleId, ref: titleRef, tabIndex: -1, fontSize: "lg", fontWeight: 500, lineHeight: "1.4", color: colors.text[800], borderRadius: "sm", sx: { textWrap: "balance", "&:focus-visible": focusVisibleRing(colors) }, children: _jsx(TitleText, { parts: entry.title, isUnread: true }) }), _jsxs(Flex, { wrap: "wrap", align: "center", gap: 2, fontSize: "xs-plus", color: colors.textMuted, children: [entry.actor && (_jsxs(_Fragment, { children: [_jsx(Avatar, { name: entry.actor.name, src: entry.actor.avatarUrl, size: "xs" }), _jsx("span", { children: entry.actor.name }), _jsx("span", { "aria-hidden": "true", children: "\u00B7" })] })), _jsx(chakra.time, { dateTime: format.iso(entry.createdAt), children: format.absolute(entry.createdAt, now) }), isWaiting && _jsx(Tag, { size: "xs", tone: "warning", leadingIcon: Hourglass, label: labels.waiting(days) })] })] }), entry.blocked ? (_jsx(Box, { borderRadius: RADIUS_ROLE.control, bg: colors.backgroundColor.subtle, children: _jsx(EmptyState, { icon: Lock, size: "sm", titleAs: "h3", title: labels.blockedTitle, description: entry.blocked, action: archiveButton }) })) : (_jsxs(_Fragment, { children: [entry.context && (_jsx(Text, { mt: -2, fontSize: "xs-plus", color: colors.textMuted, children: entry.context })), ((_a = entry.body) !== null && _a !== void 0 ? _a : entry.preview) && (_jsx(Text, { fontSize: "sm", lineHeight: "1.65", color: colors.text[800], maxW: "65ch", whiteSpace: "pre-line", children: (_b = entry.body) !== null && _b !== void 0 ? _b : entry.preview })), entry.change && _jsx(ChangeChips, { change: entry.change, labels: labels }), entry.fields && entry.fields.length > 0 && (_jsx(Box, { p: 4, borderRadius: RADIUS_ROLE.control, bg: colors.backgroundColor.subtle, children: _jsx(DescriptionList, { items: entry.fields.map(([label, value]) => ({ label, value })), layout: isPhone ? "stacked" : "inline", columns: { base: 1 } }) })), state.status !== "open" ? (_jsx(RowStatus, { item: item, shared: shared, size: size })) : formAction ? (_jsx(Box, { maxW: "32.5rem", children: _jsx(ReasonForm, { entry: entry, action: formAction, labels: labels, size: size, isOffline: isOffline, serverMessage: request ? ((_c = request.message) !== null && _c !== void 0 ? _c : labels.reasonRequiredMessage) : undefined, onCancel: closeForm, onSubmit: (reason) => {
56
+ setAsking(null);
57
+ actions.decide([entry.id], formAction, reason);
58
+ } }) })) : (actionList.length > 0 && (_jsxs(Box, { display: "grid", gap: 2, children: [_jsx(RowActions, { entry: entry, actionList: actionList, shared: shared, size: size, emphasis: "detail", onAsk: (action) => setAsking({ id: entry.id, action }) }), isOffline && decides && _jsx(OfflineNote, { text: labels.offlineMessage })] })))] })), _jsxs(Flex, { wrap: "wrap", gap: 2, pt: 4, borderTop: `${BORDER_WIDTHS.hairline} solid ${ruleOn(colors, plane)}`, children: [!entry.blocked && (_jsx(Button, { size: size, variant: decides ? "outline" : "solid", colorScheme: decides ? "gray" : "primary", rightIcon: _jsx(ExternalLink, { size: iconPx, "aria-hidden": "true" }), label: labels.openIn(appName), sx: touchTargetFor(size), "data-testid": testIdFor(entry, "open"), onClick: (event) => actions.open(entry, event) })), isQuiet && features.markRead && entry.isRead !== undefined && (_jsx(Button, { size: size, variant: "ghost", colorScheme: "gray", leftIcon: isUnread ? _jsx(MailOpen, { size: iconPx, "aria-hidden": "true" }) : _jsx(Mail, { size: iconPx, "aria-hidden": "true" }), label: isUnread ? labels.markRead : labels.markUnread, isDisabled: isOffline, sx: touchTargetFor(size), "data-testid": testIdFor(entry, "read"), onClick: () => actions.setRead([entry.id], isUnread) })), isQuiet && !entry.blocked && archiveButton, isQuiet && features.restore && state.zone === "archived" && (_jsx(Button, { size: size, variant: "ghost", colorScheme: "gray", leftIcon: _jsx(ArchiveRestore, { size: iconPx, "aria-hidden": "true" }), label: labels.restore, isDisabled: isOffline, sx: touchTargetFor(size), "data-testid": testIdFor(entry, "restore"), onClick: () => actions.restore([entry.id]) }))] })] }));
59
+ };
60
+ export default NotificationDetail;
@@ -0,0 +1,119 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { memo, useMemo, useState } from "react";
3
+ import { chakra } from "@chakra-ui/react";
4
+ import { ChevronDown, ChevronRight } from "lucide-react";
5
+ import { Box, Flex } from "../../Primitives/Primitives.js";
6
+ import Button from "../../Button/Button.js";
7
+ import Checkbox from "../../Checkbox/Checkbox.js";
8
+ import VisuallyHidden from "../../VisuallyHidden/VisuallyHidden.js";
9
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
10
+ import { listRowBackgrounds } from "../../../Theme/tokens/builders/rowStates.js";
11
+ import { insetFocusRingShadow } from "../../../Theme/chakra/focusRing.styles.js";
12
+ import { reducedMotionSx } from "../../../Theme/chakra/motion.js";
13
+ import { MOTION_MS, transitionFor } from "../../../Theme/tokens/primitives/motion.js";
14
+ import { RADIUS_ROLE } from "../../../Theme/tokens/primitives/radii.js";
15
+ import { useNotificationShared } from "../core/NotificationCenterProvider.js";
16
+ import { plainTitle } from "../core/grouping.js";
17
+ import { categoryFor } from "./category.js";
18
+ import { ReasonForm } from "./ReasonForm.js";
19
+ import { RowActions } from "./RowActions.js";
20
+ import { RowMoreMenu, RowQuickActions, QUICK_SELECTOR } from "./RowQuickActions.js";
21
+ import { RowStatus } from "./RowStatus.js";
22
+ import { ChangeChips, ROW_PLANE_VAR, RowContext, RowLeading, RowPreview, RowTime, TitleText } from "./rowParts.js";
23
+ import { quickActionsFor, rowScope, visibleActionsFor } from "./rowMenu.js";
24
+ import { CHECKBOX_TOUCH_TARGET_SX, ROW_BUTTON_SIZE, WAITING_AFTER_DAYS, iconSizeFor, testIdFor } from "./rowStyles.js";
25
+ const COLLAPSE = transitionFor(["grid-template-rows", "opacity"], "base");
26
+ const HIDE = `${COLLAPSE}, visibility ${MOTION_MS.base}ms step-end`;
27
+ const SHOW = `${COLLAPSE}, visibility ${MOTION_MS.base}ms step-start`;
28
+ const isPlainClick = (event) => event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
29
+ /** One notification as a list row: lead avatar or tile, title, context, decisions with undo, and hover actions. */
30
+ const NotificationRow = ({ item, variant = "panel", isSelected = false, isChecked = false, onCheckedChange, isThreadChild = false, thread, isThreadOpen = false, onThreadToggle, initialReasonActionId, onOpen, linkRef, }) => {
31
+ var _a;
32
+ const { colors } = useCustomTheme();
33
+ const shared = useNotificationShared();
34
+ const { labels, format, now, categories, features, isOffline, actions } = shared;
35
+ const { entry, state } = item;
36
+ const [askingId, setAskingId] = useState(initialReasonActionId);
37
+ const rows = useMemo(() => listRowBackgrounds(colors), [colors]);
38
+ const size = ROW_BUTTON_SIZE[variant];
39
+ const isPhone = variant === "phone";
40
+ const isUnread = entry.isRead === false;
41
+ const isHidden = state.isHidden;
42
+ const category = categoryFor(categories, entry.category, labels.otherCategory);
43
+ const title = plainTitle(entry.title);
44
+ const actionList = visibleActionsFor(entry, features);
45
+ const days = format.daysSince(entry.createdAt, now);
46
+ const hasDecision = actionList.some((action) => action.href === undefined);
47
+ const waitingDays = state.zone === "needsYou" && hasDecision && days >= WAITING_AFTER_DAYS ? days : null;
48
+ const request = state.reasonRequest;
49
+ const formActionId = askingId !== undefined ? askingId : request ? request.actionId : undefined;
50
+ const formAction = state.status === "open" && formActionId !== undefined
51
+ ? actionList.find((action) => action.id === formActionId)
52
+ : undefined;
53
+ const isQuiet = state.status === "open" && formAction === undefined;
54
+ const quick = isQuiet ? quickActionsFor(item, rowScope(item, thread, isThreadOpen), shared) : [];
55
+ const rest = thread ? thread.rest.length : 0;
56
+ const resting = isSelected || isChecked ? rows.selected : colors.surfaceRaised;
57
+ const hover = isSelected || isChecked ? rows.selectedActive : rows.active;
58
+ const open = (event) => {
59
+ if (onOpen)
60
+ onOpen(item, event);
61
+ else
62
+ actions.open(entry, event);
63
+ };
64
+ const onLinkClick = (event) => {
65
+ if (entry.href !== undefined) {
66
+ if (!isPlainClick(event))
67
+ return;
68
+ event.preventDefault();
69
+ }
70
+ open(event);
71
+ };
72
+ const closeForm = () => {
73
+ setAskingId(undefined);
74
+ if (request)
75
+ actions.clearReasonRequest(entry.id);
76
+ };
77
+ const sendForm = (action, reason) => {
78
+ setAskingId(undefined);
79
+ actions.decide([entry.id], action, reason);
80
+ };
81
+ const linkSx = {
82
+ flex: 1,
83
+ minW: 0,
84
+ textAlign: "left",
85
+ fontSize: "sm",
86
+ lineHeight: "1.43",
87
+ color: "inherit",
88
+ textDecoration: "none",
89
+ "&::after": {
90
+ content: '""',
91
+ position: "absolute",
92
+ inset: 0,
93
+ zIndex: 1,
94
+ borderRadius: RADIUS_ROLE.option,
95
+ },
96
+ "&:focus-visible": { outline: "none" },
97
+ "&:focus-visible::after": {
98
+ boxShadow: insetFocusRingShadow(colors),
99
+ outline: "2px solid transparent",
100
+ outlineOffset: "-2px",
101
+ },
102
+ };
103
+ const linkBody = (_jsxs(_Fragment, { children: [isUnread && _jsx(VisuallyHidden, { children: `${labels.unread}. ` }), entry.eyebrow && (_jsxs(chakra.span, { fontFamily: "mono", fontSize: "xs", fontWeight: 500, color: colors.textMuted, children: [entry.eyebrow, " "] })), _jsx(TitleText, { parts: entry.title, isUnread: isUnread })] }));
104
+ return (_jsx(chakra.li, { listStyleType: "none", display: "grid", gridTemplateRows: isHidden ? "0fr" : "1fr", opacity: isHidden ? 0 : 1, visibility: isHidden ? "hidden" : "visible", "aria-hidden": isHidden ? true : undefined, "data-testid": entry.testId, "data-notification-id": entry.id, "data-status": state.status, sx: {
105
+ transition: isHidden ? HIDE : SHOW,
106
+ contentVisibility: "auto",
107
+ containIntrinsicSize: "auto 4.5rem",
108
+ ...reducedMotionSx(),
109
+ }, children: _jsx(Box, { minH: 0, overflow: "hidden", children: _jsxs(Flex, { position: "relative", align: "flex-start", gap: variant === "inbox" ? 2.5 : 3, pt: isPhone ? 3 : 2.5, pb: isPhone ? 3 : 2.5, pr: isPhone ? 2 : 3, pl: isThreadChild ? 16 : 5, borderRadius: RADIUS_ROLE.option, bg: resting, sx: {
110
+ [ROW_PLANE_VAR]: resting,
111
+ transition: transitionFor(["background-color"]),
112
+ "&:hover": { bg: hover, [ROW_PLANE_VAR]: hover },
113
+ [`&:hover ${QUICK_SELECTOR}, &:focus-within ${QUICK_SELECTOR}`]: { opacity: 1 },
114
+ ...reducedMotionSx(),
115
+ }, children: [isSelected && (_jsx(Box, { position: "absolute", left: 0, top: 2, bottom: 2, w: "0.1875rem", borderRightRadius: "sm", bg: colors.primary[500], "aria-hidden": "true" })), isUnread && (_jsx(Box, { position: "absolute", left: isThreadChild ? "3.125rem" : "0.4375rem", top: isThreadChild ? "0.875rem" : isPhone ? "1.125rem" : "1rem", boxSize: "0.5rem", borderRadius: "full", bg: colors.accentText, "aria-hidden": "true", "data-testid": testIdFor(entry, "dot") })), onCheckedChange && (_jsx(Box, { position: "relative", zIndex: 2, mt: 1, sx: CHECKBOX_TOUCH_TARGET_SX, children: _jsx(Checkbox, { isChecked: isChecked, "aria-label": labels.select(title), onChange: (event) => onCheckedChange(item, event.target.checked) }) })), _jsx(RowLeading, { entry: entry, category: category, size: isThreadChild ? "xs" : isPhone ? "md" : "sm" }), _jsxs(Box, { flex: 1, minW: 0, children: [_jsxs(Flex, { align: "baseline", gap: 2, children: [entry.href !== undefined ? (_jsx(chakra.a, { ref: linkRef, href: entry.href, "aria-current": isSelected ? "true" : undefined, "data-testid": testIdFor(entry, "link"), onClick: onLinkClick, sx: linkSx, children: linkBody })) : (_jsx(chakra.button, { ref: linkRef, type: "button", "aria-current": isSelected ? "true" : undefined, "data-testid": testIdFor(entry, "link"), onClick: onLinkClick, sx: linkSx, children: linkBody })), _jsx(RowTime, { at: entry.createdAt, now: now, format: format })] }), _jsx(RowContext, { entry: entry, waitingDays: waitingDays, labels: labels }), entry.preview && _jsx(RowPreview, { text: entry.preview, isLarge: isPhone }), entry.change && (_jsx(Box, { mt: 1.5, children: _jsx(ChangeChips, { change: entry.change, labels: labels }) })), formAction ? (_jsx(ReasonForm, { entry: entry, action: formAction, labels: labels, size: size, isOffline: isOffline, serverMessage: request ? ((_a = request.message) !== null && _a !== void 0 ? _a : labels.reasonRequiredMessage) : undefined, onCancel: closeForm, onSubmit: (reason) => sendForm(formAction, reason) })) : (state.status === "open" &&
116
+ actionList.length > 0 && (_jsx(RowActions, { entry: entry, actionList: actionList, shared: shared, size: size, emphasis: "row", onAsk: (action) => setAskingId(action.id) }))), _jsx(RowStatus, { item: item, shared: shared, size: size }), thread && rest > 0 && onThreadToggle && (_jsx(Box, { position: "relative", zIndex: 2, mt: 2, children: _jsx(Button, { size: "xs", variant: "link", leftIcon: isThreadOpen ? (_jsx(ChevronDown, { size: iconSizeFor("xs"), "aria-hidden": "true" })) : (_jsx(ChevronRight, { size: iconSizeFor("xs"), "aria-hidden": "true" })), label: isThreadOpen ? labels.threadHide(rest) : labels.threadShow(rest), "aria-expanded": isThreadOpen, "data-testid": testIdFor(entry, "thread"), onClick: () => onThreadToggle(thread) }) }))] }), quick.length > 0 &&
117
+ (isPhone ? (_jsx(RowMoreMenu, { entry: entry, items: quick, isOffline: isOffline, label: labels.moreActions(title) })) : (_jsx(RowQuickActions, { entry: entry, items: quick, isOffline: isOffline })))] }) }) }));
118
+ };
119
+ export default memo(NotificationRow);
@@ -0,0 +1,52 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef, useState } from "react";
3
+ import { chakra } from "@chakra-ui/react";
4
+ import { CircleAlert } from "lucide-react";
5
+ import { Flex, Icon } from "../../Primitives/Primitives.js";
6
+ import Button from "../../Button/Button.js";
7
+ import InputTextArea from "../../InputTextArea/InputTextArea.js";
8
+ import Kbd from "../../Kbd/Kbd.js";
9
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
10
+ import { touchTargetFor } from "./rowStyles.js";
11
+ export const ReasonForm = ({ entry, action, labels, size, isOffline, serverMessage, onCancel, onSubmit, }) => {
12
+ var _a;
13
+ const { colors } = useCustomTheme();
14
+ const id = useId();
15
+ const hintId = `${id}-hint`;
16
+ const field = useRef(null);
17
+ const [text, setText] = useState("");
18
+ const [touched, setTouched] = useState(false);
19
+ const isServerRequest = serverMessage !== undefined;
20
+ const isRequired = action.reason === "required" || isServerRequest;
21
+ const isNegative = action.intent === "negative";
22
+ const isMissing = isRequired && !text.trim();
23
+ const missingMessage = isNegative ? labels.reasonMissingNegative : labels.reasonMissing;
24
+ const errorText = touched && isMissing ? missingMessage : isServerRequest && !text.trim() ? serverMessage : undefined;
25
+ useEffect(() => {
26
+ if (field.current)
27
+ field.current.focus();
28
+ }, []);
29
+ const submit = () => {
30
+ setTouched(true);
31
+ if (isMissing) {
32
+ if (field.current)
33
+ field.current.focus();
34
+ return;
35
+ }
36
+ onSubmit(text.trim());
37
+ };
38
+ const onKeyDown = (event) => {
39
+ if (event.key === "Escape") {
40
+ event.stopPropagation();
41
+ onCancel();
42
+ }
43
+ else if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
44
+ event.preventDefault();
45
+ submit();
46
+ }
47
+ };
48
+ return (_jsxs(chakra.form, { noValidate: true, position: "relative", zIndex: 2, display: "grid", gap: 1.5, mt: 2.5, onSubmit: (event) => {
49
+ event.preventDefault();
50
+ submit();
51
+ }, children: [_jsxs(chakra.label, { htmlFor: id, fontSize: "xs-plus", fontWeight: 500, color: colors.text[700], children: [(_a = action.reasonLabel) !== null && _a !== void 0 ? _a : labels.reasonLabel, !isRequired && (_jsxs(chakra.span, { fontWeight: 400, color: colors.textMuted, children: [" ", labels.reasonOptional] }))] }), _jsx(InputTextArea, { ref: field, id: id, rows: 2, size: size === "md" ? "md" : "sm", resize: "vertical", value: text, error: errorText !== undefined, "aria-describedby": hintId, placeholder: isNegative ? labels.reasonPlaceholder : undefined, onChange: (event) => setText(event.target.value), onKeyDown: onKeyDown }), _jsx(Flex, { id: hintId, align: "center", wrap: "wrap", gap: 1, fontSize: "xs", fontWeight: errorText !== undefined ? 500 : 400, color: errorText !== undefined ? colors.semanticText.error : colors.textMuted, children: errorText !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Icon, { as: CircleAlert, boxSize: "0.75rem", "aria-hidden": "true" }), errorText] })) : (_jsxs(_Fragment, { children: [_jsx(Kbd, { size: "xs", children: labels.keyCtrl }), _jsx(Kbd, { size: "xs", children: labels.keyEnter }), _jsxs("span", { children: [labels.toSend, " \u00B7"] }), _jsx(Kbd, { size: "xs", children: labels.keyEsc }), _jsx("span", { children: labels.toCancel })] })) }), _jsxs(Flex, { justify: "flex-end", gap: 2, children: [_jsx(Button, { size: size, variant: "ghost", colorScheme: "gray", label: labels.cancel, sx: touchTargetFor(size), onClick: onCancel }), _jsx(Button, { type: "submit", size: size, variant: "solid", colorScheme: isNegative ? "red" : "primary", label: action.label, "aria-label": labels.actionName(action.label, entry.subject), isDisabled: isOffline, sx: touchTargetFor(size), "data-testid": entry.testId ? `${entry.testId}-${action.id}-send` : undefined })] })] }));
52
+ };
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Check, ChevronRight, WifiOff } from "lucide-react";
3
+ import { Flex, Icon } from "../../Primitives/Primitives.js";
4
+ import Button from "../../Button/Button.js";
5
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
6
+ import { isDecision } from "./rowMenu.js";
7
+ import { iconSizeFor, testIdFor, touchTargetFor } from "./rowStyles.js";
8
+ export const OfflineNote = ({ text }) => {
9
+ const { colors } = useCustomTheme();
10
+ return (_jsxs(Flex, { as: "span", display: "inline-flex", align: "center", gap: 1, fontSize: "xs", color: colors.textMuted, children: [_jsx(Icon, { as: WifiOff, boxSize: "0.75rem", "aria-hidden": "true" }), text] }));
11
+ };
12
+ export const RowActions = ({ entry, actionList, shared, size, emphasis, onAsk }) => {
13
+ const { labels, actions, isOffline } = shared;
14
+ const iconPx = iconSizeFor(size);
15
+ return (_jsxs(Flex, { position: "relative", zIndex: 2, wrap: "wrap", align: "center", gap: 2, mt: emphasis === "row" ? 2.5 : 0, children: [actionList.map((action) => {
16
+ const isLink = !isDecision(action);
17
+ const isStrong = emphasis === "detail" && (action.intent === "positive" || action.intent === "primary");
18
+ return (_jsx(Button, { size: size, variant: isStrong ? "solid" : "outline", colorScheme: isStrong ? "primary" : "gray", label: action.label, "aria-label": labels.actionName(action.label, entry.subject), leftIcon: !isLink && action.intent === "positive" ? _jsx(Check, { size: iconPx, "aria-hidden": "true" }) : undefined, rightIcon: isLink ? _jsx(ChevronRight, { size: iconPx, "aria-hidden": "true" }) : undefined, isDisabled: isOffline && !isLink, sx: touchTargetFor(size), "data-testid": testIdFor(entry, action.id), onClick: (event) => {
19
+ event.stopPropagation();
20
+ if (isLink)
21
+ actions.open(entry, event, action);
22
+ else if (action.reason)
23
+ onAsk(action);
24
+ else
25
+ actions.decide([entry.id], action);
26
+ } }, action.id));
27
+ }), isOffline && actionList.some(isDecision) && _jsx(OfflineNote, { text: labels.offlineNote })] }));
28
+ };
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Ellipsis } from "lucide-react";
3
+ import { Flex, Icon } from "../../Primitives/Primitives.js";
4
+ import IconButton from "../../IconButton/IconButton.js";
5
+ import ToolTip from "../../ToolTip/ToolTip.js";
6
+ import Dropdown from "../../Dropdown/DropDown.js";
7
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
8
+ import { BORDER_WIDTHS } from "../../../Theme/tokens/primitives/borderWidths.js";
9
+ import { RADIUS_ROLE } from "../../../Theme/tokens/primitives/radii.js";
10
+ import { transitionFor } from "../../../Theme/tokens/primitives/motion.js";
11
+ import { reducedMotionSx } from "../../../Theme/chakra/motion.js";
12
+ import { TOUCH_TARGET_SX, iconSizeFor, testIdFor } from "./rowStyles.js";
13
+ export const QUICK_SELECTOR = "[data-nc-quick]";
14
+ export const RowQuickActions = ({ entry, items, isOffline }) => {
15
+ const { colors } = useCustomTheme();
16
+ return (_jsx(Flex, { "data-nc-quick": "", position: "absolute", top: "0.375rem", right: "0.375rem", zIndex: 2, gap: "0.125rem", p: "0.125rem", borderRadius: RADIUS_ROLE.control, bg: colors.surfaceRaised, border: `${BORDER_WIDTHS.hairline} solid ${colors.boxborder[200]}`, boxShadow: "sm", opacity: 0, sx: {
17
+ transition: transitionFor(["opacity"]),
18
+ "@media (hover: none)": {
19
+ position: "relative",
20
+ top: "auto",
21
+ right: "auto",
22
+ opacity: 1,
23
+ alignSelf: "flex-start",
24
+ border: "none",
25
+ boxShadow: "none",
26
+ bg: "transparent",
27
+ },
28
+ ...reducedMotionSx(),
29
+ }, children: items.map((item) => (_jsx(ToolTip, { label: item.label, children: _jsx(IconButton, { size: "xs", variant: "ghost", colorScheme: "gray", "aria-label": item.label, icon: _jsx(Icon, { as: item.icon, boxSize: `${iconSizeFor("xs") / 16}rem`, "aria-hidden": "true" }), isDisabled: isOffline, "data-testid": testIdFor(entry, item.id), onClick: (event) => {
30
+ event.stopPropagation();
31
+ item.run();
32
+ } }) }, item.id))) }));
33
+ };
34
+ export const RowMoreMenu = ({ entry, items, isOffline, label }) => {
35
+ const trigger = {
36
+ variant: "ghost",
37
+ colorScheme: "gray",
38
+ px: 0,
39
+ minW: "2.5rem",
40
+ isDisabled: isOffline,
41
+ sx: TOUCH_TARGET_SX,
42
+ "data-testid": testIdFor(entry, "more"),
43
+ };
44
+ return (_jsx(Flex, { position: "relative", zIndex: 2, flexShrink: 0, mt: "-0.5rem", mr: "-0.5rem", children: _jsx(Dropdown, { size: "md", options: items.map((item) => ({ id: item.id, label: item.label, image: item.icon })), handleOptionSelect: (id) => {
45
+ const hit = items.find((item) => item.id === id);
46
+ if (hit)
47
+ hit.run();
48
+ }, DropdownIcon: _jsx(Ellipsis, { size: iconSizeFor("md"), "aria-hidden": "true" }), isVisibleIconShow: false, ariaLabel: label, buttonProps: trigger }) }));
49
+ };
@@ -0,0 +1,35 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { chakra } from "@chakra-ui/react";
3
+ import { CircleAlert, CircleCheck, CircleX, Info, Lock, RefreshCw } from "lucide-react";
4
+ import { Flex, Icon } from "../../Primitives/Primitives.js";
5
+ import Button from "../../Button/Button.js";
6
+ import Spinner from "../../Spinner/Spinner.js";
7
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
8
+ import { resolveSoftTone, toneScale } from "../../../Theme/tokens/builders/tone.js";
9
+ import { RADIUS_ROLE } from "../../../Theme/tokens/primitives/radii.js";
10
+ import { iconSizeFor, testIdFor, touchTargetFor } from "./rowStyles.js";
11
+ const StatusLine = ({ tone, icon, role, children }) => {
12
+ const { colors } = useCustomTheme();
13
+ const soft = resolveSoftTone(colors, toneScale(colors, tone), colors.surfaceRaised);
14
+ return (_jsxs(Flex, { role: role, position: "relative", zIndex: 2, wrap: "wrap", align: "center", columnGap: 2, rowGap: 1.5, mt: 2.5, px: 2.5, py: 1.5, borderRadius: RADIUS_ROLE.control, bg: soft.bg, color: soft.fg, fontSize: "xs-plus", lineHeight: "1.45", onClick: (event) => event.stopPropagation(), children: [icon, children] }));
15
+ };
16
+ const glyph = (as) => _jsx(Icon, { as: as, boxSize: "0.875rem", flexShrink: 0, "aria-hidden": "true" });
17
+ const LINK_SX = { color: "inherit", fontWeight: 600 };
18
+ export const RowStatus = ({ item, shared, size }) => {
19
+ const { entry, state } = item;
20
+ const { labels, actions, isOffline } = shared;
21
+ const { decision, error } = state;
22
+ if (!decision || state.status === "open")
23
+ return null;
24
+ if (state.status === "failed") {
25
+ return (_jsxs(StatusLine, { tone: "error", icon: glyph(CircleAlert), role: "alert", children: [_jsxs(chakra.span, { fontWeight: 500, children: [_jsx(chakra.strong, { fontWeight: 600, children: labels.couldNot(decision.label) }), " ", error && error.message ? error.message : labels.failedMessage] }), _jsx(Button, { size: size, variant: "outline", colorScheme: "gray", leftIcon: _jsx(RefreshCw, { size: iconSizeFor(size), "aria-hidden": "true" }), label: labels.retry, "aria-label": labels.actionName(labels.retry, entry.subject), isDisabled: isOffline, ml: "auto", sx: touchTargetFor(size), "data-testid": testIdFor(entry, "retry"), onClick: () => actions.retry(entry.id) })] }));
26
+ }
27
+ if (state.status === "denied") {
28
+ const isForbidden = error !== undefined && error.kind === "forbidden";
29
+ const fallback = isForbidden ? labels.forbiddenMessage : labels.conflictMessage;
30
+ return (_jsxs(StatusLine, { tone: "info", icon: glyph(isForbidden ? Lock : Info), children: [_jsx(chakra.span, { fontWeight: 500, children: error && error.message ? error.message : fallback }), _jsx(Button, { size: "xs", variant: "link", label: labels.dismiss, "aria-label": labels.actionName(labels.dismiss, entry.subject), sx: LINK_SX, "data-testid": testIdFor(entry, "dismiss"), onClick: () => actions.dismiss(entry.id) })] }));
31
+ }
32
+ const isNegative = decision.intent === "negative";
33
+ const isSending = state.status === "sending";
34
+ return (_jsxs(StatusLine, { tone: isNegative ? "neutral" : "success", icon: isSending ? _jsx(Spinner, { size: "xs", color: "currentColor", "aria-hidden": "true" }) : glyph(isNegative ? CircleX : CircleCheck), children: [_jsx(chakra.span, { fontWeight: 500, children: isSending ? labels.sending : decision.doneLabel }), state.status === "undo" && (_jsx(Button, { size: "xs", variant: "link", label: labels.undo, "aria-label": labels.actionName(labels.undo, entry.subject), sx: LINK_SX, "data-testid": testIdFor(entry, "undo"), onClick: () => actions.undo(entry.id) })), decision.reason && (_jsx(chakra.span, { flexBasis: "100%", pl: "1.375rem", fontStyle: "italic", opacity: 0.85, children: labels.reasonQuote(decision.reason) }))] }));
35
+ };
@@ -0,0 +1,9 @@
1
+ import { Bell } from "lucide-react";
2
+ import { findCategory } from "../core/grouping.js";
3
+ export const categoryFor = (categories, key, otherLabel) => {
4
+ var _a;
5
+ const category = findCategory(categories, key);
6
+ if (!category)
7
+ return { label: otherLabel, icon: Bell, tone: "neutral" };
8
+ return { label: category.label, icon: category.icon, tone: (_a = category.tone) !== null && _a !== void 0 ? _a : "neutral" };
9
+ };
@@ -0,0 +1,30 @@
1
+ import { Archive, ArchiveRestore, Mail, MailOpen } from "lucide-react";
2
+ import { threadEntries } from "../core/grouping.js";
3
+ export const rowScope = (item, thread, isThreadOpen) => (thread && !isThreadOpen ? threadEntries(thread) : [item]);
4
+ export const quickActionsFor = (item, scope, shared) => {
5
+ const { features, labels, actions } = shared;
6
+ const { entry, state } = item;
7
+ const ids = scope.map((member) => member.entry.id);
8
+ const list = [];
9
+ if (features.markRead && entry.isRead !== undefined) {
10
+ const unread = scope.some((member) => member.entry.isRead === false);
11
+ list.push({
12
+ id: "read",
13
+ label: unread ? labels.markRead : labels.markUnread,
14
+ icon: unread ? MailOpen : Mail,
15
+ run: () => actions.setRead(ids, unread),
16
+ });
17
+ }
18
+ if (features.archive && state.zone === "activity") {
19
+ list.push({ id: "archive", label: labels.archive, icon: Archive, run: () => actions.archive(ids) });
20
+ }
21
+ if (features.restore && state.zone === "archived") {
22
+ list.push({ id: "restore", label: labels.restore, icon: ArchiveRestore, run: () => actions.restore(ids) });
23
+ }
24
+ actions.extraActionsFor(entry).forEach((extra) => {
25
+ list.push({ id: extra.id, label: extra.label, icon: extra.icon, run: () => actions.runExtraAction(entry, extra) });
26
+ });
27
+ return list;
28
+ };
29
+ export const visibleActionsFor = (entry, features) => { var _a; return ((_a = entry.actions) !== null && _a !== void 0 ? _a : []).filter((action) => action.href !== undefined || features.decide); };
30
+ export const isDecision = (action) => action.href === undefined;
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment } from "react";
3
+ import { chakra } from "@chakra-ui/react";
4
+ import { ChevronRight, Hourglass, Lock } from "lucide-react";
5
+ import { Box, Flex, Icon, Text } from "../../Primitives/Primitives.js";
6
+ import Avatar from "../../Avatar/Avatar.js";
7
+ import Tag from "../../Tag/Tag.js";
8
+ import VisuallyHidden from "../../VisuallyHidden/VisuallyHidden.js";
9
+ import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
10
+ import { isDarkCanvas } from "../../../Theme/tokens/builders/accentText.js";
11
+ import { resolveStatusTone } from "../../../Theme/tokens/builders/statusTone.js";
12
+ import { toneScale } from "../../../Theme/tokens/builders/tone.js";
13
+ import { ruleOn } from "../../../Theme/tokens/builders/planeStep.js";
14
+ import { BORDER_WIDTHS } from "../../../Theme/tokens/primitives/borderWidths.js";
15
+ import { RADIUS_ROLE } from "../../../Theme/tokens/primitives/radii.js";
16
+ import { NUMERIC_TEXT } from "../../../Theme/tokens/primitives/typography.js";
17
+ export const ROW_PLANE_VAR = "--nc-row-bg";
18
+ export const TitleText = ({ parts, isUnread }) => {
19
+ const { colors } = useCustomTheme();
20
+ return (_jsx(chakra.span, { color: isUnread ? colors.text[800] : colors.text[700], children: parts.map((part, index) => typeof part === "string" ? (_jsx(Fragment, { children: part }, index)) : (_jsx(chakra.strong, { fontWeight: isUnread ? 600 : 500, color: isUnread ? colors.header[900] : colors.text[800], children: part.b }, index))) }));
21
+ };
22
+ const LEAD_BOX = {
23
+ "2xs": "1rem",
24
+ xs: "1.5rem",
25
+ sm: "2rem",
26
+ md: "2.5rem",
27
+ lg: "3rem",
28
+ xl: "4rem",
29
+ };
30
+ export const useToneTile = (tone) => {
31
+ const { colors } = useCustomTheme();
32
+ return resolveStatusTone(toneScale(colors, tone), isDarkCanvas(colors.backgroundColor.main) ? "dark" : "light");
33
+ };
34
+ export const RowLeading = ({ entry, category, size }) => {
35
+ const tile = useToneTile(category.tone);
36
+ const iconPx = size === "md" || size === "lg" || size === "xl" ? 18 : 16;
37
+ if (entry.actor) {
38
+ return (_jsxs(Box, { position: "relative", flexShrink: 0, boxSize: LEAD_BOX[size], "aria-hidden": "true", children: [_jsx(Avatar, { name: entry.actor.name, src: entry.actor.avatarUrl, size: size }), _jsx(Flex, { position: "absolute", right: "-0.375rem", bottom: "-0.375rem", boxSize: "1.25rem", align: "center", justify: "center", borderRadius: "full", bg: tile.bg, color: tile.fg, border: `${BORDER_WIDTHS.thick} solid var(${ROW_PLANE_VAR})`, children: _jsx(Icon, { as: category.icon, boxSize: "0.625rem", strokeWidth: 2.5 }) })] }));
39
+ }
40
+ return (_jsx(Flex, { flexShrink: 0, boxSize: LEAD_BOX[size], align: "center", justify: "center", borderRadius: RADIUS_ROLE.media, bg: tile.bg, color: tile.fg, "aria-hidden": "true", children: _jsx(Icon, { as: category.icon, boxSize: `${iconPx / 16}rem` }) }));
41
+ };
42
+ export const RowTime = ({ at, now, format }) => {
43
+ const { colors } = useCustomTheme();
44
+ return (_jsxs(chakra.time, { dateTime: format.iso(at), title: format.absolute(at, now), flexShrink: 0, fontSize: "xs", color: colors.textMuted, whiteSpace: "nowrap", sx: NUMERIC_TEXT, children: [_jsx("span", { "aria-hidden": "true", children: format.shortAge(at, now) }), _jsx(VisuallyHidden, { children: format.longAge(at, now) })] }));
45
+ };
46
+ export const RowContext = ({ entry, waitingDays, labels }) => {
47
+ const { colors } = useCustomTheme();
48
+ if (waitingDays === null && !entry.blocked && !entry.context)
49
+ return null;
50
+ return (_jsxs(Flex, { wrap: "wrap", align: "center", columnGap: 2, rowGap: 1, mt: 1, fontSize: "xs", lineHeight: "1.5", color: colors.textMuted, children: [waitingDays !== null && _jsx(Tag, { size: "xs", tone: "warning", leadingIcon: Hourglass, label: labels.waiting(waitingDays) }), entry.blocked && _jsx(Tag, { size: "xs", tone: "neutral", leadingIcon: Lock, label: labels.noAccess }), entry.context && _jsx(chakra.span, { minW: 0, children: entry.context })] }));
51
+ };
52
+ export const RowPreview = ({ text, isLarge }) => {
53
+ const { colors } = useCustomTheme();
54
+ return (_jsx(Text, { mt: 1.5, pl: 2.5, borderLeft: `${BORDER_WIDTHS.thick} solid ${ruleOn(colors, colors.surfaceRaised)}`, fontSize: isLarge ? "sm" : "xs-plus", lineHeight: "1.5", color: colors.text[700], noOfLines: 2, children: text }));
55
+ };
56
+ export const ChangeChips = ({ change, labels }) => {
57
+ const { colors } = useCustomTheme();
58
+ return (_jsxs(Flex, { align: "center", wrap: "wrap", gap: 1.5, color: colors.textMuted, children: [_jsx(Tag, { size: "xs", tone: "neutral", label: change.from }), _jsx(Icon, { as: ChevronRight, boxSize: "0.75rem", "aria-hidden": "true" }), _jsx(VisuallyHidden, { children: labels.changedTo }), _jsx(Tag, { size: "xs", colorScheme: "primary", label: change.to })] }));
59
+ };