@the-portland-company/shell 0.82.0 → 0.85.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 +39 -0
- package/dist/app-chrome.cjs +284 -182
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.d.cts +17 -2
- package/dist/app-chrome.d.ts +17 -2
- package/dist/app-chrome.js +63 -14
- package/dist/app-chrome.js.map +1 -1
- package/dist/chrome.css +5 -0
- package/dist/{chunk-VWY6IIAR.js → chunk-OOVS5D2K.js} +3 -3
- package/dist/chunk-OOVS5D2K.js.map +1 -0
- package/dist/{chunk-L73FZKGE.js → chunk-PRZP2SB6.js} +229 -176
- package/dist/chunk-PRZP2SB6.js.map +1 -0
- package/dist/{index-CwTnsR9R.d.ts → index-C31E7M9l.d.ts} +70 -4
- package/dist/{index-C3PFq1hl.d.cts → index-bfbWLC7_.d.cts} +70 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/native.cjs +233 -172
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +1 -1
- package/dist/native.d.ts +1 -1
- package/dist/native.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-L73FZKGE.js.map +0 -1
- package/dist/chunk-VWY6IIAR.js.map +0 -1
package/dist/app-chrome.cjs
CHANGED
|
@@ -423,6 +423,81 @@ function SpeculationRulesScript({ items }) {
|
|
|
423
423
|
return null;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
// src/native/credits.ts
|
|
427
|
+
var CREDIT_TYPE_ORDER = [
|
|
428
|
+
"signal",
|
|
429
|
+
"broadcast",
|
|
430
|
+
"ai",
|
|
431
|
+
"enhancement"
|
|
432
|
+
];
|
|
433
|
+
var CREDIT_TYPE_LABELS = {
|
|
434
|
+
signal: "Signal Credits",
|
|
435
|
+
broadcast: "Broadcast Credits",
|
|
436
|
+
ai: "AI Credits",
|
|
437
|
+
enhancement: "Enhancement Credits"
|
|
438
|
+
};
|
|
439
|
+
var RANK = {
|
|
440
|
+
unknown: 0,
|
|
441
|
+
good: 1,
|
|
442
|
+
low: 2,
|
|
443
|
+
out: 3
|
|
444
|
+
};
|
|
445
|
+
function creditsIconState(channels) {
|
|
446
|
+
let worst = "unknown";
|
|
447
|
+
for (const channel of channels) {
|
|
448
|
+
const state = channel?.state ?? null;
|
|
449
|
+
if (state === null || state === "not_in_use") continue;
|
|
450
|
+
if (RANK[state] > RANK[worst]) worst = state;
|
|
451
|
+
}
|
|
452
|
+
return worst;
|
|
453
|
+
}
|
|
454
|
+
function creditGaugeRatio(channel) {
|
|
455
|
+
if (!channel || channel.state == null || channel.state === "not_in_use")
|
|
456
|
+
return null;
|
|
457
|
+
const fullMark = channel.fullMarkUnits ?? 0;
|
|
458
|
+
if (!(fullMark > 0)) return null;
|
|
459
|
+
return Math.min(1, Math.max(0, channel.remainingUnits / fullMark));
|
|
460
|
+
}
|
|
461
|
+
function creditStateWord(state) {
|
|
462
|
+
switch (state) {
|
|
463
|
+
case "good":
|
|
464
|
+
return "Good";
|
|
465
|
+
case "low":
|
|
466
|
+
return "Low";
|
|
467
|
+
case "out":
|
|
468
|
+
return "Out";
|
|
469
|
+
case "not_in_use":
|
|
470
|
+
return "Not in use";
|
|
471
|
+
default:
|
|
472
|
+
return "Unknown";
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function daysPhrase(days) {
|
|
476
|
+
if (days < 1) return "less than a day at your recent pace";
|
|
477
|
+
const rounded = Math.round(days);
|
|
478
|
+
return `about ${rounded} ${rounded === 1 ? "day" : "days"} at your recent pace`;
|
|
479
|
+
}
|
|
480
|
+
function creditTooltipLine(type, channel) {
|
|
481
|
+
const label = CREDIT_TYPE_LABELS[type];
|
|
482
|
+
if (!channel) return `${label}: unavailable`;
|
|
483
|
+
const left = `${Math.max(0, channel.remainingUnits).toLocaleString()} left`;
|
|
484
|
+
if (channel.state === "not_in_use") return `${label}: not in use`;
|
|
485
|
+
if (channel.state == null) return `${label}: ${left}`;
|
|
486
|
+
if (channel.state === "out") return `${label}: ${left}, out`;
|
|
487
|
+
const days = channel.daysOfUseLeft;
|
|
488
|
+
const pace = typeof days === "number" && Number.isFinite(days) ? `, ${daysPhrase(days)}` : "";
|
|
489
|
+
return `${label}: ${left}${pace}`;
|
|
490
|
+
}
|
|
491
|
+
function creditsAriaLabel(channels) {
|
|
492
|
+
const worst = creditsIconState(CREDIT_TYPE_ORDER.map((t) => channels[t]));
|
|
493
|
+
if (worst === "unknown") return "Credits";
|
|
494
|
+
if (worst === "good") return "Credits: all good";
|
|
495
|
+
const named = CREDIT_TYPE_ORDER.filter(
|
|
496
|
+
(t) => channels[t]?.state === worst
|
|
497
|
+
).map((t) => CREDIT_TYPE_LABELS[t]);
|
|
498
|
+
return `Credits: ${worst === "low" ? "low on" : "out of"} ${named.join(", ")}`;
|
|
499
|
+
}
|
|
500
|
+
|
|
426
501
|
// src/notifications/adapter.ts
|
|
427
502
|
function emptyNotificationsFeed() {
|
|
428
503
|
return { notifications: [], badge: 0 };
|
|
@@ -517,7 +592,7 @@ function resolveOrgLogoUrl(url) {
|
|
|
517
592
|
}
|
|
518
593
|
var TOPBAR_GLYPH_SIZE = 17;
|
|
519
594
|
function topBarIconButtonClass(tone = "default") {
|
|
520
|
-
const toneClass = tone === "danger" || tone === "warning" ? "text-[color:var(--chrome-danger)] hover:text-[color:var(--chrome-danger)]" : "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)]";
|
|
595
|
+
const toneClass = tone === "danger" || tone === "warning" ? "text-[color:var(--chrome-danger)] hover:text-[color:var(--chrome-danger)]" : tone === "ok" ? "text-[color:var(--credits-good,#00783B)] hover:text-[color:var(--credits-good,#00783B)]" : tone === "low" ? "text-[color:var(--credits-low,#B7791F)] hover:text-[color:var(--credits-low,#B7791F)]" : "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)]";
|
|
521
596
|
return [
|
|
522
597
|
// `group/tbtip` powers the shared <TopBarTooltip> rendered inside the trigger.
|
|
523
598
|
"group/tbtip relative flex items-center justify-center rounded-[var(--topbar-icon-button-radius,5px)] border border-transparent transition",
|
|
@@ -746,6 +821,9 @@ function ValueSkeleton({ className }) {
|
|
|
746
821
|
}
|
|
747
822
|
);
|
|
748
823
|
}
|
|
824
|
+
function IconGlyph({ icon: Icon, size = TOPBAR_GLYPH_SIZE }) {
|
|
825
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Icon, { size, className: "text-current" });
|
|
826
|
+
}
|
|
749
827
|
function SlotGlyph({
|
|
750
828
|
slot,
|
|
751
829
|
fallback: Fallback,
|
|
@@ -758,9 +836,12 @@ function SlotGlyph({
|
|
|
758
836
|
}
|
|
759
837
|
function TopBarPopover({
|
|
760
838
|
label,
|
|
839
|
+
ariaLabel,
|
|
761
840
|
description,
|
|
841
|
+
tooltip,
|
|
762
842
|
slot,
|
|
763
843
|
fallbackIcon,
|
|
844
|
+
icon,
|
|
764
845
|
badge,
|
|
765
846
|
tone,
|
|
766
847
|
onOpen,
|
|
@@ -810,7 +891,7 @@ function TopBarPopover({
|
|
|
810
891
|
"button",
|
|
811
892
|
{
|
|
812
893
|
type: "button",
|
|
813
|
-
"aria-label": label,
|
|
894
|
+
"aria-label": ariaLabel ?? label,
|
|
814
895
|
"aria-expanded": open,
|
|
815
896
|
className: topBarIconButtonClass(tone),
|
|
816
897
|
onClick: () => {
|
|
@@ -821,9 +902,9 @@ function TopBarPopover({
|
|
|
821
902
|
});
|
|
822
903
|
},
|
|
823
904
|
children: [
|
|
824
|
-
/* @__PURE__ */ jsxRuntime.jsx(SlotGlyph, { slot, fallback: fallbackIcon }),
|
|
905
|
+
icon ? /* @__PURE__ */ jsxRuntime.jsx(IconGlyph, { icon }) : /* @__PURE__ */ jsxRuntime.jsx(SlotGlyph, { slot, fallback: fallbackIcon }),
|
|
825
906
|
typeof badge === "number" ? /* @__PURE__ */ jsxRuntime.jsx(TopBarCountBadge, { count: badge }) : null,
|
|
826
|
-
!open && !hideTooltip ? /* @__PURE__ */ jsxRuntime.jsx(TopBarTooltip, { title: label, description }) : null
|
|
907
|
+
!open && !hideTooltip ? /* @__PURE__ */ jsxRuntime.jsx(TopBarTooltip, { title: label, description: tooltip ?? description }) : null
|
|
827
908
|
]
|
|
828
909
|
}
|
|
829
910
|
),
|
|
@@ -897,147 +978,145 @@ function XGlyph({ size = 14, className }) {
|
|
|
897
978
|
function CheckGlyph({ size = 14, className }) {
|
|
898
979
|
return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "20 6 9 17 4 12" }) });
|
|
899
980
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
981
|
+
function batteryFillWidth(state) {
|
|
982
|
+
switch (state) {
|
|
983
|
+
case "good":
|
|
984
|
+
return 12;
|
|
985
|
+
case "low":
|
|
986
|
+
return 5;
|
|
987
|
+
default:
|
|
988
|
+
return 0;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function creditsBatteryGlyph(state) {
|
|
992
|
+
const fill = batteryFillWidth(state);
|
|
993
|
+
const Glyph = ({ size = TOPBAR_GLYPH_SIZE, className }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
994
|
+
"svg",
|
|
995
|
+
{
|
|
996
|
+
width: size,
|
|
997
|
+
height: size,
|
|
998
|
+
viewBox: "0 0 24 24",
|
|
999
|
+
fill: "none",
|
|
1000
|
+
stroke: "currentColor",
|
|
1001
|
+
strokeWidth: 2,
|
|
1002
|
+
strokeLinecap: "round",
|
|
1003
|
+
strokeLinejoin: "round",
|
|
1004
|
+
className,
|
|
1005
|
+
"data-credits-state": state,
|
|
1006
|
+
"aria-hidden": true,
|
|
1007
|
+
children: [
|
|
1008
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "1", y: "6", width: "18", height: "12", rx: "2", ry: "2" }),
|
|
1009
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "23", y1: "10", x2: "23", y2: "14" }),
|
|
1010
|
+
fill > 0 ? /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "4", y: "9", width: fill, height: "6", rx: "1", fill: "currentColor", stroke: "none", opacity: 0.35 }) : null,
|
|
1011
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M11.5 8 8.5 12.5h3L10.5 16l3.5-4.5h-3L11.5 8z", fill: "currentColor", stroke: "none" })
|
|
1012
|
+
]
|
|
1013
|
+
}
|
|
1014
|
+
);
|
|
1015
|
+
Glyph.displayName = `CreditsBatteryGlyph(${state})`;
|
|
1016
|
+
return Glyph;
|
|
1017
|
+
}
|
|
1018
|
+
function stateTone(state) {
|
|
1019
|
+
switch (state) {
|
|
1020
|
+
case "good":
|
|
1021
|
+
return "ok";
|
|
1022
|
+
case "low":
|
|
1023
|
+
return "low";
|
|
1024
|
+
case "out":
|
|
1025
|
+
return "danger";
|
|
1026
|
+
default:
|
|
1027
|
+
return "default";
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function stateColorVar(state) {
|
|
1031
|
+
switch (state) {
|
|
1032
|
+
case "good":
|
|
1033
|
+
return "var(--credits-good,#00783B)";
|
|
1034
|
+
case "low":
|
|
1035
|
+
return "var(--credits-low,#B7791F)";
|
|
1036
|
+
case "out":
|
|
1037
|
+
return "var(--chrome-danger)";
|
|
1038
|
+
default:
|
|
1039
|
+
return "var(--muted)";
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
function CreditGauge({ channel, className }) {
|
|
1043
|
+
const ratio = creditGaugeRatio(channel);
|
|
1044
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1045
|
+
"div",
|
|
1046
|
+
{
|
|
1047
|
+
role: "meter",
|
|
1048
|
+
"aria-label": `${creditStateWord(channel?.state)} level`,
|
|
1049
|
+
"aria-valuemin": 0,
|
|
1050
|
+
"aria-valuemax": 100,
|
|
1051
|
+
"aria-valuenow": ratio === null ? void 0 : Math.round(ratio * 100),
|
|
1052
|
+
className: ["h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel)]", className ?? ""].join(" "),
|
|
1053
|
+
children: ratio === null ? null : /* @__PURE__ */ jsxRuntime.jsx(
|
|
1054
|
+
"div",
|
|
1055
|
+
{
|
|
1056
|
+
className: "h-full rounded-full transition-[width]",
|
|
1057
|
+
style: { width: `${Math.round(ratio * 100)}%`, backgroundColor: stateColorVar(channel?.state) }
|
|
1058
|
+
}
|
|
1059
|
+
)
|
|
1060
|
+
}
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
function formatGrantDate(iso) {
|
|
1064
|
+
if (!iso) return null;
|
|
1065
|
+
const t = Date.parse(iso);
|
|
1066
|
+
if (!Number.isFinite(t)) return null;
|
|
1067
|
+
return new Date(t).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
1068
|
+
}
|
|
1069
|
+
function CreditRow({
|
|
1070
|
+
type,
|
|
903
1071
|
channel,
|
|
904
1072
|
loading,
|
|
905
|
-
addHref
|
|
906
|
-
addLabel
|
|
1073
|
+
addHref
|
|
907
1074
|
}) {
|
|
1075
|
+
const label = CREDIT_TYPE_LABELS[type];
|
|
908
1076
|
const remaining = channel?.remainingUnits ?? 0;
|
|
909
|
-
const
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
|
|
1077
|
+
const state = channel?.state ?? null;
|
|
1078
|
+
const unavailable = !loading && !channel;
|
|
1079
|
+
const nextGrant = formatGrantDate(channel?.nextGrantAt);
|
|
1080
|
+
const pools = channel && typeof channel.includedUnits === "number" && typeof channel.purchasedUnits === "number" ? `Included ${channel.includedUnits.toLocaleString()}${nextGrant ? ` (next ${nextGrant})` : ""} \xB7 Purchased ${channel.purchasedUnits.toLocaleString()}` : null;
|
|
1081
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", "data-credit-type": type, children: [
|
|
913
1082
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-1 flex items-center justify-between gap-2", children: [
|
|
914
1083
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold text-[color:var(--foreground)]", children: label }),
|
|
915
1084
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
916
|
-
loading ? /* @__PURE__ */ jsxRuntime.jsx(ValueSkeleton, { className: "h-3.5 w-14" }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
" left"
|
|
926
|
-
]
|
|
927
|
-
}
|
|
928
|
-
),
|
|
929
|
-
addHref ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1085
|
+
loading ? /* @__PURE__ */ jsxRuntime.jsx(ValueSkeleton, { className: "h-3.5 w-14" }) : unavailable ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-[color:var(--muted)]", children: "Unavailable" }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-semibold tabular-nums", style: { color: stateColorVar(state) }, children: [
|
|
1086
|
+
remaining.toLocaleString(),
|
|
1087
|
+
" left",
|
|
1088
|
+
state === "not_in_use" ? null : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-normal text-[color:var(--muted)]", children: [
|
|
1089
|
+
" \xB7 ",
|
|
1090
|
+
creditStateWord(state)
|
|
1091
|
+
] })
|
|
1092
|
+
] }),
|
|
1093
|
+
addHref && !unavailable ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
930
1094
|
"a",
|
|
931
1095
|
{
|
|
932
1096
|
href: addHref,
|
|
933
|
-
"aria-label":
|
|
1097
|
+
"aria-label": `Add ${label}`,
|
|
934
1098
|
className: "flex h-5 w-5 items-center justify-center rounded-full border border-[color:var(--line)] text-[color:var(--muted)] transition hover:border-[color:var(--foreground)] hover:text-[color:var(--foreground)]",
|
|
935
1099
|
children: /* @__PURE__ */ jsxRuntime.jsx(PlusGlyph, {})
|
|
936
1100
|
}
|
|
937
1101
|
) : null
|
|
938
1102
|
] })
|
|
939
1103
|
] }),
|
|
940
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel)]", children:
|
|
941
|
-
|
|
942
|
-
{
|
|
943
|
-
className: [
|
|
944
|
-
"h-full rounded-full transition-[width]",
|
|
945
|
-
low ? "bg-[color:var(--chrome-danger)]" : "bg-[color:var(--color-ok,#00783B)]"
|
|
946
|
-
].join(" "),
|
|
947
|
-
style: { width: `${ratio}%` }
|
|
948
|
-
}
|
|
949
|
-
) })
|
|
1104
|
+
loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel)]", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full w-full animate-pulse bg-[color:var(--line)]" }) }) : /* @__PURE__ */ jsxRuntime.jsx(CreditGauge, { channel: unavailable ? null : channel }),
|
|
1105
|
+
!loading && state === "not_in_use" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-[11px] text-[color:var(--muted)]", children: "Not in use" }) : pools ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-[11px] text-[color:var(--muted)]", children: pools }) : null
|
|
950
1106
|
] });
|
|
951
1107
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1108
|
+
function CreditsTooltip({ fuel }) {
|
|
1109
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex w-56 flex-col gap-1.5", "data-testid": "credits-tooltip", children: CREDIT_TYPE_ORDER.map((type) => {
|
|
1110
|
+
const channel = fuel ? fuel[type] ?? null : null;
|
|
1111
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex flex-col gap-0.5", children: [
|
|
1112
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: creditTooltipLine(type, channel) }),
|
|
1113
|
+
/* @__PURE__ */ jsxRuntime.jsx(CreditGauge, { channel, className: "bg-[#FFFFFF]/20" })
|
|
1114
|
+
] }, type);
|
|
1115
|
+
}) });
|
|
958
1116
|
}
|
|
959
|
-
function
|
|
960
|
-
|
|
961
|
-
return
|
|
962
|
-
}
|
|
963
|
-
function useLowFuelHintVisible() {
|
|
964
|
-
return react.useSyncExternalStore(
|
|
965
|
-
subscribeLowFuelHintVisible,
|
|
966
|
-
() => lowFuelHintVisible,
|
|
967
|
-
() => false
|
|
968
|
-
);
|
|
969
|
-
}
|
|
970
|
-
var LOW_FUEL_HINT_CLEARANCE_PX = 96;
|
|
971
|
-
function LowFuelHint({ lowFuel, orgId }) {
|
|
972
|
-
const [dismissed, setDismissed] = react.useState(() => {
|
|
973
|
-
if (typeof window === "undefined") return false;
|
|
974
|
-
try {
|
|
975
|
-
return window.sessionStorage.getItem(LOW_FUEL_HINT_DISMISSED_KEY) === "1";
|
|
976
|
-
} catch {
|
|
977
|
-
return false;
|
|
978
|
-
}
|
|
979
|
-
});
|
|
980
|
-
react.useEffect(() => {
|
|
981
|
-
if (lowFuel) return;
|
|
982
|
-
try {
|
|
983
|
-
window.sessionStorage.removeItem(LOW_FUEL_HINT_DISMISSED_KEY);
|
|
984
|
-
} catch {
|
|
985
|
-
}
|
|
986
|
-
}, [lowFuel]);
|
|
987
|
-
const panelOpen = useAnyTopBarPanelOpen();
|
|
988
|
-
const showing = lowFuel && !dismissed && !panelOpen;
|
|
989
|
-
react.useEffect(() => {
|
|
990
|
-
setLowFuelHintVisible(showing);
|
|
991
|
-
return () => setLowFuelHintVisible(false);
|
|
992
|
-
}, [showing]);
|
|
993
|
-
if (!showing) return null;
|
|
994
|
-
const dismiss = () => {
|
|
995
|
-
try {
|
|
996
|
-
window.sessionStorage.setItem(LOW_FUEL_HINT_DISMISSED_KEY, "1");
|
|
997
|
-
} catch {
|
|
998
|
-
}
|
|
999
|
-
setDismissed(true);
|
|
1000
|
-
};
|
|
1001
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1002
|
-
"div",
|
|
1003
|
-
{
|
|
1004
|
-
role: "status",
|
|
1005
|
-
className: "absolute right-0 top-[calc(100%+10px)] z-[60] w-60 rounded-lg border border-[color:var(--chrome-danger)] bg-[color:var(--chrome-danger)] px-3 py-2 text-[#FFFFFF] shadow-[var(--shadow-soft)]",
|
|
1006
|
-
onClick: (e) => e.stopPropagation(),
|
|
1007
|
-
children: [
|
|
1008
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1009
|
-
"span",
|
|
1010
|
-
{
|
|
1011
|
-
"aria-hidden": true,
|
|
1012
|
-
className: "absolute -top-[5px] right-[11px] h-2.5 w-2.5 rotate-45 bg-[color:var(--chrome-danger)]"
|
|
1013
|
-
}
|
|
1014
|
-
),
|
|
1015
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
|
|
1016
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
|
|
1017
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold", children: "You\u2019re low on fuel" }),
|
|
1018
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1019
|
-
"a",
|
|
1020
|
-
{
|
|
1021
|
-
href: withOrgParam(CORE_BILLING_URL, orgId),
|
|
1022
|
-
className: "mt-1 inline-block text-sm font-bold underline hover:opacity-90",
|
|
1023
|
-
children: "Buy More Fuel"
|
|
1024
|
-
}
|
|
1025
|
-
)
|
|
1026
|
-
] }),
|
|
1027
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1028
|
-
"button",
|
|
1029
|
-
{
|
|
1030
|
-
type: "button",
|
|
1031
|
-
"aria-label": "Dismiss low fuel notice",
|
|
1032
|
-
onClick: dismiss,
|
|
1033
|
-
className: "flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[#FFFFFF]/90 transition hover:bg-[#FFFFFF]/20",
|
|
1034
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(XGlyph, {})
|
|
1035
|
-
}
|
|
1036
|
-
)
|
|
1037
|
-
] })
|
|
1038
|
-
]
|
|
1039
|
-
}
|
|
1040
|
-
);
|
|
1117
|
+
function channelsOf(fuel) {
|
|
1118
|
+
if (!fuel) return {};
|
|
1119
|
+
return { signal: fuel.signal, broadcast: fuel.broadcast, ai: fuel.ai, enhancement: fuel.enhancement };
|
|
1041
1120
|
}
|
|
1042
1121
|
function FuelAction({ orgId }) {
|
|
1043
1122
|
const { dedupedFetch } = useNavChrome("FuelAction");
|
|
@@ -1069,50 +1148,25 @@ function FuelAction({ orgId }) {
|
|
|
1069
1148
|
cancelled = true;
|
|
1070
1149
|
};
|
|
1071
1150
|
}, [dedupedFetch, orgId]);
|
|
1072
|
-
const
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
FuelRow,
|
|
1092
|
-
{
|
|
1093
|
-
label: "SMS Signal",
|
|
1094
|
-
channel: fuel?.signal ?? null,
|
|
1095
|
-
loading,
|
|
1096
|
-
addHref: withOrgParam(CORE_BILLING_URL, orgId),
|
|
1097
|
-
addLabel: "Add SMS Signal fuel"
|
|
1098
|
-
}
|
|
1099
|
-
),
|
|
1100
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1101
|
-
FuelRow,
|
|
1102
|
-
{
|
|
1103
|
-
label: "Email Broadcast",
|
|
1104
|
-
channel: fuel?.broadcast ?? null,
|
|
1105
|
-
loading,
|
|
1106
|
-
addHref: withOrgParam(CORE_BILLING_URL, orgId),
|
|
1107
|
-
addLabel: "Add Email Broadcast fuel"
|
|
1108
|
-
}
|
|
1109
|
-
)
|
|
1110
|
-
] })
|
|
1111
|
-
] })
|
|
1112
|
-
}
|
|
1113
|
-
),
|
|
1114
|
-
/* @__PURE__ */ jsxRuntime.jsx(LowFuelHint, { lowFuel: low, orgId })
|
|
1115
|
-
] })
|
|
1151
|
+
const channels = channelsOf(fuel);
|
|
1152
|
+
const iconState = creditsIconState(CREDIT_TYPE_ORDER.map((t) => channels[t]));
|
|
1153
|
+
const Glyph = react.useMemo(() => creditsBatteryGlyph(iconState), [iconState]);
|
|
1154
|
+
const addHref = withOrgParam(CORE_BILLING_URL, orgId);
|
|
1155
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1156
|
+
TopBarPopover,
|
|
1157
|
+
{
|
|
1158
|
+
label: "Credits",
|
|
1159
|
+
ariaLabel: creditsAriaLabel(channels),
|
|
1160
|
+
tooltip: /* @__PURE__ */ jsxRuntime.jsx(CreditsTooltip, { fuel }),
|
|
1161
|
+
slot: "comms-fuel",
|
|
1162
|
+
fallbackIcon: BatteryGlyph,
|
|
1163
|
+
icon: Glyph,
|
|
1164
|
+
tone: stateTone(iconState),
|
|
1165
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
|
|
1166
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Credits" }),
|
|
1167
|
+
error || !loading && !fuel ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-[color:var(--muted)]", children: "Credits are unavailable right now." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-3", children: CREDIT_TYPE_ORDER.map((type) => /* @__PURE__ */ jsxRuntime.jsx(CreditRow, { type, channel: channels[type] ?? null, loading, addHref }, type)) })
|
|
1168
|
+
] })
|
|
1169
|
+
}
|
|
1116
1170
|
);
|
|
1117
1171
|
}
|
|
1118
1172
|
function notifStateKey(orgId) {
|
|
@@ -1193,7 +1247,6 @@ function NewReplyToast({
|
|
|
1193
1247
|
const [visible, setVisible] = react.useState(false);
|
|
1194
1248
|
const onCloseRef = react.useRef(onClose);
|
|
1195
1249
|
onCloseRef.current = onClose;
|
|
1196
|
-
const lowFuelHintVisible2 = useLowFuelHintVisible();
|
|
1197
1250
|
react.useEffect(() => {
|
|
1198
1251
|
setVisible(true);
|
|
1199
1252
|
const fadeAt = Math.max(0, ttlMs - 450);
|
|
@@ -1223,7 +1276,6 @@ function NewReplyToast({
|
|
|
1223
1276
|
},
|
|
1224
1277
|
className: "absolute right-[-6px] top-[calc(100%+12px)] z-50 min-w-[240px] max-w-[320px] cursor-pointer rounded-xl bg-[#161616] px-4 py-3 text-[#FFFFFF] shadow-[var(--shadow-soft)]",
|
|
1225
1278
|
style: {
|
|
1226
|
-
top: lowFuelHintVisible2 ? `calc(100% + 12px + ${LOW_FUEL_HINT_CLEARANCE_PX}px)` : void 0,
|
|
1227
1279
|
opacity: visible ? 1 : 0,
|
|
1228
1280
|
transform: visible ? "translateY(0px)" : "translateY(-4px)",
|
|
1229
1281
|
transition: "opacity 250ms ease, transform 250ms ease"
|
|
@@ -4541,10 +4593,19 @@ var CORE_APP = "vrm";
|
|
|
4541
4593
|
var DEFAULT_SIGN_OUT_URL = "/auth/signout";
|
|
4542
4594
|
var POLITOGY_APPS = {
|
|
4543
4595
|
vrm: { title: "Politogy VRM", origin: "https://app.politogyvrm.com", storagePrefix: "vrm" },
|
|
4596
|
+
// Contacts, Forms, Surveys and CTAs are Relationship rows only (Specs/
|
|
4597
|
+
// Left-Navigation §9.1; Campaign has none of them, §9.2), so they are
|
|
4598
|
+
// PINNED to Relationship like the Dashboard below. Left unscoped, the mode
|
|
4599
|
+
// switcher and rail colour took the user's last mode choice while the rows
|
|
4600
|
+
// stayed Relationship: after picking Campaign, Contacts drew the red
|
|
4601
|
+
// "Campaign mode" rail around Relationship rows, even after a reload
|
|
4602
|
+
// (measured 2026-10-16 on We need Steve; §11.1 "the rail never shows a mix
|
|
4603
|
+
// of two modes").
|
|
4544
4604
|
contacts: {
|
|
4545
4605
|
title: "Contacts",
|
|
4546
4606
|
origin: "https://contacts.politogyvrm.com",
|
|
4547
4607
|
storagePrefix: "contacts",
|
|
4608
|
+
mode: "relationship",
|
|
4548
4609
|
pageTitle: false
|
|
4549
4610
|
},
|
|
4550
4611
|
// 'title' for forms/surveys/polls/ctas for the same reason as petitions
|
|
@@ -4553,11 +4614,11 @@ var POLITOGY_APPS = {
|
|
|
4553
4614
|
// page header falls back to the static registry title forever, so the
|
|
4554
4615
|
// designed "<mode> mode" eyebrow + H2 + subtitle block never gets the real
|
|
4555
4616
|
// per-page title an app passes it (UI-Platform → Page header).
|
|
4556
|
-
forms: { title: "Forms", origin: "https://forms.politogyvrm.com", storagePrefix: "forms", grants: ["title"] },
|
|
4557
|
-
surveys: { title: "Surveys", origin: "https://surveys.politogyvrm.com", storagePrefix: "surveys", grants: ["title"] },
|
|
4617
|
+
forms: { title: "Forms", origin: "https://forms.politogyvrm.com", storagePrefix: "forms", mode: "relationship", grants: ["title"] },
|
|
4618
|
+
surveys: { title: "Surveys", origin: "https://surveys.politogyvrm.com", storagePrefix: "surveys", mode: "relationship", grants: ["title"] },
|
|
4558
4619
|
// Specs/Left-Navigation §9.2: Polling is a Campaign row (Research).
|
|
4559
4620
|
polls: { title: "Polls", origin: "https://polls.politogyvrm.com", storagePrefix: "polls", mode: "campaign", grants: ["title"] },
|
|
4560
|
-
ctas: { title: "CTAs", origin: "https://ctas.politogyvrm.com", storagePrefix: "ctas", grants: ["title"] },
|
|
4621
|
+
ctas: { title: "CTAs", origin: "https://ctas.politogyvrm.com", storagePrefix: "ctas", mode: "relationship", grants: ["title"] },
|
|
4561
4622
|
// V2 apps that replace retired app.politogyvrm.com pages (Reference/
|
|
4562
4623
|
// UI-Platform v42 "Retirement of app.politogyvrm.com"). Dashboard is the
|
|
4563
4624
|
// Relationship Dashboard, the Relationship Mode landing page, and nothing
|
|
@@ -4781,7 +4842,7 @@ function FrameBody({
|
|
|
4781
4842
|
const coreOrgs = chrome?.orgs ?? [];
|
|
4782
4843
|
const activeCoreOrg = coreOrgs.find((o) => o.id === chrome?.orgId) ?? null;
|
|
4783
4844
|
const navDisplay = chrome?.navDisplay ?? "both";
|
|
4784
|
-
const navMode = appMode ?? "relationship";
|
|
4845
|
+
const navMode = appMode ?? (app !== CORE_APP && isStandardNavMode(mode.id) ? mode.id : "relationship");
|
|
4785
4846
|
const activeOrgId = chrome?.orgId ?? null;
|
|
4786
4847
|
const derivedNavItems = react.useMemo(
|
|
4787
4848
|
() => standardPolitogyNav({
|
|
@@ -4824,7 +4885,7 @@ function FrameBody({
|
|
|
4824
4885
|
{
|
|
4825
4886
|
visible: bootVisible,
|
|
4826
4887
|
bootProgress,
|
|
4827
|
-
syntheticReady: appReady ?? chromeResolved,
|
|
4888
|
+
syntheticReady: (appReady ?? chromeResolved) && !chrome?.orgPending && (appMode !== void 0 || mode.resolved !== false),
|
|
4828
4889
|
onExitComplete: () => setBootVisible(false)
|
|
4829
4890
|
},
|
|
4830
4891
|
bootGeneration
|
|
@@ -4914,6 +4975,18 @@ function FrameBody({
|
|
|
4914
4975
|
}
|
|
4915
4976
|
var ORG_STORAGE_KEY = "vrm.activeOrg";
|
|
4916
4977
|
var MODE_STORAGE_KEY = "vrm.activeMode";
|
|
4978
|
+
var orgSwitchLanding = {
|
|
4979
|
+
href(orgId) {
|
|
4980
|
+
return modeLandingHref("relationship", {
|
|
4981
|
+
orgId,
|
|
4982
|
+
hostname: typeof window === "undefined" ? null : window.location.hostname
|
|
4983
|
+
});
|
|
4984
|
+
},
|
|
4985
|
+
navigate(href) {
|
|
4986
|
+
window.location.assign(href);
|
|
4987
|
+
}
|
|
4988
|
+
};
|
|
4989
|
+
var ORG_SWITCH_REEXCHANGE_BUDGET_MS = 4e3;
|
|
4917
4990
|
var ChromeStateContext = react.createContext(null);
|
|
4918
4991
|
function useChromeState() {
|
|
4919
4992
|
return react.useContext(ChromeStateContext);
|
|
@@ -4929,6 +5002,7 @@ function TokenChromeStateProvider({
|
|
|
4929
5002
|
const [pendingOrg, setPendingOrg] = react.useState(null);
|
|
4930
5003
|
const tokenOrgRef = react.useRef(tokenOrg);
|
|
4931
5004
|
tokenOrgRef.current = tokenOrg;
|
|
5005
|
+
const leavingRef = react.useRef(false);
|
|
4932
5006
|
react.useEffect(() => {
|
|
4933
5007
|
setNavDisplayState(resolveNavDisplay());
|
|
4934
5008
|
}, []);
|
|
@@ -4969,6 +5043,7 @@ function TokenChromeStateProvider({
|
|
|
4969
5043
|
}, [overrideOrg, tokenOrg.orgId]);
|
|
4970
5044
|
react.useEffect(() => {
|
|
4971
5045
|
if (!pendingOrg || tokenOrg.orgId !== pendingOrg) return;
|
|
5046
|
+
if (leavingRef.current) return;
|
|
4972
5047
|
setPendingOrg(null);
|
|
4973
5048
|
try {
|
|
4974
5049
|
const next = syncOrgIdInUrl(window.location.href, pendingOrg);
|
|
@@ -4991,8 +5066,16 @@ function TokenChromeStateProvider({
|
|
|
4991
5066
|
window.history.replaceState(window.history.state, "", url.toString());
|
|
4992
5067
|
} catch {
|
|
4993
5068
|
}
|
|
4994
|
-
|
|
5069
|
+
leavingRef.current = true;
|
|
5070
|
+
await Promise.race([
|
|
5071
|
+
Promise.resolve(tokenOrgRef.current.onReexchange(claims.orgId, { perTab: false })).catch(() => {
|
|
5072
|
+
}),
|
|
5073
|
+
new Promise((resolve) => setTimeout(resolve, ORG_SWITCH_REEXCHANGE_BUDGET_MS))
|
|
5074
|
+
]);
|
|
5075
|
+
const href = orgSwitchLanding.href(claims.orgId);
|
|
5076
|
+
if (href) orgSwitchLanding.navigate(href);
|
|
4995
5077
|
} catch (err) {
|
|
5078
|
+
leavingRef.current = false;
|
|
4996
5079
|
setPendingOrg(null);
|
|
4997
5080
|
tokenOrgRef.current.onSwitchError?.(err);
|
|
4998
5081
|
}
|
|
@@ -5057,10 +5140,13 @@ function ChromeStateProvider({
|
|
|
5057
5140
|
const navReconciled = react.useRef(false);
|
|
5058
5141
|
const orgReconciled = react.useRef(false);
|
|
5059
5142
|
const hadBootSelector = react.useRef(false);
|
|
5143
|
+
const bootUrlTail = react.useRef(null);
|
|
5060
5144
|
react.useEffect(() => {
|
|
5061
5145
|
try {
|
|
5146
|
+
const urlSelector = readOrgSelectorFromUrl(window.location.search);
|
|
5147
|
+
if (urlSelector && !isOrgId(urlSelector)) bootUrlTail.current = urlSelector;
|
|
5062
5148
|
const selector = resolveActiveOrg();
|
|
5063
|
-
if (selector) hadBootSelector.current = true;
|
|
5149
|
+
if (selector || urlSelector) hadBootSelector.current = true;
|
|
5064
5150
|
const storedOrg = selector ?? localStorage.getItem(ORG_STORAGE_KEY);
|
|
5065
5151
|
if (storedOrg) setOrgIdState(storedOrg);
|
|
5066
5152
|
} catch {
|
|
@@ -5097,7 +5183,9 @@ function ChromeStateProvider({
|
|
|
5097
5183
|
}, []);
|
|
5098
5184
|
react.useEffect(() => {
|
|
5099
5185
|
if (!orgs || orgs.length === 0) return;
|
|
5100
|
-
|
|
5186
|
+
const fromUrl = matchOrgSelector(bootUrlTail.current, orgs)?.id ?? null;
|
|
5187
|
+
bootUrlTail.current = null;
|
|
5188
|
+
setOrgIdState((prev) => fromUrl ?? (prev && orgs.some((o) => o.id === prev) ? prev : orgs[0].id));
|
|
5101
5189
|
}, [orgs]);
|
|
5102
5190
|
const activeOrgRef = react.useRef(null);
|
|
5103
5191
|
const memberOrgsRef = react.useRef([]);
|
|
@@ -5155,6 +5243,10 @@ function ChromeStateProvider({
|
|
|
5155
5243
|
} catch {
|
|
5156
5244
|
}
|
|
5157
5245
|
writeActiveOrgCookie(id);
|
|
5246
|
+
if (isOrgId(id)) {
|
|
5247
|
+
const href = orgSwitchLanding.href(id);
|
|
5248
|
+
if (href) orgSwitchLanding.navigate(href);
|
|
5249
|
+
}
|
|
5158
5250
|
}, []);
|
|
5159
5251
|
const setNavDisplay = react.useCallback(
|
|
5160
5252
|
(m) => {
|
|
@@ -5221,8 +5313,10 @@ function ChromeStateProvider({
|
|
|
5221
5313
|
}
|
|
5222
5314
|
try {
|
|
5223
5315
|
const url = new URL(window.location.href);
|
|
5224
|
-
|
|
5225
|
-
|
|
5316
|
+
const tail = orgIdTail(activeOrgId);
|
|
5317
|
+
if (url.searchParams.has(ACTIVE_ORG_QUERY_PARAM) || url.searchParams.get(ORG_ID_QUERY_PARAM) !== tail) {
|
|
5318
|
+
url.searchParams.delete(ACTIVE_ORG_QUERY_PARAM);
|
|
5319
|
+
url.searchParams.set(ORG_ID_QUERY_PARAM, tail);
|
|
5226
5320
|
window.history.replaceState(window.history.state, "", url.toString());
|
|
5227
5321
|
}
|
|
5228
5322
|
} catch {
|
|
@@ -5511,6 +5605,7 @@ function useChromeMode(arg = {}) {
|
|
|
5511
5605
|
const orgId = ctx?.orgId ?? null;
|
|
5512
5606
|
const orgPending = ctx?.orgPending ?? false;
|
|
5513
5607
|
const [entitled, setEntitled] = react.useState(null);
|
|
5608
|
+
const [settledOrgId, setSettledOrgId] = react.useState(null);
|
|
5514
5609
|
const [selectedModeId, setSelectedModeId] = react.useState(null);
|
|
5515
5610
|
react.useEffect(() => {
|
|
5516
5611
|
try {
|
|
@@ -5525,6 +5620,7 @@ function useChromeMode(arg = {}) {
|
|
|
5525
5620
|
const bootMe = me;
|
|
5526
5621
|
if (bootMe && bootMe.account === orgId && bootMe.modes !== void 0) {
|
|
5527
5622
|
setEntitled(bootMe.modes ?? null);
|
|
5623
|
+
setSettledOrgId(orgId);
|
|
5528
5624
|
return;
|
|
5529
5625
|
}
|
|
5530
5626
|
let cancelled = false;
|
|
@@ -5532,7 +5628,9 @@ function useChromeMode(arg = {}) {
|
|
|
5532
5628
|
if (cancelled) return;
|
|
5533
5629
|
const modes = data?.modes ?? null;
|
|
5534
5630
|
setEntitled(modes);
|
|
5631
|
+
setSettledOrgId(orgId);
|
|
5535
5632
|
}).catch(() => {
|
|
5633
|
+
if (!cancelled) setSettledOrgId(orgId);
|
|
5536
5634
|
});
|
|
5537
5635
|
return () => {
|
|
5538
5636
|
cancelled = true;
|
|
@@ -5546,6 +5644,9 @@ function useChromeMode(arg = {}) {
|
|
|
5546
5644
|
id: current.id,
|
|
5547
5645
|
label: current.label,
|
|
5548
5646
|
icon: current.icon,
|
|
5647
|
+
// A registry pin is final at once; otherwise the mode is final when the
|
|
5648
|
+
// active org's Modes are in (or nothing can be read: no chrome or no org).
|
|
5649
|
+
resolved: Boolean(pinned) || !dedupedFetch || !orgId || settledOrgId === orgId && !orgPending,
|
|
5549
5650
|
// Left-Navigation §7 / Reference/UI-Platform: always the row of three icon
|
|
5550
5651
|
// buttons. A mode the organization does not hold is shown dimmed and not
|
|
5551
5652
|
// selectable, never hidden. Before entitlements land only the current mode
|
|
@@ -5584,6 +5685,7 @@ exports.deriveBreadcrumbs = deriveBreadcrumbs;
|
|
|
5584
5685
|
exports.footerLinks = footerLinks;
|
|
5585
5686
|
exports.looksLikeEmail = looksLikeEmail;
|
|
5586
5687
|
exports.modeLandingHref = modeLandingHref;
|
|
5688
|
+
exports.orgSwitchLanding = orgSwitchLanding;
|
|
5587
5689
|
exports.resolveIdentity = resolveIdentity;
|
|
5588
5690
|
exports.standardNavIcons = standardNavIcons;
|
|
5589
5691
|
exports.useChromeFooter = useChromeFooter;
|