@neasg/design-system 0.4.35 → 0.4.113

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 (76) hide show
  1. package/README.md +9 -0
  2. package/dist/activity-card.d.ts +31 -0
  3. package/dist/activity-card.js +65 -0
  4. package/dist/alert.d.ts +1 -1
  5. package/dist/autocomplete.d.ts +84 -0
  6. package/dist/autocomplete.js +267 -0
  7. package/dist/avatar.d.ts +3 -1
  8. package/dist/avatar.js +27 -10
  9. package/dist/badge.d.ts +1 -1
  10. package/dist/badge.js +1 -1
  11. package/dist/button.d.ts +2 -2
  12. package/dist/button.js +3 -1
  13. package/dist/calendar.js +1 -1
  14. package/dist/card.d.ts +1 -1
  15. package/dist/card.js +1 -1
  16. package/dist/changelog-action-panel.d.ts +17 -0
  17. package/dist/changelog-action-panel.js +14 -0
  18. package/dist/checklist.d.ts +63 -0
  19. package/dist/checklist.js +30 -0
  20. package/dist/compound-search-input.d.ts +10 -0
  21. package/dist/compound-search-input.js +19 -0
  22. package/dist/date-input.js +3 -3
  23. package/dist/dialog.d.ts +4 -1
  24. package/dist/dialog.js +7 -2
  25. package/dist/draggable-tabs.js +9 -8
  26. package/dist/dropdown-button.js +1 -0
  27. package/dist/editable-table.js +2 -2
  28. package/dist/field-counter.d.ts +7 -0
  29. package/dist/field-counter.js +5 -0
  30. package/dist/index.d.ts +22 -4
  31. package/dist/index.js +12 -2
  32. package/dist/input.d.ts +2 -0
  33. package/dist/input.js +11 -3
  34. package/dist/label-value-grid.js +6 -4
  35. package/dist/layout.d.ts +39 -6
  36. package/dist/layout.js +118 -42
  37. package/dist/lib/utils.js +1 -0
  38. package/dist/message-action-panel.d.ts +12 -0
  39. package/dist/message-action-panel.js +11 -0
  40. package/dist/message-item.d.ts +23 -3
  41. package/dist/message-item.js +115 -16
  42. package/dist/notification.d.ts +14 -3
  43. package/dist/notification.js +36 -7
  44. package/dist/page-section.d.ts +2 -1
  45. package/dist/page-section.js +2 -2
  46. package/dist/popover-menu.d.ts +4 -2
  47. package/dist/popover-menu.js +65 -5
  48. package/dist/popover.d.ts +10 -2
  49. package/dist/popover.js +7 -3
  50. package/dist/rich-text-editor.d.ts +2 -1
  51. package/dist/rich-text-editor.js +29 -6
  52. package/dist/routing-action-panel.d.ts +38 -0
  53. package/dist/routing-action-panel.js +19 -0
  54. package/dist/routing-timeline.d.ts +7 -2
  55. package/dist/routing-timeline.js +67 -29
  56. package/dist/search-input.d.ts +6 -0
  57. package/dist/search-input.js +6 -4
  58. package/dist/select-primitive.js +1 -1
  59. package/dist/select.d.ts +3 -1
  60. package/dist/select.js +9 -4
  61. package/dist/status-dot.d.ts +13 -0
  62. package/dist/status-dot.js +19 -0
  63. package/dist/styles.css +0 -19
  64. package/dist/table-add-row.d.ts +8 -0
  65. package/dist/table-add-row.js +7 -0
  66. package/dist/table-column-filter.js +7 -5
  67. package/dist/table.d.ts +9 -1
  68. package/dist/table.js +25 -11
  69. package/dist/tabs.d.ts +1 -1
  70. package/dist/tabs.js +6 -4
  71. package/dist/tailwind-preset.js +10 -0
  72. package/dist/textarea.d.ts +5 -0
  73. package/dist/textarea.js +18 -3
  74. package/dist/theme-switcher.d.ts +3 -1
  75. package/dist/theme-switcher.js +8 -2
  76. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { FileText } from "lucide-react";
4
+ import { FileText, Laptop, Paperclip } from "lucide-react";
5
5
  import { DownloadIcon } from "./animated-icons";
6
- import { AvatarProfile } from "./avatar";
6
+ import { Avatar, AvatarProfile } from "./avatar";
7
7
  import { Badge } from "./badge";
8
8
  import { formatNotificationTimestamp } from "./notification";
9
9
  import { Separator } from "./separator";
@@ -18,6 +18,49 @@ export const formatMessageDate = (date) => {
18
18
  const pad = (value) => String(value).padStart(2, "0");
19
19
  return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(twelveHour)}:${pad(d.getMinutes())} ${period}`;
20
20
  };
21
+ export const formatMessageTime = (date) => new Intl.DateTimeFormat("en-SG", {
22
+ hour: "numeric",
23
+ minute: "2-digit",
24
+ hour12: true,
25
+ })
26
+ .format(new Date(date))
27
+ .toLowerCase();
28
+ export const formatMessageCardTimestamp = (date, now = new Date()) => {
29
+ const value = new Date(date);
30
+ const nowValue = new Date(now);
31
+ const timeLabel = new Intl.DateTimeFormat("en-SG", {
32
+ hour: "numeric",
33
+ minute: "2-digit",
34
+ hour12: true,
35
+ }).format(value).toUpperCase();
36
+ const isToday = value.getFullYear() === nowValue.getFullYear() &&
37
+ value.getMonth() === nowValue.getMonth() &&
38
+ value.getDate() === nowValue.getDate();
39
+ if (isToday)
40
+ return `Today ${timeLabel}`;
41
+ const elapsedDays = Math.floor((new Date(nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()).getTime() -
42
+ new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime()) /
43
+ (24 * 60 * 60 * 1000));
44
+ if (elapsedDays > 0 && elapsedDays < 7) {
45
+ return `${elapsedDays} ${elapsedDays === 1 ? "day" : "days"} ago ${timeLabel}`;
46
+ }
47
+ const pad = (part) => String(part).padStart(2, "0");
48
+ const dateLabel = `${pad(value.getDate())}/${pad(value.getMonth() + 1)}/${value.getFullYear()}`;
49
+ return `${dateLabel} ${timeLabel}`;
50
+ };
51
+ export const formatMessageDateHeading = (date, now = new Date()) => {
52
+ const value = new Date(date);
53
+ const isToday = value.getFullYear() === now.getFullYear() &&
54
+ value.getMonth() === now.getMonth() &&
55
+ value.getDate() === now.getDate();
56
+ if (isToday)
57
+ return "Today";
58
+ return new Intl.DateTimeFormat("en-SG", {
59
+ day: "numeric",
60
+ month: "long",
61
+ year: "numeric",
62
+ }).format(value);
63
+ };
21
64
  function getInitials(name) {
22
65
  if (!name)
23
66
  return "";
@@ -39,9 +82,18 @@ function toHtmlContent(content) {
39
82
  .replace(/\n/g, "<br>");
40
83
  }
41
84
  export function MessageNewDivider({ children = "New Message", className, }) {
42
- return (_jsxs("div", { className: cn("mb-3 flex items-center gap-3 py-1", className), children: [_jsx(Separator, { "aria-hidden": "true", className: "flex-1" }), _jsx(Typography, { as: "span", variant: "caption", className: "shrink-0 px-2 font-medium text-muted-foreground", children: children }), _jsx(Separator, { "aria-hidden": "true", className: "flex-1" })] }));
85
+ return (_jsxs("div", { "data-slot": "message-new-divider", className: cn("mb-3 flex items-center gap-3 py-1", className), children: [_jsx(Separator, { "aria-hidden": "true", className: "flex-1" }), _jsx(Typography, { as: "span", variant: "caption", className: "shrink-0 px-2 font-medium text-muted-foreground", children: children }), _jsx(Separator, { "aria-hidden": "true", className: "flex-1" })] }));
43
86
  }
44
- export function MessageAttachmentRow({ attachment, downloadUrl, downloadLabel = `Download ${attachment.name}`, className, ...props }) {
87
+ export function MessageAttachmentRow({ attachment, downloadUrl, downloadLabel = `Download ${attachment.name}`, variant = "default", className, ...props }) {
88
+ if (variant === "compact") {
89
+ const compactContent = (_jsxs(_Fragment, { children: [_jsx(Paperclip, { "aria-hidden": "true", className: "h-4 w-4 shrink-0" }), _jsx(Typography, { as: "span", variant: "bodySm", className: "min-w-0 truncate font-normal text-interactive", children: attachment.name }), attachment.size ? (_jsx(Typography, { as: "span", variant: "bodySm", className: "shrink-0 text-muted-foreground", children: attachment.size })) : null] }));
90
+ const compactClassName = cn("grid min-h-[44px] min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-[8px] rounded-[8px] border border-border bg-card px-[12px] py-[11px] text-foreground no-underline", downloadUrl &&
91
+ "cursor-pointer transition-colors hover:border-primary/50 hover:bg-primary/5 hover:no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", className);
92
+ if (downloadUrl) {
93
+ return (_jsx("a", { className: compactClassName, href: downloadUrl, target: "_blank", rel: "noopener noreferrer", download: attachment.name, "aria-label": downloadLabel, title: downloadLabel, ...props, children: compactContent }));
94
+ }
95
+ return (_jsx("div", { className: compactClassName, "aria-disabled": "true", ...props, children: compactContent }));
96
+ }
45
97
  const renderContent = (isInteractive) => (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(FileText, { className: "h-4 w-4" }) }), _jsxs("div", { className: "min-w-0 pr-1", children: [_jsx(Typography, { as: "p", variant: "bodySm", className: "line-clamp-2 font-medium", children: attachment.name }), attachment.size ? (_jsx(Typography, { as: "p", variant: "caption", children: attachment.size })) : null] }), _jsx("span", { "aria-hidden": "true", className: cn("flex h-8 w-8 shrink-0 items-center justify-center rounded-control border border-border bg-background text-muted-foreground transition-colors", isInteractive
46
98
  ? "group-hover:border-primary/40 group-hover:bg-highlight group-hover:text-highlight-foreground group-focus-visible:border-primary/40 group-focus-visible:bg-highlight group-focus-visible:text-highlight-foreground"
47
99
  : "opacity-60"), children: _jsx(DownloadIcon, { size: 14 }) })] }));
@@ -50,7 +102,7 @@ export function MessageAttachmentRow({ attachment, downloadUrl, downloadLabel =
50
102
  }
51
103
  return (_jsx("div", { className: cn("grid min-w-0 grid-cols-[auto_minmax(0,1fr)_2.75rem] items-center gap-3 rounded-lg border border-border bg-card px-3 py-2 shadow-sm", className), "aria-disabled": "true", ...props, children: renderContent(false) }));
52
104
  }
53
- export function MessageItem({ message, caseId, showNewBadge = false, newMessageLabel = "New Message", onMarkAsRead, onMarkedAsRead, getAttachmentDownloadUrl, formatTimestamp, className, contentClassName, }) {
105
+ export function MessageItem({ message, caseId, showNewBadge = false, newMessageLabel = "New Message", newIndicatorPresentation, onMarkAsRead, onMarkedAsRead, getAttachmentDownloadUrl, formatTimestamp, variant = "default", density = "default", className, contentClassName, }) {
54
106
  var _a;
55
107
  const messageRef = React.useRef(null);
56
108
  const [hasBeenViewed, setHasBeenViewed] = React.useState(message.isRead);
@@ -59,7 +111,10 @@ export function MessageItem({ message, caseId, showNewBadge = false, newMessageL
59
111
  const isOfficer = message.senderRole === "OFFICER";
60
112
  const isApplicant = message.senderRole === "APPLICANT";
61
113
  const isUnreadApplicant = isApplicant && !message.isRead;
62
- const showUnreadDivider = showNewBadge && isUnreadApplicant;
114
+ const showUnreadIndicator = showNewBadge && isUnreadApplicant;
115
+ const resolvedNewIndicatorPresentation = newIndicatorPresentation !== null && newIndicatorPresentation !== void 0 ? newIndicatorPresentation : (variant === "card" ? "badge" : "divider");
116
+ const showUnreadBadge = showUnreadIndicator && resolvedNewIndicatorPresentation === "badge";
117
+ const showUnreadDivider = showUnreadIndicator && resolvedNewIndicatorPresentation === "divider";
63
118
  React.useEffect(() => {
64
119
  if (message.isRead ||
65
120
  message.senderRole !== "APPLICANT" ||
@@ -125,7 +180,7 @@ export function MessageItem({ message, caseId, showNewBadge = false, newMessageL
125
180
  }
126
181
  const intervalId = window.setInterval(() => setTimestampNow(new Date()), MESSAGE_TIMESTAMP_REFRESH_MS);
127
182
  return () => window.clearInterval(intervalId);
128
- }, [formatTimestamp]);
183
+ }, [formatTimestamp, variant]);
129
184
  const senderName = isOfficer
130
185
  ? message.officerName || "Officer"
131
186
  : isApplicant
@@ -137,23 +192,67 @@ export function MessageItem({ message, caseId, showNewBadge = false, newMessageL
137
192
  ? message.applicantName || message.applicantEmail || "Applicant"
138
193
  : "System";
139
194
  const officerRole = isOfficer ? message.officerRole : undefined;
140
- const senderProfileRole = isOfficer ? officerRole : undefined;
195
+ const senderProfileRole = isOfficer
196
+ ? officerRole
197
+ : variant === "card"
198
+ ? isApplicant
199
+ ? "Applicant"
200
+ : "System (Auto-generated)"
201
+ : undefined;
141
202
  const hasSenderProfileRole = Boolean(senderProfileRole);
142
203
  const attachments = (message.attachments || []);
143
204
  const messageDate = (_a = message.sentAt) !== null && _a !== void 0 ? _a : message.createdAt;
144
205
  const downloadCaseId = caseId !== null && caseId !== void 0 ? caseId : message.caseId;
145
206
  const contentHtml = toHtmlContent(message.content);
146
- const richContentClassName = cn("break-words leading-6", "[&_p]:my-0 [&_p+p]:mt-2", "[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5", "[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5", "[&_li]:my-1", "[&_a]:text-interactive [&_a]:underline [&_a:hover]:text-interactive", "[&_h1]:mb-2 [&_h1]:mt-3 [&_h1]:text-xl [&_h1]:font-semibold", "[&_h2]:mb-2 [&_h2]:mt-3 [&_h2]:text-lg [&_h2]:font-semibold", "[&_h3]:mb-1.5 [&_h3]:mt-3 [&_h3]:text-base [&_h3]:font-semibold", "[&_h4]:mb-1.5 [&_h4]:mt-3 [&_h4]:font-semibold", "[&_h5]:mb-1.5 [&_h5]:mt-3 [&_h5]:font-semibold", "[&_h6]:mb-1.5 [&_h6]:mt-3 [&_h6]:font-semibold", "[&_blockquote]:my-2 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground", "[&_table]:my-2 [&_table]:w-full [&_table]:border-collapse", "[&_td]:border [&_td]:border-border [&_td]:p-2", "[&_th]:border [&_th]:border-border [&_th]:bg-[hsl(var(--table-header-background))] [&_th]:p-2 [&_th]:font-semibold", "[&_pre]:my-2 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:font-mono [&_pre]:text-[0.85em]", "[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.85em]", "[&_pre_code]:bg-transparent [&_pre_code]:p-0", "[&_mark]:rounded-sm [&_mark]:bg-[hsl(var(--status-warning))] [&_mark]:px-0.5 [&_mark]:text-[hsl(var(--status-warning-foreground))]", "[&_img]:h-auto [&_img]:max-w-full", contentClassName);
147
- return (_jsxs(_Fragment, { children: [showUnreadDivider && (_jsx(MessageNewDivider, { children: newMessageLabel })), _jsxs("article", { ref: messageRef, className: cn("min-w-0", className), children: [_jsxs("div", { className: "flex min-w-0 items-start justify-between gap-x-3 gap-y-1", children: [_jsx(AvatarProfile, { fallback: getInitials(initialsName), name: senderName, nameAs: "h3", nameTrailing: isApplicant ? (_jsx(Badge, { variant: "warning", bordered: true, children: "Applicant" })) : undefined, role: hasSenderProfileRole ? (_jsx("span", { "data-slot": "message-author-role", children: senderProfileRole })) : undefined, rolePlacement: "below", className: "min-w-0 flex-1 [column-gap:var(--space-3)]", avatarClassName: "h-9 w-9 shrink-0", fallbackClassName: "text-xs font-medium", nameClassName: "max-w-full whitespace-normal break-words [overflow-wrap:anywhere]" }), _jsx("time", { className: "shrink-0 whitespace-nowrap pt-0.5", dateTime: messageDate.toISOString(), children: _jsx(Typography, { as: "span", variant: "caption", className: "text-muted-foreground/60", children: formatTimestamp
148
- ? formatTimestamp(messageDate, message)
149
- : formatNotificationTimestamp(messageDate, {
150
- now: timestampNow,
151
- }) }) })] }), _jsxs("div", { className: "ml-[calc(2.25rem+var(--space-3))] pt-2", children: [_jsx(Typography, { as: "div", variant: "bodySm", color: "inherit", className: richContentClassName, dangerouslySetInnerHTML: { __html: contentHtml } }), attachments.length > 0 ? (_jsx("div", { className: "mt-3 space-y-2", children: attachments.map((attachment, idx) => (_jsx(MessageAttachmentRow, { attachment: attachment, downloadUrl: getAttachmentDownloadUrl === null || getAttachmentDownloadUrl === void 0 ? void 0 : getAttachmentDownloadUrl(attachment, message, downloadCaseId) }, `${attachment.name}-${idx}`))) })) : null] })] })] }));
207
+ const richContentClassName = cn("break-words", variant === "default" && "leading-6", "[&_p]:my-0 [&_p+p]:mt-2", "[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5", "[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5", "[&_li]:my-1", "[&_a]:text-interactive [&_a]:underline [&_a:hover]:text-interactive", "[&_h1]:mb-2 [&_h1]:mt-3 [&_h1]:text-xl [&_h1]:font-semibold", "[&_h2]:mb-2 [&_h2]:mt-3 [&_h2]:text-lg [&_h2]:font-semibold", "[&_h3]:mb-1.5 [&_h3]:mt-3 [&_h3]:text-base [&_h3]:font-semibold", "[&_h4]:mb-1.5 [&_h4]:mt-3 [&_h4]:font-semibold", "[&_h5]:mb-1.5 [&_h5]:mt-3 [&_h5]:font-semibold", "[&_h6]:mb-1.5 [&_h6]:mt-3 [&_h6]:font-semibold", "[&_blockquote]:my-2 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground", "[&_table]:my-2 [&_table]:w-full [&_table]:border-collapse", "[&_td]:border [&_td]:border-border [&_td]:p-2", "[&_th]:border [&_th]:border-border [&_th]:bg-[hsl(var(--table-header-background))] [&_th]:p-2 [&_th]:font-semibold", "[&_pre]:my-2 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:font-mono [&_pre]:text-[0.85em]", "[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.85em]", "[&_pre_code]:bg-transparent [&_pre_code]:p-0", "[&_mark]:rounded-sm [&_mark]:bg-[hsl(var(--status-warning))] [&_mark]:px-0.5 [&_mark]:text-[hsl(var(--status-warning-foreground))]", "[&_img]:h-auto [&_img]:max-w-full", contentClassName);
208
+ const cardSenderName = message.senderRole === "SYSTEM"
209
+ ? "System (Auto-generated)"
210
+ : senderName;
211
+ const cardSenderRole = isOfficer
212
+ ? officerRole
213
+ : isApplicant
214
+ ? "Applicant"
215
+ : message.systemLabel || "Automated message";
216
+ const sender = variant === "card" ? (_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-[8px]", children: [isOfficer ? (_jsx(Avatar, { src: message.avatarUrl, alt: cardSenderName, fallback: getInitials(initialsName), size: "sm", className: "shrink-0" })) : message.senderRole === "SYSTEM" ? (_jsx("span", { className: "flex size-[24px] shrink-0 items-center justify-center", children: _jsx(Laptop, { "aria-hidden": "true", className: "size-[24px]" }) })) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-[4px]", children: [_jsx(Typography, { as: "h3", variant: "label", className: "min-w-0", children: cardSenderName }), message.senderBadgeLabel ? (_jsx(Badge, { variant: "muted", className: "h-5 rounded-full px-2 py-0 [&_span]:normal-case", children: message.senderBadgeLabel })) : null, showUnreadBadge ? (_jsx(Badge, { variant: "success", className: "h-5 rounded-full px-2 py-0 [&_span]:normal-case", children: newMessageLabel })) : null] }), cardSenderRole ? (_jsx(Typography, { as: "p", variant: "caption", className: "truncate text-muted-foreground", "data-slot": "message-author-role", children: cardSenderRole })) : null] })] })) : (_jsx(AvatarProfile, { fallback: getInitials(initialsName), name: senderName, nameAs: "h3", nameTrailing: variant === "default" && isApplicant ? (_jsx(Badge, { variant: "warning", bordered: true, children: "Applicant" })) : undefined, role: hasSenderProfileRole ? (_jsx("span", { "data-slot": "message-author-role", children: senderProfileRole })) : undefined, rolePlacement: "below", className: "min-w-0 flex-1 [column-gap:var(--space-3)]", avatarClassName: "h-9 w-9 shrink-0", fallbackClassName: "text-xs font-medium", nameClassName: "max-w-full whitespace-normal break-words [overflow-wrap:anywhere]" }));
217
+ const header = variant === "card" ? (_jsxs("div", { className: "flex min-w-0 items-start justify-between gap-[8px]", children: [sender, _jsx("time", { className: "shrink-0 whitespace-nowrap", dateTime: messageDate.toISOString(), children: _jsx(Typography, { as: "span", variant: "caption", className: "text-muted-foreground", children: formatTimestamp
218
+ ? formatTimestamp(messageDate, message)
219
+ : formatMessageCardTimestamp(messageDate, timestampNow) }) })] })) : (_jsxs("div", { className: "flex min-w-0 items-start justify-between gap-x-3 gap-y-1", children: [sender, _jsx("time", { className: "shrink-0 whitespace-nowrap pt-0.5", dateTime: messageDate.toISOString(), children: _jsx(Typography, { as: "span", variant: "caption", className: "text-muted-foreground/60", children: formatTimestamp
220
+ ? formatTimestamp(messageDate, message)
221
+ : formatNotificationTimestamp(messageDate, {
222
+ now: timestampNow,
223
+ }) }) })] }));
224
+ const body = (_jsxs(_Fragment, { children: [_jsx(Typography, { as: "div", variant: "bodySm", color: "inherit", className: richContentClassName, dangerouslySetInnerHTML: { __html: contentHtml } }), attachments.length > 0 ? (_jsx("div", { className: variant === "card" ? "mt-[8px] space-y-[8px]" : "mt-3 space-y-2", children: attachments.map((attachment, idx) => (_jsx(MessageAttachmentRow, { attachment: attachment, downloadUrl: getAttachmentDownloadUrl === null || getAttachmentDownloadUrl === void 0 ? void 0 : getAttachmentDownloadUrl(attachment, message, downloadCaseId), variant: variant === "card" ? "compact" : "default" }, `${attachment.name}-${idx}`))) })) : null] }));
225
+ return (_jsxs(_Fragment, { children: [showUnreadDivider && (_jsx(MessageNewDivider, { children: newMessageLabel })), _jsx("article", { "data-slot": variant === "card" ? "message-card" : "message-item", ref: messageRef, className: cn("min-w-0", variant === "card" &&
226
+ "overflow-hidden rounded-surface border border-[hsl(var(--card-border))] bg-card", className), children: variant === "card" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: density === "compact" ? "p-[12px]" : "p-[16px]", children: header }), _jsx("div", { className: cn("border-t border-border", density === "compact" ? "p-[12px]" : "p-[16px]"), children: body })] })) : (_jsxs(_Fragment, { children: [header, _jsx("div", { className: "ml-[calc(2.25rem+var(--space-3))] pt-2", children: body })] })) })] }));
152
227
  }
153
- export function MessageThread({ messages, caseId, showNewBadge = false, newMessageLabel = "New Message", onMarkAsRead, onMarkedAsRead, getAttachmentDownloadUrl, formatTimestamp, className, itemClassName, contentClassName, }) {
228
+ export function MessageThread({ messages, caseId, showNewBadge = false, newMessageLabel = "New Message", newIndicatorPresentation, onMarkAsRead, onMarkedAsRead, getAttachmentDownloadUrl, formatTimestamp, variant = "default", density = "default", groupByDate = false, showDateHeadings = true, formatDateHeading: formatDateHeadingProp, className, itemClassName, contentClassName, }) {
154
229
  var _a;
230
+ const resolvedNewIndicatorPresentation = newIndicatorPresentation !== null && newIndicatorPresentation !== void 0 ? newIndicatorPresentation : (variant === "card" ? "badge" : "divider");
155
231
  const firstUnreadMessageId = showNewBadge
156
232
  ? (_a = messages.find((message) => message.senderRole === "APPLICANT" && !message.isRead)) === null || _a === void 0 ? void 0 : _a.id
157
233
  : undefined;
158
- return (_jsx("div", { className: cn("space-y-5", className), children: messages.map((message) => (_jsx(MessageItem, { message: message, caseId: caseId, showNewBadge: message.id === firstUnreadMessageId, newMessageLabel: newMessageLabel, onMarkAsRead: onMarkAsRead, onMarkedAsRead: onMarkedAsRead, getAttachmentDownloadUrl: getAttachmentDownloadUrl, formatTimestamp: formatTimestamp, className: itemClassName, contentClassName: contentClassName }, message.id))) }));
234
+ const renderMessage = (message) => (_jsx(MessageItem, { message: message, caseId: caseId, showNewBadge: resolvedNewIndicatorPresentation === "badge" && variant === "card"
235
+ ? showNewBadge &&
236
+ message.senderRole === "APPLICANT" &&
237
+ !message.isRead
238
+ : message.id === firstUnreadMessageId, newMessageLabel: newMessageLabel, newIndicatorPresentation: resolvedNewIndicatorPresentation, onMarkAsRead: onMarkAsRead, onMarkedAsRead: onMarkedAsRead, getAttachmentDownloadUrl: getAttachmentDownloadUrl, formatTimestamp: formatTimestamp, variant: variant, density: density, className: itemClassName, contentClassName: contentClassName }, message.id));
239
+ if (groupByDate && showDateHeadings) {
240
+ const groups = messages.reduce((result, message) => {
241
+ var _a;
242
+ const date = new Date((_a = message.sentAt) !== null && _a !== void 0 ? _a : message.createdAt);
243
+ const key = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
244
+ const group = result[result.length - 1];
245
+ if ((group === null || group === void 0 ? void 0 : group.key) === key) {
246
+ group.messages.push(message);
247
+ }
248
+ else {
249
+ result.push({ key, date, messages: [message] });
250
+ }
251
+ return result;
252
+ }, []);
253
+ return (_jsx("div", { className: cn(variant === "card" ? "space-y-[24px]" : "space-y-5", className), children: groups.map((group) => (_jsxs("section", { className: variant === "card" ? "space-y-[16px]" : "space-y-3", children: [_jsx(Typography, { as: "h3", variant: "label", className: variant === "card" ? "font-semibold" : undefined, children: formatDateHeadingProp
254
+ ? formatDateHeadingProp(group.date)
255
+ : formatMessageDateHeading(group.date) }), _jsx("div", { className: variant === "card" ? "space-y-[16px]" : "space-y-3", children: group.messages.map(renderMessage) })] }, group.key))) }));
256
+ }
257
+ return (_jsx("div", { className: cn(variant === "card" ? "space-y-[16px]" : "space-y-5", className), children: messages.map(renderMessage) }));
159
258
  }
@@ -25,6 +25,14 @@ export interface NotificationItemData {
25
25
  id: string;
26
26
  title: React.ReactNode;
27
27
  description?: React.ReactNode;
28
+ /** Person or system responsible for the event. */
29
+ actor?: React.ReactNode;
30
+ /** Optional status or category displayed in an activity-card header. */
31
+ badge?: React.ReactNode;
32
+ /** Per-item activity-card density. Falls back to the list density. */
33
+ activityDensity?: "compact" | "default";
34
+ /** `plain` matches activity events whose semantic glyph needs no tone tile. */
35
+ iconAppearance?: "plain" | "toned";
28
36
  meta?: React.ReactNode;
29
37
  timestamp?: NotificationTimestamp;
30
38
  dateTime?: string;
@@ -38,18 +46,21 @@ export interface NotificationItemData {
38
46
  export interface NotificationItemProps extends Omit<React.HTMLAttributes<HTMLElement>, "children" | "onSelect" | "title"> {
39
47
  notification: NotificationItemData;
40
48
  density?: "compact" | "default";
49
+ variant?: "row" | "card";
41
50
  labelLines?: 1 | 2;
42
51
  descriptionLines?: 1 | 2 | 3;
43
52
  unreadLabel?: React.ReactNode;
53
+ actorLabel?: React.ReactNode;
44
54
  onSelect?: (notification: NotificationItemData) => void;
45
55
  }
46
56
  export interface NotificationListProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
47
57
  notifications: NotificationItemData[];
48
58
  density?: "compact" | "default";
49
- variant?: "bordered" | "plain";
59
+ variant?: "bordered" | "plain" | "cards";
50
60
  labelLines?: 1 | 2;
51
61
  descriptionLines?: 1 | 2 | 3;
52
62
  unreadLabel?: React.ReactNode;
63
+ actorLabel?: React.ReactNode;
53
64
  emptyState?: React.ReactNode;
54
65
  itemClassName?: string;
55
66
  onNotificationClick?: (notification: NotificationItemData) => void;
@@ -83,7 +94,7 @@ export interface NotificationTriggerProps extends Omit<ButtonProps, "children">
83
94
  }
84
95
  declare function formatNotificationTimestamp(timestamp: NotificationTimestamp, { now, locale }?: FormatNotificationTimestampOptions): string;
85
96
  declare const NotificationTrigger: React.ForwardRefExoticComponent<NotificationTriggerProps & React.RefAttributes<HTMLButtonElement>>;
86
- declare function NotificationItem({ notification, density, labelLines, descriptionLines, unreadLabel, className, onClick, onSelect, ...props }: NotificationItemProps): import("react/jsx-runtime").JSX.Element;
87
- declare function NotificationList({ notifications, density, variant, labelLines, descriptionLines, unreadLabel, emptyState, itemClassName, className, onNotificationClick, ...props }: NotificationListProps): import("react/jsx-runtime").JSX.Element;
97
+ declare function NotificationItem({ notification, density, variant, labelLines, descriptionLines, unreadLabel, actorLabel, className, onClick, onSelect, ...props }: NotificationItemProps): import("react/jsx-runtime").JSX.Element;
98
+ declare function NotificationList({ notifications, density, variant, labelLines, descriptionLines, unreadLabel, actorLabel, emptyState, itemClassName, className, onNotificationClick, ...props }: NotificationListProps): import("react/jsx-runtime").JSX.Element;
88
99
  declare function NotificationPopover({ trigger, notifications, title, filter, defaultFilter, onFilterChange, showFilterTabs, height, minHeight, maxHeight, emptyState, listClassName, itemClassName, unreadLabel, markAllAsReadLabel, onNotificationClick, onMarkAllAsRead, contentClassName, align, ...popoverProps }: NotificationPopoverProps): import("react/jsx-runtime").JSX.Element;
89
100
  export { formatNotificationTimestamp, NotificationItem, NotificationList, NotificationPopover, NotificationTrigger, };
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Bell, ClipboardCheck, FileText, MessageSquare, UserCheck, } from "lucide-react";
5
+ import { ActivityCard, ActivityCardActor, ActivityCardContent, } from "./activity-card";
5
6
  import { Button } from "./button";
6
7
  import { CountBadge } from "./count-badge";
7
8
  import { EmptyState } from "./empty-state";
@@ -118,8 +119,8 @@ function getNotificationTone(notification) {
118
119
  var _a, _b;
119
120
  return ((_a = notification.tone) !== null && _a !== void 0 ? _a : notificationKindTones[(_b = notification.kind) !== null && _b !== void 0 ? _b : "system"]);
120
121
  }
121
- function NotificationItem({ notification, density = "default", labelLines = 2, descriptionLines = 2, unreadLabel = "Unread", className, onClick, onSelect, ...props }) {
122
- var _a, _b, _c;
122
+ function NotificationItem({ notification, density = "default", variant = "row", labelLines = 2, descriptionLines = 2, unreadLabel = "Unread", actorLabel = "By", className, onClick, onSelect, ...props }) {
123
+ var _a, _b, _c, _d;
123
124
  const Icon = getNotificationIcon(notification);
124
125
  const tone = getNotificationTone(notification);
125
126
  const isUnread = Boolean(notification.unread);
@@ -141,7 +142,33 @@ function NotificationItem({ notification, density = "default", labelLines = 2, d
141
142
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(notification);
142
143
  }
143
144
  };
144
- const content = (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: cn("mt-0.5 flex shrink-0 items-center justify-center rounded-control border", density === "compact" ? "h-7 w-7" : "h-8 w-8", notificationToneClasses[tone]), children: _jsx(Icon, { className: density === "compact" ? "h-3.5 w-3.5" : "h-4 w-4" }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-start gap-3", children: [_jsx(Typography, { as: "span", variant: "bodySm", className: cn("min-w-0 text-left text-foreground", isUnread ? "font-semibold" : "font-medium", labelLineClasses[labelLines]), children: notification.title }), meta || (isUnread && unreadLabel !== null) ? (_jsxs("span", { className: "flex min-w-0 shrink-0 items-center justify-end gap-1.5 text-right", children: [meta ? (_jsx(Typography, { as: "span", variant: "caption", className: "shrink-0 whitespace-nowrap text-muted-foreground/70", children: meta })) : null, isUnread && unreadLabel !== null ? (_jsx("span", { "data-slot": "notification-unread-indicator", className: "mt-1 h-2.5 w-2.5 shrink-0 rounded-full bg-primary ring-2 ring-background", children: _jsx("span", { className: "sr-only", children: unreadLabel }) })) : null] })) : null] }), notification.description !== undefined ? (_jsx(Typography, { as: "span", variant: "caption", className: cn("mt-1.5 block text-left", isUnread ? "text-foreground/75" : "text-muted-foreground", descriptionLineClasses[descriptionLines]), children: notification.description })) : null, hasActions ? (_jsx("span", { className: "mt-4 flex flex-wrap gap-2", children: actions.map((action, index) => {
145
+ if (variant === "card") {
146
+ const activityDensity = (_d = notification.activityDensity) !== null && _d !== void 0 ? _d : density;
147
+ const hasDescription = notification.description !== undefined &&
148
+ notification.description !== null &&
149
+ notification.description !== "";
150
+ const actorBelongsInHeader = notification.actor !== undefined && !hasDescription && !hasActions;
151
+ const hasCardContent = hasDescription ||
152
+ (notification.actor !== undefined && !actorBelongsInHeader) ||
153
+ hasActions;
154
+ const activityElement = notification.href && !hasActions
155
+ ? "a"
156
+ : isInteractive
157
+ ? "button"
158
+ : "article";
159
+ const unreadIndicator = isUnread && unreadLabel !== null ? (_jsx("span", { "data-slot": "notification-unread-indicator", className: "block size-2.5 rounded-full bg-primary ring-2 ring-background", children: _jsx("span", { className: "sr-only", children: unreadLabel }) })) : null;
160
+ const cardTrailing = notification.badge || unreadIndicator ? (_jsxs("span", { className: "flex items-center gap-[var(--space-2)]", children: [notification.badge, unreadIndicator] })) : null;
161
+ return (_jsx(ActivityCard, { as: activityElement, "data-slot": "notification-item", "data-unread": isUnread ? "" : undefined, density: activityDensity, icon: _jsx(Icon, { className: activityDensity === "compact" ? "size-3.5" : "size-[24px]" }), iconTone: notification.iconAppearance === "plain" ? undefined : tone, title: notification.title, titleLines: labelLines, meta: meta, headerContent: actorBelongsInHeader ? (_jsx(ActivityCardActor, { actor: notification.actor, label: actorLabel })) : undefined, trailing: cardTrailing, href: notification.href, className: cn("group relative w-full text-left outline-none transition-colors", isUnread && "bg-primary/[0.045]", hasHoverSurface && "hover:bg-accent/70", isInteractive &&
162
+ "cursor-pointer focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", className), onClick: isInteractive ? handleClick : undefined, ...props, children: hasCardContent ? (_jsxs(ActivityCardContent, { density: activityDensity, children: [hasDescription ? (_jsx(Typography, { as: "div", variant: "bodySm", className: cn("text-left", isUnread ? "text-foreground/75" : "text-muted-foreground", descriptionLineClasses[descriptionLines]), children: notification.description })) : null, notification.actor !== undefined && !actorBelongsInHeader ? (_jsx(ActivityCardActor, { actor: notification.actor, label: actorLabel })) : null, hasActions ? (_jsx("div", { className: "flex flex-wrap gap-2", children: actions.map((action, index) => {
163
+ var _a, _b;
164
+ return (_jsx(Button, { type: "button", variant: (_a = action.variant) !== null && _a !== void 0 ? _a : (index === 0 ? "outline" : "default"), size: "sm", disabled: action.disabled, className: cn("min-w-24", action.className), onClick: (event) => {
165
+ var _a;
166
+ event.stopPropagation();
167
+ (_a = action.onClick) === null || _a === void 0 ? void 0 : _a.call(action, notification, event);
168
+ }, children: action.label }, (_b = action.id) !== null && _b !== void 0 ? _b : index));
169
+ }) })) : null] })) : null }));
170
+ }
171
+ const content = (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: cn("mt-0.5 flex shrink-0 items-center justify-center rounded-control border", density === "compact" ? "h-7 w-7" : "h-8 w-8", notificationToneClasses[tone]), children: _jsx(Icon, { className: density === "compact" ? "h-3.5 w-3.5" : "h-4 w-4" }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-start gap-3", children: [_jsx(Typography, { as: "span", variant: "bodySm", className: cn("min-w-0 text-left text-foreground", isUnread ? "font-semibold" : "font-medium", labelLineClasses[labelLines]), children: notification.title }), meta || (isUnread && unreadLabel !== null) ? (_jsxs("span", { className: "flex min-w-0 shrink-0 items-center justify-end gap-1.5 text-right", children: [meta ? (_jsx(Typography, { as: "span", variant: "caption", className: "shrink-0 whitespace-nowrap text-muted-foreground/70", children: meta })) : null, isUnread && unreadLabel !== null ? (_jsx("span", { "data-slot": "notification-unread-indicator", className: "mt-1 h-2.5 w-2.5 shrink-0 rounded-full bg-primary ring-2 ring-background", children: _jsx("span", { className: "sr-only", children: unreadLabel }) })) : null] })) : null] }), notification.description !== undefined ? (_jsx(Typography, { as: "span", variant: "caption", className: cn("mt-1.5 block text-left", isUnread ? "text-foreground/75" : "text-muted-foreground", descriptionLineClasses[descriptionLines]), children: notification.description })) : null, notification.actor !== undefined ? (_jsxs(Typography, { as: "span", variant: "bodySm", className: "mt-2 block text-left font-semibold text-foreground", children: [actorLabel, " ", notification.actor] })) : null, hasActions ? (_jsx("span", { className: "mt-4 flex flex-wrap gap-2", children: actions.map((action, index) => {
145
172
  var _a, _b;
146
173
  return (_jsx(Button, { type: "button", variant: (_a = action.variant) !== null && _a !== void 0 ? _a : (index === 0 ? "outline" : "default"), size: "sm", disabled: action.disabled, className: cn("min-w-24", action.className), onClick: (event) => {
147
174
  var _a;
@@ -149,7 +176,7 @@ function NotificationItem({ notification, density = "default", labelLines = 2, d
149
176
  (_a = action.onClick) === null || _a === void 0 ? void 0 : _a.call(action, notification, event);
150
177
  }, children: action.label }, (_b = action.id) !== null && _b !== void 0 ? _b : index));
151
178
  }) })) : null] })] }));
152
- const itemClassName = cn("group relative flex w-full min-w-0 items-start gap-3 overflow-hidden border-b border-border/70 bg-card text-left text-card-foreground outline-none transition-colors last:border-b-0", density === "compact" ? "px-3 py-3" : "px-4 py-4", isUnread && "bg-primary/[0.045]", hasHoverSurface && "hover:bg-accent/70", isInteractive &&
179
+ const itemClassName = cn("group relative flex w-full min-w-0 items-start gap-3 overflow-hidden bg-card text-left text-card-foreground outline-none transition-colors", "border-b border-border/70 last:border-b-0", density === "compact" ? "px-3 py-3" : "px-4 py-4", isUnread && "bg-primary/[0.045]", hasHoverSurface && "hover:bg-accent/70", isInteractive &&
153
180
  "cursor-pointer focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", className);
154
181
  if (notification.href && !hasActions) {
155
182
  return (_jsx("a", { href: notification.href, "data-slot": "notification-item", "data-unread": isUnread ? "" : undefined, className: itemClassName, onClick: handleClick, ...props, children: content }));
@@ -159,11 +186,13 @@ function NotificationItem({ notification, density = "default", labelLines = 2, d
159
186
  }
160
187
  return (_jsx("div", { "data-slot": "notification-item", "data-unread": isUnread ? "" : undefined, className: itemClassName, ...props, children: content }));
161
188
  }
162
- function NotificationList({ notifications, density = "default", variant = "bordered", labelLines = 2, descriptionLines = 2, unreadLabel = "Unread", emptyState, itemClassName, className, onNotificationClick, ...props }) {
189
+ function NotificationList({ notifications, density = "default", variant = "bordered", labelLines = 2, descriptionLines = 2, unreadLabel = "Unread", actorLabel = "By", emptyState, itemClassName, className, onNotificationClick, ...props }) {
163
190
  const hasNotifications = notifications.length > 0;
164
191
  const resolvedEmptyState = emptyState !== null && emptyState !== void 0 ? emptyState : (_jsx(EmptyState, { message: "No notifications", description: "New updates will appear here.", icon: Bell }));
165
- return (_jsx("div", { "data-slot": "notification-list", className: cn("min-w-0 overflow-hidden bg-card text-card-foreground", !hasNotifications && "flex min-h-full flex-col", variant === "bordered" && "rounded-surface border border-border/70", className), ...props, children: hasNotifications
166
- ? notifications.map((notification) => (_jsx(NotificationItem, { notification: notification, density: density, labelLines: labelLines, descriptionLines: descriptionLines, unreadLabel: unreadLabel, className: itemClassName, onSelect: onNotificationClick }, notification.id)))
192
+ return (_jsx("div", { "data-slot": "notification-list", className: cn("min-w-0 text-card-foreground", !hasNotifications && "flex min-h-full flex-col", variant === "cards"
193
+ ? "space-y-[var(--space-4)] bg-transparent"
194
+ : "overflow-hidden bg-card", variant === "bordered" && "rounded-surface border border-border/70", className), ...props, children: hasNotifications
195
+ ? notifications.map((notification) => (_jsx(NotificationItem, { notification: notification, density: density, variant: variant === "cards" ? "card" : "row", labelLines: labelLines, descriptionLines: descriptionLines, unreadLabel: unreadLabel, actorLabel: actorLabel, className: itemClassName, onSelect: onNotificationClick }, notification.id)))
167
196
  : (_jsx("div", { className: "flex min-h-0 flex-1 flex-col justify-center", children: resolvedEmptyState })) }));
168
197
  }
169
198
  function NotificationPopover({ trigger, notifications, title = "Notifications", filter, defaultFilter = "all", onFilterChange, showFilterTabs = true, height = "h-[36rem]", minHeight = "min-h-[36rem]", maxHeight = "max-h-[36rem]", emptyState, listClassName, itemClassName, unreadLabel = "Unread", markAllAsReadLabel = "Mark all as read", onNotificationClick, onMarkAllAsRead, contentClassName, align = "end", ...popoverProps }) {
@@ -8,8 +8,9 @@ export interface PageSectionProps extends Omit<React.HTMLAttributes<HTMLElement>
8
8
  description?: React.ReactNode;
9
9
  headerAction?: React.ReactNode;
10
10
  titleAdornment?: React.ReactNode;
11
+ titleAdornmentSpacing?: "default" | "comfortable";
11
12
  headerClassName?: string;
12
13
  contentClassName?: string;
13
14
  }
14
- declare function PageSection({ title, description, headerAction, titleAdornment, className, headerClassName, contentClassName, children, ...props }: PageSectionProps): import("react/jsx-runtime").JSX.Element;
15
+ declare function PageSection({ title, description, headerAction, titleAdornment, titleAdornmentSpacing, className, headerClassName, contentClassName, children, ...props }: PageSectionProps): import("react/jsx-runtime").JSX.Element;
15
16
  export { PageSection, PageSectionGroup };
@@ -6,9 +6,9 @@ function PageSectionGroup({ variant = "divided", className, ...props }) {
6
6
  return (_jsx("div", { className: cn(variant === "divided" && "divide-y divide-border", variant === "cards" &&
7
7
  "space-y-4 [&>[data-slot=page-section]]:rounded-surface [&>[data-slot=page-section]]:border [&>[data-slot=page-section]]:border-border/70 [&>[data-slot=page-section]]:bg-card [&>[data-slot=page-section]]:p-5", className), ...props }));
8
8
  }
9
- function PageSection({ title, description, headerAction, titleAdornment, className, headerClassName, contentClassName, children, ...props }) {
9
+ function PageSection({ title, description, headerAction, titleAdornment, titleAdornmentSpacing = "default", className, headerClassName, contentClassName, children, ...props }) {
10
10
  const renderedTitle = React.isValidElement(title) ? (title) : (_jsx(Typography, { as: "h3", variant: "subheading", children: title }));
11
11
  const renderedDescription = description == null ? null : React.isValidElement(description) ? (description) : (_jsx(Typography, { as: "p", variant: "caption", children: description }));
12
- return (_jsxs("section", { "data-slot": "page-section", className: cn("py-6", className), ...props, children: [_jsxs("div", { className: cn("flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between", headerClassName), children: [_jsxs("div", { className: "min-w-0 space-y-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [renderedTitle, titleAdornment] }), renderedDescription] }), headerAction ? (_jsx("div", { className: "flex flex-wrap items-center justify-end gap-3 sm:shrink-0", children: headerAction })) : null] }), children ? (_jsx("div", { className: cn("pt-4", contentClassName), children: children })) : null] }));
12
+ return (_jsxs("section", { "data-slot": "page-section", className: cn("py-6", className), ...props, children: [_jsxs("div", { className: cn("flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between", headerClassName), children: [_jsxs("div", { className: "min-w-0 space-y-1", children: [_jsxs("div", { className: cn("flex flex-wrap items-center", titleAdornmentSpacing === "comfortable" ? "gap-4" : "gap-1.5"), children: [renderedTitle, titleAdornment] }), renderedDescription] }), headerAction ? (_jsx("div", { className: "flex flex-wrap items-center justify-end gap-3 sm:shrink-0", children: headerAction })) : null] }), children ? (_jsx("div", { className: cn("pt-4", contentClassName), children: children })) : null] }));
13
13
  }
14
14
  export { PageSection, PageSectionGroup };
@@ -17,8 +17,10 @@ export interface PopoverMenuSectionProps extends React.HTMLAttributes<HTMLDivEle
17
17
  declare function PopoverMenuSection({ className, children, ...props }: PopoverMenuSectionProps): import("react/jsx-runtime").JSX.Element;
18
18
  export interface PopoverMenuItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
19
19
  active?: boolean;
20
+ /** Communicates destructive actions without changing menu-item sizing. */
21
+ tone?: "default" | "destructive";
20
22
  }
21
- declare function PopoverMenuItem({ className, children, active, type, ...props }: PopoverMenuItemProps): import("react/jsx-runtime").JSX.Element;
23
+ declare function PopoverMenuItem({ className, children, active, tone, type, ...props }: PopoverMenuItemProps): import("react/jsx-runtime").JSX.Element;
22
24
  export interface PopoverMenuCheckboxItemProps extends Omit<CheckboxProps, "className" | "label"> {
23
25
  description?: React.ReactNode;
24
26
  className?: string;
@@ -52,7 +54,7 @@ declare const PopoverMenuSearch: React.ForwardRefExoticComponent<Omit<PopoverMen
52
54
  export interface PopoverMenuScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
53
55
  maxHeight?: string;
54
56
  }
55
- declare function PopoverMenuScrollArea({ className, children, maxHeight, ...props }: PopoverMenuScrollAreaProps): import("react/jsx-runtime").JSX.Element;
57
+ declare function PopoverMenuScrollArea({ className, children, maxHeight, onScroll, ...props }: PopoverMenuScrollAreaProps): import("react/jsx-runtime").JSX.Element;
56
58
  export interface PopoverMenuViewProps extends React.HTMLAttributes<HTMLDivElement> {
57
59
  direction?: "forward" | "backward" | "none";
58
60
  viewKey?: React.Key;
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { Check } from "lucide-react";
4
+ import { Check, ChevronUp } from "lucide-react";
5
+ import { ChevronDownIcon } from "./animated-icons/chevron-down";
5
6
  import { Button } from "./button";
6
7
  import { Checkbox } from "./checkbox";
7
8
  import { Input } from "./input";
@@ -20,9 +21,10 @@ function PopoverMenuHeader({ className, children, ...props }) {
20
21
  function PopoverMenuSection({ className, children, ...props }) {
21
22
  return (_jsx("div", { className: cn("space-y-0", className), ...props, children: children }));
22
23
  }
23
- function PopoverMenuItem({ className, children, active, type = "button", ...props }) {
24
+ function PopoverMenuItem({ className, children, active, tone = "default", type = "button", ...props }) {
24
25
  return (_jsxs(Button, { type: type, variant: "ghost", "data-slot": "popover-menu-item", className: cn("h-auto min-h-8 w-full justify-start gap-2 px-2 py-2 text-left font-normal whitespace-normal aria-[selected=true]:bg-highlight aria-[selected=true]:text-highlight-foreground aria-[selected=true]:[--muted-foreground:var(--instructional-foreground)] aria-[selected=true]:hover:bg-highlight/80 aria-[selected=true]:hover:text-highlight-foreground aria-[selected=true]:focus-visible:bg-highlight/80 aria-[selected=true]:focus-visible:text-highlight-foreground", popoverMenuItemFocusClassName, active &&
25
- "bg-highlight text-highlight-foreground [--muted-foreground:var(--instructional-foreground)] hover:bg-highlight/80 hover:text-highlight-foreground focus-visible:bg-highlight/80 focus-visible:text-highlight-foreground", className), ...props, children: [children, active ? _jsx(Check, { "aria-hidden": "true", className: "ml-auto h-4 w-4 shrink-0" }) : null] }));
26
+ "bg-highlight text-highlight-foreground [--muted-foreground:var(--instructional-foreground)] hover:bg-highlight/80 hover:text-highlight-foreground focus-visible:bg-highlight/80 focus-visible:text-highlight-foreground", tone === "destructive" &&
27
+ "text-destructive hover:bg-destructive/10 hover:text-destructive focus-visible:bg-destructive/10 focus-visible:text-destructive", className), ...props, children: [children, active ? _jsx(Check, { "aria-hidden": "true", className: "ml-auto h-4 w-4 shrink-0" }) : null] }));
26
28
  }
27
29
  function PopoverMenuCheckboxItem({ className, children, checked = false, description, disabled, onChange, readOnly, ...props }) {
28
30
  return (_jsxs("label", { className: cn("flex h-auto min-h-8 w-full cursor-pointer items-start gap-1.5 rounded-sm px-2 py-2 text-left text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-within:bg-accent focus-within:text-accent-foreground", popoverMenuItemFocusWithinClassName, checked &&
@@ -52,8 +54,66 @@ const PopoverMenuSearch = React.forwardRef(({ "aria-label": ariaLabel, className
52
54
  return (_jsx("div", { className: cn("relative -mx-1 -mt-1 mb-1 px-1 py-1 after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-border after:content-['']", containerClassName), children: _jsx(Input, { ref: ref, "data-popover-menu-search": "true", label: (_b = (_a = label !== null && label !== void 0 ? label : ariaLabel) !== null && _a !== void 0 ? _a : placeholder) !== null && _b !== void 0 ? _b : "Search", hideLabel: true, className: "gap-0", inputClassName: className, "aria-label": ariaLabel, placeholder: placeholder, ...props }) }));
53
55
  });
54
56
  PopoverMenuSearch.displayName = "PopoverMenuSearch";
55
- function PopoverMenuScrollArea({ className, children, maxHeight = "max-h-64", ...props }) {
56
- return (_jsx("div", { className: cn(maxHeight, "space-y-1 overflow-y-auto overflow-x-hidden", className), ...props, children: children }));
57
+ function PopoverMenuScrollButton({ direction, onAutoScroll, }) {
58
+ const autoScrollTimerRef = React.useRef(null);
59
+ const clearAutoScrollTimer = React.useCallback(() => {
60
+ if (autoScrollTimerRef.current === null)
61
+ return;
62
+ window.clearInterval(autoScrollTimerRef.current);
63
+ autoScrollTimerRef.current = null;
64
+ }, []);
65
+ const startAutoScroll = React.useCallback(() => {
66
+ if (autoScrollTimerRef.current !== null)
67
+ return;
68
+ autoScrollTimerRef.current = window.setInterval(() => onAutoScroll(direction), 50);
69
+ }, [direction, onAutoScroll]);
70
+ React.useEffect(() => clearAutoScrollTimer, [clearAutoScrollTimer]);
71
+ return (_jsx("div", { "aria-hidden": "true", "data-slot": `popover-menu-scroll-${direction}-button`, className: "-mx-1 flex shrink-0 cursor-pointer items-center justify-center py-1", onPointerDown: startAutoScroll, onPointerMove: startAutoScroll, onPointerLeave: clearAutoScrollTimer, children: direction === "up" ? (_jsx(ChevronUp, { className: "h-4 w-4" })) : (_jsx(ChevronDownIcon, { size: 16 })) }));
72
+ }
73
+ function PopoverMenuScrollArea({ className, children, maxHeight = "max-h-64", onScroll, ...props }) {
74
+ const scrollAreaRef = React.useRef(null);
75
+ const [canScrollUp, setCanScrollUp] = React.useState(false);
76
+ const [canScrollDown, setCanScrollDown] = React.useState(false);
77
+ const updateScrollControls = React.useCallback(() => {
78
+ const scrollArea = scrollAreaRef.current;
79
+ if (!scrollArea)
80
+ return;
81
+ const maxScroll = scrollArea.scrollHeight - scrollArea.clientHeight;
82
+ setCanScrollUp(scrollArea.scrollTop > 0);
83
+ setCanScrollDown(maxScroll > 0 && Math.ceil(scrollArea.scrollTop) < maxScroll);
84
+ }, []);
85
+ useIsomorphicLayoutEffect(() => {
86
+ const scrollArea = scrollAreaRef.current;
87
+ if (!scrollArea)
88
+ return;
89
+ updateScrollControls();
90
+ const animationFrame = window.requestAnimationFrame(updateScrollControls);
91
+ const resizeObserver = typeof ResizeObserver === "undefined"
92
+ ? null
93
+ : new ResizeObserver(updateScrollControls);
94
+ resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.observe(scrollArea);
95
+ const mutationObserver = new MutationObserver(updateScrollControls);
96
+ mutationObserver.observe(scrollArea, { childList: true, subtree: true });
97
+ return () => {
98
+ window.cancelAnimationFrame(animationFrame);
99
+ resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.disconnect();
100
+ mutationObserver.disconnect();
101
+ };
102
+ }, [children, updateScrollControls]);
103
+ const handleScroll = React.useCallback((event) => {
104
+ onScroll === null || onScroll === void 0 ? void 0 : onScroll(event);
105
+ updateScrollControls();
106
+ }, [onScroll, updateScrollControls]);
107
+ const handleAutoScroll = React.useCallback((direction) => {
108
+ const scrollArea = scrollAreaRef.current;
109
+ if (!scrollArea)
110
+ return;
111
+ const firstItem = scrollArea.querySelector('[data-slot="popover-menu-item"]');
112
+ const scrollStep = (firstItem === null || firstItem === void 0 ? void 0 : firstItem.offsetHeight) || 32;
113
+ scrollArea.scrollTop += direction === "up" ? -scrollStep : scrollStep;
114
+ updateScrollControls();
115
+ }, [updateScrollControls]);
116
+ return (_jsxs("div", { "data-slot": "popover-menu-scroll-viewport", className: cn(maxHeight, "flex min-h-0 flex-col overflow-hidden"), children: [canScrollUp ? (_jsx(PopoverMenuScrollButton, { direction: "up", onAutoScroll: handleAutoScroll })) : null, _jsx("div", { ref: scrollAreaRef, className: cn("-mx-1 min-h-0 flex-1 space-y-1 overflow-y-auto overflow-x-hidden px-1", className), onScroll: handleScroll, ...props, children: children }), canScrollDown ? (_jsx(PopoverMenuScrollButton, { direction: "down", onAutoScroll: handleAutoScroll })) : null] }));
57
117
  }
58
118
  function PopoverMenuView({ children, className, direction = "forward", viewKey = "default", ...props }) {
59
119
  const [activeViewKey, setActiveViewKey] = React.useState(viewKey);
package/dist/popover.d.ts CHANGED
@@ -5,12 +5,15 @@ import { type TooltipProps } from "./tooltip";
5
5
  import { Typography } from "./typography";
6
6
  type PopoverRootProps = React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Root>;
7
7
  type PopoverContentProps = React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>;
8
+ type PopoverAnchorProps = React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Anchor>;
8
9
  export interface PopoverProps extends Pick<PopoverRootProps, "defaultOpen" | "modal" | "onOpenChange" | "open">, Omit<PopoverContentProps, "children" | "className" | "title"> {
9
10
  trigger: React.ReactElement;
11
+ /** Positions the popover from a specific element while retaining `trigger` for interaction and focus. */
12
+ virtualAnchorRef?: PopoverAnchorProps["virtualRef"];
10
13
  children: React.ReactNode;
11
14
  /** Visible popover heading. Also labels the popover for assistive technology. */
12
15
  title?: React.ReactNode;
13
- surface?: "panel" | "menu";
16
+ surface?: "panel" | "menu" | "message";
14
17
  /** Controls whether Tab stays within the popover or closes it and continues in the page Tab order. */
15
18
  tabBehavior?: PopoverTabBehavior;
16
19
  contentClassName?: string;
@@ -19,8 +22,13 @@ export interface PopoverProps extends Pick<PopoverRootProps, "defaultOpen" | "mo
19
22
  triggerTooltipSideOffset?: TooltipProps["sideOffset"];
20
23
  triggerTooltipClassName?: TooltipProps["contentClassName"];
21
24
  }
22
- declare function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, title, surface, tabBehavior, contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align, sideOffset, ...contentProps }: PopoverProps): import("react/jsx-runtime").JSX.Element;
25
+ declare function Popover({ defaultOpen, modal, onOpenChange, open, trigger, virtualAnchorRef, children, title, surface, tabBehavior, contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align, sideOffset, style, ...contentProps }: PopoverProps): import("react/jsx-runtime").JSX.Element;
23
26
  export interface PopoverTitleProps extends Omit<React.ComponentPropsWithoutRef<typeof Typography>, "as" | "asChild" | "variant" | "color"> {
24
27
  }
25
28
  declare function PopoverTitle({ className, children, ...props }: PopoverTitleProps): import("react/jsx-runtime").JSX.Element;
26
29
  export { Popover, PopoverTitle };
30
+ export interface PopoverListProps {
31
+ items: readonly string[];
32
+ }
33
+ /** Read-only information rows for a popover, without implying menu actions. */
34
+ export declare function PopoverList({ items }: PopoverListProps): import("react/jsx-runtime").JSX.Element;
package/dist/popover.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cn } from "./lib/utils";
5
- import { PopoverContent, PopoverRoot, PopoverTrigger, } from "./popover-primitive";
5
+ import { PopoverAnchor, PopoverContent, PopoverRoot, PopoverTrigger, } from "./popover-primitive";
6
6
  import { usePopoverTabExit, } from "./popover-focus";
7
7
  import { Tooltip } from "./tooltip";
8
8
  import { Typography } from "./typography";
9
- function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, title, surface = "panel", tabBehavior = "contain", contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align = "center", sideOffset = 4, ...contentProps }) {
9
+ function Popover({ defaultOpen, modal, onOpenChange, open, trigger, virtualAnchorRef, children, title, surface = "panel", tabBehavior = "contain", contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align = "center", sideOffset = 4, style, ...contentProps }) {
10
10
  const generatedTitleId = React.useId();
11
11
  const titleId = title ? `popover-title-${generatedTitleId}` : undefined;
12
12
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen !== null && defaultOpen !== void 0 ? defaultOpen : false);
@@ -79,7 +79,7 @@ function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, ti
79
79
  contentOnCloseAutoFocus === null || contentOnCloseAutoFocus === void 0 ? void 0 : contentOnCloseAutoFocus(event);
80
80
  handleCloseAutoFocus(event);
81
81
  }, [contentOnCloseAutoFocus, handleCloseAutoFocus]);
82
- return (_jsxs(PopoverRoot, { modal: modal, onOpenChange: setOpen, open: currentOpen, children: [triggerTooltip ? (_jsx(Tooltip, { content: triggerTooltip, side: triggerTooltipSide, sideOffset: triggerTooltipSideOffset, contentClassName: triggerTooltipClassName, children: triggerElement })) : (triggerElement), _jsxs(PopoverContent, { ref: contentRef, align: align, sideOffset: sideOffset, "aria-labelledby": titleId !== null && titleId !== void 0 ? titleId : contentProps["aria-labelledby"], className: cn(surface === "menu" && "overflow-hidden p-1", contentClassName), ...contentProps, onCloseAutoFocus: handleContentCloseAutoFocus, onKeyDown: handleContentKeyDown, onKeyDownCapture: handleContentKeyDownCapture, children: [title ? (_jsx(PopoverTitle, { id: titleId, className: cn(surface === "menu"
82
+ return (_jsxs(PopoverRoot, { modal: modal, onOpenChange: setOpen, open: currentOpen, children: [virtualAnchorRef ? (_jsx(PopoverAnchor, { virtualRef: virtualAnchorRef })) : null, triggerTooltip ? (_jsx(Tooltip, { content: triggerTooltip, side: triggerTooltipSide, sideOffset: triggerTooltipSideOffset, contentClassName: triggerTooltipClassName, children: triggerElement })) : (triggerElement), _jsxs(PopoverContent, { ref: contentRef, align: align, sideOffset: sideOffset, "aria-labelledby": titleId !== null && titleId !== void 0 ? titleId : contentProps["aria-labelledby"], className: cn(surface === "menu" && "overflow-hidden p-1", surface === "message" && "overflow-hidden p-0", contentClassName), style: style, ...contentProps, onCloseAutoFocus: handleContentCloseAutoFocus, onKeyDown: handleContentKeyDown, onKeyDownCapture: handleContentKeyDownCapture, children: [title ? (_jsx(PopoverTitle, { id: titleId, className: cn(surface === "menu"
83
83
  ? "px-2 py-1.5"
84
84
  : "-mx-4 -mt-4 mb-4 border-b px-4 py-3"), children: title })) : null, children] })] }));
85
85
  }
@@ -87,3 +87,7 @@ function PopoverTitle({ className, children, ...props }) {
87
87
  return (_jsx(Typography, { as: "p", variant: "label", "data-slot": "popover-title", className: className, ...props, children: children }));
88
88
  }
89
89
  export { Popover, PopoverTitle };
90
+ /** Read-only information rows for a popover, without implying menu actions. */
91
+ export function PopoverList({ items }) {
92
+ return (_jsx("ul", { "data-slot": "popover-list", className: "max-h-64 space-y-3 overflow-y-auto text-sm text-foreground", children: items.map((item, index) => (_jsxs("li", { className: "flex items-start gap-2", children: [_jsx("span", { "aria-hidden": "true", className: "mt-2 size-1 shrink-0 rounded-full bg-muted-foreground" }), _jsx("span", { className: "min-w-0 break-words", children: item })] }, index))) }));
93
+ }
@@ -25,6 +25,7 @@ export interface RichTextEditorSendAction {
25
25
  disabled?: boolean;
26
26
  }
27
27
  export interface RichTextEditorProps {
28
+ variant?: "default" | "message";
28
29
  placeholder?: string;
29
30
  editorAriaLabel?: string;
30
31
  header?: React.ReactNode;
@@ -53,4 +54,4 @@ export interface RichTextEditorProps {
53
54
  export declare function formatFileSize(bytes: number): string;
54
55
  export declare function sanitizeRichTextHtml(html: string): string;
55
56
  export declare function replaceTemplateVariables(content: string, context: TemplateContext): string;
56
- export declare function RichTextEditor({ placeholder, editorAriaLabel, header, isExpanded, onExpandToggle, onSend, className, maxFileSize, maxFileSizeLabel, acceptedFileTypes, acceptedFileTypesLabel, isSending, showTemplates, templates, templateContext, templateCategoryLabels, templateCategoryOrder, templateCreateHref, templateCreateLabel, onFileRejected, colorPalette, sendLabel, sendingLabel, sendActions, }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;
57
+ export declare function RichTextEditor({ variant, placeholder, editorAriaLabel, header, isExpanded, onExpandToggle, onSend, className, maxFileSize, maxFileSizeLabel, acceptedFileTypes, acceptedFileTypesLabel, isSending, showTemplates, templates, templateContext, templateCategoryLabels, templateCategoryOrder, templateCreateHref, templateCreateLabel, onFileRejected, colorPalette, sendLabel, sendingLabel, sendActions, }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;