@the-portland-company/shell 0.36.0 → 0.36.2

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.
@@ -2154,116 +2154,118 @@ function NavTopBar({
2154
2154
  }
2155
2155
  ) })
2156
2156
  ] }),
2157
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-0 z-30 mt-2 w-72 rounded-xl border border-[color:var(--line)] bg-[color:var(--panel-elevated)] py-1 shadow-[var(--shadow-soft)]", children: [
2157
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-0 z-30 mt-2 flex max-h-[calc(100vh-5rem)] w-72 flex-col overflow-hidden rounded-xl border border-[color:var(--line)] bg-[color:var(--panel-elevated)] py-1 shadow-[var(--shadow-soft)]", children: [
2158
2158
  /* @__PURE__ */ jsxRuntime.jsx(TopBarPanelArrow, {}),
2159
- userMenu?.myProfileHref ? /* @__PURE__ */ jsxRuntime.jsxs(
2160
- "a",
2161
- {
2162
- href: safeHref(userMenu.myProfileHref),
2163
- className: "flex w-full items-center gap-2.5 px-4 py-2.5 text-left text-sm font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)]",
2164
- children: [
2165
- /* @__PURE__ */ jsxRuntime.jsx(ProfileIcon, {}),
2166
- "My Profile"
2167
- ]
2168
- }
2169
- ) : null,
2170
- userMenu?.organizationSettingsHref ? /* @__PURE__ */ jsxRuntime.jsxs(
2171
- "a",
2172
- {
2173
- href: safeHref(userMenu.organizationSettingsHref),
2174
- className: "flex w-full items-center gap-2.5 border-t border-[color:var(--line)] px-4 py-2.5 text-left text-sm font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)]",
2175
- children: [
2176
- /* @__PURE__ */ jsxRuntime.jsx(BriefcaseIcon, {}),
2177
- "Organization Settings"
2178
- ]
2179
- }
2180
- ) : null,
2181
- userMenu?.roles && userMenu.roles.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-[color:var(--line)] px-4 py-3", children: [
2182
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Roles & Organizations" }),
2183
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-1.5", children: userMenu.roles.map((r, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex flex-wrap items-center gap-1.5 text-sm", children: [
2184
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-md bg-[color:var(--panel)] px-2 py-0.5 text-xs font-bold uppercase tracking-wide text-[color:var(--foreground)]", children: r.role }),
2185
- r.orgName ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[color:var(--muted)]", children: [
2186
- "in ",
2187
- r.orgName
2188
- ] }) : null
2189
- ] }, `${r.role}-${i}`)) })
2190
- ] }) : null,
2191
- userMenu?.accounts && userMenu.accounts.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-[color:var(--line)] px-4 py-3", children: [
2192
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Accounts" }),
2193
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-1", children: userMenu.accounts.map((a) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex items-center gap-2.5", children: [
2194
- /* @__PURE__ */ jsxRuntime.jsxs(
2195
- "button",
2159
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain", children: [
2160
+ userMenu?.myProfileHref ? /* @__PURE__ */ jsxRuntime.jsxs(
2161
+ "a",
2162
+ {
2163
+ href: safeHref(userMenu.myProfileHref),
2164
+ className: "flex w-full items-center gap-2.5 px-4 py-2.5 text-left text-sm font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)]",
2165
+ children: [
2166
+ /* @__PURE__ */ jsxRuntime.jsx(ProfileIcon, {}),
2167
+ "My Profile"
2168
+ ]
2169
+ }
2170
+ ) : null,
2171
+ userMenu?.organizationSettingsHref ? /* @__PURE__ */ jsxRuntime.jsxs(
2172
+ "a",
2173
+ {
2174
+ href: safeHref(userMenu.organizationSettingsHref),
2175
+ className: "flex w-full items-center gap-2.5 border-t border-[color:var(--line)] px-4 py-2.5 text-left text-sm font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)]",
2176
+ children: [
2177
+ /* @__PURE__ */ jsxRuntime.jsx(BriefcaseIcon, {}),
2178
+ "Organization Settings"
2179
+ ]
2180
+ }
2181
+ ) : null,
2182
+ userMenu?.roles && userMenu.roles.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-[color:var(--line)] px-4 py-3", children: [
2183
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Roles & Organizations" }),
2184
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-1.5", children: userMenu.roles.map((r, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex flex-wrap items-center gap-1.5 text-sm", children: [
2185
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-md bg-[color:var(--panel)] px-2 py-0.5 text-xs font-bold uppercase tracking-wide text-[color:var(--foreground)]", children: r.role }),
2186
+ r.orgName ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[color:var(--muted)]", children: [
2187
+ "in ",
2188
+ r.orgName
2189
+ ] }) : null
2190
+ ] }, `${r.role}-${i}`)) })
2191
+ ] }) : null,
2192
+ userMenu?.accounts && userMenu.accounts.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-[color:var(--line)] px-4 py-3", children: [
2193
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Accounts" }),
2194
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-1", children: userMenu.accounts.map((a) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex items-center gap-2.5", children: [
2195
+ /* @__PURE__ */ jsxRuntime.jsxs(
2196
+ "button",
2197
+ {
2198
+ type: "button",
2199
+ onClick: () => {
2200
+ if (!a.active) userMenu.onSwitchAccount?.(a.id);
2201
+ },
2202
+ className: "flex min-w-0 flex-1 items-center gap-2.5 rounded-lg px-1 py-1 text-left hover:bg-[color:var(--panel)]",
2203
+ children: [
2204
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-[color:var(--color-race-red)] text-[11px] font-bold text-white", children: a.avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: a.avatarUrl, alt: "", className: "h-full w-full object-cover" }) : (a.name ?? a.email ?? "?").slice(0, 2).toUpperCase() }),
2205
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col leading-tight", children: [
2206
+ /* @__PURE__ */ jsxRuntime.jsx(
2207
+ "span",
2208
+ {
2209
+ className: [
2210
+ "truncate text-sm font-semibold",
2211
+ a.active ? "text-[color:var(--color-race-red)]" : "text-[color:var(--foreground)]"
2212
+ ].join(" "),
2213
+ children: a.name ?? a.email ?? a.id.slice(0, 8)
2214
+ }
2215
+ ),
2216
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-xs text-[color:var(--muted)]", children: a.active ? "Active" : a.email ?? "" })
2217
+ ] })
2218
+ ]
2219
+ }
2220
+ ),
2221
+ !a.active && userMenu.onRemoveAccount ? /* @__PURE__ */ jsxRuntime.jsx(
2222
+ "button",
2223
+ {
2224
+ type: "button",
2225
+ "aria-label": `Remove ${a.name ?? a.email ?? "account"}`,
2226
+ onClick: () => userMenu.onRemoveAccount?.(a.id),
2227
+ className: "shrink-0 rounded p-1 text-[color:var(--color-race-red)] hover:bg-[color:var(--panel)]",
2228
+ children: /* @__PURE__ */ jsxRuntime.jsx(TrashIcon, {})
2229
+ }
2230
+ ) : null
2231
+ ] }, a.id)) }),
2232
+ userMenu.addAccountHref || userMenu.onAddAccount ? userMenu.addAccountHref ? /* @__PURE__ */ jsxRuntime.jsxs(
2233
+ "a",
2196
2234
  {
2197
- type: "button",
2198
- onClick: () => {
2199
- if (!a.active) userMenu.onSwitchAccount?.(a.id);
2200
- },
2201
- className: "flex min-w-0 flex-1 items-center gap-2.5 rounded-lg px-1 py-1 text-left hover:bg-[color:var(--panel)]",
2235
+ href: safeHref(userMenu.addAccountHref),
2236
+ className: "mt-1.5 flex items-center gap-2.5 rounded-lg px-1 py-1.5 text-sm font-semibold text-[color:var(--color-race-red)] hover:bg-[color:var(--panel)]",
2202
2237
  children: [
2203
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-[color:var(--color-race-red)] text-[11px] font-bold text-white", children: a.avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: a.avatarUrl, alt: "", className: "h-full w-full object-cover" }) : (a.name ?? a.email ?? "?").slice(0, 2).toUpperCase() }),
2204
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col leading-tight", children: [
2205
- /* @__PURE__ */ jsxRuntime.jsx(
2206
- "span",
2207
- {
2208
- className: [
2209
- "truncate text-sm font-semibold",
2210
- a.active ? "text-[color:var(--color-race-red)]" : "text-[color:var(--foreground)]"
2211
- ].join(" "),
2212
- children: a.name ?? a.email ?? a.id.slice(0, 8)
2213
- }
2214
- ),
2215
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-xs text-[color:var(--muted)]", children: a.active ? "Active" : a.email ?? "" })
2216
- ] })
2238
+ /* @__PURE__ */ jsxRuntime.jsx(AddUserIcon, {}),
2239
+ "Add Account"
2217
2240
  ]
2218
2241
  }
2219
- ),
2220
- !a.active && userMenu.onRemoveAccount ? /* @__PURE__ */ jsxRuntime.jsx(
2242
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
2221
2243
  "button",
2222
2244
  {
2223
2245
  type: "button",
2224
- "aria-label": `Remove ${a.name ?? a.email ?? "account"}`,
2225
- onClick: () => userMenu.onRemoveAccount?.(a.id),
2226
- className: "shrink-0 rounded p-1 text-[color:var(--color-race-red)] hover:bg-[color:var(--panel)]",
2227
- children: /* @__PURE__ */ jsxRuntime.jsx(TrashIcon, {})
2246
+ onClick: userMenu.onAddAccount,
2247
+ className: "mt-1.5 flex w-full items-center gap-2.5 rounded-lg px-1 py-1.5 text-left text-sm font-semibold text-[color:var(--color-race-red)] hover:bg-[color:var(--panel)]",
2248
+ children: [
2249
+ /* @__PURE__ */ jsxRuntime.jsx(AddUserIcon, {}),
2250
+ "Add Account"
2251
+ ]
2228
2252
  }
2229
2253
  ) : null
2230
- ] }, a.id)) }),
2231
- userMenu.addAccountHref || userMenu.onAddAccount ? userMenu.addAccountHref ? /* @__PURE__ */ jsxRuntime.jsxs(
2232
- "a",
2254
+ ] }) : null,
2255
+ userMenu?.theme ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-[color:var(--line)]", children: /* @__PURE__ */ jsxRuntime.jsx(
2256
+ NavThemeControl,
2233
2257
  {
2234
- href: safeHref(userMenu.addAccountHref),
2235
- className: "mt-1.5 flex items-center gap-2.5 rounded-lg px-1 py-1.5 text-sm font-semibold text-[color:var(--color-race-red)] hover:bg-[color:var(--panel)]",
2236
- children: [
2237
- /* @__PURE__ */ jsxRuntime.jsx(AddUserIcon, {}),
2238
- "Add Account"
2239
- ]
2258
+ value: userMenu.theme.value,
2259
+ onChange: userMenu.theme.onChange
2240
2260
  }
2241
- ) : /* @__PURE__ */ jsxRuntime.jsxs(
2242
- "button",
2243
- {
2244
- type: "button",
2245
- onClick: userMenu.onAddAccount,
2246
- className: "mt-1.5 flex w-full items-center gap-2.5 rounded-lg px-1 py-1.5 text-left text-sm font-semibold text-[color:var(--color-race-red)] hover:bg-[color:var(--panel)]",
2247
- children: [
2248
- /* @__PURE__ */ jsxRuntime.jsx(AddUserIcon, {}),
2249
- "Add Account"
2250
- ]
2251
- }
2252
- ) : null
2253
- ] }) : null,
2254
- userMenu?.theme ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-[color:var(--line)]", children: /* @__PURE__ */ jsxRuntime.jsx(
2255
- NavThemeControl,
2256
- {
2257
- value: userMenu.theme.value,
2258
- onChange: userMenu.theme.onChange
2259
- }
2260
- ) }) : null,
2261
+ ) }) : null
2262
+ ] }),
2261
2263
  onSignOut ? /* @__PURE__ */ jsxRuntime.jsxs(
2262
2264
  "button",
2263
2265
  {
2264
2266
  type: "button",
2265
2267
  onClick: onSignOut,
2266
- className: "flex w-full items-center gap-2.5 border-t border-[color:var(--line)] px-4 py-2.5 text-left text-sm font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)] hover:text-[color:var(--color-race-red)]",
2268
+ className: "flex w-full shrink-0 items-center gap-2.5 border-t border-[color:var(--line)] px-4 py-2.5 text-left text-sm font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)] hover:text-[color:var(--color-race-red)]",
2267
2269
  children: [
2268
2270
  /* @__PURE__ */ jsxRuntime.jsx(SignOutIcon, {}),
2269
2271
  "Sign out"
@@ -2602,6 +2604,89 @@ function resolveIdentity(input) {
2602
2604
  loaded: input.loaded ?? false
2603
2605
  };
2604
2606
  }
2607
+
2608
+ // src/logout-signal.ts
2609
+ var LOGOUT_COOKIE = "politogy_logout_at";
2610
+ var LOGOUT_CHANNEL = "politogy-logout";
2611
+ var POLL_MS = 1e3;
2612
+ function parseTimestamp(value) {
2613
+ if (!value) return null;
2614
+ const n = Number(value);
2615
+ return Number.isFinite(n) && n > 0 ? n : null;
2616
+ }
2617
+ function isSessionRevoked(logoutCookieValue, authTime) {
2618
+ const loggedOutAt = parseTimestamp(logoutCookieValue);
2619
+ if (loggedOutAt === null) return false;
2620
+ if (authTime == null || !Number.isFinite(authTime) || authTime <= 0) return true;
2621
+ const authMs = authTime < 1e12 ? authTime * 1e3 : authTime;
2622
+ return authMs < loggedOutAt;
2623
+ }
2624
+ function readLogoutCookie() {
2625
+ if (typeof document === "undefined") return null;
2626
+ try {
2627
+ const m = document.cookie.match(new RegExp(`(?:^|;\\s*)${LOGOUT_COOKIE}=([^;]+)`));
2628
+ return parseTimestamp(m ? decodeURIComponent(m[1]) : null);
2629
+ } catch {
2630
+ return null;
2631
+ }
2632
+ }
2633
+ function broadcastLogout(at = Date.now()) {
2634
+ if (typeof document === "undefined" || typeof window === "undefined") return;
2635
+ const host = window.location.hostname;
2636
+ const domainAttr = /(^|\.)politogyvrm\.com$/i.test(host) ? "; Domain=.politogyvrm.com" : "";
2637
+ const secure = window.location.protocol === "https:" ? "; Secure" : "";
2638
+ document.cookie = `${LOGOUT_COOKIE}=${at}; Path=/; Max-Age=2592000; SameSite=Lax${domainAttr}${secure}`;
2639
+ try {
2640
+ new BroadcastChannel(LOGOUT_CHANNEL).postMessage(at);
2641
+ } catch {
2642
+ }
2643
+ }
2644
+ function watchLogout(onLogout, options = {}) {
2645
+ if (typeof document === "undefined" || typeof window === "undefined") {
2646
+ return () => {
2647
+ };
2648
+ }
2649
+ const { authTime = null, intervalMs = POLL_MS } = options;
2650
+ const baseline = readLogoutCookie() ?? 0;
2651
+ let done = false;
2652
+ const fire = (at) => {
2653
+ if (done) return;
2654
+ if (at <= baseline) return;
2655
+ if (authTime != null && !isSessionRevoked(String(at), authTime)) return;
2656
+ done = true;
2657
+ cleanup();
2658
+ onLogout();
2659
+ };
2660
+ const check = () => {
2661
+ const at = readLogoutCookie();
2662
+ if (at !== null) fire(at);
2663
+ };
2664
+ let channel = null;
2665
+ try {
2666
+ channel = new BroadcastChannel(LOGOUT_CHANNEL);
2667
+ channel.onmessage = (event) => {
2668
+ const at = typeof event.data === "number" ? event.data : readLogoutCookie();
2669
+ if (at !== null) fire(at);
2670
+ };
2671
+ } catch {
2672
+ channel = null;
2673
+ }
2674
+ const timer = window.setInterval(check, intervalMs);
2675
+ window.addEventListener("visibilitychange", check);
2676
+ window.addEventListener("focus", check);
2677
+ window.addEventListener("pageshow", check);
2678
+ function cleanup() {
2679
+ window.clearInterval(timer);
2680
+ window.removeEventListener("visibilitychange", check);
2681
+ window.removeEventListener("focus", check);
2682
+ window.removeEventListener("pageshow", check);
2683
+ try {
2684
+ channel?.close();
2685
+ } catch {
2686
+ }
2687
+ }
2688
+ return cleanup;
2689
+ }
2605
2690
  var CORE_APP = "vrm";
2606
2691
  var POLITOGY_APPS = {
2607
2692
  vrm: { title: "Politogy VRM", origin: "https://app.politogyvrm.com", storagePrefix: "vrm" },
@@ -2678,6 +2763,42 @@ function FrameBody({
2678
2763
  }) {
2679
2764
  const meta = POLITOGY_APPS[app];
2680
2765
  const chrome = useChromeState();
2766
+ const signOutRef = react.useRef(onSignOut);
2767
+ signOutRef.current = onSignOut;
2768
+ const signingOutRef = react.useRef(false);
2769
+ const handleSignOut = react.useCallback(() => {
2770
+ signingOutRef.current = true;
2771
+ broadcastLogout();
2772
+ signOutRef.current();
2773
+ window.setTimeout(() => {
2774
+ window.location.replace(window.location.href);
2775
+ }, 2e3);
2776
+ }, []);
2777
+ react.useEffect(
2778
+ () => (
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.
2781
+ //
2782
+ // Measured against production before this landed: calling the app's
2783
+ // handler did nothing at all. Apps wire it as
2784
+ // `onSignOut={() => { void signOutAction(); }}` — a server action invoked
2785
+ // outside a transition, so the `redirect()` inside it can never be
2786
+ // applied by the client and the tab just sits there. Building cross-tab
2787
+ // logout on that path made the whole feature a no-op.
2788
+ //
2789
+ // A hard navigation cannot fail that way, and it hands the decision to
2790
+ // the authority rather than guessing: the app's middleware sees a session
2791
+ // whose auth_time predates `politogy_logout_at`, treats it as revoked,
2792
+ // and redirects to Politogy ID. Server-side truth decides; the client
2793
+ // only forces the request. No re-broadcast — the signal that woke us is
2794
+ // already on record for every tab.
2795
+ watchLogout(() => {
2796
+ if (signingOutRef.current) return;
2797
+ window.location.replace(window.location.href);
2798
+ })
2799
+ ),
2800
+ []
2801
+ );
2681
2802
  const core = app === CORE_APP ? data?.core : void 0;
2682
2803
  const identity = useChromeIdentity({ userName: session.userName, email: session.email });
2683
2804
  const derivedUserMenu = useChromeUserMenu({
@@ -2760,7 +2881,7 @@ function FrameBody({
2760
2881
  role: session.role
2761
2882
  },
2762
2883
  userMenu,
2763
- onSignOut,
2884
+ onSignOut: handleSignOut,
2764
2885
  // The action cluster is IDENTICAL everywhere — core included. Page
2765
2886
  // actions sit BEFORE it, never inside it, so the Alerts bell keeps its
2766
2887
  // required position: farthest right, immediately before My Account.