@open-mercato/ui 0.6.8-develop.6975.1.b21e19d444 → 0.6.8-develop.6977.1.5041da373e

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.
@@ -7,6 +7,7 @@ import { Button } from "../../primitives/button.js";
7
7
  import { IconButton } from "../../primitives/icon-button.js";
8
8
  import { cn } from "@open-mercato/shared/lib/utils";
9
9
  import { formatRelativeTime } from "@open-mercato/shared/lib/time";
10
+ import { useOptionalLocale } from "@open-mercato/shared/lib/i18n/context";
10
11
  function resolveNotificationText(params) {
11
12
  const { key, fallback, variables, t } = params;
12
13
  if (key) {
@@ -48,6 +49,7 @@ function NotificationItem({
48
49
  customRenderer: CustomRenderer
49
50
  }) {
50
51
  const router = useRouter();
52
+ const locale = useOptionalLocale();
51
53
  const [executing, setExecuting] = React.useState(null);
52
54
  const isUnread = notification.status === "unread";
53
55
  const hasActions = notification.actions && notification.actions.length > 0;
@@ -67,7 +69,7 @@ function NotificationItem({
67
69
  variables: notification.bodyVariables ?? void 0,
68
70
  t
69
71
  });
70
- const timeAgo = formatRelativeTime(notification.createdAt, { translate: t }) ?? "";
72
+ const timeAgo = formatRelativeTime(notification.createdAt, { locale, translate: t }) ?? "";
71
73
  const sourceLabel = humanizeModuleId(notification.sourceModule);
72
74
  const handleClick = async () => {
73
75
  if (isUnread) await onMarkAsRead();
@@ -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 { 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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",
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 { useOptionalLocale } from '@open-mercato/shared/lib/i18n/context'\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 locale = useOptionalLocale()\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, { locale, 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.8-develop.6975.1.b21e19d444",
3
+ "version": "0.6.8-develop.6977.1.5041da373e",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -155,14 +155,14 @@
155
155
  "remark-gfm": "^4.0.1"
156
156
  },
157
157
  "peerDependencies": {
158
- "@open-mercato/shared": "0.6.8-develop.6975.1.b21e19d444",
158
+ "@open-mercato/shared": "0.6.8-develop.6977.1.5041da373e",
159
159
  "react": ">=18.0.0",
160
160
  "react-dom": ">=18.0.0",
161
161
  "react-is": ">=18.0.0"
162
162
  },
163
163
  "devDependencies": {
164
164
  "@figma/code-connect": "^1.3.4",
165
- "@open-mercato/shared": "0.6.8-develop.6975.1.b21e19d444",
165
+ "@open-mercato/shared": "0.6.8-develop.6977.1.5041da373e",
166
166
  "@testing-library/dom": "^10.4.1",
167
167
  "@testing-library/jest-dom": "^7.0.0",
168
168
  "@testing-library/react": "^16.3.1",
@@ -7,6 +7,7 @@ import { IconButton } from '../../primitives/icon-button'
7
7
  import { cn } from '@open-mercato/shared/lib/utils'
8
8
  import { formatRelativeTime } from '@open-mercato/shared/lib/time'
9
9
  import type { NotificationDto, NotificationRendererProps, NotificationTypeAction } from '@open-mercato/shared/modules/notifications/types'
10
+ import { useOptionalLocale } from '@open-mercato/shared/lib/i18n/context'
10
11
  import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
11
12
  import type { ComponentType } from 'react'
12
13
 
@@ -75,6 +76,7 @@ export function NotificationItem({
75
76
  customRenderer: CustomRenderer,
76
77
  }: NotificationItemProps) {
77
78
  const router = useRouter()
79
+ const locale = useOptionalLocale()
78
80
  const [executing, setExecuting] = React.useState<string | null>(null)
79
81
 
80
82
  const isUnread = notification.status === 'unread'
@@ -96,7 +98,7 @@ export function NotificationItem({
96
98
  variables: notification.bodyVariables ?? undefined,
97
99
  t,
98
100
  })
99
- const timeAgo = formatRelativeTime(notification.createdAt, { translate: t }) ?? ''
101
+ const timeAgo = formatRelativeTime(notification.createdAt, { locale, translate: t }) ?? ''
100
102
  const sourceLabel = humanizeModuleId(notification.sourceModule)
101
103
 
102
104
  const handleClick = async () => {