@the-portland-company/shell 0.7.0 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 — 2026-07-12
4
+
5
+ ### Fixed (Chakra chrome — reconciled with main app)
6
+
7
+ - **`NavItem` re-synced to main-app `AuthenticatedLayout`**: removed the orange→red gradient hover sweep that had diverged from production chrome; restored gray hover, parent active when any child route matches (including Tabs Only), and the effectiveHref vs href-less accordion header split. `brandIcon` + `safeHref` shell additions retained.
8
+
9
+ ### Note for hosts
10
+
11
+ - Main VRM app now renders chrome exclusively via `ShellChrome` + `AppLayout` (no dual `AuthenticatedLayoutFrame` chrome path). Contacts already used this path — both apps should pin `^0.9.0`.
12
+
13
+ ## 0.8.0 — 2026-07-02
14
+
15
+ ### Added (backward-compatible)
16
+
17
+ - **Host-driven auth for non-Supabase apps**: `ShellProviderProps.supabaseClient` is now optional, and a new optional `auth: ShellInjectedAuth` prop (`{ status, user, signOut }`, exported type) takes precedence over Supabase session tracking. This lets OIDC apps (e.g. the standalone Contacts app) mount the full Chakra chrome — `ShellProvider` → `ShellChrome` → `AppLayout` — with their own identity feeding `useShellAuth`/`UserMenu`. The host builds a Supabase-shaped `user.raw` (`id`/`email`/`user_metadata.full_name`) so chrome components render identically. With neither prop, auth resolves to `unauthenticated`. Existing `supabaseClient` consumers are unaffected.
18
+
19
+ ### Tests
20
+
21
+ - Injected auth: precedence over Supabase (no `getSession`/`onAuthStateChange`/`auth.signOut` calls), client-less mounting, neither-prop fallback (3 tests).
22
+
3
23
  ## 0.7.0 — 2026-07-01
4
24
 
5
25
  ### Added (native chrome — all backward-compatible)
package/dist/index.cjs CHANGED
@@ -1126,6 +1126,7 @@ function toShellUser(session) {
1126
1126
  function ShellProvider({
1127
1127
  children,
1128
1128
  supabaseClient,
1129
+ auth,
1129
1130
  currentApp,
1130
1131
  appRegistry,
1131
1132
  basename,
@@ -1139,6 +1140,11 @@ function ShellProvider({
1139
1140
  const [status, setStatus] = react$1.useState("loading");
1140
1141
  const [session, setSession] = react$1.useState(null);
1141
1142
  react$1.useEffect(() => {
1143
+ if (auth || !supabaseClient) {
1144
+ setSession(null);
1145
+ if (!auth) setStatus("unauthenticated");
1146
+ return;
1147
+ }
1142
1148
  let cancelled = false;
1143
1149
  supabaseClient.auth.getSession().then(({ data }) => {
1144
1150
  if (cancelled) return;
@@ -1153,18 +1159,27 @@ function ShellProvider({
1153
1159
  cancelled = true;
1154
1160
  sub.data.subscription.unsubscribe();
1155
1161
  };
1156
- }, [supabaseClient]);
1157
- const value = react$1.useMemo(
1158
- () => ({
1162
+ }, [supabaseClient, auth]);
1163
+ const value = react$1.useMemo(() => {
1164
+ if (auth) {
1165
+ return {
1166
+ status: auth.status,
1167
+ session: null,
1168
+ user: auth.user,
1169
+ signOut: async () => {
1170
+ await auth.signOut();
1171
+ }
1172
+ };
1173
+ }
1174
+ return {
1159
1175
  status,
1160
1176
  session,
1161
1177
  user: toShellUser(session),
1162
1178
  signOut: async () => {
1163
- await supabaseClient.auth.signOut();
1179
+ await supabaseClient?.auth.signOut();
1164
1180
  }
1165
- }),
1166
- [status, session, supabaseClient]
1167
- );
1181
+ };
1182
+ }, [auth, status, session, supabaseClient]);
1168
1183
  const meta = react$1.useMemo(
1169
1184
  () => ({
1170
1185
  currentApp,
@@ -1558,8 +1573,7 @@ function BrandIcon({
1558
1573
  );
1559
1574
  }
1560
1575
  var BrandIcon_default = BrandIcon;
1561
- var MODE_CAMPAIGN = "campaign";
1562
- var MODE_PETITION = "petition";
1576
+ var safeHref = (h) => h && /^(https?:|mailto:|tel:|\/|#|\.)/i.test(h.trim()) ? h : "#";
1563
1577
  function NavItem({
1564
1578
  icon,
1565
1579
  brandIcon,
@@ -1580,23 +1594,9 @@ function NavItem({
1580
1594
  const location = reactRouterDom.useLocation();
1581
1595
  const navigate = reactRouterDom.useNavigate();
1582
1596
  const mode = useShellMode();
1583
- const currentMode = mode?.currentMode ?? null;
1584
1597
  const modeConfig = resolveModeConfig(mode);
1585
1598
  const navPref = useShellNavigationPreference();
1586
1599
  const showSubMenu = navPref?.showSubMenu ?? true;
1587
- const navHoverGradient = currentMode === MODE_CAMPAIGN ? "linear-gradient(90deg, #F2C94C 0%, #E63946 100%)" : currentMode === MODE_PETITION ? "linear-gradient(90deg, #F2C94C 0%, #007BFF 100%)" : "linear-gradient(90deg, #ea6a3a 0%, #E63946 100%)";
1588
- const gradientSweepBefore = {
1589
- content: '""',
1590
- position: "absolute",
1591
- top: 0,
1592
- left: 0,
1593
- bottom: 0,
1594
- width: "0%",
1595
- background: navHoverGradient,
1596
- transition: "width 300ms ease",
1597
- pointerEvents: "none",
1598
- zIndex: 0
1599
- };
1600
1600
  const isDisabled = Boolean(navIsDisabled || disabled);
1601
1601
  const effectiveHref = href || (Array.isArray(subItems) && subItems.length > 0 ? subItems[0].href : void 0);
1602
1602
  const effectivePathname = effectiveHref?.split("?")[0];
@@ -1606,6 +1606,7 @@ function NavItem({
1606
1606
  [location.search]
1607
1607
  );
1608
1608
  const matchesSubItem = (itemHref) => {
1609
+ if (!itemHref) return false;
1609
1610
  if (itemHref === currentFullPath) return true;
1610
1611
  const itemPath = itemHref.split("?")[0];
1611
1612
  const itemSearch = itemHref.includes("?") ? itemHref.split("?")[1] : "";
@@ -1649,83 +1650,130 @@ function NavItem({
1649
1650
  return effectivePathname !== `/${location.pathname.split("/")[1]}/dashboard` && location.pathname.startsWith(effectivePathname + "/");
1650
1651
  })();
1651
1652
  const hasSubItems = showSubMenu && Array.isArray(subItems) && subItems.length > 0;
1652
- const hasActiveChild = hasSubItems && subItems.some((item) => matchesSubItem(item.href || ""));
1653
+ const hasActiveChild = Array.isArray(subItems) && subItems.some((item) => matchesSubItem(item.href || ""));
1653
1654
  const isParentActive = isActive || hasActiveChild;
1654
1655
  const isSubMenuOpen = isExpanded !== void 0 ? isExpanded : hasSubItems && subItems.some((item) => matchesSubItem(item.href || ""));
1655
1656
  const expandedRowBg = isParentActive ? modeConfig?.primaryColor || "brand.500" : isSubMenuOpen ? "gray.100" : "transparent";
1656
1657
  const expandedRowTextColor = isParentActive ? "white" : isSubMenuOpen ? "gray.900" : "gray.600";
1657
1658
  const disabledRowBg = isSubItem ? "gray.50" : "transparent";
1658
1659
  const disabledRowTextColor = "gray.400";
1660
+ const renderIcon = (activeForHover) => {
1661
+ if (brandIcon) {
1662
+ return /* @__PURE__ */ jsxRuntime.jsx(
1663
+ react.Box,
1664
+ {
1665
+ mr: "4",
1666
+ display: "inline-flex",
1667
+ alignItems: "center",
1668
+ filter: isParentActive || isActive ? "brightness(0) invert(1)" : void 0,
1669
+ _groupHover: isDisabled ? void 0 : {
1670
+ filter: activeForHover ? "brightness(0) invert(1)" : void 0
1671
+ },
1672
+ children: /* @__PURE__ */ jsxRuntime.jsx(BrandIcon_default, { name: brandIcon, size: 18 })
1673
+ }
1674
+ );
1675
+ }
1676
+ if (icon) {
1677
+ return /* @__PURE__ */ jsxRuntime.jsx(
1678
+ react.Box,
1679
+ {
1680
+ mr: "4",
1681
+ fontSize: "16",
1682
+ _groupHover: {
1683
+ color: isDisabled ? disabledRowTextColor : activeForHover ? "white" : "gray.900"
1684
+ },
1685
+ as: icon
1686
+ }
1687
+ );
1688
+ }
1689
+ return null;
1690
+ };
1659
1691
  if (hasSubItems) {
1660
- const headerProps = {
1661
- align: "center",
1662
- px: "4",
1663
- py: "2.5",
1664
- mx: "4",
1665
- w: "auto",
1666
- borderTopRadius: "lg",
1667
- borderBottomRadius: isSubMenuOpen ? 0 : "lg",
1668
- role: "group",
1669
- cursor: isDisabled ? "not-allowed" : "pointer",
1670
- opacity: 1,
1671
- pointerEvents: isDisabled ? "none" : "auto",
1672
- "data-nav-item-active": String(isParentActive),
1673
- position: "relative",
1674
- overflow: "hidden",
1675
- bg: isDisabled ? disabledRowBg : expandedRowBg,
1676
- color: isDisabled ? disabledRowTextColor : expandedRowTextColor,
1677
- _before: isDisabled ? void 0 : gradientSweepBefore,
1678
- _hover: {
1679
- color: isDisabled ? disabledRowTextColor : "white",
1680
- _before: isDisabled ? void 0 : { width: "100%" }
1681
- },
1682
- "aria-disabled": isDisabled ? "true" : "false",
1683
- onClick: (e) => {
1684
- if (isDisabled) return;
1685
- if (effectiveHref) e.preventDefault();
1686
- e.stopPropagation();
1687
- onToggle?.(menuKey || null);
1688
- }
1689
- };
1690
1692
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { children: [
1691
- /* @__PURE__ */ jsxRuntime.jsxs(react.Flex, { ...headerProps, ...rest, children: [
1692
- brandIcon ? /* @__PURE__ */ jsxRuntime.jsx(
1693
- react.Box,
1694
- {
1695
- mr: "4",
1696
- display: "inline-flex",
1697
- alignItems: "center",
1698
- position: "relative",
1699
- zIndex: 1,
1700
- filter: isParentActive ? "brightness(0) invert(1)" : void 0,
1701
- _groupHover: isDisabled ? void 0 : { filter: "brightness(0) invert(1)" },
1702
- children: /* @__PURE__ */ jsxRuntime.jsx(BrandIcon_default, { name: brandIcon, size: 18 })
1703
- }
1704
- ) : icon ? /* @__PURE__ */ jsxRuntime.jsx(
1705
- react.Box,
1706
- {
1707
- mr: "4",
1708
- fontSize: "16",
1709
- position: "relative",
1710
- zIndex: 1,
1711
- _groupHover: {
1712
- color: isDisabled ? disabledRowTextColor : "white"
1713
- },
1714
- as: icon
1715
- }
1716
- ) : null,
1717
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { flex: "1", position: "relative", zIndex: 1, children }),
1718
- /* @__PURE__ */ jsxRuntime.jsx(
1719
- react.Box,
1720
- {
1721
- as: fi.FiChevronDown,
1722
- position: "relative",
1723
- zIndex: 1,
1724
- transform: isSubMenuOpen ? "rotate(180deg)" : void 0,
1725
- transition: "transform 0.2s"
1726
- }
1727
- )
1728
- ] }),
1693
+ effectiveHref ? /* @__PURE__ */ jsxRuntime.jsxs(
1694
+ react.Flex,
1695
+ {
1696
+ align: "center",
1697
+ px: "4",
1698
+ py: "2.5",
1699
+ mx: "4",
1700
+ w: "auto",
1701
+ borderTopRadius: "lg",
1702
+ borderBottomRadius: isSubMenuOpen ? 0 : "lg",
1703
+ role: "group",
1704
+ cursor: isDisabled ? "not-allowed" : "pointer",
1705
+ opacity: 1,
1706
+ pointerEvents: isDisabled ? "none" : "auto",
1707
+ "data-nav-item-active": String(isParentActive),
1708
+ bg: isDisabled ? disabledRowBg : expandedRowBg,
1709
+ color: isDisabled ? disabledRowTextColor : expandedRowTextColor,
1710
+ _hover: {
1711
+ bg: isDisabled ? disabledRowBg : isParentActive ? modeConfig?.accentColor || "brand.600" : "gray.200",
1712
+ color: isDisabled ? disabledRowTextColor : isParentActive ? "white" : "gray.900"
1713
+ },
1714
+ "aria-disabled": isDisabled ? "true" : "false",
1715
+ onClick: (e) => {
1716
+ if (isDisabled) return;
1717
+ e.preventDefault();
1718
+ e.stopPropagation();
1719
+ onToggle?.(menuKey || null);
1720
+ },
1721
+ ...rest,
1722
+ children: [
1723
+ renderIcon(isParentActive),
1724
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { flex: "1", children }),
1725
+ /* @__PURE__ */ jsxRuntime.jsx(
1726
+ react.Box,
1727
+ {
1728
+ as: fi.FiChevronDown,
1729
+ transform: isSubMenuOpen ? "rotate(180deg)" : void 0,
1730
+ transition: "transform 0.2s"
1731
+ }
1732
+ )
1733
+ ]
1734
+ }
1735
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
1736
+ react.Flex,
1737
+ {
1738
+ align: "center",
1739
+ px: "4",
1740
+ py: "2.5",
1741
+ mx: "4",
1742
+ w: "auto",
1743
+ borderTopRadius: "lg",
1744
+ borderBottomRadius: isSubMenuOpen ? 0 : "lg",
1745
+ role: "group",
1746
+ cursor: isDisabled ? "not-allowed" : "pointer",
1747
+ opacity: 1,
1748
+ pointerEvents: isDisabled ? "none" : "auto",
1749
+ "data-nav-item-active": String(isParentActive),
1750
+ bg: isDisabled ? disabledRowBg : expandedRowBg,
1751
+ color: isDisabled ? disabledRowTextColor : expandedRowTextColor,
1752
+ _hover: {
1753
+ bg: isDisabled ? disabledRowBg : hasActiveChild ? modeConfig?.accentColor || "brand.600" : "gray.200",
1754
+ color: isDisabled ? disabledRowTextColor : hasActiveChild ? "white" : "gray.900"
1755
+ },
1756
+ "aria-disabled": isDisabled ? "true" : "false",
1757
+ onClick: (e) => {
1758
+ if (isDisabled) return;
1759
+ e.stopPropagation();
1760
+ onToggle?.(menuKey || null);
1761
+ },
1762
+ ...rest,
1763
+ children: [
1764
+ renderIcon(hasActiveChild),
1765
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { flex: "1", children }),
1766
+ /* @__PURE__ */ jsxRuntime.jsx(
1767
+ react.Box,
1768
+ {
1769
+ as: fi.FiChevronDown,
1770
+ transform: isSubMenuOpen ? "rotate(180deg)" : void 0,
1771
+ transition: "transform 0.2s"
1772
+ }
1773
+ )
1774
+ ]
1775
+ }
1776
+ ),
1729
1777
  /* @__PURE__ */ jsxRuntime.jsx(react.Collapse, { in: isSubMenuOpen, animateOpacity: true, children: /* @__PURE__ */ jsxRuntime.jsx(
1730
1778
  react.Box,
1731
1779
  {
@@ -1769,74 +1817,68 @@ function NavItem({
1769
1817
  navigate(effectiveHref);
1770
1818
  }
1771
1819
  };
1772
- return /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "a", href: effectiveHref || "#", onClick: handleClick, style: { textDecoration: "none" }, children: /* @__PURE__ */ jsxRuntime.jsxs(
1773
- react.Flex,
1820
+ const rowActive = isSubItem ? isActive : isParentActive;
1821
+ return /* @__PURE__ */ jsxRuntime.jsx(
1822
+ react.Box,
1774
1823
  {
1775
- align: "center",
1776
- px: "4",
1777
- py: "2.5",
1778
- mx: isSubItem ? "0" : "4",
1779
- w: isSubItem ? "full" : "auto",
1780
- borderRadius: isSubItem ? "0" : "lg",
1781
- role: "group",
1782
- cursor: isDisabled ? "not-allowed" : "pointer",
1783
- opacity: 1,
1784
- pointerEvents: isDisabled ? "none" : "auto",
1785
- "data-nav-item-active": String(isActive),
1786
- position: "relative",
1787
- overflow: "hidden",
1788
- bg: isDisabled ? disabledRowBg : isActive ? isSubItem ? "gray.100" : modeConfig?.primaryColor || "brand.500" : "transparent",
1789
- color: isDisabled ? disabledRowTextColor : isActive ? isSubItem ? modeConfig?.primaryColor || "brand.500" : "white" : "gray.600",
1790
- _before: isDisabled ? void 0 : gradientSweepBefore,
1791
- _hover: {
1792
- color: isDisabled ? disabledRowTextColor : "white",
1793
- _before: isDisabled ? void 0 : { width: "100%" }
1794
- },
1795
- "aria-disabled": isDisabled ? "true" : "false",
1824
+ as: "a",
1825
+ href: safeHref(effectiveHref),
1796
1826
  onClick: handleClick,
1797
- children: [
1798
- brandIcon ? /* @__PURE__ */ jsxRuntime.jsx(
1799
- react.Box,
1800
- {
1801
- mr: "4",
1802
- display: "inline-flex",
1803
- alignItems: "center",
1804
- position: "relative",
1805
- zIndex: 1,
1806
- filter: isActive && !isSubItem ? "brightness(0) invert(1)" : void 0,
1807
- _groupHover: isDisabled ? void 0 : { filter: "brightness(0) invert(1)" },
1808
- children: /* @__PURE__ */ jsxRuntime.jsx(BrandIcon_default, { name: brandIcon, size: 18 })
1809
- }
1810
- ) : icon ? /* @__PURE__ */ jsxRuntime.jsx(
1811
- react.Box,
1812
- {
1813
- mr: "4",
1814
- fontSize: "16",
1815
- position: "relative",
1816
- zIndex: 1,
1817
- _groupHover: {
1818
- color: isDisabled ? disabledRowTextColor : "white"
1819
- },
1820
- as: icon
1821
- }
1822
- ) : null,
1823
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { flex: "1", position: "relative", zIndex: 1, children: [
1824
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { children }),
1825
- subLabel ? /* @__PURE__ */ jsxRuntime.jsx(
1826
- react.Text,
1827
- {
1828
- mt: 0.5,
1829
- fontSize: "10px",
1830
- lineHeight: "1.3",
1831
- color: isDisabled ? "gray.400" : "gray.400",
1832
- children: subLabel
1833
- }
1834
- ) : null,
1835
- badge && /* @__PURE__ */ jsxRuntime.jsx(react.Badge, { colorScheme: "orange", size: "xs", mt: 1, fontSize: "xs", display: "block", children: badge })
1836
- ] })
1837
- ]
1827
+ style: { textDecoration: "none" },
1828
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1829
+ react.Flex,
1830
+ {
1831
+ align: "center",
1832
+ px: "4",
1833
+ py: "2.5",
1834
+ mx: isSubItem ? "0" : "4",
1835
+ w: isSubItem ? "full" : "auto",
1836
+ borderRadius: isSubItem ? "0" : "lg",
1837
+ role: "group",
1838
+ cursor: isDisabled ? "not-allowed" : "pointer",
1839
+ opacity: 1,
1840
+ pointerEvents: isDisabled ? "none" : "auto",
1841
+ "data-nav-item-active": String(rowActive),
1842
+ bg: isDisabled ? disabledRowBg : rowActive ? isSubItem ? "gray.100" : modeConfig?.primaryColor || "brand.500" : "transparent",
1843
+ color: isDisabled ? disabledRowTextColor : rowActive ? isSubItem ? modeConfig?.primaryColor || "brand.500" : "white" : "gray.600",
1844
+ _hover: {
1845
+ bg: isDisabled ? disabledRowBg : rowActive ? isSubItem ? "gray.200" : modeConfig?.accentColor || "brand.600" : "gray.100",
1846
+ color: isDisabled ? disabledRowTextColor : rowActive ? isSubItem ? modeConfig?.primaryColor || "brand.500" : "white" : "gray.900"
1847
+ },
1848
+ "aria-disabled": isDisabled ? "true" : "false",
1849
+ onClick: handleClick,
1850
+ ...rest,
1851
+ children: [
1852
+ brandIcon ? /* @__PURE__ */ jsxRuntime.jsx(
1853
+ react.Box,
1854
+ {
1855
+ mr: "4",
1856
+ display: "inline-flex",
1857
+ alignItems: "center",
1858
+ filter: rowActive && !isSubItem ? "brightness(0) invert(1)" : void 0,
1859
+ children: /* @__PURE__ */ jsxRuntime.jsx(BrandIcon_default, { name: brandIcon, size: 18 })
1860
+ }
1861
+ ) : icon ? /* @__PURE__ */ jsxRuntime.jsx(
1862
+ react.Box,
1863
+ {
1864
+ mr: "4",
1865
+ fontSize: "16",
1866
+ _groupHover: {
1867
+ color: isDisabled ? disabledRowTextColor : rowActive ? isSubItem ? modeConfig?.primaryColor || "brand.500" : "white" : "gray.900"
1868
+ },
1869
+ as: icon
1870
+ }
1871
+ ) : null,
1872
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { flex: "1", children: [
1873
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { children }),
1874
+ subLabel ? /* @__PURE__ */ jsxRuntime.jsx(react.Text, { mt: 0.5, fontSize: "10px", lineHeight: "1.3", color: "gray.400", children: subLabel }) : null,
1875
+ badge && /* @__PURE__ */ jsxRuntime.jsx(react.Badge, { colorScheme: "orange", size: "xs", mt: 1, fontSize: "xs", display: "block", children: badge })
1876
+ ] })
1877
+ ]
1878
+ }
1879
+ )
1838
1880
  }
1839
- ) });
1881
+ );
1840
1882
  }
1841
1883
  var NavItem_default = NavItem;
1842
1884