@campfire-interactive/shell-header 0.6.4 → 0.7.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
@@ -44,6 +44,26 @@ interface CurrencyOption {
44
44
  /** Human-readable label shown in the dropdown, e.g. "US Dollar", "Euro". */
45
45
  label: string;
46
46
  }
47
+ /**
48
+ * Build/deploy metadata surfaced by the hidden About modal (Ctrl+Alt+A).
49
+ * All fields are host-supplied; the package never fetches anything itself.
50
+ */
51
+ interface AboutInfo {
52
+ /** Display name of the app, e.g. "OMSF". */
53
+ productTitle: string;
54
+ /** Environment label, e.g. "dev", "prod", "pr-42". */
55
+ environment: string;
56
+ /** Short git SHA of the commit the build came from, e.g. "a4b1c8d". */
57
+ revisionId: string;
58
+ /** ISO 8601 commit date of that SHA. */
59
+ revisionDate: string;
60
+ /**
61
+ * ISO 8601 timestamp of when the current artifact was deployed to this
62
+ * environment. Written separately from the build artifact so promotions
63
+ * (e.g. dev → prod) reflect the destination deploy time, not the build time.
64
+ */
65
+ rolloutDate: string;
66
+ }
47
67
  /**
48
68
  * Subset of the platform-notifications `Notification` shape that the bell UI
49
69
  * actually renders. Hosts typically map their fetched items (e.g., from
@@ -147,13 +167,20 @@ interface ShellHeaderProps {
147
167
  * endpoint) and for propagating it through the rest of the UI.
148
168
  */
149
169
  onCurrencyChange?: (currency: string) => void | Promise<void>;
170
+ /**
171
+ * Optional build/deploy metadata. When provided, enables a hidden
172
+ * Ctrl+Alt+A (Cmd+Opt+A on Mac) shortcut that opens an About modal
173
+ * showing this metadata with a copy-to-clipboard button. Omitting this
174
+ * prop disables the shortcut and renders nothing extra.
175
+ */
176
+ about?: AboutInfo;
150
177
  /** Fixed width for the app brand area (e.g., to align with a sidebar below). */
151
178
  brandWidth?: number;
152
179
  /** Optional content in the left/center area (filters, search, breadcrumbs, etc.) */
153
180
  children?: React.ReactNode;
154
181
  }
155
182
 
156
- declare function ShellHeader({ appId, user, authorizedApps, currentTenantRole, notificationCount, onNotificationClick, notifications, onMarkRead, onMarkAllRead, onNotificationItemClick, onLogout, locale, supportedLocales, onLocaleChange, currency, supportedCurrencies, onCurrencyChange, tenant, tenants, onTenantSwitch, brandWidth, children, }: ShellHeaderProps): react_jsx_runtime.JSX.Element;
183
+ declare function ShellHeader({ appId, user, authorizedApps, currentTenantRole, notificationCount, onNotificationClick, notifications, onMarkRead, onMarkAllRead, onNotificationItemClick, onLogout, locale, supportedLocales, onLocaleChange, currency, supportedCurrencies, onCurrencyChange, tenant, tenants, onTenantSwitch, about, brandWidth, children, }: ShellHeaderProps): react_jsx_runtime.JSX.Element;
157
184
 
158
185
  interface LocaleSwitcherProps {
159
186
  currentLocale: string;
@@ -171,4 +198,4 @@ declare const appCatalog: AppDefinition[];
171
198
  */
172
199
  declare function getAppUrl(app: AppDefinition): string;
173
200
 
174
- export { type AppDefinition, type LocaleOption, LocaleSwitcher, type NotificationItem, ShellHeader, type ShellHeaderProps, type ShellUser, appCatalog, getAppUrl };
201
+ export { type AboutInfo, type AppDefinition, type CurrencyOption, 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-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-tenant-static {\n cursor: default;\n pointer-events: none;\n}\n.cfi-sh-tenant-check {\n color: var(--cfi-color-violet-600, #7c3aed);\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-tenant-static {\n cursor: default;\n pointer-events: none;\n}\n.cfi-sh-tenant-check {\n color: var(--cfi-color-violet-600, #7c3aed);\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.cfi-sh-about-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1100;\n font-family: var(--cfi-font-family, system-ui, sans-serif);\n}\n.cfi-sh-about-card {\n background: white;\n border-radius: var(--cfi-radius-lg, 0.5rem);\n box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);\n min-width: 360px;\n max-width: 500px;\n color: var(--cfi-color-gray-900, #111827);\n}\n.cfi-sh-about-header {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-sm, 0.5rem);\n padding: var(--cfi-spacing-md, 1rem);\n border-bottom: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n}\n.cfi-sh-about-title {\n font-weight: 600;\n font-size: var(--cfi-font-size-lg, 1.125rem);\n flex: 1;\n}\n.cfi-sh-about-env {\n font-size: var(--cfi-font-size-xs, 0.75rem);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n background: var(--cfi-color-gray-100, #f3f4f6);\n color: var(--cfi-color-gray-700, #374151);\n padding: 2px 8px;\n border-radius: var(--cfi-radius-sm, 0.25rem);\n font-weight: 600;\n}\n.cfi-sh-about-close {\n background: none;\n border: none;\n font-size: 1.5rem;\n line-height: 1;\n cursor: pointer;\n color: var(--cfi-color-gray-500, #6b7280);\n padding: 0 4px;\n font-family: inherit;\n}\n.cfi-sh-about-close:hover {\n color: var(--cfi-color-gray-900, #111827);\n}\n.cfi-sh-about-list {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: var(--cfi-spacing-xs, 0.25rem) var(--cfi-spacing-md, 1rem);\n padding: var(--cfi-spacing-md, 1rem);\n margin: 0;\n font-size: var(--cfi-font-size-sm, 0.875rem);\n}\n.cfi-sh-about-list dt {\n color: var(--cfi-color-gray-600, #4b5563);\n font-weight: 500;\n}\n.cfi-sh-about-list dd {\n margin: 0;\n color: var(--cfi-color-gray-900, #111827);\n}\n.cfi-sh-about-list code {\n font-family:\n ui-monospace,\n SFMono-Regular,\n Menlo,\n Consolas,\n monospace;\n font-size: var(--cfi-font-size-sm, 0.875rem);\n background: var(--cfi-color-gray-100, #f3f4f6);\n padding: 1px 6px;\n border-radius: var(--cfi-radius-sm, 0.25rem);\n}\n.cfi-sh-about-date {\n color: var(--cfi-color-gray-500, #6b7280);\n}\n.cfi-sh-about-footer {\n display: flex;\n justify-content: flex-end;\n padding: var(--cfi-spacing-md, 1rem);\n border-top: 1px solid var(--cfi-color-gray-200, #e5e7eb);\n}\n.cfi-sh-about-copy {\n background: var(--cfi-color-gray-100, #f3f4f6);\n border: 1px solid var(--cfi-color-gray-300, #d1d5db);\n color: var(--cfi-color-gray-900, #111827);\n padding: 6px 12px;\n border-radius: var(--cfi-radius-md, 0.375rem);\n font-size: var(--cfi-font-size-sm, 0.875rem);\n font-weight: 500;\n cursor: pointer;\n font-family: inherit;\n transition: background 150ms;\n}\n.cfi-sh-about-copy:hover {\n background: var(--cfi-color-gray-200, #e5e7eb);\n}\n");
28
28
 
29
29
  // src/appCatalog.ts
30
30
  var appCatalog = [
@@ -496,8 +496,122 @@ function UserMenu({
496
496
  ] });
497
497
  }
498
498
 
499
- // src/ShellHeader.tsx
499
+ // src/AboutModal.tsx
500
+ import { useEffect as useEffect4, useState as useState4 } from "react";
500
501
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
502
+ function AboutModal({ about, onClose }) {
503
+ const [copied, setCopied] = useState4(false);
504
+ useEffect4(() => {
505
+ function handleEsc(e) {
506
+ if (e.key === "Escape") onClose();
507
+ }
508
+ document.addEventListener("keydown", handleEsc);
509
+ return () => document.removeEventListener("keydown", handleEsc);
510
+ }, [onClose]);
511
+ async function handleCopy() {
512
+ const text = formatClipboardPayload(about);
513
+ try {
514
+ await navigator.clipboard.writeText(text);
515
+ setCopied(true);
516
+ window.setTimeout(() => setCopied(false), 2e3);
517
+ } catch {
518
+ }
519
+ }
520
+ return /* @__PURE__ */ jsx5(
521
+ "div",
522
+ {
523
+ className: "cfi-sh-about-overlay",
524
+ role: "dialog",
525
+ "aria-modal": "true",
526
+ "aria-label": "About this app",
527
+ onClick: (e) => {
528
+ if (e.target === e.currentTarget) onClose();
529
+ },
530
+ children: /* @__PURE__ */ jsxs5("div", { className: "cfi-sh-about-card", children: [
531
+ /* @__PURE__ */ jsxs5("div", { className: "cfi-sh-about-header", children: [
532
+ /* @__PURE__ */ jsx5("span", { className: "cfi-sh-about-title", children: about.productTitle }),
533
+ /* @__PURE__ */ jsx5("span", { className: "cfi-sh-about-env", children: about.environment }),
534
+ /* @__PURE__ */ jsx5(
535
+ "button",
536
+ {
537
+ className: "cfi-sh-about-close",
538
+ type: "button",
539
+ onClick: onClose,
540
+ "aria-label": "Close",
541
+ children: "\xD7"
542
+ }
543
+ )
544
+ ] }),
545
+ /* @__PURE__ */ jsxs5("dl", { className: "cfi-sh-about-list", children: [
546
+ /* @__PURE__ */ jsx5("dt", { children: "Revision" }),
547
+ /* @__PURE__ */ jsxs5("dd", { children: [
548
+ /* @__PURE__ */ jsx5("code", { children: about.revisionId }),
549
+ /* @__PURE__ */ jsxs5("span", { className: "cfi-sh-about-date", children: [
550
+ " (",
551
+ formatDate(about.revisionDate),
552
+ ")"
553
+ ] })
554
+ ] }),
555
+ /* @__PURE__ */ jsx5("dt", { children: "Deployed" }),
556
+ /* @__PURE__ */ jsx5("dd", { children: formatDateTime(about.rolloutDate) })
557
+ ] }),
558
+ /* @__PURE__ */ jsx5("div", { className: "cfi-sh-about-footer", children: /* @__PURE__ */ jsx5(
559
+ "button",
560
+ {
561
+ className: "cfi-sh-about-copy",
562
+ type: "button",
563
+ onClick: handleCopy,
564
+ children: copied ? "Copied!" : "Copy"
565
+ }
566
+ ) })
567
+ ] })
568
+ }
569
+ );
570
+ }
571
+ function formatClipboardPayload(about) {
572
+ return [
573
+ `**${about.productTitle}** (${about.environment})`,
574
+ `Revision: ${about.revisionId} (${formatDate(about.revisionDate)})`,
575
+ `Deployed: ${formatDateTime(about.rolloutDate)}`
576
+ ].join("\n");
577
+ }
578
+ function formatDate(iso) {
579
+ const d = new Date(iso);
580
+ if (Number.isNaN(d.getTime())) return iso;
581
+ return d.toISOString().slice(0, 10);
582
+ }
583
+ function formatDateTime(iso) {
584
+ const d = new Date(iso);
585
+ if (Number.isNaN(d.getTime())) return iso;
586
+ return d.toISOString().replace("T", " ").slice(0, 19) + " UTC";
587
+ }
588
+
589
+ // src/useAboutShortcut.ts
590
+ import { useEffect as useEffect5, useState as useState5 } from "react";
591
+ function useAboutShortcut(enabled) {
592
+ const [isOpen, setIsOpen] = useState5(false);
593
+ useEffect5(() => {
594
+ if (!enabled) return;
595
+ function handleKeyDown(e) {
596
+ const platformMod = e.ctrlKey || e.metaKey;
597
+ if (!platformMod || !e.altKey || e.shiftKey) return;
598
+ if (e.key.toLowerCase() !== "a") return;
599
+ const target = e.target;
600
+ if (target) {
601
+ const tag = target.tagName;
602
+ if (tag === "INPUT" || tag === "TEXTAREA" || target.isContentEditable) return;
603
+ }
604
+ e.preventDefault();
605
+ setIsOpen((open) => !open);
606
+ }
607
+ document.addEventListener("keydown", handleKeyDown);
608
+ return () => document.removeEventListener("keydown", handleKeyDown);
609
+ }, [enabled]);
610
+ return [isOpen, setIsOpen];
611
+ }
612
+
613
+ // src/ShellHeader.tsx
614
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
501
615
  function ShellHeader({
502
616
  appId,
503
617
  user,
@@ -519,61 +633,66 @@ function ShellHeader({
519
633
  tenant,
520
634
  tenants,
521
635
  onTenantSwitch,
636
+ about,
522
637
  brandWidth,
523
638
  children
524
639
  }) {
525
640
  const currentApp = appCatalog.find((a) => a.id === appId);
641
+ const [aboutOpen, setAboutOpen] = useAboutShortcut(about !== void 0);
526
642
  const brandStyle = brandWidth ? { width: `${brandWidth}px`, flexShrink: 0, boxSizing: "border-box" } : void 0;
527
- return /* @__PURE__ */ jsxs5("header", { className: "cfi-sh-header", children: [
528
- /* @__PURE__ */ jsxs5("div", { className: "cfi-sh-left", children: [
529
- /* @__PURE__ */ jsxs5("div", { className: "cfi-sh-app-brand", style: brandStyle, children: [
530
- currentApp && /* @__PURE__ */ jsx5("span", { className: "cfi-sh-app-badge", style: { background: currentApp.color }, children: currentApp.letter }),
531
- /* @__PURE__ */ jsx5("span", { className: "cfi-sh-app-title", children: currentApp?.name || appId })
643
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
644
+ /* @__PURE__ */ jsxs6("header", { className: "cfi-sh-header", children: [
645
+ /* @__PURE__ */ jsxs6("div", { className: "cfi-sh-left", children: [
646
+ /* @__PURE__ */ jsxs6("div", { className: "cfi-sh-app-brand", style: brandStyle, children: [
647
+ currentApp && /* @__PURE__ */ jsx6("span", { className: "cfi-sh-app-badge", style: { background: currentApp.color }, children: currentApp.letter }),
648
+ /* @__PURE__ */ jsx6("span", { className: "cfi-sh-app-title", children: currentApp?.name || appId })
649
+ ] }),
650
+ children
532
651
  ] }),
533
- children
652
+ /* @__PURE__ */ jsxs6("div", { className: "cfi-sh-right", children: [
653
+ /* @__PURE__ */ jsx6(
654
+ NotificationBell,
655
+ {
656
+ count: notificationCount,
657
+ onClick: onNotificationClick,
658
+ items: notifications,
659
+ onMarkRead,
660
+ onMarkAllRead,
661
+ onItemClick: onNotificationItemClick
662
+ }
663
+ ),
664
+ /* @__PURE__ */ jsx6(AppSwitcher, { currentAppId: appId, authorizedApps, currentTenantRole }),
665
+ /* @__PURE__ */ jsx6(
666
+ UserMenu,
667
+ {
668
+ user,
669
+ onLogout,
670
+ color: currentApp?.color,
671
+ locale,
672
+ supportedLocales,
673
+ onLocaleChange,
674
+ currency,
675
+ supportedCurrencies,
676
+ onCurrencyChange,
677
+ tenant,
678
+ tenants,
679
+ onTenantSwitch
680
+ }
681
+ )
682
+ ] })
534
683
  ] }),
535
- /* @__PURE__ */ jsxs5("div", { className: "cfi-sh-right", children: [
536
- /* @__PURE__ */ jsx5(
537
- NotificationBell,
538
- {
539
- count: notificationCount,
540
- onClick: onNotificationClick,
541
- items: notifications,
542
- onMarkRead,
543
- onMarkAllRead,
544
- onItemClick: onNotificationItemClick
545
- }
546
- ),
547
- /* @__PURE__ */ jsx5(AppSwitcher, { currentAppId: appId, authorizedApps, currentTenantRole }),
548
- /* @__PURE__ */ jsx5(
549
- UserMenu,
550
- {
551
- user,
552
- onLogout,
553
- color: currentApp?.color,
554
- locale,
555
- supportedLocales,
556
- onLocaleChange,
557
- currency,
558
- supportedCurrencies,
559
- onCurrencyChange,
560
- tenant,
561
- tenants,
562
- onTenantSwitch
563
- }
564
- )
565
- ] })
684
+ about && aboutOpen && /* @__PURE__ */ jsx6(AboutModal, { about, onClose: () => setAboutOpen(false) })
566
685
  ] });
567
686
  }
568
687
 
569
688
  // src/LocaleSwitcher.tsx
570
- import { useState as useState4, useRef as useRef4, useEffect as useEffect4 } from "react";
571
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
689
+ import { useState as useState6, useRef as useRef4, useEffect as useEffect6 } from "react";
690
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
572
691
  function LocaleSwitcher({ currentLocale, supportedLocales, onLocaleChange }) {
573
- const [open, setOpen] = useState4(false);
574
- const [updating, setUpdating] = useState4(false);
692
+ const [open, setOpen] = useState6(false);
693
+ const [updating, setUpdating] = useState6(false);
575
694
  const ref = useRef4(null);
576
- useEffect4(() => {
695
+ useEffect6(() => {
577
696
  function handleClickOutside(e) {
578
697
  if (ref.current && !ref.current.contains(e.target)) {
579
698
  setOpen(false);
@@ -597,8 +716,8 @@ function LocaleSwitcher({ currentLocale, supportedLocales, onLocaleChange }) {
597
716
  setOpen(false);
598
717
  }
599
718
  }
600
- return /* @__PURE__ */ jsxs6("div", { className: "cfi-sh-locale-switcher", ref, children: [
601
- /* @__PURE__ */ jsxs6(
719
+ return /* @__PURE__ */ jsxs7("div", { className: "cfi-sh-locale-switcher", ref, children: [
720
+ /* @__PURE__ */ jsxs7(
602
721
  "button",
603
722
  {
604
723
  className: "cfi-sh-locale-btn",
@@ -609,20 +728,20 @@ function LocaleSwitcher({ currentLocale, supportedLocales, onLocaleChange }) {
609
728
  disabled: updating,
610
729
  title: current?.label ?? currentLocale,
611
730
  children: [
612
- /* @__PURE__ */ jsx6(Globe, { size: 16 }),
613
- /* @__PURE__ */ jsx6("span", { className: "cfi-sh-locale-label", children: buttonLabel })
731
+ /* @__PURE__ */ jsx7(Globe, { size: 16 }),
732
+ /* @__PURE__ */ jsx7("span", { className: "cfi-sh-locale-label", children: buttonLabel })
614
733
  ]
615
734
  }
616
735
  ),
617
- open && /* @__PURE__ */ jsx6("div", { className: "cfi-sh-locale-dropdown", children: supportedLocales.map((loc) => /* @__PURE__ */ jsxs6(
736
+ open && /* @__PURE__ */ jsx7("div", { className: "cfi-sh-locale-dropdown", children: supportedLocales.map((loc) => /* @__PURE__ */ jsxs7(
618
737
  "button",
619
738
  {
620
739
  className: loc.code === currentLocale ? "cfi-sh-menu-item cfi-sh-locale-item-active" : "cfi-sh-menu-item",
621
740
  onClick: () => handleSelect(loc.code),
622
741
  disabled: updating,
623
742
  children: [
624
- /* @__PURE__ */ jsx6("span", { className: "cfi-sh-locale-item-label", children: loc.label }),
625
- /* @__PURE__ */ jsx6("span", { className: "cfi-sh-locale-item-code", children: loc.code.toUpperCase() })
743
+ /* @__PURE__ */ jsx7("span", { className: "cfi-sh-locale-item-label", children: loc.label }),
744
+ /* @__PURE__ */ jsx7("span", { className: "cfi-sh-locale-item-code", children: loc.code.toUpperCase() })
626
745
  ]
627
746
  },
628
747
  loc.code
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@campfire-interactive/shell-header",
3
- "version": "0.6.4",
3
+ "version": "0.7.1",
4
4
  "description": "Shared shell header with app switcher for Campfire Suite",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",