@the-portland-company/shell 0.36.3 → 0.37.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,3 +1,21 @@
1
+ ## 0.37.0 — 2026-07-27
2
+
3
+ ### Fixed — organization logo renders in every app, not just VRM core
4
+
5
+ `organizations.logo_url` is authored by VRM core and stored as a ROOT-RELATIVE
6
+ core route (`/api/storage/public?bucket=organization-logos&path=<org>/logo.png`
7
+ — core's signed-storage proxy). VRM core rendered it fine because the path
8
+ resolved against its own origin; every feature app resolved the same path
9
+ against ITS origin (`forms.politogyvrm.com/api/storage/public`), 404'd, and
10
+ showed a broken image in the sidebar org pill and the org dropdown.
11
+
12
+ `resolveOrgLogoUrl` (exported from `./native`) now absolutizes a relative logo
13
+ path against `CORE_APP_ORIGIN` at the point of render, for all three org-badge
14
+ sites (active pill, dropdown rows, non-switcher pill). Absolute, protocol-
15
+ relative and `data:` URLs pass through untouched; empty values still fall back
16
+ to initials. This is the same treatment `absolutizeAvatarUrl` already applied to
17
+ `profiles.avatar_url` in the chrome API — the org logo was simply missed.
18
+
1
19
  ## 0.36.0 — 2026-07-24
2
20
 
3
21
  ### Added — cross-subdomain active-org + favicon cache-busting
@@ -150,6 +150,12 @@ var CORE_APP_ORIGIN = "https://app.politogyvrm.com";
150
150
  var CORE_BILLING_PATH = "/organization-settings/subscription-billing/text-email";
151
151
  var CORE_BILLING_URL = `${CORE_APP_ORIGIN}${CORE_BILLING_PATH}`;
152
152
  var QUARANTINED_CONTACTS_URL = `${CORE_APP_ORIGIN}/relationship/contacts/quarantined`;
153
+ function resolveOrgLogoUrl(url) {
154
+ const raw = url?.trim();
155
+ if (!raw) return void 0;
156
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(raw)) return raw;
157
+ return `${CORE_APP_ORIGIN}${raw.startsWith("/") ? "" : "/"}${raw}`;
158
+ }
153
159
  function topBarIconButtonClass(tone = "default") {
154
160
  const toneClass = tone === "danger" || tone === "warning" ? "text-[color:var(--color-race-red)] hover:text-[color:var(--color-race-red)]" : "text-[color:var(--topbar-icon-color,var(--muted))] hover:text-[color:var(--topbar-icon-hover-color,var(--foreground))] hover:bg-[color:var(--topbar-icon-hover-bg,transparent)]";
155
161
  return [
@@ -1588,6 +1594,7 @@ function NavSidebar({
1588
1594
  const activeFn = nav.isActive ?? isNavItemActive;
1589
1595
  const ModeIcon = mode?.icon;
1590
1596
  const orgBg = org?.brandColor || ORG_PILL_DEFAULT;
1597
+ const orgLogoSrc = resolveOrgLogoUrl(org?.logoUrl);
1591
1598
  const activeGroupKey = react.useMemo(() => {
1592
1599
  for (let i = 0; i < nav.items.length; i++) {
1593
1600
  const item = nav.items[i];
@@ -1655,7 +1662,7 @@ function NavSidebar({
1655
1662
  {
1656
1663
  style: { backgroundColor: orgBg },
1657
1664
  className: "flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-full text-[11px] font-bold text-white",
1658
- children: org.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: org.logoUrl, alt: "", className: "h-full w-full object-cover" }) : (org.name ?? "Org").slice(0, 2).toUpperCase()
1665
+ children: orgLogoSrc ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: orgLogoSrc, alt: "", className: "h-full w-full object-cover" }) : (org.name ?? "Org").slice(0, 2).toUpperCase()
1659
1666
  }
1660
1667
  ),
1661
1668
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1690,7 +1697,14 @@ function NavSidebar({
1690
1697
  {
1691
1698
  style: { backgroundColor: o.brandColor || ORG_PILL_DEFAULT },
1692
1699
  className: "flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-full text-[10px] font-bold text-white",
1693
- children: o.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: o.logoUrl, alt: "", className: "h-full w-full object-cover" }) : (o.name ?? "Org").slice(0, 2).toUpperCase()
1700
+ children: resolveOrgLogoUrl(o.logoUrl) ? /* @__PURE__ */ jsxRuntime.jsx(
1701
+ "img",
1702
+ {
1703
+ src: resolveOrgLogoUrl(o.logoUrl),
1704
+ alt: "",
1705
+ className: "h-full w-full object-cover"
1706
+ }
1707
+ ) : (o.name ?? "Org").slice(0, 2).toUpperCase()
1694
1708
  }
1695
1709
  ),
1696
1710
  /* @__PURE__ */ jsxRuntime.jsx("span", { title: o.name ?? o.id, className: "truncate", children: o.name ?? o.id.slice(0, 8) })
@@ -1712,7 +1726,7 @@ function NavSidebar({
1712
1726
  {
1713
1727
  style: { backgroundColor: orgBg },
1714
1728
  className: "flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-full text-[11px] font-bold text-white",
1715
- children: org.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: org.logoUrl, alt: "", className: "h-full w-full object-cover" }) : (org.name ?? "Org").slice(0, 2).toUpperCase()
1729
+ children: orgLogoSrc ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: orgLogoSrc, alt: "", className: "h-full w-full object-cover" }) : (org.name ?? "Org").slice(0, 2).toUpperCase()
1716
1730
  }
1717
1731
  ),
1718
1732
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-sm font-semibold", children: org.name ?? "No organizations" })
@@ -2535,6 +2549,66 @@ function resolveActiveOrg(search) {
2535
2549
  return readActiveOrgCookie();
2536
2550
  }
2537
2551
 
2552
+ // src/nav-preference.ts
2553
+ var NAV_DISPLAY_STORAGE_KEY = "politogy_nav_display";
2554
+ var NAV_DISPLAY_COOKIE = "politogy_nav_display";
2555
+ var LEGACY_NAV_DISPLAY_KEY = "vrm.navDisplay";
2556
+ var NAV_DISPLAY_DEFAULT = "both";
2557
+ var VALID2 = ["menu", "tabs", "both"];
2558
+ function isNavDisplayPreference(v) {
2559
+ return typeof v === "string" && VALID2.includes(v);
2560
+ }
2561
+ function readNavDisplayCookie() {
2562
+ if (typeof document === "undefined") return null;
2563
+ try {
2564
+ const m = document.cookie.match(
2565
+ new RegExp(`(?:^|;\\s*)${NAV_DISPLAY_COOKIE}=([^;]+)`)
2566
+ );
2567
+ const v = m ? decodeURIComponent(m[1]) : null;
2568
+ return isNavDisplayPreference(v) ? v : null;
2569
+ } catch {
2570
+ return null;
2571
+ }
2572
+ }
2573
+ function writeNavDisplayCookie(pref) {
2574
+ if (typeof document === "undefined" || typeof window === "undefined") return;
2575
+ const host = window.location.hostname;
2576
+ const domainAttr = /(^|\.)politogyvrm\.com$/i.test(host) ? "; Domain=.politogyvrm.com" : "";
2577
+ const secure = window.location.protocol === "https:" ? "; Secure" : "";
2578
+ document.cookie = `${NAV_DISPLAY_COOKIE}=${encodeURIComponent(
2579
+ pref
2580
+ )}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`;
2581
+ }
2582
+ function readNavDisplayLocal() {
2583
+ if (typeof localStorage === "undefined") return null;
2584
+ try {
2585
+ const current = localStorage.getItem(NAV_DISPLAY_STORAGE_KEY);
2586
+ if (isNavDisplayPreference(current)) return current;
2587
+ const legacy = localStorage.getItem(LEGACY_NAV_DISPLAY_KEY);
2588
+ return isNavDisplayPreference(legacy) ? legacy : null;
2589
+ } catch {
2590
+ return null;
2591
+ }
2592
+ }
2593
+ function writeNavDisplayLocal(pref) {
2594
+ if (typeof localStorage === "undefined") return;
2595
+ try {
2596
+ localStorage.setItem(NAV_DISPLAY_STORAGE_KEY, pref);
2597
+ } catch {
2598
+ }
2599
+ }
2600
+ function resolveNavDisplay() {
2601
+ return readNavDisplayCookie() ?? readNavDisplayLocal() ?? NAV_DISPLAY_DEFAULT;
2602
+ }
2603
+ `(function(){try{
2604
+ var KEY=${JSON.stringify(NAV_DISPLAY_STORAGE_KEY)};
2605
+ var VALID=["menu","tabs","both"];
2606
+ function fromCookie(){try{var m=document.cookie.match(/(?:^|;\\s*)${NAV_DISPLAY_COOKIE}=([^;]+)/);return m?decodeURIComponent(m[1]):null;}catch(e){return null;}}
2607
+ var cookie=fromCookie();
2608
+ if(cookie&&VALID.indexOf(cookie)!==-1){var ls=null;try{ls=localStorage.getItem(KEY);}catch(e){}
2609
+ if(cookie!==ls){try{localStorage.setItem(KEY,cookie);}catch(e){}}}
2610
+ }catch(e){}})();`;
2611
+
2538
2612
  // src/lib/breadcrumbs.ts
2539
2613
  function deriveBreadcrumbs(items, pathname, opts) {
2540
2614
  const search = opts?.search ?? (typeof window !== "undefined" ? window.location.search : "");
@@ -2927,7 +3001,6 @@ var standardNavIcons = {
2927
3001
  };
2928
3002
  var ORG_STORAGE_KEY = "vrm.activeOrg";
2929
3003
  var MODE_STORAGE_KEY = "vrm.activeMode";
2930
- var NAV_DISPLAY_KEY = "vrm.navDisplay";
2931
3004
  var CORE_BASE_URL = "https://app.politogyvrm.com";
2932
3005
  var ChromeStateContext = react.createContext(null);
2933
3006
  function useChromeState() {
@@ -2938,18 +3011,46 @@ function ChromeStateProvider({
2938
3011
  children
2939
3012
  }) {
2940
3013
  const { orgs, loading: orgsFetching, error: orgsError } = useChromeOrgs();
3014
+ const chromeMe = useChromeMe();
3015
+ const chromeFetch = useNavChromeFetcher();
2941
3016
  const [orgId, setOrgIdState] = react.useState(null);
2942
- const [navDisplay, setNavDisplayState] = react.useState("both");
3017
+ const [navDisplay, setNavDisplayState] = react.useState(NAV_DISPLAY_DEFAULT);
3018
+ const navReconciled = react.useRef(false);
2943
3019
  react.useEffect(() => {
2944
3020
  try {
2945
3021
  const storedOrg = resolveActiveOrg() ?? localStorage.getItem(ORG_STORAGE_KEY);
2946
3022
  if (storedOrg) setOrgIdState(storedOrg);
2947
- const storedNav = localStorage.getItem(NAV_DISPLAY_KEY);
2948
- if (storedNav === "menu" || storedNav === "tabs" || storedNav === "both") {
2949
- setNavDisplayState(storedNav);
2950
- }
2951
3023
  } catch {
2952
3024
  }
3025
+ setNavDisplayState(resolveNavDisplay());
3026
+ }, []);
3027
+ react.useEffect(() => {
3028
+ if (navReconciled.current || !chromeMe.me) return;
3029
+ const pref = chromeMe.me.navDisplay;
3030
+ if (!isNavDisplayPreference(pref)) return;
3031
+ navReconciled.current = true;
3032
+ setNavDisplayState(pref);
3033
+ writeNavDisplayCookie(pref);
3034
+ writeNavDisplayLocal(pref);
3035
+ }, [chromeMe]);
3036
+ react.useEffect(() => {
3037
+ const onStorage = (e) => {
3038
+ if (e.key === NAV_DISPLAY_STORAGE_KEY && isNavDisplayPreference(e.newValue)) {
3039
+ setNavDisplayState(e.newValue);
3040
+ }
3041
+ };
3042
+ const onWake = () => {
3043
+ const cookie = readNavDisplayCookie();
3044
+ if (cookie) setNavDisplayState(cookie);
3045
+ };
3046
+ window.addEventListener("storage", onStorage);
3047
+ window.addEventListener("focus", onWake);
3048
+ document.addEventListener("visibilitychange", onWake);
3049
+ return () => {
3050
+ window.removeEventListener("storage", onStorage);
3051
+ window.removeEventListener("focus", onWake);
3052
+ document.removeEventListener("visibilitychange", onWake);
3053
+ };
2953
3054
  }, []);
2954
3055
  react.useEffect(() => {
2955
3056
  if (!orgs || orgs.length === 0) return;
@@ -2963,13 +3064,20 @@ function ChromeStateProvider({
2963
3064
  }
2964
3065
  writeActiveOrgCookie(id);
2965
3066
  }, []);
2966
- const setNavDisplay = react.useCallback((m) => {
2967
- setNavDisplayState(m);
2968
- try {
2969
- localStorage.setItem(NAV_DISPLAY_KEY, m);
2970
- } catch {
2971
- }
2972
- }, []);
3067
+ const setNavDisplay = react.useCallback(
3068
+ (m) => {
3069
+ setNavDisplayState(m);
3070
+ writeNavDisplayLocal(m);
3071
+ writeNavDisplayCookie(m);
3072
+ chromeFetch?.("me/preferences", {
3073
+ method: "PATCH",
3074
+ headers: { "Content-Type": "application/json" },
3075
+ body: JSON.stringify({ navDisplay: m })
3076
+ })?.catch(() => {
3077
+ });
3078
+ },
3079
+ [chromeFetch]
3080
+ );
2973
3081
  const value = react.useMemo(() => {
2974
3082
  if (controlledOrg) {
2975
3083
  return {