@campfire-interactive/shell-header 0.1.5 → 0.2.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.d.ts +36 -4
- package/dist/index.js +120 -10
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,20 @@ interface ShellUser {
|
|
|
16
16
|
email: string;
|
|
17
17
|
avatarUrl?: string;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Subset of the platform-notifications `Notification` shape that the bell UI
|
|
21
|
+
* actually renders. Hosts typically map their fetched items (e.g., from
|
|
22
|
+
* `useNotifications` in @campfire-interactive/notifications-client/react)
|
|
23
|
+
* into this shape.
|
|
24
|
+
*/
|
|
25
|
+
interface NotificationItem {
|
|
26
|
+
id: string;
|
|
27
|
+
title: string;
|
|
28
|
+
body: string;
|
|
29
|
+
link: string | null;
|
|
30
|
+
readAt: string | null;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
}
|
|
19
33
|
interface ShellHeaderProps {
|
|
20
34
|
/** Current app ID — used to highlight the active app in the switcher */
|
|
21
35
|
appId: string;
|
|
@@ -23,10 +37,28 @@ interface ShellHeaderProps {
|
|
|
23
37
|
user: ShellUser;
|
|
24
38
|
/** App IDs the user is authorized to access in the current tenant */
|
|
25
39
|
authorizedApps: string[];
|
|
26
|
-
/** Unread
|
|
40
|
+
/** Unread count for the bell badge. */
|
|
27
41
|
notificationCount?: number;
|
|
28
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Legacy: fires when the bell icon is clicked. Only used when `notifications`
|
|
44
|
+
* is NOT provided (icon-only mode). When `notifications` is an array, the
|
|
45
|
+
* bell opens a dropdown instead.
|
|
46
|
+
*/
|
|
29
47
|
onNotificationClick?: () => void;
|
|
48
|
+
/**
|
|
49
|
+
* If provided, the bell renders a popover dropdown with these items. Pass
|
|
50
|
+
* an empty array for the "no notifications yet" empty state.
|
|
51
|
+
*/
|
|
52
|
+
notifications?: NotificationItem[];
|
|
53
|
+
/** Optimistically mark a single notification read. */
|
|
54
|
+
onMarkRead?: (id: string) => void;
|
|
55
|
+
/** Mark every unread notification in scope read. */
|
|
56
|
+
onMarkAllRead?: () => void;
|
|
57
|
+
/**
|
|
58
|
+
* Fires when a notification row in the dropdown is clicked (after the
|
|
59
|
+
* built-in auto-mark-read). Hosts handle navigation here using `item.link`.
|
|
60
|
+
*/
|
|
61
|
+
onNotificationItemClick?: (item: NotificationItem) => void;
|
|
30
62
|
/** Called when logout is clicked */
|
|
31
63
|
onLogout?: () => void;
|
|
32
64
|
/** Fixed width for the app brand area (e.g., to align with a sidebar below). */
|
|
@@ -35,7 +67,7 @@ interface ShellHeaderProps {
|
|
|
35
67
|
children?: React.ReactNode;
|
|
36
68
|
}
|
|
37
69
|
|
|
38
|
-
declare function ShellHeader({ appId, user, authorizedApps, notificationCount, onNotificationClick, onLogout, brandWidth, children, }: ShellHeaderProps): react_jsx_runtime.JSX.Element;
|
|
70
|
+
declare function ShellHeader({ appId, user, authorizedApps, notificationCount, onNotificationClick, notifications, onMarkRead, onMarkAllRead, onNotificationItemClick, onLogout, brandWidth, children, }: ShellHeaderProps): react_jsx_runtime.JSX.Element;
|
|
39
71
|
|
|
40
72
|
declare const appCatalog: AppDefinition[];
|
|
41
73
|
/**
|
|
@@ -46,4 +78,4 @@ declare const appCatalog: AppDefinition[];
|
|
|
46
78
|
*/
|
|
47
79
|
declare function getAppUrl(app: AppDefinition): string;
|
|
48
80
|
|
|
49
|
-
export { type AppDefinition, ShellHeader, type ShellHeaderProps, type ShellUser, appCatalog, getAppUrl };
|
|
81
|
+
export { type AppDefinition, type NotificationItem, ShellHeader, type ShellHeaderProps, type ShellUser, appCatalog, getAppUrl };
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ function styleInject(css, { insertAt } = {}) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// src/styles.css
|
|
27
|
-
styleInject(".cfi-sh-header {\n display: flex;\n align-items: center;\n height: var(--cfi-shell-header-height, 48px);\n padding: 0 var(--cfi-spacing-md, 1rem);\n background: white;\n color: var(--cfi-color-gray-900, #111827);\n font-family: var(--cfi-font-family, system-ui, sans-serif);\n font-size: var(--cfi-font-size-sm, 0.875rem);\n border-bottom: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n position: relative;\n z-index: 1000;\n}\n.cfi-sh-left {\n display: flex;\n align-items: center;\n flex: 1;\n gap: var(--cfi-spacing-md, 1rem);\n overflow: hidden;\n}\n.cfi-sh-app-brand {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-sm, 0.5rem);\n flex-shrink: 0;\n}\n.cfi-sh-app-badge {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n color: white;\n font-size: 14px;\n font-weight: 700;\n font-family: inherit;\n border-radius: var(--cfi-radius-md, 0.375rem);\n flex-shrink: 0;\n}\n.cfi-sh-app-title {\n font-weight: 600;\n font-size: var(--cfi-font-size-base, 1rem);\n color: var(--cfi-color-gray-900, #111827);\n white-space: nowrap;\n}\n.cfi-sh-right {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-xs, 0.25rem);\n flex-shrink: 0;\n}\n.cfi-sh-icon-btn {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n background: transparent;\n border: none;\n border-radius: 50%;\n color: var(--cfi-color-gray-600, #4b5563);\n cursor: pointer;\n transition: background 150ms;\n}\n.cfi-sh-icon-btn:hover {\n background: var(--cfi-color-gray-100, #f3f4f6);\n}\n.cfi-sh-badge {\n position: absolute;\n top: 2px;\n right: 2px;\n min-width: 16px;\n height: 16px;\n padding: 0 4px;\n background: var(--cfi-color-error, #ef4444);\n color: white;\n font-size: 10px;\n font-weight: 600;\n line-height: 16px;\n text-align: center;\n border-radius: 99px;\n}\n.cfi-sh-app-switcher {\n position: relative;\n}\n.cfi-sh-app-grid {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: var(--cfi-spacing-xs, 0.25rem);\n padding: var(--cfi-spacing-sm, 0.5rem);\n background: white;\n border-radius: var(--cfi-radius-lg, 0.5rem);\n box-shadow: var(--cfi-shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.1));\n border: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n z-index: 1001;\n min-width: 280px;\n}\n.cfi-sh-app-grid-item {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--cfi-spacing-xs, 0.25rem);\n padding: var(--cfi-spacing-sm, 0.5rem) var(--cfi-spacing-xs, 0.25rem);\n border-radius: var(--cfi-radius-md, 0.375rem);\n color: var(--cfi-color-gray-700, #374151);\n text-decoration: none;\n cursor: pointer;\n transition: background 100ms;\n font-family: inherit;\n}\n.cfi-sh-app-grid-item:hover {\n background: var(--cfi-color-gray-100, #f3f4f6);\n}\n.cfi-sh-app-grid-item-active {\n background: var(--cfi-color-primary-50, #fff7ed);\n color: var(--cfi-color-primary-700, #c2410c);\n}\n.cfi-sh-app-grid-item-active:hover {\n background: var(--cfi-color-primary-100, #ffedd5);\n}\n.cfi-sh-app-grid-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border-radius: 50%;\n}\n.cfi-sh-app-grid-label {\n font-size: var(--cfi-font-size-xs, 0.75rem);\n text-align: center;\n line-height: 1.2;\n max-width: 80px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cfi-sh-user-menu {\n position: relative;\n}\n.cfi-sh-avatar-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n padding: 0;\n background: var(--cfi-color-primary-600, #ea580c);\n border: none;\n border-radius: 50%;\n cursor: pointer;\n transition: opacity 150ms;\n}\n.cfi-sh-avatar-btn:hover {\n opacity: 0.85;\n}\n.cfi-sh-avatar-img {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n object-fit: cover;\n}\n.cfi-sh-avatar-initials {\n color: white;\n font-size: var(--cfi-font-size-xs, 0.75rem);\n font-weight: 600;\n font-family: inherit;\n}\n.cfi-sh-user-dropdown {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n min-width: 200px;\n background: white;\n border-radius: var(--cfi-radius-lg, 0.5rem);\n box-shadow: var(--cfi-shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.1));\n border: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n z-index: 1001;\n}\n.cfi-sh-user-header {\n padding: var(--cfi-spacing-sm, 0.5rem) var(--cfi-spacing-md, 1rem);\n display: flex;\n flex-direction: column;\n}\n.cfi-sh-user-name {\n font-weight: 500;\n color: var(--cfi-color-gray-900, #111827);\n font-size: var(--cfi-font-size-sm, 0.875rem);\n}\n.cfi-sh-user-email {\n color: var(--cfi-color-gray-500, #6b7280);\n font-size: var(--cfi-font-size-xs, 0.75rem);\n}\n.cfi-sh-divider {\n height: 1px;\n background: var(--cfi-color-gray-200, #e5e7eb);\n}\n.cfi-sh-menu-item {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-sm, 0.5rem);\n width: 100%;\n padding: var(--cfi-spacing-sm, 0.5rem) var(--cfi-spacing-md, 1rem);\n background: none;\n border: none;\n color: var(--cfi-color-gray-700, #374151);\n cursor: pointer;\n font-size: var(--cfi-font-size-sm, 0.875rem);\n font-family: inherit;\n transition: background 100ms;\n}\n.cfi-sh-menu-item:hover {\n background: var(--cfi-color-gray-100, #f3f4f6);\n}\n");
|
|
27
|
+
styleInject(".cfi-sh-header {\n display: flex;\n align-items: center;\n height: var(--cfi-shell-header-height, 48px);\n padding: 0 var(--cfi-spacing-md, 1rem);\n background: white;\n color: var(--cfi-color-gray-900, #111827);\n font-family: var(--cfi-font-family, system-ui, sans-serif);\n font-size: var(--cfi-font-size-sm, 0.875rem);\n border-bottom: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n position: relative;\n z-index: 1000;\n}\n.cfi-sh-left {\n display: flex;\n align-items: center;\n flex: 1;\n gap: var(--cfi-spacing-md, 1rem);\n overflow: hidden;\n}\n.cfi-sh-app-brand {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-sm, 0.5rem);\n flex-shrink: 0;\n}\n.cfi-sh-app-badge {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n color: white;\n font-size: 14px;\n font-weight: 700;\n font-family: inherit;\n border-radius: var(--cfi-radius-md, 0.375rem);\n flex-shrink: 0;\n}\n.cfi-sh-app-title {\n font-weight: 600;\n font-size: var(--cfi-font-size-base, 1rem);\n color: var(--cfi-color-gray-900, #111827);\n white-space: nowrap;\n}\n.cfi-sh-right {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-xs, 0.25rem);\n flex-shrink: 0;\n}\n.cfi-sh-icon-btn {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n background: transparent;\n border: none;\n border-radius: 50%;\n color: var(--cfi-color-gray-600, #4b5563);\n cursor: pointer;\n transition: background 150ms;\n}\n.cfi-sh-icon-btn:hover {\n background: var(--cfi-color-gray-100, #f3f4f6);\n}\n.cfi-sh-badge {\n position: absolute;\n top: 2px;\n right: 2px;\n min-width: 16px;\n height: 16px;\n padding: 0 4px;\n background: var(--cfi-color-error, #ef4444);\n color: white;\n font-size: 10px;\n font-weight: 600;\n line-height: 16px;\n text-align: center;\n border-radius: 99px;\n}\n.cfi-sh-app-switcher {\n position: relative;\n}\n.cfi-sh-app-grid {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: var(--cfi-spacing-xs, 0.25rem);\n padding: var(--cfi-spacing-sm, 0.5rem);\n background: white;\n border-radius: var(--cfi-radius-lg, 0.5rem);\n box-shadow: var(--cfi-shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.1));\n border: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n z-index: 1001;\n min-width: 280px;\n}\n.cfi-sh-app-grid-item {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--cfi-spacing-xs, 0.25rem);\n padding: var(--cfi-spacing-sm, 0.5rem) var(--cfi-spacing-xs, 0.25rem);\n border-radius: var(--cfi-radius-md, 0.375rem);\n color: var(--cfi-color-gray-700, #374151);\n text-decoration: none;\n cursor: pointer;\n transition: background 100ms;\n font-family: inherit;\n}\n.cfi-sh-app-grid-item:hover {\n background: var(--cfi-color-gray-100, #f3f4f6);\n}\n.cfi-sh-app-grid-item-active {\n background: var(--cfi-color-primary-50, #fff7ed);\n color: var(--cfi-color-primary-700, #c2410c);\n}\n.cfi-sh-app-grid-item-active:hover {\n background: var(--cfi-color-primary-100, #ffedd5);\n}\n.cfi-sh-app-grid-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border-radius: 50%;\n}\n.cfi-sh-app-grid-label {\n font-size: var(--cfi-font-size-xs, 0.75rem);\n text-align: center;\n line-height: 1.2;\n max-width: 80px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cfi-sh-user-menu {\n position: relative;\n}\n.cfi-sh-avatar-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n padding: 0;\n background: var(--cfi-color-primary-600, #ea580c);\n border: none;\n border-radius: 50%;\n cursor: pointer;\n transition: opacity 150ms;\n}\n.cfi-sh-avatar-btn:hover {\n opacity: 0.85;\n}\n.cfi-sh-avatar-img {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n object-fit: cover;\n}\n.cfi-sh-avatar-initials {\n color: white;\n font-size: var(--cfi-font-size-xs, 0.75rem);\n font-weight: 600;\n font-family: inherit;\n}\n.cfi-sh-user-dropdown {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n min-width: 200px;\n background: white;\n border-radius: var(--cfi-radius-lg, 0.5rem);\n box-shadow: var(--cfi-shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.1));\n border: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n z-index: 1001;\n}\n.cfi-sh-user-header {\n padding: var(--cfi-spacing-sm, 0.5rem) var(--cfi-spacing-md, 1rem);\n display: flex;\n flex-direction: column;\n}\n.cfi-sh-user-name {\n font-weight: 500;\n color: var(--cfi-color-gray-900, #111827);\n font-size: var(--cfi-font-size-sm, 0.875rem);\n}\n.cfi-sh-user-email {\n color: var(--cfi-color-gray-500, #6b7280);\n font-size: var(--cfi-font-size-xs, 0.75rem);\n}\n.cfi-sh-divider {\n height: 1px;\n background: var(--cfi-color-gray-200, #e5e7eb);\n}\n.cfi-sh-menu-item {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-sm, 0.5rem);\n width: 100%;\n padding: var(--cfi-spacing-sm, 0.5rem) var(--cfi-spacing-md, 1rem);\n background: none;\n border: none;\n color: var(--cfi-color-gray-700, #374151);\n cursor: pointer;\n font-size: var(--cfi-font-size-sm, 0.875rem);\n font-family: inherit;\n transition: background 100ms;\n}\n.cfi-sh-menu-item:hover {\n background: var(--cfi-color-gray-100, #f3f4f6);\n}\n.cfi-sh-notif {\n position: relative;\n}\n.cfi-sh-notif-dropdown {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n width: 360px;\n max-height: 480px;\n display: flex;\n flex-direction: column;\n background: white;\n border-radius: var(--cfi-radius-lg, 0.5rem);\n box-shadow: var(--cfi-shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.1));\n border: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n z-index: 1001;\n overflow: hidden;\n}\n.cfi-sh-notif-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--cfi-spacing-sm, 0.5rem) var(--cfi-spacing-md, 1rem);\n border-bottom: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n flex-shrink: 0;\n}\n.cfi-sh-notif-title {\n font-weight: 600;\n color: var(--cfi-color-gray-900, #111827);\n}\n.cfi-sh-notif-mark-all {\n background: none;\n border: none;\n color: var(--cfi-color-primary-600, #ea580c);\n cursor: pointer;\n font-size: var(--cfi-font-size-xs, 0.75rem);\n font-family: inherit;\n padding: 0;\n}\n.cfi-sh-notif-mark-all:hover {\n text-decoration: underline;\n}\n.cfi-sh-notif-list {\n overflow-y: auto;\n flex: 1;\n}\n.cfi-sh-notif-empty {\n padding: var(--cfi-spacing-lg, 1.5rem) var(--cfi-spacing-md, 1rem);\n color: var(--cfi-color-gray-500, #6b7280);\n text-align: center;\n font-size: var(--cfi-font-size-sm, 0.875rem);\n}\n.cfi-sh-notif-item {\n display: flex;\n align-items: flex-start;\n gap: var(--cfi-spacing-sm, 0.5rem);\n width: 100%;\n padding: var(--cfi-spacing-sm, 0.5rem) var(--cfi-spacing-md, 1rem);\n background: none;\n border: none;\n border-bottom: 1px solid var(--cfi-color-gray-100, #f3f4f6);\n text-align: left;\n cursor: pointer;\n font-family: inherit;\n transition: background 100ms;\n}\n.cfi-sh-notif-item:hover {\n background: var(--cfi-color-gray-50, #f9fafb);\n}\n.cfi-sh-notif-item:last-child {\n border-bottom: none;\n}\n.cfi-sh-notif-dot {\n flex-shrink: 0;\n width: 8px;\n height: 8px;\n margin-top: 6px;\n background: var(--cfi-color-primary-600, #ea580c);\n border-radius: 50%;\n}\n.cfi-sh-notif-dot-placeholder {\n flex-shrink: 0;\n width: 8px;\n height: 8px;\n margin-top: 6px;\n}\n.cfi-sh-notif-content {\n flex: 1;\n min-width: 0;\n}\n.cfi-sh-notif-item-title {\n font-weight: 500;\n color: var(--cfi-color-gray-900, #111827);\n font-size: var(--cfi-font-size-sm, 0.875rem);\n margin-bottom: 2px;\n}\n.cfi-sh-notif-item-unread .cfi-sh-notif-item-title {\n font-weight: 600;\n}\n.cfi-sh-notif-item-body {\n color: var(--cfi-color-gray-600, #4b5563);\n font-size: var(--cfi-font-size-xs, 0.75rem);\n line-height: 1.4;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n.cfi-sh-notif-item-time {\n color: var(--cfi-color-gray-400, #9ca3af);\n font-size: 11px;\n margin-top: 4px;\n}\n");
|
|
28
28
|
|
|
29
29
|
// src/appCatalog.ts
|
|
30
30
|
var appCatalog = [
|
|
@@ -142,21 +142,117 @@ function AppSwitcher({ currentAppId, authorizedApps }) {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
// src/NotificationBell.tsx
|
|
145
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
145
146
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
146
|
-
function NotificationBell({
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
function NotificationBell({
|
|
148
|
+
count = 0,
|
|
149
|
+
onClick,
|
|
150
|
+
items,
|
|
151
|
+
onMarkRead,
|
|
152
|
+
onMarkAllRead,
|
|
153
|
+
onItemClick
|
|
154
|
+
}) {
|
|
155
|
+
const hasDropdown = items !== void 0;
|
|
156
|
+
const [open, setOpen] = useState2(false);
|
|
157
|
+
const ref = useRef2(null);
|
|
158
|
+
useEffect2(() => {
|
|
159
|
+
function handleClickOutside(e) {
|
|
160
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
161
|
+
setOpen(false);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (open) document.addEventListener("mousedown", handleClickOutside);
|
|
165
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
166
|
+
}, [open]);
|
|
167
|
+
useEffect2(() => {
|
|
168
|
+
function handleEsc(e) {
|
|
169
|
+
if (e.key === "Escape") setOpen(false);
|
|
170
|
+
}
|
|
171
|
+
if (open) document.addEventListener("keydown", handleEsc);
|
|
172
|
+
return () => document.removeEventListener("keydown", handleEsc);
|
|
173
|
+
}, [open]);
|
|
174
|
+
const handleBellClick = () => {
|
|
175
|
+
if (hasDropdown) {
|
|
176
|
+
setOpen((prev) => !prev);
|
|
177
|
+
} else if (onClick) {
|
|
178
|
+
onClick();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const handleItemClick = (item) => {
|
|
182
|
+
if (item.readAt === null && onMarkRead) onMarkRead(item.id);
|
|
183
|
+
if (onItemClick) onItemClick(item);
|
|
184
|
+
setOpen(false);
|
|
185
|
+
};
|
|
186
|
+
const hasUnread = items?.some((i) => i.readAt === null) ?? false;
|
|
187
|
+
return /* @__PURE__ */ jsxs3("div", { className: "cfi-sh-notif", ref, children: [
|
|
188
|
+
/* @__PURE__ */ jsxs3(
|
|
189
|
+
"button",
|
|
190
|
+
{
|
|
191
|
+
className: "cfi-sh-icon-btn",
|
|
192
|
+
onClick: handleBellClick,
|
|
193
|
+
"aria-expanded": hasDropdown ? open : void 0,
|
|
194
|
+
"aria-haspopup": hasDropdown ? "true" : void 0,
|
|
195
|
+
"aria-label": "Notifications",
|
|
196
|
+
children: [
|
|
197
|
+
/* @__PURE__ */ jsx3(Bell, { size: 18 }),
|
|
198
|
+
count > 0 && /* @__PURE__ */ jsx3("span", { className: "cfi-sh-badge", children: count > 99 ? "99+" : count })
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
),
|
|
202
|
+
hasDropdown && open && /* @__PURE__ */ jsxs3("div", { className: "cfi-sh-notif-dropdown", role: "dialog", "aria-label": "Notifications", children: [
|
|
203
|
+
/* @__PURE__ */ jsxs3("div", { className: "cfi-sh-notif-header", children: [
|
|
204
|
+
/* @__PURE__ */ jsx3("span", { className: "cfi-sh-notif-title", children: "Notifications" }),
|
|
205
|
+
hasUnread && onMarkAllRead && /* @__PURE__ */ jsx3(
|
|
206
|
+
"button",
|
|
207
|
+
{
|
|
208
|
+
className: "cfi-sh-notif-mark-all",
|
|
209
|
+
onClick: () => onMarkAllRead(),
|
|
210
|
+
type: "button",
|
|
211
|
+
children: "Mark all read"
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
] }),
|
|
215
|
+
/* @__PURE__ */ jsx3("div", { className: "cfi-sh-notif-list", children: items.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "cfi-sh-notif-empty", children: "No notifications yet." }) : items.map((n) => /* @__PURE__ */ jsxs3(
|
|
216
|
+
"button",
|
|
217
|
+
{
|
|
218
|
+
className: `cfi-sh-notif-item ${n.readAt === null ? "cfi-sh-notif-item-unread" : ""}`,
|
|
219
|
+
onClick: () => handleItemClick(n),
|
|
220
|
+
type: "button",
|
|
221
|
+
children: [
|
|
222
|
+
n.readAt === null ? /* @__PURE__ */ jsx3("span", { className: "cfi-sh-notif-dot", "aria-hidden": "true" }) : /* @__PURE__ */ jsx3("span", { className: "cfi-sh-notif-dot-placeholder", "aria-hidden": "true" }),
|
|
223
|
+
/* @__PURE__ */ jsxs3("div", { className: "cfi-sh-notif-content", children: [
|
|
224
|
+
/* @__PURE__ */ jsx3("div", { className: "cfi-sh-notif-item-title", children: n.title }),
|
|
225
|
+
/* @__PURE__ */ jsx3("div", { className: "cfi-sh-notif-item-body", children: n.body }),
|
|
226
|
+
/* @__PURE__ */ jsx3("div", { className: "cfi-sh-notif-item-time", children: formatRelative(n.createdAt) })
|
|
227
|
+
] })
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
n.id
|
|
231
|
+
)) })
|
|
232
|
+
] })
|
|
150
233
|
] });
|
|
151
234
|
}
|
|
235
|
+
function formatRelative(iso) {
|
|
236
|
+
const t = Date.parse(iso);
|
|
237
|
+
if (Number.isNaN(t)) return "";
|
|
238
|
+
const diff = Math.max(0, Date.now() - t);
|
|
239
|
+
const min = Math.floor(diff / 6e4);
|
|
240
|
+
if (min < 1) return "just now";
|
|
241
|
+
if (min < 60) return `${min}m`;
|
|
242
|
+
const hr = Math.floor(min / 60);
|
|
243
|
+
if (hr < 24) return `${hr}h`;
|
|
244
|
+
const days = Math.floor(hr / 24);
|
|
245
|
+
if (days < 7) return `${days}d`;
|
|
246
|
+
return new Date(t).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
247
|
+
}
|
|
152
248
|
|
|
153
249
|
// src/UserMenu.tsx
|
|
154
|
-
import { useState as
|
|
250
|
+
import { useState as useState3, useRef as useRef3, useEffect as useEffect3 } from "react";
|
|
155
251
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
156
252
|
function UserMenu({ user, onLogout, color }) {
|
|
157
|
-
const [open, setOpen] =
|
|
158
|
-
const ref =
|
|
159
|
-
|
|
253
|
+
const [open, setOpen] = useState3(false);
|
|
254
|
+
const ref = useRef3(null);
|
|
255
|
+
useEffect3(() => {
|
|
160
256
|
function handleClickOutside(e) {
|
|
161
257
|
if (ref.current && !ref.current.contains(e.target)) {
|
|
162
258
|
setOpen(false);
|
|
@@ -210,6 +306,10 @@ function ShellHeader({
|
|
|
210
306
|
authorizedApps,
|
|
211
307
|
notificationCount,
|
|
212
308
|
onNotificationClick,
|
|
309
|
+
notifications,
|
|
310
|
+
onMarkRead,
|
|
311
|
+
onMarkAllRead,
|
|
312
|
+
onNotificationItemClick,
|
|
213
313
|
onLogout,
|
|
214
314
|
brandWidth,
|
|
215
315
|
children
|
|
@@ -225,7 +325,17 @@ function ShellHeader({
|
|
|
225
325
|
children
|
|
226
326
|
] }),
|
|
227
327
|
/* @__PURE__ */ jsxs5("div", { className: "cfi-sh-right", children: [
|
|
228
|
-
/* @__PURE__ */ jsx5(
|
|
328
|
+
/* @__PURE__ */ jsx5(
|
|
329
|
+
NotificationBell,
|
|
330
|
+
{
|
|
331
|
+
count: notificationCount,
|
|
332
|
+
onClick: onNotificationClick,
|
|
333
|
+
items: notifications,
|
|
334
|
+
onMarkRead,
|
|
335
|
+
onMarkAllRead,
|
|
336
|
+
onItemClick: onNotificationItemClick
|
|
337
|
+
}
|
|
338
|
+
),
|
|
229
339
|
/* @__PURE__ */ jsx5(AppSwitcher, { currentAppId: appId, authorizedApps }),
|
|
230
340
|
/* @__PURE__ */ jsx5(UserMenu, { user, onLogout, color: currentApp?.color })
|
|
231
341
|
] })
|