@the-portland-company/shell 0.55.0 → 0.57.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 +70 -0
- package/dist/app-chrome.cjs +412 -63
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.d.cts +13 -0
- package/dist/app-chrome.d.ts +13 -0
- package/dist/app-chrome.js +136 -10
- package/dist/app-chrome.js.map +1 -1
- package/dist/{chunk-K5GWJSHE.js → chunk-TMOZTLUZ.js} +281 -58
- package/dist/chunk-TMOZTLUZ.js.map +1 -0
- package/dist/native.cjs +279 -54
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +6 -1
- package/dist/native.d.ts +6 -1
- package/dist/native.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-K5GWJSHE.js.map +0 -1
package/dist/app-chrome.cjs
CHANGED
|
@@ -169,6 +169,7 @@ var CORE_APP_ORIGIN = "https://app.politogyvrm.com";
|
|
|
169
169
|
var CORE_BILLING_PATH = "/organization-settings/subscription-billing/text-email";
|
|
170
170
|
var CORE_BILLING_URL = `${CORE_APP_ORIGIN}${CORE_BILLING_PATH}`;
|
|
171
171
|
var QUARANTINED_CONTACTS_URL = `https://contacts.politogyvrm.com/duplicates`;
|
|
172
|
+
var SPECS_ORIGIN = "https://specs.politogyvrm.com";
|
|
172
173
|
function withOrgParam(url, orgId) {
|
|
173
174
|
if (!orgId) return url;
|
|
174
175
|
return `${url}${url.includes("?") ? "&" : "?"}org=${orgId}`;
|
|
@@ -291,16 +292,46 @@ function NavThemeControl({
|
|
|
291
292
|
)
|
|
292
293
|
] });
|
|
293
294
|
}
|
|
295
|
+
var CHROME_FETCH_CACHE_TTL_MS = 3e4;
|
|
296
|
+
function chromeCacheKey(path, orgId) {
|
|
297
|
+
return orgId === void 0 ? path : `${path}::${orgId ?? ""}`;
|
|
298
|
+
}
|
|
299
|
+
function createChromeFetchCache(getFetchJson) {
|
|
300
|
+
const cache = /* @__PURE__ */ new Map();
|
|
301
|
+
function dedupedFetch(path, init, opts = {}) {
|
|
302
|
+
const key = chromeCacheKey(path, opts.orgId);
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
const hit = cache.get(key);
|
|
305
|
+
if (hit && hit.expiresAt > now) return hit.promise;
|
|
306
|
+
const promise = getFetchJson()(path, init);
|
|
307
|
+
promise.catch(() => cache.delete(key));
|
|
308
|
+
cache.set(key, { promise, expiresAt: now + (opts.ttlMs ?? CHROME_FETCH_CACHE_TTL_MS) });
|
|
309
|
+
return promise;
|
|
310
|
+
}
|
|
311
|
+
function invalidate(path) {
|
|
312
|
+
for (const key of cache.keys()) {
|
|
313
|
+
if (key === path || key.startsWith(`${path}::`)) cache.delete(key);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { dedupedFetch, invalidate };
|
|
317
|
+
}
|
|
294
318
|
var NavChromeContext = react.createContext(null);
|
|
295
319
|
function NavChromeProvider({
|
|
296
320
|
fetchJson,
|
|
297
321
|
children
|
|
298
322
|
}) {
|
|
323
|
+
const fetchJsonRef = react.useRef(fetchJson);
|
|
324
|
+
fetchJsonRef.current = fetchJson;
|
|
325
|
+
const cacheRef = react.useRef(null);
|
|
326
|
+
if (!cacheRef.current) {
|
|
327
|
+
cacheRef.current = createChromeFetchCache(() => fetchJsonRef.current);
|
|
328
|
+
}
|
|
329
|
+
const { dedupedFetch, invalidate: invalidateChromeCache } = cacheRef.current;
|
|
299
330
|
const [me, setMe] = react.useState(null);
|
|
300
331
|
const [meLoaded, setMeLoaded] = react.useState(false);
|
|
301
332
|
react.useEffect(() => {
|
|
302
333
|
let cancelled = false;
|
|
303
|
-
|
|
334
|
+
dedupedFetch("me", void 0).then((data) => {
|
|
304
335
|
if (!cancelled) setMe(data);
|
|
305
336
|
}).catch(() => {
|
|
306
337
|
}).finally(() => {
|
|
@@ -309,8 +340,8 @@ function NavChromeProvider({
|
|
|
309
340
|
return () => {
|
|
310
341
|
cancelled = true;
|
|
311
342
|
};
|
|
312
|
-
}, [
|
|
313
|
-
return /* @__PURE__ */ jsxRuntime.jsx(NavChromeContext.Provider, { value: { fetchJson, me, meLoaded }, children });
|
|
343
|
+
}, [dedupedFetch]);
|
|
344
|
+
return /* @__PURE__ */ jsxRuntime.jsx(NavChromeContext.Provider, { value: { fetchJson, dedupedFetch, invalidateChromeCache, me, meLoaded }, children });
|
|
314
345
|
}
|
|
315
346
|
function useChromeMe() {
|
|
316
347
|
const ctx = react.useContext(NavChromeContext);
|
|
@@ -329,7 +360,7 @@ function useNavChrome(component) {
|
|
|
329
360
|
return ctx;
|
|
330
361
|
}
|
|
331
362
|
function useChromeOrgs() {
|
|
332
|
-
const {
|
|
363
|
+
const { dedupedFetch } = useNavChrome("useChromeOrgs");
|
|
333
364
|
const [orgs, setOrgs] = react.useState(null);
|
|
334
365
|
const [loading, setLoading] = react.useState(true);
|
|
335
366
|
const [error, setError] = react.useState(false);
|
|
@@ -337,7 +368,7 @@ function useChromeOrgs() {
|
|
|
337
368
|
let cancelled = false;
|
|
338
369
|
setLoading(true);
|
|
339
370
|
setError(false);
|
|
340
|
-
|
|
371
|
+
dedupedFetch("orgs", void 0).then((data) => {
|
|
341
372
|
if (cancelled) return;
|
|
342
373
|
const list = data?.orgs ?? [];
|
|
343
374
|
setOrgs(list);
|
|
@@ -349,7 +380,7 @@ function useChromeOrgs() {
|
|
|
349
380
|
return () => {
|
|
350
381
|
cancelled = true;
|
|
351
382
|
};
|
|
352
|
-
}, [
|
|
383
|
+
}, [dedupedFetch]);
|
|
353
384
|
return { orgs, loading, error };
|
|
354
385
|
}
|
|
355
386
|
function ValueSkeleton({ className }) {
|
|
@@ -633,7 +664,7 @@ function LowFuelHint({ lowFuel, orgId }) {
|
|
|
633
664
|
);
|
|
634
665
|
}
|
|
635
666
|
function FuelAction({ orgId }) {
|
|
636
|
-
const {
|
|
667
|
+
const { dedupedFetch } = useNavChrome("FuelAction");
|
|
637
668
|
const [fuel, setFuel] = react.useState(null);
|
|
638
669
|
const [loading, setLoading] = react.useState(true);
|
|
639
670
|
const [error, setError] = react.useState(false);
|
|
@@ -642,9 +673,11 @@ function FuelAction({ orgId }) {
|
|
|
642
673
|
setLoading(true);
|
|
643
674
|
setError(false);
|
|
644
675
|
setFuel(null);
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
676
|
+
dedupedFetch(
|
|
677
|
+
"communications/fuel-status",
|
|
678
|
+
{ headers: orgId ? { "x-contacts-org": orgId } : void 0 },
|
|
679
|
+
{ orgId }
|
|
680
|
+
).then((data) => {
|
|
648
681
|
if (cancelled) return;
|
|
649
682
|
setFuel(data?.fuel ?? null);
|
|
650
683
|
}).catch(() => {
|
|
@@ -655,7 +688,7 @@ function FuelAction({ orgId }) {
|
|
|
655
688
|
return () => {
|
|
656
689
|
cancelled = true;
|
|
657
690
|
};
|
|
658
|
-
}, [
|
|
691
|
+
}, [dedupedFetch, orgId]);
|
|
659
692
|
const low = !!fuel && (fuel.signal.lowFuel || fuel.broadcast.lowFuel);
|
|
660
693
|
return (
|
|
661
694
|
// The nudge anchors to the trigger, so it sits OUTSIDE the popover panel in
|
|
@@ -726,7 +759,6 @@ function writeNotifState(orgId, state) {
|
|
|
726
759
|
} catch {
|
|
727
760
|
}
|
|
728
761
|
}
|
|
729
|
-
var TEN_DLC_REDIRECTED_KEY = "politogy_10dlc_redirected";
|
|
730
762
|
var TOASTS_MUTED_KEY = "politogy_chrome_toasts_muted";
|
|
731
763
|
function readToastsMuted() {
|
|
732
764
|
if (typeof window === "undefined") return false;
|
|
@@ -857,7 +889,7 @@ function NewReplyToast({
|
|
|
857
889
|
);
|
|
858
890
|
}
|
|
859
891
|
function NotificationsAction({ orgId }) {
|
|
860
|
-
const {
|
|
892
|
+
const { dedupedFetch } = useNavChrome("NotificationsAction");
|
|
861
893
|
const [payload, setPayload] = react.useState(null);
|
|
862
894
|
const [loading, setLoading] = react.useState(true);
|
|
863
895
|
const [error, setError] = react.useState(false);
|
|
@@ -876,9 +908,11 @@ function NotificationsAction({ orgId }) {
|
|
|
876
908
|
setLoading(true);
|
|
877
909
|
setError(false);
|
|
878
910
|
setPayload(null);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
911
|
+
dedupedFetch(
|
|
912
|
+
"notifications",
|
|
913
|
+
{ headers: orgId ? { "x-contacts-org": orgId } : void 0 },
|
|
914
|
+
{ orgId }
|
|
915
|
+
).then((data) => {
|
|
882
916
|
if (!cancelled) setPayload(data);
|
|
883
917
|
}).catch(() => {
|
|
884
918
|
if (!cancelled) setError(true);
|
|
@@ -888,29 +922,7 @@ function NotificationsAction({ orgId }) {
|
|
|
888
922
|
return () => {
|
|
889
923
|
cancelled = true;
|
|
890
924
|
};
|
|
891
|
-
}, [
|
|
892
|
-
react.useEffect(() => {
|
|
893
|
-
if (!payload || typeof window === "undefined") return;
|
|
894
|
-
const alert = payload.alerts.find((a) => a.kind === "10dlc_registration" && a.href);
|
|
895
|
-
if (!alert?.href) return;
|
|
896
|
-
try {
|
|
897
|
-
if (window.sessionStorage.getItem(TEN_DLC_REDIRECTED_KEY) === "1") return;
|
|
898
|
-
} catch {
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
let target;
|
|
902
|
-
try {
|
|
903
|
-
target = new URL(alert.href, window.location.href);
|
|
904
|
-
} catch {
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
if (target.origin !== window.location.origin) return;
|
|
908
|
-
try {
|
|
909
|
-
window.sessionStorage.setItem(TEN_DLC_REDIRECTED_KEY, "1");
|
|
910
|
-
} catch {
|
|
911
|
-
}
|
|
912
|
-
window.location.assign(alert.href);
|
|
913
|
-
}, [payload]);
|
|
925
|
+
}, [dedupedFetch, orgId]);
|
|
914
926
|
const markRead = react.useCallback(
|
|
915
927
|
(id) => {
|
|
916
928
|
setNotif((prev) => {
|
|
@@ -1311,6 +1323,216 @@ function SupportAction({ orgId } = {}) {
|
|
|
1311
1323
|
) : null
|
|
1312
1324
|
] });
|
|
1313
1325
|
}
|
|
1326
|
+
var SPEC_OVERLAY_ACCESS_CACHE_KEY = "politogy_spec_overlay_access";
|
|
1327
|
+
function readSpecOverlayAccessCache() {
|
|
1328
|
+
if (typeof window === "undefined") return null;
|
|
1329
|
+
try {
|
|
1330
|
+
const raw = window.sessionStorage.getItem(SPEC_OVERLAY_ACCESS_CACHE_KEY);
|
|
1331
|
+
if (!raw) return null;
|
|
1332
|
+
const parsed = JSON.parse(raw);
|
|
1333
|
+
if (typeof parsed?.allowed === "boolean") return parsed;
|
|
1334
|
+
} catch {
|
|
1335
|
+
}
|
|
1336
|
+
return null;
|
|
1337
|
+
}
|
|
1338
|
+
function writeSpecOverlayAccessCache(access) {
|
|
1339
|
+
if (typeof window === "undefined") return;
|
|
1340
|
+
try {
|
|
1341
|
+
window.sessionStorage.setItem(SPEC_OVERLAY_ACCESS_CACHE_KEY, JSON.stringify(access));
|
|
1342
|
+
} catch {
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
var locationChangeListeners = /* @__PURE__ */ new Set();
|
|
1346
|
+
var historyPatched = false;
|
|
1347
|
+
function ensureHistoryPatched() {
|
|
1348
|
+
if (historyPatched || typeof window === "undefined") return;
|
|
1349
|
+
historyPatched = true;
|
|
1350
|
+
const notify = () => locationChangeListeners.forEach((fn) => fn());
|
|
1351
|
+
const originalPushState = window.history.pushState.bind(window.history);
|
|
1352
|
+
const originalReplaceState = window.history.replaceState.bind(window.history);
|
|
1353
|
+
window.history.pushState = (...args) => {
|
|
1354
|
+
originalPushState(...args);
|
|
1355
|
+
notify();
|
|
1356
|
+
};
|
|
1357
|
+
window.history.replaceState = (...args) => {
|
|
1358
|
+
originalReplaceState(...args);
|
|
1359
|
+
notify();
|
|
1360
|
+
};
|
|
1361
|
+
window.addEventListener("popstate", notify);
|
|
1362
|
+
}
|
|
1363
|
+
function useLocationHref() {
|
|
1364
|
+
const [href, setHref] = react.useState(
|
|
1365
|
+
() => typeof window === "undefined" ? "" : window.location.href
|
|
1366
|
+
);
|
|
1367
|
+
react.useEffect(() => {
|
|
1368
|
+
ensureHistoryPatched();
|
|
1369
|
+
const onChange = () => setHref(window.location.href);
|
|
1370
|
+
locationChangeListeners.add(onChange);
|
|
1371
|
+
return () => {
|
|
1372
|
+
locationChangeListeners.delete(onChange);
|
|
1373
|
+
};
|
|
1374
|
+
}, []);
|
|
1375
|
+
return href;
|
|
1376
|
+
}
|
|
1377
|
+
function specOverlayFetchJson(path) {
|
|
1378
|
+
return fetch(`${SPECS_ORIGIN}${path}`, { credentials: "include" }).then((res) => {
|
|
1379
|
+
if (!res.ok) throw new Error(`spec overlay ${path}: ${res.status}`);
|
|
1380
|
+
return res.json();
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
function SpecOverlayAction({ orgId } = {}) {
|
|
1384
|
+
const [access, setAccess] = react.useState(() => readSpecOverlayAccessCache());
|
|
1385
|
+
const [status, setStatus] = react.useState(null);
|
|
1386
|
+
const [open, setOpen] = react.useState(false);
|
|
1387
|
+
const href = useLocationHref();
|
|
1388
|
+
react.useEffect(() => {
|
|
1389
|
+
if (access) return;
|
|
1390
|
+
let cancelled = false;
|
|
1391
|
+
specOverlayFetchJson("/api/overlay/access").then((data) => {
|
|
1392
|
+
if (cancelled) return;
|
|
1393
|
+
const next = data && typeof data.allowed === "boolean" ? data : { allowed: false };
|
|
1394
|
+
setAccess(next);
|
|
1395
|
+
writeSpecOverlayAccessCache(next);
|
|
1396
|
+
}).catch(() => {
|
|
1397
|
+
if (cancelled) return;
|
|
1398
|
+
const next = { allowed: false };
|
|
1399
|
+
setAccess(next);
|
|
1400
|
+
writeSpecOverlayAccessCache(next);
|
|
1401
|
+
});
|
|
1402
|
+
return () => {
|
|
1403
|
+
cancelled = true;
|
|
1404
|
+
};
|
|
1405
|
+
}, [access]);
|
|
1406
|
+
react.useEffect(() => {
|
|
1407
|
+
if (!access?.allowed || typeof window === "undefined") return;
|
|
1408
|
+
const timer = window.setTimeout(() => {
|
|
1409
|
+
const url = `/api/overlay/status?url=${encodeURIComponent(window.location.href)}`;
|
|
1410
|
+
specOverlayFetchJson(url).then((data) => setStatus(data)).catch(() => setStatus(null));
|
|
1411
|
+
}, 300);
|
|
1412
|
+
return () => window.clearTimeout(timer);
|
|
1413
|
+
}, [access?.allowed, href, orgId]);
|
|
1414
|
+
if (!access?.allowed) return null;
|
|
1415
|
+
const badgeLabel = status && status.matched ? `${Math.round(status.mismatch_pct)}%` : status ? "\u2014" : void 0;
|
|
1416
|
+
const badgeTone = status && status.matched ? status.passed ? "default" : "danger" : "default";
|
|
1417
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1418
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1419
|
+
"button",
|
|
1420
|
+
{
|
|
1421
|
+
type: "button",
|
|
1422
|
+
"aria-label": "Spec",
|
|
1423
|
+
"aria-expanded": open,
|
|
1424
|
+
className: topBarIconButtonClass(badgeTone),
|
|
1425
|
+
onClick: () => setOpen(true),
|
|
1426
|
+
children: [
|
|
1427
|
+
/* @__PURE__ */ jsxRuntime.jsx(LayoutGlyph, { size: 20, className: "text-current" }),
|
|
1428
|
+
badgeLabel ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1429
|
+
"span",
|
|
1430
|
+
{
|
|
1431
|
+
className: [
|
|
1432
|
+
"absolute -top-1 -right-1 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-normal leading-none text-white",
|
|
1433
|
+
status && status.matched ? status.passed ? "bg-[color:var(--chrome-success,#2f855a)]" : "bg-[color:var(--chrome-danger)]" : "bg-[color:var(--muted)]"
|
|
1434
|
+
].join(" "),
|
|
1435
|
+
children: badgeLabel
|
|
1436
|
+
}
|
|
1437
|
+
) : null,
|
|
1438
|
+
!open ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1439
|
+
TopBarTooltip,
|
|
1440
|
+
{
|
|
1441
|
+
title: "Spec",
|
|
1442
|
+
description: "Spec design + visual regression for this page."
|
|
1443
|
+
}
|
|
1444
|
+
) : null
|
|
1445
|
+
]
|
|
1446
|
+
}
|
|
1447
|
+
),
|
|
1448
|
+
open && typeof document !== "undefined" ? reactDom.createPortal(
|
|
1449
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1450
|
+
"div",
|
|
1451
|
+
{
|
|
1452
|
+
className: "fixed inset-0 z-50 flex justify-end bg-black/40",
|
|
1453
|
+
role: "dialog",
|
|
1454
|
+
"aria-modal": "true",
|
|
1455
|
+
"aria-label": "Spec overlay",
|
|
1456
|
+
onMouseDown: (e) => {
|
|
1457
|
+
if (e.target === e.currentTarget) setOpen(false);
|
|
1458
|
+
},
|
|
1459
|
+
onKeyDown: (e) => {
|
|
1460
|
+
if (e.key === "Escape") setOpen(false);
|
|
1461
|
+
},
|
|
1462
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full w-full max-w-[480px] flex-col border-l border-[color:var(--line)] bg-[color:var(--panel-elevated)] shadow-[var(--shadow-soft)]", children: [
|
|
1463
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-[color:var(--line)] px-4 py-3", children: [
|
|
1464
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h2", { className: "truncate text-sm font-bold tracking-[-0.01em] text-[color:var(--foreground)]", children: [
|
|
1465
|
+
"Spec",
|
|
1466
|
+
status && status.matched ? `: ${status.spec_title}` : ""
|
|
1467
|
+
] }),
|
|
1468
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1469
|
+
"button",
|
|
1470
|
+
{
|
|
1471
|
+
type: "button",
|
|
1472
|
+
"aria-label": "Close",
|
|
1473
|
+
onClick: () => setOpen(false),
|
|
1474
|
+
className: "rounded-full p-1 text-[color:var(--muted)] hover:bg-[color:var(--panel)] hover:text-[color:var(--foreground)]",
|
|
1475
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(CloseGlyph, { size: 16 })
|
|
1476
|
+
}
|
|
1477
|
+
)
|
|
1478
|
+
] }),
|
|
1479
|
+
status && status.matched ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1480
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 border-b border-[color:var(--line)] px-4 py-2 text-xs font-semibold", children: [
|
|
1481
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1482
|
+
"a",
|
|
1483
|
+
{
|
|
1484
|
+
href: status.spec_url,
|
|
1485
|
+
target: "_blank",
|
|
1486
|
+
rel: "noreferrer",
|
|
1487
|
+
className: "text-[color:var(--color-accent)] hover:underline",
|
|
1488
|
+
children: "Open spec \u2197"
|
|
1489
|
+
}
|
|
1490
|
+
),
|
|
1491
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1492
|
+
"a",
|
|
1493
|
+
{
|
|
1494
|
+
href: status.compare_url,
|
|
1495
|
+
target: "_blank",
|
|
1496
|
+
rel: "noreferrer",
|
|
1497
|
+
className: "text-[color:var(--color-accent)] hover:underline",
|
|
1498
|
+
children: "Compare tab \u2197"
|
|
1499
|
+
}
|
|
1500
|
+
)
|
|
1501
|
+
] }),
|
|
1502
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1503
|
+
"iframe",
|
|
1504
|
+
{
|
|
1505
|
+
src: status.overlay_url,
|
|
1506
|
+
title: "Spec overlay",
|
|
1507
|
+
className: "h-full w-full flex-1 border-0"
|
|
1508
|
+
}
|
|
1509
|
+
)
|
|
1510
|
+
] }) : status && !status.matched ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-sm text-[color:var(--muted)]", children: [
|
|
1511
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "No compare target covers this page." }),
|
|
1512
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1513
|
+
"a",
|
|
1514
|
+
{
|
|
1515
|
+
href: status.search_url,
|
|
1516
|
+
target: "_blank",
|
|
1517
|
+
rel: "noreferrer",
|
|
1518
|
+
className: "mt-2 inline-block font-semibold text-[color:var(--color-accent)] hover:underline",
|
|
1519
|
+
children: "Search specs \u2197"
|
|
1520
|
+
}
|
|
1521
|
+
)
|
|
1522
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4", children: /* @__PURE__ */ jsxRuntime.jsx(ValueSkeleton, { className: "h-4 w-full" }) })
|
|
1523
|
+
] })
|
|
1524
|
+
}
|
|
1525
|
+
),
|
|
1526
|
+
document.body
|
|
1527
|
+
) : null
|
|
1528
|
+
] });
|
|
1529
|
+
}
|
|
1530
|
+
function CloseGlyph({ size = 16, className }) {
|
|
1531
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": true, children: [
|
|
1532
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
1533
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
1534
|
+
] });
|
|
1535
|
+
}
|
|
1314
1536
|
var NAV_DISPLAY_OPTIONS = [
|
|
1315
1537
|
{ value: "menu", label: "Sub Menu" },
|
|
1316
1538
|
{ value: "tabs", label: "Tabs Only" },
|
|
@@ -1494,11 +1716,6 @@ var APP_EMAIL_CHILDREN = [
|
|
|
1494
1716
|
{ label: "Deliverability", icon: "activity", href: `${EMAIL_APP}/deliverability` }
|
|
1495
1717
|
];
|
|
1496
1718
|
var EMAIL_V1_LEGACY = { label: "Email V1 (legacy)", icon: "clock", path: "/relationship/email/dashboard" };
|
|
1497
|
-
var SMS_CHILDREN = [
|
|
1498
|
-
{ label: "Dashboard", icon: "grid", path: "/relationship/sms-mms-blast/dashboard" },
|
|
1499
|
-
{ label: "Compose & Send", icon: "send", path: "/relationship/sms-mms-blast/compose-and-send" },
|
|
1500
|
-
{ label: "Conversations", icon: "messageSquare", path: "/relationship/sms-mms-blast/conversations" }
|
|
1501
|
-
];
|
|
1502
1719
|
var CTA_CHILDREN = [
|
|
1503
1720
|
{ label: "Dashboard", icon: "grid", path: "/relationship/cta-database/dashboard" },
|
|
1504
1721
|
{ label: "Create & Edit", icon: "edit", path: "/relationship/cta-creator/create-and-edit" },
|
|
@@ -1579,6 +1796,14 @@ function standardPolitogyNav(opts) {
|
|
|
1579
1796
|
href: withOrg(abs(c.path)),
|
|
1580
1797
|
children: c.children ? leaves(c.children) : void 0
|
|
1581
1798
|
}));
|
|
1799
|
+
const appLeaves = (origin, children, modeParam) => {
|
|
1800
|
+
const base2 = origin.replace(/\/+$/, "");
|
|
1801
|
+
return children.map((c) => {
|
|
1802
|
+
const url = c.path ? `${base2}/${c.path}` : `${base2}/`;
|
|
1803
|
+
const withMode = modeParam ? `${url}${url.includes("?") ? "&" : "?"}mode=${modeParam}` : url;
|
|
1804
|
+
return { label: c.label, icon: icon(c.icon), href: withOrg(withMode) };
|
|
1805
|
+
});
|
|
1806
|
+
};
|
|
1582
1807
|
const buildRelationship = () => {
|
|
1583
1808
|
const contacts = opts.contactsItem ? {
|
|
1584
1809
|
label: "Contacts",
|
|
@@ -1607,11 +1832,17 @@ function standardPolitogyNav(opts) {
|
|
|
1607
1832
|
...leaves([EMAIL_V1_LEGACY])
|
|
1608
1833
|
]
|
|
1609
1834
|
},
|
|
1835
|
+
// SMS/MMS Blast IS a2p (Specs/A2P) — the standalone delivery engine
|
|
1836
|
+
// shared with Campaign Mode, not a core-hosted surface (the old
|
|
1837
|
+
// `/relationship/sms-mms-blast/*` core routes are dead; core no longer
|
|
1838
|
+
// hosts them). Every link carries `mode=relationship` so a2p's nav
|
|
1839
|
+
// resolves the Relationship tree instead of its own Campaign default
|
|
1840
|
+
// (resolveActiveMode, mode-preference.ts).
|
|
1610
1841
|
{
|
|
1611
1842
|
label: "SMS/MMS Blast",
|
|
1612
1843
|
icon: icon("messageSquare"),
|
|
1613
|
-
href: withOrg(
|
|
1614
|
-
children:
|
|
1844
|
+
href: withOrg(`${A2P_ORIGIN}?mode=relationship`),
|
|
1845
|
+
children: appLeaves(A2P_ORIGIN, APP_A2P_CHILDREN, "relationship")
|
|
1615
1846
|
},
|
|
1616
1847
|
// These two carry no top-level href in VRM core (parent row only toggles).
|
|
1617
1848
|
{ label: "Calls to Action", icon: icon("layout"), children: leaves(CTA_CHILDREN) },
|
|
@@ -1652,14 +1883,6 @@ function standardPolitogyNav(opts) {
|
|
|
1652
1883
|
...CAMPAIGN_FEATURE_AVAILABILITY,
|
|
1653
1884
|
...opts.campaignAvailability
|
|
1654
1885
|
};
|
|
1655
|
-
const appLeaves = (origin, children) => {
|
|
1656
|
-
const base2 = origin.replace(/\/+$/, "");
|
|
1657
|
-
return children.map((c) => ({
|
|
1658
|
-
label: c.label,
|
|
1659
|
-
icon: icon(c.icon),
|
|
1660
|
-
href: withOrg(c.path ? `${base2}/${c.path}` : `${base2}/`)
|
|
1661
|
-
}));
|
|
1662
|
-
};
|
|
1663
1886
|
const rows = [
|
|
1664
1887
|
{ label: "Dashboard", icon: icon("home"), href: withOrg(abs("/campaign/dashboard")) },
|
|
1665
1888
|
{ label: "Voter Contact", icon: icon("target"), href: withOrg(abs("/campaign/campaigns")) },
|
|
@@ -3256,6 +3479,58 @@ function watchLogout(onLogout, options = {}) {
|
|
|
3256
3479
|
}
|
|
3257
3480
|
return cleanup;
|
|
3258
3481
|
}
|
|
3482
|
+
|
|
3483
|
+
// src/mode-preference.ts
|
|
3484
|
+
var ACTIVE_MODE_COOKIE = "politogy_active_mode";
|
|
3485
|
+
var VALID_MODES = ["relationship", "campaign", "petition"];
|
|
3486
|
+
function isStandardNavMode(v) {
|
|
3487
|
+
return typeof v === "string" && VALID_MODES.includes(v);
|
|
3488
|
+
}
|
|
3489
|
+
function readActiveModeCookie() {
|
|
3490
|
+
if (typeof document === "undefined") return null;
|
|
3491
|
+
try {
|
|
3492
|
+
const m = document.cookie.match(
|
|
3493
|
+
new RegExp(`(?:^|;\\s*)${ACTIVE_MODE_COOKIE}=([^;]+)`)
|
|
3494
|
+
);
|
|
3495
|
+
const v = m ? decodeURIComponent(m[1]) : null;
|
|
3496
|
+
return isStandardNavMode(v) ? v : null;
|
|
3497
|
+
} catch {
|
|
3498
|
+
return null;
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
function writeActiveModeCookie(mode) {
|
|
3502
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
3503
|
+
const host = window.location.hostname;
|
|
3504
|
+
const domainAttr = /(^|\.)politogyvrm\.com$/i.test(host) ? "; Domain=.politogyvrm.com" : "";
|
|
3505
|
+
const secure = window.location.protocol === "https:" ? "; Secure" : "";
|
|
3506
|
+
document.cookie = `${ACTIVE_MODE_COOKIE}=${encodeURIComponent(
|
|
3507
|
+
mode
|
|
3508
|
+
)}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`;
|
|
3509
|
+
}
|
|
3510
|
+
function modeFromSearch(search) {
|
|
3511
|
+
if (!search) return null;
|
|
3512
|
+
try {
|
|
3513
|
+
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
3514
|
+
return params.get("mode");
|
|
3515
|
+
} catch {
|
|
3516
|
+
return null;
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
function resolveActiveMode({
|
|
3520
|
+
search,
|
|
3521
|
+
modes,
|
|
3522
|
+
defaultMode
|
|
3523
|
+
}) {
|
|
3524
|
+
const fromQuery = modeFromSearch(search);
|
|
3525
|
+
if (fromQuery && isStandardNavMode(fromQuery) && modes.includes(fromQuery)) {
|
|
3526
|
+
return fromQuery;
|
|
3527
|
+
}
|
|
3528
|
+
const fromCookie = readActiveModeCookie();
|
|
3529
|
+
if (fromCookie && modes.includes(fromCookie)) {
|
|
3530
|
+
return fromCookie;
|
|
3531
|
+
}
|
|
3532
|
+
return defaultMode;
|
|
3533
|
+
}
|
|
3259
3534
|
var BOOT_STAGE_TABLE = [
|
|
3260
3535
|
{ id: "s0_session", weight: 5, critical: true, waitsOn: "Politogy ID session resolution" },
|
|
3261
3536
|
{ id: "s1_org", weight: 10, critical: true, waitsOn: "Chrome API /api/chrome/me + orgs" },
|
|
@@ -3625,7 +3900,17 @@ var POLITOGY_APPS = {
|
|
|
3625
3900
|
// Campaign Mode apps — each scoped to Campaign, so they render the Campaign
|
|
3626
3901
|
// tree and a static Campaign pill. Not yet deployed; hidden from the Campaign
|
|
3627
3902
|
// nav by CAMPAIGN_FEATURE_AVAILABILITY until their launch flips the flag.
|
|
3628
|
-
|
|
3903
|
+
// Dual-mode: a2p is the SMS/MMS delivery engine for BOTH Relationship Mode
|
|
3904
|
+
// and Campaign Mode (Specs/A2P). `mode: 'campaign'` stays the fallback for
|
|
3905
|
+
// anything reading `meta.mode` directly; the effective mode a user sees is
|
|
3906
|
+
// resolved per-visit (see `dual`, resolveActiveMode).
|
|
3907
|
+
a2p: {
|
|
3908
|
+
title: "A2P Messaging",
|
|
3909
|
+
origin: "https://a2p.politogyvrm.com",
|
|
3910
|
+
storagePrefix: "a2p",
|
|
3911
|
+
mode: "campaign",
|
|
3912
|
+
dual: { modes: ["relationship", "campaign"], defaultMode: "campaign" }
|
|
3913
|
+
},
|
|
3629
3914
|
phonebank: { title: "Phone Banking", origin: "https://phonebank.politogyvrm.com", storagePrefix: "phonebank", mode: "campaign" },
|
|
3630
3915
|
canvass: { title: "Field Canvassing", origin: "https://canvass.politogyvrm.com", storagePrefix: "canvass", mode: "campaign" },
|
|
3631
3916
|
signs: { title: "Sign Mapping", origin: "https://signs.politogyvrm.com", storagePrefix: "signs", mode: "campaign" }
|
|
@@ -3762,9 +4047,19 @@ function FrameBody({
|
|
|
3762
4047
|
});
|
|
3763
4048
|
const userMenu = core?.userMenu ?? derivedUserMenu;
|
|
3764
4049
|
const footer = useChromeFooter(buildMeta ?? {});
|
|
3765
|
-
const
|
|
4050
|
+
const resolvedDualMode = meta.dual ? resolveActiveMode({
|
|
4051
|
+
search: search ?? (typeof window !== "undefined" ? window.location.search : void 0),
|
|
4052
|
+
modes: meta.dual.modes,
|
|
4053
|
+
defaultMode: meta.dual.defaultMode
|
|
4054
|
+
}) : void 0;
|
|
4055
|
+
const appMode = resolvedDualMode ?? meta.mode;
|
|
3766
4056
|
const derivedMode = useChromeMode(appMode ? { pinnedModeId: appMode } : void 0);
|
|
3767
4057
|
const mode = core?.mode ?? derivedMode;
|
|
4058
|
+
const coreLiveMode = app === CORE_APP ? pathname.startsWith("/campaign") ? "campaign" : pathname.startsWith("/relationship") ? "relationship" : void 0 : void 0;
|
|
4059
|
+
const cookieMode = coreLiveMode ?? (meta.dual ? void 0 : meta.mode);
|
|
4060
|
+
react.useEffect(() => {
|
|
4061
|
+
if (cookieMode) writeActiveModeCookie(cookieMode);
|
|
4062
|
+
}, [cookieMode]);
|
|
3768
4063
|
const coreOrgs = chrome?.orgs ?? [];
|
|
3769
4064
|
const activeCoreOrg = coreOrgs.find((o) => o.id === chrome?.orgId) ?? null;
|
|
3770
4065
|
const navDisplay = chrome?.navDisplay ?? "both";
|
|
@@ -4137,18 +4432,71 @@ function AppDevNotes({
|
|
|
4137
4432
|
}),
|
|
4138
4433
|
[storagePrefix, identity.role]
|
|
4139
4434
|
);
|
|
4140
|
-
|
|
4141
|
-
|
|
4435
|
+
const activatedRef = react.useRef(false);
|
|
4436
|
+
const activationWaitersRef = react.useRef([]);
|
|
4437
|
+
const activate = react.useCallback(() => {
|
|
4438
|
+
if (activatedRef.current) return;
|
|
4439
|
+
activatedRef.current = true;
|
|
4440
|
+
const waiters = activationWaitersRef.current;
|
|
4441
|
+
activationWaitersRef.current = [];
|
|
4442
|
+
waiters.forEach((resolve) => resolve());
|
|
4443
|
+
}, []);
|
|
4444
|
+
const waitForActivation = react.useCallback(() => {
|
|
4445
|
+
if (activatedRef.current) return Promise.resolve();
|
|
4446
|
+
return new Promise((resolve) => {
|
|
4447
|
+
activationWaitersRef.current.push(resolve);
|
|
4448
|
+
});
|
|
4449
|
+
}, []);
|
|
4450
|
+
const lazyAdapter = react.useMemo(() => {
|
|
4451
|
+
const GATED_METHODS = [
|
|
4452
|
+
"fetchTasks",
|
|
4453
|
+
"fetchTaskTypes",
|
|
4454
|
+
"fetchTaskLists",
|
|
4455
|
+
"fetchCollaborators",
|
|
4456
|
+
"fetchCapabilities",
|
|
4457
|
+
"getAppLinkStatus",
|
|
4458
|
+
"fetchBugReports",
|
|
4459
|
+
"fetchBugReportTypes"
|
|
4460
|
+
];
|
|
4461
|
+
const wrapped = { ...adapter };
|
|
4462
|
+
for (const method of GATED_METHODS) {
|
|
4463
|
+
const original = adapter[method].bind(adapter);
|
|
4464
|
+
wrapped[method] = (...args) => waitForActivation().then(() => original(...args));
|
|
4465
|
+
}
|
|
4466
|
+
let idleScheduled = false;
|
|
4467
|
+
const originalUnreadCounts = adapter.fetchUnreadCounts.bind(adapter);
|
|
4468
|
+
wrapped.fetchUnreadCounts = () => {
|
|
4469
|
+
if (idleScheduled || typeof window === "undefined") return originalUnreadCounts();
|
|
4470
|
+
idleScheduled = true;
|
|
4471
|
+
return new Promise((resolve, reject) => {
|
|
4472
|
+
const run = () => originalUnreadCounts().then(resolve, reject);
|
|
4473
|
+
const ric = window.requestIdleCallback;
|
|
4474
|
+
if (ric) ric(run);
|
|
4475
|
+
else setTimeout(run, 0);
|
|
4476
|
+
});
|
|
4477
|
+
};
|
|
4478
|
+
return wrapped;
|
|
4479
|
+
}, [adapter, waitForActivation]);
|
|
4480
|
+
if (devNotes?.providedByApp) return /* @__PURE__ */ jsxRuntime.jsx(DevNotesSurface, { activate, children });
|
|
4481
|
+
return /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesProvider, { adapter: lazyAdapter, user, config, children: /* @__PURE__ */ jsxRuntime.jsx(DevNotesSurface, { activate, children }) });
|
|
4142
4482
|
}
|
|
4143
4483
|
var DevNotesTasksContext = react.createContext({
|
|
4144
4484
|
ready: false,
|
|
4145
4485
|
openTasks: () => {
|
|
4486
|
+
},
|
|
4487
|
+
activate: () => {
|
|
4146
4488
|
}
|
|
4147
4489
|
});
|
|
4148
|
-
function DevNotesSurface({
|
|
4490
|
+
function DevNotesSurface({
|
|
4491
|
+
activate,
|
|
4492
|
+
children
|
|
4493
|
+
}) {
|
|
4149
4494
|
const [tasksOpen, setTasksOpen] = react.useState(false);
|
|
4150
|
-
const openTasks = react.useCallback(() =>
|
|
4151
|
-
|
|
4495
|
+
const openTasks = react.useCallback(() => {
|
|
4496
|
+
activate();
|
|
4497
|
+
setTasksOpen(true);
|
|
4498
|
+
}, [activate]);
|
|
4499
|
+
const tasksCtx = react.useMemo(() => ({ ready: true, openTasks, activate }), [openTasks, activate]);
|
|
4152
4500
|
return /* @__PURE__ */ jsxRuntime.jsxs(DevNotesTasksContext.Provider, { value: tasksCtx, children: [
|
|
4153
4501
|
children,
|
|
4154
4502
|
tasksOpen ? /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesTaskListModal, { open: true, onClose: () => setTasksOpen(false) }) : null,
|
|
@@ -4204,14 +4552,15 @@ function ChromeTopBarActions() {
|
|
|
4204
4552
|
children: /* @__PURE__ */ jsxRuntime.jsx(ChromeDevNotesMenu, {})
|
|
4205
4553
|
}
|
|
4206
4554
|
),
|
|
4555
|
+
/* @__PURE__ */ jsxRuntime.jsx(SpecOverlayAction, { orgId: state.orgId }),
|
|
4207
4556
|
/* @__PURE__ */ jsxRuntime.jsx(SupportAction, { orgId: state.orgId }),
|
|
4208
4557
|
/* @__PURE__ */ jsxRuntime.jsx(NotificationsAction, { orgId: state.orgId })
|
|
4209
4558
|
] });
|
|
4210
4559
|
}
|
|
4211
4560
|
function ChromeDevNotesMenu() {
|
|
4212
|
-
const { ready, openTasks } = react.useContext(DevNotesTasksContext);
|
|
4561
|
+
const { ready, openTasks, activate } = react.useContext(DevNotesTasksContext);
|
|
4213
4562
|
if (!ready) return null;
|
|
4214
|
-
return /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesMenu, { position: "top-right", onViewTasks: openTasks });
|
|
4563
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { display: "contents" }, onPointerDownCapture: activate, onFocusCapture: activate, onKeyDownCapture: activate, children: /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesMenu, { position: "top-right", onViewTasks: openTasks }) });
|
|
4215
4564
|
}
|
|
4216
4565
|
function useIsSuperAdmin() {
|
|
4217
4566
|
const state = useChromeState();
|