@the-portland-company/shell 0.11.4 → 0.13.0

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.
@@ -1,8 +1,13 @@
1
- import { FiUser, FiSettings, FiMessageSquare, FiMail, FiInfo, FiSun, FiMoon, FiHelpCircle, FiLifeBuoy, FiAlertCircle, FiAlertTriangle, FiBell, FiGrid, FiZap } from 'react-icons/fi';
1
+ import { FiUser, FiSettings, FiMessageSquare, FiMail, FiInfo, FiSun, FiMoon, FiHelpCircle, FiLifeBuoy, FiAlertCircle, FiAlertTriangle, FiBell, FiGrid, FiZap, FiBatteryCharging, FiBattery } from 'react-icons/fi';
2
2
  import { useState, useEffect } from 'react';
3
3
 
4
4
  // src/lib/brandGlyphs.tsx
5
5
  var GLYPHS = {
6
+ // Communications Fuel reads as a LEVEL, so its glyph is a battery. `bolt`/
7
+ // `zap` stay mapped for older Brand values.
8
+ "battery-charging": FiBatteryCharging,
9
+ battery: FiBattery,
10
+ fuel: FiBatteryCharging,
6
11
  bolt: FiZap,
7
12
  zap: FiZap,
8
13
  grid: FiGrid,
@@ -45,5 +50,5 @@ function useTopBarIconSlotGlyph(slot) {
45
50
  }
46
51
 
47
52
  export { resolveBrandGlyph, useTopBarIconSlotGlyph };
48
- //# sourceMappingURL=chunk-2GFWARYP.js.map
49
- //# sourceMappingURL=chunk-2GFWARYP.js.map
53
+ //# sourceMappingURL=chunk-NV6WHTEZ.js.map
54
+ //# sourceMappingURL=chunk-NV6WHTEZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/brandGlyphs.tsx","../src/lib/topbarIconSlot.ts"],"names":[],"mappings":";;;;AAkCA,IAAM,MAAA,GAAyC;AAAA;AAAA;AAAA,EAG7C,kBAAA,EAAoB,iBAAA;AAAA,EACpB,OAAA,EAAS,SAAA;AAAA,EACT,IAAA,EAAM,iBAAA;AAAA,EACN,IAAA,EAAM,KAAA;AAAA,EACN,GAAA,EAAK,KAAA;AAAA,EACL,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM,MAAA;AAAA,EACN,YAAA,EAAc,MAAA;AAAA,EACd,gBAAA,EAAkB,eAAA;AAAA,EAClB,KAAA,EAAO,eAAA;AAAA,EACP,cAAA,EAAgB,aAAA;AAAA,EAChB,WAAA,EAAa,UAAA;AAAA,EACb,WAAA,EAAa,UAAA;AAAA,EACb,OAAA,EAAS,UAAA;AAAA,EACT,aAAA,EAAe,YAAA;AAAA,EACf,IAAA,EAAM,MAAA;AAAA,EACN,GAAA,EAAK,KAAA;AAAA,EACL,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM,MAAA;AAAA,EACN,gBAAA,EAAkB,eAAA;AAAA,EAClB,QAAA,EAAU,UAAA;AAAA,EACV,IAAA,EAAM;AACR,CAAA;AAOO,SAAS,kBAAkB,IAAA,EAAwD;AACxF,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,EAAA,OAAO,OAAO,IAAA,CAAK,IAAA,EAAK,CAAE,WAAA,EAAa,CAAA,IAAK,IAAA;AAC9C;ACzCO,SAAS,uBAAuB,IAAA,EAAqC;AAC1E,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,SAAwB,IAAI,CAAA;AAEpD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAEnC,IAAA,MAAM,OAAO,MAAM;AACjB,MAAA,MAAM,GAAA,GAAM,MAAA,CACT,gBAAA,CAAiB,QAAA,CAAS,eAAe,CAAA,CACzC,gBAAA,CAAiB,CAAA,cAAA,EAAiB,IAAI,EAAE,CAAA,CACxC,IAAA,EAAK,CAEL,OAAA,CAAQ,gBAAgB,EAAE,CAAA;AAC7B,MAAA,OAAA,CAAQ,GAAA,CAAI,MAAA,GAAS,CAAA,GAAI,GAAA,GAAM,IAAI,CAAA;AAAA,IACrC,CAAA;AAEA,IAAA,IAAA,EAAK;AAEL,IAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,IAAI,CAAA;AAC1C,IAAA,QAAA,CAAS,OAAA,CAAQ,SAAS,IAAA,EAAM,EAAE,WAAW,IAAA,EAAM,OAAA,EAAS,MAAM,CAAA;AAClE,IAAA,OAAO,MAAM,SAAS,UAAA,EAAW;AAAA,EACnC,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAET,EAAA,OAAO,IAAA;AACT","file":"chunk-NV6WHTEZ.js","sourcesContent":["import type { ComponentType } from 'react'\nimport {\n FiAlertCircle,\n FiBattery,\n FiBatteryCharging,\n FiAlertTriangle,\n FiBell,\n FiGrid,\n FiHelpCircle,\n FiInfo,\n FiLifeBuoy,\n FiMail,\n FiMessageSquare,\n FiMoon,\n FiSettings,\n FiSun,\n FiUser,\n FiZap,\n} from 'react-icons/fi'\n\nexport type GlyphComponent = ComponentType<{ size?: number | string; className?: string }>\n\n/**\n * Brand-glyph registry: maps the icon NAME the Brand API emits (e.g.\n * `--topbar-icon-comms-fuel: bolt`) to a component.\n *\n * These names are Feather/Lucide icon names, NOT asset filenames — there is no\n * `/brand/icons/<name>.svg` for them in any app, and treating them as URLs\n * renders a broken image. Resolving them against the icon library the shell\n * already depends on keeps every app consistent with zero vendored assets.\n *\n * Aliases are included where Brand and Feather disagree on a name (`bolt` vs\n * `zap`, `life-buoy` vs `life-ring`).\n */\nconst GLYPHS: Record<string, GlyphComponent> = {\n // Communications Fuel reads as a LEVEL, so its glyph is a battery. `bolt`/\n // `zap` stay mapped for older Brand values.\n 'battery-charging': FiBatteryCharging,\n battery: FiBattery,\n fuel: FiBatteryCharging,\n bolt: FiZap,\n zap: FiZap,\n grid: FiGrid,\n apps: FiGrid,\n bell: FiBell,\n notification: FiBell,\n 'alert-triangle': FiAlertTriangle,\n alert: FiAlertTriangle,\n 'alert-circle': FiAlertCircle,\n 'life-buoy': FiLifeBuoy,\n 'life-ring': FiLifeBuoy,\n support: FiLifeBuoy,\n 'help-circle': FiHelpCircle,\n moon: FiMoon,\n sun: FiSun,\n info: FiInfo,\n mail: FiMail,\n 'message-square': FiMessageSquare,\n settings: FiSettings,\n user: FiUser,\n}\n\n/**\n * Resolve a Brand glyph name to a component. Returns null for an unknown name\n * so the caller renders its own fallback — an unrecognized token must never\n * produce a broken or empty icon.\n */\nexport function resolveBrandGlyph(name: string | null | undefined): GlyphComponent | null {\n if (!name) return null\n return GLYPHS[name.trim().toLowerCase()] ?? null\n}\n\n/** Names the registry knows — exported for tests and tooling. */\nexport const KNOWN_BRAND_GLYPHS = Object.keys(GLYPHS)\n","import { useEffect, useState } from 'react'\n\n/**\n * The six canonical top-bar action slots (Reference/Global-Rules v9 → top-bar\n * icons). Each slot's GLYPH is unique and comes from the Brand API\n * `topbarIcons.items.*` tokens, exposed as `--topbar-icon-<slot>` CSS vars\n * whose value is the brand-icon NAME. Every OTHER property — size, gap, color,\n * hover, selected — is universal across all six slots.\n */\nexport type TopBarIconSlot =\n | 'comms-fuel'\n | 'apps'\n | 'notifications'\n | 'alerts'\n | 'support'\n | 'dark-mode'\n\n/**\n * Reads the glyph NAME for a slot from its `--topbar-icon-<slot>` CSS var.\n *\n * Runs in an effect because the Brand stylesheet is appended to <head> at\n * runtime (Global Rules forbids baking it into the build), so the var is not\n * resolvable during SSR or the first synchronous render. The MutationObserver\n * re-reads once the stylesheet lands so the glyph upgrades from the caller's\n * fallback without a reload.\n *\n * Returns null when the slot has no Brand glyph — callers render their own\n * fallback icon in that case.\n */\nexport function useTopBarIconSlotGlyph(slot: TopBarIconSlot): string | null {\n const [name, setName] = useState<string | null>(null)\n\n useEffect(() => {\n if (typeof window === 'undefined') return\n\n const read = () => {\n const raw = window\n .getComputedStyle(document.documentElement)\n .getPropertyValue(`--topbar-icon-${slot}`)\n .trim()\n // Brand may emit the name quoted (e.g. \"bell\"); strip the quotes.\n .replace(/^['\"]|['\"]$/g, '')\n setName(raw.length > 0 ? raw : null)\n }\n\n read()\n\n const observer = new MutationObserver(read)\n observer.observe(document.head, { childList: true, subtree: true })\n return () => observer.disconnect()\n }, [slot])\n\n return name\n}\n\n/** Path to a brand-icon SVG in the consuming app's public dir. */\nexport function brandIconSrc(name: string, basePath = '/brand/icons/') {\n return `${basePath}${name}.svg`\n}\n"]}
package/dist/index.cjs CHANGED
@@ -1303,7 +1303,7 @@ function UserMenu() {
1303
1303
  const org = useShellOrganization();
1304
1304
  const role = useShellRole();
1305
1305
  const navigate = reactRouterDom.useNavigate();
1306
- const menuListBg = react.useColorModeValue("white", "gray.900");
1306
+ const menuListBg = "var(--topbar-bg, #FFFFFF)";
1307
1307
  const menuListBorderColor = react.useColorModeValue("gray.200", "gray.700");
1308
1308
  if (status !== "authenticated" || !user) return null;
1309
1309
  const displayName = getUserDisplayName(user.raw);
@@ -1464,8 +1464,8 @@ function Header({ onOpen, id }) {
1464
1464
  headerIconsSlot,
1465
1465
  brandLogoSrc
1466
1466
  } = useShellChrome();
1467
- const mobileNavBg = react.useColorModeValue("white", "gray.900");
1468
- const mobileNavBorderColor = react.useColorModeValue("gray.200", "gray.700");
1467
+ const mobileNavBg = "var(--sidebar-bg, #FFFFFF)";
1468
+ const mobileNavBorderColor = "var(--sidebar-border, #E2E8F0)";
1469
1469
  return /* @__PURE__ */ jsxRuntime.jsx(
1470
1470
  react.Flex,
1471
1471
  {
@@ -1533,7 +1533,8 @@ function Header({ onOpen, id }) {
1533
1533
  },
1534
1534
  '& button:not([role="menuitem"]):not([data-menu-item]) svg, & button:not([role="menuitem"]):not([data-menu-item]) img': {
1535
1535
  width: "var(--topbar-icon-size, 20px)",
1536
- height: "var(--topbar-icon-size, 20px)"
1536
+ height: "var(--topbar-icon-size, 20px)",
1537
+ strokeWidth: "var(--topbar-icon-stroke-width, 2)"
1537
1538
  }
1538
1539
  },
1539
1540
  children: [
@@ -1653,9 +1654,10 @@ function NavItem({
1653
1654
  const hasActiveChild = Array.isArray(subItems) && subItems.some((item) => matchesSubItem(item.href || ""));
1654
1655
  const isParentActive = isActive || hasActiveChild;
1655
1656
  const isSubMenuOpen = isExpanded !== void 0 ? isExpanded : hasSubItems && subItems.some((item) => matchesSubItem(item.href || ""));
1656
- const expandedRowBg = isParentActive ? modeConfig?.primaryColor || "brand.500" : isSubMenuOpen ? "gray.100" : "transparent";
1657
- const expandedRowTextColor = isParentActive ? "white" : isSubMenuOpen ? "gray.900" : "gray.600";
1658
- const disabledRowBg = isSubItem ? "gray.50" : "transparent";
1657
+ const submenuBg = "var(--sidebar-submenu-bg, #F1F5F9)";
1658
+ const expandedRowBg = isParentActive ? modeConfig?.primaryColor || "brand.500" : isSubMenuOpen ? submenuBg : "transparent";
1659
+ const expandedRowTextColor = isParentActive ? "white" : isSubMenuOpen ? "var(--sidebar-text, #1A202C)" : "var(--sidebar-text-muted, #4A5568)";
1660
+ const disabledRowBg = isSubItem ? submenuBg : "transparent";
1659
1661
  const disabledRowTextColor = "gray.400";
1660
1662
  const renderIcon = (activeForHover) => {
1661
1663
  if (brandIcon) {
@@ -1708,8 +1710,8 @@ function NavItem({
1708
1710
  bg: isDisabled ? disabledRowBg : expandedRowBg,
1709
1711
  color: isDisabled ? disabledRowTextColor : expandedRowTextColor,
1710
1712
  _hover: {
1711
- bg: isDisabled ? disabledRowBg : isParentActive ? modeConfig?.accentColor || "brand.600" : "gray.200",
1712
- color: isDisabled ? disabledRowTextColor : isParentActive ? "white" : "gray.900"
1713
+ bg: isDisabled ? disabledRowBg : isParentActive ? modeConfig?.accentColor || "brand.600" : "var(--sidebar-hover-bg, #E2E8F0)",
1714
+ color: isDisabled ? disabledRowTextColor : isParentActive ? "white" : "var(--sidebar-text, #1A202C)"
1713
1715
  },
1714
1716
  "aria-disabled": isDisabled ? "true" : "false",
1715
1717
  onClick: (e) => {
@@ -1750,8 +1752,8 @@ function NavItem({
1750
1752
  bg: isDisabled ? disabledRowBg : expandedRowBg,
1751
1753
  color: isDisabled ? disabledRowTextColor : expandedRowTextColor,
1752
1754
  _hover: {
1753
- bg: isDisabled ? disabledRowBg : hasActiveChild ? modeConfig?.accentColor || "brand.600" : "gray.200",
1754
- color: isDisabled ? disabledRowTextColor : hasActiveChild ? "white" : "gray.900"
1755
+ bg: isDisabled ? disabledRowBg : hasActiveChild ? modeConfig?.accentColor || "brand.600" : "var(--sidebar-hover-bg, #E2E8F0)",
1756
+ color: isDisabled ? disabledRowTextColor : hasActiveChild ? "white" : "var(--sidebar-text, #1A202C)"
1755
1757
  },
1756
1758
  "aria-disabled": isDisabled ? "true" : "false",
1757
1759
  onClick: (e) => {
@@ -1784,7 +1786,7 @@ function NavItem({
1784
1786
  py: "1",
1785
1787
  borderTopRadius: 0,
1786
1788
  borderBottomRadius: "md",
1787
- bg: isDisabled ? "gray.100" : "gray.200",
1789
+ bg: submenuBg,
1788
1790
  children: subItems.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
1789
1791
  NavItem,
1790
1792
  {
@@ -1839,10 +1841,10 @@ function NavItem({
1839
1841
  opacity: 1,
1840
1842
  pointerEvents: isDisabled ? "none" : "auto",
1841
1843
  "data-nav-item-active": String(rowActive),
1842
- bg: isDisabled ? disabledRowBg : rowActive ? isSubItem ? "gray.100" : modeConfig?.primaryColor || "brand.500" : "transparent",
1844
+ bg: isDisabled ? disabledRowBg : rowActive ? isSubItem ? "var(--sidebar-hover-bg, #EDF2F7)" : modeConfig?.primaryColor || "brand.500" : "transparent",
1843
1845
  color: isDisabled ? disabledRowTextColor : rowActive ? isSubItem ? modeConfig?.primaryColor || "brand.500" : "white" : "gray.600",
1844
1846
  _hover: {
1845
- bg: isDisabled ? disabledRowBg : rowActive ? isSubItem ? "gray.200" : modeConfig?.accentColor || "brand.600" : "gray.100",
1847
+ bg: isDisabled ? disabledRowBg : rowActive ? isSubItem ? "var(--sidebar-hover-bg, #E2E8F0)" : modeConfig?.accentColor || "brand.600" : "var(--sidebar-hover-bg, #EDF2F7)",
1846
1848
  color: isDisabled ? disabledRowTextColor : rowActive ? isSubItem ? modeConfig?.primaryColor || "brand.500" : "white" : "gray.900"
1847
1849
  },
1848
1850
  "aria-disabled": isDisabled ? "true" : "false",
@@ -2459,8 +2461,8 @@ function Sidebar({ onClose, id }) {
2459
2461
  const org = useShellOrganization();
2460
2462
  const currentOrg = org?.currentOrg ?? null;
2461
2463
  const location = reactRouterDom.useLocation();
2462
- const sidebarBg = react.useColorModeValue("white", "gray.900");
2463
- const sidebarBorderColor = react.useColorModeValue("gray.200", "gray.700");
2464
+ const sidebarBg = "var(--sidebar-bg, #FFFFFF)";
2465
+ const sidebarBorderColor = "var(--sidebar-border, #E2E8F0)";
2464
2466
  const [expandedMenu, setExpandedMenu] = react$1.useState(null);
2465
2467
  react$1.useEffect(() => {
2466
2468
  const activeMenu = navItems.find(
@@ -2656,6 +2658,11 @@ function AppLayout({
2656
2658
  ] });
2657
2659
  }
2658
2660
  var GLYPHS = {
2661
+ // Communications Fuel reads as a LEVEL, so its glyph is a battery. `bolt`/
2662
+ // `zap` stay mapped for older Brand values.
2663
+ "battery-charging": fi.FiBatteryCharging,
2664
+ battery: fi.FiBattery,
2665
+ fuel: fi.FiBatteryCharging,
2659
2666
  bolt: fi.FiZap,
2660
2667
  zap: fi.FiZap,
2661
2668
  grid: fi.FiGrid,
@@ -2696,28 +2703,72 @@ function useTopBarIconSlotGlyph(slot) {
2696
2703
  }, [slot]);
2697
2704
  return name;
2698
2705
  }
2706
+ function TopBarBadge({ count }) {
2707
+ if (!count || count <= 0) return null;
2708
+ return /* @__PURE__ */ jsxRuntime.jsx(
2709
+ react.Badge,
2710
+ {
2711
+ position: "absolute",
2712
+ top: "-2px",
2713
+ right: "-2px",
2714
+ bg: "var(--topbar-badge-bg, #E63946)",
2715
+ color: "var(--topbar-badge-fg, #FFFFFF)",
2716
+ rounded: "full",
2717
+ fontSize: "0.625rem",
2718
+ fontWeight: "bold",
2719
+ lineHeight: "1",
2720
+ minW: "1rem",
2721
+ h: "1rem",
2722
+ px: "1",
2723
+ display: "flex",
2724
+ alignItems: "center",
2725
+ justifyContent: "center",
2726
+ pointerEvents: "none",
2727
+ "aria-hidden": true,
2728
+ children: count > 9 ? "9+" : count
2729
+ }
2730
+ );
2731
+ }
2699
2732
  var TopBarIcon = react$1.forwardRef(
2700
- function TopBarIcon2({ slot, fallbackIcon, isSelected, ...rest }, ref) {
2733
+ function TopBarIcon2({ slot, fallbackIcon, isSelected, badgeCount, children, ...rest }, ref) {
2701
2734
  const glyphName = useTopBarIconSlotGlyph(slot);
2702
2735
  const Glyph = resolveBrandGlyph(glyphName);
2703
- const icon = Glyph ? /* @__PURE__ */ jsxRuntime.jsx(Glyph, { size: "var(--topbar-icon-size, 20px)" }) : fallbackIcon;
2704
- return /* @__PURE__ */ jsxRuntime.jsx(
2736
+ const icon = Glyph ? /* @__PURE__ */ jsxRuntime.jsx(Glyph, {}) : fallbackIcon;
2737
+ const button = /* @__PURE__ */ jsxRuntime.jsx(
2705
2738
  react.IconButton,
2706
2739
  {
2707
2740
  ref,
2708
2741
  icon,
2709
2742
  variant: "ghost",
2710
2743
  minW: "var(--topbar-icon-button-size, 32px)",
2744
+ w: "var(--topbar-icon-button-size, 32px)",
2711
2745
  h: "var(--topbar-icon-button-size, 32px)",
2746
+ p: "0",
2712
2747
  bg: isSelected ? "var(--topbar-icon-selected-bg, transparent)" : "transparent",
2713
2748
  color: isSelected ? "var(--topbar-icon-selected-color, var(--topbar-icon-color, #64748B))" : "var(--topbar-icon-color, #64748B)",
2714
2749
  _hover: {
2715
2750
  bg: "var(--topbar-icon-hover-bg, transparent)",
2716
2751
  color: "var(--topbar-icon-hover-color, var(--topbar-icon-color, #64748B))"
2717
2752
  },
2753
+ sx: {
2754
+ // Normalize the glyph itself, not just its box. Covers react-icons
2755
+ // <svg> and brand <img> alike, and pins stroke weight so one glyph
2756
+ // can't read heavier than its neighbours.
2757
+ "& svg, & img": {
2758
+ width: "var(--topbar-icon-size, 20px)",
2759
+ height: "var(--topbar-icon-size, 20px)",
2760
+ strokeWidth: "var(--topbar-icon-stroke-width, 2)"
2761
+ }
2762
+ },
2718
2763
  ...rest
2719
2764
  }
2720
2765
  );
2766
+ if (!badgeCount && !children) return button;
2767
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { position: "relative", display: "inline-flex", children: [
2768
+ button,
2769
+ /* @__PURE__ */ jsxRuntime.jsx(TopBarBadge, { count: badgeCount ?? 0 }),
2770
+ children
2771
+ ] });
2721
2772
  }
2722
2773
  );
2723
2774
  var SUPPORT_REQUEST_SUCCESS_MESSAGE = "Your support request was sent to Politogy leadership.";
@@ -2872,12 +2923,11 @@ ${formatted}` : formatted,
2872
2923
  if (!support) return null;
2873
2924
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2874
2925
  /* @__PURE__ */ jsxRuntime.jsx(
2875
- react.IconButton,
2926
+ TopBarIcon,
2876
2927
  {
2928
+ slot: "support",
2877
2929
  "aria-label": "Request support",
2878
- icon: /* @__PURE__ */ jsxRuntime.jsx(LifebuoyIcon, {}),
2879
- variant: "ghost",
2880
- size: "sm",
2930
+ fallbackIcon: /* @__PURE__ */ jsxRuntime.jsx(LifebuoyIcon, {}),
2881
2931
  onClick: onOpen
2882
2932
  }
2883
2933
  ),
@@ -3120,6 +3170,7 @@ exports.ShellRoleContext = ShellRoleContext;
3120
3170
  exports.ShellSupportProviderContext = ShellSupportProviderContext;
3121
3171
  exports.Sidebar = Sidebar;
3122
3172
  exports.SupportRequestButton = SupportRequestButton;
3173
+ exports.TopBarBadge = TopBarBadge;
3123
3174
  exports.TopBarIcon = TopBarIcon;
3124
3175
  exports.UserMenu = UserMenu;
3125
3176
  exports.buildSpeculationRules = buildSpeculationRules;