@campfire-interactive/shell-header 0.1.5 → 0.3.3
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 +64 -4
- package/dist/index.js +223 -34
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,26 @@ interface ShellUser {
|
|
|
16
16
|
email: string;
|
|
17
17
|
avatarUrl?: string;
|
|
18
18
|
}
|
|
19
|
+
interface LocaleOption {
|
|
20
|
+
/** BCP-47-ish language code, e.g. "en", "de". */
|
|
21
|
+
code: string;
|
|
22
|
+
/** Human-readable label shown in the dropdown, e.g. "English", "Deutsch". */
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Subset of the platform-notifications `Notification` shape that the bell UI
|
|
27
|
+
* actually renders. Hosts typically map their fetched items (e.g., from
|
|
28
|
+
* `useNotifications` in @campfire-interactive/notifications-client/react)
|
|
29
|
+
* into this shape.
|
|
30
|
+
*/
|
|
31
|
+
interface NotificationItem {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
body: string;
|
|
35
|
+
link: string | null;
|
|
36
|
+
readAt: string | null;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
}
|
|
19
39
|
interface ShellHeaderProps {
|
|
20
40
|
/** Current app ID — used to highlight the active app in the switcher */
|
|
21
41
|
appId: string;
|
|
@@ -23,19 +43,59 @@ interface ShellHeaderProps {
|
|
|
23
43
|
user: ShellUser;
|
|
24
44
|
/** App IDs the user is authorized to access in the current tenant */
|
|
25
45
|
authorizedApps: string[];
|
|
26
|
-
/** Unread
|
|
46
|
+
/** Unread count for the bell badge. */
|
|
27
47
|
notificationCount?: number;
|
|
28
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Legacy: fires when the bell icon is clicked. Only used when `notifications`
|
|
50
|
+
* is NOT provided (icon-only mode). When `notifications` is an array, the
|
|
51
|
+
* bell opens a dropdown instead.
|
|
52
|
+
*/
|
|
29
53
|
onNotificationClick?: () => void;
|
|
54
|
+
/**
|
|
55
|
+
* If provided, the bell renders a popover dropdown with these items. Pass
|
|
56
|
+
* an empty array for the "no notifications yet" empty state.
|
|
57
|
+
*/
|
|
58
|
+
notifications?: NotificationItem[];
|
|
59
|
+
/** Optimistically mark a single notification read. */
|
|
60
|
+
onMarkRead?: (id: string) => void;
|
|
61
|
+
/** Mark every unread notification in scope read. */
|
|
62
|
+
onMarkAllRead?: () => void;
|
|
63
|
+
/**
|
|
64
|
+
* Fires when a notification row in the dropdown is clicked (after the
|
|
65
|
+
* built-in auto-mark-read). Hosts handle navigation here using `item.link`.
|
|
66
|
+
*/
|
|
67
|
+
onNotificationItemClick?: (item: NotificationItem) => void;
|
|
30
68
|
/** Called when logout is clicked */
|
|
31
69
|
onLogout?: () => void;
|
|
70
|
+
/**
|
|
71
|
+
* Current user locale (e.g. "en", "de"). When provided alongside
|
|
72
|
+
* `supportedLocales` and `onLocaleChange`, a locale dropdown renders
|
|
73
|
+
* in the right-side cluster. Omitting any of the three hides it.
|
|
74
|
+
*/
|
|
75
|
+
locale?: string;
|
|
76
|
+
/** Locales the user can pick from. */
|
|
77
|
+
supportedLocales?: ReadonlyArray<LocaleOption>;
|
|
78
|
+
/**
|
|
79
|
+
* Called when the user picks a different locale. Consumer is responsible
|
|
80
|
+
* for persisting (e.g. via identity-client.me.updatePreferences) and for
|
|
81
|
+
* refreshing the JWT / reloading the page so other parts of the UI
|
|
82
|
+
* pick up the change.
|
|
83
|
+
*/
|
|
84
|
+
onLocaleChange?: (locale: string) => void | Promise<void>;
|
|
32
85
|
/** Fixed width for the app brand area (e.g., to align with a sidebar below). */
|
|
33
86
|
brandWidth?: number;
|
|
34
87
|
/** Optional content in the left/center area (filters, search, breadcrumbs, etc.) */
|
|
35
88
|
children?: React.ReactNode;
|
|
36
89
|
}
|
|
37
90
|
|
|
38
|
-
declare function ShellHeader({ appId, user, authorizedApps, notificationCount, onNotificationClick, onLogout, brandWidth, children, }: ShellHeaderProps): react_jsx_runtime.JSX.Element;
|
|
91
|
+
declare function ShellHeader({ appId, user, authorizedApps, notificationCount, onNotificationClick, notifications, onMarkRead, onMarkAllRead, onNotificationItemClick, onLogout, locale, supportedLocales, onLocaleChange, brandWidth, children, }: ShellHeaderProps): react_jsx_runtime.JSX.Element;
|
|
92
|
+
|
|
93
|
+
interface LocaleSwitcherProps {
|
|
94
|
+
currentLocale: string;
|
|
95
|
+
supportedLocales: ReadonlyArray<LocaleOption>;
|
|
96
|
+
onLocaleChange: (locale: string) => void | Promise<void>;
|
|
97
|
+
}
|
|
98
|
+
declare function LocaleSwitcher({ currentLocale, supportedLocales, onLocaleChange }: LocaleSwitcherProps): react_jsx_runtime.JSX.Element;
|
|
39
99
|
|
|
40
100
|
declare const appCatalog: AppDefinition[];
|
|
41
101
|
/**
|
|
@@ -46,4 +106,4 @@ declare const appCatalog: AppDefinition[];
|
|
|
46
106
|
*/
|
|
47
107
|
declare function getAppUrl(app: AppDefinition): string;
|
|
48
108
|
|
|
49
|
-
export { type AppDefinition, ShellHeader, type ShellHeaderProps, type ShellUser, appCatalog, getAppUrl };
|
|
109
|
+
export { type AppDefinition, type LocaleOption, LocaleSwitcher, 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-menu-item:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.cfi-sh-locale-switcher {\n position: relative;\n}\n.cfi-sh-locale-btn {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-xs, 0.25rem);\n height: 32px;\n padding: 0 var(--cfi-spacing-sm, 0.5rem);\n background: transparent;\n border: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n border-radius: var(--cfi-radius-md, 0.375rem);\n color: var(--cfi-color-gray-700, #374151);\n cursor: pointer;\n font-size: var(--cfi-font-size-xs, 0.75rem);\n font-weight: 600;\n font-family: inherit;\n transition: background 100ms;\n}\n.cfi-sh-locale-btn:hover:not(:disabled) {\n background: var(--cfi-color-gray-100, #f3f4f6);\n}\n.cfi-sh-locale-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.cfi-sh-locale-label {\n letter-spacing: 0.02em;\n}\n.cfi-sh-locale-dropdown {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n min-width: 180px;\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 padding: var(--cfi-spacing-xs, 0.25rem) 0;\n}\n.cfi-sh-locale-dropdown .cfi-sh-menu-item {\n justify-content: space-between;\n}\n.cfi-sh-locale-item-active {\n background: var(--cfi-color-gray-100, #f3f4f6);\n}\n.cfi-sh-locale-item-label {\n flex: 1;\n text-align: left;\n}\n.cfi-sh-locale-item-code {\n color: var(--cfi-color-gray-400, #9ca3af);\n font-size: var(--cfi-font-size-xs, 0.75rem);\n font-weight: 600;\n letter-spacing: 0.02em;\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 = [
|
|
@@ -34,7 +34,8 @@ var appCatalog = [
|
|
|
34
34
|
{ id: "pim", name: "Price Index", icon: "BarChart3", letter: "P", color: "#f97316", localPort: 3300 },
|
|
35
35
|
{ id: "cpq", name: "CPQ", icon: "Calculator", letter: "C", color: "#4f46e5", localPort: 3400 },
|
|
36
36
|
{ id: "omsf", name: "OMSF", icon: "FileText", letter: "O", color: "#0ea5e9", localPort: 3500 },
|
|
37
|
-
{ id: "identity", name: "Identity", icon: "Shield", letter: "I", color: "#8b5cf6", localPort: 3600 }
|
|
37
|
+
{ id: "identity", name: "Identity", icon: "Shield", letter: "I", color: "#8b5cf6", localPort: 3600 },
|
|
38
|
+
{ id: "bom", name: "BOM", icon: "Network", letter: "B", color: "#0d9488", localPort: 3700 }
|
|
38
39
|
];
|
|
39
40
|
function getAppUrl(app) {
|
|
40
41
|
const host = typeof window !== "undefined" ? window.location.hostname : "";
|
|
@@ -62,7 +63,8 @@ import {
|
|
|
62
63
|
Shield,
|
|
63
64
|
GripHorizontal,
|
|
64
65
|
Bell,
|
|
65
|
-
LogOut
|
|
66
|
+
LogOut,
|
|
67
|
+
Globe
|
|
66
68
|
} from "lucide-react";
|
|
67
69
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
68
70
|
var iconMap = {
|
|
@@ -141,22 +143,182 @@ function AppSwitcher({ currentAppId, authorizedApps }) {
|
|
|
141
143
|
] });
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
// src/
|
|
146
|
+
// src/LocaleSwitcher.tsx
|
|
147
|
+
import { useState as useState2, useRef as useRef2, useEffect as useEffect2 } from "react";
|
|
145
148
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
function LocaleSwitcher({ currentLocale, supportedLocales, onLocaleChange }) {
|
|
150
|
+
const [open, setOpen] = useState2(false);
|
|
151
|
+
const [updating, setUpdating] = useState2(false);
|
|
152
|
+
const ref = useRef2(null);
|
|
153
|
+
useEffect2(() => {
|
|
154
|
+
function handleClickOutside(e) {
|
|
155
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
156
|
+
setOpen(false);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (open) document.addEventListener("mousedown", handleClickOutside);
|
|
160
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
161
|
+
}, [open]);
|
|
162
|
+
const current = supportedLocales.find((l) => l.code === currentLocale);
|
|
163
|
+
const buttonLabel = (current?.code ?? currentLocale).toUpperCase();
|
|
164
|
+
async function handleSelect(code) {
|
|
165
|
+
if (code === currentLocale) {
|
|
166
|
+
setOpen(false);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setUpdating(true);
|
|
170
|
+
try {
|
|
171
|
+
await onLocaleChange(code);
|
|
172
|
+
} finally {
|
|
173
|
+
setUpdating(false);
|
|
174
|
+
setOpen(false);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return /* @__PURE__ */ jsxs3("div", { className: "cfi-sh-locale-switcher", ref, children: [
|
|
178
|
+
/* @__PURE__ */ jsxs3(
|
|
179
|
+
"button",
|
|
180
|
+
{
|
|
181
|
+
className: "cfi-sh-locale-btn",
|
|
182
|
+
onClick: () => setOpen(!open),
|
|
183
|
+
"aria-expanded": open,
|
|
184
|
+
"aria-haspopup": "true",
|
|
185
|
+
"aria-label": `Language: ${current?.label ?? currentLocale}`,
|
|
186
|
+
disabled: updating,
|
|
187
|
+
title: current?.label ?? currentLocale,
|
|
188
|
+
children: [
|
|
189
|
+
/* @__PURE__ */ jsx3(Globe, { size: 16 }),
|
|
190
|
+
/* @__PURE__ */ jsx3("span", { className: "cfi-sh-locale-label", children: buttonLabel })
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
open && /* @__PURE__ */ jsx3("div", { className: "cfi-sh-locale-dropdown", children: supportedLocales.map((loc) => /* @__PURE__ */ jsxs3(
|
|
195
|
+
"button",
|
|
196
|
+
{
|
|
197
|
+
className: loc.code === currentLocale ? "cfi-sh-menu-item cfi-sh-locale-item-active" : "cfi-sh-menu-item",
|
|
198
|
+
onClick: () => handleSelect(loc.code),
|
|
199
|
+
disabled: updating,
|
|
200
|
+
children: [
|
|
201
|
+
/* @__PURE__ */ jsx3("span", { className: "cfi-sh-locale-item-label", children: loc.label }),
|
|
202
|
+
/* @__PURE__ */ jsx3("span", { className: "cfi-sh-locale-item-code", children: loc.code.toUpperCase() })
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
loc.code
|
|
206
|
+
)) })
|
|
150
207
|
] });
|
|
151
208
|
}
|
|
152
209
|
|
|
153
|
-
// src/
|
|
154
|
-
import {
|
|
210
|
+
// src/NotificationBell.tsx
|
|
211
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
155
212
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
213
|
+
function NotificationBell({
|
|
214
|
+
count = 0,
|
|
215
|
+
onClick,
|
|
216
|
+
items,
|
|
217
|
+
onMarkRead,
|
|
218
|
+
onMarkAllRead,
|
|
219
|
+
onItemClick
|
|
220
|
+
}) {
|
|
221
|
+
const hasDropdown = items !== void 0;
|
|
222
|
+
const [open, setOpen] = useState3(false);
|
|
223
|
+
const ref = useRef3(null);
|
|
224
|
+
useEffect3(() => {
|
|
225
|
+
function handleClickOutside(e) {
|
|
226
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
227
|
+
setOpen(false);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (open) document.addEventListener("mousedown", handleClickOutside);
|
|
231
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
232
|
+
}, [open]);
|
|
233
|
+
useEffect3(() => {
|
|
234
|
+
function handleEsc(e) {
|
|
235
|
+
if (e.key === "Escape") setOpen(false);
|
|
236
|
+
}
|
|
237
|
+
if (open) document.addEventListener("keydown", handleEsc);
|
|
238
|
+
return () => document.removeEventListener("keydown", handleEsc);
|
|
239
|
+
}, [open]);
|
|
240
|
+
const handleBellClick = () => {
|
|
241
|
+
if (hasDropdown) {
|
|
242
|
+
setOpen((prev) => !prev);
|
|
243
|
+
} else if (onClick) {
|
|
244
|
+
onClick();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const handleItemClick = (item) => {
|
|
248
|
+
if (item.readAt === null && onMarkRead) onMarkRead(item.id);
|
|
249
|
+
if (onItemClick) onItemClick(item);
|
|
250
|
+
setOpen(false);
|
|
251
|
+
};
|
|
252
|
+
const hasUnread = items?.some((i) => i.readAt === null) ?? false;
|
|
253
|
+
return /* @__PURE__ */ jsxs4("div", { className: "cfi-sh-notif", ref, children: [
|
|
254
|
+
/* @__PURE__ */ jsxs4(
|
|
255
|
+
"button",
|
|
256
|
+
{
|
|
257
|
+
className: "cfi-sh-icon-btn",
|
|
258
|
+
onClick: handleBellClick,
|
|
259
|
+
"aria-expanded": hasDropdown ? open : void 0,
|
|
260
|
+
"aria-haspopup": hasDropdown ? "true" : void 0,
|
|
261
|
+
"aria-label": "Notifications",
|
|
262
|
+
children: [
|
|
263
|
+
/* @__PURE__ */ jsx4(Bell, { size: 18 }),
|
|
264
|
+
count > 0 && /* @__PURE__ */ jsx4("span", { className: "cfi-sh-badge", children: count > 99 ? "99+" : count })
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
),
|
|
268
|
+
hasDropdown && open && /* @__PURE__ */ jsxs4("div", { className: "cfi-sh-notif-dropdown", role: "dialog", "aria-label": "Notifications", children: [
|
|
269
|
+
/* @__PURE__ */ jsxs4("div", { className: "cfi-sh-notif-header", children: [
|
|
270
|
+
/* @__PURE__ */ jsx4("span", { className: "cfi-sh-notif-title", children: "Notifications" }),
|
|
271
|
+
hasUnread && onMarkAllRead && /* @__PURE__ */ jsx4(
|
|
272
|
+
"button",
|
|
273
|
+
{
|
|
274
|
+
className: "cfi-sh-notif-mark-all",
|
|
275
|
+
onClick: () => onMarkAllRead(),
|
|
276
|
+
type: "button",
|
|
277
|
+
children: "Mark all read"
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
] }),
|
|
281
|
+
/* @__PURE__ */ jsx4("div", { className: "cfi-sh-notif-list", children: items.length === 0 ? /* @__PURE__ */ jsx4("div", { className: "cfi-sh-notif-empty", children: "No notifications yet." }) : items.map((n) => /* @__PURE__ */ jsxs4(
|
|
282
|
+
"button",
|
|
283
|
+
{
|
|
284
|
+
className: `cfi-sh-notif-item ${n.readAt === null ? "cfi-sh-notif-item-unread" : ""}`,
|
|
285
|
+
onClick: () => handleItemClick(n),
|
|
286
|
+
type: "button",
|
|
287
|
+
children: [
|
|
288
|
+
n.readAt === null ? /* @__PURE__ */ jsx4("span", { className: "cfi-sh-notif-dot", "aria-hidden": "true" }) : /* @__PURE__ */ jsx4("span", { className: "cfi-sh-notif-dot-placeholder", "aria-hidden": "true" }),
|
|
289
|
+
/* @__PURE__ */ jsxs4("div", { className: "cfi-sh-notif-content", children: [
|
|
290
|
+
/* @__PURE__ */ jsx4("div", { className: "cfi-sh-notif-item-title", children: n.title }),
|
|
291
|
+
/* @__PURE__ */ jsx4("div", { className: "cfi-sh-notif-item-body", children: n.body }),
|
|
292
|
+
/* @__PURE__ */ jsx4("div", { className: "cfi-sh-notif-item-time", children: formatRelative(n.createdAt) })
|
|
293
|
+
] })
|
|
294
|
+
]
|
|
295
|
+
},
|
|
296
|
+
n.id
|
|
297
|
+
)) })
|
|
298
|
+
] })
|
|
299
|
+
] });
|
|
300
|
+
}
|
|
301
|
+
function formatRelative(iso) {
|
|
302
|
+
const t = Date.parse(iso);
|
|
303
|
+
if (Number.isNaN(t)) return "";
|
|
304
|
+
const diff = Math.max(0, Date.now() - t);
|
|
305
|
+
const min = Math.floor(diff / 6e4);
|
|
306
|
+
if (min < 1) return "just now";
|
|
307
|
+
if (min < 60) return `${min}m`;
|
|
308
|
+
const hr = Math.floor(min / 60);
|
|
309
|
+
if (hr < 24) return `${hr}h`;
|
|
310
|
+
const days = Math.floor(hr / 24);
|
|
311
|
+
if (days < 7) return `${days}d`;
|
|
312
|
+
return new Date(t).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/UserMenu.tsx
|
|
316
|
+
import { useState as useState4, useRef as useRef4, useEffect as useEffect4 } from "react";
|
|
317
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
156
318
|
function UserMenu({ user, onLogout, color }) {
|
|
157
|
-
const [open, setOpen] =
|
|
158
|
-
const ref =
|
|
159
|
-
|
|
319
|
+
const [open, setOpen] = useState4(false);
|
|
320
|
+
const ref = useRef4(null);
|
|
321
|
+
useEffect4(() => {
|
|
160
322
|
function handleClickOutside(e) {
|
|
161
323
|
if (ref.current && !ref.current.contains(e.target)) {
|
|
162
324
|
setOpen(false);
|
|
@@ -166,8 +328,8 @@ function UserMenu({ user, onLogout, color }) {
|
|
|
166
328
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
167
329
|
}, [open]);
|
|
168
330
|
const initials = user.name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2);
|
|
169
|
-
return /* @__PURE__ */
|
|
170
|
-
/* @__PURE__ */
|
|
331
|
+
return /* @__PURE__ */ jsxs5("div", { className: "cfi-sh-user-menu", ref, children: [
|
|
332
|
+
/* @__PURE__ */ jsx5(
|
|
171
333
|
"button",
|
|
172
334
|
{
|
|
173
335
|
className: "cfi-sh-avatar-btn",
|
|
@@ -175,16 +337,16 @@ function UserMenu({ user, onLogout, color }) {
|
|
|
175
337
|
onClick: () => setOpen(!open),
|
|
176
338
|
"aria-expanded": open,
|
|
177
339
|
"aria-haspopup": "true",
|
|
178
|
-
children: user.avatarUrl ? /* @__PURE__ */
|
|
340
|
+
children: user.avatarUrl ? /* @__PURE__ */ jsx5("img", { src: user.avatarUrl, alt: user.name, className: "cfi-sh-avatar-img" }) : /* @__PURE__ */ jsx5("span", { className: "cfi-sh-avatar-initials", children: initials })
|
|
179
341
|
}
|
|
180
342
|
),
|
|
181
|
-
open && /* @__PURE__ */
|
|
182
|
-
/* @__PURE__ */
|
|
183
|
-
/* @__PURE__ */
|
|
184
|
-
/* @__PURE__ */
|
|
343
|
+
open && /* @__PURE__ */ jsxs5("div", { className: "cfi-sh-user-dropdown", children: [
|
|
344
|
+
/* @__PURE__ */ jsxs5("div", { className: "cfi-sh-user-header", children: [
|
|
345
|
+
/* @__PURE__ */ jsx5("span", { className: "cfi-sh-user-name", children: user.name }),
|
|
346
|
+
/* @__PURE__ */ jsx5("span", { className: "cfi-sh-user-email", children: user.email })
|
|
185
347
|
] }),
|
|
186
|
-
/* @__PURE__ */
|
|
187
|
-
/* @__PURE__ */
|
|
348
|
+
/* @__PURE__ */ jsx5("div", { className: "cfi-sh-divider" }),
|
|
349
|
+
/* @__PURE__ */ jsxs5(
|
|
188
350
|
"button",
|
|
189
351
|
{
|
|
190
352
|
className: "cfi-sh-menu-item",
|
|
@@ -193,8 +355,8 @@ function UserMenu({ user, onLogout, color }) {
|
|
|
193
355
|
onLogout?.();
|
|
194
356
|
},
|
|
195
357
|
children: [
|
|
196
|
-
/* @__PURE__ */
|
|
197
|
-
/* @__PURE__ */
|
|
358
|
+
/* @__PURE__ */ jsx5(LogOut, { size: 14 }),
|
|
359
|
+
/* @__PURE__ */ jsx5("span", { children: "Sign out" })
|
|
198
360
|
]
|
|
199
361
|
}
|
|
200
362
|
)
|
|
@@ -203,35 +365,62 @@ function UserMenu({ user, onLogout, color }) {
|
|
|
203
365
|
}
|
|
204
366
|
|
|
205
367
|
// src/ShellHeader.tsx
|
|
206
|
-
import { jsx as
|
|
368
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
207
369
|
function ShellHeader({
|
|
208
370
|
appId,
|
|
209
371
|
user,
|
|
210
372
|
authorizedApps,
|
|
211
373
|
notificationCount,
|
|
212
374
|
onNotificationClick,
|
|
375
|
+
notifications,
|
|
376
|
+
onMarkRead,
|
|
377
|
+
onMarkAllRead,
|
|
378
|
+
onNotificationItemClick,
|
|
213
379
|
onLogout,
|
|
380
|
+
locale,
|
|
381
|
+
supportedLocales,
|
|
382
|
+
onLocaleChange,
|
|
214
383
|
brandWidth,
|
|
215
384
|
children
|
|
216
385
|
}) {
|
|
217
386
|
const currentApp = appCatalog.find((a) => a.id === appId);
|
|
218
387
|
const brandStyle = brandWidth ? { width: `${brandWidth}px`, flexShrink: 0, boxSizing: "border-box" } : void 0;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
/* @__PURE__ */
|
|
388
|
+
const showLocaleSwitcher = locale !== void 0 && supportedLocales !== void 0 && supportedLocales.length > 0 && onLocaleChange !== void 0;
|
|
389
|
+
return /* @__PURE__ */ jsxs6("header", { className: "cfi-sh-header", children: [
|
|
390
|
+
/* @__PURE__ */ jsxs6("div", { className: "cfi-sh-left", children: [
|
|
391
|
+
/* @__PURE__ */ jsxs6("div", { className: "cfi-sh-app-brand", style: brandStyle, children: [
|
|
392
|
+
currentApp && /* @__PURE__ */ jsx6("span", { className: "cfi-sh-app-badge", style: { background: currentApp.color }, children: currentApp.letter }),
|
|
393
|
+
/* @__PURE__ */ jsx6("span", { className: "cfi-sh-app-title", children: currentApp?.name || appId })
|
|
224
394
|
] }),
|
|
225
395
|
children
|
|
226
396
|
] }),
|
|
227
|
-
/* @__PURE__ */
|
|
228
|
-
/* @__PURE__ */
|
|
229
|
-
|
|
230
|
-
|
|
397
|
+
/* @__PURE__ */ jsxs6("div", { className: "cfi-sh-right", children: [
|
|
398
|
+
showLocaleSwitcher && /* @__PURE__ */ jsx6(
|
|
399
|
+
LocaleSwitcher,
|
|
400
|
+
{
|
|
401
|
+
currentLocale: locale,
|
|
402
|
+
supportedLocales,
|
|
403
|
+
onLocaleChange
|
|
404
|
+
}
|
|
405
|
+
),
|
|
406
|
+
/* @__PURE__ */ jsx6(
|
|
407
|
+
NotificationBell,
|
|
408
|
+
{
|
|
409
|
+
count: notificationCount,
|
|
410
|
+
onClick: onNotificationClick,
|
|
411
|
+
items: notifications,
|
|
412
|
+
onMarkRead,
|
|
413
|
+
onMarkAllRead,
|
|
414
|
+
onItemClick: onNotificationItemClick
|
|
415
|
+
}
|
|
416
|
+
),
|
|
417
|
+
/* @__PURE__ */ jsx6(AppSwitcher, { currentAppId: appId, authorizedApps }),
|
|
418
|
+
/* @__PURE__ */ jsx6(UserMenu, { user, onLogout, color: currentApp?.color })
|
|
231
419
|
] })
|
|
232
420
|
] });
|
|
233
421
|
}
|
|
234
422
|
export {
|
|
423
|
+
LocaleSwitcher,
|
|
235
424
|
ShellHeader,
|
|
236
425
|
appCatalog,
|
|
237
426
|
getAppUrl
|