@neasg/design-system 0.4.8 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/dist/alert.d.ts +21 -2
- package/dist/alert.js +10 -3
- package/dist/avatar.d.ts +5 -2
- package/dist/avatar.js +76 -8
- package/dist/back-button.d.ts +2 -0
- package/dist/back-button.js +1 -1
- package/dist/badge.d.ts +3 -2
- package/dist/badge.js +9 -3
- package/dist/breadcrumb.js +2 -1
- package/dist/button.d.ts +1 -1
- package/dist/button.js +12 -9
- package/dist/calendar.js +10 -9
- package/dist/card.d.ts +29 -1
- package/dist/card.js +61 -7
- package/dist/checkbox.d.ts +2 -0
- package/dist/checkbox.js +9 -3
- package/dist/close-button.d.ts +6 -0
- package/dist/close-button.js +8 -0
- package/dist/collapsible.d.ts +7 -1
- package/dist/collapsible.js +2 -2
- package/dist/command-search.d.ts +16 -1
- package/dist/command-search.js +42 -16
- package/dist/command.js +3 -3
- package/dist/copy-button.d.ts +3 -1
- package/dist/copy-button.js +4 -2
- package/dist/count-badge.d.ts +3 -1
- package/dist/count-badge.js +10 -5
- package/dist/date-input.d.ts +18 -3
- package/dist/date-input.js +94 -21
- package/dist/dialog-primitive.js +4 -3
- package/dist/draggable-tabs.d.ts +7 -0
- package/dist/draggable-tabs.js +6 -5
- package/dist/drawer.js +2 -2
- package/dist/dropdown-button.d.ts +18 -0
- package/dist/dropdown-button.js +31 -0
- package/dist/edit-button.d.ts +10 -0
- package/dist/edit-button.js +7 -0
- package/dist/editable-table.d.ts +3 -1
- package/dist/editable-table.js +25 -12
- package/dist/field.d.ts +7 -3
- package/dist/field.js +8 -4
- package/dist/file-upload-status-toast.d.ts +28 -0
- package/dist/file-upload-status-toast.js +136 -0
- package/dist/file-upload.js +8 -7
- package/dist/filter-popover.d.ts +37 -0
- package/dist/filter-popover.js +73 -0
- package/dist/filter-toggle.js +2 -2
- package/dist/index.d.ts +45 -14
- package/dist/index.js +19 -4
- package/dist/input-control.d.ts +3 -2
- package/dist/input-control.js +3 -2
- package/dist/label-value-grid.d.ts +4 -2
- package/dist/label-value-grid.js +7 -6
- package/dist/label-value.js +3 -1
- package/dist/layout-right-panel-edge.d.ts +6 -0
- package/dist/layout-right-panel-edge.js +6 -0
- package/dist/layout.d.ts +77 -5
- package/dist/layout.js +439 -35
- package/dist/lib/date-utils.d.ts +10 -2
- package/dist/lib/date-utils.js +91 -11
- package/dist/lib/utils.js +8 -1
- package/dist/link.js +2 -2
- package/dist/message-item.d.ts +62 -0
- package/dist/message-item.js +148 -0
- package/dist/multi-select.js +31 -5
- package/dist/notification.d.ts +82 -0
- package/dist/notification.js +121 -0
- package/dist/otp-input.js +1 -1
- package/dist/page-header.js +1 -1
- package/dist/page-section.d.ts +2 -1
- package/dist/page-section.js +4 -3
- package/dist/pagination.js +2 -2
- package/dist/popover-menu.d.ts +23 -1
- package/dist/popover-menu.js +75 -5
- package/dist/popover-primitive.js +1 -1
- package/dist/progress.d.ts +5 -2
- package/dist/progress.js +6 -3
- package/dist/radio-group.d.ts +27 -0
- package/dist/radio-group.js +50 -0
- package/dist/rich-text-editor.d.ts +56 -0
- package/dist/rich-text-editor.js +394 -0
- package/dist/routing-timeline.d.ts +22 -0
- package/dist/routing-timeline.js +58 -0
- package/dist/scroll-hint.d.ts +8 -0
- package/dist/scroll-hint.js +7 -0
- package/dist/search-input-shell.js +1 -1
- package/dist/search-input.js +2 -2
- package/dist/section-nav.js +4 -4
- package/dist/select-primitive.js +3 -3
- package/dist/select.js +1 -1
- package/dist/skeleton.js +1 -1
- package/dist/styles.css +241 -55
- package/dist/table-column-visibility.d.ts +21 -0
- package/dist/table-column-visibility.js +74 -0
- package/dist/table-toolbar.js +1 -1
- package/dist/table.d.ts +12 -1
- package/dist/table.js +67 -33
- package/dist/tabs.d.ts +14 -3
- package/dist/tabs.js +60 -6
- package/dist/tailwind-preset.js +46 -0
- package/dist/task-mode-shell.d.ts +24 -0
- package/dist/task-mode-shell.js +110 -0
- package/dist/textarea.js +2 -1
- package/dist/theme-switcher.d.ts +28 -0
- package/dist/theme-switcher.js +27 -0
- package/dist/theme.d.ts +167 -0
- package/dist/theme.js +112 -0
- package/dist/toaster.js +5 -4
- package/dist/tooltip.js +1 -1
- package/dist/typography.d.ts +1 -1
- package/dist/typography.js +5 -5
- package/dist/workspace-header.d.ts +13 -0
- package/dist/workspace-header.js +11 -0
- package/package.json +18 -2
package/dist/lib/date-utils.js
CHANGED
|
@@ -1,16 +1,96 @@
|
|
|
1
|
+
const monthIndexes = {
|
|
2
|
+
jan: 0,
|
|
3
|
+
january: 0,
|
|
4
|
+
feb: 1,
|
|
5
|
+
february: 1,
|
|
6
|
+
mar: 2,
|
|
7
|
+
march: 2,
|
|
8
|
+
apr: 3,
|
|
9
|
+
april: 3,
|
|
10
|
+
may: 4,
|
|
11
|
+
jun: 5,
|
|
12
|
+
june: 5,
|
|
13
|
+
jul: 6,
|
|
14
|
+
july: 6,
|
|
15
|
+
aug: 7,
|
|
16
|
+
august: 7,
|
|
17
|
+
sep: 8,
|
|
18
|
+
sept: 8,
|
|
19
|
+
september: 8,
|
|
20
|
+
oct: 9,
|
|
21
|
+
october: 9,
|
|
22
|
+
nov: 10,
|
|
23
|
+
november: 10,
|
|
24
|
+
dec: 11,
|
|
25
|
+
december: 11,
|
|
26
|
+
};
|
|
27
|
+
function getHour24(hours, ampm) {
|
|
28
|
+
const parsedHours = Number.parseInt(hours, 10);
|
|
29
|
+
const normalizedAmpm = ampm.toUpperCase();
|
|
30
|
+
if (normalizedAmpm === "PM" && parsedHours !== 12) {
|
|
31
|
+
return parsedHours + 12;
|
|
32
|
+
}
|
|
33
|
+
if (normalizedAmpm === "AM" && parsedHours === 12) {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
return parsedHours;
|
|
37
|
+
}
|
|
38
|
+
function getValidDateTime(year, monthIndex, day, hour, minute) {
|
|
39
|
+
const date = new Date(year, monthIndex, day, hour, minute);
|
|
40
|
+
if (date.getFullYear() !== year ||
|
|
41
|
+
date.getMonth() !== monthIndex ||
|
|
42
|
+
date.getDate() !== day ||
|
|
43
|
+
date.getHours() !== hour ||
|
|
44
|
+
date.getMinutes() !== minute) {
|
|
45
|
+
return Number.NaN;
|
|
46
|
+
}
|
|
47
|
+
return date.getTime();
|
|
48
|
+
}
|
|
1
49
|
/**
|
|
2
|
-
* Parse
|
|
50
|
+
* Parse common CLIP date/time strings to a timestamp (ms).
|
|
51
|
+
*
|
|
52
|
+
* Supports numeric dates (`23/03/2025 01:51 AM`), compact meridiem times
|
|
53
|
+
* (`12/05/2026 10:00AM`), month-name dates (`13 Feb 2026, 03:13 PM`), and
|
|
54
|
+
* relative sandbox labels (`Today, 11:12 AM`, `Yesterday, 5:28 PM`).
|
|
3
55
|
*/
|
|
4
|
-
export function parseDateTime(dateStr) {
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
56
|
+
export function parseDateTime(dateStr, options = {}) {
|
|
57
|
+
var _a;
|
|
58
|
+
const value = dateStr.trim();
|
|
59
|
+
const referenceDate = (_a = options.referenceDate) !== null && _a !== void 0 ? _a : new Date();
|
|
60
|
+
const timePattern = "(\\d{1,2}):(\\d{2})\\s*([ap]m)";
|
|
61
|
+
const numericMatch = value.match(new RegExp(`^(\\d{1,2})/(\\d{1,2})/(\\d{4}),?\\s+${timePattern}$`, "i"));
|
|
62
|
+
if (numericMatch) {
|
|
63
|
+
const [, day, month, year, hours, minutes, ampm] = numericMatch;
|
|
64
|
+
return getValidDateTime(Number.parseInt(year, 10), Number.parseInt(month, 10) - 1, Number.parseInt(day, 10), getHour24(hours, ampm), Number.parseInt(minutes, 10));
|
|
65
|
+
}
|
|
66
|
+
const monthNameMatch = value.match(new RegExp(`^(\\d{1,2})\\s+([a-z]+)\\s+(\\d{4}),?\\s+${timePattern}$`, "i"));
|
|
67
|
+
if (monthNameMatch) {
|
|
68
|
+
const [, day, monthName, year, hours, minutes, ampm] = monthNameMatch;
|
|
69
|
+
const monthIndex = monthIndexes[monthName.toLowerCase()];
|
|
70
|
+
if (monthIndex === undefined) {
|
|
71
|
+
return Number.NaN;
|
|
72
|
+
}
|
|
73
|
+
return getValidDateTime(Number.parseInt(year, 10), monthIndex, Number.parseInt(day, 10), getHour24(hours, ampm), Number.parseInt(minutes, 10));
|
|
74
|
+
}
|
|
75
|
+
const monthNameWithoutYearMatch = value.match(new RegExp(`^(\\d{1,2})\\s+([a-z]+),?\\s+${timePattern}$`, "i"));
|
|
76
|
+
if (monthNameWithoutYearMatch) {
|
|
77
|
+
const [, day, monthName, hours, minutes, ampm] = monthNameWithoutYearMatch;
|
|
78
|
+
const monthIndex = monthIndexes[monthName.toLowerCase()];
|
|
79
|
+
if (monthIndex === undefined) {
|
|
80
|
+
return Number.NaN;
|
|
81
|
+
}
|
|
82
|
+
return getValidDateTime(referenceDate.getFullYear(), monthIndex, Number.parseInt(day, 10), getHour24(hours, ampm), Number.parseInt(minutes, 10));
|
|
83
|
+
}
|
|
84
|
+
const relativeMatch = value.match(new RegExp(`^(today|yesterday),?\\s+${timePattern}$`, "i"));
|
|
85
|
+
if (relativeMatch) {
|
|
86
|
+
const [, relativeDay, hours, minutes, ampm] = relativeMatch;
|
|
87
|
+
const date = new Date(referenceDate.getFullYear(), referenceDate.getMonth(), referenceDate.getDate());
|
|
88
|
+
if (relativeDay.toLowerCase() === "yesterday") {
|
|
89
|
+
date.setDate(date.getDate() - 1);
|
|
90
|
+
}
|
|
91
|
+
return getValidDateTime(date.getFullYear(), date.getMonth(), date.getDate(), getHour24(hours, ampm), Number.parseInt(minutes, 10));
|
|
92
|
+
}
|
|
93
|
+
return Number.NaN;
|
|
14
94
|
}
|
|
15
95
|
/**
|
|
16
96
|
* Format a Date object or ISO string to DD/MM/YYYY HH:MM AM/PM format.
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { clsx } from "clsx";
|
|
2
|
-
import {
|
|
2
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
3
|
+
const twMerge = extendTailwindMerge({
|
|
4
|
+
extend: {
|
|
5
|
+
theme: {
|
|
6
|
+
borderRadius: ["control", "surface", "overlay"],
|
|
7
|
+
},
|
|
8
|
+
},
|
|
9
|
+
});
|
|
3
10
|
export function cn(...inputs) {
|
|
4
11
|
return twMerge(clsx(inputs));
|
|
5
12
|
}
|
package/dist/link.js
CHANGED
|
@@ -2,10 +2,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
4
|
import { cn } from "./lib/utils";
|
|
5
|
-
const linkVariants = cva("rounded-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", {
|
|
5
|
+
const linkVariants = cva("rounded-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", {
|
|
6
6
|
variants: {
|
|
7
7
|
variant: {
|
|
8
|
-
default: "text-primary underline-offset-4 hover:underline",
|
|
8
|
+
default: "text-primary underline-offset-4 hover:text-primary/85 hover:underline",
|
|
9
9
|
subtle: "text-foreground underline-offset-4 hover:text-primary hover:underline",
|
|
10
10
|
inherit: "text-inherit no-underline hover:no-underline",
|
|
11
11
|
},
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface MessageItemAttachment {
|
|
3
|
+
name: string;
|
|
4
|
+
size?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
key?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MessageItemData {
|
|
9
|
+
id: string;
|
|
10
|
+
caseId?: string;
|
|
11
|
+
senderRole: "OFFICER" | "APPLICANT" | "SYSTEM";
|
|
12
|
+
officerName?: string | null;
|
|
13
|
+
officerRole?: string | null;
|
|
14
|
+
applicantEmail?: string | null;
|
|
15
|
+
applicantName?: string | null;
|
|
16
|
+
content: string;
|
|
17
|
+
attachments?: unknown[] | null;
|
|
18
|
+
isRead: boolean;
|
|
19
|
+
sentAt?: Date | null;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
}
|
|
22
|
+
export interface MessageItemProps {
|
|
23
|
+
message: MessageItemData;
|
|
24
|
+
caseId?: string;
|
|
25
|
+
showNewBadge?: boolean;
|
|
26
|
+
newMessageLabel?: React.ReactNode;
|
|
27
|
+
/** @deprecated Unread messages render a divider. Use `newMessageLabel` to customize its text. */
|
|
28
|
+
newIndicator?: React.ReactNode;
|
|
29
|
+
onMarkAsRead?: (message: MessageItemData) => Promise<boolean | void> | boolean | void;
|
|
30
|
+
onMarkedAsRead?: (message: MessageItemData) => void;
|
|
31
|
+
getAttachmentDownloadUrl?: (attachment: MessageItemAttachment, message: MessageItemData, caseId?: string) => string | null | undefined;
|
|
32
|
+
formatTimestamp?: (date: Date, message: MessageItemData) => React.ReactNode;
|
|
33
|
+
className?: string;
|
|
34
|
+
contentClassName?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface MessageAttachmentRowProps extends React.HTMLAttributes<HTMLElement> {
|
|
37
|
+
attachment: MessageItemAttachment;
|
|
38
|
+
downloadUrl?: string | null;
|
|
39
|
+
downloadLabel?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface MessageNewDividerProps {
|
|
42
|
+
children?: React.ReactNode;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface MessageThreadProps {
|
|
46
|
+
messages: MessageItemData[];
|
|
47
|
+
caseId?: string;
|
|
48
|
+
showNewBadge?: boolean;
|
|
49
|
+
newMessageLabel?: React.ReactNode;
|
|
50
|
+
onMarkAsRead?: MessageItemProps["onMarkAsRead"];
|
|
51
|
+
onMarkedAsRead?: MessageItemProps["onMarkedAsRead"];
|
|
52
|
+
getAttachmentDownloadUrl?: MessageItemProps["getAttachmentDownloadUrl"];
|
|
53
|
+
formatTimestamp?: MessageItemProps["formatTimestamp"];
|
|
54
|
+
className?: string;
|
|
55
|
+
itemClassName?: string;
|
|
56
|
+
contentClassName?: string;
|
|
57
|
+
}
|
|
58
|
+
export declare const formatMessageDate: (date: Date) => string;
|
|
59
|
+
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;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Download, FileText } from "lucide-react";
|
|
5
|
+
import { AvatarProfile } from "./avatar";
|
|
6
|
+
import { Badge } from "./badge";
|
|
7
|
+
import { Separator } from "./separator";
|
|
8
|
+
import { Typography } from "./typography";
|
|
9
|
+
import { cn } from "./lib/utils";
|
|
10
|
+
export const formatMessageDate = (date) => {
|
|
11
|
+
const d = new Date(date);
|
|
12
|
+
return d.toLocaleDateString("en-SG", {
|
|
13
|
+
day: "numeric",
|
|
14
|
+
month: "short",
|
|
15
|
+
year: "numeric",
|
|
16
|
+
hour: "2-digit",
|
|
17
|
+
minute: "2-digit",
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
function getInitials(name) {
|
|
21
|
+
if (!name)
|
|
22
|
+
return "";
|
|
23
|
+
return name
|
|
24
|
+
.split(/\s+/)
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.slice(0, 2)
|
|
27
|
+
.map((part) => { var _a, _b; return (_b = (_a = part[0]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : ""; })
|
|
28
|
+
.join("");
|
|
29
|
+
}
|
|
30
|
+
function toHtmlContent(content) {
|
|
31
|
+
if (/<[a-z][\s\S]*>/i.test(content)) {
|
|
32
|
+
return content;
|
|
33
|
+
}
|
|
34
|
+
return content
|
|
35
|
+
.replace(/&/g, "&")
|
|
36
|
+
.replace(/</g, "<")
|
|
37
|
+
.replace(/>/g, ">")
|
|
38
|
+
.replace(/\n/g, "<br>");
|
|
39
|
+
}
|
|
40
|
+
export function MessageNewDivider({ children = "New Message", className, }) {
|
|
41
|
+
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" })] }));
|
|
42
|
+
}
|
|
43
|
+
export function MessageAttachmentRow({ attachment, downloadUrl, downloadLabel = `Download ${attachment.name}`, className, ...props }) {
|
|
44
|
+
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
|
|
45
|
+
? "group-hover:border-primary/40 group-hover:bg-primary/10 group-hover:text-primary group-focus-visible:border-primary/40 group-focus-visible:bg-primary/10 group-focus-visible:text-primary"
|
|
46
|
+
: "opacity-60"), children: _jsx(Download, { className: "h-3.5 w-3.5" }) })] }));
|
|
47
|
+
if (downloadUrl) {
|
|
48
|
+
return (_jsx("a", { className: cn("group grid min-w-0 cursor-pointer grid-cols-[auto_minmax(0,1fr)_2.75rem] items-center gap-3 rounded-lg border border-border bg-card px-3 py-2 text-foreground no-underline shadow-sm transition hover:border-primary/50 hover:bg-primary/5 hover:no-underline hover:shadow-md focus-visible:border-primary/50 focus-visible:bg-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", className), href: downloadUrl, target: "_blank", rel: "noopener noreferrer", download: attachment.name, "aria-label": downloadLabel, title: downloadLabel, ...props, children: renderContent(true) }));
|
|
49
|
+
}
|
|
50
|
+
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) }));
|
|
51
|
+
}
|
|
52
|
+
export function MessageItem({ message, caseId, showNewBadge = false, newMessageLabel = "New Message", onMarkAsRead, onMarkedAsRead, getAttachmentDownloadUrl, formatTimestamp, className, contentClassName, }) {
|
|
53
|
+
var _a;
|
|
54
|
+
const messageRef = React.useRef(null);
|
|
55
|
+
const [hasBeenViewed, setHasBeenViewed] = React.useState(message.isRead);
|
|
56
|
+
const isVisibleEnoughRef = React.useRef(false);
|
|
57
|
+
const isOfficer = message.senderRole === "OFFICER";
|
|
58
|
+
const isApplicant = message.senderRole === "APPLICANT";
|
|
59
|
+
const isUnreadApplicant = isApplicant && !message.isRead;
|
|
60
|
+
const showUnreadDivider = showNewBadge && isUnreadApplicant;
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
if (message.isRead ||
|
|
63
|
+
message.senderRole !== "APPLICANT" ||
|
|
64
|
+
hasBeenViewed ||
|
|
65
|
+
!onMarkAsRead) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const isPageActivelyVisible = () => {
|
|
69
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return document.visibilityState === "visible" && document.hasFocus();
|
|
73
|
+
};
|
|
74
|
+
const attemptMarkAsRead = () => {
|
|
75
|
+
if (!isVisibleEnoughRef.current ||
|
|
76
|
+
hasBeenViewed ||
|
|
77
|
+
!isPageActivelyVisible()) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setHasBeenViewed(true);
|
|
81
|
+
Promise.resolve(onMarkAsRead(message))
|
|
82
|
+
.then((result) => {
|
|
83
|
+
if (result !== false) {
|
|
84
|
+
onMarkedAsRead === null || onMarkedAsRead === void 0 ? void 0 : onMarkedAsRead(message);
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.catch(() => {
|
|
88
|
+
setHasBeenViewed(false);
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const observer = new IntersectionObserver((entries) => {
|
|
92
|
+
entries.forEach((entry) => {
|
|
93
|
+
isVisibleEnoughRef.current =
|
|
94
|
+
entry.isIntersecting && entry.intersectionRatio >= 0.5;
|
|
95
|
+
attemptMarkAsRead();
|
|
96
|
+
});
|
|
97
|
+
}, { threshold: 0.5 });
|
|
98
|
+
if (messageRef.current) {
|
|
99
|
+
observer.observe(messageRef.current);
|
|
100
|
+
}
|
|
101
|
+
const handleVisibilityOrFocus = () => {
|
|
102
|
+
attemptMarkAsRead();
|
|
103
|
+
};
|
|
104
|
+
document.addEventListener("visibilitychange", handleVisibilityOrFocus);
|
|
105
|
+
window.addEventListener("focus", handleVisibilityOrFocus);
|
|
106
|
+
return () => {
|
|
107
|
+
observer.disconnect();
|
|
108
|
+
document.removeEventListener("visibilitychange", handleVisibilityOrFocus);
|
|
109
|
+
window.removeEventListener("focus", handleVisibilityOrFocus);
|
|
110
|
+
};
|
|
111
|
+
}, [
|
|
112
|
+
message,
|
|
113
|
+
message.id,
|
|
114
|
+
message.isRead,
|
|
115
|
+
message.senderRole,
|
|
116
|
+
hasBeenViewed,
|
|
117
|
+
onMarkAsRead,
|
|
118
|
+
onMarkedAsRead,
|
|
119
|
+
]);
|
|
120
|
+
const senderName = isOfficer
|
|
121
|
+
? message.officerName || "Officer"
|
|
122
|
+
: isApplicant
|
|
123
|
+
? message.applicantName || message.applicantEmail || "Applicant"
|
|
124
|
+
: "System";
|
|
125
|
+
const initialsName = isOfficer
|
|
126
|
+
? message.officerName || "Officer"
|
|
127
|
+
: isApplicant
|
|
128
|
+
? message.applicantName || message.applicantEmail || "Applicant"
|
|
129
|
+
: "System";
|
|
130
|
+
const officerRole = isOfficer ? message.officerRole : undefined;
|
|
131
|
+
const senderProfileRole = isOfficer ? officerRole : undefined;
|
|
132
|
+
const hasSenderProfileRole = Boolean(senderProfileRole);
|
|
133
|
+
const attachments = (message.attachments || []);
|
|
134
|
+
const messageDate = (_a = message.sentAt) !== null && _a !== void 0 ? _a : message.createdAt;
|
|
135
|
+
const downloadCaseId = caseId !== null && caseId !== void 0 ? caseId : message.caseId;
|
|
136
|
+
const contentHtml = toHtmlContent(message.content);
|
|
137
|
+
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-primary [&_a]:underline [&_a:hover]:text-primary/80", "[&_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);
|
|
138
|
+
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", children: formatTimestamp
|
|
139
|
+
? formatTimestamp(messageDate, message)
|
|
140
|
+
: formatMessageDate(messageDate) }) })] }), _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] })] })] }));
|
|
141
|
+
}
|
|
142
|
+
export function MessageThread({ messages, caseId, showNewBadge = false, newMessageLabel = "New Message", onMarkAsRead, onMarkedAsRead, getAttachmentDownloadUrl, formatTimestamp, className, itemClassName, contentClassName, }) {
|
|
143
|
+
var _a;
|
|
144
|
+
const firstUnreadMessageId = showNewBadge
|
|
145
|
+
? (_a = messages.find((message) => message.senderRole === "APPLICANT" && !message.isRead)) === null || _a === void 0 ? void 0 : _a.id
|
|
146
|
+
: undefined;
|
|
147
|
+
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))) }));
|
|
148
|
+
}
|
package/dist/multi-select.js
CHANGED
|
@@ -11,13 +11,16 @@ import { Typography } from "./typography";
|
|
|
11
11
|
function MultiSelect({ label, options, value, onValueChange, placeholder = "Select options", description, error, invalid = false, disabled = false, hideLabel = false, searchable = false, searchPlaceholder = "Search...", maxVisibleTags = 2, className, triggerClassName, labelClassName, descriptionClassName, errorClassName, id, }) {
|
|
12
12
|
const generatedId = React.useId();
|
|
13
13
|
const selectId = id !== null && id !== void 0 ? id : `multi-select-${generatedId}`;
|
|
14
|
+
const labelId = `${selectId}-label`;
|
|
14
15
|
const errorId = error ? `${selectId}-error` : undefined;
|
|
15
16
|
const showDescription = Boolean(description) && !error;
|
|
16
17
|
const descriptionId = showDescription ? `${selectId}-description` : undefined;
|
|
18
|
+
const listboxId = `${selectId}-listbox`;
|
|
17
19
|
const isInvalid = invalid || Boolean(error);
|
|
18
20
|
const [open, setOpen] = React.useState(false);
|
|
19
21
|
const [searchQuery, setSearchQuery] = React.useState("");
|
|
20
22
|
const containerRef = React.useRef(null);
|
|
23
|
+
const triggerRef = React.useRef(null);
|
|
21
24
|
const searchInputRef = React.useRef(null);
|
|
22
25
|
const normalizedQuery = searchQuery.trim().toLowerCase();
|
|
23
26
|
const filteredOptions = searchable && normalizedQuery
|
|
@@ -64,15 +67,38 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
|
|
|
64
67
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
65
68
|
}
|
|
66
69
|
}, [open]);
|
|
67
|
-
|
|
70
|
+
const handleTriggerKeyDown = React.useCallback((event) => {
|
|
71
|
+
if (disabled)
|
|
72
|
+
return;
|
|
73
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
setOpen((prev) => !prev);
|
|
76
|
+
}
|
|
77
|
+
else if (event.key === "ArrowDown") {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
setOpen(true);
|
|
80
|
+
}
|
|
81
|
+
else if (event.key === "Escape") {
|
|
82
|
+
setOpen(false);
|
|
83
|
+
}
|
|
84
|
+
}, [disabled]);
|
|
85
|
+
return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { id: labelId, htmlFor: selectId, className: cn(hideLabel && "sr-only", labelClassName), onClick: (event) => {
|
|
86
|
+
var _a;
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
(_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
89
|
+
}, children: label }), _jsxs("div", { ref: containerRef, className: "relative", children: [_jsx("div", { ref: triggerRef, role: "combobox", id: selectId, tabIndex: disabled ? -1 : 0, onClick: () => {
|
|
90
|
+
if (!disabled) {
|
|
91
|
+
setOpen((prev) => !prev);
|
|
92
|
+
}
|
|
93
|
+
}, onKeyDown: handleTriggerKeyDown, "aria-expanded": open, "aria-controls": listboxId, "aria-haspopup": "listbox", "aria-invalid": isInvalid ? "true" : undefined, "aria-disabled": disabled ? "true" : undefined, "aria-labelledby": labelId, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn("flex h-control w-full cursor-pointer items-center gap-1.5 overflow-hidden rounded-control border border-input bg-background px-2.5 py-1.5 text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", isInvalid ? "border-destructive" : "border-input", disabled && "cursor-not-allowed opacity-50", triggerClassName), children: selectedLabels.length > 0 ? (_jsxs(_Fragment, { children: [selectedLabels.slice(0, maxVisibleTags).map((opt) => (_jsxs(Badge, { variant: "secondary", className: "max-w-[10rem] shrink-0 justify-start gap-1 overflow-visible text-sm font-medium", children: [_jsx("span", { className: "min-w-0 truncate", children: opt.label }), _jsx("button", { type: "button", className: "inline-flex cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: (e) => {
|
|
68
94
|
e.stopPropagation();
|
|
69
95
|
removeOption(opt.value);
|
|
70
|
-
}, "aria-label": `Remove ${opt.label}`, children: _jsx(XIcon, { size: 12 }) })] }, opt.value))), selectedLabels.length > maxVisibleTags ? (_jsxs(Badge, { variant: "secondary", className: "shrink-0 text-sm font-medium
|
|
96
|
+
}, "aria-label": `Remove ${opt.label}`, children: _jsx(XIcon, { size: 12 }) })] }, opt.value))), selectedLabels.length > maxVisibleTags ? (_jsxs(Badge, { variant: "secondary", className: "shrink-0 text-sm font-medium", title: selectedLabels
|
|
71
97
|
.slice(maxVisibleTags)
|
|
72
98
|
.map((o) => o.label)
|
|
73
|
-
.join(", "), children: ["+", selectedLabels.length - maxVisibleTags] })) : null] })) : (_jsx("span", { className: "text-muted-foreground", children: placeholder })) }), open ? (_jsxs("div", { className: "absolute z-50 mt-1 w-full rounded-
|
|
99
|
+
.join(", "), children: ["+", selectedLabels.length - maxVisibleTags] })) : null] })) : (_jsx("span", { className: "text-muted-foreground", children: placeholder })) }), open ? (_jsxs("div", { className: "absolute z-50 mt-1 w-full rounded-overlay border bg-popover shadow-md", children: [searchable ? (_jsx("div", { className: "border-b px-2 py-1", children: _jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: searchPlaceholder, className: "h-8 border-0 bg-transparent px-2 py-0 text-sm shadow-none focus-visible:ring-0", onKeyDown: (e) => e.stopPropagation() }) })) : null, _jsx("div", { id: listboxId, role: "listbox", "aria-label": typeof label === "string" ? label : "Options", "aria-multiselectable": "true", className: "max-h-64 overflow-y-auto p-1", children: filteredOptions.length > 0 ? (filteredOptions.map((option) => {
|
|
74
100
|
const isSelected = value.includes(option.value);
|
|
75
|
-
return (_jsxs("button", { type: "button", role: "option", "aria-selected": isSelected, disabled: option.disabled, onClick: () => toggleOption(option.value), className: cn("flex min-h-
|
|
76
|
-
})) : (_jsx("div", { className: "px-
|
|
101
|
+
return (_jsxs("button", { type: "button", role: "option", "aria-selected": isSelected, disabled: option.disabled, onClick: () => toggleOption(option.value), className: cn("flex min-h-8 w-full cursor-pointer items-center gap-1.5 rounded-sm px-2 py-1.5 text-left text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50", isSelected && "bg-accent"), children: [_jsx(Checkbox, { checked: isSelected, disabled: option.disabled, tabIndex: -1, readOnly: true, "aria-hidden": "true", className: "mt-0.5" }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: option.label }), option.description ? (_jsx(Typography, { as: "span", variant: "caption", children: option.description })) : null] })] }, option.value));
|
|
102
|
+
})) : (_jsx("div", { className: "px-2.5 py-1.5 text-center text-sm text-muted-foreground", children: "No options found." })) })] })) : null] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
|
|
77
103
|
}
|
|
78
104
|
export { MultiSelect };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type ButtonProps } from "./button";
|
|
3
|
+
import { type CountBadgeProps } from "./count-badge";
|
|
4
|
+
import { type PopoverProps } from "./popover";
|
|
5
|
+
export type NotificationKind = "assignment" | "message" | "request" | "review" | "system";
|
|
6
|
+
export type NotificationTone = "accent" | "destructive" | "info" | "muted" | "success" | "warning";
|
|
7
|
+
export type NotificationIcon = React.ComponentType<{
|
|
8
|
+
className?: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type NotificationFilter = "all" | "unread";
|
|
11
|
+
export interface NotificationAction {
|
|
12
|
+
id?: string;
|
|
13
|
+
label: React.ReactNode;
|
|
14
|
+
variant?: ButtonProps["variant"];
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
onClick?: (notification: NotificationItemData, event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
18
|
+
}
|
|
19
|
+
export interface NotificationItemData {
|
|
20
|
+
id: string;
|
|
21
|
+
title: React.ReactNode;
|
|
22
|
+
description?: React.ReactNode;
|
|
23
|
+
meta?: React.ReactNode;
|
|
24
|
+
dateTime?: string;
|
|
25
|
+
unread?: boolean;
|
|
26
|
+
kind?: NotificationKind;
|
|
27
|
+
tone?: NotificationTone;
|
|
28
|
+
icon?: NotificationIcon;
|
|
29
|
+
href?: string;
|
|
30
|
+
actions?: NotificationAction[];
|
|
31
|
+
}
|
|
32
|
+
export interface NotificationItemProps extends Omit<React.HTMLAttributes<HTMLElement>, "children" | "onSelect" | "title"> {
|
|
33
|
+
notification: NotificationItemData;
|
|
34
|
+
density?: "compact" | "default";
|
|
35
|
+
labelLines?: 1 | 2;
|
|
36
|
+
descriptionLines?: 1 | 2 | 3;
|
|
37
|
+
unreadLabel?: React.ReactNode;
|
|
38
|
+
onSelect?: (notification: NotificationItemData) => void;
|
|
39
|
+
}
|
|
40
|
+
export interface NotificationListProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
41
|
+
notifications: NotificationItemData[];
|
|
42
|
+
density?: "compact" | "default";
|
|
43
|
+
variant?: "bordered" | "plain";
|
|
44
|
+
labelLines?: 1 | 2;
|
|
45
|
+
descriptionLines?: 1 | 2 | 3;
|
|
46
|
+
unreadLabel?: React.ReactNode;
|
|
47
|
+
emptyState?: React.ReactNode;
|
|
48
|
+
itemClassName?: string;
|
|
49
|
+
onNotificationClick?: (notification: NotificationItemData) => void;
|
|
50
|
+
}
|
|
51
|
+
export interface NotificationPopoverProps extends Omit<PopoverProps, "children" | "surface" | "title"> {
|
|
52
|
+
notifications: NotificationItemData[];
|
|
53
|
+
title?: React.ReactNode;
|
|
54
|
+
filter?: NotificationFilter;
|
|
55
|
+
defaultFilter?: NotificationFilter;
|
|
56
|
+
onFilterChange?: (filter: NotificationFilter) => void;
|
|
57
|
+
showFilterTabs?: boolean;
|
|
58
|
+
height?: string;
|
|
59
|
+
minHeight?: string;
|
|
60
|
+
maxHeight?: string;
|
|
61
|
+
emptyState?: React.ReactNode;
|
|
62
|
+
listClassName?: string;
|
|
63
|
+
itemClassName?: string;
|
|
64
|
+
unreadLabel?: React.ReactNode;
|
|
65
|
+
markAllAsReadLabel?: React.ReactNode;
|
|
66
|
+
onNotificationClick?: (notification: NotificationItemData) => void;
|
|
67
|
+
onMarkAllAsRead?: () => void;
|
|
68
|
+
}
|
|
69
|
+
export interface NotificationTriggerProps extends Omit<ButtonProps, "children"> {
|
|
70
|
+
count?: number;
|
|
71
|
+
countMax?: number;
|
|
72
|
+
countSize?: CountBadgeProps["size"];
|
|
73
|
+
hideCountWhenZero?: boolean;
|
|
74
|
+
isCollapsed?: boolean;
|
|
75
|
+
label?: React.ReactNode;
|
|
76
|
+
triggerVariant?: "default" | "sidebar";
|
|
77
|
+
}
|
|
78
|
+
declare const NotificationTrigger: React.ForwardRefExoticComponent<NotificationTriggerProps & React.RefAttributes<HTMLButtonElement>>;
|
|
79
|
+
declare function NotificationItem({ notification, density, labelLines, descriptionLines, unreadLabel, className, onClick, onSelect, ...props }: NotificationItemProps): import("react/jsx-runtime").JSX.Element;
|
|
80
|
+
declare function NotificationList({ notifications, density, variant, labelLines, descriptionLines, unreadLabel, emptyState, itemClassName, className, onNotificationClick, ...props }: NotificationListProps): import("react/jsx-runtime").JSX.Element;
|
|
81
|
+
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;
|
|
82
|
+
export { NotificationItem, NotificationList, NotificationPopover, NotificationTrigger, };
|