@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 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 notification count */
40
+ /** Unread count for the bell badge. */
27
41
  notificationCount?: number;
28
- /** Called when the notification bell is clicked */
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({ count = 0, onClick }) {
147
- return /* @__PURE__ */ jsxs3("button", { className: "cfi-sh-icon-btn", onClick, "aria-label": "Notifications", children: [
148
- /* @__PURE__ */ jsx3(Bell, { size: 18 }),
149
- count > 0 && /* @__PURE__ */ jsx3("span", { className: "cfi-sh-badge", children: count > 99 ? "99+" : count })
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 useState2, useRef as useRef2, useEffect as useEffect2 } from "react";
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] = useState2(false);
158
- const ref = useRef2(null);
159
- useEffect2(() => {
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(NotificationBell, { count: notificationCount, onClick: onNotificationClick }),
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
  ] })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@campfire-interactive/shell-header",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "Shared shell header with app switcher for Campfire Suite",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",