@campfire-interactive/shell-header 0.5.3 → 0.6.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
@@ -10,6 +10,12 @@ interface AppDefinition {
10
10
  color: string;
11
11
  /** Local dev port (e.g., 3000). Omit if the app doesn't run locally. */
12
12
  localPort?: number;
13
+ /**
14
+ * If set, the app tile is only visible when the host passes a
15
+ * `currentTenantRole` that matches one of these values. Used to gate
16
+ * admin-style apps (e.g. User Management → ['admin', 'owner']).
17
+ */
18
+ requiresRole?: string[];
13
19
  }
14
20
  interface ShellUser {
15
21
  name: string;
@@ -32,6 +38,12 @@ interface LocaleOption {
32
38
  /** Human-readable label shown in the dropdown, e.g. "English", "Deutsch". */
33
39
  label: string;
34
40
  }
41
+ interface CurrencyOption {
42
+ /** ISO-4217 code, e.g. "USD", "EUR", "JPY". */
43
+ code: string;
44
+ /** Human-readable label shown in the dropdown, e.g. "US Dollar", "Euro". */
45
+ label: string;
46
+ }
35
47
  /**
36
48
  * Subset of the platform-notifications `Notification` shape that the bell UI
37
49
  * actually renders. Hosts typically map their fetched items (e.g., from
@@ -53,6 +65,13 @@ interface ShellHeaderProps {
53
65
  user: ShellUser;
54
66
  /** App IDs the user is authorized to access in the current tenant */
55
67
  authorizedApps: string[];
68
+ /**
69
+ * The user's role in their currently-active tenant (e.g. "admin", "owner",
70
+ * "member"). Apps in the catalog with `requiresRole` are shown only when
71
+ * this matches one of the allowed roles. Hosts typically derive this from
72
+ * `user.tenants.find(t => t.id === user.tenant.id)?.role`.
73
+ */
74
+ currentTenantRole?: string;
56
75
  /** Unread count for the bell badge. */
57
76
  notificationCount?: number;
58
77
  /**
@@ -110,13 +129,31 @@ interface ShellHeaderProps {
110
129
  * pick up the change.
111
130
  */
112
131
  onLocaleChange?: (locale: string) => void | Promise<void>;
132
+ /**
133
+ * Current display currency (ISO-4217, e.g. "USD"). When provided alongside
134
+ * `supportedCurrencies` and `onCurrencyChange`, a Currency section renders
135
+ * inside the dropdown. Omitting any of the three hides it.
136
+ *
137
+ * The picker is a UI-only surface — this package neither knows nor cares
138
+ * about FX rates or conversion. Hosts handle rate tables, conversion, and
139
+ * persistence themselves.
140
+ */
141
+ currency?: string;
142
+ /** Currencies the user can pick from. */
143
+ supportedCurrencies?: ReadonlyArray<CurrencyOption>;
144
+ /**
145
+ * Called when the user picks a different currency. Consumer is responsible
146
+ * for persisting the choice (typically to localStorage or a user-prefs
147
+ * endpoint) and for propagating it through the rest of the UI.
148
+ */
149
+ onCurrencyChange?: (currency: string) => void | Promise<void>;
113
150
  /** Fixed width for the app brand area (e.g., to align with a sidebar below). */
114
151
  brandWidth?: number;
115
152
  /** Optional content in the left/center area (filters, search, breadcrumbs, etc.) */
116
153
  children?: React.ReactNode;
117
154
  }
118
155
 
119
- declare function ShellHeader({ appId, user, authorizedApps, notificationCount, onNotificationClick, notifications, onMarkRead, onMarkAllRead, onNotificationItemClick, onLogout, locale, supportedLocales, onLocaleChange, tenant, tenants, onTenantSwitch, brandWidth, children, }: ShellHeaderProps): react_jsx_runtime.JSX.Element;
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;
120
157
 
121
158
  interface LocaleSwitcherProps {
122
159
  currentLocale: string;
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ 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: "User Management", icon: "Shield", letter: "U", color: "#8b5cf6", localPort: 3600, requiresRole: ["admin", "owner"] },
38
38
  { id: "bom", name: "BOM", icon: "Network", letter: "B", color: "#0d9488", localPort: 3700 }
39
39
  ];
40
40
  function getAppUrl(app) {
@@ -66,7 +66,8 @@ import {
66
66
  LogOut,
67
67
  Globe,
68
68
  ChevronDown,
69
- Check
69
+ Check,
70
+ Coins
70
71
  } from "lucide-react";
71
72
  import { jsx, jsxs } from "react/jsx-runtime";
72
73
  var iconMap = {
@@ -97,10 +98,12 @@ function GridIcon({ size = 20 }) {
97
98
 
98
99
  // src/AppSwitcher.tsx
99
100
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
100
- function AppSwitcher({ currentAppId, authorizedApps }) {
101
+ function AppSwitcher({ currentAppId, authorizedApps, currentTenantRole }) {
101
102
  const [open, setOpen] = useState(false);
102
103
  const ref = useRef(null);
103
- const visibleApps = appCatalog.filter((a) => authorizedApps.includes(a.id));
104
+ const visibleApps = appCatalog.filter(
105
+ (a) => authorizedApps.includes(a.id) && (!a.requiresRole || currentTenantRole !== void 0 && a.requiresRole.includes(currentTenantRole))
106
+ );
104
107
  useEffect(() => {
105
108
  function handleClickOutside(e) {
106
109
  if (ref.current && !ref.current.contains(e.target)) {
@@ -260,12 +263,16 @@ function UserMenu({
260
263
  locale,
261
264
  supportedLocales,
262
265
  onLocaleChange,
266
+ currency,
267
+ supportedCurrencies,
268
+ onCurrencyChange,
263
269
  tenant,
264
270
  tenants,
265
271
  onTenantSwitch
266
272
  }) {
267
273
  const [open, setOpen] = useState3(false);
268
274
  const [langExpanded, setLangExpanded] = useState3(false);
275
+ const [currExpanded, setCurrExpanded] = useState3(false);
269
276
  const [tenantExpanded, setTenantExpanded] = useState3(false);
270
277
  const [updating, setUpdating] = useState3(false);
271
278
  const ref = useRef3(null);
@@ -274,6 +281,7 @@ function UserMenu({
274
281
  if (ref.current && !ref.current.contains(e.target)) {
275
282
  setOpen(false);
276
283
  setLangExpanded(false);
284
+ setCurrExpanded(false);
277
285
  setTenantExpanded(false);
278
286
  }
279
287
  }
@@ -283,6 +291,7 @@ function UserMenu({
283
291
  useEffect3(() => {
284
292
  if (!open) {
285
293
  setLangExpanded(false);
294
+ setCurrExpanded(false);
286
295
  setTenantExpanded(false);
287
296
  }
288
297
  }, [open]);
@@ -303,6 +312,22 @@ function UserMenu({
303
312
  setOpen(false);
304
313
  }
305
314
  }
315
+ const showCurrency = currency !== void 0 && supportedCurrencies !== void 0 && supportedCurrencies.length > 0 && onCurrencyChange !== void 0;
316
+ const currentCurrency = showCurrency ? supportedCurrencies.find((c) => c.code === currency) : void 0;
317
+ async function handleCurrencySelect(code) {
318
+ if (code === currency || !onCurrencyChange) {
319
+ setCurrExpanded(false);
320
+ return;
321
+ }
322
+ setUpdating(true);
323
+ try {
324
+ await onCurrencyChange(code);
325
+ } finally {
326
+ setUpdating(false);
327
+ setCurrExpanded(false);
328
+ setOpen(false);
329
+ }
330
+ }
306
331
  const showTenant = tenant !== void 0;
307
332
  const canSwitchTenant = showTenant && onTenantSwitch !== void 0 && Array.isArray(tenants) && tenants.length > 1;
308
333
  async function handleTenantSelect(tenantId) {
@@ -310,15 +335,6 @@ function UserMenu({
310
335
  setTenantExpanded(false);
311
336
  return;
312
337
  }
313
- const target = tenants?.find((t) => t.id === tenantId);
314
- const targetName = target?.name ?? "this tenant";
315
- const confirmed = window.confirm(
316
- `Switch to ${targetName}? Other open suite tabs will reload.`
317
- );
318
- if (!confirmed) {
319
- setTenantExpanded(false);
320
- return;
321
- }
322
338
  setUpdating(true);
323
339
  try {
324
340
  await onTenantSwitch(tenantId);
@@ -424,6 +440,43 @@ function UserMenu({
424
440
  loc.code
425
441
  )) })
426
442
  ] }),
443
+ showCurrency && /* @__PURE__ */ jsxs4(Fragment, { children: [
444
+ /* @__PURE__ */ jsx4("div", { className: "cfi-sh-divider" }),
445
+ /* @__PURE__ */ jsxs4(
446
+ "button",
447
+ {
448
+ className: "cfi-sh-menu-item cfi-sh-locale-row",
449
+ onClick: () => setCurrExpanded(!currExpanded),
450
+ "aria-expanded": currExpanded,
451
+ disabled: updating,
452
+ children: [
453
+ /* @__PURE__ */ jsx4(Coins, { size: 14 }),
454
+ /* @__PURE__ */ jsx4("span", { className: "cfi-sh-locale-row-label", children: "Currency" }),
455
+ /* @__PURE__ */ jsx4("span", { className: "cfi-sh-locale-row-current", children: currentCurrency?.code ?? currency }),
456
+ /* @__PURE__ */ jsx4(
457
+ ChevronDown,
458
+ {
459
+ size: 14,
460
+ className: currExpanded ? "cfi-sh-locale-chevron cfi-sh-locale-chevron-open" : "cfi-sh-locale-chevron"
461
+ }
462
+ )
463
+ ]
464
+ }
465
+ ),
466
+ currExpanded && /* @__PURE__ */ jsx4("div", { className: "cfi-sh-locale-sublist", children: supportedCurrencies.map((cur) => /* @__PURE__ */ jsxs4(
467
+ "button",
468
+ {
469
+ className: cur.code === currency ? "cfi-sh-menu-item cfi-sh-locale-sub-item cfi-sh-locale-item-active" : "cfi-sh-menu-item cfi-sh-locale-sub-item",
470
+ onClick: () => handleCurrencySelect(cur.code),
471
+ disabled: updating,
472
+ children: [
473
+ /* @__PURE__ */ jsx4("span", { className: "cfi-sh-locale-item-label", children: cur.label }),
474
+ /* @__PURE__ */ jsx4("span", { className: "cfi-sh-locale-item-code", children: cur.code })
475
+ ]
476
+ },
477
+ cur.code
478
+ )) })
479
+ ] }),
427
480
  /* @__PURE__ */ jsx4("div", { className: "cfi-sh-divider" }),
428
481
  /* @__PURE__ */ jsxs4(
429
482
  "button",
@@ -449,6 +502,7 @@ function ShellHeader({
449
502
  appId,
450
503
  user,
451
504
  authorizedApps,
505
+ currentTenantRole,
452
506
  notificationCount,
453
507
  onNotificationClick,
454
508
  notifications,
@@ -459,6 +513,9 @@ function ShellHeader({
459
513
  locale,
460
514
  supportedLocales,
461
515
  onLocaleChange,
516
+ currency,
517
+ supportedCurrencies,
518
+ onCurrencyChange,
462
519
  tenant,
463
520
  tenants,
464
521
  onTenantSwitch,
@@ -487,7 +544,7 @@ function ShellHeader({
487
544
  onItemClick: onNotificationItemClick
488
545
  }
489
546
  ),
490
- /* @__PURE__ */ jsx5(AppSwitcher, { currentAppId: appId, authorizedApps }),
547
+ /* @__PURE__ */ jsx5(AppSwitcher, { currentAppId: appId, authorizedApps, currentTenantRole }),
491
548
  /* @__PURE__ */ jsx5(
492
549
  UserMenu,
493
550
  {
@@ -497,6 +554,9 @@ function ShellHeader({
497
554
  locale,
498
555
  supportedLocales,
499
556
  onLocaleChange,
557
+ currency,
558
+ supportedCurrencies,
559
+ onCurrencyChange,
500
560
  tenant,
501
561
  tenants,
502
562
  onTenantSwitch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@campfire-interactive/shell-header",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
4
4
  "description": "Shared shell header with app switcher for Campfire Suite",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",