@the-portland-company/shell 0.82.0 → 0.84.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 +29 -0
- package/dist/app-chrome.cjs +264 -178
- 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 +43 -10
- package/dist/app-chrome.js.map +1 -1
- package/dist/chrome.css +5 -0
- package/dist/{chunk-L73FZKGE.js → chunk-PRZP2SB6.js} +229 -176
- package/dist/chunk-PRZP2SB6.js.map +1 -0
- package/dist/{chunk-VWY6IIAR.js → chunk-QS3Z4N73.js} +3 -3
- package/dist/chunk-QS3Z4N73.js.map +1 -0
- package/dist/{index-C3PFq1hl.d.cts → index--q40spnY.d.cts} +64 -4
- package/dist/{index-CwTnsR9R.d.ts → index-C4ob05Pc.d.ts} +64 -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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## 0.84.0
|
|
2
|
+
|
|
3
|
+
An organization switch lands on the new organization's Relationship Dashboard. Spec cited: Specs/Left-Navigation §6 and §11.2 (v21, decided 2026-10-15 by Ben; supersedes the 2026-10-06 rule that kept mode, page and tab).
|
|
4
|
+
|
|
5
|
+
- **Why.** Measured by Ben on production 2026-10-15: switching from "We need Steve" in Campaign Mode to an organization without Campaign Mode kept the Campaign rail, then showed an access error that required signing out and in again.
|
|
6
|
+
- **Token-org path.** After the IdP accepts the switch, the tab keeps the switch pending (Loading Screen up), lets the current app re-exchange for up to 4 seconds, then navigates to `https://dashboard.politogyvrm.com/dashboard?mode=relationship&org=<new org>` (`-staging` twin from a staging host). A refused switch still rolls back and calls `onSwitchError`.
|
|
7
|
+
- **Legacy path.** `ChromeStateProvider` navigates to the same landing after writing the cookie.
|
|
8
|
+
- **Frame.** The Loading Screen's synthetic ready signal waits while `orgPending` is true.
|
|
9
|
+
- **New export** `orgSwitchLanding` (`href`, `navigate`); tests replace `navigate`.
|
|
10
|
+
- **Not changed.** A per-tab `?org_id=` override and a switch made in another tab (followed on focus) do not navigate: neither is a switch made in this tab.
|
|
11
|
+
|
|
12
|
+
## 0.83.0
|
|
13
|
+
|
|
14
|
+
The top bar's Credits icon shows credit health. Spec cited: Reference/UI-Platform v57 "Top bar → Credits" and "Credit health in the payload" (decided by Ben, 2026-10-13); VRM-Admin/Pricing-and-Metering v5 §7.5.
|
|
15
|
+
|
|
16
|
+
- **Icon.** The battery with the lightning bolt, coloured by the worst state among the credit types in use: green when every one is Good, yellow when at least one is Low, red when at least one is Out. Not in use never changes it. The fill drains with the state (full, part, empty) and the state is in the `aria-label` ("Credits: low on Signal Credits"), so colour is never the only signal. A payload without `state` is unknown: neutral icon, no colour, no gauge level.
|
|
17
|
+
- **Tooltip** (hover or focus) lists all four types, each with Available, a gauge against its full mark and the state in words ("Signal Credits: 38 left, about 1 day at your recent pace"). **Click** opens the breakdown: four rows, Available, Included (with the next grant date) and Purchased when the ledger publishes them, gauge, state, and a "+" to buy that type.
|
|
18
|
+
- **Retired.** The low-balance hint that hung under the icon ("You're low on fuel" / "Buy More Fuel"), its sessionStorage dismissal and the new-reply toast's clearance for it. No pop-up about credits opens by itself. `lowFuel` / `lowFuelThreshold` are no longer read. The word is Credits everywhere; "fuel" appears in no string.
|
|
19
|
+
- API: `FuelChannelStatus` gains `state`, `fullMarkUnits`, `daysOfUseLeft`, `includedUnits`, `purchasedUnits`, `nextGrantAt`; `CommunicationsFuelStatus` gains optional `ai` and `enhancement`. `TopBarPopover` gains `icon`, `ariaLabel` and `tooltip`; `topBarIconButtonClass` tones gain `ok` and `low` (`TopBarIconTone`). New export `CreditsAction` (alias of `FuelAction`) and the pure helpers from `./native` (`creditsIconState`, `creditGaugeRatio`, `creditTooltipLine`, `creditsAriaLabel`). New tokens `--credits-good`, `--credits-low`.
|
|
20
|
+
- Tests: `__tests__/credits.test.ts`, `__tests__/chromeActions.test.tsx` (Credits block).
|
|
21
|
+
|
|
22
|
+
## 0.82.1
|
|
23
|
+
|
|
24
|
+
The legacy chrome provider (Contacts, and any app not yet on `tokenOrg`) writes the organization to the address bar as the 7-char `org_id` tail and never the full-uuid `?org=`. Spec cited: Reference/UI-Platform "URL state" (`org_id`, always present; the legacy `org=` and cookie stay only until every app reads the org from the token).
|
|
25
|
+
|
|
26
|
+
- **Cause.** The provider's sync effect set `?org=<full uuid>` whenever the active org changed, so Contacts' canonical URL regained a second org key after every load (reported by Ben's agent, 2026-10-13).
|
|
27
|
+
- **Fix.** The effect deletes `org=` and sets `org_id=<tail>` (a replace, never a navigation); the cookie mirror is unchanged. At boot an `org_id` tail counts as an explicit selector and, once the member list resolves, selects that org (uncontrolled apps); a non-member tail is rewritten to the active org.
|
|
28
|
+
- Tests: `__tests__/legacyOrgIdUrl.test.tsx`.
|
|
29
|
+
|
|
1
30
|
## 0.82.0
|
|
2
31
|
|
|
3
32
|
The universal-search placeholder names what the current Mode searches. Spec cited: Reference/UI-Platform v56 "Top bar — required elements" (decided by Ben, 2026-10-12).
|
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"
|
|
@@ -4824,7 +4876,7 @@ function FrameBody({
|
|
|
4824
4876
|
{
|
|
4825
4877
|
visible: bootVisible,
|
|
4826
4878
|
bootProgress,
|
|
4827
|
-
syntheticReady: appReady ?? chromeResolved,
|
|
4879
|
+
syntheticReady: (appReady ?? chromeResolved) && !chrome?.orgPending,
|
|
4828
4880
|
onExitComplete: () => setBootVisible(false)
|
|
4829
4881
|
},
|
|
4830
4882
|
bootGeneration
|
|
@@ -4914,6 +4966,18 @@ function FrameBody({
|
|
|
4914
4966
|
}
|
|
4915
4967
|
var ORG_STORAGE_KEY = "vrm.activeOrg";
|
|
4916
4968
|
var MODE_STORAGE_KEY = "vrm.activeMode";
|
|
4969
|
+
var orgSwitchLanding = {
|
|
4970
|
+
href(orgId) {
|
|
4971
|
+
return modeLandingHref("relationship", {
|
|
4972
|
+
orgId,
|
|
4973
|
+
hostname: typeof window === "undefined" ? null : window.location.hostname
|
|
4974
|
+
});
|
|
4975
|
+
},
|
|
4976
|
+
navigate(href) {
|
|
4977
|
+
window.location.assign(href);
|
|
4978
|
+
}
|
|
4979
|
+
};
|
|
4980
|
+
var ORG_SWITCH_REEXCHANGE_BUDGET_MS = 4e3;
|
|
4917
4981
|
var ChromeStateContext = react.createContext(null);
|
|
4918
4982
|
function useChromeState() {
|
|
4919
4983
|
return react.useContext(ChromeStateContext);
|
|
@@ -4929,6 +4993,7 @@ function TokenChromeStateProvider({
|
|
|
4929
4993
|
const [pendingOrg, setPendingOrg] = react.useState(null);
|
|
4930
4994
|
const tokenOrgRef = react.useRef(tokenOrg);
|
|
4931
4995
|
tokenOrgRef.current = tokenOrg;
|
|
4996
|
+
const leavingRef = react.useRef(false);
|
|
4932
4997
|
react.useEffect(() => {
|
|
4933
4998
|
setNavDisplayState(resolveNavDisplay());
|
|
4934
4999
|
}, []);
|
|
@@ -4969,6 +5034,7 @@ function TokenChromeStateProvider({
|
|
|
4969
5034
|
}, [overrideOrg, tokenOrg.orgId]);
|
|
4970
5035
|
react.useEffect(() => {
|
|
4971
5036
|
if (!pendingOrg || tokenOrg.orgId !== pendingOrg) return;
|
|
5037
|
+
if (leavingRef.current) return;
|
|
4972
5038
|
setPendingOrg(null);
|
|
4973
5039
|
try {
|
|
4974
5040
|
const next = syncOrgIdInUrl(window.location.href, pendingOrg);
|
|
@@ -4991,8 +5057,16 @@ function TokenChromeStateProvider({
|
|
|
4991
5057
|
window.history.replaceState(window.history.state, "", url.toString());
|
|
4992
5058
|
} catch {
|
|
4993
5059
|
}
|
|
4994
|
-
|
|
5060
|
+
leavingRef.current = true;
|
|
5061
|
+
await Promise.race([
|
|
5062
|
+
Promise.resolve(tokenOrgRef.current.onReexchange(claims.orgId, { perTab: false })).catch(() => {
|
|
5063
|
+
}),
|
|
5064
|
+
new Promise((resolve) => setTimeout(resolve, ORG_SWITCH_REEXCHANGE_BUDGET_MS))
|
|
5065
|
+
]);
|
|
5066
|
+
const href = orgSwitchLanding.href(claims.orgId);
|
|
5067
|
+
if (href) orgSwitchLanding.navigate(href);
|
|
4995
5068
|
} catch (err) {
|
|
5069
|
+
leavingRef.current = false;
|
|
4996
5070
|
setPendingOrg(null);
|
|
4997
5071
|
tokenOrgRef.current.onSwitchError?.(err);
|
|
4998
5072
|
}
|
|
@@ -5057,10 +5131,13 @@ function ChromeStateProvider({
|
|
|
5057
5131
|
const navReconciled = react.useRef(false);
|
|
5058
5132
|
const orgReconciled = react.useRef(false);
|
|
5059
5133
|
const hadBootSelector = react.useRef(false);
|
|
5134
|
+
const bootUrlTail = react.useRef(null);
|
|
5060
5135
|
react.useEffect(() => {
|
|
5061
5136
|
try {
|
|
5137
|
+
const urlSelector = readOrgSelectorFromUrl(window.location.search);
|
|
5138
|
+
if (urlSelector && !isOrgId(urlSelector)) bootUrlTail.current = urlSelector;
|
|
5062
5139
|
const selector = resolveActiveOrg();
|
|
5063
|
-
if (selector) hadBootSelector.current = true;
|
|
5140
|
+
if (selector || urlSelector) hadBootSelector.current = true;
|
|
5064
5141
|
const storedOrg = selector ?? localStorage.getItem(ORG_STORAGE_KEY);
|
|
5065
5142
|
if (storedOrg) setOrgIdState(storedOrg);
|
|
5066
5143
|
} catch {
|
|
@@ -5097,7 +5174,9 @@ function ChromeStateProvider({
|
|
|
5097
5174
|
}, []);
|
|
5098
5175
|
react.useEffect(() => {
|
|
5099
5176
|
if (!orgs || orgs.length === 0) return;
|
|
5100
|
-
|
|
5177
|
+
const fromUrl = matchOrgSelector(bootUrlTail.current, orgs)?.id ?? null;
|
|
5178
|
+
bootUrlTail.current = null;
|
|
5179
|
+
setOrgIdState((prev) => fromUrl ?? (prev && orgs.some((o) => o.id === prev) ? prev : orgs[0].id));
|
|
5101
5180
|
}, [orgs]);
|
|
5102
5181
|
const activeOrgRef = react.useRef(null);
|
|
5103
5182
|
const memberOrgsRef = react.useRef([]);
|
|
@@ -5155,6 +5234,10 @@ function ChromeStateProvider({
|
|
|
5155
5234
|
} catch {
|
|
5156
5235
|
}
|
|
5157
5236
|
writeActiveOrgCookie(id);
|
|
5237
|
+
if (isOrgId(id)) {
|
|
5238
|
+
const href = orgSwitchLanding.href(id);
|
|
5239
|
+
if (href) orgSwitchLanding.navigate(href);
|
|
5240
|
+
}
|
|
5158
5241
|
}, []);
|
|
5159
5242
|
const setNavDisplay = react.useCallback(
|
|
5160
5243
|
(m) => {
|
|
@@ -5221,8 +5304,10 @@ function ChromeStateProvider({
|
|
|
5221
5304
|
}
|
|
5222
5305
|
try {
|
|
5223
5306
|
const url = new URL(window.location.href);
|
|
5224
|
-
|
|
5225
|
-
|
|
5307
|
+
const tail = orgIdTail(activeOrgId);
|
|
5308
|
+
if (url.searchParams.has(ACTIVE_ORG_QUERY_PARAM) || url.searchParams.get(ORG_ID_QUERY_PARAM) !== tail) {
|
|
5309
|
+
url.searchParams.delete(ACTIVE_ORG_QUERY_PARAM);
|
|
5310
|
+
url.searchParams.set(ORG_ID_QUERY_PARAM, tail);
|
|
5226
5311
|
window.history.replaceState(window.history.state, "", url.toString());
|
|
5227
5312
|
}
|
|
5228
5313
|
} catch {
|
|
@@ -5584,6 +5669,7 @@ exports.deriveBreadcrumbs = deriveBreadcrumbs;
|
|
|
5584
5669
|
exports.footerLinks = footerLinks;
|
|
5585
5670
|
exports.looksLikeEmail = looksLikeEmail;
|
|
5586
5671
|
exports.modeLandingHref = modeLandingHref;
|
|
5672
|
+
exports.orgSwitchLanding = orgSwitchLanding;
|
|
5587
5673
|
exports.resolveIdentity = resolveIdentity;
|
|
5588
5674
|
exports.standardNavIcons = standardNavIcons;
|
|
5589
5675
|
exports.useChromeFooter = useChromeFooter;
|