@campfire-interactive/shell-header 0.2.1 → 0.4.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,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-locale-row {\n justify-content: flex-start;\n}\n.cfi-sh-locale-row-label {\n flex: 1;\n text-align: left;\n}\n.cfi-sh-locale-row-current {\n color: var(--cfi-color-gray-500, #6b7280);\n font-size: var(--cfi-font-size-xs, 0.75rem);\n margin-right: var(--cfi-spacing-xs, 0.25rem);\n}\n.cfi-sh-locale-chevron {\n transition: transform 150ms ease;\n flex-shrink: 0;\n}\n.cfi-sh-locale-chevron-open {\n transform: rotate(180deg);\n}\n.cfi-sh-locale-sublist {\n border-top: 1px solid var(--cfi-color-gray-100, #f3f4f6);\n background: var(--cfi-color-gray-50, #f9fafb);\n}\n.cfi-sh-locale-sub-item {\n padding-left: calc(var(--cfi-spacing-md, 1rem) + var(--cfi-spacing-md, 1rem));\n justify-content: space-between;\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,9 @@ import {
62
63
  Shield,
63
64
  GripHorizontal,
64
65
  Bell,
65
- LogOut
66
+ LogOut,
67
+ Globe,
68
+ ChevronDown
66
69
  } from "lucide-react";
67
70
  import { jsx, jsxs } from "react/jsx-runtime";
68
71
  var iconMap = {
@@ -248,20 +251,49 @@ function formatRelative(iso) {
248
251
 
249
252
  // src/UserMenu.tsx
250
253
  import { useState as useState3, useRef as useRef3, useEffect as useEffect3 } from "react";
251
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
252
- function UserMenu({ user, onLogout, color }) {
254
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
255
+ function UserMenu({
256
+ user,
257
+ onLogout,
258
+ color,
259
+ locale,
260
+ supportedLocales,
261
+ onLocaleChange
262
+ }) {
253
263
  const [open, setOpen] = useState3(false);
264
+ const [langExpanded, setLangExpanded] = useState3(false);
265
+ const [updating, setUpdating] = useState3(false);
254
266
  const ref = useRef3(null);
255
267
  useEffect3(() => {
256
268
  function handleClickOutside(e) {
257
269
  if (ref.current && !ref.current.contains(e.target)) {
258
270
  setOpen(false);
271
+ setLangExpanded(false);
259
272
  }
260
273
  }
261
274
  if (open) document.addEventListener("mousedown", handleClickOutside);
262
275
  return () => document.removeEventListener("mousedown", handleClickOutside);
263
276
  }, [open]);
277
+ useEffect3(() => {
278
+ if (!open) setLangExpanded(false);
279
+ }, [open]);
264
280
  const initials = user.name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2);
281
+ const showLocale = locale !== void 0 && supportedLocales !== void 0 && supportedLocales.length > 0 && onLocaleChange !== void 0;
282
+ const currentLocale = showLocale ? supportedLocales.find((l) => l.code === locale) : void 0;
283
+ async function handleLocaleSelect(code) {
284
+ if (code === locale || !onLocaleChange) {
285
+ setLangExpanded(false);
286
+ return;
287
+ }
288
+ setUpdating(true);
289
+ try {
290
+ await onLocaleChange(code);
291
+ } finally {
292
+ setUpdating(false);
293
+ setLangExpanded(false);
294
+ setOpen(false);
295
+ }
296
+ }
265
297
  return /* @__PURE__ */ jsxs4("div", { className: "cfi-sh-user-menu", ref, children: [
266
298
  /* @__PURE__ */ jsx4(
267
299
  "button",
@@ -279,6 +311,43 @@ function UserMenu({ user, onLogout, color }) {
279
311
  /* @__PURE__ */ jsx4("span", { className: "cfi-sh-user-name", children: user.name }),
280
312
  /* @__PURE__ */ jsx4("span", { className: "cfi-sh-user-email", children: user.email })
281
313
  ] }),
314
+ showLocale && /* @__PURE__ */ jsxs4(Fragment, { children: [
315
+ /* @__PURE__ */ jsx4("div", { className: "cfi-sh-divider" }),
316
+ /* @__PURE__ */ jsxs4(
317
+ "button",
318
+ {
319
+ className: "cfi-sh-menu-item cfi-sh-locale-row",
320
+ onClick: () => setLangExpanded(!langExpanded),
321
+ "aria-expanded": langExpanded,
322
+ disabled: updating,
323
+ children: [
324
+ /* @__PURE__ */ jsx4(Globe, { size: 14 }),
325
+ /* @__PURE__ */ jsx4("span", { className: "cfi-sh-locale-row-label", children: "Language" }),
326
+ /* @__PURE__ */ jsx4("span", { className: "cfi-sh-locale-row-current", children: currentLocale?.label ?? locale }),
327
+ /* @__PURE__ */ jsx4(
328
+ ChevronDown,
329
+ {
330
+ size: 14,
331
+ className: langExpanded ? "cfi-sh-locale-chevron cfi-sh-locale-chevron-open" : "cfi-sh-locale-chevron"
332
+ }
333
+ )
334
+ ]
335
+ }
336
+ ),
337
+ langExpanded && /* @__PURE__ */ jsx4("div", { className: "cfi-sh-locale-sublist", children: supportedLocales.map((loc) => /* @__PURE__ */ jsxs4(
338
+ "button",
339
+ {
340
+ className: loc.code === locale ? "cfi-sh-menu-item cfi-sh-locale-sub-item cfi-sh-locale-item-active" : "cfi-sh-menu-item cfi-sh-locale-sub-item",
341
+ onClick: () => handleLocaleSelect(loc.code),
342
+ disabled: updating,
343
+ children: [
344
+ /* @__PURE__ */ jsx4("span", { className: "cfi-sh-locale-item-label", children: loc.label }),
345
+ /* @__PURE__ */ jsx4("span", { className: "cfi-sh-locale-item-code", children: loc.code.toUpperCase() })
346
+ ]
347
+ },
348
+ loc.code
349
+ )) })
350
+ ] }),
282
351
  /* @__PURE__ */ jsx4("div", { className: "cfi-sh-divider" }),
283
352
  /* @__PURE__ */ jsxs4(
284
353
  "button",
@@ -311,6 +380,9 @@ function ShellHeader({
311
380
  onMarkAllRead,
312
381
  onNotificationItemClick,
313
382
  onLogout,
383
+ locale,
384
+ supportedLocales,
385
+ onLocaleChange,
314
386
  brandWidth,
315
387
  children
316
388
  }) {
@@ -337,11 +409,86 @@ function ShellHeader({
337
409
  }
338
410
  ),
339
411
  /* @__PURE__ */ jsx5(AppSwitcher, { currentAppId: appId, authorizedApps }),
340
- /* @__PURE__ */ jsx5(UserMenu, { user, onLogout, color: currentApp?.color })
412
+ /* @__PURE__ */ jsx5(
413
+ UserMenu,
414
+ {
415
+ user,
416
+ onLogout,
417
+ color: currentApp?.color,
418
+ locale,
419
+ supportedLocales,
420
+ onLocaleChange
421
+ }
422
+ )
341
423
  ] })
342
424
  ] });
343
425
  }
426
+
427
+ // src/LocaleSwitcher.tsx
428
+ import { useState as useState4, useRef as useRef4, useEffect as useEffect4 } from "react";
429
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
430
+ function LocaleSwitcher({ currentLocale, supportedLocales, onLocaleChange }) {
431
+ const [open, setOpen] = useState4(false);
432
+ const [updating, setUpdating] = useState4(false);
433
+ const ref = useRef4(null);
434
+ useEffect4(() => {
435
+ function handleClickOutside(e) {
436
+ if (ref.current && !ref.current.contains(e.target)) {
437
+ setOpen(false);
438
+ }
439
+ }
440
+ if (open) document.addEventListener("mousedown", handleClickOutside);
441
+ return () => document.removeEventListener("mousedown", handleClickOutside);
442
+ }, [open]);
443
+ const current = supportedLocales.find((l) => l.code === currentLocale);
444
+ const buttonLabel = (current?.code ?? currentLocale).toUpperCase();
445
+ async function handleSelect(code) {
446
+ if (code === currentLocale) {
447
+ setOpen(false);
448
+ return;
449
+ }
450
+ setUpdating(true);
451
+ try {
452
+ await onLocaleChange(code);
453
+ } finally {
454
+ setUpdating(false);
455
+ setOpen(false);
456
+ }
457
+ }
458
+ return /* @__PURE__ */ jsxs6("div", { className: "cfi-sh-locale-switcher", ref, children: [
459
+ /* @__PURE__ */ jsxs6(
460
+ "button",
461
+ {
462
+ className: "cfi-sh-locale-btn",
463
+ onClick: () => setOpen(!open),
464
+ "aria-expanded": open,
465
+ "aria-haspopup": "true",
466
+ "aria-label": `Language: ${current?.label ?? currentLocale}`,
467
+ disabled: updating,
468
+ title: current?.label ?? currentLocale,
469
+ children: [
470
+ /* @__PURE__ */ jsx6(Globe, { size: 16 }),
471
+ /* @__PURE__ */ jsx6("span", { className: "cfi-sh-locale-label", children: buttonLabel })
472
+ ]
473
+ }
474
+ ),
475
+ open && /* @__PURE__ */ jsx6("div", { className: "cfi-sh-locale-dropdown", children: supportedLocales.map((loc) => /* @__PURE__ */ jsxs6(
476
+ "button",
477
+ {
478
+ className: loc.code === currentLocale ? "cfi-sh-menu-item cfi-sh-locale-item-active" : "cfi-sh-menu-item",
479
+ onClick: () => handleSelect(loc.code),
480
+ disabled: updating,
481
+ children: [
482
+ /* @__PURE__ */ jsx6("span", { className: "cfi-sh-locale-item-label", children: loc.label }),
483
+ /* @__PURE__ */ jsx6("span", { className: "cfi-sh-locale-item-code", children: loc.code.toUpperCase() })
484
+ ]
485
+ },
486
+ loc.code
487
+ )) })
488
+ ] });
489
+ }
344
490
  export {
491
+ LocaleSwitcher,
345
492
  ShellHeader,
346
493
  appCatalog,
347
494
  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.4.1",
4
4
  "description": "Shared shell header with app switcher for Campfire Suite",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",