@the-portland-company/shell 0.76.15 → 0.76.18

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