@the-portland-company/shell 0.36.2 → 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 : "");
@@ -2688,6 +2762,7 @@ function watchLogout(onLogout, options = {}) {
2688
2762
  return cleanup;
2689
2763
  }
2690
2764
  var CORE_APP = "vrm";
2765
+ var DEFAULT_SIGN_OUT_URL = "/auth/signout";
2691
2766
  var POLITOGY_APPS = {
2692
2767
  vrm: { title: "Politogy VRM", origin: "https://app.politogyvrm.com", storagePrefix: "vrm" },
2693
2768
  contacts: { title: "Contacts", origin: "https://contacts.politogyvrm.com", storagePrefix: "contacts" },
@@ -2715,6 +2790,7 @@ function PolitogyAppFrame({
2715
2790
  linkComponent,
2716
2791
  session,
2717
2792
  onSignOut,
2793
+ signOutUrl = DEFAULT_SIGN_OUT_URL,
2718
2794
  buildMeta,
2719
2795
  data,
2720
2796
  children
@@ -2742,6 +2818,7 @@ function PolitogyAppFrame({
2742
2818
  linkComponent,
2743
2819
  session,
2744
2820
  onSignOut,
2821
+ signOutUrl,
2745
2822
  buildMeta,
2746
2823
  data,
2747
2824
  children
@@ -2757,6 +2834,7 @@ function FrameBody({
2757
2834
  linkComponent,
2758
2835
  session,
2759
2836
  onSignOut,
2837
+ signOutUrl,
2760
2838
  buildMeta,
2761
2839
  data,
2762
2840
  children
@@ -2770,14 +2848,12 @@ function FrameBody({
2770
2848
  signingOutRef.current = true;
2771
2849
  broadcastLogout();
2772
2850
  signOutRef.current();
2773
- window.setTimeout(() => {
2774
- window.location.replace(window.location.href);
2775
- }, 2e3);
2776
- }, []);
2851
+ window.location.assign(signOutUrl);
2852
+ }, [signOutUrl]);
2777
2853
  react.useEffect(
2778
2854
  () => (
2779
- // A tab that learns of a sign-out elsewhere forces a real NAVIGATION
2780
- // rather than calling the app's own sign-out handler.
2855
+ // A tab that learns of a sign-out elsewhere ends its own session for
2856
+ // real, by navigating to the same sign-out route.
2781
2857
  //
2782
2858
  // Measured against production before this landed: calling the app's
2783
2859
  // handler did nothing at all. Apps wire it as
@@ -2794,10 +2870,10 @@ function FrameBody({
2794
2870
  // already on record for every tab.
2795
2871
  watchLogout(() => {
2796
2872
  if (signingOutRef.current) return;
2797
- window.location.replace(window.location.href);
2873
+ window.location.assign(signOutUrl);
2798
2874
  })
2799
2875
  ),
2800
- []
2876
+ [signOutUrl]
2801
2877
  );
2802
2878
  const core = app === CORE_APP ? data?.core : void 0;
2803
2879
  const identity = useChromeIdentity({ userName: session.userName, email: session.email });
@@ -2925,7 +3001,6 @@ var standardNavIcons = {
2925
3001
  };
2926
3002
  var ORG_STORAGE_KEY = "vrm.activeOrg";
2927
3003
  var MODE_STORAGE_KEY = "vrm.activeMode";
2928
- var NAV_DISPLAY_KEY = "vrm.navDisplay";
2929
3004
  var CORE_BASE_URL = "https://app.politogyvrm.com";
2930
3005
  var ChromeStateContext = react.createContext(null);
2931
3006
  function useChromeState() {
@@ -2936,18 +3011,46 @@ function ChromeStateProvider({
2936
3011
  children
2937
3012
  }) {
2938
3013
  const { orgs, loading: orgsFetching, error: orgsError } = useChromeOrgs();
3014
+ const chromeMe = useChromeMe();
3015
+ const chromeFetch = useNavChromeFetcher();
2939
3016
  const [orgId, setOrgIdState] = react.useState(null);
2940
- const [navDisplay, setNavDisplayState] = react.useState("both");
3017
+ const [navDisplay, setNavDisplayState] = react.useState(NAV_DISPLAY_DEFAULT);
3018
+ const navReconciled = react.useRef(false);
2941
3019
  react.useEffect(() => {
2942
3020
  try {
2943
3021
  const storedOrg = resolveActiveOrg() ?? localStorage.getItem(ORG_STORAGE_KEY);
2944
3022
  if (storedOrg) setOrgIdState(storedOrg);
2945
- const storedNav = localStorage.getItem(NAV_DISPLAY_KEY);
2946
- if (storedNav === "menu" || storedNav === "tabs" || storedNav === "both") {
2947
- setNavDisplayState(storedNav);
2948
- }
2949
3023
  } catch {
2950
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
+ };
2951
3054
  }, []);
2952
3055
  react.useEffect(() => {
2953
3056
  if (!orgs || orgs.length === 0) return;
@@ -2961,13 +3064,20 @@ function ChromeStateProvider({
2961
3064
  }
2962
3065
  writeActiveOrgCookie(id);
2963
3066
  }, []);
2964
- const setNavDisplay = react.useCallback((m) => {
2965
- setNavDisplayState(m);
2966
- try {
2967
- localStorage.setItem(NAV_DISPLAY_KEY, m);
2968
- } catch {
2969
- }
2970
- }, []);
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
+ );
2971
3081
  const value = react.useMemo(() => {
2972
3082
  if (controlledOrg) {
2973
3083
  return {