@the-portland-company/shell 0.76.15 → 0.77.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 +131 -195
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.d.cts +9 -4
- package/dist/app-chrome.d.ts +9 -4
- package/dist/app-chrome.js +8 -6
- package/dist/app-chrome.js.map +1 -1
- package/dist/{chunk-HSC6EYRR.js → chunk-GGM4AD43.js} +6 -1769
- package/dist/chunk-GGM4AD43.js.map +1 -0
- package/dist/chunk-TUK2IEL5.js +2002 -0
- package/dist/chunk-TUK2IEL5.js.map +1 -0
- package/dist/index-Cu5frXRQ.d.cts +990 -0
- package/dist/index-DJ3qBzfQ.d.ts +990 -0
- package/dist/index.cjs +170 -85
- 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 +37 -36
- package/dist/index.js.map +1 -1
- package/dist/native.cjs +131 -15
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +4 -929
- package/dist/native.d.ts +4 -929
- package/dist/native.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-HSC6EYRR.js.map +0 -1
- package/dist/chunk-MY6G3LMN.js +0 -125
- package/dist/chunk-MY6G3LMN.js.map +0 -1
|
@@ -0,0 +1,2002 @@
|
|
|
1
|
+
import { readThemeCookie, THEME_STORAGE_KEY, isThemePreference, applyThemePreference, readPendingThemeChoice, writeThemeCookie, clearPendingThemeChoice, markThemeChoicePending } from './chunk-BHRVZPZ7.js';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
import { createContext, useState, useEffect, useRef, useContext, useCallback, useMemo, useSyncExternalStore } from 'react';
|
|
5
|
+
import { FiUser, FiSettings, FiMessageSquare, FiMail, FiInfo, FiSun, FiMoon, FiHelpCircle, FiLifeBuoy, FiAlertCircle, FiAlertTriangle, FiBell, FiGrid, FiZap, FiBatteryCharging, FiBattery } from 'react-icons/fi';
|
|
6
|
+
import { createPortal } from 'react-dom';
|
|
7
|
+
|
|
8
|
+
function FormColumn({ children, className }) {
|
|
9
|
+
return /* @__PURE__ */ jsx("div", { "data-shell-form-column": "", className: clsx("w-full max-w-[960px]", className), children });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/lib/deprecation.ts
|
|
13
|
+
var warned = /* @__PURE__ */ new Set();
|
|
14
|
+
function isDev() {
|
|
15
|
+
try {
|
|
16
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function warnDeprecated(key, message) {
|
|
22
|
+
if (!isDev() || warned.has(key)) return;
|
|
23
|
+
warned.add(key);
|
|
24
|
+
console.warn(`[@the-portland-company/shell] ${message}`);
|
|
25
|
+
}
|
|
26
|
+
function useTopBarIconSlotGlyph(slot) {
|
|
27
|
+
const [name, setName] = useState(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (typeof window === "undefined") return;
|
|
30
|
+
const read = () => {
|
|
31
|
+
const raw = window.getComputedStyle(document.documentElement).getPropertyValue(`--topbar-icon-${slot}`).trim().replace(/^['"]|['"]$/g, "");
|
|
32
|
+
setName(raw.length > 0 ? raw : null);
|
|
33
|
+
};
|
|
34
|
+
read();
|
|
35
|
+
const observer = new MutationObserver(read);
|
|
36
|
+
observer.observe(document.head, { childList: true, subtree: true });
|
|
37
|
+
return () => observer.disconnect();
|
|
38
|
+
}, [slot]);
|
|
39
|
+
return name;
|
|
40
|
+
}
|
|
41
|
+
var GLYPHS = {
|
|
42
|
+
// Communications Fuel reads as a LEVEL, so its glyph is a battery. `bolt`/
|
|
43
|
+
// `zap` stay mapped for older Brand values.
|
|
44
|
+
"battery-charging": FiBatteryCharging,
|
|
45
|
+
battery: FiBattery,
|
|
46
|
+
fuel: FiBatteryCharging,
|
|
47
|
+
bolt: FiZap,
|
|
48
|
+
zap: FiZap,
|
|
49
|
+
grid: FiGrid,
|
|
50
|
+
apps: FiGrid,
|
|
51
|
+
bell: FiBell,
|
|
52
|
+
notification: FiBell,
|
|
53
|
+
"alert-triangle": FiAlertTriangle,
|
|
54
|
+
alert: FiAlertTriangle,
|
|
55
|
+
"alert-circle": FiAlertCircle,
|
|
56
|
+
"life-buoy": FiLifeBuoy,
|
|
57
|
+
"life-ring": FiLifeBuoy,
|
|
58
|
+
support: FiLifeBuoy,
|
|
59
|
+
"help-circle": FiHelpCircle,
|
|
60
|
+
moon: FiMoon,
|
|
61
|
+
sun: FiSun,
|
|
62
|
+
info: FiInfo,
|
|
63
|
+
mail: FiMail,
|
|
64
|
+
"message-square": FiMessageSquare,
|
|
65
|
+
settings: FiSettings,
|
|
66
|
+
user: FiUser
|
|
67
|
+
};
|
|
68
|
+
function resolveBrandGlyph(name) {
|
|
69
|
+
if (!name) return null;
|
|
70
|
+
return GLYPHS[name.trim().toLowerCase()] ?? null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/notifications/adapter.ts
|
|
74
|
+
function emptyNotificationsFeed() {
|
|
75
|
+
return { notifications: [], badge: 0 };
|
|
76
|
+
}
|
|
77
|
+
function asString(value) {
|
|
78
|
+
return typeof value === "string" ? value : null;
|
|
79
|
+
}
|
|
80
|
+
function normalizeNotificationsFeed(raw) {
|
|
81
|
+
if (!raw || typeof raw !== "object") return emptyNotificationsFeed();
|
|
82
|
+
const body = raw;
|
|
83
|
+
const rows = Array.isArray(body.notifications) ? body.notifications : [];
|
|
84
|
+
const notifications = [];
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
if (!row || typeof row !== "object") continue;
|
|
87
|
+
const r = row;
|
|
88
|
+
const id = asString(r.id);
|
|
89
|
+
if (!id || asString(r.archived_at)) continue;
|
|
90
|
+
notifications.push({
|
|
91
|
+
id,
|
|
92
|
+
kind: r.kind === "alert" ? "alert" : "notification",
|
|
93
|
+
title: asString(r.title) ?? "",
|
|
94
|
+
body: asString(r.body) ?? "",
|
|
95
|
+
icon: asString(r.icon),
|
|
96
|
+
link: asString(r.link),
|
|
97
|
+
readAt: asString(r.read_at),
|
|
98
|
+
orgId: asString(r.org_id),
|
|
99
|
+
createdAt: asString(r.created_at)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const badge = typeof body.badge === "number" && Number.isFinite(body.badge) ? Math.max(0, Math.floor(body.badge)) : notifications.filter((n) => n.kind === "alert" || n.readAt === null).length;
|
|
103
|
+
return { notifications, badge };
|
|
104
|
+
}
|
|
105
|
+
function createCommsNotificationsAdapter(fetchJson) {
|
|
106
|
+
const act = async (id, action) => {
|
|
107
|
+
if (!fetchJson || !id) return false;
|
|
108
|
+
try {
|
|
109
|
+
await fetchJson(`me/notifications/${encodeURIComponent(id)}/${action}`, { method: "POST" });
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
async list(orgId) {
|
|
117
|
+
if (!fetchJson) return emptyNotificationsFeed();
|
|
118
|
+
try {
|
|
119
|
+
const query = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
|
|
120
|
+
return normalizeNotificationsFeed(await fetchJson(`me/notifications${query}`));
|
|
121
|
+
} catch {
|
|
122
|
+
return emptyNotificationsFeed();
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
markRead: (id) => act(id, "read"),
|
|
126
|
+
archive: (id) => act(id, "archive"),
|
|
127
|
+
resolve: (id) => act(id, "resolve")
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function notificationsFeedToPayload(feed, orgId) {
|
|
131
|
+
return {
|
|
132
|
+
organizationId: orgId ?? "",
|
|
133
|
+
alerts: feed.notifications.map((n) => ({
|
|
134
|
+
id: n.id,
|
|
135
|
+
title: n.title,
|
|
136
|
+
description: n.body,
|
|
137
|
+
href: n.link,
|
|
138
|
+
kind: n.kind,
|
|
139
|
+
read: n.readAt !== null
|
|
140
|
+
})),
|
|
141
|
+
replies: [],
|
|
142
|
+
badge: feed.badge
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
var CORE_APP_ORIGIN = "https://app.politogyvrm.com";
|
|
146
|
+
var CORE_BILLING_PATH = "/organization-settings/subscription-billing/text-email";
|
|
147
|
+
var CORE_BILLING_URL = `${CORE_APP_ORIGIN}${CORE_BILLING_PATH}`;
|
|
148
|
+
var QUARANTINED_CONTACTS_URL = `https://contacts.politogyvrm.com/duplicates`;
|
|
149
|
+
var SPECS_ORIGIN = "https://specs.politogyvrm.com";
|
|
150
|
+
function withOrgParam(url, orgId) {
|
|
151
|
+
if (!orgId) return url;
|
|
152
|
+
return `${url}${url.includes("?") ? "&" : "?"}org=${orgId}`;
|
|
153
|
+
}
|
|
154
|
+
function withPhoneParam(url, phone) {
|
|
155
|
+
if (!phone) return url;
|
|
156
|
+
return `${url}${url.includes("?") ? "&" : "?"}phone=${encodeURIComponent(phone)}`;
|
|
157
|
+
}
|
|
158
|
+
function resolveOrgLogoUrl(url) {
|
|
159
|
+
const raw = url?.trim();
|
|
160
|
+
if (!raw) return void 0;
|
|
161
|
+
if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(raw)) return raw;
|
|
162
|
+
return `${CORE_APP_ORIGIN}${raw.startsWith("/") ? "" : "/"}${raw}`;
|
|
163
|
+
}
|
|
164
|
+
var TOPBAR_GLYPH_SIZE = 17;
|
|
165
|
+
function topBarIconButtonClass(tone = "default") {
|
|
166
|
+
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)]";
|
|
167
|
+
return [
|
|
168
|
+
// `group/tbtip` powers the shared <TopBarTooltip> rendered inside the trigger.
|
|
169
|
+
"group/tbtip relative flex items-center justify-center rounded-[var(--topbar-icon-button-radius,5px)] border border-transparent transition",
|
|
170
|
+
"h-[var(--topbar-icon-button-size,34px)] w-[var(--topbar-icon-button-size,34px)]",
|
|
171
|
+
toneClass
|
|
172
|
+
].join(" ");
|
|
173
|
+
}
|
|
174
|
+
var openPanelCount = 0;
|
|
175
|
+
var panelListeners = /* @__PURE__ */ new Set();
|
|
176
|
+
var emitPanelChange = () => panelListeners.forEach((l) => l());
|
|
177
|
+
function setPanelOpenGlobal(open) {
|
|
178
|
+
openPanelCount = Math.max(0, openPanelCount + (open ? 1 : -1));
|
|
179
|
+
emitPanelChange();
|
|
180
|
+
}
|
|
181
|
+
function useAnyTopBarPanelOpen() {
|
|
182
|
+
return useSyncExternalStore(
|
|
183
|
+
(cb) => {
|
|
184
|
+
panelListeners.add(cb);
|
|
185
|
+
return () => panelListeners.delete(cb);
|
|
186
|
+
},
|
|
187
|
+
() => openPanelCount > 0,
|
|
188
|
+
() => false
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
function TopBarCountBadge({ count }) {
|
|
192
|
+
if (!(count > 0)) return null;
|
|
193
|
+
return /* @__PURE__ */ jsx("span", { className: "absolute -top-[5px] -right-[5px] flex h-4 min-w-4 items-center justify-center rounded-full bg-[color:var(--chrome-danger)] px-1 text-[10px] font-bold leading-4 text-[#FFFFFF]", children: count > 99 ? "99+" : count });
|
|
194
|
+
}
|
|
195
|
+
function TopBarTooltip({
|
|
196
|
+
title,
|
|
197
|
+
description
|
|
198
|
+
}) {
|
|
199
|
+
return /* @__PURE__ */ jsxs(
|
|
200
|
+
"span",
|
|
201
|
+
{
|
|
202
|
+
role: "tooltip",
|
|
203
|
+
className: "pointer-events-none absolute left-1/2 top-full z-50 mt-2 hidden w-max max-w-56 -translate-x-1/2 flex-col rounded-lg border border-[color:var(--app-btn-tooltip-bg,#2F3136)] bg-[color:var(--app-btn-tooltip-bg,#2F3136)] px-3 py-2 text-left shadow-[var(--shadow-soft)] group-hover/tbtip:flex group-focus-visible/tbtip:flex",
|
|
204
|
+
children: [
|
|
205
|
+
/* @__PURE__ */ jsx(
|
|
206
|
+
"span",
|
|
207
|
+
{
|
|
208
|
+
"aria-hidden": true,
|
|
209
|
+
className: "absolute -top-[5px] left-1/2 h-2.5 w-2.5 -translate-x-1/2 rotate-45 border-l border-t border-[color:var(--app-btn-tooltip-bg,#2F3136)] bg-[color:var(--app-btn-tooltip-bg,#2F3136)]"
|
|
210
|
+
}
|
|
211
|
+
),
|
|
212
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-[color:var(--app-btn-tooltip-fg,#FFFFFF)]", children: title }),
|
|
213
|
+
description ? /* @__PURE__ */ jsx("span", { className: "mt-0.5 text-[11px] leading-snug text-[color:var(--app-btn-tooltip-fg,#FFFFFF)] opacity-75", children: description }) : null
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
function TopBarPanelArrow() {
|
|
219
|
+
return /* @__PURE__ */ jsx(
|
|
220
|
+
"span",
|
|
221
|
+
{
|
|
222
|
+
"aria-hidden": true,
|
|
223
|
+
className: "absolute -top-[5px] right-[11px] h-2.5 w-2.5 rotate-45 border-l border-t border-[color:var(--line)] bg-[color:var(--panel-elevated)]"
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
function TopBarWidgetSlot({
|
|
228
|
+
title,
|
|
229
|
+
description,
|
|
230
|
+
children
|
|
231
|
+
}) {
|
|
232
|
+
return /* @__PURE__ */ jsxs("span", { className: "topbar-widget-slot group/tbtip relative inline-flex", children: [
|
|
233
|
+
children,
|
|
234
|
+
/* @__PURE__ */ jsx(TopBarTooltip, { title, description })
|
|
235
|
+
] });
|
|
236
|
+
}
|
|
237
|
+
var THEME_OPTIONS = [
|
|
238
|
+
{ value: "light", label: "Light" }
|
|
239
|
+
];
|
|
240
|
+
function NavThemeControl({
|
|
241
|
+
value,
|
|
242
|
+
onChange
|
|
243
|
+
}) {
|
|
244
|
+
return /* @__PURE__ */ jsxs("div", { className: "px-4 py-3", children: [
|
|
245
|
+
/* @__PURE__ */ jsx("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Appearance" }),
|
|
246
|
+
/* @__PURE__ */ jsx(
|
|
247
|
+
"div",
|
|
248
|
+
{
|
|
249
|
+
role: "radiogroup",
|
|
250
|
+
"aria-label": "Appearance",
|
|
251
|
+
className: "flex overflow-hidden rounded-full border border-[color:var(--line)]",
|
|
252
|
+
children: THEME_OPTIONS.map((opt) => /* @__PURE__ */ jsx(
|
|
253
|
+
"button",
|
|
254
|
+
{
|
|
255
|
+
type: "button",
|
|
256
|
+
role: "radio",
|
|
257
|
+
"aria-checked": value === opt.value,
|
|
258
|
+
onClick: () => onChange(opt.value),
|
|
259
|
+
className: [
|
|
260
|
+
"flex-1 px-3 py-1.5 text-xs font-semibold transition",
|
|
261
|
+
value === opt.value ? "bg-[color:var(--color-accent-bg-safe,var(--color-accent))] text-[#FFFFFF]" : "text-[color:var(--muted)] hover:bg-[color:var(--panel)] hover:text-[color:var(--foreground)]"
|
|
262
|
+
].join(" "),
|
|
263
|
+
children: opt.label
|
|
264
|
+
},
|
|
265
|
+
opt.value
|
|
266
|
+
))
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
] });
|
|
270
|
+
}
|
|
271
|
+
var CHROME_FETCH_CACHE_TTL_MS = 3e4;
|
|
272
|
+
function chromeCacheKey(path, orgId) {
|
|
273
|
+
return orgId === void 0 ? path : `${path}::${orgId ?? ""}`;
|
|
274
|
+
}
|
|
275
|
+
function createChromeFetchCache(getFetchJson) {
|
|
276
|
+
const cache = /* @__PURE__ */ new Map();
|
|
277
|
+
function dedupedFetch(path, init, opts = {}) {
|
|
278
|
+
const key = chromeCacheKey(path, opts.orgId);
|
|
279
|
+
const now = Date.now();
|
|
280
|
+
const hit = cache.get(key);
|
|
281
|
+
if (hit && hit.expiresAt > now) return hit.promise;
|
|
282
|
+
const promise = getFetchJson()(path, init);
|
|
283
|
+
promise.catch(() => cache.delete(key));
|
|
284
|
+
cache.set(key, { promise, expiresAt: now + (opts.ttlMs ?? CHROME_FETCH_CACHE_TTL_MS) });
|
|
285
|
+
return promise;
|
|
286
|
+
}
|
|
287
|
+
function invalidate(path) {
|
|
288
|
+
for (const key of cache.keys()) {
|
|
289
|
+
if (key === path || key.startsWith(`${path}::`)) cache.delete(key);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { dedupedFetch, invalidate };
|
|
293
|
+
}
|
|
294
|
+
var NavChromeContext = createContext(null);
|
|
295
|
+
function NavChromeProvider({
|
|
296
|
+
fetchJson,
|
|
297
|
+
notificationsAdapter,
|
|
298
|
+
children
|
|
299
|
+
}) {
|
|
300
|
+
const fetchJsonRef = useRef(fetchJson);
|
|
301
|
+
fetchJsonRef.current = fetchJson;
|
|
302
|
+
const cacheRef = useRef(null);
|
|
303
|
+
if (!cacheRef.current) {
|
|
304
|
+
cacheRef.current = createChromeFetchCache(() => fetchJsonRef.current);
|
|
305
|
+
}
|
|
306
|
+
const { dedupedFetch, invalidate: invalidateChromeCache } = cacheRef.current;
|
|
307
|
+
const defaultAdapterRef = useRef(null);
|
|
308
|
+
if (!defaultAdapterRef.current) {
|
|
309
|
+
defaultAdapterRef.current = createCommsNotificationsAdapter(
|
|
310
|
+
(path, init) => fetchJsonRef.current(path, init)
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
const adapter = notificationsAdapter === void 0 ? defaultAdapterRef.current : notificationsAdapter;
|
|
314
|
+
const [me, setMe] = useState(null);
|
|
315
|
+
const [meLoaded, setMeLoaded] = useState(false);
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
let cancelled = false;
|
|
318
|
+
dedupedFetch("me", void 0).then((data) => {
|
|
319
|
+
if (!cancelled) setMe(data);
|
|
320
|
+
}).catch(() => {
|
|
321
|
+
}).finally(() => {
|
|
322
|
+
if (!cancelled) setMeLoaded(true);
|
|
323
|
+
});
|
|
324
|
+
return () => {
|
|
325
|
+
cancelled = true;
|
|
326
|
+
};
|
|
327
|
+
}, [dedupedFetch]);
|
|
328
|
+
return /* @__PURE__ */ jsx(
|
|
329
|
+
NavChromeContext.Provider,
|
|
330
|
+
{
|
|
331
|
+
value: { fetchJson, dedupedFetch, invalidateChromeCache, me, meLoaded, notificationsAdapter: adapter },
|
|
332
|
+
children
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
function useChromeMe() {
|
|
337
|
+
const ctx = useContext(NavChromeContext);
|
|
338
|
+
return { me: ctx?.me ?? null, loaded: ctx?.meLoaded ?? false };
|
|
339
|
+
}
|
|
340
|
+
function useNavChromeDedupedFetch() {
|
|
341
|
+
return useContext(NavChromeContext)?.dedupedFetch ?? null;
|
|
342
|
+
}
|
|
343
|
+
function useNavChromeFetcher() {
|
|
344
|
+
return useContext(NavChromeContext)?.fetchJson ?? null;
|
|
345
|
+
}
|
|
346
|
+
function useNavChrome(component) {
|
|
347
|
+
const ctx = useContext(NavChromeContext);
|
|
348
|
+
if (!ctx) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
`${component} must be rendered inside <NavChromeProvider> (inject the chrome API fetcher there).`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return ctx;
|
|
354
|
+
}
|
|
355
|
+
function useChromeOrgs({ enabled = true } = {}) {
|
|
356
|
+
const { dedupedFetch } = useNavChrome("useChromeOrgs");
|
|
357
|
+
const [orgs, setOrgs] = useState(null);
|
|
358
|
+
const [loading, setLoading] = useState(enabled);
|
|
359
|
+
const [error, setError] = useState(false);
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
if (!enabled) {
|
|
362
|
+
setLoading(false);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
let cancelled = false;
|
|
366
|
+
setLoading(true);
|
|
367
|
+
setError(false);
|
|
368
|
+
dedupedFetch("orgs", void 0).then((data) => {
|
|
369
|
+
if (cancelled) return;
|
|
370
|
+
const list = data?.orgs ?? [];
|
|
371
|
+
setOrgs(list);
|
|
372
|
+
}).catch(() => {
|
|
373
|
+
if (!cancelled) setError(true);
|
|
374
|
+
}).finally(() => {
|
|
375
|
+
if (!cancelled) setLoading(false);
|
|
376
|
+
});
|
|
377
|
+
return () => {
|
|
378
|
+
cancelled = true;
|
|
379
|
+
};
|
|
380
|
+
}, [dedupedFetch, enabled]);
|
|
381
|
+
return { orgs, loading, error };
|
|
382
|
+
}
|
|
383
|
+
function ValueSkeleton({ className }) {
|
|
384
|
+
return /* @__PURE__ */ jsx(
|
|
385
|
+
"span",
|
|
386
|
+
{
|
|
387
|
+
"aria-hidden": true,
|
|
388
|
+
className: [
|
|
389
|
+
"inline-block animate-pulse rounded bg-[color:var(--panel-elevated)]",
|
|
390
|
+
className ?? "h-4 w-16"
|
|
391
|
+
].join(" ")
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
function SlotGlyph({
|
|
396
|
+
slot,
|
|
397
|
+
fallback: Fallback,
|
|
398
|
+
size = TOPBAR_GLYPH_SIZE
|
|
399
|
+
}) {
|
|
400
|
+
const glyphName = useTopBarIconSlotGlyph(slot ?? "apps");
|
|
401
|
+
const BrandGlyph = slot ? resolveBrandGlyph(glyphName) : null;
|
|
402
|
+
const Glyph = BrandGlyph ?? Fallback;
|
|
403
|
+
return /* @__PURE__ */ jsx(Glyph, { size, className: "text-current" });
|
|
404
|
+
}
|
|
405
|
+
function TopBarPopover({
|
|
406
|
+
label,
|
|
407
|
+
description,
|
|
408
|
+
slot,
|
|
409
|
+
fallbackIcon,
|
|
410
|
+
badge,
|
|
411
|
+
tone,
|
|
412
|
+
onOpen,
|
|
413
|
+
openToken,
|
|
414
|
+
onOpenChange,
|
|
415
|
+
hideTooltip = false,
|
|
416
|
+
children,
|
|
417
|
+
panelClassName
|
|
418
|
+
}) {
|
|
419
|
+
const [open, setOpen] = useState(false);
|
|
420
|
+
const onOpenChangeRef = useRef(onOpenChange);
|
|
421
|
+
onOpenChangeRef.current = onOpenChange;
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
onOpenChangeRef.current?.(open);
|
|
424
|
+
if (!open) return;
|
|
425
|
+
setPanelOpenGlobal(true);
|
|
426
|
+
return () => setPanelOpenGlobal(false);
|
|
427
|
+
}, [open]);
|
|
428
|
+
const rootRef = useRef(null);
|
|
429
|
+
const lastOpenTokenRef = useRef(openToken);
|
|
430
|
+
useEffect(() => {
|
|
431
|
+
if (openToken === void 0) return;
|
|
432
|
+
if (openToken === lastOpenTokenRef.current) return;
|
|
433
|
+
lastOpenTokenRef.current = openToken;
|
|
434
|
+
setOpen((v) => {
|
|
435
|
+
if (!v) onOpen?.();
|
|
436
|
+
return true;
|
|
437
|
+
});
|
|
438
|
+
}, [openToken, onOpen]);
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (!open) return;
|
|
441
|
+
const onDocClick = (e) => {
|
|
442
|
+
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
|
443
|
+
};
|
|
444
|
+
const onKey = (e) => {
|
|
445
|
+
if (e.key === "Escape") setOpen(false);
|
|
446
|
+
};
|
|
447
|
+
document.addEventListener("mousedown", onDocClick);
|
|
448
|
+
document.addEventListener("keydown", onKey);
|
|
449
|
+
return () => {
|
|
450
|
+
document.removeEventListener("mousedown", onDocClick);
|
|
451
|
+
document.removeEventListener("keydown", onKey);
|
|
452
|
+
};
|
|
453
|
+
}, [open]);
|
|
454
|
+
return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: "relative", children: [
|
|
455
|
+
/* @__PURE__ */ jsxs(
|
|
456
|
+
"button",
|
|
457
|
+
{
|
|
458
|
+
type: "button",
|
|
459
|
+
"aria-label": label,
|
|
460
|
+
"aria-expanded": open,
|
|
461
|
+
className: topBarIconButtonClass(tone),
|
|
462
|
+
onClick: () => {
|
|
463
|
+
setOpen((v) => {
|
|
464
|
+
const next = !v;
|
|
465
|
+
if (next) onOpen?.();
|
|
466
|
+
return next;
|
|
467
|
+
});
|
|
468
|
+
},
|
|
469
|
+
children: [
|
|
470
|
+
/* @__PURE__ */ jsx(SlotGlyph, { slot, fallback: fallbackIcon }),
|
|
471
|
+
typeof badge === "number" ? /* @__PURE__ */ jsx(TopBarCountBadge, { count: badge }) : null,
|
|
472
|
+
!open && !hideTooltip ? /* @__PURE__ */ jsx(TopBarTooltip, { title: label, description }) : null
|
|
473
|
+
]
|
|
474
|
+
}
|
|
475
|
+
),
|
|
476
|
+
open ? /* @__PURE__ */ jsxs(
|
|
477
|
+
"div",
|
|
478
|
+
{
|
|
479
|
+
className: [
|
|
480
|
+
"absolute right-0 z-40 mt-2 rounded-xl border border-[color:var(--line)] bg-[color:var(--panel-elevated)] shadow-[var(--shadow-soft)]",
|
|
481
|
+
panelClassName ?? "w-80"
|
|
482
|
+
].join(" "),
|
|
483
|
+
children: [
|
|
484
|
+
/* @__PURE__ */ jsx(TopBarPanelArrow, {}),
|
|
485
|
+
/* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-xl", children })
|
|
486
|
+
]
|
|
487
|
+
}
|
|
488
|
+
) : null
|
|
489
|
+
] });
|
|
490
|
+
}
|
|
491
|
+
function BatteryGlyph({ size = TOPBAR_GLYPH_SIZE, className }) {
|
|
492
|
+
return /* @__PURE__ */ 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: [
|
|
493
|
+
/* @__PURE__ */ jsx("rect", { x: "1", y: "6", width: "18", height: "12", rx: "2", ry: "2" }),
|
|
494
|
+
/* @__PURE__ */ jsx("line", { x1: "23", y1: "10", x2: "23", y2: "14" }),
|
|
495
|
+
/* @__PURE__ */ jsx("line", { x1: "5", y1: "10", x2: "5", y2: "14" }),
|
|
496
|
+
/* @__PURE__ */ jsx("line", { x1: "9", y1: "10", x2: "9", y2: "14" })
|
|
497
|
+
] });
|
|
498
|
+
}
|
|
499
|
+
function BellGlyph({ size = TOPBAR_GLYPH_SIZE, className }) {
|
|
500
|
+
return /* @__PURE__ */ 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: [
|
|
501
|
+
/* @__PURE__ */ jsx("path", { d: "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" }),
|
|
502
|
+
/* @__PURE__ */ jsx("path", { d: "M13.73 21a2 2 0 0 1-3.46 0" })
|
|
503
|
+
] });
|
|
504
|
+
}
|
|
505
|
+
function BellOffGlyph({ size = 14, className }) {
|
|
506
|
+
return /* @__PURE__ */ 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: [
|
|
507
|
+
/* @__PURE__ */ jsx("path", { d: "M13.73 21a2 2 0 0 1-3.46 0" }),
|
|
508
|
+
/* @__PURE__ */ jsx("path", { d: "M18.63 13A17.9 17.9 0 0 1 18 8" }),
|
|
509
|
+
/* @__PURE__ */ jsx("path", { d: "M6.26 6.26A5.86 5.86 0 0 0 6 8c0 7-3 9-3 9h14" }),
|
|
510
|
+
/* @__PURE__ */ jsx("path", { d: "M18 8a6 6 0 0 0-9.33-5" }),
|
|
511
|
+
/* @__PURE__ */ jsx("line", { x1: "1", y1: "1", x2: "23", y2: "23" })
|
|
512
|
+
] });
|
|
513
|
+
}
|
|
514
|
+
function LifeBuoyGlyph({ size = TOPBAR_GLYPH_SIZE, className }) {
|
|
515
|
+
return /* @__PURE__ */ 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: [
|
|
516
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
517
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "4" }),
|
|
518
|
+
/* @__PURE__ */ jsx("line", { x1: "4.93", y1: "4.93", x2: "9.17", y2: "9.17" }),
|
|
519
|
+
/* @__PURE__ */ jsx("line", { x1: "14.83", y1: "14.83", x2: "19.07", y2: "19.07" }),
|
|
520
|
+
/* @__PURE__ */ jsx("line", { x1: "14.83", y1: "9.17", x2: "19.07", y2: "4.93" }),
|
|
521
|
+
/* @__PURE__ */ jsx("line", { x1: "4.93", y1: "19.07", x2: "9.17", y2: "14.83" })
|
|
522
|
+
] });
|
|
523
|
+
}
|
|
524
|
+
function LayoutGlyph({ size = TOPBAR_GLYPH_SIZE, className }) {
|
|
525
|
+
return /* @__PURE__ */ 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: [
|
|
526
|
+
/* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
|
|
527
|
+
/* @__PURE__ */ jsx("line", { x1: "3", y1: "9", x2: "21", y2: "9" }),
|
|
528
|
+
/* @__PURE__ */ jsx("line", { x1: "9", y1: "21", x2: "9", y2: "9" })
|
|
529
|
+
] });
|
|
530
|
+
}
|
|
531
|
+
function PlusGlyph({ size = 14, className }) {
|
|
532
|
+
return /* @__PURE__ */ jsxs("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: [
|
|
533
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
|
|
534
|
+
/* @__PURE__ */ jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
|
|
535
|
+
] });
|
|
536
|
+
}
|
|
537
|
+
function XGlyph({ size = 14, className }) {
|
|
538
|
+
return /* @__PURE__ */ jsxs("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: [
|
|
539
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
540
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
541
|
+
] });
|
|
542
|
+
}
|
|
543
|
+
function CheckGlyph({ size = 14, className }) {
|
|
544
|
+
return /* @__PURE__ */ 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__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) });
|
|
545
|
+
}
|
|
546
|
+
var LOW_FUEL_HINT_DISMISSED_KEY = "politogy_low_fuel_hint_dismissed";
|
|
547
|
+
function FuelRow({
|
|
548
|
+
label,
|
|
549
|
+
channel,
|
|
550
|
+
loading,
|
|
551
|
+
addHref,
|
|
552
|
+
addLabel
|
|
553
|
+
}) {
|
|
554
|
+
const remaining = channel?.remainingUnits ?? 0;
|
|
555
|
+
const total = channel ? Math.max(channel.rawBalanceUnits, channel.remainingUnits) : 0;
|
|
556
|
+
const ratio = total > 0 ? Math.min(100, Math.max(0, remaining / total * 100)) : 0;
|
|
557
|
+
const low = channel?.lowFuel ?? false;
|
|
558
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-full", children: [
|
|
559
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between gap-2", children: [
|
|
560
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-[color:var(--foreground)]", children: label }),
|
|
561
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
562
|
+
loading ? /* @__PURE__ */ jsx(ValueSkeleton, { className: "h-3.5 w-14" }) : /* @__PURE__ */ jsxs(
|
|
563
|
+
"span",
|
|
564
|
+
{
|
|
565
|
+
className: [
|
|
566
|
+
"text-xs font-semibold tabular-nums",
|
|
567
|
+
low ? "text-[color:var(--chrome-danger)]" : "text-[color:var(--muted)]"
|
|
568
|
+
].join(" "),
|
|
569
|
+
children: [
|
|
570
|
+
remaining.toLocaleString(),
|
|
571
|
+
" left"
|
|
572
|
+
]
|
|
573
|
+
}
|
|
574
|
+
),
|
|
575
|
+
addHref ? /* @__PURE__ */ jsx(
|
|
576
|
+
"a",
|
|
577
|
+
{
|
|
578
|
+
href: addHref,
|
|
579
|
+
"aria-label": addLabel,
|
|
580
|
+
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)]",
|
|
581
|
+
children: /* @__PURE__ */ jsx(PlusGlyph, {})
|
|
582
|
+
}
|
|
583
|
+
) : null
|
|
584
|
+
] })
|
|
585
|
+
] }),
|
|
586
|
+
/* @__PURE__ */ jsx("div", { className: "h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel)]", children: loading ? /* @__PURE__ */ jsx("div", { className: "h-full w-full animate-pulse bg-[color:var(--line)]" }) : /* @__PURE__ */ jsx(
|
|
587
|
+
"div",
|
|
588
|
+
{
|
|
589
|
+
className: [
|
|
590
|
+
"h-full rounded-full transition-[width]",
|
|
591
|
+
low ? "bg-[color:var(--chrome-danger)]" : "bg-[color:var(--color-ok,#00783B)]"
|
|
592
|
+
].join(" "),
|
|
593
|
+
style: { width: `${ratio}%` }
|
|
594
|
+
}
|
|
595
|
+
) })
|
|
596
|
+
] });
|
|
597
|
+
}
|
|
598
|
+
var lowFuelHintVisible = false;
|
|
599
|
+
var lowFuelHintListeners = /* @__PURE__ */ new Set();
|
|
600
|
+
function setLowFuelHintVisible(visible) {
|
|
601
|
+
if (lowFuelHintVisible === visible) return;
|
|
602
|
+
lowFuelHintVisible = visible;
|
|
603
|
+
lowFuelHintListeners.forEach((l) => l());
|
|
604
|
+
}
|
|
605
|
+
function subscribeLowFuelHintVisible(onChange) {
|
|
606
|
+
lowFuelHintListeners.add(onChange);
|
|
607
|
+
return () => lowFuelHintListeners.delete(onChange);
|
|
608
|
+
}
|
|
609
|
+
function useLowFuelHintVisible() {
|
|
610
|
+
return useSyncExternalStore(
|
|
611
|
+
subscribeLowFuelHintVisible,
|
|
612
|
+
() => lowFuelHintVisible,
|
|
613
|
+
() => false
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
var LOW_FUEL_HINT_CLEARANCE_PX = 96;
|
|
617
|
+
function LowFuelHint({ lowFuel, orgId }) {
|
|
618
|
+
const [dismissed, setDismissed] = useState(() => {
|
|
619
|
+
if (typeof window === "undefined") return false;
|
|
620
|
+
try {
|
|
621
|
+
return window.sessionStorage.getItem(LOW_FUEL_HINT_DISMISSED_KEY) === "1";
|
|
622
|
+
} catch {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
useEffect(() => {
|
|
627
|
+
if (lowFuel) return;
|
|
628
|
+
try {
|
|
629
|
+
window.sessionStorage.removeItem(LOW_FUEL_HINT_DISMISSED_KEY);
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
}, [lowFuel]);
|
|
633
|
+
const panelOpen = useAnyTopBarPanelOpen();
|
|
634
|
+
const showing = lowFuel && !dismissed && !panelOpen;
|
|
635
|
+
useEffect(() => {
|
|
636
|
+
setLowFuelHintVisible(showing);
|
|
637
|
+
return () => setLowFuelHintVisible(false);
|
|
638
|
+
}, [showing]);
|
|
639
|
+
if (!showing) return null;
|
|
640
|
+
const dismiss = () => {
|
|
641
|
+
try {
|
|
642
|
+
window.sessionStorage.setItem(LOW_FUEL_HINT_DISMISSED_KEY, "1");
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
setDismissed(true);
|
|
646
|
+
};
|
|
647
|
+
return /* @__PURE__ */ jsxs(
|
|
648
|
+
"div",
|
|
649
|
+
{
|
|
650
|
+
role: "status",
|
|
651
|
+
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)]",
|
|
652
|
+
onClick: (e) => e.stopPropagation(),
|
|
653
|
+
children: [
|
|
654
|
+
/* @__PURE__ */ jsx(
|
|
655
|
+
"span",
|
|
656
|
+
{
|
|
657
|
+
"aria-hidden": true,
|
|
658
|
+
className: "absolute -top-[5px] right-[11px] h-2.5 w-2.5 rotate-45 bg-[color:var(--chrome-danger)]"
|
|
659
|
+
}
|
|
660
|
+
),
|
|
661
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
|
|
662
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
663
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-semibold", children: "You\u2019re low on fuel" }),
|
|
664
|
+
/* @__PURE__ */ jsx(
|
|
665
|
+
"a",
|
|
666
|
+
{
|
|
667
|
+
href: withOrgParam(CORE_BILLING_URL, orgId),
|
|
668
|
+
className: "mt-1 inline-block text-sm font-bold underline hover:opacity-90",
|
|
669
|
+
children: "Buy More Fuel"
|
|
670
|
+
}
|
|
671
|
+
)
|
|
672
|
+
] }),
|
|
673
|
+
/* @__PURE__ */ jsx(
|
|
674
|
+
"button",
|
|
675
|
+
{
|
|
676
|
+
type: "button",
|
|
677
|
+
"aria-label": "Dismiss low fuel notice",
|
|
678
|
+
onClick: dismiss,
|
|
679
|
+
className: "flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[#FFFFFF]/90 transition hover:bg-[#FFFFFF]/20",
|
|
680
|
+
children: /* @__PURE__ */ jsx(XGlyph, {})
|
|
681
|
+
}
|
|
682
|
+
)
|
|
683
|
+
] })
|
|
684
|
+
]
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
function FuelAction({ orgId }) {
|
|
689
|
+
const { dedupedFetch } = useNavChrome("FuelAction");
|
|
690
|
+
const [fuel, setFuel] = useState(null);
|
|
691
|
+
const [loading, setLoading] = useState(true);
|
|
692
|
+
const [error, setError] = useState(false);
|
|
693
|
+
useEffect(() => {
|
|
694
|
+
let cancelled = false;
|
|
695
|
+
if (!orgId) {
|
|
696
|
+
setLoading(false);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
setLoading(true);
|
|
700
|
+
setError(false);
|
|
701
|
+
setFuel(null);
|
|
702
|
+
dedupedFetch(
|
|
703
|
+
"communications/fuel-status",
|
|
704
|
+
{ headers: orgId ? { "x-contacts-org": orgId } : void 0 },
|
|
705
|
+
{ orgId }
|
|
706
|
+
).then((data) => {
|
|
707
|
+
if (cancelled) return;
|
|
708
|
+
setFuel(data?.fuel ?? null);
|
|
709
|
+
}).catch(() => {
|
|
710
|
+
if (!cancelled) setError(true);
|
|
711
|
+
}).finally(() => {
|
|
712
|
+
if (!cancelled) setLoading(false);
|
|
713
|
+
});
|
|
714
|
+
return () => {
|
|
715
|
+
cancelled = true;
|
|
716
|
+
};
|
|
717
|
+
}, [dedupedFetch, orgId]);
|
|
718
|
+
const low = !!fuel && (fuel.signal.lowFuel || fuel.broadcast.lowFuel);
|
|
719
|
+
const lowFuelHintUp = useLowFuelHintVisible();
|
|
720
|
+
return (
|
|
721
|
+
// The nudge anchors to the trigger, so it sits OUTSIDE the popover panel in
|
|
722
|
+
// its own relative wrapper (same shape the VRM widget used).
|
|
723
|
+
/* @__PURE__ */ jsxs("div", { className: "relative inline-flex", children: [
|
|
724
|
+
/* @__PURE__ */ jsx(
|
|
725
|
+
TopBarPopover,
|
|
726
|
+
{
|
|
727
|
+
label: "Communications Fuel",
|
|
728
|
+
description: "Remaining SMS and email sending balance for the active organization.",
|
|
729
|
+
slot: "comms-fuel",
|
|
730
|
+
fallbackIcon: BatteryGlyph,
|
|
731
|
+
tone: low ? "warning" : "default",
|
|
732
|
+
hideTooltip: lowFuelHintUp,
|
|
733
|
+
children: /* @__PURE__ */ jsxs("div", { className: "px-4 py-3", children: [
|
|
734
|
+
/* @__PURE__ */ jsx("p", { className: "mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Communications Fuel" }),
|
|
735
|
+
error || !loading && !fuel ? /* @__PURE__ */ jsx("p", { className: "text-sm text-[color:var(--muted)]", children: "Fuel balance is unavailable right now." }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
736
|
+
/* @__PURE__ */ jsx(
|
|
737
|
+
FuelRow,
|
|
738
|
+
{
|
|
739
|
+
label: "SMS Signal",
|
|
740
|
+
channel: fuel?.signal ?? null,
|
|
741
|
+
loading,
|
|
742
|
+
addHref: withOrgParam(CORE_BILLING_URL, orgId),
|
|
743
|
+
addLabel: "Add SMS Signal fuel"
|
|
744
|
+
}
|
|
745
|
+
),
|
|
746
|
+
/* @__PURE__ */ jsx(
|
|
747
|
+
FuelRow,
|
|
748
|
+
{
|
|
749
|
+
label: "Email Broadcast",
|
|
750
|
+
channel: fuel?.broadcast ?? null,
|
|
751
|
+
loading,
|
|
752
|
+
addHref: withOrgParam(CORE_BILLING_URL, orgId),
|
|
753
|
+
addLabel: "Add Email Broadcast fuel"
|
|
754
|
+
}
|
|
755
|
+
)
|
|
756
|
+
] })
|
|
757
|
+
] })
|
|
758
|
+
}
|
|
759
|
+
),
|
|
760
|
+
/* @__PURE__ */ jsx(LowFuelHint, { lowFuel: low, orgId })
|
|
761
|
+
] })
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
function notifStateKey(orgId) {
|
|
765
|
+
return `politogy_chrome_notif_state:${orgId ?? "none"}`;
|
|
766
|
+
}
|
|
767
|
+
function readNotifState(orgId) {
|
|
768
|
+
const empty = { readAlertIds: [], lastSeenAt: null, toastedReplyIds: [] };
|
|
769
|
+
if (typeof window === "undefined") return empty;
|
|
770
|
+
try {
|
|
771
|
+
const raw = window.localStorage.getItem(notifStateKey(orgId));
|
|
772
|
+
if (!raw) return empty;
|
|
773
|
+
const parsed = JSON.parse(raw);
|
|
774
|
+
return {
|
|
775
|
+
readAlertIds: Array.isArray(parsed?.readAlertIds) ? parsed.readAlertIds.filter((id) => typeof id === "string") : [],
|
|
776
|
+
lastSeenAt: typeof parsed?.lastSeenAt === "string" ? parsed.lastSeenAt : null,
|
|
777
|
+
toastedReplyIds: Array.isArray(parsed?.toastedReplyIds) ? parsed.toastedReplyIds.filter((id) => typeof id === "string") : []
|
|
778
|
+
};
|
|
779
|
+
} catch {
|
|
780
|
+
return empty;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function writeNotifState(orgId, state) {
|
|
784
|
+
if (typeof window === "undefined") return;
|
|
785
|
+
try {
|
|
786
|
+
window.localStorage.setItem(notifStateKey(orgId), JSON.stringify(state));
|
|
787
|
+
} catch {
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
var TOASTS_MUTED_KEY = "politogy_chrome_toasts_muted";
|
|
791
|
+
function readToastsMuted() {
|
|
792
|
+
if (typeof window === "undefined") return false;
|
|
793
|
+
try {
|
|
794
|
+
return window.sessionStorage.getItem(TOASTS_MUTED_KEY) === "1";
|
|
795
|
+
} catch {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function writeToastsMuted(muted) {
|
|
800
|
+
if (typeof window === "undefined") return;
|
|
801
|
+
try {
|
|
802
|
+
if (muted) window.sessionStorage.setItem(TOASTS_MUTED_KEY, "1");
|
|
803
|
+
else window.sessionStorage.removeItem(TOASTS_MUTED_KEY);
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function formatPhone(raw) {
|
|
808
|
+
const digits = raw.replace(/\D/g, "");
|
|
809
|
+
const ten = digits.length === 11 && digits.startsWith("1") ? digits.slice(1) : digits;
|
|
810
|
+
if (ten.length === 10) return `(${ten.slice(0, 3)}) ${ten.slice(3, 6)}-${ten.slice(6)}`;
|
|
811
|
+
return raw;
|
|
812
|
+
}
|
|
813
|
+
function ImportRow({ imp }) {
|
|
814
|
+
const pct = imp.percent == null ? null : Math.max(0, Math.min(100, imp.percent));
|
|
815
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
816
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
817
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm text-[color:var(--foreground)]", children: imp.label }),
|
|
818
|
+
pct != null ? /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-xs tabular-nums text-[color:var(--muted)]", children: [
|
|
819
|
+
Math.round(pct),
|
|
820
|
+
"%"
|
|
821
|
+
] }) : null
|
|
822
|
+
] }),
|
|
823
|
+
pct != null ? /* @__PURE__ */ jsx("div", { className: "mt-1 h-1 w-full overflow-hidden rounded-full bg-[color:var(--panel)]", children: /* @__PURE__ */ jsx(
|
|
824
|
+
"div",
|
|
825
|
+
{
|
|
826
|
+
className: "h-full rounded-full bg-[color:var(--color-accent)] transition-[width]",
|
|
827
|
+
style: { width: `${pct}%` }
|
|
828
|
+
}
|
|
829
|
+
) }) : null
|
|
830
|
+
] });
|
|
831
|
+
}
|
|
832
|
+
function NewReplyToast({
|
|
833
|
+
reply,
|
|
834
|
+
onActivate,
|
|
835
|
+
onClose,
|
|
836
|
+
onSilence,
|
|
837
|
+
ttlMs = 12e3
|
|
838
|
+
}) {
|
|
839
|
+
const [visible, setVisible] = useState(false);
|
|
840
|
+
const onCloseRef = useRef(onClose);
|
|
841
|
+
onCloseRef.current = onClose;
|
|
842
|
+
const lowFuelHintVisible2 = useLowFuelHintVisible();
|
|
843
|
+
useEffect(() => {
|
|
844
|
+
setVisible(true);
|
|
845
|
+
const fadeAt = Math.max(0, ttlMs - 450);
|
|
846
|
+
const t1 = window.setTimeout(() => setVisible(false), fadeAt);
|
|
847
|
+
const t2 = window.setTimeout(() => onCloseRef.current(), ttlMs);
|
|
848
|
+
return () => {
|
|
849
|
+
window.clearTimeout(t1);
|
|
850
|
+
window.clearTimeout(t2);
|
|
851
|
+
};
|
|
852
|
+
}, [ttlMs]);
|
|
853
|
+
const activate = () => {
|
|
854
|
+
setVisible(false);
|
|
855
|
+
onActivate();
|
|
856
|
+
};
|
|
857
|
+
return /* @__PURE__ */ jsxs(
|
|
858
|
+
"div",
|
|
859
|
+
{
|
|
860
|
+
role: "button",
|
|
861
|
+
tabIndex: 0,
|
|
862
|
+
"aria-label": "Open new reply",
|
|
863
|
+
onClick: activate,
|
|
864
|
+
onKeyDown: (e) => {
|
|
865
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
866
|
+
e.preventDefault();
|
|
867
|
+
activate();
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
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)]",
|
|
871
|
+
style: {
|
|
872
|
+
top: lowFuelHintVisible2 ? `calc(100% + 12px + ${LOW_FUEL_HINT_CLEARANCE_PX}px)` : void 0,
|
|
873
|
+
opacity: visible ? 1 : 0,
|
|
874
|
+
transform: visible ? "translateY(0px)" : "translateY(-4px)",
|
|
875
|
+
transition: "opacity 250ms ease, transform 250ms ease"
|
|
876
|
+
},
|
|
877
|
+
children: [
|
|
878
|
+
/* @__PURE__ */ jsx(
|
|
879
|
+
"span",
|
|
880
|
+
{
|
|
881
|
+
"aria-hidden": true,
|
|
882
|
+
className: "absolute right-[18px] top-[-7px] h-0 w-0 border-x-[8px] border-x-transparent border-b-[8px] border-b-[#161616]"
|
|
883
|
+
}
|
|
884
|
+
),
|
|
885
|
+
/* @__PURE__ */ jsx(
|
|
886
|
+
"button",
|
|
887
|
+
{
|
|
888
|
+
type: "button",
|
|
889
|
+
"aria-label": "Dismiss notification",
|
|
890
|
+
onClick: (e) => {
|
|
891
|
+
e.stopPropagation();
|
|
892
|
+
setVisible(false);
|
|
893
|
+
onClose();
|
|
894
|
+
},
|
|
895
|
+
className: "absolute right-2 top-2 flex h-5 w-5 items-center justify-center rounded-full text-[#FFFFFF] opacity-60 transition hover:bg-[#FFFFFF]/10 hover:opacity-100",
|
|
896
|
+
children: /* @__PURE__ */ jsx(XGlyph, { size: 12 })
|
|
897
|
+
}
|
|
898
|
+
),
|
|
899
|
+
/* @__PURE__ */ jsx("p", { className: "truncate pr-6 text-sm font-semibold", children: reply.name }),
|
|
900
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 truncate text-xs opacity-85", children: reply.preview }),
|
|
901
|
+
/* @__PURE__ */ jsxs(
|
|
902
|
+
"button",
|
|
903
|
+
{
|
|
904
|
+
type: "button",
|
|
905
|
+
onClick: (e) => {
|
|
906
|
+
e.stopPropagation();
|
|
907
|
+
setVisible(false);
|
|
908
|
+
onSilence();
|
|
909
|
+
},
|
|
910
|
+
className: "mt-2 inline-flex items-center gap-1 text-[11px] font-semibold text-[#FFFFFF] opacity-60 transition hover:opacity-100",
|
|
911
|
+
children: [
|
|
912
|
+
/* @__PURE__ */ jsx(BellOffGlyph, { size: 12 }),
|
|
913
|
+
"Silence notifications"
|
|
914
|
+
]
|
|
915
|
+
}
|
|
916
|
+
)
|
|
917
|
+
]
|
|
918
|
+
}
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
function NotificationsAction({ orgId }) {
|
|
922
|
+
const { dedupedFetch, notificationsAdapter } = useNavChrome("NotificationsAction");
|
|
923
|
+
const [payload, setPayload] = useState(null);
|
|
924
|
+
const [loading, setLoading] = useState(true);
|
|
925
|
+
const [error, setError] = useState(false);
|
|
926
|
+
const [notif, setNotif] = useState(() => readNotifState(orgId));
|
|
927
|
+
const [toastReply, setToastReply] = useState(null);
|
|
928
|
+
const [toastsMuted, setToastsMuted] = useState(() => readToastsMuted());
|
|
929
|
+
const [openToken, setOpenToken] = useState(0);
|
|
930
|
+
const [panelOpen, setPanelOpen] = useState(false);
|
|
931
|
+
const [reloadToken, setReloadToken] = useState(0);
|
|
932
|
+
const anyPanelOpen = useAnyTopBarPanelOpen();
|
|
933
|
+
useEffect(() => {
|
|
934
|
+
setNotif(readNotifState(orgId));
|
|
935
|
+
setToastReply(null);
|
|
936
|
+
}, [orgId]);
|
|
937
|
+
useEffect(() => {
|
|
938
|
+
let cancelled = false;
|
|
939
|
+
if (!orgId) {
|
|
940
|
+
setLoading(false);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (notificationsAdapter) {
|
|
944
|
+
setError(false);
|
|
945
|
+
notificationsAdapter.list(orgId).then((feed) => {
|
|
946
|
+
if (!cancelled) setPayload(notificationsFeedToPayload(feed, orgId));
|
|
947
|
+
}).catch(() => {
|
|
948
|
+
if (!cancelled) setPayload(notificationsFeedToPayload(emptyNotificationsFeed(), orgId));
|
|
949
|
+
}).finally(() => {
|
|
950
|
+
if (!cancelled) setLoading(false);
|
|
951
|
+
});
|
|
952
|
+
return () => {
|
|
953
|
+
cancelled = true;
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
setLoading(true);
|
|
957
|
+
setError(false);
|
|
958
|
+
setPayload(null);
|
|
959
|
+
dedupedFetch(
|
|
960
|
+
"notifications",
|
|
961
|
+
{ headers: orgId ? { "x-contacts-org": orgId } : void 0 },
|
|
962
|
+
{ orgId }
|
|
963
|
+
).then((data) => {
|
|
964
|
+
if (!cancelled) setPayload(data);
|
|
965
|
+
}).catch(() => {
|
|
966
|
+
if (!cancelled) setError(true);
|
|
967
|
+
}).finally(() => {
|
|
968
|
+
if (!cancelled) setLoading(false);
|
|
969
|
+
});
|
|
970
|
+
return () => {
|
|
971
|
+
cancelled = true;
|
|
972
|
+
};
|
|
973
|
+
}, [dedupedFetch, notificationsAdapter, orgId, reloadToken]);
|
|
974
|
+
const markRead = useCallback(
|
|
975
|
+
(id) => {
|
|
976
|
+
if (notificationsAdapter) {
|
|
977
|
+
void notificationsAdapter.markRead(id).then((ok) => {
|
|
978
|
+
if (ok) setReloadToken((t) => t + 1);
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
setNotif((prev) => {
|
|
982
|
+
if (prev.readAlertIds.includes(id)) return prev;
|
|
983
|
+
const next = { ...prev, readAlertIds: [...prev.readAlertIds, id] };
|
|
984
|
+
writeNotifState(orgId, next);
|
|
985
|
+
return next;
|
|
986
|
+
});
|
|
987
|
+
},
|
|
988
|
+
[orgId, notificationsAdapter]
|
|
989
|
+
);
|
|
990
|
+
const markUnread = useCallback(
|
|
991
|
+
(id) => {
|
|
992
|
+
setNotif((prev) => {
|
|
993
|
+
if (!prev.readAlertIds.includes(id)) return prev;
|
|
994
|
+
const next = { ...prev, readAlertIds: prev.readAlertIds.filter((x) => x !== id) };
|
|
995
|
+
writeNotifState(orgId, next);
|
|
996
|
+
return next;
|
|
997
|
+
});
|
|
998
|
+
},
|
|
999
|
+
[orgId]
|
|
1000
|
+
);
|
|
1001
|
+
const onOpen = useCallback(() => {
|
|
1002
|
+
setToastReply(null);
|
|
1003
|
+
setNotif((prev) => {
|
|
1004
|
+
const next = { ...prev, lastSeenAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1005
|
+
writeNotifState(orgId, next);
|
|
1006
|
+
return next;
|
|
1007
|
+
});
|
|
1008
|
+
}, [orgId]);
|
|
1009
|
+
const alerts = payload?.alerts ?? [];
|
|
1010
|
+
const replies = payload?.replies ?? [];
|
|
1011
|
+
const imports = payload?.imports ?? [];
|
|
1012
|
+
const quarantined = payload?.quarantined ?? [];
|
|
1013
|
+
useEffect(() => {
|
|
1014
|
+
if (toastsMuted) return;
|
|
1015
|
+
if (!payload || toastReply || panelOpen) return;
|
|
1016
|
+
const candidate = replies.filter((r) => {
|
|
1017
|
+
if (!r.createdAt) return false;
|
|
1018
|
+
if (notif.lastSeenAt && r.createdAt <= notif.lastSeenAt) return false;
|
|
1019
|
+
return !notif.toastedReplyIds.includes(r.id);
|
|
1020
|
+
}).sort((a, b) => a.createdAt < b.createdAt ? 1 : -1)[0];
|
|
1021
|
+
if (!candidate) return;
|
|
1022
|
+
setToastReply(candidate);
|
|
1023
|
+
setNotif((prev) => {
|
|
1024
|
+
const next = {
|
|
1025
|
+
...prev,
|
|
1026
|
+
toastedReplyIds: [candidate.id, ...prev.toastedReplyIds].slice(0, 50)
|
|
1027
|
+
};
|
|
1028
|
+
writeNotifState(orgId, next);
|
|
1029
|
+
return next;
|
|
1030
|
+
});
|
|
1031
|
+
}, [
|
|
1032
|
+
payload,
|
|
1033
|
+
replies,
|
|
1034
|
+
toastReply,
|
|
1035
|
+
panelOpen,
|
|
1036
|
+
toastsMuted,
|
|
1037
|
+
notif.lastSeenAt,
|
|
1038
|
+
notif.toastedReplyIds,
|
|
1039
|
+
orgId
|
|
1040
|
+
]);
|
|
1041
|
+
const setMuted = useCallback((muted) => {
|
|
1042
|
+
writeToastsMuted(muted);
|
|
1043
|
+
setToastsMuted(muted);
|
|
1044
|
+
}, []);
|
|
1045
|
+
const onToastSilence = useCallback(() => {
|
|
1046
|
+
setMuted(true);
|
|
1047
|
+
setToastReply(null);
|
|
1048
|
+
}, [setMuted]);
|
|
1049
|
+
const onToastActivate = useCallback(() => {
|
|
1050
|
+
const href = toastReply?.href;
|
|
1051
|
+
setToastReply(null);
|
|
1052
|
+
if (href && typeof window !== "undefined") {
|
|
1053
|
+
window.location.assign(href);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
setOpenToken((t) => t + 1);
|
|
1057
|
+
}, [toastReply]);
|
|
1058
|
+
const isAlertRead = (a) => a.read === true || notif.readAlertIds.includes(a.id);
|
|
1059
|
+
const unreadAlertCount = alerts.filter((a) => !isAlertRead(a)).length;
|
|
1060
|
+
const unseenReplyCount = replies.filter((r) => {
|
|
1061
|
+
if (!notif.lastSeenAt) return true;
|
|
1062
|
+
if (!r.createdAt) return false;
|
|
1063
|
+
return r.createdAt > notif.lastSeenAt;
|
|
1064
|
+
}).length;
|
|
1065
|
+
const badgeCount = notificationsAdapter && payload?.badge !== void 0 ? payload.badge : unreadAlertCount + unseenReplyCount;
|
|
1066
|
+
const isEmpty = alerts.length === 0 && replies.length === 0 && imports.length === 0 && quarantined.length === 0;
|
|
1067
|
+
const toastVisible = toastReply !== null && !panelOpen && !anyPanelOpen;
|
|
1068
|
+
return (
|
|
1069
|
+
// Relative wrapper so the new-reply toast anchors to the bell trigger,
|
|
1070
|
+
// outside the popover panel (same pattern as the fuel low-fuel nudge).
|
|
1071
|
+
/* @__PURE__ */ jsxs("div", { className: "relative inline-flex", children: [
|
|
1072
|
+
/* @__PURE__ */ jsx(
|
|
1073
|
+
TopBarPopover,
|
|
1074
|
+
{
|
|
1075
|
+
label: "Alerts & Notifications",
|
|
1076
|
+
description: "System alerts and recent activity for the active organization.",
|
|
1077
|
+
slot: "notifications",
|
|
1078
|
+
fallbackIcon: BellGlyph,
|
|
1079
|
+
badge: loading ? 0 : badgeCount,
|
|
1080
|
+
onOpen,
|
|
1081
|
+
openToken,
|
|
1082
|
+
onOpenChange: setPanelOpen,
|
|
1083
|
+
hideTooltip: toastVisible,
|
|
1084
|
+
children: /* @__PURE__ */ jsxs("div", { className: "px-4 py-3", children: [
|
|
1085
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
|
|
1086
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Notifications" }),
|
|
1087
|
+
toastsMuted ? /* @__PURE__ */ jsx(
|
|
1088
|
+
"button",
|
|
1089
|
+
{
|
|
1090
|
+
type: "button",
|
|
1091
|
+
onClick: () => setMuted(false),
|
|
1092
|
+
className: "text-[11px] font-semibold text-[color:var(--color-accent-fg,var(--color-accent))] hover:underline",
|
|
1093
|
+
children: "Unsilence"
|
|
1094
|
+
}
|
|
1095
|
+
) : null
|
|
1096
|
+
] }),
|
|
1097
|
+
loading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 py-1", children: [
|
|
1098
|
+
/* @__PURE__ */ jsx(ValueSkeleton, { className: "h-4 w-full" }),
|
|
1099
|
+
/* @__PURE__ */ jsx(ValueSkeleton, { className: "h-4 w-3/4" })
|
|
1100
|
+
] }) : error ? /* @__PURE__ */ jsx("p", { className: "text-sm text-[color:var(--muted)]", children: "Notifications are unavailable right now." }) : isEmpty ? /* @__PURE__ */ jsx("p", { className: "py-1 text-sm text-[color:var(--muted)]", children: "You\u2019re all caught up." }) : /* @__PURE__ */ jsxs("div", { className: "flex max-h-80 flex-col gap-2 overflow-y-auto", children: [
|
|
1101
|
+
alerts.length > 0 || replies.length > 0 ? /* @__PURE__ */ jsxs("ul", { className: "flex flex-col gap-1", children: [
|
|
1102
|
+
alerts.map((a) => {
|
|
1103
|
+
const isRead = isAlertRead(a);
|
|
1104
|
+
return /* @__PURE__ */ jsx("li", { className: isRead ? "opacity-50" : void 0, children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1 rounded-lg px-2 py-2 hover:bg-[color:var(--panel)]", children: [
|
|
1105
|
+
/* @__PURE__ */ jsxs(
|
|
1106
|
+
"a",
|
|
1107
|
+
{
|
|
1108
|
+
href: a.href ?? "#",
|
|
1109
|
+
onClick: () => markRead(a.id),
|
|
1110
|
+
className: "block min-w-0 flex-1",
|
|
1111
|
+
children: [
|
|
1112
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-[color:var(--foreground)]", children: a.title }),
|
|
1113
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-[color:var(--muted)]", children: a.description })
|
|
1114
|
+
]
|
|
1115
|
+
}
|
|
1116
|
+
),
|
|
1117
|
+
/* @__PURE__ */ jsx(
|
|
1118
|
+
"button",
|
|
1119
|
+
{
|
|
1120
|
+
type: "button",
|
|
1121
|
+
"aria-label": isRead ? "Mark as unread" : "Mark as read",
|
|
1122
|
+
onClick: () => isRead && !notificationsAdapter ? markUnread(a.id) : markRead(a.id),
|
|
1123
|
+
className: "mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[color:var(--muted)] transition hover:bg-[color:var(--panel-elevated)] hover:text-[color:var(--foreground)]",
|
|
1124
|
+
children: /* @__PURE__ */ jsx(CheckGlyph, {})
|
|
1125
|
+
}
|
|
1126
|
+
)
|
|
1127
|
+
] }) }, a.id);
|
|
1128
|
+
}),
|
|
1129
|
+
replies.map((r) => {
|
|
1130
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1131
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-[color:var(--foreground)]", children: r.name }),
|
|
1132
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-xs text-[color:var(--muted)]", children: r.preview })
|
|
1133
|
+
] });
|
|
1134
|
+
return /* @__PURE__ */ jsx("li", { children: r.href ? /* @__PURE__ */ jsx(
|
|
1135
|
+
"a",
|
|
1136
|
+
{
|
|
1137
|
+
href: r.href,
|
|
1138
|
+
"aria-label": `Open the conversation with ${r.name}`,
|
|
1139
|
+
className: "block rounded-lg px-2 py-2 transition hover:bg-[color:var(--panel)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-accent)]",
|
|
1140
|
+
children: body
|
|
1141
|
+
}
|
|
1142
|
+
) : /* @__PURE__ */ jsx("div", { className: "rounded-lg px-2 py-2", children: body }) }, r.id);
|
|
1143
|
+
})
|
|
1144
|
+
] }) : null,
|
|
1145
|
+
imports.length > 0 ? /* @__PURE__ */ jsxs("div", { children: [
|
|
1146
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1 px-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Imports" }),
|
|
1147
|
+
/* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-1", children: imports.map((imp) => /* @__PURE__ */ jsx("li", { className: "rounded-lg px-2 py-2 hover:bg-[color:var(--panel)]", children: imp.href ? /* @__PURE__ */ jsx("a", { href: imp.href, className: "block", children: /* @__PURE__ */ jsx(ImportRow, { imp }) }) : /* @__PURE__ */ jsx(ImportRow, { imp }) }, imp.id)) })
|
|
1148
|
+
] }) : null,
|
|
1149
|
+
quarantined.length > 0 ? /* @__PURE__ */ jsxs("div", { children: [
|
|
1150
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between px-2", children: [
|
|
1151
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Quarantined Contacts" }),
|
|
1152
|
+
/* @__PURE__ */ jsx(
|
|
1153
|
+
"a",
|
|
1154
|
+
{
|
|
1155
|
+
href: withOrgParam(QUARANTINED_CONTACTS_URL, orgId),
|
|
1156
|
+
className: "text-[11px] font-semibold text-[color:var(--color-accent-fg,var(--color-accent))] hover:underline",
|
|
1157
|
+
children: "View all"
|
|
1158
|
+
}
|
|
1159
|
+
)
|
|
1160
|
+
] }),
|
|
1161
|
+
/* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-1", children: quarantined.map((q) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
1162
|
+
"a",
|
|
1163
|
+
{
|
|
1164
|
+
href: withPhoneParam(withOrgParam(QUARANTINED_CONTACTS_URL, orgId), q.phone),
|
|
1165
|
+
"aria-label": `Review ${q.count} quarantined message${q.count === 1 ? "" : "s"} from ${formatPhone(q.phone)}`,
|
|
1166
|
+
className: "block rounded-lg px-2 py-2 transition hover:bg-[color:var(--nav-row-hover)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-accent)]",
|
|
1167
|
+
children: [
|
|
1168
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
1169
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-[color:var(--foreground)]", children: formatPhone(q.phone) }),
|
|
1170
|
+
/* @__PURE__ */ jsx("span", { className: "flex h-4 min-w-4 items-center justify-center rounded-full bg-[color:var(--panel)] px-1 text-[10px] font-normal leading-none tabular-nums text-[color:var(--muted)]", children: q.count > 99 ? "99+" : q.count })
|
|
1171
|
+
] }),
|
|
1172
|
+
q.latestAt ? /* @__PURE__ */ jsx("p", { className: "text-xs text-[color:var(--muted)]", children: new Date(q.latestAt).toLocaleString() }) : null
|
|
1173
|
+
]
|
|
1174
|
+
}
|
|
1175
|
+
) }, q.phone)) })
|
|
1176
|
+
] }) : null
|
|
1177
|
+
] })
|
|
1178
|
+
] })
|
|
1179
|
+
}
|
|
1180
|
+
),
|
|
1181
|
+
toastVisible && toastReply ? /* @__PURE__ */ jsx(
|
|
1182
|
+
NewReplyToast,
|
|
1183
|
+
{
|
|
1184
|
+
reply: toastReply,
|
|
1185
|
+
onActivate: onToastActivate,
|
|
1186
|
+
onClose: () => setToastReply(null),
|
|
1187
|
+
onSilence: onToastSilence
|
|
1188
|
+
}
|
|
1189
|
+
) : null
|
|
1190
|
+
] })
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
function composeSupportMessage(input) {
|
|
1194
|
+
const ctx = [`Page: ${input.pageTitle || "Unknown"}`, `URL: ${input.url}`];
|
|
1195
|
+
if (input.orgName) ctx.push(`Org: ${input.orgName}`);
|
|
1196
|
+
const reply = [input.replyEmail?.trim(), input.replyPhone?.trim()].filter(Boolean).join(" / ");
|
|
1197
|
+
if (reply) ctx.push(`Reply-to: ${reply}`);
|
|
1198
|
+
return `${input.note.trim()}
|
|
1199
|
+
|
|
1200
|
+
---
|
|
1201
|
+
${ctx.join("\n")}`;
|
|
1202
|
+
}
|
|
1203
|
+
function SupportAction({
|
|
1204
|
+
orgId,
|
|
1205
|
+
orgs: knownOrgs
|
|
1206
|
+
} = {}) {
|
|
1207
|
+
const { fetchJson } = useNavChrome("SupportAction");
|
|
1208
|
+
const { me, loaded } = useChromeMe();
|
|
1209
|
+
const { orgs: fetchedOrgs } = useChromeOrgs({ enabled: !knownOrgs });
|
|
1210
|
+
const orgs = knownOrgs ?? fetchedOrgs;
|
|
1211
|
+
const [open, setOpen] = useState(false);
|
|
1212
|
+
const [message, setMessage] = useState("");
|
|
1213
|
+
const [email, setEmail] = useState("");
|
|
1214
|
+
const [phone, setPhone] = useState("");
|
|
1215
|
+
const [showEmailError, setShowEmailError] = useState(false);
|
|
1216
|
+
const [state, setState] = useState("idle");
|
|
1217
|
+
const meEmail = me?.email ?? null;
|
|
1218
|
+
const mePhone = me?.phone ?? null;
|
|
1219
|
+
const missingEmail = loaded && !meEmail;
|
|
1220
|
+
const missingPhone = loaded && !mePhone;
|
|
1221
|
+
const activeOrg = (orgId ? orgs?.find((o) => o.id === orgId) : null) ?? (orgs && orgs.length === 1 ? orgs[0] : null);
|
|
1222
|
+
const orgName = activeOrg?.name ?? null;
|
|
1223
|
+
const close = useCallback(() => {
|
|
1224
|
+
setOpen(false);
|
|
1225
|
+
setState("idle");
|
|
1226
|
+
setMessage("");
|
|
1227
|
+
setShowEmailError(false);
|
|
1228
|
+
}, []);
|
|
1229
|
+
useEffect(() => {
|
|
1230
|
+
if (!open) return;
|
|
1231
|
+
setEmail(meEmail ?? "");
|
|
1232
|
+
setPhone(mePhone ?? "");
|
|
1233
|
+
setShowEmailError(false);
|
|
1234
|
+
}, [open, meEmail, mePhone]);
|
|
1235
|
+
useEffect(() => {
|
|
1236
|
+
if (!open) return;
|
|
1237
|
+
const onKey = (e) => {
|
|
1238
|
+
if (e.key === "Escape") close();
|
|
1239
|
+
};
|
|
1240
|
+
document.addEventListener("keydown", onKey);
|
|
1241
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
1242
|
+
}, [open, close]);
|
|
1243
|
+
const submit = async () => {
|
|
1244
|
+
if (!message.trim() || state === "sending") return;
|
|
1245
|
+
const resolvedEmail = (missingEmail ? email : meEmail ?? "").trim();
|
|
1246
|
+
const resolvedPhone = (missingPhone ? phone : mePhone ?? "").trim();
|
|
1247
|
+
if (missingEmail && !resolvedEmail) {
|
|
1248
|
+
setShowEmailError(true);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
setShowEmailError(false);
|
|
1252
|
+
const collectedContact = missingEmail || missingPhone;
|
|
1253
|
+
const fullMessage = composeSupportMessage({
|
|
1254
|
+
note: message,
|
|
1255
|
+
pageTitle: typeof document !== "undefined" ? document.title : "",
|
|
1256
|
+
url: typeof window !== "undefined" ? window.location.href : "",
|
|
1257
|
+
orgName,
|
|
1258
|
+
replyEmail: collectedContact ? resolvedEmail : void 0,
|
|
1259
|
+
replyPhone: collectedContact ? resolvedPhone : void 0
|
|
1260
|
+
});
|
|
1261
|
+
setState("sending");
|
|
1262
|
+
try {
|
|
1263
|
+
await fetchJson("support", {
|
|
1264
|
+
method: "POST",
|
|
1265
|
+
headers: { "Content-Type": "application/json" },
|
|
1266
|
+
body: JSON.stringify({ message: fullMessage })
|
|
1267
|
+
});
|
|
1268
|
+
setState("sent");
|
|
1269
|
+
} catch {
|
|
1270
|
+
setState("error");
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1274
|
+
/* @__PURE__ */ jsxs(
|
|
1275
|
+
"button",
|
|
1276
|
+
{
|
|
1277
|
+
type: "button",
|
|
1278
|
+
"aria-label": "Support",
|
|
1279
|
+
className: topBarIconButtonClass(),
|
|
1280
|
+
onClick: () => setOpen(true),
|
|
1281
|
+
children: [
|
|
1282
|
+
/* @__PURE__ */ jsx(SlotGlyph, { slot: "support", fallback: LifeBuoyGlyph }),
|
|
1283
|
+
/* @__PURE__ */ jsx(
|
|
1284
|
+
TopBarTooltip,
|
|
1285
|
+
{
|
|
1286
|
+
title: "Support",
|
|
1287
|
+
description: "Send a support request to the Politogy team."
|
|
1288
|
+
}
|
|
1289
|
+
)
|
|
1290
|
+
]
|
|
1291
|
+
}
|
|
1292
|
+
),
|
|
1293
|
+
open && typeof document !== "undefined" ? createPortal(
|
|
1294
|
+
/* @__PURE__ */ jsx(
|
|
1295
|
+
"div",
|
|
1296
|
+
{
|
|
1297
|
+
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4",
|
|
1298
|
+
role: "dialog",
|
|
1299
|
+
"aria-modal": "true",
|
|
1300
|
+
"aria-label": "Support request",
|
|
1301
|
+
onMouseDown: (e) => {
|
|
1302
|
+
if (e.target === e.currentTarget) close();
|
|
1303
|
+
},
|
|
1304
|
+
children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-md rounded-2xl border border-[color:var(--line)] bg-[color:var(--panel-elevated)] p-6 shadow-[var(--shadow-soft)]", children: [
|
|
1305
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-bold tracking-[-0.01em] text-[color:var(--foreground)]", children: "Contact Support" }),
|
|
1306
|
+
state === "sent" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1307
|
+
/* @__PURE__ */ jsx("p", { className: "mt-3 text-sm text-[color:var(--muted)]", children: "Your request was sent. The team will reply by email." }),
|
|
1308
|
+
/* @__PURE__ */ jsx("div", { className: "mt-5 flex justify-end", children: /* @__PURE__ */ jsx(
|
|
1309
|
+
"button",
|
|
1310
|
+
{
|
|
1311
|
+
type: "button",
|
|
1312
|
+
onClick: close,
|
|
1313
|
+
className: "rounded-full bg-[color:var(--color-accent-bg-safe,var(--color-accent))] px-4 py-2 text-sm font-semibold text-[#FFFFFF] hover:opacity-90",
|
|
1314
|
+
children: "Done"
|
|
1315
|
+
}
|
|
1316
|
+
) })
|
|
1317
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1318
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-[color:var(--muted)]", children: "Describe the problem or question. Your account details ride along automatically." }),
|
|
1319
|
+
missingEmail ? /* @__PURE__ */ jsxs("label", { className: "mt-4 block", children: [
|
|
1320
|
+
/* @__PURE__ */ jsx("span", { className: "mb-1 block text-xs font-semibold text-[color:var(--foreground)]", children: "Your email" }),
|
|
1321
|
+
/* @__PURE__ */ jsx(
|
|
1322
|
+
"input",
|
|
1323
|
+
{
|
|
1324
|
+
type: "email",
|
|
1325
|
+
value: email,
|
|
1326
|
+
onChange: (e) => setEmail(e.target.value),
|
|
1327
|
+
placeholder: "you@example.com",
|
|
1328
|
+
className: "w-full rounded-lg border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2 text-sm text-[color:var(--foreground)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-2 focus:ring-[color:var(--color-accent)]"
|
|
1329
|
+
}
|
|
1330
|
+
),
|
|
1331
|
+
showEmailError ? /* @__PURE__ */ jsx("span", { className: "mt-1 block text-xs text-[color:var(--chrome-danger)]", children: "Enter an email so support can reply." }) : null
|
|
1332
|
+
] }) : null,
|
|
1333
|
+
missingPhone ? /* @__PURE__ */ jsxs("label", { className: "mt-3 block", children: [
|
|
1334
|
+
/* @__PURE__ */ jsx("span", { className: "mb-1 block text-xs font-semibold text-[color:var(--foreground)]", children: "Your phone \u2014 optional" }),
|
|
1335
|
+
/* @__PURE__ */ jsx(
|
|
1336
|
+
"input",
|
|
1337
|
+
{
|
|
1338
|
+
type: "tel",
|
|
1339
|
+
value: phone,
|
|
1340
|
+
onChange: (e) => setPhone(e.target.value),
|
|
1341
|
+
placeholder: "(503) 555-1212",
|
|
1342
|
+
className: "w-full rounded-lg border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2 text-sm text-[color:var(--foreground)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-2 focus:ring-[color:var(--color-accent)]"
|
|
1343
|
+
}
|
|
1344
|
+
)
|
|
1345
|
+
] }) : null,
|
|
1346
|
+
/* @__PURE__ */ jsx(
|
|
1347
|
+
"textarea",
|
|
1348
|
+
{
|
|
1349
|
+
value: message,
|
|
1350
|
+
onChange: (e) => setMessage(e.target.value),
|
|
1351
|
+
rows: 5,
|
|
1352
|
+
placeholder: "How can we help?",
|
|
1353
|
+
className: "mt-4 w-full resize-y rounded-lg border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2 text-sm text-[color:var(--foreground)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-2 focus:ring-[color:var(--color-accent)]"
|
|
1354
|
+
}
|
|
1355
|
+
),
|
|
1356
|
+
state === "error" ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-[color:var(--chrome-danger)]", children: "Sending failed \u2014 try again." }) : null,
|
|
1357
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
|
|
1358
|
+
/* @__PURE__ */ jsx(
|
|
1359
|
+
"button",
|
|
1360
|
+
{
|
|
1361
|
+
type: "button",
|
|
1362
|
+
onClick: close,
|
|
1363
|
+
className: "rounded-full border border-[color:var(--line)] px-4 py-2 text-sm font-semibold text-[color:var(--foreground)] hover:bg-[color:var(--panel)]",
|
|
1364
|
+
children: "Cancel"
|
|
1365
|
+
}
|
|
1366
|
+
),
|
|
1367
|
+
/* @__PURE__ */ jsx(
|
|
1368
|
+
"button",
|
|
1369
|
+
{
|
|
1370
|
+
type: "button",
|
|
1371
|
+
onClick: submit,
|
|
1372
|
+
disabled: !message.trim() || state === "sending",
|
|
1373
|
+
className: "rounded-full bg-[color:var(--color-accent-bg-safe,var(--color-accent))] px-4 py-2 text-sm font-semibold text-[#FFFFFF] transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50",
|
|
1374
|
+
children: state === "sending" ? "Sending\u2026" : "Send request"
|
|
1375
|
+
}
|
|
1376
|
+
)
|
|
1377
|
+
] })
|
|
1378
|
+
] })
|
|
1379
|
+
] })
|
|
1380
|
+
}
|
|
1381
|
+
),
|
|
1382
|
+
document.body
|
|
1383
|
+
) : null
|
|
1384
|
+
] });
|
|
1385
|
+
}
|
|
1386
|
+
var SPEC_OVERLAY_ACCESS_CACHE_KEY = "politogy_spec_overlay_access";
|
|
1387
|
+
function readSpecOverlayAccessCache() {
|
|
1388
|
+
if (typeof window === "undefined") return null;
|
|
1389
|
+
try {
|
|
1390
|
+
const raw = window.sessionStorage.getItem(SPEC_OVERLAY_ACCESS_CACHE_KEY);
|
|
1391
|
+
if (!raw) return null;
|
|
1392
|
+
const parsed = JSON.parse(raw);
|
|
1393
|
+
if (typeof parsed?.allowed === "boolean") return parsed;
|
|
1394
|
+
} catch {
|
|
1395
|
+
}
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
function writeSpecOverlayAccessCache(access) {
|
|
1399
|
+
if (typeof window === "undefined") return;
|
|
1400
|
+
try {
|
|
1401
|
+
window.sessionStorage.setItem(SPEC_OVERLAY_ACCESS_CACHE_KEY, JSON.stringify(access));
|
|
1402
|
+
} catch {
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
var locationChangeListeners = /* @__PURE__ */ new Set();
|
|
1406
|
+
var historyPatched = false;
|
|
1407
|
+
function ensureHistoryPatched() {
|
|
1408
|
+
if (historyPatched || typeof window === "undefined") return;
|
|
1409
|
+
historyPatched = true;
|
|
1410
|
+
const notify = () => locationChangeListeners.forEach((fn) => fn());
|
|
1411
|
+
const originalPushState = window.history.pushState.bind(window.history);
|
|
1412
|
+
const originalReplaceState = window.history.replaceState.bind(window.history);
|
|
1413
|
+
window.history.pushState = (...args) => {
|
|
1414
|
+
originalPushState(...args);
|
|
1415
|
+
notify();
|
|
1416
|
+
};
|
|
1417
|
+
window.history.replaceState = (...args) => {
|
|
1418
|
+
originalReplaceState(...args);
|
|
1419
|
+
notify();
|
|
1420
|
+
};
|
|
1421
|
+
window.addEventListener("popstate", notify);
|
|
1422
|
+
}
|
|
1423
|
+
function useLocationHref() {
|
|
1424
|
+
const [href, setHref] = useState(
|
|
1425
|
+
() => typeof window === "undefined" ? "" : window.location.href
|
|
1426
|
+
);
|
|
1427
|
+
useEffect(() => {
|
|
1428
|
+
ensureHistoryPatched();
|
|
1429
|
+
const onChange = () => setHref(window.location.href);
|
|
1430
|
+
locationChangeListeners.add(onChange);
|
|
1431
|
+
return () => {
|
|
1432
|
+
locationChangeListeners.delete(onChange);
|
|
1433
|
+
};
|
|
1434
|
+
}, []);
|
|
1435
|
+
return href;
|
|
1436
|
+
}
|
|
1437
|
+
function specOverlayFetchJson(path) {
|
|
1438
|
+
return fetch(`${SPECS_ORIGIN}${path}`, { credentials: "include", signal: AbortSignal.timeout(1e4) }).then((res) => {
|
|
1439
|
+
if (!res.ok) throw new Error(`spec overlay ${path}: ${res.status}`);
|
|
1440
|
+
return res.json();
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
function isAutomatedCaptureSession() {
|
|
1444
|
+
return typeof navigator !== "undefined" && navigator.webdriver === true;
|
|
1445
|
+
}
|
|
1446
|
+
function currentChromeTheme() {
|
|
1447
|
+
const root = document.documentElement;
|
|
1448
|
+
if (root.classList.contains("dark") || root.getAttribute("data-theme") === "dark") return "dark";
|
|
1449
|
+
if (root.classList.contains("light") || root.getAttribute("data-theme") === "light") return "light";
|
|
1450
|
+
return "light";
|
|
1451
|
+
}
|
|
1452
|
+
var SPEC_OVERLAY_ROOT_ATTR = "data-spec-overlay-root";
|
|
1453
|
+
async function captureMyView(status, orgId, orgName) {
|
|
1454
|
+
const { domToCanvas } = await import('modern-screenshot');
|
|
1455
|
+
const w = window.innerWidth;
|
|
1456
|
+
const h = window.innerHeight;
|
|
1457
|
+
const theme = currentChromeTheme();
|
|
1458
|
+
const bg = getComputedStyle(document.body).backgroundColor || (theme === "dark" ? "#0b0b0c" : "#ffffff");
|
|
1459
|
+
const full = await domToCanvas(document.body, {
|
|
1460
|
+
width: w,
|
|
1461
|
+
height: h,
|
|
1462
|
+
scale: 1,
|
|
1463
|
+
backgroundColor: bg,
|
|
1464
|
+
// Cross-origin images that refuse CORS render blank instead of failing.
|
|
1465
|
+
fetch: { requestInit: { mode: "cors", credentials: "omit" }, placeholderImage: "" },
|
|
1466
|
+
// Drop the Spec drawer and everything fixed (sidebar, toasts, popovers):
|
|
1467
|
+
// the design is the page body only, so chrome would only add noise.
|
|
1468
|
+
filter: (node) => !(node instanceof Element) || !node.hasAttribute(SPEC_OVERLAY_ROOT_ATTR) && getComputedStyle(node).position !== "fixed",
|
|
1469
|
+
style: { transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)` }
|
|
1470
|
+
});
|
|
1471
|
+
const el = (status.live_selector ? document.querySelector(status.live_selector) : null) ?? document.querySelector("[data-shell-content]");
|
|
1472
|
+
let region = { x: 0, y: 0, width: w, height: h };
|
|
1473
|
+
if (el) {
|
|
1474
|
+
const r = el.getBoundingClientRect();
|
|
1475
|
+
const x = Math.max(0, Math.round(r.left));
|
|
1476
|
+
const y = Math.max(0, Math.round(r.top));
|
|
1477
|
+
region = { x, y, width: Math.min(w - x, Math.round(r.width)), height: Math.min(h - y, Math.round(r.height)) };
|
|
1478
|
+
}
|
|
1479
|
+
const out = document.createElement("canvas");
|
|
1480
|
+
out.width = region.width;
|
|
1481
|
+
out.height = region.height;
|
|
1482
|
+
const ctx = out.getContext("2d");
|
|
1483
|
+
if (!ctx) throw new Error("page raster failed");
|
|
1484
|
+
ctx.drawImage(full, region.x, region.y, region.width, region.height, 0, 0, region.width, region.height);
|
|
1485
|
+
const blob = await new Promise((resolve) => out.toBlob(resolve, "image/png"));
|
|
1486
|
+
if (!blob) throw new Error("page raster failed");
|
|
1487
|
+
const form = new FormData();
|
|
1488
|
+
form.append("image", blob, "live.png");
|
|
1489
|
+
form.append(
|
|
1490
|
+
"meta",
|
|
1491
|
+
JSON.stringify({ url: window.location.href, theme, org_id: orgId ?? null, org_name: orgName ?? null, crop: null })
|
|
1492
|
+
);
|
|
1493
|
+
const res = await fetch(status.capture_url ?? `${SPECS_ORIGIN}/api/overlay/capture`, {
|
|
1494
|
+
method: "POST",
|
|
1495
|
+
credentials: "include",
|
|
1496
|
+
headers: { "X-Spec-Capture": "1" },
|
|
1497
|
+
body: form,
|
|
1498
|
+
signal: AbortSignal.timeout(1e4)
|
|
1499
|
+
});
|
|
1500
|
+
if (!res.ok) throw new Error(`capture ${res.status}: ${(await res.text()).slice(0, 160)}`);
|
|
1501
|
+
}
|
|
1502
|
+
function SpecCaptureLoading() {
|
|
1503
|
+
return /* @__PURE__ */ jsxs("div", { "data-spec-capture-loading": "", role: "status", "aria-live": "polite", className: "flex-1 overflow-hidden px-4 py-4", children: [
|
|
1504
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-center gap-3 text-sm text-[color:var(--foreground)]", children: [
|
|
1505
|
+
/* @__PURE__ */ jsx(
|
|
1506
|
+
"span",
|
|
1507
|
+
{
|
|
1508
|
+
"aria-hidden": "true",
|
|
1509
|
+
className: "inline-block h-5 w-5 animate-spin rounded-full border-2 border-[color:var(--line)] border-t-[color:var(--color-accent)] motion-reduce:animate-none"
|
|
1510
|
+
}
|
|
1511
|
+
),
|
|
1512
|
+
"Capturing your view and comparing\u2026"
|
|
1513
|
+
] }),
|
|
1514
|
+
["Live \u2014 your view", "Diff"].map((label) => /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
|
|
1515
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1 text-xs font-semibold uppercase tracking-wide text-[color:var(--muted)]", children: label }),
|
|
1516
|
+
/* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "h-40 w-full animate-pulse rounded border border-[color:var(--line)] bg-[color:var(--panel)] motion-reduce:animate-none" })
|
|
1517
|
+
] }, label))
|
|
1518
|
+
] });
|
|
1519
|
+
}
|
|
1520
|
+
function specOverlayStatusPath() {
|
|
1521
|
+
return `/api/overlay/status?url=${encodeURIComponent(window.location.href)}&theme=${currentChromeTheme()}`;
|
|
1522
|
+
}
|
|
1523
|
+
function SpecOverlayAction({ orgId, orgName } = {}) {
|
|
1524
|
+
const [access, setAccess] = useState(() => readSpecOverlayAccessCache());
|
|
1525
|
+
const [status, setStatus] = useState(null);
|
|
1526
|
+
const [open, setOpen] = useState(false);
|
|
1527
|
+
const [capture, setCapture] = useState({ state: "idle" });
|
|
1528
|
+
const [frameKey, setFrameKey] = useState(0);
|
|
1529
|
+
const [copied, setCopied] = useState("idle");
|
|
1530
|
+
const href = useLocationHref();
|
|
1531
|
+
const automated = isAutomatedCaptureSession();
|
|
1532
|
+
useEffect(() => {
|
|
1533
|
+
if (access || automated) return;
|
|
1534
|
+
let cancelled = false;
|
|
1535
|
+
specOverlayFetchJson("/api/overlay/access").then((data) => {
|
|
1536
|
+
if (cancelled) return;
|
|
1537
|
+
const next = data && typeof data.allowed === "boolean" ? data : { allowed: false };
|
|
1538
|
+
setAccess(next);
|
|
1539
|
+
writeSpecOverlayAccessCache(next);
|
|
1540
|
+
}).catch(() => {
|
|
1541
|
+
if (cancelled) return;
|
|
1542
|
+
const next = { allowed: false };
|
|
1543
|
+
setAccess(next);
|
|
1544
|
+
writeSpecOverlayAccessCache(next);
|
|
1545
|
+
});
|
|
1546
|
+
return () => {
|
|
1547
|
+
cancelled = true;
|
|
1548
|
+
};
|
|
1549
|
+
}, [access]);
|
|
1550
|
+
useEffect(() => {
|
|
1551
|
+
if (automated || !access?.allowed || typeof window === "undefined") return;
|
|
1552
|
+
const timer = window.setTimeout(() => {
|
|
1553
|
+
specOverlayFetchJson(specOverlayStatusPath()).then((data) => setStatus(data)).catch(() => setStatus(null));
|
|
1554
|
+
}, 300);
|
|
1555
|
+
return () => window.clearTimeout(timer);
|
|
1556
|
+
}, [access?.allowed, href, orgId]);
|
|
1557
|
+
const captureRun = useRef(0);
|
|
1558
|
+
const accessRef = useRef(access);
|
|
1559
|
+
accessRef.current = access;
|
|
1560
|
+
const runCapture = async (s) => {
|
|
1561
|
+
const run = ++captureRun.current;
|
|
1562
|
+
const before = s.live_source === "viewer" ? s.finished_at : null;
|
|
1563
|
+
setCapture({ state: "busy" });
|
|
1564
|
+
try {
|
|
1565
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1566
|
+
await captureMyView(s, orgId, orgName);
|
|
1567
|
+
for (let i = 0; i < 45; i++) {
|
|
1568
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
1569
|
+
if (run !== captureRun.current) return;
|
|
1570
|
+
if (!accessRef.current?.allowed) {
|
|
1571
|
+
setCapture({ state: "idle" });
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const next = await specOverlayFetchJson(specOverlayStatusPath()).catch(() => null);
|
|
1575
|
+
if (next && next.matched && next.live_source === "viewer" && next.finished_at !== before) {
|
|
1576
|
+
setStatus(next);
|
|
1577
|
+
setFrameKey((k) => k + 1);
|
|
1578
|
+
setCapture({ state: "idle" });
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
setCapture({ state: "error", message: "Compare did not finish in 2 min." });
|
|
1583
|
+
} catch (e) {
|
|
1584
|
+
if (run === captureRun.current) setCapture({ state: "error", message: e instanceof Error ? e.message : String(e) });
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
useEffect(() => {
|
|
1588
|
+
if (automated || !open || !status || !status.matched) return;
|
|
1589
|
+
void runCapture(status);
|
|
1590
|
+
return () => {
|
|
1591
|
+
captureRun.current++;
|
|
1592
|
+
};
|
|
1593
|
+
}, [open, status && status.matched ? status.target_id : null]);
|
|
1594
|
+
if (automated || !access?.allowed) return null;
|
|
1595
|
+
const hasScore = status && status.matched && typeof status.mismatch_pct === "number";
|
|
1596
|
+
const badgeLabel = hasScore ? `${Math.round(status.mismatch_pct)}%` : status ? "\u2014" : void 0;
|
|
1597
|
+
const badgeTone = hasScore ? status && status.matched && status.passed ? "default" : "danger" : "default";
|
|
1598
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1599
|
+
/* @__PURE__ */ jsxs(
|
|
1600
|
+
"button",
|
|
1601
|
+
{
|
|
1602
|
+
type: "button",
|
|
1603
|
+
"aria-label": "Spec",
|
|
1604
|
+
"aria-expanded": open,
|
|
1605
|
+
className: topBarIconButtonClass(badgeTone),
|
|
1606
|
+
onClick: () => setOpen(true),
|
|
1607
|
+
children: [
|
|
1608
|
+
/* @__PURE__ */ jsx(LayoutGlyph, { size: TOPBAR_GLYPH_SIZE, className: "text-current" }),
|
|
1609
|
+
badgeLabel ? /* @__PURE__ */ jsx(
|
|
1610
|
+
"span",
|
|
1611
|
+
{
|
|
1612
|
+
className: [
|
|
1613
|
+
"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-[#FFFFFF]",
|
|
1614
|
+
hasScore ? status && status.matched && status.passed ? "bg-[color:var(--chrome-success,#2f855a)]" : "bg-[color:var(--chrome-danger)]" : "bg-[color:var(--muted)]"
|
|
1615
|
+
].join(" "),
|
|
1616
|
+
children: badgeLabel
|
|
1617
|
+
}
|
|
1618
|
+
) : null,
|
|
1619
|
+
!open ? /* @__PURE__ */ jsx(
|
|
1620
|
+
TopBarTooltip,
|
|
1621
|
+
{
|
|
1622
|
+
title: "Spec",
|
|
1623
|
+
description: "Spec design + visual regression for this page."
|
|
1624
|
+
}
|
|
1625
|
+
) : null
|
|
1626
|
+
]
|
|
1627
|
+
}
|
|
1628
|
+
),
|
|
1629
|
+
open && typeof document !== "undefined" ? createPortal(
|
|
1630
|
+
/* @__PURE__ */ jsx(
|
|
1631
|
+
"div",
|
|
1632
|
+
{
|
|
1633
|
+
className: "fixed inset-0 z-50 flex justify-end bg-black/40",
|
|
1634
|
+
...{ [SPEC_OVERLAY_ROOT_ATTR]: "" },
|
|
1635
|
+
role: "dialog",
|
|
1636
|
+
"aria-modal": "true",
|
|
1637
|
+
"aria-label": "Spec overlay",
|
|
1638
|
+
onMouseDown: (e) => {
|
|
1639
|
+
if (e.target === e.currentTarget) setOpen(false);
|
|
1640
|
+
},
|
|
1641
|
+
onKeyDown: (e) => {
|
|
1642
|
+
if (e.key === "Escape") setOpen(false);
|
|
1643
|
+
},
|
|
1644
|
+
children: /* @__PURE__ */ 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: [
|
|
1645
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-[color:var(--line)] px-4 py-3", children: [
|
|
1646
|
+
/* @__PURE__ */ jsxs("h2", { className: "truncate text-sm font-bold tracking-[-0.01em] text-[color:var(--foreground)]", children: [
|
|
1647
|
+
"Spec",
|
|
1648
|
+
status && status.matched ? `: ${status.spec_title}` : ""
|
|
1649
|
+
] }),
|
|
1650
|
+
/* @__PURE__ */ jsx(
|
|
1651
|
+
"button",
|
|
1652
|
+
{
|
|
1653
|
+
type: "button",
|
|
1654
|
+
"aria-label": "Close",
|
|
1655
|
+
onClick: () => setOpen(false),
|
|
1656
|
+
className: "rounded-full p-1 text-[color:var(--muted)] hover:bg-[color:var(--panel)] hover:text-[color:var(--foreground)]",
|
|
1657
|
+
children: /* @__PURE__ */ jsx(CloseGlyph, { size: 16 })
|
|
1658
|
+
}
|
|
1659
|
+
)
|
|
1660
|
+
] }),
|
|
1661
|
+
status && status.matched ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1662
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 border-b border-[color:var(--line)] px-4 py-2 text-xs font-semibold", children: [
|
|
1663
|
+
/* @__PURE__ */ jsx(
|
|
1664
|
+
"a",
|
|
1665
|
+
{
|
|
1666
|
+
href: status.spec_url,
|
|
1667
|
+
target: "_blank",
|
|
1668
|
+
rel: "noreferrer",
|
|
1669
|
+
className: "text-[color:var(--color-accent-fg,var(--color-accent))] hover:underline",
|
|
1670
|
+
children: "Open spec \u2197"
|
|
1671
|
+
}
|
|
1672
|
+
),
|
|
1673
|
+
/* @__PURE__ */ jsx(
|
|
1674
|
+
"a",
|
|
1675
|
+
{
|
|
1676
|
+
href: status.compare_url,
|
|
1677
|
+
target: "_blank",
|
|
1678
|
+
rel: "noreferrer",
|
|
1679
|
+
className: "text-[color:var(--color-accent-fg,var(--color-accent))] hover:underline",
|
|
1680
|
+
children: "Compare tab \u2197"
|
|
1681
|
+
}
|
|
1682
|
+
),
|
|
1683
|
+
/* @__PURE__ */ jsx(
|
|
1684
|
+
"button",
|
|
1685
|
+
{
|
|
1686
|
+
type: "button",
|
|
1687
|
+
disabled: capture.state === "busy",
|
|
1688
|
+
onClick: () => void runCapture(status),
|
|
1689
|
+
className: "ml-auto rounded-md border border-[color:var(--line)] px-2 py-1 text-[color:var(--foreground)] hover:bg-[color:var(--panel)] disabled:opacity-60",
|
|
1690
|
+
title: "Your view is captured when the drawer opens. Recapture after changing the page.",
|
|
1691
|
+
children: capture.state === "busy" ? "Capturing\u2026" : "Recapture"
|
|
1692
|
+
}
|
|
1693
|
+
),
|
|
1694
|
+
/* @__PURE__ */ jsx(
|
|
1695
|
+
"button",
|
|
1696
|
+
{
|
|
1697
|
+
type: "button",
|
|
1698
|
+
"data-spec-copy-prompt": "",
|
|
1699
|
+
disabled: capture.state === "busy" || !status.prompt,
|
|
1700
|
+
onClick: () => {
|
|
1701
|
+
const text = status.prompt;
|
|
1702
|
+
if (!text) return;
|
|
1703
|
+
navigator.clipboard.writeText(text).then(() => setCopied("copied")).catch(() => setCopied("failed")).finally(() => window.setTimeout(() => setCopied("idle"), 2e3));
|
|
1704
|
+
},
|
|
1705
|
+
className: "rounded-md border border-[color:var(--line)] px-2 py-1 text-[color:var(--foreground)] hover:bg-[color:var(--panel)] disabled:opacity-60",
|
|
1706
|
+
title: "Copy a repair prompt for an AI agent: spec, score, image links, top mismatched regions and instructions.",
|
|
1707
|
+
children: copied === "copied" ? "Copied \u2713" : copied === "failed" ? "Copy failed" : "Copy prompt"
|
|
1708
|
+
}
|
|
1709
|
+
)
|
|
1710
|
+
] }),
|
|
1711
|
+
status.live_size && status.design_size && capture.state !== "busy" ? /* @__PURE__ */ jsxs(
|
|
1712
|
+
"p",
|
|
1713
|
+
{
|
|
1714
|
+
"data-spec-sizes": "",
|
|
1715
|
+
className: [
|
|
1716
|
+
"border-b border-[color:var(--line)] px-4 py-1.5 text-xs",
|
|
1717
|
+
status.live_size.width === status.design_size.width && status.live_size.height === status.design_size.height ? "text-[color:var(--muted)]" : "font-semibold text-[color:var(--chrome-danger)]"
|
|
1718
|
+
].join(" "),
|
|
1719
|
+
children: [
|
|
1720
|
+
"Live ",
|
|
1721
|
+
status.live_size.width,
|
|
1722
|
+
"\xD7",
|
|
1723
|
+
status.live_size.height,
|
|
1724
|
+
" \xB7 Design ",
|
|
1725
|
+
status.design_size.width,
|
|
1726
|
+
"\xD7",
|
|
1727
|
+
status.design_size.height,
|
|
1728
|
+
status.live_size.width === status.design_size.width && status.live_size.height === status.design_size.height ? " \xB7 sizes match" : " \xB7 SIZE MISMATCH"
|
|
1729
|
+
]
|
|
1730
|
+
}
|
|
1731
|
+
) : null,
|
|
1732
|
+
capture.state === "error" ? /* @__PURE__ */ jsx("p", { className: "border-b border-[color:var(--line)] px-4 py-2 text-xs text-[color:var(--chrome-danger)]", children: capture.message }) : null,
|
|
1733
|
+
capture.state === "busy" ? /* @__PURE__ */ jsx(SpecCaptureLoading, {}) : /* @__PURE__ */ jsx(
|
|
1734
|
+
"iframe",
|
|
1735
|
+
{
|
|
1736
|
+
src: status.overlay_url,
|
|
1737
|
+
title: "Spec overlay",
|
|
1738
|
+
className: "h-full w-full flex-1 border-0"
|
|
1739
|
+
},
|
|
1740
|
+
frameKey
|
|
1741
|
+
)
|
|
1742
|
+
] }) : status && !status.matched ? /* @__PURE__ */ jsxs("div", { className: "p-4 text-sm text-[color:var(--muted)]", children: [
|
|
1743
|
+
/* @__PURE__ */ jsx("p", { children: "No compare target covers this page." }),
|
|
1744
|
+
/* @__PURE__ */ jsx(
|
|
1745
|
+
"a",
|
|
1746
|
+
{
|
|
1747
|
+
href: status.search_url,
|
|
1748
|
+
target: "_blank",
|
|
1749
|
+
rel: "noreferrer",
|
|
1750
|
+
className: "mt-2 inline-block font-semibold text-[color:var(--color-accent-fg,var(--color-accent))] hover:underline",
|
|
1751
|
+
children: "Search specs \u2197"
|
|
1752
|
+
}
|
|
1753
|
+
)
|
|
1754
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: /* @__PURE__ */ jsx(ValueSkeleton, { className: "h-4 w-full" }) })
|
|
1755
|
+
] })
|
|
1756
|
+
}
|
|
1757
|
+
),
|
|
1758
|
+
document.body
|
|
1759
|
+
) : null
|
|
1760
|
+
] });
|
|
1761
|
+
}
|
|
1762
|
+
function CloseGlyph({ size = 16, className }) {
|
|
1763
|
+
return /* @__PURE__ */ 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: [
|
|
1764
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
1765
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
1766
|
+
] });
|
|
1767
|
+
}
|
|
1768
|
+
var NAV_DISPLAY_OPTIONS = [
|
|
1769
|
+
{ value: "menu", label: "Sub Menu" },
|
|
1770
|
+
{ value: "tabs", label: "Tabs Only" },
|
|
1771
|
+
{ value: "both", label: "Both" }
|
|
1772
|
+
];
|
|
1773
|
+
function NavDisplayAction({
|
|
1774
|
+
value,
|
|
1775
|
+
onChange
|
|
1776
|
+
}) {
|
|
1777
|
+
return /* @__PURE__ */ jsx(
|
|
1778
|
+
TopBarPopover,
|
|
1779
|
+
{
|
|
1780
|
+
label: "Navigation Display",
|
|
1781
|
+
description: "Show navigation as a sub-menu, tabs, or both.",
|
|
1782
|
+
fallbackIcon: LayoutGlyph,
|
|
1783
|
+
panelClassName: "w-44",
|
|
1784
|
+
children: /* @__PURE__ */ jsx("div", { className: "py-1", role: "radiogroup", "aria-label": "Navigation display", children: NAV_DISPLAY_OPTIONS.map((opt) => /* @__PURE__ */ jsxs(
|
|
1785
|
+
"button",
|
|
1786
|
+
{
|
|
1787
|
+
type: "button",
|
|
1788
|
+
role: "radio",
|
|
1789
|
+
"aria-checked": value === opt.value,
|
|
1790
|
+
onClick: () => onChange(opt.value),
|
|
1791
|
+
className: [
|
|
1792
|
+
"flex w-full items-center justify-between px-4 py-2 text-left text-sm font-medium",
|
|
1793
|
+
value === opt.value ? "text-[color:var(--color-accent-fg,var(--color-accent))]" : "text-[color:var(--foreground)] hover:bg-[color:var(--panel)]"
|
|
1794
|
+
].join(" "),
|
|
1795
|
+
children: [
|
|
1796
|
+
opt.label,
|
|
1797
|
+
value === opt.value ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u2713" }) : null
|
|
1798
|
+
]
|
|
1799
|
+
},
|
|
1800
|
+
opt.value
|
|
1801
|
+
)) })
|
|
1802
|
+
}
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
function useThemePreference() {
|
|
1806
|
+
const ctx = useContext(NavChromeContext);
|
|
1807
|
+
const fetchJson = ctx?.fetchJson ?? null;
|
|
1808
|
+
const [value, setValueState] = useState("system");
|
|
1809
|
+
const reconciled = useRef(false);
|
|
1810
|
+
useEffect(() => {
|
|
1811
|
+
const cookie = readThemeCookie();
|
|
1812
|
+
let ls = null;
|
|
1813
|
+
try {
|
|
1814
|
+
ls = localStorage.getItem(THEME_STORAGE_KEY);
|
|
1815
|
+
} catch {
|
|
1816
|
+
}
|
|
1817
|
+
const pref = cookie ?? (isThemePreference(ls) ? ls : null) ?? "system";
|
|
1818
|
+
setValueState(pref);
|
|
1819
|
+
applyThemePreference(pref);
|
|
1820
|
+
}, []);
|
|
1821
|
+
const meCtx = useContext(NavChromeContext);
|
|
1822
|
+
useEffect(() => {
|
|
1823
|
+
if (reconciled.current) return;
|
|
1824
|
+
const pending = readPendingThemeChoice();
|
|
1825
|
+
if (pending) {
|
|
1826
|
+
if (meCtx?.me) reconciled.current = true;
|
|
1827
|
+
setValueState(pending);
|
|
1828
|
+
applyThemePreference(pending);
|
|
1829
|
+
writeThemeCookie(pending);
|
|
1830
|
+
try {
|
|
1831
|
+
localStorage.setItem(THEME_STORAGE_KEY, pending);
|
|
1832
|
+
} catch {
|
|
1833
|
+
}
|
|
1834
|
+
if (!meCtx?.me) return;
|
|
1835
|
+
fetchJson?.("me/preferences", {
|
|
1836
|
+
method: "PATCH",
|
|
1837
|
+
headers: { "Content-Type": "application/json" },
|
|
1838
|
+
body: JSON.stringify({ themePreference: pending })
|
|
1839
|
+
}).then(() => clearPendingThemeChoice()).catch(() => {
|
|
1840
|
+
});
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
const pref = meCtx?.me?.themePreference;
|
|
1844
|
+
if (isThemePreference(pref)) {
|
|
1845
|
+
reconciled.current = true;
|
|
1846
|
+
setValueState(pref);
|
|
1847
|
+
applyThemePreference(pref);
|
|
1848
|
+
writeThemeCookie(pref);
|
|
1849
|
+
try {
|
|
1850
|
+
localStorage.setItem(THEME_STORAGE_KEY, pref);
|
|
1851
|
+
} catch {
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}, [meCtx?.me]);
|
|
1855
|
+
useEffect(() => {
|
|
1856
|
+
const onStorage = (e) => {
|
|
1857
|
+
if (e.key === THEME_STORAGE_KEY && isThemePreference(e.newValue)) {
|
|
1858
|
+
setValueState(e.newValue);
|
|
1859
|
+
applyThemePreference(e.newValue);
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
const onWake = () => {
|
|
1863
|
+
const cookie = readThemeCookie();
|
|
1864
|
+
if (cookie) {
|
|
1865
|
+
setValueState((prev) => {
|
|
1866
|
+
if (cookie !== prev) applyThemePreference(cookie);
|
|
1867
|
+
return cookie;
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
window.addEventListener("storage", onStorage);
|
|
1872
|
+
window.addEventListener("focus", onWake);
|
|
1873
|
+
document.addEventListener("visibilitychange", onWake);
|
|
1874
|
+
return () => {
|
|
1875
|
+
window.removeEventListener("storage", onStorage);
|
|
1876
|
+
window.removeEventListener("focus", onWake);
|
|
1877
|
+
document.removeEventListener("visibilitychange", onWake);
|
|
1878
|
+
};
|
|
1879
|
+
}, []);
|
|
1880
|
+
const setValue = useCallback(
|
|
1881
|
+
(next) => {
|
|
1882
|
+
setValueState(next);
|
|
1883
|
+
applyThemePreference(next);
|
|
1884
|
+
writeThemeCookie(next);
|
|
1885
|
+
try {
|
|
1886
|
+
localStorage.setItem(THEME_STORAGE_KEY, next);
|
|
1887
|
+
} catch {
|
|
1888
|
+
}
|
|
1889
|
+
markThemeChoicePending(next);
|
|
1890
|
+
fetchJson?.("me/preferences", {
|
|
1891
|
+
method: "PATCH",
|
|
1892
|
+
headers: { "Content-Type": "application/json" },
|
|
1893
|
+
body: JSON.stringify({ themePreference: next })
|
|
1894
|
+
}).then(() => clearPendingThemeChoice()).catch(() => {
|
|
1895
|
+
});
|
|
1896
|
+
},
|
|
1897
|
+
[fetchJson]
|
|
1898
|
+
);
|
|
1899
|
+
return { value, setValue };
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// src/speculation/emitRules.ts
|
|
1903
|
+
function buildSpeculationRules(currentApp, registry) {
|
|
1904
|
+
const others = registry.filter(
|
|
1905
|
+
(entry) => entry.id !== currentApp && entry.visible !== false
|
|
1906
|
+
);
|
|
1907
|
+
if (others.length === 0) return null;
|
|
1908
|
+
const payload = {
|
|
1909
|
+
prerender: [
|
|
1910
|
+
{
|
|
1911
|
+
urls: others.map((entry) => entry.path),
|
|
1912
|
+
eagerness: "moderate"
|
|
1913
|
+
}
|
|
1914
|
+
]
|
|
1915
|
+
};
|
|
1916
|
+
return JSON.stringify(payload);
|
|
1917
|
+
}
|
|
1918
|
+
var DESTRUCTIVE_LABEL_PATTERNS = ["sign out", "signout", "log out", "logout", "delete"];
|
|
1919
|
+
var DESTRUCTIVE_PATH_PATTERNS = ["/signout", "/sign-out", "/logout", "/log-out"];
|
|
1920
|
+
function isDestructive(item) {
|
|
1921
|
+
const label = item.label.toLowerCase();
|
|
1922
|
+
if (DESTRUCTIVE_LABEL_PATTERNS.some((p) => label.includes(p))) return true;
|
|
1923
|
+
const href = (item.href ?? "").toLowerCase();
|
|
1924
|
+
return DESTRUCTIVE_PATH_PATTERNS.some((p) => href.includes(p));
|
|
1925
|
+
}
|
|
1926
|
+
function collectSiblingAppUrls(items, currentOrigin) {
|
|
1927
|
+
const urls = /* @__PURE__ */ new Set();
|
|
1928
|
+
const walk = (list) => {
|
|
1929
|
+
for (const item of list) {
|
|
1930
|
+
if (item.kind === "section") continue;
|
|
1931
|
+
if (item.href && !isDestructive(item)) {
|
|
1932
|
+
try {
|
|
1933
|
+
const url = new URL(item.href, currentOrigin);
|
|
1934
|
+
if (url.origin !== currentOrigin && /^https?:$/.test(url.protocol)) {
|
|
1935
|
+
urls.add(url.href);
|
|
1936
|
+
}
|
|
1937
|
+
} catch {
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
if (item.children?.length) walk(item.children);
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
walk(items);
|
|
1944
|
+
return Array.from(urls);
|
|
1945
|
+
}
|
|
1946
|
+
function buildNavSpeculationRules(items, currentOrigin = typeof window !== "undefined" ? window.location.origin : "") {
|
|
1947
|
+
const urls = collectSiblingAppUrls(items, currentOrigin);
|
|
1948
|
+
if (urls.length === 0) return null;
|
|
1949
|
+
return {
|
|
1950
|
+
prerender: [{ source: "list", urls, eagerness: "moderate" }]
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
var SCRIPT_ID = "politogy-speculation-rules";
|
|
1954
|
+
var PREFETCH_ATTR = "data-politogy-speculation-prefetch";
|
|
1955
|
+
function SpeculationRulesScript({ items }) {
|
|
1956
|
+
const origin = typeof window !== "undefined" ? window.location.origin : "";
|
|
1957
|
+
const rules = useMemo(() => buildNavSpeculationRules(items, origin), [items, origin]);
|
|
1958
|
+
const json = rules ? JSON.stringify(rules) : null;
|
|
1959
|
+
useEffect(() => {
|
|
1960
|
+
if (!json || typeof document === "undefined") return;
|
|
1961
|
+
let el = document.getElementById(SCRIPT_ID);
|
|
1962
|
+
if (!el) {
|
|
1963
|
+
el = document.createElement("script");
|
|
1964
|
+
el.id = SCRIPT_ID;
|
|
1965
|
+
el.type = "speculationrules";
|
|
1966
|
+
document.head.appendChild(el);
|
|
1967
|
+
}
|
|
1968
|
+
el.textContent = json;
|
|
1969
|
+
return () => {
|
|
1970
|
+
};
|
|
1971
|
+
}, [json]);
|
|
1972
|
+
useEffect(() => {
|
|
1973
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
1974
|
+
const supportsSpecRules = typeof HTMLScriptElement !== "undefined" && typeof HTMLScriptElement.supports === "function" && HTMLScriptElement.supports(
|
|
1975
|
+
"speculationrules"
|
|
1976
|
+
);
|
|
1977
|
+
if (supportsSpecRules) return;
|
|
1978
|
+
const urls = collectSiblingAppUrls(items, origin);
|
|
1979
|
+
if (urls.length === 0) return;
|
|
1980
|
+
const schedule = typeof window.requestIdleCallback === "function" ? window.requestIdleCallback : (cb) => setTimeout(cb, 2e3);
|
|
1981
|
+
const added = [];
|
|
1982
|
+
schedule(() => {
|
|
1983
|
+
for (const url of urls) {
|
|
1984
|
+
if (document.querySelector(`link[${PREFETCH_ATTR}][href="${url}"]`)) continue;
|
|
1985
|
+
const link = document.createElement("link");
|
|
1986
|
+
link.rel = "prefetch";
|
|
1987
|
+
link.href = url;
|
|
1988
|
+
link.setAttribute(PREFETCH_ATTR, "");
|
|
1989
|
+
document.head.appendChild(link);
|
|
1990
|
+
added.push(link);
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
return () => {
|
|
1994
|
+
added.forEach((l) => l.remove());
|
|
1995
|
+
};
|
|
1996
|
+
}, [items, origin]);
|
|
1997
|
+
return null;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
export { CORE_APP_ORIGIN, CORE_BILLING_PATH, FormColumn, FuelAction, NAV_DISPLAY_OPTIONS, NavChromeProvider, NavDisplayAction, NavThemeControl, NotificationsAction, SPECS_ORIGIN, SpecOverlayAction, SpeculationRulesScript, SupportAction, TOPBAR_GLYPH_SIZE, TopBarCountBadge, TopBarPanelArrow, TopBarPopover, TopBarTooltip, TopBarWidgetSlot, buildNavSpeculationRules, buildSpeculationRules, collectSiblingAppUrls, createCommsNotificationsAdapter, emptyNotificationsFeed, normalizeNotificationsFeed, resolveBrandGlyph, resolveOrgLogoUrl, topBarIconButtonClass, useChromeMe, useChromeOrgs, useNavChromeDedupedFetch, useNavChromeFetcher, useThemePreference, useTopBarIconSlotGlyph, warnDeprecated };
|
|
2001
|
+
//# sourceMappingURL=chunk-TUK2IEL5.js.map
|
|
2002
|
+
//# sourceMappingURL=chunk-TUK2IEL5.js.map
|