@neasg/design-system 0.4.35 → 0.4.95

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 (54) hide show
  1. package/dist/activity-card.d.ts +29 -0
  2. package/dist/activity-card.js +65 -0
  3. package/dist/autocomplete.d.ts +84 -0
  4. package/dist/autocomplete.js +267 -0
  5. package/dist/avatar.d.ts +3 -1
  6. package/dist/avatar.js +27 -10
  7. package/dist/badge.js +1 -1
  8. package/dist/button.d.ts +1 -1
  9. package/dist/button.js +1 -0
  10. package/dist/calendar.js +1 -1
  11. package/dist/card.d.ts +1 -1
  12. package/dist/card.js +1 -1
  13. package/dist/compound-search-input.d.ts +8 -0
  14. package/dist/compound-search-input.js +9 -0
  15. package/dist/date-input.js +3 -3
  16. package/dist/dialog.d.ts +3 -1
  17. package/dist/dialog.js +6 -2
  18. package/dist/draggable-tabs.js +9 -8
  19. package/dist/index.d.ts +12 -4
  20. package/dist/index.js +6 -2
  21. package/dist/label-value-grid.js +6 -4
  22. package/dist/layout.d.ts +39 -6
  23. package/dist/layout.js +118 -42
  24. package/dist/lib/utils.js +1 -0
  25. package/dist/message-item.d.ts +23 -3
  26. package/dist/message-item.js +115 -16
  27. package/dist/notification.d.ts +14 -3
  28. package/dist/notification.js +32 -7
  29. package/dist/popover-menu.d.ts +4 -2
  30. package/dist/popover-menu.js +65 -5
  31. package/dist/popover.d.ts +5 -2
  32. package/dist/popover.js +3 -3
  33. package/dist/rich-text-editor.d.ts +2 -1
  34. package/dist/rich-text-editor.js +28 -3
  35. package/dist/routing-timeline.d.ts +7 -2
  36. package/dist/routing-timeline.js +67 -29
  37. package/dist/search-input.d.ts +4 -0
  38. package/dist/search-input.js +2 -2
  39. package/dist/select-primitive.js +1 -1
  40. package/dist/select.d.ts +3 -1
  41. package/dist/select.js +9 -4
  42. package/dist/status-dot.d.ts +13 -0
  43. package/dist/status-dot.js +19 -0
  44. package/dist/styles.css +0 -19
  45. package/dist/table-column-filter.js +7 -5
  46. package/dist/table.d.ts +9 -1
  47. package/dist/table.js +25 -11
  48. package/dist/tabs.d.ts +1 -1
  49. package/dist/tabs.js +6 -4
  50. package/dist/textarea.d.ts +1 -0
  51. package/dist/textarea.js +3 -2
  52. package/dist/theme-switcher.d.ts +3 -1
  53. package/dist/theme-switcher.js +8 -2
  54. package/package.json +1 -1
package/dist/layout.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
5
- import { GripVertical, PanelRightClose, PanelRightOpen } from "lucide-react";
5
+ import { ChevronsUpDown, GripVertical, PanelRightClose, PanelRightOpen, } from "lucide-react";
6
6
  import { ChevronDownIcon } from "./animated-icons/chevron-down";
7
7
  import { ChevronLeftIcon } from "./animated-icons/chevron-left";
8
8
  import { ChevronRightIcon } from "./animated-icons/chevron-right";
@@ -10,6 +10,7 @@ import { Avatar, AvatarProfile } from "./avatar";
10
10
  import { AvatarProfilePopover, } from "./avatar-profile-popover";
11
11
  import { Button } from "./button";
12
12
  import { CountBadge } from "./count-badge";
13
+ import { EmptyState } from "./empty-state";
13
14
  import { ScrollHint } from "./scroll-hint";
14
15
  import { Tabs } from "./tabs";
15
16
  import { Tooltip } from "./tooltip";
@@ -17,6 +18,7 @@ import { Typography } from "./typography";
17
18
  import { cn } from "./lib/utils";
18
19
  import { rightPanelEdgeClassName, rightPanelEdgeClosedClassName, rightPanelEdgeOpenClassName, rightPanelResizeHandleSurfaceClassName, } from "./layout-right-panel-edge";
19
20
  const COLLAPSE_BREAKPOINT = 1800;
21
+ const SIDEBAR_CONTENT_CROSSFADE_MS = 100;
20
22
  const RIGHT_PANEL_AUTO_COLLAPSE_BREAKPOINT = 1800;
21
23
  const RIGHT_PANEL_TRANSITION_MS = 300;
22
24
  const RIGHT_PANEL_RESIZE_DEFAULT_WIDTH = 432;
@@ -81,32 +83,77 @@ function useScrollOverflowHint() {
81
83
  }
82
84
  export function SidebarProvider({ children, defaultCollapsed = false, }) {
83
85
  const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed);
86
+ const [isContentCollapsed, setIsContentCollapsed] = React.useState(defaultCollapsed);
87
+ const [isContentVisible, setIsContentVisible] = React.useState(true);
88
+ const isCollapsedRef = React.useRef(defaultCollapsed);
89
+ const isContentCollapsedRef = React.useRef(defaultCollapsed);
84
90
  const manualCollapsedRef = React.useRef(defaultCollapsed);
91
+ const collapseTimerRef = React.useRef(null);
92
+ const clearCollapseTimer = React.useCallback(() => {
93
+ if (collapseTimerRef.current === null)
94
+ return;
95
+ clearTimeout(collapseTimerRef.current);
96
+ collapseTimerRef.current = null;
97
+ }, []);
98
+ const updateCollapsedState = React.useCallback((nextCollapsed) => {
99
+ var _a;
100
+ clearCollapseTimer();
101
+ isCollapsedRef.current = nextCollapsed;
102
+ setIsCollapsed(nextCollapsed);
103
+ const reduceMotion = typeof window !== "undefined" &&
104
+ ((_a = window.matchMedia) === null || _a === void 0 ? void 0 : _a.call(window, "(prefers-reduced-motion: reduce)").matches);
105
+ if (reduceMotion) {
106
+ isContentCollapsedRef.current = nextCollapsed;
107
+ setIsContentCollapsed(nextCollapsed);
108
+ setIsContentVisible(true);
109
+ return;
110
+ }
111
+ if (isContentCollapsedRef.current === nextCollapsed) {
112
+ setIsContentVisible(true);
113
+ return;
114
+ }
115
+ setIsContentVisible(false);
116
+ collapseTimerRef.current = setTimeout(() => {
117
+ collapseTimerRef.current = null;
118
+ if (isCollapsedRef.current === nextCollapsed) {
119
+ isContentCollapsedRef.current = nextCollapsed;
120
+ setIsContentCollapsed(nextCollapsed);
121
+ setIsContentVisible(true);
122
+ }
123
+ }, SIDEBAR_CONTENT_CROSSFADE_MS);
124
+ }, [clearCollapseTimer]);
85
125
  const collapse = React.useCallback(() => {
86
- setIsCollapsed(true);
87
126
  manualCollapsedRef.current = true;
88
- }, []);
89
- const toggle = () => {
90
- setIsCollapsed((prev) => {
91
- const next = !prev;
92
- manualCollapsedRef.current = next;
93
- return next;
94
- });
95
- };
127
+ updateCollapsedState(true);
128
+ }, [updateCollapsedState]);
129
+ const toggle = React.useCallback(() => {
130
+ const nextCollapsed = !isCollapsedRef.current;
131
+ manualCollapsedRef.current = nextCollapsed;
132
+ updateCollapsedState(nextCollapsed);
133
+ }, [updateCollapsedState]);
96
134
  React.useEffect(() => {
97
135
  const handleResize = () => {
98
136
  if (window.innerWidth < COLLAPSE_BREAKPOINT) {
99
- setIsCollapsed(true);
137
+ updateCollapsedState(true);
100
138
  }
101
139
  else {
102
- setIsCollapsed(manualCollapsedRef.current);
140
+ updateCollapsedState(manualCollapsedRef.current);
103
141
  }
104
142
  };
105
143
  handleResize();
106
144
  window.addEventListener("resize", handleResize);
107
- return () => window.removeEventListener("resize", handleResize);
108
- }, []);
109
- return (_jsx(SidebarContext.Provider, { value: { isCollapsed, toggle, collapse }, children: children }));
145
+ return () => {
146
+ window.removeEventListener("resize", handleResize);
147
+ clearCollapseTimer();
148
+ };
149
+ }, [clearCollapseTimer, updateCollapsedState]);
150
+ return (_jsx(SidebarContext.Provider, { value: {
151
+ isCollapsed,
152
+ isContentCollapsed,
153
+ isContentVisible,
154
+ toggle,
155
+ collapse,
156
+ }, children: children }));
110
157
  }
111
158
  export function useSidebar() {
112
159
  const context = React.useContext(SidebarContext);
@@ -142,10 +189,10 @@ function DashboardLayout({ className, masthead, prototypeBanner, sidebar, rightP
142
189
  }, [masthead, prototypeBanner]);
143
190
  return (_jsx(SidebarProvider, { defaultCollapsed: defaultSidebarCollapsed, children: _jsxs("div", { className: cn("flex min-h-screen flex-col bg-[hsl(var(--layout-shell-background))]", className), style: layoutStyle, ...props, children: [masthead || prototypeBanner ? (_jsxs("div", { ref: chromeRef, className: "sticky top-0 z-40 shrink-0", children: [masthead, prototypeBanner] })) : null, _jsxs("div", { className: "flex min-h-0 flex-1", children: [sidebar, _jsx(DashboardContent, { className: contentClassName, hasSidebar: Boolean(sidebar), rightPanel: rightPanel, rightPanelClassName: rightPanelClassName, rightPanelFallbackTrigger: rightPanelFallbackTrigger, rightPanelMode: rightPanelMode, rightPanelResizable: rightPanelResizable, rightPanelCollapsible: rightPanelCollapsible, rightPanelCollapsed: rightPanelCollapsed, defaultRightPanelCollapsed: defaultRightPanelCollapsed, onRightPanelCollapsedChange: onRightPanelCollapsedChange, rightPanelDefaultWidth: rightPanelDefaultWidth, rightPanelMinWidth: rightPanelMinWidth, rightPanelMaxWidth: rightPanelMaxWidth, rightPanelAutoCollapseSidebarBreakpoint: rightPanelAutoCollapseSidebarBreakpoint, surfaceClassName: surfaceClassName, children: children })] })] }) }));
144
191
  }
145
- function SidebarShell({ isCollapsed, className, children, ...props }) {
146
- return (_jsx("aside", { className: cn("fixed left-0 top-[var(--layout-top-offset)] z-30 flex h-[calc(100dvh-var(--layout-top-offset))] shrink-0 flex-col bg-[hsl(var(--layout-sidebar-background))] pb-2 pt-[var(--layout-sidebar-padding-top)] transition-all duration-300 ease-in-out", isCollapsed
147
- ? "w-[var(--layout-sidebar-collapsed-width)] overflow-visible px-[var(--layout-sidebar-padding-inline)]"
148
- : "w-[var(--layout-sidebar-width)] overflow-hidden px-[var(--layout-sidebar-padding-inline)]", className), ...props, children: children }));
192
+ function SidebarShell({ isCollapsed, isContentCollapsed, isContentVisible, className, children, ...props }) {
193
+ return (_jsx("aside", { className: cn("fixed left-0 top-[var(--layout-top-offset)] z-30 flex h-[calc(100dvh-var(--layout-top-offset))] shrink-0 flex-col bg-[hsl(var(--layout-sidebar-background))] pb-2 pt-[var(--layout-sidebar-padding-top)] transition-[width] duration-300 ease-in-out will-change-[width] motion-reduce:transition-none", isCollapsed
194
+ ? "w-[var(--layout-sidebar-collapsed-width)] px-[var(--layout-sidebar-padding-inline)]"
195
+ : "w-[var(--layout-sidebar-width)] px-[var(--layout-sidebar-padding-inline)]", isContentCollapsed ? "overflow-visible" : "overflow-hidden", className), ...props, children: _jsx("div", { "data-slot": "sidebar-transition-content", className: cn("flex min-h-0 flex-1 flex-col transition-opacity duration-100 ease-out motion-reduce:transition-none", isContentVisible ? "opacity-100" : "opacity-0"), children: children }) }));
149
196
  }
150
197
  function SidebarHeader({ className, children, }) {
151
198
  return _jsx("div", { className: cn("shrink-0", className), children: children });
@@ -242,7 +289,9 @@ function getDashboardSidebarProfileAvatarOverlay(status) {
242
289
  return (_jsx("span", { className: cn("absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-[hsl(var(--layout-sidebar-background))] shadow-sm", getDashboardSidebarProfileStatusClassName(status)) }));
243
290
  }
244
291
  function DashboardSidebarProfileView({ isCollapsed = false, status = "online", name, role, rolePlacement = "below", size = "sm", ...props }) {
245
- return (_jsx(AvatarProfile, { ...props, size: size, name: isCollapsed ? undefined : name, role: isCollapsed ? undefined : role, rolePlacement: rolePlacement, className: cn("rounded-control py-2 text-muted-foreground transition-colors hover:bg-card hover:text-foreground", isCollapsed ? "justify-center px-0" : "min-w-0 px-1.5"), avatarClassName: "h-9 w-9 ring-1 ring-border/60 shadow-sm", fallbackClassName: "bg-background text-foreground", avatarOverlay: getDashboardSidebarProfileAvatarOverlay(status) }));
292
+ return (_jsx(AvatarProfile, { ...props, size: size, name: isCollapsed ? undefined : name, role: isCollapsed ? undefined : role, rolePlacement: rolePlacement, className: cn("w-full items-center rounded-control text-muted-foreground transition-colors hover:bg-card hover:text-foreground", isCollapsed
293
+ ? "justify-center p-0"
294
+ : "min-w-0 gap-[8px] p-[10px]"), avatarClassName: "mt-0 h-[32px] w-[32px] rounded-[10px]", fallbackClassName: "rounded-[10px]", contentClassName: "max-w-[120px] gap-[2px]", nameClassName: "font-semibold leading-none", roleClassName: "leading-none text-foreground", topInfoClassName: "leading-none text-foreground", bottomInfoClassName: "leading-none text-foreground", avatarOverlay: getDashboardSidebarProfileAvatarOverlay(status) }));
246
295
  }
247
296
  function DashboardSidebarProfile({ isCollapsed = false, ...props }) {
248
297
  return (_jsx("div", { className: cn(isCollapsed ? "px-0" : "px-1", "pb-4 pt-2"), children: _jsx(DashboardSidebarProfileView, { isCollapsed: isCollapsed, ...props }) }));
@@ -254,15 +303,13 @@ const DashboardSidebarProfileTrigger = React.forwardRef(({ isCollapsed = false,
254
303
  : "Open profile menu");
255
304
  const resolvedTop = topInfo !== null && topInfo !== void 0 ? topInfo : (rolePlacement === "above" ? role : undefined);
256
305
  const resolvedBottom = bottomInfo !== null && bottomInfo !== void 0 ? bottomInfo : (rolePlacement === "below" ? role : undefined);
257
- const hasSupportingInfo = (resolvedTop !== undefined && resolvedTop !== null) ||
258
- (resolvedBottom !== undefined && resolvedBottom !== null);
259
306
  const hasMeta = name !== undefined ||
260
307
  nameTrailing !== undefined ||
261
308
  resolvedTop !== undefined ||
262
309
  resolvedBottom !== undefined;
263
- const infoClassName = "min-w-0 truncate text-xs leading-4 text-muted-foreground";
264
- const nameNode = name !== undefined && name !== null ? (_jsx("span", { className: "min-w-0 truncate text-sm font-medium leading-5 text-foreground", children: name })) : null;
265
- return (_jsx("div", { className: cn(isCollapsed ? "px-0" : "px-1", "pb-4 pt-2"), children: _jsx("button", { ...buttonProps, ref: ref, type: "button", "aria-label": profileLabel, "aria-busy": loading ? "true" : undefined, className: cn("group/profile-trigger block h-auto w-full cursor-pointer rounded-control border-0 bg-transparent p-0 text-left text-muted-foreground ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0", isCollapsed ? "justify-center" : "justify-start", triggerClassName), children: _jsxs("span", { className: cn("flex min-w-0 max-w-full rounded-control py-2 transition-colors hover:bg-card hover:text-foreground", hasSupportingInfo ? "items-start" : "items-center", isCollapsed ? "justify-center px-0" : "gap-2.5 px-1.5"), children: [_jsxs("span", { className: "relative shrink-0", children: [loading ? (_jsx("span", { "aria-hidden": "true", className: cn("block h-9 w-9 shrink-0 animate-pulse rounded-full bg-muted ring-1 ring-border/60 shadow-sm", hasSupportingInfo && "mt-0.5") })) : (_jsx(Avatar, { src: src, alt: alt, fallback: fallback, size: size, className: cn("h-9 w-9 ring-1 ring-border/60 shadow-sm", hasSupportingInfo && "mt-0.5") })), getDashboardSidebarProfileAvatarOverlay(status)] }), !isCollapsed && hasMeta ? (_jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-[2px]", children: [resolvedTop !== undefined && resolvedTop !== null ? (_jsx("span", { className: infoClassName, children: resolvedTop })) : null, nameTrailing !== undefined && nameTrailing !== null ? (_jsxs("span", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", children: [nameNode, nameTrailing] })) : (nameNode), resolvedBottom !== undefined && resolvedBottom !== null ? (_jsx("span", { className: infoClassName, children: resolvedBottom })) : null] })) : null, !isCollapsed ? (_jsx(ChevronDownIcon, { "aria-hidden": "true", className: cn("mt-2 shrink-0 text-muted-foreground transition-transform duration-200", "group-data-[state=open]/profile-trigger:rotate-180 group-hover/profile-trigger:text-foreground"), size: 16 })) : null] }) }) }));
310
+ const infoClassName = "min-w-0 truncate text-xs leading-none text-foreground";
311
+ const nameNode = name !== undefined && name !== null ? (_jsx("span", { className: "min-w-0 truncate text-sm font-semibold leading-none text-foreground", children: name })) : null;
312
+ return (_jsx("div", { className: cn(isCollapsed ? "px-0" : "px-1", "pb-4 pt-2"), children: _jsx("button", { ...buttonProps, ref: ref, type: "button", "aria-label": profileLabel, "aria-busy": loading ? "true" : undefined, className: cn("group/profile-trigger block h-auto w-full cursor-pointer rounded-control border-0 bg-transparent p-0 text-left text-muted-foreground ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0", isCollapsed ? "justify-center" : "justify-start", triggerClassName), children: _jsxs("span", { className: cn("flex w-full min-w-0 max-w-full items-center rounded-control transition-colors hover:bg-card hover:text-foreground", isCollapsed ? "justify-center p-0" : "gap-[8px] p-[10px]"), children: [_jsxs("span", { className: "relative shrink-0", children: [loading ? (_jsx("span", { "aria-hidden": "true", className: "block h-[32px] w-[32px] shrink-0 animate-pulse rounded-[10px] bg-muted" })) : (_jsx(Avatar, { src: src, alt: alt, fallback: fallback, size: size, className: "h-[32px] w-[32px] rounded-[10px]", fallbackClassName: "rounded-[10px]" })), getDashboardSidebarProfileAvatarOverlay(status)] }), !isCollapsed && hasMeta ? (_jsxs("span", { className: "flex min-w-0 max-w-[120px] flex-1 flex-col gap-[2px]", children: [resolvedTop !== undefined && resolvedTop !== null ? (_jsx("span", { className: infoClassName, children: resolvedTop })) : null, nameTrailing !== undefined && nameTrailing !== null ? (_jsxs("span", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", children: [nameNode, nameTrailing] })) : (nameNode), resolvedBottom !== undefined && resolvedBottom !== null ? (_jsx("span", { className: infoClassName, children: resolvedBottom })) : null] })) : null, !isCollapsed ? (_jsx(ChevronsUpDown, { "aria-hidden": "true", className: "ml-auto shrink-0 text-foreground", size: 16 })) : null] }) }) }));
266
313
  });
267
314
  DashboardSidebarProfileTrigger.displayName = "DashboardSidebarProfileTrigger";
268
315
  function DashboardSidebarProfilePopover({ align = "end", alignOffset, avoidCollisions, children, collisionPadding = 8, contentClassName, defaultOpen, modal, onOpenChange, open, side = "right", sideOffset = 4, triggerAriaLabel, triggerClassName, ...profileProps }) {
@@ -315,44 +362,73 @@ function DashboardSidebarItem({ item, isCollapsed, depth = 0, }) {
315
362
  }
316
363
  return (_jsx(DashboardSidebarLeafItem, { item: item, isCollapsed: isCollapsed, depth: depth }));
317
364
  }
318
- function DashboardSidebar({ className, children, logo, search, navGroups, footerItems, footer, toggleAriaLabel = "Toggle sidebar", ...props }) {
365
+ function DashboardSidebar({ className, children, logo, collapsedToggleIcon, search, navGroups, showNavigationDivider = true, footerItems, footer, toggleAriaLabel = "Toggle sidebar", ...props }) {
319
366
  var _a;
320
- const { isCollapsed, toggle } = useSidebar();
367
+ const { isCollapsed, isContentCollapsed, isContentVisible, toggle, } = useSidebar();
321
368
  const hasStructuredSidebar = logo !== undefined ||
322
369
  search !== undefined ||
323
370
  navGroups !== undefined ||
324
371
  footerItems !== undefined ||
325
372
  footer !== undefined;
326
373
  if (!hasStructuredSidebar) {
327
- return (_jsx(SidebarShell, { isCollapsed: isCollapsed, className: className, ...props, children: children }));
374
+ return (_jsx(SidebarShell, { isCollapsed: isCollapsed, isContentCollapsed: isContentCollapsed, isContentVisible: isContentVisible, className: className, ...props, children: children }));
328
375
  }
329
- return (_jsxs(SidebarShell, { isCollapsed: isCollapsed, className: className, ...props, children: [_jsxs(SidebarHeader, { className: cn("flex h-14 items-center gap-2", isCollapsed ? "justify-center px-0" : "justify-between px-3"), children: [renderSidebarSlot(logo, isCollapsed), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": toggleAriaLabel, className: cn("shrink-0 rounded-control", isCollapsed ? collapsedSidebarControlClassName : "h-7 w-7"), onClick: toggle, children: isCollapsed ? (_jsx(ChevronRightIcon, { size: 16 })) : (_jsx(ChevronLeftIcon, { size: 16 })) })] }), search ? (_jsx(SidebarHeader, { className: cn(isCollapsed
376
+ return (_jsxs(SidebarShell, { isCollapsed: isCollapsed, isContentCollapsed: isContentCollapsed, isContentVisible: isContentVisible, className: className, ...props, children: [_jsxs(SidebarHeader, { className: cn("flex h-14 items-center gap-2", isContentCollapsed
377
+ ? "justify-center px-0"
378
+ : "justify-between px-3"), children: [renderSidebarSlot(logo, isContentCollapsed), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": toggleAriaLabel, title: isContentCollapsed && collapsedToggleIcon
379
+ ? toggleAriaLabel
380
+ : undefined, className: cn("shrink-0 rounded-control", isContentCollapsed
381
+ ? collapsedSidebarControlClassName
382
+ : "h-7 w-7"), onClick: toggle, children: isContentCollapsed ? (collapsedToggleIcon !== null && collapsedToggleIcon !== void 0 ? collapsedToggleIcon : _jsx(ChevronRightIcon, { size: 16 })) : (_jsx(ChevronLeftIcon, { size: 16 })) })] }), search ? (_jsx(SidebarHeader, { className: cn(isContentCollapsed
330
383
  ? "flex justify-center px-0 pb-3 pt-2"
331
- : "px-2 pb-3 pt-2"), children: renderSidebarSlot(search, isCollapsed) })) : null, search || ((_a = navGroups === null || navGroups === void 0 ? void 0 : navGroups.length) !== null && _a !== void 0 ? _a : 0) > 0 ? (_jsx(SidebarHeader, { className: "mx-2 mb-2 border-t border-[hsl(var(--layout-sidebar-border))]" })) : null, (navGroups === null || navGroups === void 0 ? void 0 : navGroups.length) ? (_jsx(SidebarContent, { className: cn(isCollapsed
384
+ : "px-2 pb-3 pt-2"), children: renderSidebarSlot(search, isContentCollapsed) })) : null, showNavigationDivider &&
385
+ (search || ((_a = navGroups === null || navGroups === void 0 ? void 0 : navGroups.length) !== null && _a !== void 0 ? _a : 0) > 0) ? (_jsx(SidebarHeader, { className: "mx-2 mb-2 border-t border-[hsl(var(--layout-sidebar-border))]" })) : null, (navGroups === null || navGroups === void 0 ? void 0 : navGroups.length) ? (_jsx(SidebarContent, { className: cn(isContentCollapsed
332
386
  ? "overflow-visible p-[var(--layout-sidebar-content-padding)]"
333
387
  : "p-[var(--layout-sidebar-content-padding)]"), children: _jsx("nav", { "aria-label": "Sidebar navigation", children: navGroups.map((group, groupIndex) => {
334
388
  var _a;
335
- return (_jsx(SidebarGroup, { children: _jsxs("div", { className: cn(groupIndex > 0 && "mt-[var(--layout-sidebar-section-gap)]"), children: [group.label ? (_jsx(SidebarGroupLabel, { "aria-hidden": isCollapsed ? true : undefined, className: isCollapsed ? "invisible" : undefined, children: group.label })) : null, _jsx(SidebarMenu, { children: group.items.map((item) => (_jsx(SidebarMenuItem, { collapsed: isCollapsed, children: _jsx(DashboardSidebarItem, { item: item, isCollapsed: isCollapsed }) }, item.id))) })] }) }, `${(_a = group.label) !== null && _a !== void 0 ? _a : "group"}-${groupIndex}`));
336
- }) }) })) : null, (footerItems === null || footerItems === void 0 ? void 0 : footerItems.length) ? (_jsx(SidebarFooter, { className: cn(isCollapsed ? "px-0 py-1" : "px-1.5 py-1"), children: _jsx(SidebarMenu, { children: footerItems.map((item) => (_jsx(SidebarMenuItem, { collapsed: isCollapsed, children: _jsx(DashboardSidebarItem, { item: item, isCollapsed: isCollapsed }) }, item.id))) }) })) : null, footer ? _jsx(SidebarFooter, { className: "mx-1.5 mt-1.5 border-t border-[hsl(var(--layout-sidebar-border))] pt-1.5" }) : null, footer ? (_jsx(SidebarFooter, { children: renderSidebarSlot(footer, isCollapsed) })) : null] }));
389
+ return (_jsx(SidebarGroup, { children: _jsxs("div", { className: cn(groupIndex > 0 &&
390
+ "mt-[var(--layout-sidebar-section-gap)]"), children: [group.label ? (_jsx(SidebarGroupLabel, { "aria-hidden": isContentCollapsed ? true : undefined, className: isContentCollapsed ? "invisible" : undefined, children: group.label })) : null, _jsx(SidebarMenu, { children: group.items.map((item) => (_jsx(SidebarMenuItem, { collapsed: isContentCollapsed, children: _jsx(DashboardSidebarItem, { item: item, isCollapsed: isContentCollapsed }) }, item.id))) })] }) }, `${(_a = group.label) !== null && _a !== void 0 ? _a : "group"}-${groupIndex}`));
391
+ }) }) })) : null, (footerItems === null || footerItems === void 0 ? void 0 : footerItems.length) ? (_jsx(SidebarFooter, { className: cn(isContentCollapsed ? "px-0 py-1" : "px-1.5 py-1"), children: _jsx(SidebarMenu, { children: footerItems.map((item) => (_jsx(SidebarMenuItem, { collapsed: isContentCollapsed, children: _jsx(DashboardSidebarItem, { item: item, isCollapsed: isContentCollapsed }) }, item.id))) }) })) : null, footer ? _jsx(SidebarFooter, { className: "mx-1.5 mt-1.5 border-t border-[hsl(var(--layout-sidebar-border))] pt-1.5" }) : null, footer ? (_jsx(SidebarFooter, { children: renderSidebarSlot(footer, isContentCollapsed) })) : null] }));
337
392
  }
338
393
  function DashboardRightPanelSection({ className, title, description, titleClassName, descriptionClassName, children, ...props }) {
339
394
  return (_jsxs("section", { className: cn("space-y-[var(--layout-right-panel-section-gap)]", className), ...props, children: [title || description ? (_jsxs("div", { className: "space-y-[var(--layout-right-panel-heading-gap)]", children: [title ? (_jsx(Typography, { as: "h3", variant: "body", className: cn("font-semibold", titleClassName), children: title })) : null, description ? (_jsx(Typography, { as: "p", variant: "caption", className: descriptionClassName, children: description })) : null] })) : null, children] }));
340
395
  }
341
- function DashboardRightPanelTabs({ className, listWrapperClassName, panelClassName, ...props }) {
342
- return (_jsx(Tabs, { className: cn("flex min-h-0 flex-1 flex-col space-y-0", className), listWrapperClassName: cn("scrollbar-none w-full min-w-0 max-w-full shrink-0 self-stretch overflow-x-auto overflow-y-hidden pb-[var(--layout-right-panel-tabs-gap)] pl-[var(--layout-right-panel-padding)] pr-[calc(var(--layout-right-panel-padding)+var(--layout-right-panel-action-space,0px))] pt-[var(--layout-right-panel-padding)]", listWrapperClassName), panelClassName: cn("min-h-0 flex-1 flex-col overflow-hidden pt-0 data-[state=active]:flex", panelClassName), ...props }));
396
+ function DashboardRightPanelTabs({ className, listWrapperClassName, panelClassName, spacing = "default", size, ...props }) {
397
+ return (_jsx(Tabs, { className: cn("flex min-h-0 flex-1 flex-col space-y-0", className), listWrapperClassName: cn("scrollbar-none w-full min-w-0 max-w-full shrink-0 self-stretch overflow-x-auto overflow-y-hidden", spacing === "spacious"
398
+ ? "px-[32px] pb-[24px] pt-[24px]"
399
+ : "pb-[var(--layout-right-panel-tabs-gap)] pl-[var(--layout-right-panel-padding)] pr-[calc(var(--layout-right-panel-padding)+var(--layout-right-panel-action-space,0px))] pt-[var(--layout-right-panel-padding)]", listWrapperClassName), panelClassName: cn("min-h-0 flex-1 flex-col overflow-hidden pt-0 data-[state=active]:flex", panelClassName), size: size !== null && size !== void 0 ? size : (spacing === "spacious" ? "comfortable" : undefined), ...props }));
343
400
  }
344
- function DashboardRightPanel({ className, ...props }) {
345
- return (_jsx("div", { className: cn("flex min-h-0 flex-1 flex-col bg-[hsl(var(--layout-right-panel-background))]", className), ...props }));
401
+ const DashboardRightPanel = React.forwardRef(({ className, surface = "default", ...props }, ref) => (_jsx("div", { ref: ref, className: cn("flex min-h-0 flex-1 flex-col", surface === "plain"
402
+ ? "bg-background"
403
+ : "bg-[hsl(var(--layout-right-panel-background))]", className), ...props })));
404
+ DashboardRightPanel.displayName = "DashboardRightPanel";
405
+ function DashboardRightPanelHeader({ className, spacing = "default", title, subtitle, badge, action, children, ...props }) {
406
+ return (_jsx("div", { className: cn(spacing === "comfortable"
407
+ ? "shrink-0 p-[16px]"
408
+ : "shrink-0 pb-[var(--layout-right-panel-padding)] pl-[var(--layout-right-panel-padding)] pr-[calc(var(--layout-right-panel-padding)+var(--layout-right-panel-action-space,0px))] pt-[var(--layout-right-panel-padding)]", className), ...props, children: _jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: cn("min-w-0 flex-1", spacing === "comfortable" && "space-y-[4px]"), children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Typography, { as: "h2", variant: "subheading", children: title }), badge] }), subtitle ? (_jsx(Typography, { as: "p", variant: "caption", className: spacing === "comfortable"
409
+ ? "text-muted-foreground"
410
+ : undefined, children: subtitle })) : null, children] }), action ? _jsx("div", { className: "shrink-0", children: action }) : null] }) }));
346
411
  }
347
- function DashboardRightPanelHeader({ className, title, subtitle, badge, action, children, ...props }) {
348
- return (_jsx("div", { className: cn("shrink-0 pb-[var(--layout-right-panel-padding)] pl-[var(--layout-right-panel-padding)] pr-[calc(var(--layout-right-panel-padding)+var(--layout-right-panel-action-space,0px))] pt-[var(--layout-right-panel-padding)]", className), ...props, children: _jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Typography, { as: "h2", variant: "body", className: "font-semibold", children: title }), badge] }), subtitle ? (_jsx(Typography, { as: "p", variant: "caption", children: subtitle })) : null, children] }), action ? _jsx("div", { className: "shrink-0", children: action }) : null] }) }));
412
+ const DashboardActionPanel = React.forwardRef(({ panelId, headerSpacing, title, subtitle, badge, action, headerChildren, className, children, ...props }, ref) => (_jsxs(DashboardRightPanel, { ref: ref, "data-slot": "dashboard-action-panel", "data-panel-id": panelId, className: cn("h-full min-h-0 w-full", className), ...props, children: [_jsx(DashboardRightPanelHeader, { spacing: headerSpacing, title: title, subtitle: subtitle, badge: badge, action: action, children: headerChildren }), children] })));
413
+ DashboardActionPanel.displayName = "DashboardActionPanel";
414
+ function DashboardActionPanelEmptyState(props) {
415
+ return (_jsx("div", { "data-slot": "dashboard-action-panel-empty-state", className: "absolute inset-0 flex items-center justify-center overflow-y-auto", children: _jsx(EmptyState, { ...props }) }));
349
416
  }
350
- function DashboardRightPanelBody({ className, contentClassName, footer, footerClassName, children, ...props }) {
417
+ function DashboardRightPanelBody({ className, contentClassName, footer, footerVariant = "inline", footerClassName, spacing = "default", topInset = "default", children, ...props }) {
351
418
  const { scrollRef, canScrollDown } = useScrollOverflowHint();
419
+ const spacingClassName = spacing === "spacious"
420
+ ? "px-[32px] py-[24px]"
421
+ : spacing === "comfortable"
422
+ ? "p-[16px]"
423
+ : "p-[var(--layout-right-panel-padding)]";
424
+ const topInsetClassName = topInset === "none" ? "pt-0" : undefined;
352
425
  if (footer) {
353
- return (_jsxs("div", { className: cn("flex min-h-0 flex-1 flex-col overflow-hidden p-[var(--layout-right-panel-padding)]", className), ...props, children: [_jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx("div", { ref: scrollRef, className: cn("scrollbar-none h-full min-h-0 space-y-[var(--layout-right-panel-gap)] overflow-y-auto", contentClassName), children: children }), _jsx(ScrollHint, { visible: canScrollDown })] }), _jsx("div", { className: cn("shrink-0 space-y-[var(--layout-right-panel-footer-gap)] pt-[var(--layout-right-panel-gap)]", footerClassName), children: footer })] }));
426
+ if (footerVariant === "separated") {
427
+ return (_jsxs("div", { className: cn("flex min-h-0 flex-1 flex-col overflow-hidden", className), ...props, children: [_jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx("div", { ref: scrollRef, "data-slot": "dashboard-right-panel-scroll-area", className: cn("scrollbar-none h-full min-h-0 overflow-y-auto", spacingClassName, topInsetClassName, contentClassName), children: children }), _jsx(ScrollHint, { visible: canScrollDown })] }), _jsx("div", { className: cn("shrink-0 border-t border-border bg-muted/50", spacingClassName, footerClassName), children: footer })] }));
428
+ }
429
+ return (_jsxs("div", { className: cn("flex min-h-0 flex-1 flex-col overflow-hidden", spacingClassName, topInsetClassName, className), ...props, children: [_jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx("div", { ref: scrollRef, "data-slot": "dashboard-right-panel-scroll-area", className: cn("scrollbar-none h-full min-h-0 space-y-[var(--layout-right-panel-gap)] overflow-y-auto", contentClassName), children: children }), _jsx(ScrollHint, { visible: canScrollDown })] }), _jsx("div", { className: cn("shrink-0 space-y-[var(--layout-right-panel-footer-gap)] pt-[var(--layout-right-panel-gap)]", footerClassName), children: footer })] }));
354
430
  }
355
- return (_jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx("div", { ref: scrollRef, className: cn("scrollbar-none h-full min-h-0 space-y-[var(--layout-right-panel-gap)] overflow-y-auto p-[var(--layout-right-panel-padding)]", className), ...props, children: children }), _jsx(ScrollHint, { visible: canScrollDown })] }));
431
+ return (_jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx("div", { ref: scrollRef, "data-slot": "dashboard-right-panel-scroll-area", className: cn("scrollbar-none h-full min-h-0 space-y-[var(--layout-right-panel-gap)] overflow-y-auto", spacingClassName, topInsetClassName, className), ...props, children: children }), _jsx(ScrollHint, { visible: canScrollDown })] }));
356
432
  }
357
433
  function DashboardContent({ className, hasSidebar = true, rightPanel, rightPanelClassName, rightPanelFallbackTrigger, rightPanelMode = "responsive", rightPanelResizable = false, rightPanelCollapsible = false, rightPanelCollapsed, defaultRightPanelCollapsed = false, onRightPanelCollapsedChange, rightPanelDefaultWidth = RIGHT_PANEL_RESIZE_DEFAULT_WIDTH, rightPanelMinWidth = RIGHT_PANEL_RESIZE_MIN_WIDTH, rightPanelMaxWidth = RIGHT_PANEL_RESIZE_MAX_WIDTH, rightPanelAutoCollapseSidebarBreakpoint = RIGHT_PANEL_AUTO_COLLAPSE_BREAKPOINT, surfaceClassName, children, ...props }) {
358
434
  const { isCollapsed, toggle, collapse } = useSidebar();
@@ -725,4 +801,4 @@ function DashboardContent({ className, hasSidebar = true, rightPanel, rightPanel
725
801
  ? "translate-x-0"
726
802
  : "pointer-events-none translate-x-full", rightPanelClassName), children: _jsx("div", { className: "scrollbar-none flex h-full w-full flex-col overflow-y-auto overflow-x-hidden", children: renderedRightPanel }) })] })) : null] }));
727
803
  }
728
- export { DashboardLayout, DashboardSidebar, DashboardSidebarProfile, DashboardSidebarProfilePopover, DashboardContent, DashboardRightPanel, DashboardRightPanelHeader, DashboardRightPanelBody, DashboardRightPanelSection, DashboardRightPanelTabs, };
804
+ export { DashboardLayout, DashboardSidebar, DashboardSidebarProfile, DashboardSidebarProfilePopover, DashboardContent, DashboardActionPanel, DashboardActionPanelEmptyState, DashboardRightPanel, DashboardRightPanelHeader, DashboardRightPanelBody, DashboardRightPanelSection, DashboardRightPanelTabs, };
package/dist/lib/utils.js CHANGED
@@ -4,6 +4,7 @@ const twMerge = extendTailwindMerge({
4
4
  extend: {
5
5
  theme: {
6
6
  borderRadius: ["control", "surface", "overlay"],
7
+ spacing: ["control"],
7
8
  },
8
9
  },
9
10
  });
@@ -11,6 +11,9 @@ export interface MessageItemData {
11
11
  senderRole: "OFFICER" | "APPLICANT" | "SYSTEM";
12
12
  officerName?: string | null;
13
13
  officerRole?: string | null;
14
+ avatarUrl?: string | null;
15
+ systemLabel?: string | null;
16
+ senderBadgeLabel?: React.ReactNode;
14
17
  applicantEmail?: string | null;
15
18
  applicantName?: string | null;
16
19
  content: string;
@@ -24,12 +27,17 @@ export interface MessageItemProps {
24
27
  caseId?: string;
25
28
  showNewBadge?: boolean;
26
29
  newMessageLabel?: React.ReactNode;
30
+ /** Controls whether unread state is shown as an inline badge or a divider before the message. */
31
+ newIndicatorPresentation?: "badge" | "divider";
27
32
  /** @deprecated Unread messages render a divider. Use `newMessageLabel` to customize its text. */
28
33
  newIndicator?: React.ReactNode;
29
34
  onMarkAsRead?: (message: MessageItemData) => Promise<boolean | void> | boolean | void;
30
35
  onMarkedAsRead?: (message: MessageItemData) => void;
31
36
  getAttachmentDownloadUrl?: (attachment: MessageItemAttachment, message: MessageItemData, caseId?: string) => string | null | undefined;
32
37
  formatTimestamp?: (date: Date, message: MessageItemData) => React.ReactNode;
38
+ variant?: "default" | "card";
39
+ /** `compact` reduces card insets for constrained correspondence panels. */
40
+ density?: "default" | "compact";
33
41
  className?: string;
34
42
  contentClassName?: string;
35
43
  }
@@ -37,6 +45,7 @@ export interface MessageAttachmentRowProps extends React.HTMLAttributes<HTMLElem
37
45
  attachment: MessageItemAttachment;
38
46
  downloadUrl?: string | null;
39
47
  downloadLabel?: string;
48
+ variant?: "default" | "compact";
40
49
  }
41
50
  export interface MessageNewDividerProps {
42
51
  children?: React.ReactNode;
@@ -47,16 +56,27 @@ export interface MessageThreadProps {
47
56
  caseId?: string;
48
57
  showNewBadge?: boolean;
49
58
  newMessageLabel?: React.ReactNode;
59
+ /** Controls whether unread state is shown as an inline badge or a divider before the first unread message. */
60
+ newIndicatorPresentation?: "badge" | "divider";
50
61
  onMarkAsRead?: MessageItemProps["onMarkAsRead"];
51
62
  onMarkedAsRead?: MessageItemProps["onMarkedAsRead"];
52
63
  getAttachmentDownloadUrl?: MessageItemProps["getAttachmentDownloadUrl"];
53
64
  formatTimestamp?: MessageItemProps["formatTimestamp"];
65
+ variant?: MessageItemProps["variant"];
66
+ density?: MessageItemProps["density"];
67
+ groupByDate?: boolean;
68
+ /** Keeps timestamps in each card while omitting redundant date-group headings. */
69
+ showDateHeadings?: boolean;
70
+ formatDateHeading?: (date: Date) => React.ReactNode;
54
71
  className?: string;
55
72
  itemClassName?: string;
56
73
  contentClassName?: string;
57
74
  }
58
75
  export declare const formatMessageDate: (date: Date) => string;
76
+ export declare const formatMessageTime: (date: Date) => string;
77
+ export declare const formatMessageCardTimestamp: (date: Date, now?: Date) => string;
78
+ export declare const formatMessageDateHeading: (date: Date, now?: Date) => string;
59
79
  export declare function MessageNewDivider({ children, className, }: MessageNewDividerProps): import("react/jsx-runtime").JSX.Element;
60
- export declare function MessageAttachmentRow({ attachment, downloadUrl, downloadLabel, className, ...props }: MessageAttachmentRowProps): import("react/jsx-runtime").JSX.Element;
61
- export declare function MessageItem({ message, caseId, showNewBadge, newMessageLabel, onMarkAsRead, onMarkedAsRead, getAttachmentDownloadUrl, formatTimestamp, className, contentClassName, }: MessageItemProps): import("react/jsx-runtime").JSX.Element;
62
- export declare function MessageThread({ messages, caseId, showNewBadge, newMessageLabel, onMarkAsRead, onMarkedAsRead, getAttachmentDownloadUrl, formatTimestamp, className, itemClassName, contentClassName, }: MessageThreadProps): import("react/jsx-runtime").JSX.Element;
80
+ export declare function MessageAttachmentRow({ attachment, downloadUrl, downloadLabel, variant, className, ...props }: MessageAttachmentRowProps): import("react/jsx-runtime").JSX.Element;
81
+ export declare function MessageItem({ message, caseId, showNewBadge, newMessageLabel, newIndicatorPresentation, onMarkAsRead, onMarkedAsRead, getAttachmentDownloadUrl, formatTimestamp, variant, density, className, contentClassName, }: MessageItemProps): import("react/jsx-runtime").JSX.Element;
82
+ export declare function MessageThread({ messages, caseId, showNewBadge, newMessageLabel, newIndicatorPresentation, onMarkAsRead, onMarkedAsRead, getAttachmentDownloadUrl, formatTimestamp, variant, density, groupByDate, showDateHeadings, formatDateHeading: formatDateHeadingProp, className, itemClassName, contentClassName, }: MessageThreadProps): import("react/jsx-runtime").JSX.Element;
@@ -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
  }