@papernote/ui 1.8.3 → 1.9.1

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/dist/index.js CHANGED
@@ -10076,6 +10076,203 @@ function NotificationIndicator({ count = 0, onClick, className = '', maxCount =
10076
10076
  return (jsxRuntime.jsxs("button", { onClick: onClick, className: `relative bg-white p-2.5 rounded-lg text-ink-400 hover:text-ink-600 hover:bg-paper-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400 transition-all shadow-xs dark:bg-slate-800 dark:text-slate-400 dark:hover:text-slate-100 dark:hover:bg-slate-700 ${className}`, "aria-label": "View notifications", children: [jsxRuntime.jsx(lucideReact.Bell, { className: "h-5 w-5" }), showBadge && (jsxRuntime.jsx("span", { className: `absolute -top-1 -right-1 ${variantClasses[variant]} text-white text-xs font-semibold rounded-full h-5 min-w-5 px-1 flex items-center justify-center`, children: displayCount }))] }));
10077
10077
  }
10078
10078
 
10079
+ /**
10080
+ * Format a date to a relative time string
10081
+ */
10082
+ function formatTimeAgo(date) {
10083
+ const now = new Date();
10084
+ const then = new Date(date);
10085
+ const diffMs = now.getTime() - then.getTime();
10086
+ const diffSeconds = Math.floor(diffMs / 1000);
10087
+ const diffMinutes = Math.floor(diffSeconds / 60);
10088
+ const diffHours = Math.floor(diffMinutes / 60);
10089
+ const diffDays = Math.floor(diffHours / 24);
10090
+ if (diffSeconds < 60) {
10091
+ return 'Just now';
10092
+ }
10093
+ else if (diffMinutes < 60) {
10094
+ return `${diffMinutes}m ago`;
10095
+ }
10096
+ else if (diffHours < 24) {
10097
+ return `${diffHours}h ago`;
10098
+ }
10099
+ else if (diffDays < 7) {
10100
+ return `${diffDays}d ago`;
10101
+ }
10102
+ else {
10103
+ return then.toLocaleDateString();
10104
+ }
10105
+ }
10106
+ /**
10107
+ * Map notification type to Badge variant
10108
+ */
10109
+ const typeToBadgeVariant = {
10110
+ info: 'info',
10111
+ success: 'success',
10112
+ warning: 'warning',
10113
+ error: 'error',
10114
+ };
10115
+ /**
10116
+ * Default labels for notification types
10117
+ */
10118
+ const defaultTypeLabels = {
10119
+ info: 'Info',
10120
+ success: 'Success',
10121
+ warning: 'Warning',
10122
+ error: 'Alert',
10123
+ };
10124
+ /**
10125
+ * Map dropdown position to Popover placement
10126
+ */
10127
+ function getPopoverPlacement(position) {
10128
+ switch (position) {
10129
+ case 'bottom-right':
10130
+ case 'right':
10131
+ return 'bottom-start';
10132
+ case 'bottom-left':
10133
+ case 'left':
10134
+ return 'bottom-end';
10135
+ case 'top-right':
10136
+ return 'top-start';
10137
+ case 'top-left':
10138
+ return 'top-end';
10139
+ default:
10140
+ return 'bottom-start';
10141
+ }
10142
+ }
10143
+ /**
10144
+ * NotificationBell - A bell icon with badge and dropdown for displaying notifications
10145
+ *
10146
+ * Displays a bell icon with an optional unread count badge. When clicked, shows a
10147
+ * dropdown panel with recent notifications, mark as read actions, and a link to
10148
+ * view all notifications.
10149
+ *
10150
+ * @example Basic usage (compact variant)
10151
+ * ```tsx
10152
+ * <NotificationBell
10153
+ * notifications={notifications}
10154
+ * onMarkAsRead={(id) => markRead(id)}
10155
+ * onMarkAllRead={() => markAllRead()}
10156
+ * onNotificationClick={(n) => navigate(n.actionUrl)}
10157
+ * onViewAll={() => navigate('/notifications')}
10158
+ * />
10159
+ * ```
10160
+ *
10161
+ * @example Detailed variant with labeled badges
10162
+ * ```tsx
10163
+ * <NotificationBell
10164
+ * notifications={notifications}
10165
+ * variant="detailed"
10166
+ * showUnreadInHeader
10167
+ * dropdownPosition="bottom-left"
10168
+ * />
10169
+ * ```
10170
+ */
10171
+ function NotificationBell({ notifications, unreadCount: providedUnreadCount, onMarkAsRead, onMarkAllRead, onNotificationClick, onViewAll, loading = false, dropdownPosition = 'bottom-right', maxHeight = '400px', size = 'md', emptyMessage = 'No notifications', viewAllText = 'View all notifications', disabled = false, className = '', variant = 'compact', showUnreadInHeader = false, bellStyle = 'ghost', }) {
10172
+ const [isOpen, setIsOpen] = React.useState(false);
10173
+ // Calculate unread count if not provided
10174
+ const unreadCount = React.useMemo(() => {
10175
+ if (providedUnreadCount !== undefined) {
10176
+ return providedUnreadCount;
10177
+ }
10178
+ return notifications.filter((n) => !n.isRead).length;
10179
+ }, [providedUnreadCount, notifications]);
10180
+ // Handle notification click
10181
+ const handleNotificationClick = React.useCallback((notification) => {
10182
+ onNotificationClick?.(notification);
10183
+ }, [onNotificationClick]);
10184
+ // Handle mark as read
10185
+ const handleMarkAsRead = React.useCallback((e, id) => {
10186
+ e.stopPropagation();
10187
+ onMarkAsRead?.(id);
10188
+ }, [onMarkAsRead]);
10189
+ // Handle mark all as read
10190
+ const handleMarkAllRead = React.useCallback(() => {
10191
+ onMarkAllRead?.();
10192
+ }, [onMarkAllRead]);
10193
+ // Handle view all
10194
+ const handleViewAll = React.useCallback(() => {
10195
+ onViewAll?.();
10196
+ setIsOpen(false);
10197
+ }, [onViewAll]);
10198
+ // Icon sizes based on button size
10199
+ const iconSizes = {
10200
+ sm: 'h-4 w-4',
10201
+ md: 'h-5 w-5',
10202
+ lg: 'h-6 w-6',
10203
+ };
10204
+ // Dropdown width based on size
10205
+ const dropdownWidths = {
10206
+ sm: 'w-72',
10207
+ md: 'w-80',
10208
+ lg: 'w-96',
10209
+ };
10210
+ // Outlined bell style classes
10211
+ const outlinedSizeClasses = {
10212
+ sm: 'p-2',
10213
+ md: 'p-3',
10214
+ lg: 'p-4',
10215
+ };
10216
+ // Trigger button
10217
+ const triggerButton = bellStyle === 'outlined' ? (jsxRuntime.jsxs("div", { className: "relative inline-block", children: [jsxRuntime.jsx("button", { className: `
10218
+ ${outlinedSizeClasses[size]}
10219
+ bg-white border-2 border-paper-300 rounded-xl
10220
+ hover:bg-paper-50 hover:border-paper-400
10221
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
10222
+ transition-all duration-200
10223
+ disabled:opacity-40 disabled:cursor-not-allowed
10224
+ ${className}
10225
+ `, disabled: disabled, "aria-label": unreadCount > 0
10226
+ ? `Notifications - ${unreadCount} unread`
10227
+ : 'Notifications', children: jsxRuntime.jsx(lucideReact.Bell, { className: `${iconSizes[size]} text-ink-600` }) }), unreadCount > 0 && (jsxRuntime.jsx("span", { className: `
10228
+ absolute -top-1 -right-1
10229
+ flex items-center justify-center
10230
+ min-w-[18px] h-[18px] px-1.5
10231
+ rounded-full text-white font-semibold text-[11px]
10232
+ bg-error-500 shadow-sm
10233
+ pointer-events-none
10234
+ `, "aria-label": `${unreadCount > 99 ? '99+' : unreadCount} notifications`, children: unreadCount > 99 ? '99+' : unreadCount }))] })) : (jsxRuntime.jsx(Button, { variant: "ghost", iconOnly: true, size: size, disabled: disabled, badge: unreadCount > 0 ? unreadCount : undefined, badgeVariant: "error", "aria-label": unreadCount > 0
10235
+ ? `Notifications - ${unreadCount} unread`
10236
+ : 'Notifications', className: className, children: jsxRuntime.jsx(lucideReact.Bell, { className: iconSizes[size] }) }));
10237
+ // Header title with optional unread count
10238
+ const headerTitle = showUnreadInHeader && unreadCount > 0
10239
+ ? `Notifications (${unreadCount} unread)`
10240
+ : 'Notifications';
10241
+ // Render compact notification item
10242
+ const renderCompactItem = (notification) => (jsxRuntime.jsxs("div", { className: "flex gap-3", children: [jsxRuntime.jsx("div", { className: "flex-shrink-0 pt-1", children: jsxRuntime.jsx(Badge, { dot: true, variant: typeToBadgeVariant[notification.type], size: "sm" }) }), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntime.jsx(Text, { size: "sm", weight: notification.priority === 'high' ||
10243
+ notification.priority === 'urgent' ||
10244
+ !notification.isRead
10245
+ ? 'medium'
10246
+ : 'normal', truncate: true, children: notification.title }), jsxRuntime.jsx(Text, { size: "xs", color: "muted", lineClamp: 2, className: "mt-0.5", children: notification.message }), jsxRuntime.jsx(Text, { size: "xs", color: "muted", className: "mt-1", children: formatTimeAgo(notification.createdAt) })] }), !notification.isRead && onMarkAsRead && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 text-ink-400 hover:text-ink-600 hover:bg-paper-100 rounded transition-colors", onClick: (e) => handleMarkAsRead(e, notification.id), "aria-label": "Mark as read", title: "Mark as read", children: jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4" }) }))] }));
10247
+ // Render detailed notification item
10248
+ const renderDetailedItem = (notification) => (jsxRuntime.jsxs("div", { className: "flex gap-3", children: [jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-2", children: [jsxRuntime.jsx(Text, { size: "sm", weight: notification.priority === 'high' ||
10249
+ notification.priority === 'urgent' ||
10250
+ !notification.isRead
10251
+ ? 'semibold'
10252
+ : 'medium', className: "flex-1", children: notification.title }), jsxRuntime.jsx(Text, { size: "xs", color: "muted", className: "flex-shrink-0 whitespace-nowrap", children: formatTimeAgo(notification.createdAt) })] }), jsxRuntime.jsx(Text, { size: "xs", color: "muted", lineClamp: 2, className: "mt-1", children: notification.message }), jsxRuntime.jsx("div", { className: "mt-2", children: jsxRuntime.jsx(Badge, { variant: typeToBadgeVariant[notification.type], size: "sm", children: notification.typeLabel || defaultTypeLabels[notification.type] }) })] }), !notification.isRead && onMarkAsRead && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 text-ink-400 hover:text-ink-600 hover:bg-paper-100 rounded transition-colors self-center", onClick: (e) => handleMarkAsRead(e, notification.id), "aria-label": "Mark as read", title: "Mark as read", children: jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4" }) }))] }));
10253
+ // Dropdown content
10254
+ const dropdownContent = (jsxRuntime.jsxs("div", { className: `${dropdownWidths[size]} bg-white`, children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-paper-200", children: [jsxRuntime.jsx(Text, { weight: "semibold", size: "sm", children: headerTitle }), unreadCount > 0 && onMarkAllRead && (jsxRuntime.jsxs("button", { className: "flex items-center gap-1.5 text-xs text-ink-600 hover:text-ink-800 transition-colors", onClick: handleMarkAllRead, children: [jsxRuntime.jsx(lucideReact.Check, { className: "h-3.5 w-3.5" }), "Mark all read"] }))] }), jsxRuntime.jsx("div", { className: "overflow-y-auto", style: { maxHeight }, role: "list", "aria-label": "Notifications", children: loading ? (
10255
+ // Loading state
10256
+ jsxRuntime.jsx("div", { className: "p-4", children: jsxRuntime.jsx(Stack, { spacing: "sm", children: [1, 2, 3].map((i) => (jsxRuntime.jsxs("div", { className: "flex gap-3", children: [variant === 'compact' && (jsxRuntime.jsx(Skeleton, { className: "h-4 w-4 rounded-full flex-shrink-0" })), jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsxs("div", { className: "flex justify-between", children: [jsxRuntime.jsx(Skeleton, { className: "h-4 w-3/4 mb-2" }), variant === 'detailed' && (jsxRuntime.jsx(Skeleton, { className: "h-3 w-12" }))] }), jsxRuntime.jsx(Skeleton, { className: "h-3 w-full mb-1" }), variant === 'compact' ? (jsxRuntime.jsx(Skeleton, { className: "h-3 w-1/4" })) : (jsxRuntime.jsx(Skeleton, { className: "h-5 w-16 mt-2" }))] })] }, i))) }) })) : notifications.length === 0 ? (
10257
+ // Empty state
10258
+ jsxRuntime.jsxs("div", { className: "py-8 px-4 text-center", children: [jsxRuntime.jsx(lucideReact.Bell, { className: "h-10 w-10 text-ink-300 mx-auto mb-3" }), jsxRuntime.jsx(Text, { color: "muted", size: "sm", children: emptyMessage })] })) : (
10259
+ // Notification items
10260
+ notifications.map((notification) => (jsxRuntime.jsx("div", { role: "listitem", className: `
10261
+ px-4 py-3 border-b border-paper-100 last:border-b-0
10262
+ hover:bg-paper-50 transition-colors cursor-pointer
10263
+ ${!notification.isRead ? 'bg-primary-50/30' : ''}
10264
+ ${notification.priority === 'urgent' ? 'border-l-2 border-l-error-500' : ''}
10265
+ `, onClick: () => handleNotificationClick(notification), onKeyDown: (e) => {
10266
+ if (e.key === 'Enter' || e.key === ' ') {
10267
+ e.preventDefault();
10268
+ handleNotificationClick(notification);
10269
+ }
10270
+ }, tabIndex: 0, children: variant === 'compact'
10271
+ ? renderCompactItem(notification)
10272
+ : renderDetailedItem(notification) }, notification.id)))) }), onViewAll && notifications.length > 0 && (jsxRuntime.jsx("div", { className: "px-4 py-3 border-t border-paper-200", children: jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", fullWidth: true, onClick: handleViewAll, icon: jsxRuntime.jsx(lucideReact.ExternalLink, { className: "h-3.5 w-3.5" }), iconPosition: "right", children: viewAllText }) }))] }));
10273
+ return (jsxRuntime.jsx(Popover, { trigger: triggerButton, placement: getPopoverPlacement(dropdownPosition), triggerMode: "click", showArrow: false, offset: 4, open: isOpen, onOpenChange: setIsOpen, closeOnClickOutside: true, closeOnEscape: true, disabled: disabled, className: "p-0 overflow-hidden", children: dropdownContent }));
10274
+ }
10275
+
10079
10276
  /**
10080
10277
  * Get value from item by key path (supports nested keys like 'user.name')
10081
10278
  */
@@ -57799,6 +57996,7 @@ exports.ModalFooter = ModalFooter;
57799
57996
  exports.MultiSelect = MultiSelect;
57800
57997
  exports.NotificationBanner = NotificationBanner;
57801
57998
  exports.NotificationBar = NotificationBar;
57999
+ exports.NotificationBell = NotificationBell;
57802
58000
  exports.NotificationIndicator = NotificationIndicator;
57803
58001
  exports.NumberInput = NumberInput;
57804
58002
  exports.Page = Page;