@campfire-interactive/shell-header 0.2.1 → 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 CHANGED
@@ -16,6 +16,12 @@ 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
+ }
19
25
  /**
20
26
  * Subset of the platform-notifications `Notification` shape that the bell UI
21
27
  * actually renders. Hosts typically map their fetched items (e.g., from
@@ -61,13 +67,35 @@ interface ShellHeaderProps {
61
67
  onNotificationItemClick?: (item: NotificationItem) => void;
62
68
  /** Called when logout is clicked */
63
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>;
64
85
  /** Fixed width for the app brand area (e.g., to align with a sidebar below). */
65
86
  brandWidth?: number;
66
87
  /** Optional content in the left/center area (filters, search, breadcrumbs, etc.) */
67
88
  children?: React.ReactNode;
68
89
  }
69
90
 
70
- declare function ShellHeader({ appId, user, authorizedApps, notificationCount, onNotificationClick, notifications, onMarkRead, onMarkAllRead, onNotificationItemClick, 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;
71
99
 
72
100
  declare const appCatalog: AppDefinition[];
73
101
  /**
@@ -78,4 +106,4 @@ declare const appCatalog: AppDefinition[];
78
106
  */
79
107
  declare function getAppUrl(app: AppDefinition): string;
80
108
 
81
- export { type AppDefinition, type NotificationItem, 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.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");
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,9 +143,73 @@ function AppSwitcher({ currentAppId, authorizedApps }) {
141
143
  ] });
142
144
  }
143
145
 
144
- // src/NotificationBell.tsx
145
- import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
146
+ // src/LocaleSwitcher.tsx
147
+ import { useState as useState2, useRef as useRef2, useEffect as useEffect2 } from "react";
146
148
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
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
+ )) })
207
+ ] });
208
+ }
209
+
210
+ // src/NotificationBell.tsx
211
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
212
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
147
213
  function NotificationBell({
148
214
  count = 0,
149
215
  onClick,
@@ -153,9 +219,9 @@ function NotificationBell({
153
219
  onItemClick
154
220
  }) {
155
221
  const hasDropdown = items !== void 0;
156
- const [open, setOpen] = useState2(false);
157
- const ref = useRef2(null);
158
- useEffect2(() => {
222
+ const [open, setOpen] = useState3(false);
223
+ const ref = useRef3(null);
224
+ useEffect3(() => {
159
225
  function handleClickOutside(e) {
160
226
  if (ref.current && !ref.current.contains(e.target)) {
161
227
  setOpen(false);
@@ -164,7 +230,7 @@ function NotificationBell({
164
230
  if (open) document.addEventListener("mousedown", handleClickOutside);
165
231
  return () => document.removeEventListener("mousedown", handleClickOutside);
166
232
  }, [open]);
167
- useEffect2(() => {
233
+ useEffect3(() => {
168
234
  function handleEsc(e) {
169
235
  if (e.key === "Escape") setOpen(false);
170
236
  }
@@ -184,8 +250,8 @@ function NotificationBell({
184
250
  setOpen(false);
185
251
  };
186
252
  const hasUnread = items?.some((i) => i.readAt === null) ?? false;
187
- return /* @__PURE__ */ jsxs3("div", { className: "cfi-sh-notif", ref, children: [
188
- /* @__PURE__ */ jsxs3(
253
+ return /* @__PURE__ */ jsxs4("div", { className: "cfi-sh-notif", ref, children: [
254
+ /* @__PURE__ */ jsxs4(
189
255
  "button",
190
256
  {
191
257
  className: "cfi-sh-icon-btn",
@@ -194,15 +260,15 @@ function NotificationBell({
194
260
  "aria-haspopup": hasDropdown ? "true" : void 0,
195
261
  "aria-label": "Notifications",
196
262
  children: [
197
- /* @__PURE__ */ jsx3(Bell, { size: 18 }),
198
- count > 0 && /* @__PURE__ */ jsx3("span", { className: "cfi-sh-badge", children: count > 99 ? "99+" : count })
263
+ /* @__PURE__ */ jsx4(Bell, { size: 18 }),
264
+ count > 0 && /* @__PURE__ */ jsx4("span", { className: "cfi-sh-badge", children: count > 99 ? "99+" : count })
199
265
  ]
200
266
  }
201
267
  ),
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(
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(
206
272
  "button",
207
273
  {
208
274
  className: "cfi-sh-notif-mark-all",
@@ -212,18 +278,18 @@ function NotificationBell({
212
278
  }
213
279
  )
214
280
  ] }),
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(
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(
216
282
  "button",
217
283
  {
218
284
  className: `cfi-sh-notif-item ${n.readAt === null ? "cfi-sh-notif-item-unread" : ""}`,
219
285
  onClick: () => handleItemClick(n),
220
286
  type: "button",
221
287
  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) })
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) })
227
293
  ] })
228
294
  ]
229
295
  },
@@ -247,12 +313,12 @@ function formatRelative(iso) {
247
313
  }
248
314
 
249
315
  // src/UserMenu.tsx
250
- import { useState as useState3, useRef as useRef3, useEffect as useEffect3 } from "react";
251
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
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";
252
318
  function UserMenu({ user, onLogout, color }) {
253
- const [open, setOpen] = useState3(false);
254
- const ref = useRef3(null);
255
- useEffect3(() => {
319
+ const [open, setOpen] = useState4(false);
320
+ const ref = useRef4(null);
321
+ useEffect4(() => {
256
322
  function handleClickOutside(e) {
257
323
  if (ref.current && !ref.current.contains(e.target)) {
258
324
  setOpen(false);
@@ -262,8 +328,8 @@ function UserMenu({ user, onLogout, color }) {
262
328
  return () => document.removeEventListener("mousedown", handleClickOutside);
263
329
  }, [open]);
264
330
  const initials = user.name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2);
265
- return /* @__PURE__ */ jsxs4("div", { className: "cfi-sh-user-menu", ref, children: [
266
- /* @__PURE__ */ jsx4(
331
+ return /* @__PURE__ */ jsxs5("div", { className: "cfi-sh-user-menu", ref, children: [
332
+ /* @__PURE__ */ jsx5(
267
333
  "button",
268
334
  {
269
335
  className: "cfi-sh-avatar-btn",
@@ -271,16 +337,16 @@ function UserMenu({ user, onLogout, color }) {
271
337
  onClick: () => setOpen(!open),
272
338
  "aria-expanded": open,
273
339
  "aria-haspopup": "true",
274
- children: user.avatarUrl ? /* @__PURE__ */ jsx4("img", { src: user.avatarUrl, alt: user.name, className: "cfi-sh-avatar-img" }) : /* @__PURE__ */ jsx4("span", { className: "cfi-sh-avatar-initials", children: initials })
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 })
275
341
  }
276
342
  ),
277
- open && /* @__PURE__ */ jsxs4("div", { className: "cfi-sh-user-dropdown", children: [
278
- /* @__PURE__ */ jsxs4("div", { className: "cfi-sh-user-header", children: [
279
- /* @__PURE__ */ jsx4("span", { className: "cfi-sh-user-name", children: user.name }),
280
- /* @__PURE__ */ jsx4("span", { className: "cfi-sh-user-email", children: user.email })
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 })
281
347
  ] }),
282
- /* @__PURE__ */ jsx4("div", { className: "cfi-sh-divider" }),
283
- /* @__PURE__ */ jsxs4(
348
+ /* @__PURE__ */ jsx5("div", { className: "cfi-sh-divider" }),
349
+ /* @__PURE__ */ jsxs5(
284
350
  "button",
285
351
  {
286
352
  className: "cfi-sh-menu-item",
@@ -289,8 +355,8 @@ function UserMenu({ user, onLogout, color }) {
289
355
  onLogout?.();
290
356
  },
291
357
  children: [
292
- /* @__PURE__ */ jsx4(LogOut, { size: 14 }),
293
- /* @__PURE__ */ jsx4("span", { children: "Sign out" })
358
+ /* @__PURE__ */ jsx5(LogOut, { size: 14 }),
359
+ /* @__PURE__ */ jsx5("span", { children: "Sign out" })
294
360
  ]
295
361
  }
296
362
  )
@@ -299,7 +365,7 @@ function UserMenu({ user, onLogout, color }) {
299
365
  }
300
366
 
301
367
  // src/ShellHeader.tsx
302
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
368
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
303
369
  function ShellHeader({
304
370
  appId,
305
371
  user,
@@ -311,21 +377,33 @@ function ShellHeader({
311
377
  onMarkAllRead,
312
378
  onNotificationItemClick,
313
379
  onLogout,
380
+ locale,
381
+ supportedLocales,
382
+ onLocaleChange,
314
383
  brandWidth,
315
384
  children
316
385
  }) {
317
386
  const currentApp = appCatalog.find((a) => a.id === appId);
318
387
  const brandStyle = brandWidth ? { width: `${brandWidth}px`, flexShrink: 0, boxSizing: "border-box" } : void 0;
319
- return /* @__PURE__ */ jsxs5("header", { className: "cfi-sh-header", children: [
320
- /* @__PURE__ */ jsxs5("div", { className: "cfi-sh-left", children: [
321
- /* @__PURE__ */ jsxs5("div", { className: "cfi-sh-app-brand", style: brandStyle, children: [
322
- currentApp && /* @__PURE__ */ jsx5("span", { className: "cfi-sh-app-badge", style: { background: currentApp.color }, children: currentApp.letter }),
323
- /* @__PURE__ */ jsx5("span", { className: "cfi-sh-app-title", children: currentApp?.name || appId })
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 })
324
394
  ] }),
325
395
  children
326
396
  ] }),
327
- /* @__PURE__ */ jsxs5("div", { className: "cfi-sh-right", children: [
328
- /* @__PURE__ */ jsx5(
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(
329
407
  NotificationBell,
330
408
  {
331
409
  count: notificationCount,
@@ -336,12 +414,13 @@ function ShellHeader({
336
414
  onItemClick: onNotificationItemClick
337
415
  }
338
416
  ),
339
- /* @__PURE__ */ jsx5(AppSwitcher, { currentAppId: appId, authorizedApps }),
340
- /* @__PURE__ */ jsx5(UserMenu, { user, onLogout, color: currentApp?.color })
417
+ /* @__PURE__ */ jsx6(AppSwitcher, { currentAppId: appId, authorizedApps }),
418
+ /* @__PURE__ */ jsx6(UserMenu, { user, onLogout, color: currentApp?.color })
341
419
  ] })
342
420
  ] });
343
421
  }
344
422
  export {
423
+ LocaleSwitcher,
345
424
  ShellHeader,
346
425
  appCatalog,
347
426
  getAppUrl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@campfire-interactive/shell-header",
3
- "version": "0.2.1",
3
+ "version": "0.3.3",
4
4
  "description": "Shared shell header with app switcher for Campfire Suite",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",