@open-mercato/ui 0.6.2-develop.3372.1.de9a169973 → 0.6.2-develop.3380.1.bb14509041
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -0
- package/dist/ai/AiIcon.js +1 -1
- package/dist/ai/AiIcon.js.map +2 -2
- package/dist/backend/AppShell.js +223 -153
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/ProfileDropdown.js +101 -79
- package/dist/backend/ProfileDropdown.js.map +2 -2
- package/dist/backend/notifications/NotificationCountBadge.js +1 -1
- package/dist/backend/notifications/NotificationCountBadge.js.map +2 -2
- package/dist/backend/notifications/NotificationItem.js +67 -50
- package/dist/backend/notifications/NotificationItem.js.map +2 -2
- package/dist/backend/notifications/NotificationPanel.js +108 -85
- package/dist/backend/notifications/NotificationPanel.js.map +2 -2
- package/dist/primitives/breadcrumb.js +161 -0
- package/dist/primitives/breadcrumb.js.map +7 -0
- package/dist/primitives/sheet.js +114 -0
- package/dist/primitives/sheet.js.map +7 -0
- package/package.json +3 -3
- package/src/ai/AiIcon.tsx +1 -1
- package/src/backend/AppShell.tsx +151 -49
- package/src/backend/ProfileDropdown.tsx +115 -88
- package/src/backend/__tests__/AppShell.test.tsx +8 -0
- package/src/backend/notifications/NotificationCountBadge.tsx +1 -1
- package/src/backend/notifications/NotificationItem.tsx +97 -104
- package/src/backend/notifications/NotificationPanel.tsx +126 -102
- package/src/primitives/__tests__/breadcrumb.test.tsx +298 -0
- package/src/primitives/__tests__/sheet.test.tsx +152 -0
- package/src/primitives/breadcrumb.tsx +185 -0
- package/src/primitives/sheet.tsx +165 -0
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
|
-
import {
|
|
5
|
+
import { Bell, AlertTriangle, CheckCircle2, XCircle, Info, Loader2, X } from "lucide-react";
|
|
6
6
|
import { Button } from "../../primitives/button.js";
|
|
7
|
+
import { IconButton } from "../../primitives/icon-button.js";
|
|
7
8
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
8
9
|
import { formatRelativeTime } from "@open-mercato/shared/lib/time";
|
|
9
10
|
function resolveNotificationText(params) {
|
|
@@ -22,12 +23,22 @@ const severityIcons = {
|
|
|
22
23
|
success: CheckCircle2,
|
|
23
24
|
error: XCircle
|
|
24
25
|
};
|
|
25
|
-
const
|
|
26
|
+
const severityAvatarBg = {
|
|
27
|
+
info: "bg-status-info-bg",
|
|
28
|
+
warning: "bg-status-warning-bg",
|
|
29
|
+
success: "bg-status-success-bg",
|
|
30
|
+
error: "bg-status-error-bg"
|
|
31
|
+
};
|
|
32
|
+
const severityAvatarText = {
|
|
26
33
|
info: "text-status-info-icon",
|
|
27
34
|
warning: "text-status-warning-icon",
|
|
28
35
|
success: "text-status-success-icon",
|
|
29
36
|
error: "text-status-error-icon"
|
|
30
37
|
};
|
|
38
|
+
function humanizeModuleId(moduleId) {
|
|
39
|
+
if (!moduleId) return null;
|
|
40
|
+
return moduleId.split(/[_:.-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
41
|
+
}
|
|
31
42
|
function NotificationItem({
|
|
32
43
|
notification,
|
|
33
44
|
onMarkAsRead,
|
|
@@ -40,8 +51,10 @@ function NotificationItem({
|
|
|
40
51
|
const [executing, setExecuting] = React.useState(null);
|
|
41
52
|
const isUnread = notification.status === "unread";
|
|
42
53
|
const hasActions = notification.actions && notification.actions.length > 0;
|
|
43
|
-
const severity = notification.severity;
|
|
54
|
+
const severity = notification.severity ?? "info";
|
|
44
55
|
const IconComponent = severityIcons[severity] ?? Bell;
|
|
56
|
+
const avatarBg = severityAvatarBg[severity] ?? "bg-muted";
|
|
57
|
+
const avatarText = severityAvatarText[severity] ?? "text-muted-foreground";
|
|
45
58
|
const titleText = resolveNotificationText({
|
|
46
59
|
key: notification.titleKey,
|
|
47
60
|
fallback: notification.title,
|
|
@@ -54,22 +67,18 @@ function NotificationItem({
|
|
|
54
67
|
variables: notification.bodyVariables ?? void 0,
|
|
55
68
|
t
|
|
56
69
|
});
|
|
70
|
+
const timeAgo = formatRelativeTime(notification.createdAt, { translate: t }) ?? "";
|
|
71
|
+
const sourceLabel = humanizeModuleId(notification.sourceModule);
|
|
57
72
|
const handleClick = async () => {
|
|
58
|
-
if (isUnread)
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
if (notification.linkHref) {
|
|
62
|
-
router.push(notification.linkHref);
|
|
63
|
-
}
|
|
73
|
+
if (isUnread) await onMarkAsRead();
|
|
74
|
+
if (notification.linkHref) router.push(notification.linkHref);
|
|
64
75
|
};
|
|
65
76
|
const handleAction = async (actionId, event) => {
|
|
66
77
|
event?.stopPropagation();
|
|
67
78
|
setExecuting(actionId);
|
|
68
79
|
try {
|
|
69
80
|
const result = await onExecuteAction(actionId);
|
|
70
|
-
if (result.href)
|
|
71
|
-
router.push(result.href);
|
|
72
|
-
}
|
|
81
|
+
if (result.href) router.push(result.href);
|
|
73
82
|
} finally {
|
|
74
83
|
setExecuting(null);
|
|
75
84
|
}
|
|
@@ -112,12 +121,14 @@ function NotificationItem({
|
|
|
112
121
|
}
|
|
113
122
|
);
|
|
114
123
|
}
|
|
124
|
+
const showActions = hasActions && notification.status !== "actioned";
|
|
125
|
+
const showBodyBubble = !!bodyText && bodyText.length > 0;
|
|
115
126
|
return /* @__PURE__ */ jsxs(
|
|
116
127
|
"div",
|
|
117
128
|
{
|
|
118
129
|
className: cn(
|
|
119
|
-
"group relative
|
|
120
|
-
isUnread && "bg-muted/
|
|
130
|
+
"group relative flex gap-4 items-start rounded-xl p-3 transition-colors hover:bg-muted/40 cursor-pointer",
|
|
131
|
+
isUnread && "bg-muted/20"
|
|
121
132
|
),
|
|
122
133
|
onClick: handleClick,
|
|
123
134
|
onKeyDown: (e) => {
|
|
@@ -129,54 +140,60 @@ function NotificationItem({
|
|
|
129
140
|
role: "button",
|
|
130
141
|
tabIndex: 0,
|
|
131
142
|
children: [
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/* @__PURE__ */ jsx(
|
|
135
|
-
"
|
|
143
|
+
/* @__PURE__ */ jsxs("div", { className: cn("relative shrink-0 flex size-10 items-center justify-center rounded-full", avatarBg), children: [
|
|
144
|
+
/* @__PURE__ */ jsx(IconComponent, { className: cn("size-5", avatarText), "aria-hidden": "true" }),
|
|
145
|
+
isUnread ? /* @__PURE__ */ jsx(
|
|
146
|
+
"span",
|
|
136
147
|
{
|
|
137
|
-
className:
|
|
138
|
-
|
|
139
|
-
severityColors[severity] ?? "text-muted-foreground"
|
|
140
|
-
),
|
|
141
|
-
children: /* @__PURE__ */ jsx(IconComponent, { className: "h-5 w-5" })
|
|
148
|
+
className: "absolute -right-1 -top-1 size-3 rounded-full bg-accent-indigo ring-2 ring-background",
|
|
149
|
+
"aria-hidden": "true"
|
|
142
150
|
}
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
) : null
|
|
152
|
+
] }),
|
|
153
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
|
|
154
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium leading-5 tracking-tight text-foreground", children: titleText }),
|
|
155
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1 text-xs leading-4 text-muted-foreground", children: [
|
|
156
|
+
timeAgo ? /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap", children: timeAgo }) : null,
|
|
157
|
+
timeAgo && sourceLabel ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-text-disabled", children: "\xB7" }) : null,
|
|
158
|
+
sourceLabel ? /* @__PURE__ */ jsx("span", { className: "truncate", children: sourceLabel }) : null
|
|
159
|
+
] }),
|
|
160
|
+
showBodyBubble ? /* @__PURE__ */ jsx("div", { className: "mt-2 inline-flex max-w-full self-start rounded-tr-lg rounded-br-lg rounded-bl-lg rounded-tl-sm border bg-background px-3 py-2 text-sm leading-5 tracking-tight text-muted-foreground shadow-xs", children: /* @__PURE__ */ jsx("p", { className: "whitespace-pre-line break-words", children: bodyText }) }) : null,
|
|
161
|
+
showActions ? /* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: notification.actions.map((action, idx) => {
|
|
162
|
+
const isPrimary = idx === notification.actions.length - 1 || action.variant === "default";
|
|
163
|
+
return /* @__PURE__ */ jsxs(
|
|
151
164
|
Button,
|
|
152
165
|
{
|
|
153
|
-
|
|
166
|
+
type: "button",
|
|
167
|
+
variant: isPrimary ? "default" : "outline",
|
|
154
168
|
size: "sm",
|
|
155
|
-
className:
|
|
169
|
+
className: cn(
|
|
170
|
+
"h-8 rounded-md px-2.5 py-1.5 text-sm font-medium",
|
|
171
|
+
isPrimary && "bg-accent-indigo text-accent-indigo-foreground hover:bg-accent-indigo/90"
|
|
172
|
+
),
|
|
156
173
|
onClick: (event) => handleAction(action.id, event),
|
|
157
174
|
disabled: executing !== null,
|
|
158
175
|
children: [
|
|
159
176
|
action.labelKey ? t(action.labelKey, action.label) : t(action.label, action.label),
|
|
160
|
-
executing === action.id
|
|
177
|
+
executing === action.id ? /* @__PURE__ */ jsx(Loader2, { className: "ml-1 size-3 animate-spin" }) : null
|
|
161
178
|
]
|
|
162
179
|
},
|
|
163
180
|
action.id
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
);
|
|
182
|
+
}) }) : null,
|
|
183
|
+
notification.status === "actioned" && notification.actionTaken ? /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs italic text-muted-foreground", children: t("notifications.actionTaken", "Action taken: {action}", { action: notification.actionTaken }) }) : null
|
|
184
|
+
] }),
|
|
185
|
+
/* @__PURE__ */ jsx(
|
|
186
|
+
IconButton,
|
|
187
|
+
{
|
|
188
|
+
type: "button",
|
|
189
|
+
variant: "ghost",
|
|
190
|
+
size: "xs",
|
|
191
|
+
className: "absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100",
|
|
192
|
+
onClick: handleDismiss,
|
|
193
|
+
"aria-label": t("notifications.actions.dismiss", "Dismiss"),
|
|
194
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
|
|
195
|
+
}
|
|
196
|
+
)
|
|
180
197
|
]
|
|
181
198
|
}
|
|
182
199
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/notifications/NotificationItem.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Bell, AlertTriangle, CheckCircle2, XCircle, Info, Loader2, X } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { formatRelativeTime } from '@open-mercato/shared/lib/time'\nimport type { NotificationDto, NotificationRendererProps, NotificationTypeAction } from '@open-mercato/shared/modules/notifications/types'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { ComponentType } from 'react'\n\nexport type NotificationItemProps = {\n notification: NotificationDto\n onMarkAsRead: () => Promise<void>\n onExecuteAction: (actionId: string) => Promise<{ href?: string }>\n onDismiss: () => Promise<void>\n t: TranslateFn\n customRenderer?: ComponentType<NotificationRendererProps>\n}\n\nfunction resolveNotificationText(params: {\n key?: string | null\n fallback?: string | null\n variables?: Record<string, string> | null\n t: TranslateFn\n}): string {\n const { key, fallback, variables, t } = params\n if (key) {\n return t(key, fallback ?? key, variables ?? undefined)\n }\n if (fallback) {\n return t(fallback, variables ?? undefined)\n }\n return ''\n}\n\nconst severityIcons = {\n info: Info,\n warning: AlertTriangle,\n success: CheckCircle2,\n error: XCircle,\n}\n\n// Avatar background \u2014 soft tints matching the Figma example item palette\nconst severityAvatarBg = {\n info: 'bg-status-info-bg',\n warning: 'bg-status-warning-bg',\n success: 'bg-status-success-bg',\n error: 'bg-status-error-bg',\n}\n\nconst severityAvatarText = {\n info: 'text-status-info-icon',\n warning: 'text-status-warning-icon',\n success: 'text-status-success-icon',\n error: 'text-status-error-icon',\n}\n\nfunction humanizeModuleId(moduleId?: string | null): string | null {\n if (!moduleId) return null\n return moduleId\n .split(/[_:.-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nexport function NotificationItem({\n notification,\n onMarkAsRead,\n onExecuteAction,\n onDismiss,\n t,\n customRenderer: CustomRenderer,\n}: NotificationItemProps) {\n const router = useRouter()\n const [executing, setExecuting] = React.useState<string | null>(null)\n\n const isUnread = notification.status === 'unread'\n const hasActions = notification.actions && notification.actions.length > 0\n const severity = (notification.severity as keyof typeof severityIcons) ?? 'info'\n const IconComponent = severityIcons[severity] ?? Bell\n const avatarBg = severityAvatarBg[severity] ?? 'bg-muted'\n const avatarText = severityAvatarText[severity] ?? 'text-muted-foreground'\n\n const titleText = resolveNotificationText({\n key: notification.titleKey,\n fallback: notification.title,\n variables: notification.titleVariables ?? undefined,\n t,\n })\n const bodyText = resolveNotificationText({\n key: notification.bodyKey,\n fallback: notification.body ?? undefined,\n variables: notification.bodyVariables ?? undefined,\n t,\n })\n const timeAgo = formatRelativeTime(notification.createdAt, { translate: t }) ?? ''\n const sourceLabel = humanizeModuleId(notification.sourceModule)\n\n const handleClick = async () => {\n if (isUnread) await onMarkAsRead()\n if (notification.linkHref) router.push(notification.linkHref)\n }\n\n const handleAction = async (actionId: string, event?: React.MouseEvent) => {\n event?.stopPropagation()\n setExecuting(actionId)\n try {\n const result = await onExecuteAction(actionId)\n if (result.href) router.push(result.href)\n } finally {\n setExecuting(null)\n }\n }\n\n const handleDismiss = async (event?: React.MouseEvent) => {\n event?.stopPropagation()\n await onDismiss()\n }\n\n const rendererActions: NotificationTypeAction[] = notification.actions.map((action) => ({\n id: action.id,\n labelKey: action.labelKey ?? action.label,\n variant: action.variant as NotificationTypeAction['variant'],\n icon: action.icon,\n }))\n\n if (CustomRenderer) {\n return (\n <CustomRenderer\n notification={{\n id: notification.id,\n type: notification.type,\n title: titleText,\n body: bodyText || null,\n titleKey: notification.titleKey,\n bodyKey: notification.bodyKey,\n titleVariables: notification.titleVariables ?? null,\n bodyVariables: notification.bodyVariables ?? null,\n icon: notification.icon,\n severity: notification.severity,\n status: notification.status,\n sourceModule: notification.sourceModule,\n sourceEntityType: notification.sourceEntityType,\n sourceEntityId: notification.sourceEntityId,\n linkHref: notification.linkHref ?? null,\n createdAt: notification.createdAt,\n }}\n onAction={handleAction}\n onDismiss={handleDismiss}\n actions={rendererActions}\n />\n )\n }\n\n const showActions = hasActions && notification.status !== 'actioned'\n const showBodyBubble = !!bodyText && bodyText.length > 0\n\n return (\n <div\n className={cn(\n 'group relative flex gap-4 items-start rounded-xl p-3 transition-colors hover:bg-muted/40 cursor-pointer',\n isUnread && 'bg-muted/20'\n )}\n onClick={handleClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n handleClick()\n }\n }}\n role=\"button\"\n tabIndex={0}\n >\n {/* Avatar: severity-tinted circle with severity icon */}\n <div className={cn('relative shrink-0 flex size-10 items-center justify-center rounded-full', avatarBg)}>\n <IconComponent className={cn('size-5', avatarText)} aria-hidden=\"true\" />\n {isUnread ? (\n <span\n className=\"absolute -right-1 -top-1 size-3 rounded-full bg-accent-indigo ring-2 ring-background\"\n aria-hidden=\"true\"\n />\n ) : null}\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n {/* Title row */}\n <p className=\"text-sm font-medium leading-5 tracking-tight text-foreground\">\n {titleText}\n </p>\n\n {/* Description row: time \u00B7 source */}\n <div className=\"flex flex-wrap items-center gap-1 text-xs leading-4 text-muted-foreground\">\n {timeAgo ? <span className=\"whitespace-nowrap\">{timeAgo}</span> : null}\n {timeAgo && sourceLabel ? (\n <span aria-hidden=\"true\" className=\"text-text-disabled\">\u00B7</span>\n ) : null}\n {sourceLabel ? <span className=\"truncate\">{sourceLabel}</span> : null}\n </div>\n\n {/* Optional body bubble */}\n {showBodyBubble ? (\n <div className=\"mt-2 inline-flex max-w-full self-start rounded-tr-lg rounded-br-lg rounded-bl-lg rounded-tl-sm border bg-background px-3 py-2 text-sm leading-5 tracking-tight text-muted-foreground shadow-xs\">\n <p className=\"whitespace-pre-line break-words\">{bodyText}</p>\n </div>\n ) : null}\n\n {/* Optional actions row */}\n {showActions ? (\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n {notification.actions.map((action, idx) => {\n const isPrimary = idx === notification.actions.length - 1 || action.variant === 'default'\n return (\n <Button\n key={action.id}\n type=\"button\"\n variant={isPrimary ? 'default' : 'outline'}\n size=\"sm\"\n className={cn(\n 'h-8 rounded-md px-2.5 py-1.5 text-sm font-medium',\n isPrimary && 'bg-accent-indigo text-accent-indigo-foreground hover:bg-accent-indigo/90',\n )}\n onClick={(event) => handleAction(action.id, event)}\n disabled={executing !== null}\n >\n {action.labelKey ? t(action.labelKey, action.label) : t(action.label, action.label)}\n {executing === action.id ? <Loader2 className=\"ml-1 size-3 animate-spin\" /> : null}\n </Button>\n )\n })}\n </div>\n ) : null}\n\n {notification.status === 'actioned' && notification.actionTaken ? (\n <p className=\"mt-1 text-xs italic text-muted-foreground\">\n {t('notifications.actionTaken', 'Action taken: {action}', { action: notification.actionTaken })}\n </p>\n ) : null}\n </div>\n\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100\"\n onClick={handleDismiss}\n aria-label={t('notifications.actions.dismiss', 'Dismiss')}\n >\n <X className=\"size-3.5\" />\n </IconButton>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { ArrowDown, ArrowUp, Bell, Loader2, RotateCcw, Settings2, X } from "lucide-react";
|
|
5
6
|
import { Button } from "../../primitives/button.js";
|
|
6
7
|
import { IconButton } from "../../primitives/icon-button.js";
|
|
7
|
-
import {
|
|
8
|
+
import { Sheet, SheetContent, SheetTitle } from "../../primitives/sheet.js";
|
|
8
9
|
import { NotificationItem } from "./NotificationItem.js";
|
|
9
10
|
function NotificationPanel({
|
|
10
11
|
open,
|
|
@@ -43,87 +44,90 @@ function NotificationPanel({
|
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
46
|
React.useEffect(() => {
|
|
46
|
-
if (!open) return;
|
|
47
|
-
const
|
|
47
|
+
if (!open || typeof document === "undefined") return;
|
|
48
|
+
const previous = document.body.style.overflow;
|
|
48
49
|
document.body.style.overflow = "hidden";
|
|
49
50
|
return () => {
|
|
50
|
-
document.body.style.overflow =
|
|
51
|
+
document.body.style.overflow = previous;
|
|
51
52
|
};
|
|
52
53
|
}, [open]);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
markingAllRead ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(CheckCheck, { className: "mr-1 h-4 w-4" }),
|
|
96
|
-
t("notifications.markAllRead", "Mark all read")
|
|
97
|
-
]
|
|
98
|
-
}
|
|
99
|
-
),
|
|
100
|
-
/* @__PURE__ */ jsx(IconButton, { variant: "ghost", size: "lg", onClick: () => onOpenChange(false), children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" }) })
|
|
101
|
-
] })
|
|
102
|
-
] }),
|
|
103
|
-
/* @__PURE__ */ jsx(
|
|
104
|
-
Tabs,
|
|
54
|
+
return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
|
|
55
|
+
SheetContent,
|
|
56
|
+
{
|
|
57
|
+
side: "right",
|
|
58
|
+
className: "gap-0 p-0",
|
|
59
|
+
hideClose: true,
|
|
60
|
+
"aria-label": t("notifications.title", "Notifications"),
|
|
61
|
+
children: [
|
|
62
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b px-5 py-4", children: [
|
|
63
|
+
/* @__PURE__ */ jsx(SheetTitle, { className: "text-base font-medium leading-6 tracking-tight text-foreground", children: t("notifications.title", "Notifications") }),
|
|
64
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
65
|
+
unreadCount > 0 ? /* @__PURE__ */ jsxs(
|
|
66
|
+
"button",
|
|
67
|
+
{
|
|
68
|
+
type: "button",
|
|
69
|
+
onClick: handleMarkAllRead,
|
|
70
|
+
disabled: markingAllRead,
|
|
71
|
+
className: "inline-flex items-center gap-1 rounded-md px-1 text-sm font-medium leading-5 text-accent-indigo transition-colors hover:text-accent-indigo/80 disabled:opacity-50",
|
|
72
|
+
children: [
|
|
73
|
+
markingAllRead ? /* @__PURE__ */ jsx(Loader2, { className: "size-3.5 animate-spin" }) : null,
|
|
74
|
+
t("notifications.markAllRead", "Mark all read")
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
) : null,
|
|
78
|
+
/* @__PURE__ */ jsx(
|
|
79
|
+
IconButton,
|
|
80
|
+
{
|
|
81
|
+
type: "button",
|
|
82
|
+
variant: "ghost",
|
|
83
|
+
size: "sm",
|
|
84
|
+
onClick: () => onOpenChange(false),
|
|
85
|
+
"aria-label": t("notifications.close", "Close notifications"),
|
|
86
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
] })
|
|
90
|
+
] }),
|
|
91
|
+
/* @__PURE__ */ jsx("div", { role: "tablist", className: "flex items-center gap-5 border-b px-5 py-3.5", children: ["all", "unread", "action"].map((value) => {
|
|
92
|
+
const isActive = filter === value;
|
|
93
|
+
const label = value === "all" ? t("notifications.filters.all", "All") : value === "unread" ? t("notifications.filters.unread", "Unread") : t("notifications.filters.actionRequired", "Action Required");
|
|
94
|
+
return /* @__PURE__ */ jsxs(
|
|
95
|
+
"button",
|
|
105
96
|
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
97
|
+
type: "button",
|
|
98
|
+
role: "tab",
|
|
99
|
+
"aria-selected": isActive,
|
|
100
|
+
onClick: () => setFilter(value),
|
|
101
|
+
className: "relative inline-flex items-center gap-1.5 pb-2 text-sm font-medium leading-5 tracking-tight transition-colors focus:outline-none focus-visible:text-foreground data-[active=true]:text-foreground data-[active=false]:text-muted-foreground hover:text-foreground",
|
|
102
|
+
"data-active": isActive,
|
|
103
|
+
children: [
|
|
104
|
+
/* @__PURE__ */ jsx("span", { children: label }),
|
|
105
|
+
value === "unread" && unreadCount > 0 ? /* @__PURE__ */ jsx("span", { className: "inline-flex min-w-4 items-center justify-center rounded-full bg-accent-indigo px-1 text-overline font-medium text-accent-indigo-foreground", children: unreadCount > 99 ? "99+" : unreadCount }) : null,
|
|
106
|
+
isActive ? /* @__PURE__ */ jsx(
|
|
107
|
+
"span",
|
|
108
|
+
{
|
|
109
|
+
className: "absolute bottom-[-14px] left-0 right-0 h-0.5 bg-foreground",
|
|
110
|
+
"aria-hidden": "true"
|
|
111
|
+
}
|
|
112
|
+
) : null
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
value
|
|
116
|
+
);
|
|
117
|
+
}) }),
|
|
118
|
+
dismissUndo && onUndoDismiss && /* @__PURE__ */ jsx("div", { className: "border-b bg-muted/50 px-4 py-2 text-sm", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
119
|
+
/* @__PURE__ */ jsx("span", { children: t("notifications.toast.dismissed", "Notification dismissed") }),
|
|
120
|
+
/* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => onUndoDismiss(), children: [
|
|
121
|
+
/* @__PURE__ */ jsx(RotateCcw, { className: "mr-1 h-3 w-3" }),
|
|
122
|
+
t("notifications.actions.undo", "Undo")
|
|
123
|
+
] })
|
|
124
|
+
] }) }),
|
|
125
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto overflow-x-hidden overscroll-contain", children: filteredNotifications.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-16 text-muted-foreground", children: [
|
|
126
|
+
/* @__PURE__ */ jsx("div", { className: "flex size-12 items-center justify-center rounded-full bg-muted/50", children: /* @__PURE__ */ jsx(Bell, { className: "size-5", "aria-hidden": "true" }) }),
|
|
127
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm", children: t("notifications.empty", "No notifications") })
|
|
128
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1 p-2", children: filteredNotifications.map((notification, idx) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
129
|
+
idx > 0 ? /* @__PURE__ */ jsx("div", { className: "my-px h-px bg-border", "aria-hidden": "true" }) : null,
|
|
130
|
+
/* @__PURE__ */ jsx(
|
|
127
131
|
NotificationItem,
|
|
128
132
|
{
|
|
129
133
|
notification,
|
|
@@ -132,13 +136,32 @@ function NotificationPanel({
|
|
|
132
136
|
onDismiss: () => onDismiss(notification.id),
|
|
133
137
|
t,
|
|
134
138
|
customRenderer: customRenderers?.[notification.type]
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
] }, notification.id)) }) }),
|
|
142
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-t px-5 py-3.5 text-xs leading-4 text-muted-foreground", children: [
|
|
143
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
144
|
+
/* @__PURE__ */ jsx("span", { children: t("notifications.footer.useHint", "Use") }),
|
|
145
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded border bg-muted/50 px-1 py-0.5", children: /* @__PURE__ */ jsx(ArrowUp, { className: "size-3", "aria-hidden": "true" }) }),
|
|
146
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded border bg-muted/50 px-1 py-0.5", children: /* @__PURE__ */ jsx(ArrowDown, { className: "size-3", "aria-hidden": "true" }) }),
|
|
147
|
+
/* @__PURE__ */ jsx("span", { children: t("notifications.footer.toNavigate", "to navigate") })
|
|
148
|
+
] }),
|
|
149
|
+
/* @__PURE__ */ jsxs(
|
|
150
|
+
Link,
|
|
151
|
+
{
|
|
152
|
+
href: "/backend/config/notifications",
|
|
153
|
+
onClick: () => onOpenChange(false),
|
|
154
|
+
className: "inline-flex items-center gap-1.5 font-medium text-muted-foreground transition-colors hover:text-foreground",
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ jsx(Settings2, { className: "size-3.5", "aria-hidden": "true" }),
|
|
157
|
+
t("notifications.footer.manage", "Manage notifications")
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
)
|
|
138
161
|
] })
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
) });
|
|
142
165
|
}
|
|
143
166
|
export {
|
|
144
167
|
NotificationPanel
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/notifications/NotificationPanel.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { X, Bell, CheckCheck, Loader2, RotateCcw } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Tabs, TabsList, TabsTrigger } from '../../primitives/tabs'\nimport { NotificationItem } from './NotificationItem'\nimport type { NotificationDto, NotificationRendererProps } from '@open-mercato/shared/modules/notifications/types'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { ComponentType } from 'react'\n\n/**\n * Map of notification type to custom renderer component.\n * Used to provide custom rendering for specific notification types.\n *\n * @example\n * ```tsx\n * const customRenderers = {\n * 'sales.order.created': SalesOrderCreatedRenderer,\n * 'sales.quote.created': SalesQuoteCreatedRenderer,\n * }\n * ```\n */\nexport type NotificationRenderers = Record<string, ComponentType<NotificationRendererProps>>\n\nexport type NotificationPanelProps = {\n open: boolean\n onOpenChange: (open: boolean) => void\n notifications: NotificationDto[]\n unreadCount: number\n onMarkAsRead: (id: string) => Promise<void>\n onExecuteAction: (id: string, actionId: string) => Promise<{ href?: string }>\n onDismiss: (id: string) => Promise<void>\n dismissUndo?: { notification: NotificationDto; previousStatus: 'read' | 'unread' } | null\n onUndoDismiss?: () => Promise<void>\n onMarkAllRead: () => Promise<void>\n t: TranslateFn\n /**\n * Optional map of notification type to custom renderer component.\n * When a notification's type matches a key in this map, the corresponding\n * renderer will be used instead of the default NotificationItem rendering.\n *\n * @example\n * ```tsx\n * import { salesNotificationTypes } from '@open-mercato/core/modules/sales/notifications.client'\n *\n * // Build renderers map from notification types\n * const renderers = Object.fromEntries(\n * salesNotificationTypes\n * .filter(t => t.Renderer)\n * .map(t => [t.type, t.Renderer!])\n * )\n *\n * <NotificationPanel customRenderers={renderers} ... />\n * ```\n */\n customRenderers?: NotificationRenderers\n}\n\nexport function NotificationPanel({\n open,\n onOpenChange,\n notifications,\n unreadCount,\n onMarkAsRead,\n onExecuteAction,\n onDismiss,\n dismissUndo,\n onUndoDismiss,\n onMarkAllRead,\n t,\n customRenderers,\n}: NotificationPanelProps) {\n const [filter, setFilter] = React.useState<'all' | 'unread' | 'action'>('all')\n const [markingAllRead, setMarkingAllRead] = React.useState(false)\n\n const filteredNotifications = React.useMemo(() => {\n switch (filter) {\n case 'unread':\n return notifications.filter((n) => n.status === 'unread')\n case 'action':\n return notifications.filter(\n (n) => n.actions && n.actions.length > 0 && n.status !== 'actioned'\n )\n default:\n return notifications\n }\n }, [notifications, filter])\n\n const handleMarkAllRead = async () => {\n setMarkingAllRead(true)\n try {\n await onMarkAllRead()\n } finally {\n setMarkingAllRead(false)\n }\n }\n\n React.useEffect(() => {\n if (!open) return\n const prevOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prevOverflow\n }\n }, [open])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && open) {\n onOpenChange(false)\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open, onOpenChange])\n\n if (!open) return null\n\n return (\n <>\n <div\n className=\"fixed inset-0 z-overlay bg-black/20\"\n onClick={() => onOpenChange(false)}\n aria-hidden=\"true\"\n />\n\n <div\n className=\"fixed right-0 top-0 z-modal h-full w-full max-w-md border-l bg-background shadow-lg\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('notifications.title', 'Notifications')}\n >\n <div className=\"flex h-full flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <Bell className=\"h-5 w-5\" />\n <h2 className=\"font-semibold\">{t('notifications.title', 'Notifications')}</h2>\n {unreadCount > 0 && (\n <span className=\"rounded-full bg-destructive px-2 py-0.5 text-xs text-destructive-foreground\">\n {unreadCount}\n </span>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n {unreadCount > 0 && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleMarkAllRead}\n disabled={markingAllRead}\n >\n {markingAllRead ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCheck className=\"mr-1 h-4 w-4\" />\n )}\n {t('notifications.markAllRead', 'Mark all read')}\n </Button>\n )}\n <IconButton variant=\"ghost\" size=\"lg\" onClick={() => onOpenChange(false)}>\n <X className=\"h-5 w-5\" />\n </IconButton>\n </div>\n </div>\n\n <Tabs\n value={filter}\n onValueChange={(v) => setFilter(v as typeof filter)}\n className=\"border-b\"\n >\n <TabsList className=\"w-full justify-start rounded-none border-0 bg-transparent px-4\">\n <TabsTrigger value=\"all\">\n {t('notifications.filters.all', 'All')}\n </TabsTrigger>\n <TabsTrigger value=\"unread\">\n {t('notifications.filters.unread', 'Unread')}\n </TabsTrigger>\n <TabsTrigger value=\"action\">\n {t('notifications.filters.actionRequired', 'Action Required')}\n </TabsTrigger>\n </TabsList>\n </Tabs>\n\n {dismissUndo && onUndoDismiss && (\n <div className=\"border-b bg-muted/50 px-4 py-2 text-sm\">\n <div className=\"flex items-center justify-between gap-3\">\n <span>\n {t('notifications.toast.dismissed', 'Notification dismissed')}\n </span>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => onUndoDismiss()}>\n <RotateCcw className=\"mr-1 h-3 w-3\" />\n {t('notifications.actions.undo', 'Undo')}\n </Button>\n </div>\n </div>\n )}\n\n <div className=\"flex-1 overflow-y-auto overscroll-contain\">\n {filteredNotifications.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Bell className=\"mb-2 h-8 w-8 opacity-50\" />\n <p>{t('notifications.empty', 'No notifications')}</p>\n </div>\n ) : (\n <div className=\"divide-y\">\n {filteredNotifications.map((notification) => (\n <NotificationItem\n key={notification.id}\n notification={notification}\n onMarkAsRead={() => onMarkAsRead(notification.id)}\n onExecuteAction={(actionId) => onExecuteAction(notification.id, actionId)}\n onDismiss={() => onDismiss(notification.id)}\n t={t}\n customRenderer={customRenderers?.[notification.type]}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n </div>\n </>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { ArrowDown, ArrowUp, Bell, Loader2, RotateCcw, Settings2, X } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Sheet, SheetContent, SheetTitle } from '../../primitives/sheet'\nimport { NotificationItem } from './NotificationItem'\nimport type { NotificationDto, NotificationRendererProps } from '@open-mercato/shared/modules/notifications/types'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { ComponentType } from 'react'\n\n/**\n * Map of notification type to custom renderer component.\n * Used to provide custom rendering for specific notification types.\n *\n * @example\n * ```tsx\n * const customRenderers = {\n * 'sales.order.created': SalesOrderCreatedRenderer,\n * 'sales.quote.created': SalesQuoteCreatedRenderer,\n * }\n * ```\n */\nexport type NotificationRenderers = Record<string, ComponentType<NotificationRendererProps>>\n\nexport type NotificationPanelProps = {\n open: boolean\n onOpenChange: (open: boolean) => void\n notifications: NotificationDto[]\n unreadCount: number\n onMarkAsRead: (id: string) => Promise<void>\n onExecuteAction: (id: string, actionId: string) => Promise<{ href?: string }>\n onDismiss: (id: string) => Promise<void>\n dismissUndo?: { notification: NotificationDto; previousStatus: 'read' | 'unread' } | null\n onUndoDismiss?: () => Promise<void>\n onMarkAllRead: () => Promise<void>\n t: TranslateFn\n /**\n * Optional map of notification type to custom renderer component.\n * When a notification's type matches a key in this map, the corresponding\n * renderer will be used instead of the default NotificationItem rendering.\n *\n * @example\n * ```tsx\n * import { salesNotificationTypes } from '@open-mercato/core/modules/sales/notifications.client'\n *\n * // Build renderers map from notification types\n * const renderers = Object.fromEntries(\n * salesNotificationTypes\n * .filter(t => t.Renderer)\n * .map(t => [t.type, t.Renderer!])\n * )\n *\n * <NotificationPanel customRenderers={renderers} ... />\n * ```\n */\n customRenderers?: NotificationRenderers\n}\n\nexport function NotificationPanel({\n open,\n onOpenChange,\n notifications,\n unreadCount,\n onMarkAsRead,\n onExecuteAction,\n onDismiss,\n dismissUndo,\n onUndoDismiss,\n onMarkAllRead,\n t,\n customRenderers,\n}: NotificationPanelProps) {\n const [filter, setFilter] = React.useState<'all' | 'unread' | 'action'>('all')\n const [markingAllRead, setMarkingAllRead] = React.useState(false)\n\n const filteredNotifications = React.useMemo(() => {\n switch (filter) {\n case 'unread':\n return notifications.filter((n) => n.status === 'unread')\n case 'action':\n return notifications.filter(\n (n) => n.actions && n.actions.length > 0 && n.status !== 'actioned'\n )\n default:\n return notifications\n }\n }, [notifications, filter])\n\n const handleMarkAllRead = async () => {\n setMarkingAllRead(true)\n try {\n await onMarkAllRead()\n } finally {\n setMarkingAllRead(false)\n }\n }\n\n // Preserve the body scroll lock contract that consumers (and integration\n // tests) rely on. Radix Dialog locks scroll via react-remove-scroll which\n // does not set `document.body.style.overflow` \u2014 we set it ourselves so the\n // contract is observable.\n React.useEffect(() => {\n if (!open || typeof document === 'undefined') return\n const previous = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = previous\n }\n }, [open])\n\n return (\n <Sheet open={open} onOpenChange={onOpenChange}>\n <SheetContent\n side=\"right\"\n className=\"gap-0 p-0\"\n hideClose\n aria-label={t('notifications.title', 'Notifications')}\n >\n <div className=\"flex items-center justify-between gap-3 border-b px-5 py-4\">\n <SheetTitle className=\"text-base font-medium leading-6 tracking-tight text-foreground\">\n {t('notifications.title', 'Notifications')}\n </SheetTitle>\n <div className=\"flex items-center gap-1\">\n {unreadCount > 0 ? (\n <button\n type=\"button\"\n onClick={handleMarkAllRead}\n disabled={markingAllRead}\n className=\"inline-flex items-center gap-1 rounded-md px-1 text-sm font-medium leading-5 text-accent-indigo transition-colors hover:text-accent-indigo/80 disabled:opacity-50\"\n >\n {markingAllRead ? <Loader2 className=\"size-3.5 animate-spin\" /> : null}\n {t('notifications.markAllRead', 'Mark all read')}\n </button>\n ) : null}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => onOpenChange(false)}\n aria-label={t('notifications.close', 'Close notifications')}\n >\n <X className=\"size-4\" />\n </IconButton>\n </div>\n </div>\n\n <div role=\"tablist\" className=\"flex items-center gap-5 border-b px-5 py-3.5\">\n {(['all', 'unread', 'action'] as const).map((value) => {\n const isActive = filter === value\n const label =\n value === 'all'\n ? t('notifications.filters.all', 'All')\n : value === 'unread'\n ? t('notifications.filters.unread', 'Unread')\n : t('notifications.filters.actionRequired', 'Action Required')\n return (\n <button\n key={value}\n type=\"button\"\n role=\"tab\"\n aria-selected={isActive}\n onClick={() => setFilter(value)}\n className=\"relative inline-flex items-center gap-1.5 pb-2 text-sm font-medium leading-5 tracking-tight transition-colors focus:outline-none focus-visible:text-foreground data-[active=true]:text-foreground data-[active=false]:text-muted-foreground hover:text-foreground\"\n data-active={isActive}\n >\n <span>{label}</span>\n {value === 'unread' && unreadCount > 0 ? (\n <span className=\"inline-flex min-w-4 items-center justify-center rounded-full bg-accent-indigo px-1 text-overline font-medium text-accent-indigo-foreground\">\n {unreadCount > 99 ? '99+' : unreadCount}\n </span>\n ) : null}\n {isActive ? (\n <span\n className=\"absolute bottom-[-14px] left-0 right-0 h-0.5 bg-foreground\"\n aria-hidden=\"true\"\n />\n ) : null}\n </button>\n )\n })}\n </div>\n\n {dismissUndo && onUndoDismiss && (\n <div className=\"border-b bg-muted/50 px-4 py-2 text-sm\">\n <div className=\"flex items-center justify-between gap-3\">\n <span>\n {t('notifications.toast.dismissed', 'Notification dismissed')}\n </span>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => onUndoDismiss()}>\n <RotateCcw className=\"mr-1 h-3 w-3\" />\n {t('notifications.actions.undo', 'Undo')}\n </Button>\n </div>\n </div>\n )}\n\n <div className=\"flex-1 overflow-y-auto overflow-x-hidden overscroll-contain\">\n {filteredNotifications.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center gap-2 py-16 text-muted-foreground\">\n <div className=\"flex size-12 items-center justify-center rounded-full bg-muted/50\">\n <Bell className=\"size-5\" aria-hidden=\"true\" />\n </div>\n <p className=\"text-sm\">{t('notifications.empty', 'No notifications')}</p>\n </div>\n ) : (\n <div className=\"flex flex-col gap-1 p-2\">\n {filteredNotifications.map((notification, idx) => (\n <React.Fragment key={notification.id}>\n {idx > 0 ? <div className=\"my-px h-px bg-border\" aria-hidden=\"true\" /> : null}\n <NotificationItem\n notification={notification}\n onMarkAsRead={() => onMarkAsRead(notification.id)}\n onExecuteAction={(actionId) => onExecuteAction(notification.id, actionId)}\n onDismiss={() => onDismiss(notification.id)}\n t={t}\n customRenderer={customRenderers?.[notification.type]}\n />\n </React.Fragment>\n ))}\n </div>\n )}\n </div>\n\n <div className=\"flex items-center justify-between gap-3 border-t px-5 py-3.5 text-xs leading-4 text-muted-foreground\">\n <div className=\"flex items-center gap-2\">\n <span>{t('notifications.footer.useHint', 'Use')}</span>\n <span className=\"inline-flex items-center rounded border bg-muted/50 px-1 py-0.5\">\n <ArrowUp className=\"size-3\" aria-hidden=\"true\" />\n </span>\n <span className=\"inline-flex items-center rounded border bg-muted/50 px-1 py-0.5\">\n <ArrowDown className=\"size-3\" aria-hidden=\"true\" />\n </span>\n <span>{t('notifications.footer.toNavigate', 'to navigate')}</span>\n </div>\n <Link\n href=\"/backend/config/notifications\"\n onClick={() => onOpenChange(false)}\n className=\"inline-flex items-center gap-1.5 font-medium text-muted-foreground transition-colors hover:text-foreground\"\n >\n <Settings2 className=\"size-3.5\" aria-hidden=\"true\" />\n {t('notifications.footer.manage', 'Manage notifications')}\n </Link>\n </div>\n </SheetContent>\n </Sheet>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|